As I explain previously, the first part of the emulation was a tool that translates the source code into a form suitable for a PC host. A new directory structure is created. Then all of the generated code, templates, et cetera are deposited into this directory structure. The structure includes:
Then I create a Visual Studio project from this "existing" source base. Now that I have expressed translating the source code, I will roughly describe the Framework for running the code, and filling in the missing elements.
This is an emulation of a smaller system on a broader, often more generic one. This means that the functionality used by the software is remapped to what is provided by the host. I will discuss the process of remapping the functionality.
First, we need to supply a table of globals that the system uses. This includes the named processor registers, and the C run time globals. A file needs to provide a definition for them, and a header file that declares them. (Although some key register are mapped to getter/setter procedures, not every register feature is emulated. Just enough to allow development of that action. Those registers that do something some just float as variables, disconnected in the world.)
Next, the framework needs to provide a set of procedures that were not translated (or translated poorly). This might be assembly procedures from the platform library or the C run time library. Or it might be for procedures that were too icky translate. Or we might not have the source for other reasons.
Whenever I supply a replacement procedure, I usually used the prefix
X_ for the procedure. The prefix is arbitrary, but the intent is to prevent collisions with translated procedures of the same name that I choose not use, for one reason or another.
The first procedure I always replace is
main(). This is done because
main is needed to be the entry point for the host program on the PC. The host needs to configure the emulation (and so forth) and then pass control to the emulated
main(), which has now been renamed. Second, I want to replace main anyway, with something I can use to test out the modules I am writing.
The main routine is responsible for preparing the emulation. It calls routines to initialize the microcontroller emulation, including putting its registers into their default setting. Then it calls the initializers in all of the emulation modules. Their internal "registers" are set to their default values.
The modules may preload their memory image. This is most useful with devices such as EEPROM or Flash components, which can load their image from a file, from a save point. But conceivably it could a saved board state, of all the components. Either way, these images might have been saved at some point during execution, sent in from the field, or be an interesting image saved from the test bench.
The initialization also sets up memory and other state to reflect what a boot loader or debug monitor may have done to prepare the system for execution.
Emulating the micro-controller has four elements:
Microcontrollers typically put a lot of emphasis on the flags in the registers. Reading or writing them triggers actions in peripherals. And the register settings are used to communicate with the controlling firmware. There are three ways of emulating the behaviour of the most important registers:
#definemasks. No actions are triggered by the embedded software accessing these register variables. Or the peripherals may use a hidden set of state variables and update the register variables periodically.
Interrupts are emulated in two parts. First, a helper routine is used to determine if interrupts are enabled, and whether control should be passed to the interrupt service routine. Second, many of the API stubs and peripheral emulations call this helper routine to trigger the interrupt handling. In practice, this has proven to be pretty ad hoc.
Microcontrollers have a sleep instruction - often wrapped in a macro - which bears some discussion. This missing procedure is emulated not by sleeping the device. Most often is emulated by adjusting the "board time" to the next board event or interrupt, triggering the interrupt emulation.
What if there is no clear next event? For instance, let us say the embedded software is sleeping until a byte is received on the UART, which will wake it. In this case, the UART is emulated (see below) with a socket or COM port. The software sleeps on a block
read() of that IO handle.
The passage of time for the board is only loosely modeled. In most cases time is modeled as sequences; they have order, but no measurable difference between two points.
In some cases, it is important to have a more meaningful measure of the passage of time relative to board operation. This is useful since EEPROM write or erase cycles typically take a fixed amount of time, independent of the processor speed, and we would like to exercise the "wait" loop of the firmware. Or the analog emulation uses the passage of time to emulate electrical change.
To do this I used the system PC high-resolution timer and map this to a representation of the flow of time, as it would be on the board. (Attempts to use processor cycle counting in the uController won't work here either). This is a pretty straightforward mapping. PC timer value t0 is board time b; 1 board timer tick is m PC timer ticks. Sleep and waiting for interrupts tweaks the mapping between the host time and the emulated time, so that other parts of the emulation are up to date.
As I mentioned earlier, the peripherals access is triggered by linking to register getter/setters, or by shims to key firmware procedures.
The getter/setter procedures have roughly the following outline:
Although the procedure shim for an API varies with the API, it shares the idea that it carries out the peripheral action and checking for interrupts. These actions include:
Note: There isn't a provision to allow the embedded software to emulate an I2C, SPI interface by bit banging. I think that is more work to decode it than is worth it; better to contain the bit-banging to a few procedures and shim them out.
When emulating devices on serial buses, I choose to use an "instance" structure with pointers to key behaviour routines. (Others may wish to use C++ class with virtual methods). This allows more than one DAC of the same model, for instance. I2C and SPI calls a procedure to indicate the start of a message, and a send procedure to exchange (send and receive) a byte at a time.
The basic approach is that the device emulator stores its state, buffers, etc in the instance structure. As operations occur, it updates its internal state.
Because the communication does have a protocol, I typically implement a simple state machine. For instance, I2C devices receive a byte or two for the address (two states) and a flag indicating a read or a write, governing the next state. A write will follow with some bytes. Then it makes changes internally and gives bytes back for the ack/nak of the response. These are read piece by piece (including target bytes). I usually had the state machines check for protocol errors.
For simple memory devices, this simple writes to a small memory space on the device. Slightly more complex devices, like a DAC, use this memory state to change an output voltage, used elsewhere. More complex devices like a calendar/clock, has its memory mapped controls that need to be simulated. (In the case of the calendar/clock, it grabs a timestamp and smooshes them into the memory at the start of a transaction to simplify the reading).
SPI devices, e.g. flash or DACs, are similar. The difference is that they receive commands to erase the device, as well as others. This is handled by the state machine changing state based on the command field in the message.
Note: One idea I had was to use the LabJack (or a USB development board) and its software to redirect the ports, I2C, SPI, ADC, DAC, etc to connect to real peripherals. This is more for the "cool" factor than a practical one - if I had real hardware to talk to, I'd use an ICE.
I found it necessary to emulate some analog state of the board, but much of this emulation is cursory. It is a step above not doing it at all, and well below SPICE simulation.
Found I needed to do this to check battery and power levels, measure DAC outputs, controllable power supplies, RF signal strength, and so forth. The intent is to capture enough of the measurements to allow development to reasonably proceed. (A more complex framework emulating the analog and physical domains can certainly be warranted. I may cover that in the future.)
The emulation is little more than a set of global variables and helper procedures. These are set or called by the emulated peripherals.
Next time I'll examine the techniques I've tried to help catch bugs and do unit tests.