Joachim Schlosser: Development and Verification of fast C/C++ Simulation Models for the Star12 Microcontroller
Usually the models reflect a virtual component, so a portion of a chip. They call Octopus functions and provide callback routines to be called by Octopus; they may never use any functions of other models. This allows the models to be reused in other chips. All the models are tied together using the configuration file, a C source file that includes structures and arrays for the port lists and function pointers for all models used. With the information in the configuration file, the models are registered and instantiated in Octopus in the elaboration stage. Instantiation means that a model can be used multiple times representing the fact that a component is present more than once in a system.
Also available is the so-called “base class”, a system of C++ classes that allows the easy implementation of micro controller peripherals. It supports control registers in addition to bit and complex signals and the bus interface that come along with Octopus. The registers are usually updated as a result to certain state changes, but there is also a possibility to gain “backdoor” access to the registers, enabling a debugger to read values without any effect and to modify them without timeline advance. The term “base class” is referred to hereafter not as a single base class as known from object oriented programming languages but as the system of classes giving easier access to Octopus from the model view.
A basic concept in Octopus is the signal technology. A signal is identified by a name that is unique in the particular hardware system. It is attached to an arbitrary number of component ports, whereas one of them is external. A signal itself has no data type, but it only determines the connection without specifying the data type. This is both advantage and disadvantage in one: On one hand it is difficult to ensure type safety, when reading and writing signals in different components; on the other hand, the signal can carry type information, which gives the power to a signal to carry different, specified, data types at different times, because always the type and the information is transferred. The ports, the signals are attached to, can be configured input, output or bi-directional.
The other basic concept is the bus interface. It allows assigning a memory range to a component. If the bus driver generates a bus access, Octopus can determine to which model instance the event should be sent. So a component has a bus port, which is a special kind of a port using the pre-defined bus signal data structure.
Octopus itself is controlled by an application. The application’s main task after the elaboration session is to stimulate the advance of time by calling the evaluation function with the number of cycles to be simulated. All necessary actions are performed now within this call so when the control is returned, the complete system under Octopus is then in the state that it should be at the given offset.
Due to its nature as a model manager, Octopus has two interfaces to be described in this section: the simulator interface for the application side, and the model interface for the model side.
The simulator interface consists of a set of functions and callback definitions. The easiest way to describe the interface is to go through the three sessions like the table in the basic description of [Roh01b]: elaboration, simulation, and termination.
As first action in the elaboration session, the application prepares a data structure, including function pointers to the callback functions, the application implements. This structure is passed to the motInit function, which registers the callbacks and initializes internal variables. Before returning, it calls the motInitialSetup function that resides in the configuration file of the modeled system and registers the models. Because Octopus now knows the callback addresses of the application, it can invoke the Prepare function of the application. Here the application is to set up the system configuration, regarding the simulator itself. As we will later see in section 4.2, this is also the ideal place for manipulating model registration information. Back in the motInit function Octopus processes potential command line arguments and the system configuration. The debug commands of the models are registered before the Init callback of each model is executed. The last action of motInit is to let the application perform the setup of its interface with the information of the already registered models. The next step for the simulator is to define the clock frequency and then begin to reset the model manager with the motReset function. motReset in turn invokes three application callbacks. The first one, EnterReset, takes place before Octopus resets time and ports. After the second one, Reset, all model initialization callbacks are invoked and followed by the LeaveReset callback. Finally the motReset function informs all models that the reset stage has ended. This system of callbacks calling other callbacks and back and forth may appear chaotic and redundant at first sight, but it ensures that all possibilities are given to perform initialization actions at various times on both sides – application and models.
For the simulation session the most important functions are motEval to tell Octopus to work off a certain amount of cycles, and motGetNextEvent to determine when the next event will take place. With these two functions the application can easily perform its own actions, let Octopus react and then find out when Octopus will do the next step. Of course these two functions alone do not make much sense if there is no data exchange. Ways of data exchange are explained in the practical part. The models may use debug output functions, for each one there is a callback in the application interface, like the Debug or Message callbacks. In order to use model manager commands, the application interface can implement a possibility to accept user interaction and then pass it to Octopus with the motUserCommands function.
If the application wants to terminate the simulation session, it calls the motExit function of Octopus. Now all dump files and stimulus files are closed and the Close callback of the application is invoked, which reverts all actions of the Setup callback in the elaboration session. Then Octopus destroys all internal structures, created by motInit and then raises the Exit callback of each model. The models terminate, and then Octopus executes the application’s Cleanup function, being the opposite of the Prepare routine. Now Octopus can close all message- and log-files and invoke the simulated system’s ExitCode callback. Rarely used, it would close any globally opened files and release globally allocated memory. This is the very last function to be executed, after it the motExit function returns, too.
To the model side, the interface of Octopus is much more refined, due to the various mechanisms that are necessary to let the models interact through Octopus. First all callbacks, a model has to register, will be presented. Later on a selection of functions will be discussed. The exact type and function definitions can be read in the Octopus reference [Roh01a].
There are six callbacks a model has to provide. Function pointers of the callbacks have to be passed to octopus when starting the session.
As already mentioned in the previous section the Init callback performs actions at the beginning of certain stages. Along with the function call always comes the TaskTypeT parameter, which reflects the cause for the call: it could be initialization, power on, or a reset. At the end of these tasks the Exit callback is invoked. This is important because a reset for example could take several cycles, so it may be not possible to perform all steps necessary at once when wanting to preserve full accuracy.
The Update callback is activated when anyone of the input or bi-directional ports has been sensitive for a modification of an attached signal. The call is the pure information, that something has happened, but not what. The function must determine this itself, by calling the appropriate Octopus functions, as there may be more than one updated port. When not used, this function can be omitted.
Very similar to the prior one is the Access callback, being called when a bus access takes place. This is an own function because the mechanism is different: signals are attached especially to a model, whereas the bus passes all models. Octopus differentiates the targets by the memory address in the memory map. It is possible to overlap memory ranges, then all models, associated with a particular address, are accessed. The Access callback may be omitted.
The Backdoor callback allows debug accesses while providing basically the same interface as Update or Access.
A model may schedule events only for itself. Octopus then executes these events by raising the Eval callback. The function carries a 31 bit wide TaskTypeT parameter that can be used to forward information about what actions to be performed. This parameter is the conjunction of all task types that were scheduled for the particular cycle. So it becomes clear that there are not 231 possible actions but only 31. If the model does not schedule any events, this callback can be omitted of course.
Although for each model these callbacks have to be implemented separately, in most cases the implementations will pass the information to the base class, which processes it and allows to receive updates and events in special handler functions registered in the base class at model instantiation time. So the base class e. g. encapsulates the Access callback in a way, that it is possible to define register-based access handler functions, each for read and for write access. The analysis of the information that comes along with the Access callback, has not to be programmed manually, but is done by the base class. In model creation, a handler function for a memory-mapped register is defined, registered and called by the base class if especially that register is accessed via the bus. More details on the base class will follow in the subsequent sections.
Important for the model interface is the signal concept, introduced in one of the previous sections. A signal, in most cases, is a logical connection between two ports of different models, or between a port of a model and the outside world. Input ports receive update events, pre-processed by the base class, via handler callback functions. Output ports are written from model functions using the motPutPort function. Signal information, read or written, consists of a variable of type motSignalValueT and a pointer variable of type motTokenT, generally specifying the type of signal and the address of the carried data. motTokenT technically is simply a void pointer, so without the type information it would be erroneous to use it, as it may e. g. point to a double value, reflecting an analog signal, or an integer, reflecting a data line, as well as to a complex data structure. A special case is the usage of bit lines: For the simple information carried, it would be inefficient to use pointers, so the pointer here always has to be NULL, the actual value is ASCII-coded in the motSignalValueT number. The possible values for bit lines are ‘0’, ‘1’, ‘Z’ for tristate and ‘?’ for the “undriven” state. The full bit signals of course again consist of the ASCII value and the NULL pointer. This concept of signals has the big advantage, that it defines a communication channel without restricting the type of information using that channel.