Building for a Simulator

(or maybe possibly your target)

So you've decided to build your tests using your target's cross compiler. Good choice. As you are probably already aware, there are a couple advantages to this approach:

  • Tests are built using the same compiler as your release code
  • You have full test control over all registers and peripherals (if you're using a simulator)

There are also a couple of challenges:

  • Simulators can be slow to execute tests
  • Configuration can sometimes be complicated (depending on your simulator)

Tool Setup

You're going to start your tool setup following the basic instructions. These focus primarily on native builds (because they are easier) but there are really only a couple of differences.

First, you're going to be targeting cross-compilers instead of native compilers. Because you're specifying tool details anyway, this should be fairly straightforward. 

Second, you can't just run the executable and expect a result to come pouring out of stdout. Very likely you will need to do some setup first. This is going to include setup in the executable and as instrumentation outside of it to run the simulator. 

Start by refreshing your brain on the build method of your choice:

Simulators

The first thing you need to worry about when you're running a simulator is startup. Simulators, like their target micros, often want some things to be configured before you get running. This is likely a much smaller set of requirements than the full-blown target, but might include things like specifying vector tables or calling variable initialization before calling main. Chances are good that you will be specifying a test_startup file to be linked with your release code.

Speaking of linking, you'll very likely need to create a linker file for this too. You might be able to get away with reusing your release code's linker file... but that might not be sufficient. While your unit tests are still going to need to know ranges to avoid (like where registers are waiting), they may require more RAM to run than your release code. If you have CMock configured for dynamic memory, you're going to need heap space. Very likely, a single linker can be reused for all your tests, though.

At this point you can build an executable that can actually be RUN on your simulator, but what about the output of those tests? Unity has a number of macros built in that you can take advantage of by defining:

  • UNITY_OUTPUT_CHAR(a)
  • UNITY_OUTPUT_FLUSH()
  • UNITY_OUTPUT_START()
  • UNITY_OUTPUT_COMPLETE()

These macros can be defined in two ways:

  • define them in a unity_config.h file and then define UNITY_INCLUDE_CONFIG_H at the command line to make that header get included
  • define them directly as part of your command line arguments

Often simulators can be configured to funnel a certain peripheral or register to either stdout or an external text file. This is what you want to utilize with these macros. Once the results have gotten outside the simulator, they can be collected and analyzed, just like native build.

Finally the most challenging part of using the simulator to run tests is automation. Simulators are often designed to be run interactively or through a debugger. It might require you to jump through some hoops to get the smooth automatic experience you're looking for. Often it will mean writing a set of simulator or debugger instructions, which you can then trigger from your build system. Once the simulator has exited, you collect the results from stdout or the static file, and move on.

Example?

Let's look at an example. In this case, we'll try a PIC24EP128GP206. The experience should be similar for most Microchip devices. Their toolchain around the XC16 compiler comes with a debugging tool called mdb (probably for Microchip DeBug?). This debugging tool has the option of pointing at a simulator for the device. Perfect!

First, we set up a text file called sim_instructions_mdb.txt. It looks something like this:

Device PIC24EP128GP206
Hwtool SIM
set oscillator.frequency 50
set oscillator.frequencyunit Mega
set oscillator.rcfrequency 250
set oscillator.rcfrequencyunit Kilo
Set uart1io.uartioenabled true
Set uart1io.output file
Set uart1io.outputfile "./build/release/out.txt"
Program "./build/release/TestBuild.out"
Reset
Sleep 500
Break 0x280
Run
Wait 120000
Quit

You can see that this script starts by specifying the platform we're going to simulate. It specifies the microcontroller, and then sets a bunch of clock configuration. Then, it tells the simulator we're going to use uart1io to route to an output file. This is how we're going to gather the results. Then it loads our program (we're going to be copying our bin files to this in our build tool, to keep it easy). Then it resets the simulated micro, and pauses long enough for that to happen. Now here is something interesting. It sets a breakpoint at a specific memory address. We're not going to dump an entire linker file here, but one thing to note is that this memory address has been removed from the main part of the application. We're going to be mapping a specific function here, and then calling it when we're finished.

Once the breakpoint is set, we let the simulator run. It runs through all the tests. The Wait and Quit shouldn't really happen. We should hit that breakpoint way before then... but this is here as a fallback in case something gets stuck in an infinite loop.

Next we create need to add this to our build tool. If you're using make, you might want to create an external script to make this easier, but honestly you could do it with pipes. Let's say we're using Ruby. It might look something like this:

OUT_FILE = "build/release/out.txt"
File.delete OUT_FILE if File.exists? OUT_FILE 
response = `mdb ./simulator/sim_instructions_mdb.txt`
if File.exists? OUT_FILE 
    file_contents = File.read OUT_FILE
    file_contents.gsub!("\r\n", "\n")
    STDOUT.print file_contents
else
    puts "Cannot find file #{OUT_FILE}"
end

Ok, that's a little ugly... but all it is doing really is clearing our output file before we start, then calling MDB with our instruction file. When the simulator has finished executing, it checks to see if the file was created or not.

Okay, so we're almost done. The only other thing we need to do is tell our builds how to deal with all of this stuff. In particular, we need to tell it how to know when it's done, and how to deal with output. For this, we fill out unity_config.h and tell Unity to look for it. It looks something like this:

#ifndef UNITY_CONFIG_H
#define UNITY_CONFIG_H

#define UNITY_INT_WIDTH      16
#define UNITY_POINTER_WIDTH  16
#define CMOCK_MEM_INDEX_TYPE uint16_t
#define CMOCK_MEM_PTR_AS_INT uint16_t
#define CMOCK_MEM_ALIGN      1
#define CMOCK_MEM_SIZE       4096

#include "unity.h"

void UnityHelperDeadLoop(void);

//Put our exit breakpoint in a place where it won't get sat on: an unused interrupt vector
#define UNITY_OUTPUT_COMPLETE() UnityHelperDeadLoop()

#endif

You can see we are declaring a function here called UnityHelperDeadLoop. We're assigning it to get called when we have finished all our output. In our UnityHelper.c file, we define it:

void UnityHelperDeadLoop(void) __attribute__((address(0x0280)));
void UnityHelperDeadLoop(void)
{
    while(1) {};
}

Just as it's name implies, it's an infinite loop. Notice the address it has been assigned to? 0x0280 happens to be the location we set our breakpoint!

Now it's ready to finish pulling into your build system and you're ready to go!

Target Hardware

First, allow me to refer you to the discussion on testing methods, just in case you haven't read it. You are NEVER going to do thorough Unit Testing with your hardware in the loop. That's the domain of System Testing, and System Testing should verify your release code, not little chunks of code like a Unit Test.

If you INSIST on using Unity for testing on your Target Hardware, the process is very similar to the simulator. You're going to have to care about more startup and configuration issues for your micro. You're going to have to pull the execution process into some sort of wrapper to handle programming the device and triggering the app. You're also going to have to worry about piping your results somehow to the host machine. It's all the same issues as the simulator, just on a larger scale. 

Happy Testing!