Simavr Manual
Simavr Manual
Jakob Gruber
[email protected]
October 5, 2020
Contents
1. Introduction 1
2. simavr Internals 2
2.1. simavr Example Walkthrough . . . . . . . . . . . . . . . . . . . 2
2.2. The Main Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3. Initialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.1. avr_t Initialization . . . . . . . . . . . . . . . . . . . . . 9
2.3.2. Firmware . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.4. Instruction Processing . . . . . . . . . . . . . . . . . . . . . . . 11
2.5. Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.1. Data Structures . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.2. Raising and Servicing Interrupts . . . . . . . . . . . . . . 13
2.6. Cycle Timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.7. GNU Debugger (GDB) Support . . . . . . . . . . . . . . . . . . 15
2.8. Interrupt Requests (IRQs) . . . . . . . . . . . . . . . . . . . . . 17
2.9. Input/Output (IO) . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.9.1. Input/Output (IO) Register Callbacks . . . . . . . . . . 19
2.9.2. The avr_io_t Module . . . . . . . . . . . . . . . . . . . 20
2.10. Value Change Dump (VCD) Files . . . . . . . . . . . . . . . . . 23
2.11. Core Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
A. Setup Guide 27
A.1. simavr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
A.1.1. Getting the source code . . . . . . . . . . . . . . . . . . 27
A.1.2. Software Dependencies . . . . . . . . . . . . . . . . . . . 27
A.1.3. Compilation and Installation . . . . . . . . . . . . . . . . 28
i
1. Introduction
This manual is an excerpt of the bachelor’s thesis “qsimavr: Graphical Simu-
lation of an AVR Processor and Periphery” by Jakob Gruber. The full thesis
is available at https://github.com/schuay/bachelors_thesis.
Chapter 2 provides a brief overview of simavr internals, followed by a setup
guide in appendix A.
1
2. simavr Internals
simavr is a small cross-platform Alf and Vegard’s Risc processor (AVR) simula-
tor written with simplicity, efficiency and hackability in mind1 . It is supported
on Linux and OS X, but should run on any platform with avr-libc support.
In the following sections, we will take a tour through simavr internals2 . We
will begin by examining short (but complete) demonstration application.
i2c_eeprom_t ee ;
1
For some more technical principles, simavr also tries to avoid heap allocation at runtime
and often relies on C99’s struct set initialization.
2
Most, if not all of the code examined in this chapter is taken directly from simavr .
2
2.1 simavr Example Walkthrough 2 simavr Internals
avr is the main data structure. It encapsulates the entire state of the core
simulation, including register, Static Random-Access Memory (SRAM) and
flash contents, the Central Processing Unit (CPU) state, the current cycle
count, callbacks for various tasks, pending interrupts, and more.
vcd_file represents the file target for the Value Change Dump (VCD) mod-
ule. It is used to dump the level changes of desired pins (or Interrupt Re-
quests (IRQs) in general) into a file which can be subsequently viewed using
utilities such as gtkwave.
ee contains the internal state of the simulated external EEPROM.
int main ( int argc , char * argv [])
{
elf_firmware_t f ;
elf_read_firmware ( " atmega1280_i2ctest . axf " , & f ) ;
The firmware is loaded from the specified file. Note that exactly the same
file can be executed on the AVR hardware without changes. Microcontroller
(MCU) and frequency information have been embedded into the binary and
are therefore available in elf_firmware_t.
avr = avr_make_mcu_by_name ( f . mmcu ) ;
avr_init ( avr ) ;
avr_load_firmware ( avr , & f ) ;
The avr_t instance is then constructed from the core file of the specified
MCU and initialized. avr_load_firmware copies the firmware into program
memory.
i2c_eeprom_init ( avr , & ee , 0 xa0 , 0 xfe , NULL , 1024) ;
i2c_eeprom_attach ( avr , & ee , AVR_IOCTL_TWI_GETIRQ
(0) ) ;
3
2.1 simavr Example Walkthrough 2 simavr Internals
return 0;
}
Finally, we have reached the simple main loop. Each iteration executes one
instruction, handles any pending interrupts and cycle timers, and sleeps if
possible. As soon as execution completes or crashes, simulation stops and we
exit the program.
We will now examine the relevant parts of the i2c_eeprom implementation.
Details have been omitted and only communication with the avr_t instance
are shown.
static const char * _ee_irq_names [2] = {
[ TWI_IRQ_MISO ] = " 8 > eeprom . out " ,
[ TWI_IRQ_MOSI ] = " 32 < eeprom . in " ,
};
void
i2c_eeprom_init (
struct avr_t * avr ,
i2c_eeprom_t * p ,
uint8_t addr ,
uint8_t mask ,
uint8_t * data ,
size_t size )
{
4
2.1 simavr Example Walkthrough 2 simavr Internals
/* [...] */
/* [...] */
}
First, the EEPROM allocates its own private IRQs. The EEPROM imple-
mentation does not know or care to which simavr IRQs they will be attached.
It then attaches a callback function (i2c_eeprom_in_hook) to the Master Out,
Slave In (MOSI) IRQ. This function will be called whenever a value is written
to the IRQ. The pointer to the EEPROM state p is passed to each of these
callback function calls.
void
i2c_eeprom_attach (
struct avr_t * avr ,
i2c_eeprom_t * p ,
uint32_t i2c_irq_base )
{
avr_connect_irq (
p - > irq + TWI_IRQ_MISO ,
avr_io_getirq ( avr , i2c_irq_base ,
TWI_IRQ_MISO ) ) ;
avr_connect_irq (
avr_io_getirq ( avr , i2c_irq_base ,
TWI_IRQ_MOSI ) ,
p - > irq + TWI_IRQ_MOSI ) ;
}
The private IRQs are then attached to simavr ’s internal IRQs. This is called
chaining - all messages raised are forwarded to all chained IRQs.
static void
i2c_eeprom_in_hook (
struct avr_irq_t * irq ,
uint32_t value ,
void * param )
{
i2c_eeprom_t * p = ( i2c_eeprom_t *) param ;
5
2.2 The Main Loop 2 simavr Internals
/* [...] */
/* [...] */
}
Finally, we’ve reached the IRQ callback function. It is responsible for sim-
ulating communications between simavr (acting as the TWI master) and the
EEPROM (as the TWI slave). The EEPROM state which was previously
passed to avr_irq_register_notify is contained in the param variable and
cast back to an i2c_eeprom_t pointer for further use.
Outgoing messages are sent by raising the internal IRQ. This message is
then forwarded to all chained IRQs.
3
Whenever avr is mentioned in a code section, it is assumed to be the main avr_t struct.
6
2.2 The Main Loop 2 simavr Internals
7
2.2 The Main Loop 2 simavr Internals
This section ensures that interrupts are not triggered immediately when en-
abling the interrupt flag in the status register, but with an (additional) delay
of one instruction.
avr_cycle_count_t sleep = avr_cycle_timer_process (
avr ) ;
avr - > pc = new_pc ;
Next, all due cycle timers are processed. Cycle timers are one of the most
important and heavily used mechanisms in simavr . A timer allows scheduling
execution of a callback function once a specific count of execution cycles have
passed, thus simulating events which occur after a specific amount of time has
passed. For example, the avr_timer module uses cycle timers to schedule timer
interrupts.
The returned estimated sleep time is set to the next pending event cycle (or
a hardcoded limit of 1000 cycles if none exist).
if ( avr - > state == cpu_Sleeping ) {
if (! avr - > sreg [ S_I ]) {
avr - > state = cpu_Done ;
return ;
}
avr - > sleep ( avr , sleep ) ;
avr - > cycle += 1 + sleep ;
}
If the CPU is currently sleeping, the time spent is simulated using the call-
back stored in avr->sleep. In GDB mode, the time is used to listen for GDB
commands, while the raw version simply calls usleep.
It is worth noting that we have improved the timing behavior by accumu-
lating requested sleep cycles until a minimum of 200 usec has been reached.
usleep cannot handle lower sleep times accurately, which caused an unrealistic
execution slowdown.
A special case occurs when the CPU is sleeping while interrupts are turned
off. In this scenario, there is no way of ever waking up. Therefore, execution
is halted gracefully.
if ( avr - > state == cpu_Running || avr - > state ==
cpu_Sleeping )
avr_service_interrupts ( avr ) ;
Finally, any immediately pending interrupts are handled. The highest prior-
ity interrupt (this depends solely on the interrupt vector address) is removed
8
2.3 Initialization 2 simavr Internals
from the pending queue, interrupts are disabled in the status register, and the
program counter is set to the interrupt vector.
If the CPU is sleeping, interrupts can be raised by cycle timers.
if ( step )
avr - > state = cpu_StepDone ;
}
Wrapping up, if the current loop iteration was a GDB step, the state is set
such that the next iteration will inform GDB and halt the CPU.
2.3. Initialization
2.3.2. Firmware
We now have a fully initialized avr_t struct and are ready to load code. This
is accomplished using avr_read_firmware, which uses elfutils to decode the
Executable and Linkable Format (ELF) file and read it into an elf_firmware_t
struct and avr_load_firmware to load its contents into the avr_t struct.
9
2.3 Initialization 2 simavr Internals
Besides loading the program code into avr->flash (and EEPROM contents
into avr->eeprom, if available), there are several useful extended features which
can be embedded directly into the ELF file.
The target MCU, frequency and voltages can be specified in the ELF
file by using the AVR_MCU and AVR_MCU_VOLTAGES macros provided by
avr_mcu_section.h:
# include " avr_mcu_section . h "
AVR_MCU (16000000 /* Hz */ , " atmega1280 " ) ;
AVR_MCU_VOLTAGES (3300 /* milliVolt */ , 3300 /*
milliVolt */ , 3300 /* milliVolt */ ) ;
VCD traces can be set up automatically. The following code will create an 8-
bit trace on the UDR0 register, and a trace masked to display only the UDRE0
bit of the UCSR0A register.
const struct avr_mmcu_vcd_trace_t _mytrace [] _MMCU_ =
{
{ AVR_MCU_VCD_SYMBOL ( " UDR0 " ) , . what = ( void *) & UDR0
, },
{ AVR_MCU_VCD_SYMBOL ( " UDRE0 " ) , . mask = (1 << UDRE0
) , . what = ( void *) & UCSR0A , } ,
};
Several predefined commands can be sent from the firmware to simavr during
program execution. At the time of writing, these include starting and stopping
VCD traces, and putting UART0 into loopback mode. An otherwise unused
register must be specified to listen for command requests. During execution,
writing a command to this register will trigger the associated action within
simavr .
AVR_MCU_SIMAVR_COMMAND (& GPIOR0 ) ;
int main () {
/* [...] */
GPIOR0 = SIMAVR_CMD_VCD_START_TRACE ;
/* [...] */
}
Likewise, a register can be specified for use as a debugging output. All bytes
written to this register will be output to the console.
AVR_MCU_SIMAVR_CONSOLE (& GPIOR0 ) ;
int main () {
10
2.4 Instruction Processing 2 simavr Internals
/* [...] */
const char * s = " Hello World \ r " ;
for ( const char * t = s ; * t ; t ++)
GPIOR0 = * t ;
/* [...] */
}
Usually, UART0 is used for this purpose. The simplest debug output can be
achieved by binding stdout to UART0 as described by the avr-libc documen-
tation, and then using printf and similar functions. This alternate console
output is provided in case using UART0 is not possible or desired.
11
2.5 Interrupts 2 simavr Internals
2.5. Interrupts
An interrupt is an asynchronous signal which causes the the CPU to jump to
the associated Interrupt Service Routine (ISR) and continue execution there.
In the AVR architecture, the interrupt priority is ordered according to its place
in the interrupt vector table. When an interrupt is serviced, interrupts are
disabled globally.
12
2.5 Interrupts 2 simavr Internals
13
2.6 Cycle Timers 2 simavr Internals
Otherwise, the current program counter is pushed onto the stack. This il-
lustrates the difference between byte addresses (as used in avr->pc) and word
addresses (as expected by the AVR processor). Interrupts are then disabled by
clearing the I bit of the status register, and the program counter is set to the
ISR vector. Finally, if raise_sticky is 0, the interrupt flag is cleared.
void
avr_cycle_timer_register (
struct avr_t * avr ,
avr_cycle_count_t when ,
avr_cycle_timer_t timer ,
14
2.7 GNU Debugger (GDB) Support 2 simavr Internals
void * param ) ;
15
2.7 GNU Debugger (GDB) Support 2 simavr Internals
We have covered how to enable GDB support in section 2.3.1, and when
GDB handler functions are called during the main loop in section 2.2. In the
following, we will explain further the methods simavr employs to communicate
with GDB and how breakpoints and data watchpoints are implemented.
simavr has a fully featured implementation of the GDB Remote Serial Proto-
col, which allows it to communicate with avr-gdb. A complete reference of the
protocol can be obtained from the GDB manual. Essentially, communication
boils down to packets of the format $packet-data#checksum. The packet data
itself consists of a command and its arguments. The syntax of all commands
supported by simavr is as follows:
’?’ Indicate the reason the target halted.
’G XX...’ Write general registers.
’g’ Read general registers.
’p n’ Read the value of register n.
’P n...=r...’ Write register n with value r.
’m addr,length’ Read length bytes of memory starting at address
addr.
’M addr,length:XX...’ Write length bytes of memory starting
address addr. XX... is the data.
’c’ Continue.
’s’ Step.
’r’ Reset the entire system.
’z type,addr,kind’ Delete break and watchpoints.
’Z type,addr,kind’ Insert break and watchpoints.
Many of these commands expect a reply value. This could be a simple as
sending "OK" to confirm successful execution, or it could contain the requested
data, such as the reply to the ’m’ command. A single reply can chain several
data fields. For example, whenever a watchpoint is hit, the reply contains the
signal the program received (0x05 represents the “trap” signal), the SREG,
Stack Pointer (SP), and PC values, the type of watchpoint which was hit
(either "awatch", "watch", or "rwatch"), and the watchpoint address.
The packets themselves are received and sent over an AF_INET socket listening
on the avr->gdb_port.
Both watchpoints and breakpoints are stored within an
avr_gdb_watchpoints_t struct in avr->gdb and are limited to 32 ac-
tive instances of each. Breakpoints are set at a particular location in flash
memory. Whenever the PC reaches that that point, execution is halted, a sta-
tus report containing a summary of current register values is sent, and control
is passed to GDB. This range check takes place in avr_gdb_processor, which
16
2.8 Interrupt Requests (IRQs) 2 simavr Internals
17
2.8 Interrupt Requests (IRQs) 2 simavr Internals
void
avr_irq_register_notify (
avr_irq_t * irq ,
avr_irq_notify_t notify ,
void * param ) ;
void
avr_irq_register_notify (
avr_irq_t * irq ,
avr_irq_notify_t notify ,
void * param ) ;
void
8
IRQ_FLAG_ALLOC and IRQ_FLAG_INIT are of internal interest only and not mentioned
further.
18
2.9 Input/Output (IO) 2 simavr Internals
avr_connect_irq (
avr_irq_t * src ,
avr_irq_t * dst ) ;
void
avr_raise_irq (
avr_irq_t * irq ,
uint32_t value ) ;
19
2.9 Input/Output (IO) 2 simavr Internals
}
/* [...] */
}
This snippet contains several interesting bits; first of all, we are reminded that
IO addresses are offset by 0x20 (these are added by AVR_DATA_TO_IO). Next
up, we see that write callbacks need to set the avr->data value themselves if
necessary. Notice also that a custom parameter is passed into the callback, like
most other callback systems in simavr . Finally, the associated IOMEM IRQs are
raised; both bitwise and the byte IRQ AVR_IOMEM_IRQ_ALL.
Read accesses are very similar, except that (somewhat counter-intuitively),
the value returned by the callback is automatically written to avr->data.
Access callbacks plus associated IOMEM IRQs are stored in the avr->io array.
MAX_IOs is currently set to 279, enough to handle all used IO registers on AVRs
like the atmega1280, which go up to an address of 0x1369 .
struct {
struct avr_irq_t * irq ;
struct {
void * param ;
avr_io_read_t c ;
} r;
struct {
void * param ;
avr_io_write_t c ;
} w;
} io [ MAX_IOs ];
Callbacks are registered using the function duo of avr_register_io_write
and avr_register_io_read. IRQs are created on-demand whenever the
avr_iomem_getirq function is called.
The included simavr modules (implemented in files beginning with the avr_
prefix) provide many practical examples of IO callback usage; for example, the
avr_timer module uses IO callbacks to start the timer when a clock source is
enabled through the timer registers.
20
2.9 Input/Output (IO) 2 simavr Internals
uint32_t irq_ioctl_get ;
int irq_count ;
struct avr_irq_t * irq ;
21
2.9 Input/Output (IO) 2 simavr Internals
Implementation Overview
22
2.10 Value Change Dump (VCD) Files 2 simavr Internals
23
2.11 Core Definitions 2 simavr Internals
1"
1!
1#
[...]
24
2.11 Core Definitions 2 simavr Internals
. init = m1280_init ,
. reset = m1280_reset ,
. rampz = RAMPZ ,
},
/* [...] */
}
13
The argument specifies the vector size.
25
2.11 Core Definitions 2 simavr Internals
[2] = AVR_TIMER_WGM_CTC () ,
[3] = AVR_TIMER_WGM_FASTPWM8 () ,
[7] = AVR_TIMER_WGM_OCPWM () ,
},
. cs = { AVR_IO_REGBIT ( TCCR0B , CS00 ) , AVR_IO_REGBIT
( TCCR0B , CS01 ) , AVR_IO_REGBIT ( TCCR0B , CS02 ) } ,
. cs_div = { 0 , 0 , 3 /* 8 */ , 6 /* 64 */ , 8 /* 256
*/ , 10 /* 1024 */ } ,
. r_tcnt = TCNT0 ,
. overflow = {
. enable = AVR_IO_REGBIT ( TIMSK0 , TOIE0 ) ,
. raised = AVR_IO_REGBIT ( TIFR0 , TOV0 ) ,
. vector = TIMER0_OVF_vect ,
},
/* ... */
}
26
A. Setup Guide
This section provides instructions on how to retrieve, compile and install simavr
on the GNU/Linux operating system.
A.1. simavr
A.1.1. Getting the source code
The official home of simavr is https://github.com/buserror/simavr. Stable
releases are published as git repository tags (direct downloads are available at
https://github.com/buserror/simavr/tags). To clone a local copy of the
repository, run
27
A.1 simavr A Setup Guide
make
make install
As usual, there are several variables to allow configuration of the build pro-
cedure. The most important ones are described in the following section:
• AVR ROOT
The path to the system’s avr-libc installation.
While the default value should be correct for many systems, it may
need to be set manually if the message ’WARNING . . . did not compile,
check your avr-gcc toolchain’ appears during the build. For example, if
iomxx0 1.h is located at /usr/avr/include/avr/iomxx0 1.h, AVR ROOT
must be set to /usr/avr.
• CFLAGS
The standard compiler flags variable.
It may be useful to modify CFLAGS for easier debugging (in which case
optimizations should be disabled and debugging information enabled: -
O0 -g). Additionally adding -DCONFIG SIMAVR TRACE=1 enables
extra verbose output and extended execution tracing.
These variables may be set either directly in Makefile.common, or alter-
natively can be passed to the make invocation (make AVR ROOT=/usr/avr
DESTDIR=/usr install).
Installation is managed through the usual
make install
The DESTDIR variable can be used in association with the PREFIX variable
to create a simavr package. DESTDIR=/dest/dir PREFIX=/usr installs to
/dest/dir but keeps the package configured to the standard prefix (/usr).
For development, we built and installed simavr with the following procedure:
make clean
make AVR_ROOT=/usr/avr CFLAGS="-O0 -Wall -Wextra -g -fPIC \
-std=gnu99 -Wno-sign-compare -Wno-unused-parameter"
make DESTDIR="/usr" install
28