In order to run a sketch, the values of the input pins over time has to be specified. A simulation script allows doing it: it can set some input pins high and some low, let the sketch run for some time and then change the input pin values, possibly depending on the values of the output pins.
In a simulation script, one can:
Some of these features require a patched version of simulavr. The patch is simulavr-1.0.0.patch. Apply by moving it in the root of the simulavr source, changing to that directory, running patch -p1 < simulavr-1.0.0.patch and compiling the program as usual: configure --enable-tcl; make.
Each sketch uses a different set of pins and is meant to be run with a different kind of input. As a result, each sketch requires its own simulation script. Still better, each simulation test for each sketch requires its own simulation script, written for the sketch and including a possible way in which the input pins change over time.
The following example is for simulating the sketch Arduino.ino, which uses digital pins 13 and 12 as output and 4 as input, and transfer data over the UART. The simulation script simulate.tcl makes some changes on pin 4 and prints how and when the sketch changes the value of pin 13, then writes a character on the serial port and reads one.
The script requires libsimulavr.tcl in the current directory. Its procedures of main interest are:
The simulation results is in the file log.txt. The following table explains what the simulation script does, and how it is reported in the log file.
tcl simulation script | log | explanation |
---|---|---|
#!/usr/bin/tclsh # # ... source libsimulavr.tcl # parameters set program Arduino set interface "netcat -l -p 7777" set frequency 16000000 set model atmega328 # set up device, ui and clock set dev [createDevice $model \ $frequency applet/$program.elf] set ui [createUi $interface] set sc [createClock $dev $ui] |
clock cycle: 62 nanoseconds No connect to socket possible now... retry Connection refused User Interface Connection opened by host 127.0.0.1 port 7777 create UpdateControl dummy dummy |
initialization, nothing to be changed except the name of the sketch (in this case, Arduino.ino) |
# make two pins accessible # D4 = digital pin 4, B5 = digital pin 13 createPin D4 $dev $ui createPin B5 $dev $ui |
create Net D4 .D4 set D4 t 0 create Net B5 .B5 set B5 t 0 |
pins D4 (digital pin 4) and B5 (digital pin 13) are made accessible to the simulation script |
# stop simulation whenever the sketch # changes B4 (digital pin 12) stopPin B4 $dev $sc |
*** pinstop created |
the simulation is frozen and the control is handled back to the simulation script whenever the sketch changes the value of pin B4 (digital pin 12) this has two possible uses: as in this script, the otherwise unused digital pin 12 is configured so that digitalWrite(12, opposite_value) stops the simulation and return to this script; otherwise, configure this way all pins that require some action to be taken on the part of the simulation script (like changing some input pins in response) |
# create the serial line createRx D0 $dev $ui 115200 false createTx D1 $dev $ui 115200 false true |
create SerialTx rxD0 rxD0 create SerialRx txD1 txD1 |
the simulation script can read from and write to the sketch through the UART at pins D0 and D1 (digital pin 0 and 1) |
# set the initial value of input pin setPin D4 L |
<<<<<< setvalue: D4 L <<<<<< set D4 L 0 |
the simulation script can change the value of an input pin via the setPin command; this is logged as "setvalue: ..."; the following line "set D4 L 0" means: pin D4 (digital pin 4) changed to LOW after 0 nanoseconds of simulation; this is correct, as the simulation has not been started yet |
# run for at least 2000 microseconds; # in practice, the run terminates earlier # because of the StopPin change runClock $sc 2000 |
simulavr_port_print: 0x20 >>> simulation started set B5 L 217868 *** pin state changed; time=222084 |
runClock is the command that starts the simulation and makes it run for a given number of microseconds these lines of the log are generated by the sketch running, not by the simulation script; in particular, set B5 L 217868 means: pin B5 (digital pin 13) changed to LOW after 217868 nanosecond of simulation; the following line pin state changed; time=222084 reported the time the simulation was stopped because of change in a PinStop pin |
# now pulse the input pin 4 # for 10 microseconds setPin D4 H runClock $sc 10 setPin D4 L |
<<<<<< setvalue: D4 H <<<<<< set D4 H 222084 <<<<<< setvalue: D4 L <<<<<< set D4 L 232128 |
changing the input pins at fixed intervals is realized by running the simulation for the interval time and then changing the pin value if using a PinStop, the command runClock $sc 10 may terminate earlier than ten microseconds; the script can retrieve the current simulation time with set c [$sc GetCurrentTime] and use it to run a cycle until the required time is reached |
# run 8000 microseconds or # until the PinStop changes runClock $sc 8000 |
set B5 H 235352 set B5 L 1241054 set B5 H 2242602 set B5 L 3247994 set B5 H 4253510 set B5 L 5262870 SystemClock (request 1): 6312716 SystemClock (request 2): 6313150 >>> value of i: 0x001d >>> setup complete *** pin state changed; time=6362440 |
this particular sketch alternates B5 (digital pin 13) from low to high three times at fixed intervals; this is detected and reported in the lines set B5 ...; the number is the time when the pin changed, expressed in the number of nanoseconds since the start of the simulation the time simulavr_time is called in the sketch is reported in the lines SystemClock ...; note that each call requires two clocks by itself, so the difference is actually (6313150 - 6312716) / 62 - 2 = 5 clocks finally, the sketch prints the value of the variable i, writes that the setup is complete and stops the simulation by changing the value of the PinStop |
# write something to the serial port, # depending on the value of digital pin 13 set c [getPin B5 $dev] puts "status of B5: $c" if {$c == "H"} { puts "sending 0x45" rxD0 Send 0x45 } else { puts "sending 0x46" rxD0 Send 0x46 } |
status of B5: L sending 0x46 TX: F |
this is an example of how the value of the ouput pins can be read and used in the simulation script: depending on digital pin 13, what is written to the Arduino via the serial port is 0x45 or 0x46 the value that is written to the Arduino is logged as TX: F, since F is ascii 0x46 |
# this time the sketch runs for # the full 2000 microseconds # since it does not change the # StopPin in loop() runClock $sc 2000 |
>>> received on the serial port: 0x46 >>> sending back: 0x47 set txD1 0x47 |
the first two lines are written by the sketch itself via the simulavr_print function the last is a consequence of Serial.write(c+1) |
# read from the serial port if {[txD1 Size]} { set r [txD1 Get] set c [format "0x%02X" $r] puts "received from the serial port: $c" } |
received from the serial port: 0x47 |
the simulation script can check if the Arduino has written something on the serial port via txD1 Size and read it via txD1 Get; this value can be then be printed (like in this case) or used for taking some other action |
Including Simulavr.h at the top of the sketch allows using some functions that are only meaningful when the sketch is run in the simulator, using a simulator script: simulavr_print, simulavr_newline and simulavr_time.
#define SIMULAVR #define SIMULAVRTIME #include "Simulavr.h"
Removing (or renaming) the macros SIMULAVR and SIMULAVRTIME makes the three functions no-ops. This is useful to save space and time when downloading the sketch to a real board, without having to change the rest of the code.
This function has three arguments: simulavr_print(string, number, bits). It prints the string, then the number with the given number of bits and then a newline. If the number of bits is zero, only the string is printed. To print just a newline, the function simulavr_newline() can be used instead. Technically, these functions work by cycling over the string and the number and writing to a microcontroller register, so they take some time to be executed.
This function requires SIMULAVR to be defined before including Simulavr.h; see Arduino.ino as an example. Otherwise, it is a no-op.
Whenever the simulated sketch calls simulavr_time(n), a line is printed in the form SystemClock (request n): time, where the time is in nanosecond since the start of the simulation. This can be used to check if a section of code is ever executed or how many times it is, and to measure the time it takes to execute it. The example Arduino.ino times the execution of an instruction:
simulavr_time(1); i = TCNT0 + TCNT1; simulavr_time(2);
When running the simulator script, this fragment of code prints the time before and after executing the assigment.
SystemClock (request 1): 6312716 SystemClock (request 2): 6313150
The measure script calculates the difference in clock ticks: simulate.tcl | tee log; measure 1 2. This script requires the output of the simulation script to be redirected to file log.
Each call to simulavr_time takes two clocks, so the difference has to be subtracted 2 to obtain the number of clocks needed to run i = TCNT0 + TCNT1. Since the compiler may have moved the instructions around the calls to simulavr_time, this difference may still not be exact.
The simulavr_time function requires the macro SIMULAVRTIME to be defined before including Simulate.h; see Arduino.ino as an example. Otherwise, it is a no-op.