3.2.2.  Implementation

3.2.2.1 Test Model

As mentioned in section  3.1.1.2 on page 172, it is not possible to use the Barracuda Integration Platform – and so the Star12 CPU model – in another environment than the Hiwave debugger. For enabling the usage in various environments the integration platform will have to be modified. So for testing the adaptation layer between Octopus and Simulink, a small model was created, not reflecting any existent hardware.


PIC

Figure 3.14: Test Model for Adaptation Layer

As illustrated in Figure  3.14 on page 242, the test model, called Delayer, has four input and five output ports. Besides the output-only clock port, there are two analog and two digital channels, which invert input signals on the first and delay input signals on the second channel. There are no registers, no bus or anything else. The Delayer is the only model in the test system, a circumstance, which removes all effects that could distract from the main problems.

3.2.2.2 S-Function Interface

The S-function interface here means the implementation of all the callbacks that are required from the Simulink side.

The first callback to be invoked is mdlInitializeSizes. Here, the number of input and output ports, as well as the number of sample times, is defined. This function also sets the number of parameters that are expected. Parameters are all variables that are entered in the parameter field of the S-function dialog box in Simulink.

The Octopus S-function takes five parameters, each of them enclosed in double quotes when containing more that one word:

Initialization command.
This parameter allows specifying commands that should be processed by Octopus at startup time. This could be telling Octopus to load a binary, in case that a CPU is integrated into the set of models. Besides the Octopus commands, all model manager commands that were registered, can be used.
Output signals.
The names of all Octopus signals that should be monitored and passed to Simulink are given in the second parameter. For each signal there will be an own output port. Up to now, only bit and analog signals are supported, but – theoretically – also complex signals, e. g. complete bus tokens, could be sent to Simulink. Details on signal monitoring will follow in section  3.2.2.4 on page 248.
Input signals.
Equivalent to the output signals, the names of all Octopus signals that should receive input from Simulink are given here. Again, only bit and analog signals are possible up to now.
Frequency.
Octopus is required to run at a specific frequency. The fourth parameter takes this frequency in Megahertz. This value is also important to control the synchronization.
Granularity.
This parameter takes effect on the synchronization, as explained in section 4.2.2.4. It affects the ratio of performance and accuracy.

So mdlInitializeSizes checks, whether the number of parameters matches five, and makes all parameters permanent, which means, they cannot be changed during simulation. This is logical, because it would neither make any sense, nor be possible for Simulink to change the number of input or output ports during simulation; the same applies for granularity.

As the callback is invoked every time the parameters for that block are changed, here the output and input signals parameter is processed by the means that the number of signals is reflected in the model’s block symbol, which therefore always shows the appropriate number of connection points. This ensures that the Octopus model can be integrated in various ways to test different functionalities of the SoC.


PIC

Figure 3.15: Octopus S-function variable port number

The next callback to be executed by Simulink is the mdlInitializeSampleTimes function. It is possible to assign more than one sample time to a block, either by allocating different sample times to different ports, or by assigning several sample times to the complete block. In the concrete case, two sample times are specified.

The final function in the initialization stage is mdlStart. Its main task is to start Octopus. It fills the motCallbackStruct with pointers to the appropriate Octopus callback functions, then calls moInit and so triggers the startup process. After this, the commands from the S-function parameter dialog box are executed.

After initialization there starts the simulation loop. A major simulation time step starts with execution of the mdlGetTimeOfNextVarHit. As mentioned before, the problem for the variable time step technology arises from the fact that the size of the next time step has to be determined before the current step is executed. So this time is determined in another function and only transferred to Simulink in here.

In mdlOutputs the actual Octopus simulation is executed, by determining the amount of cycles that have to be executed. Then with motEval the time for Octopus is advanced to the particular point in time. The other important task to be accomplished in mdlOutputs is to pass the input port signals to Octopus.

The mdlUpdate callback is not used in this implementation, as there are no states that are useful for Simulink.

3.2.2.3 Octopus Simulator Interface

The Octopus simulator interface implementation has to provide all callbacks that are necessary for correct execution, as presented earlier in section  2.4.1.2 on page 143. The list of functions starts with some output functions for different purposes, although doing the same: the callbacks Error, Warning, Debug and Print simply put a message on the Simulink command line window. They are invoked from Octopus for the specific reasons. The AskUser callback of Octopus is defined to allow user interaction by the means that the user can pass model manager commands to Octopus. This is not implemented up to now.

A very important callback for in the elaboration session is the Prepare callback, which is invoked from motInit. It is executed after the information, contained in the models.cpp configuration file, has been read. The same applies to motInitialSetup function, where the arrays of motModelEntryT and motSignalT structures are passed to Octopus. In Prepare lies the most important task to enable the interface to monitor and affect Octopus signals. For this, a special model is dynamically registered in Octopus and then initialized with the signal names, specified as S-function parameters.

The rest of the callbacks, specified in the Octopus simulator interface document, are not used for the particular implementation, although they are defined in the source code file for future usage.

3.2.2.4 Data exchange

It is vital for the adaptation layer to allow data exchange between Simulink and Octopus, otherwise it would not make any sense to include Octopus simulators in a Simulink model. For the Simulink side, there are input and output ports available, as we are inside a block. But at the Octopus side there are only the model manager commands get_signal and put_signal available, to print and set signals. This would result in executing motUserCommands several times during each simulation step, and additionally pre- and post-process the string representations of the signals and the model manager command’s output. A loss in performance would be sure. Another idea was to use the dumper to get signals from Octopus. But besides that, this means that the signal dumper would not be available for its natural purpose, the problem of putting signals to Octopus would not be solved anyway. The more sophisticated way is to use an extra model that takes the signals we want to monitor as input, and those we get from Simulink as output.

The direct approach would be to add a model to the configuration file models.cpp that has the complete set of signals present in the system once as input and once as output ports, or once as bi-directional ports. But on the one hand, this would mean that all ports – which can be several hundreds for big systems – would have to be monitored, with the resulting loss of performance. On the other hand, the user would have to specify all signals by hand, which is a very uncomfortable way. A much better solution is to determine dynamically which signals are present in the system. But then the model cannot be included in the configuration file, because when this file is executed, Octopus does not yet know the signals. And of course, if the signal names are determined dynamically anyway, it is possible to use only the signals specified as S-function parameter.


PIC

Figure 3.16: Control and Data Flow for Adaptation Layer

So the signal monitor is created dynamically, after all the other models have been registered to Octopus. First the Prepare callback invokes a function named CreatePortList, which takes as argument two string arrays: one for the input signals, one for the output signals. All arrays are always NULL-pointer terminated to include array size information. This function – as the name suggests – creates the array of motPortT structures that will be used for the signal monitor model. As Octopus ports have no data type, the monitor will not know what type of signal to create when sending a signal to Octopus. So in the S-function parameter the input signals definition carries not only the name but also the type information: <name>:<type>, e. g. PORTAD:r, which means that the signal PORTAD should carry real number signals. The ports get the same name as the signals they monitor, only with a prefix _MONi for input, and _MONo for output signals. The following code excerpt is a part of that function.

 
1motPortT* CreatePortList(const char** signals_In , 
2const char** signals_Out) 
3// create and return port  l i s t  to be used within a motModelEntryT struct . 
4{ 
5  motUINT16 iCount_In = 0; motUINT16 iCount_Out = 0; 
6  motUINT16 i ;  string sBuffer ; 
7  // check  i f  input signal names available , count how many 
8  if (signals_In != NULL) 
9  {      for ( ; signals_In [iCount_In] != NULL; iCount_In++);      } 
10  // check  i f  output signal names available , count how many 
11  if (signals_Out != NULL) 
12  {      for ( ; signals_Out[iCount_Out] != NULL; iCount_Out++);  } 
13  // create a new port array with the appropriate size 
14  motPortT* ThePortList = new motPortT[iCount_In+iCount_Out+1]; 
15  //  i f  there are input signals , create ports for them 
16  if (signals_In != NULL) 
17  { 
18     for ( i=0; i < iCount_In; i++) 
19     { 
20        //  f i l t e r  out the type information after the colon 
21        sBuffer = string(signals_In [ i ] ) ; 
22        if (sBuffer . find(":") != string : : npos) 
23            sBuffer = sBuffer . erase(sBuffer . find(":") , sBuffer . length ()) ; 
24         // set properties 
25         ThePortList [ i ] .Name = strdup(const_cast<char*>(string(sBuffer + string("_MONi")) .c_str())) ; 
26         ThePortList [ i ] .Type = motPORTTYPE_OUTPUT; 
27         //[ several member assignments l i k e the above one] 
28      } 
29    } 
30    //  i f  there are output signals , create ports for them 
31    // [same loop for output ports , not printed here] 
32    // create the last entry . . . 
33    motPortT LastEntry = motPORT_LAST_ENTRY; 
34    // and copy i t to the end of the array 
35    memcpy(&(ThePortList [iCount_In+iCount_Out] ) , &LastEntry , sizeof(motPortT)) ; 
36    // return the generated port array 
37    return ThePortList ; 
38}
Listing 3.6: Port list creator routine

See lines 21–22 of the code excerpt above, where the name is split off the arguments from the parameter list. The very convenient macros, Octopus offers for port definitions, cannot be used here, because they only work while initializing variables, not while dynamically filling the structures. In lines 24–25 the port structure’s members are filled with data. In lines 30 and 32, a trick is used to avoid having to fill in manually the last port, which has to have special values for its members. The macro motPORT_LAST_ENTRY is such a macro that can only be used for variable initialization, so a variable is initialized and then its contents is copied to the end of the list. It is quite obvious that this could have been done with the other members of the array, too, but that would have caused lots of memory copy actions, which are very critical to performance.

Of course, the model has an initialization callback named ModelInit, where it determines the port handles of its ports by a series of calls to motPortStructure, which returns a pointer to a port for the port name as argument. The actual use is as follows:

 
portHandles [ i ] = motPortStructure(const_cast<char*>(string( 
  string(signalNameList_FromSL[ i ] ) + string("_MONi") ) .c_str())) ;

For the S-function output ports – actually input ports for the signal monitor – there is an Update callback named ModelUpdate. As defined by Octopus, this callback does not receive any arguments, it has to call motGetUpdatesCount to determine how many ports have been updated, and then invoke motGetUpdatePort repeatedly. For each port that was updated, the new value is directly saved to the S-function output port address range, which can be ascertained by calling Simulink’s ssGetOutputPortRealSignal. Also bit signals are converted to the double type, taking 0.0 for a logical 0, 1.0 for logical 1, 0.5 for tristate and 2.5 for undriven signals.

Back in Prepare, the type information of the signal definitions in the S-function parameter field is processed. As the only implemented types are bit and real signals, specifying ‘b’ or ‘r’ is sufficient. Then the motModelEntryT is created as a stand-alone variable:

 
1motModelEntryT SignalMonitorSL_tmp = { 
2           "SignalMonitorSL" , 
3           motMODEL__NO_CONFIG_INFO(motINACTIVE) , 
4           motMODEL__USE_SYSTEM_CLOCK(0 ,0 ,0) , 
5           motMODEL__PORT_LIST(PortList) , 
6           ModelInit , NULL, NULL, NULL, ModelUpdate , NULL, 
7           motMODEL__NO_USER_CMDS 
8};
Listing 3.7: Signal monitor routine

Line 2 specifies the name, in lines 3 and 4 general configuration and timing information is passed, line 5 passes the port list, created with the CreatePortList function, line 6 points to the two existing model callback functions, and for line 7 there are of course no model manager commands to register.

Of course, the signal monitor is of no use if Octopus does not have it in its list of models. Unfortunately, there is no function to register a model dynamically, usually all models are collected in an array that is passed in the configuration file’s motInitialSetup function. So we must hack the model entry into the model table. This is done by first copying the model table into a memory space that is big enough to take the whole model table plus the additional entry for the signal monitor, seen in lines 1 and 2 of the code excerpt below. Then the signal monitor’s motModelEntry is copied to the table in line 3, then the obligatory last entry from the original to the end, as stated in line 4. Now the original table can be deleted, as line 5 shows. Finally, the original model table pointer that Octopus uses, must point to the new table, see line 6. Note that ModelTable is of type motModelEntryT**, an indirect array, because it is argument to the Prepare callback.

 
1motModelEntryT* ModelTableCopy = new motModelEntryT[EntryCount+2]; 
2memcpy(ModelTableCopy , *ModelTable , EntryCount*sizeof(motModelEntryT)) ; 
3memcpy(&(ModelTableCopy[EntryCount] ) , &SignalMonitorSL_tmp , sizeof(motModelEntryT)) ; 
4memcpy(&(ModelTableCopy[EntryCount+1]) , &((*ModelTable) [EntryCount] ) , sizeof(motModelEntryT)) ; 
5delete [ ] *ModelTable; 
6*ModelTable = ModelTableCopy; 
7SignalMonitorSL = &(ModelTableCopy[EntryCount] ) ;
Listing 3.8: Hack into model table

Line 8 assigns the signal monitor entry of the model table to a variable named SignalMonitorSL. This has nothing to do with the actual patching, but is important for delivering the signals to Octopus. When signals are updated from Octopus, these events are handled in the model callback routine. But when getting signals from Simulink, we are not within an Octopus callback. If the motPutPort function – does exactly what the name suggests – is used outside a callback, Octopus crashes, because a port manipulation function always has to occur in a particular model context. So how can we persuade Octopus of being in the context of the signal monitor? The context in Octopus is set with a global pointer named motModel, which points to the model that is active or NULL if not working currently on models. Octopus sets this variable to the correct model before it invokes a callback. So the only thing to do is to set motModel to the SignalMonitorSL pointer we have defined before, then do the port manipulation, then reset motModel to NULL. See the code excerpt of the S-function’s mdlOutput callback routine to see how the signals are sent to Octopus.

 
1. . . 
2InputRealPtrsType uPtrs; 
3motModel = SignalMonitorSL; 
4for (int i=0; i < listSize_FromSL; i++) 
5{ 
6  if (signalTypeList_FromSL[ i ] == signalNUMBER) // real signals 
7  { 
8     // put the signals on the output ports of the signal monitor 
9     uPtrs = ssGetInputPortRealSignalPtrs(S, i ) ; 
10     motPutPort(portHandles [ i ] , 0 , motTOKEN(signalNUMBER, uPtrs [ 0 ] ) ) ; 
11  } 
12  else if (signalTypeList_FromSL[ i ] == 0)         // bit signals 
13  { 
14     uPtrs = ssGetInputPortRealSignalPtrs(S, i ) ; 
15     if (*uPtrs [ 0 ] == 0.0) 
16        motPutPort(portHandles [ i ] , 0 , motTOKEN( 0 , NULL)) ; 
17     else if (*uPtrs [ 0 ] == 1.0) 
18        motPutPort(portHandles [ i ] , 0 , motTOKEN( 1 , NULL)) ; 
19     else if (*uPtrs [ 0 ] == 0.5) 
20        motPutPort(portHandles [ i ] , 0 , motTOKEN( Z , NULL)) ; 
21     else                     // error case or undefined 
22        motPutPort(portHandles [ i ] , 0 , motTOKEN( ? , NULL)) ; 
23  } 
24}         /* for ( each input port) */ 
25motModel = NULL; 
26. . .
Listing 3.9: S-function output routine

This section showed that quite a number of considerations were necessary to allow performant data exchange, but now works completely transparent to both, the Octopus user and the Simulink user.

3.2.2.5 Time synchronization

The most challenging task, without any doubt, was the synchronization of time between Simulink and Octopus. The ideal case would be to register the model to have variable sample time, so that simulation could advance from Octopus event to Octopus event. But that would bring with it the disadvantage that inputs from Simulink would only be recognized when the next event occurs, even if they have changed dramatically during that time. Possibly, Octopus would have to react on input changes immediately, but gets the input at the point in time where it had its next event before the input change. So the variable sample time concept alone would be perfect for a system that has output only, but not for those having inputs, too.

So a second sample time has to be invented to ensure the input ports are updated regularly. In these regular intervals all input ports are read out and their values fed into Octopus. After this, Octopus is evaluated to determine if the potentially changed input values caused a new event. If so, the variable sample time is specified to reflect the new events.

From the variable sample time technique there arises another problem. As mentioned before, the callback routine mdlGetTimeOfNextVarHit, which Simulink calls to determine the next step size, is invoked at the beginning of a simulation time step. At this point, Octopus was not yet evaluated, no inputs have been updated and so no current information about the time of the next event is available. But a big advantage is that the callback, although required to be present, does not deliver the next step size by a return value, but by calling the Simulink function ssSetTNext. Fortunately this routine can be called also outside the callback and Simulink considers its argument for the next simulation time step. So instead of passing the step size at the beginning of the loop, it is transferred at the end.

Basically the scheduling works as follows:

 
time_T now = ssGetT(S) ; 
todo = (now * dClockFrequency * 1000000; 
motEval( todo ) ; 
motGetNextEvent(&cyclesToEval)) ; 
now += ( cyclesToEval / dClockFrequency / 1000000); 
ssSetTNext(S, now ) ;

This is not the real code, but only the basic scheme, without all error and special case handling. In line 1 the current Simulink time is fetched. Line 2 determines the total simulation cycles Octopus has to be advanced to. The actual model evaluation takes place in line 3, followed with the determination of the next pending event in line 4. The Simulink time for the next call is calculated in line 5 and finally passed in line 6.

All these actions are settled in the mdlOutputs callback. The update process can be outlined in four steps:

  1. Bring Octopus to the current Simulink time by evaluating it.
  2. Pass input port values to Octopus.
  3. Determine if passing caused a new event.
  4. Schedule Simulink for execution of the next Octopus event.

The periodic execution of Octopus and the variable time step technique generate another problem: First, the periodic input passing can be redundant, when the particular Octopus system does react on this change of values very slowly. Second, a CPU will have events nearly every cycle, which means that Simulink would have to call Octopus every 40 nanoseconds, a much higher resolution than Simulink has as default. As every evaluation of Octopus causes a series of function calls, performance would decrease dramatically. The problem becomes even worse, considering that not every CPU cycle corresponds to a change in an output signal, assuming we only monitor signals that a micro controller would pass to the outside world in reality. So evaluating Octopus separately each cycle is simply superfluous. Usually, the user who assembles a Simulink model, knows what type of micro controller is simulated with Octopus, and – where applicable – what software is running on it. So he knows at which approximate rate output changes occur.

This brings in the granularity S-function parameter. The granularity value affects the overall simulation speed. Its value is the number of Octopus cycles that Simulink should let pass between subsequent calls to Octopus. A value of 1000, e. g. means that Octopus is evaluated every 1000 cycles, regardless whether there are events during this time or not.

Additionally it is possible to specify a granularity value of zero. As it makes no sense to have zero cycles between subsequent evaluation, of course, a zero is interpreted as the command to assign continuous sample time, fixed in minor time step, to the block. This means that the block is evaluated when the simulation enters a new simulation time step, so the rate is dependent on the simulation parameters, as seen before in Figure  3.11 on page 227.

The following paragraphs show the effects of the different settings. All following figures show a sinus wave as input signal to the del_an0_in port of the Delayer model, the corresponding output, del_an0_out and the module-internal clock signal del_clk_out.

When configuring the granularity to zero, continuous sample time with fixed values in minor time steps is assumed. Figure  3.17 on page 258 shows the scope diagram. It can be seen that the output is stepped, a consequence of the Simulink simulation time step size. As the block was specified to have fixed values during minor time steps, the single points are not interpolated.


pict

Figure 3.17: Continuous sample time

When zooming into the graph, as showed in Figure  3.18 on page 261, it can be clearly seen that the steps are not equidistant, but sometimes have sub-steps. The regular steps are a result from the clock events, whereas the sub-steps are caused by Simulink’s signal updates. This configuration is ideal for systems that produce few or none events, like a direct-feed-through circuit. The system is perfectly and performantly integrated into Simulink, because evaluation does only take place when Simulink simulation wants it.


pict

Figure 3.18: Continuous sample time, zoomed

Besides the possibility to make the simulation steps dependent on Simulink, a fixed step size can be given using the granularity S-function parameter. Figure  3.19 on page 264 shows the graph for a granularity of 500 cycles. Keeping in mind that the Delayer module clock frequency is 10000 cycles, this is a high resolution. 500 cycles at 25 MHz are 20μs, because of this, the output graph is smooth, without any steps.


pict

Figure 3.19: Fixed sample time, granularity 500 cycles

Of course the configurability imposes additional responsibility onto the user. The user must know what the system does and what its timing characteristics are. The effect of selecting a granularity value higher than an important event rate of the system, causes irregularities in the output, presented in Figure  3.20 on page 267. When looking at the clock signal, it can be seen that the rising and falling edges are not equidistant, as they should be. The mistake was to evaluate Octopus every 6500 cycles. The module clock period length is 10000, which means there is a transition every 5000 cycles, and so more often than evaluation occurs.


pict

Figure 3.20: Fixed sample time, granularity 6500 cycles

So for the correct setting, the knowledge about the Nyquist frequency is essential. The Nyquist theorem states that the sampling rate must be greater than two times the highest frequency in the input signal. In our case, the “input” signals are all relevant signal changing events of the Octopus system.

But the granularity parameter so allows a high performant simulation. For a video presentation refer to section  4 on page 287.

The synchronization between Simulink and Octopus took much time for testing and interpreting the different effects. Several considerations about the correct implementation were made, and the solution presented was rated to be the best way.

The Octopus/Simulink adaptation layer sources take about 1,200 lines of code, not counting the test code.