Skip to content
/ simavr Public
forked from buserror/simavr

simavr is a lean, mean and hackable AVR simulator for linux & OSX

License

Notifications You must be signed in to change notification settings

gatk555/simavr

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This repository is a fork from the original here.

New Features

At the time of writing (December 2023) this fork contains some new or updated items.

  • A brief "Getting Started" guide, intended for new users. See below, but the HTML file in the doc directory looks better. Github's HTML processing is a little off.
  • A GUI control panel for a simulated AVR. A picture and a short description are in the guide, below.
  • New IRQs for setting analogue voltages during simulation and for ACOMP.
  • Some additional and some expanded tests.
  • Changes intended to help to add newer core types, such as the ATmega4809.
  • Support for additional internally-clocked timer modes (incomplete).
  • The GPIO pull-up code is conditionally compiled.
  • Some Pull Requests that are not yet integrated upstream may be included.
  • The old AVR header files have been removed.
  • Miscellaneous bug fixes.

Some of these changes may be available as upstream Pull Requests.

Because the old AVR header files have been removed, it is necessary to create a link, simavr/simavr/cores/avr to the set of header files that come with avr-libc. The target directory should be the one containing io.h. (On Windows, the mklink command creates symbolic links.) Alternatively, copy the header files into a subdirectory at that location.

Getting Started with Simavr.

This is a guide to getting started with, simavr, "a lean and mean Atmel AVR simulator". It is intended as a supplement to the existing documentation and describes how to load and run AVR firmware in the simulator and how to begin writing code that simulates the rest of your circuit.

Before using simavr, you will need the ELF library, libelf and an AVR toolchain (programs for building AVR programs). The preferred toolchain is gcc-avr but anything that can produce Intel HEX files should work. If using gcc-avr with C or C++, you will also probably need the C library, avr-libc.

Instructions for building simavr from source can be found in the PDF manual or the Bulid Guide page of the github Wiki. Updated instructions for Windows are here.

Suppose you have downloaded and (if necessary) built simavr. Now what?

You have a program called run_avr, or possibly simavr that can load and run AVR programs (firmware). You also have the simulator built as a library that can be combined with your own software in a more complete circuit simulation. The tree also contains some examples of using the library and some tests to verify that simavr is working properly. This guide is mostly an introduction to the library.

Using run-avr.

An easy way to get your firmware compiled and running with gcc-avr is to create a new sub-directory of simavr/examples containing your firmware source and a simple Makefile like this one. The simavr convention is that C source files with names beginning "attiny" or "atmega" are firmware: the first component of the name identifies the target AVR part and is used to select the appropriate compiler options. If your new directory has a name beginning "board_", the Makefile will be called by those higher in the directory tree. To execute your firmware, invoke run-avr with the compiled firmware as the only non-option argument.

While your firmware is running, run-avr can show the instructions being executed, using the "--trace" option, and can also trace actions in the peripherals. These tracing options are disabled by default and are enabled by modifying simavr/Makefile to turn on CONFIG_SIMAVR_TRACE and recompiling.

The interface from your firmware program to the world is a simulation of the interface of physical AVR microcontrollers: voltages input and output on the device's pins. The simulator can emulate these voltages by reading and writing Value Change Dump (VCD) files, controlled by command line options and data in the firmware file. VCD files are formatted text files, but are not intended to be read directly. There are tools, for example gtkwave for displaying VCD files but, as far as I know, few dedicated software tools for creating them. Two programs that may help are PICSimLab and simutron They are both larger graphical circuit designer/emulator that include simavr as a component and have various input devices whose actions can be captured as VCD files.

The filename and included items for VCD output may be specified in the firmware, by function calls (sim_vcd_file.h) or by the --add_trace option of run_avr. The option value contains a name for the traced item, a keyword and two hexadecimal numbers in the form name=kind@addr/mask. For example,

  --add-trace ucsr0a=trace@0xc0/0xff
will add a trace named "ucsr0a" on the register at address 0xc0. There are three keywords: for trace the parameters are an address and a mask to set which bits will be traced; for portpin a GPIO port pin number and the capital letter that identifies the port; and for irq an interrupt event index and an interrupt vector number. The event index is 0 for interrupt pending events (the interrupt is enabled and raised) or 1 for interrupt handler execution. So
  --add-trace pwm=portpin@0x2/0x42 --add-trace interrupt=irq@0x1/0x3
will trace the state of pin 2 on port B, and execution of the interrupt handler for vector 3.

VCD files for input must follow a convention for variable names so that they match the printf() format "%c%c%c%c_%d". The inserted values correspond to the last two arguments to avr_io_getirq(), described below. If input files require translation, another program, vcdmaker, may be useful. It is a utility for extracting information from various log files and generating VCD data. A following section describes another option for creating VCD input files that already follow simavr's rules.

Debugging with run-avr.

The simulator has support for debugging firmware with the AVR version of the GNU debugger, avr-gdb. Simply start run_avr as usual, with the extra option --gdb and put the process in the background. Then start avr-gdb with your firmware binary (ELF format with debugging symbols is best) as argument. The first gdb command should be:
  target remote :1234
That connects the debugger to the simulated AVR. Following that there is little difference from ordinary debugging with the standard gdb. An initial gdb command can be included in the shell command:
   avr-gdb -ex 'target remote :1234' firmware.elf
Gdb has a command, monitor, for sending device-specific commands to the device being debugged. With simavr the subcommands halt and reset are supported.

There is support for the standard avr-gdb commands

  load
  info io_registers
for loading flash memory from a file and for displaying I/O register names and values. Also
  monitor ior hex-base-address register-count
to specify a sub-range of I/O registers to be shown. Without arguments, monitor ior returns to displaying all the I/O registers. Several monitor sub-commands can be combined on one line.

Options in firmware.

A slightly unusual feature of simavr is that options for the simulator can be specified by macro calls included in the firmware (ELF compilation only). In some cases there is no other way to set them. These options include peripheral tracing details, VCD file tracing details, some analogue voltage inputs and others. Details are in the file simavr/simavr/sim/avr/avr_mcu_section.h and the included example firmware programs show how the macros can be used.

Interactive run-avr.

The version of run-avr in the gatk555 fork that contains this file has an additional option "--panel" that displays an interactive control panel, allowing you to control firmware execution, view output and provide inputs.

Image of simavr window

At the top of the window is the "Clock" sub-panel that controls execution: "Run" is a toggle button to start and stop execution, "Go" executes a limited number of instructions, set by the "Burst" entry field. The "Fast" button makes the simulator run at full speed, with the display updated only at the end of a burst. Otherwise, the speed is set by "Speed" field. The units are 0.1Hz, so 20 means two AVR cycles per second.

The current Program Counter and cycle count are shown below, followed by sub-panels for the GPIO ports and ADC (if present). GPIO pins are colour-coded: 0 and 1 are shown as black/red for outputs, blue/green for inputs.

Buttons labelled "SoR" are for Stop-on-read: execution will be halted when the AVR reads that input source. The button changes from red to green when the halt occurs. A new input value can then be entered and execution continued. Use the Enter key after entering new ADC inputs, otherwise they are visible but not sent. The GPIO ports now also have "SoW" (Stop-on-Write) buttons that work similarly.

If "--panel" is used with "--output", the inputs from the control panel are captured in a separate VCD file that can be played back later with "--input". This may be useful for creating test data files and demonstrations. Playback can be combined with additional interactive input.

The panel uses blink, a small library based on Gtk. Its source can be found in the gatk555/blink repository. To build the version with the panel, make a symbolic link to the blink source directory: simavr/simavr/sim/blink or use a real directory and put the blink source or outputs there.

Using libsimavr.

If you are developing new code to work with libsimavr, it is likely that you will need to change the source code, if only for debugging. So it is better to use the library and headers from the build tree, rather than installed version. After building from source, the library can be found in a directory simavr/simavr/obj*' with the full name depending on your hardware and OS, and the headers are in simavr/simavr/sim.
Initialisation.
The first step in developing a program that uses libsimavr is to create an instance of a simulated MCU and load the firmware. Then it needs to be connected to some kind of external circuit simulation and code to start and possibly control execution. It may be worth considering adding new functions to run_avr rather than writing a new program, as the initialisation and some control code is already there. That is how the control panel was implemented.

Because an ELF firmware file can contain parameters for the simulator, the first step is to read the file:

	elf_firmware_t  fw = {};  //  Must be initialised,
	char           *firmware; //  File name for firmware
sim_setup_firmware(firmware, 0, &fw, argv[0]);
An ELF file is assumed, unless the file name ends ".hex". When using ihex format, the elf_firmware_t structure should be be further initialiased with the name and clock frequency of the specific AVR part before the function call:
	strcpy(fw.mmcu, name);
	fw.frequency = f_cpu;
	sim_setup_firmware(firmware, 0, &fw, argv[0]);
With the firmware prepared for loading, the next step is to create the simulated microcontroller and load the firmware:
	avr_t *avr;
avr = avr_make_mcu_by_name(f.mmcu);
if (!avr) {
	fprintf(stderr, "%s: AVR '%s' not known\n", argv[0], f.mmcu);
	exit(1);
}
avr_init(avr);
avr_load_firmware(avr, &f);
The simulator is now ready to run. The simplest option:
	for (;;) {
		int state = avr_run(avr);
		if (state == cpu_Done || state == cpu_Crashed)
			break;
	}
By default, the loop executes one instruction at a time. To make simulation faster, a burst size can be set as avr->run_cycle_limit.
Connecting the external circuit.
If the firmware is to do anything, there needs to be a connection to a simulated external circuit. In many cases this will use the digital input and output functions of the AVR general-purpose I/O ports (GPIOs). Some internal peripheral simulations route through the GPIO simulation, so external code does not need to consider what goes on inside, while others are handled through their own interfaces.

External code interacts with simavr through function calls labelled "IRQ", described in simavr/simavr/sim/sim_irq.h.) Pointers to structures (struct avr_irq_t) serve as "handles" to events and operations within the simulator. Each peripheral, including the 8-bit GPIO ports, has a list of IRQs that it publishes and the first step in connecting code to the simulation is to obtain an IRQ pointer. This is how to get access to output on the 8 PORTB pins of an AVR:

	avr_irq_t *output_handle;
        uint32_t   ioctl;
    ioctl = (uint32_t)AVR_IOCTL_IOPORT_GETIRQ('B');
output_handle = avr_io_getirq(avr, ioctl, IOPORT_IRQ_REG_PORT);
The second argument selects the peripheral and the third the specific IRQ supported by the peripheral.

There are two basic actions on an IRQ: pass a value into the simulator, or request that the simulator calls an external function when an event associated with the IRQ occurs. Most IRQs support only one of these methods, some support both. The direction is given in the IRQ's name, which can be found in the structure or seen near the end of the peripheral's source code file. In simavr/simavr/sim/avr_ioport.c that IRQ's name is shown as "8>port". That means it is associated with 8 bits of data and the transfer direction is from the MCU to the outside. An external function must be connected to this IRQ to see output from the port.

// This function will be called when the AVR changes the port output.

static void d_out_notify(avr_irq_t *irq, uint32_t value, void *param) { // Do something with param and value ... }

    // ... back in main program initialisation.

avr_irq_register_notify(output_handle, d_out_notify, param);
The third argument is a void pointer that is passed through to the called function. Usually it will be a pointer to a program structure representing the connections to the port. If the pin assignments to input or output are not fixed for the duration of execution it is also necessary to track the port direction register. The panel code does it like this.
/* Notification of output to a port. This is called whenever the output
 * or data direction registers are written.  Writing to the input register
 * to toggle output register bits is handled internally by simavr
 * and appears as an output register change.
 *
 * New pin values are calculated ignoring pullups, including simavr's
 * external pullup feature - the panel provides "strong" inputs.
 */

static void d_out_notify(avr_irq_t *irq, uint32_t value, void *param) { struct port *pp; uint8_t ddr, out;

pp = (struct port *)param;
if (irq->irq == IOPORT_IRQ_DIRECTION_ALL) {
    Bfp->new_flags(PORT_HANDLE(pp, 0), ~value);
    ddr = pp->ddr = (uint8_t)value;
    out = pp->output;
} else {
    ddr = pp->ddr;
    out = pp->output = (uint8_t)value;
}
pp->actual = (out & ddr) | (pp->actual & ~ddr);
Bfp->new_value(PORT_HANDLE(pp, 0), pp->actual);

}

...

    ioctl = (uint32_t)AVR_IOCTL_IOPORT_GETIRQ(port_letter);
    base_irq = avr_io_getirq(avr, ioctl, 0);

...

if (base_irq != NULL) { /* Some port letters are skipped. */

...

        avr_irq_register_notify(base_irq + IOPORT_IRQ_REG_PORT,
                                d_out_notify, pp);
        avr_irq_register_notify(base_irq + IOPORT_IRQ_DIRECTION_ALL,
                                d_out_notify, pp);
The handler combines input and output values to produce a single byte value for display. This takes advantage of two additional features of IRQs: each peripheral IRQ includes its own selector value (irq->irq;) and the IRQs for any one peripheral form an array (there are some exceptions), so it is really only necessary to call avr_io_getirq() once for each peripheral used. (Another useful feature is that the IRQ structure contains the value passed on the previous call, so it easy to detect changes.)

The first 8 GPIO IRQ's are assigned to individual pins and are bi-directional. (There is an infinite recursion hazard!) These IRQ's are the way to do digital input and avr_raise_irq() is the usual way to pass values into the simulation. This simplified example is from the panel code.

    unsigned int             changed, mask, dirty, i;
changed = value ^ pp->actual;
if (changed) {
    /* Scan for changed bits. */

    for (i = 0, mask = 1; i < 8; ++i, mask <<= 1) {
        if (mask & changed) {
                int bit;

...

                bit = ((value & mask) != 0);

                /* Push changed bit into simavr.
                 * The first 8 IRQs set individual bits.
                 */

                avr_raise_irq(pp->base_irq + i, bit);
If pin-change interrupts on a port and other digital peripherals are not used, it is possible to delay setting new input values until the firmware reads them. A handler for the IOPORT_IRQ_REG_PIN IRQ will be called when the port is read. New pin values can be set in the handler.
Analogue input peripherals
The analogue input peripherals are the ADC and the comparator.

For these peripherals, the external code needs to set a voltage (in millivolts) on an input pin. Simavr treats analogue input as completely separate from digital I/O, even though in reality they share pins. There is nothing in Simavr that connects the two, so it would allow you to do digital output on a pin that you are simultaneously using for analogue input.

This fragment shows the IRQs used for analogue input to the ADC.

	avr_irq_t *ADC_base_irq;
ADC_base_irq = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, 0);

...

avr_raise_irq(ADC_base_irq + n, millivolts); // Set ADCn
As with the GPIO ports, there is an outbound IRQ, ADC_IRQ_OUT_TRIGGER, whose handler will be called when analogue input is sampled, and a new value can be set in the handler. The 32-bit value passed to the handler is a structure (not a pointer) with bit fields indicating the type of input (single source, differential or special-purpose) and the source(s). New values for the conventional ADC channels can be set in the handler.

The analogue comparator can share the physical ADC inputs on some variants and has its own inputs. Simavr treats all of these as independent of both GPIO and ADC inputs. Although ADC_IRQ_ADC1 and ACOMP_IRQ_ADC1 may represent the same pin, those are different entities and simavr would allow you to set different voltages. Providing input values for the comparator is similar to the ADC, except that monitoring is continuous, so there is no IRQ to intercept reads. ACOMP_IRQ_OUT offers notification of changes to the comparator's output bit.

	avr_irq_t *AC_base_irq;
AC_base_irq = avr_io_getirq(avr, AVR_IOCTL_ACOMP_GETIRQ, 0);

...

avr_raise_irq(AC_base_irq + channel, millivolts);
Other peripherals.
It might seem that the IRQs mentioned above are all that are needed, as all the chip functions visible to external circuits use an I/O pin. That is true for the timers; their externally-visible functions are routed through the GPIO ports.

The serial communication peripherals (UART, SPI, TWI) are different, as transmissions do not modify pin state and reception does not examine it. Instead, each peripheral has its own IRQs to send and receive bytes. That simplifies both the simulator and external code, and the omission should only be visible if the pins used are also linked to unrelated external circuitry. If you think of doing that, this restriction is telling you to choose an AVR with more pins.

Simavr Timers.
The implementation of peripherals such as serial communications and the ADC (and simulated AVR timers!) requires timed events that are independent of the firmware but must be synchronised with its execution. The same interface can be used when simulating external circuitry. Like IRQs, this is a callback interface. This example from device simulation simavr/examples/parts/hd44780.c also shows how external simulation code can also create and use its own IRQs.
static avr_cycle_count_t
_hd44780_busy_timer(
		struct avr_t * avr,
        avr_cycle_count_t when, void * param)
{
	hd44780_t *b = (hd44780_t *) param;
	hd44780_set_flag(b, HD44780_FLAG_BUSY, 0);
	avr_raise_irq(b->irq + IRQ_HD44780_BUSY, 0);
	return 0;
}

...

if (delay) {
	hd44780_set_flag(b, HD44780_FLAG_BUSY, 1);
	avr_raise_irq(b->irq + IRQ_HD44780_BUSY, 1);
	avr_cycle_timer_register_usec(b->avr, delay,
		_hd44780_busy_timer, b);
}
The return value from a timer function specifies when and if it will be called again. The avr_cycle_timer_register() function specifies the delay in clock cycles rather than time. There are also functions to cancel a timer or query its status (sim_cycle_timers.h).

Other features.
For many applications, the few function calls shown above are all that is needed. One other thing an application may need to do is to take full control of execution. That is done with a dedicated run() function. It is required to call the simulator core to execute a burst of instructions (usually one), then check for and handle pending interrupts, timer events and the sleep states. The standard version handles sleep with delays, so that execution occurs in approximately real time. A different run function is needed to run at full speed, as in this example from simavr/simavr/sim/panel.c As before, it needs to be called in a loop until simulation ends.
static int my_avr_run(avr_t *avr)
{
    avr_cycle_count_t sleep;
    uint16_t          new_pc;
if (avr->state == cpu_Stopped)
    return avr->state;

new_pc = avr->pc;
if (avr->state == cpu_Running)
    new_pc = avr_run_one(avr);

// Run the cycle timers, get the suggested sleep time
// until the next timer is due.

sleep = avr_cycle_timer_process(avr);
avr->pc = new_pc;

if (avr->state == cpu_Sleeping) {
    if (!avr->sreg[S_I]) {
        printf("simavr: sleeping with interrupts off, quitting.\n");
        avr_terminate(avr);
        return cpu_Done;
    }
    avr->cycle += 1 + sleep;
}

// Interrupt service.

if (avr->state == cpu_Running || avr->state == cpu_Sleeping)
    avr_service_interrupts(avr);
return avr->state;

}

Interactive debugging also uses a variant run loop. The normal procedure is to copy run_avr:

	if (gdb) {
		avr->state = cpu_Stopped;
		avr_gdb_init(avr);
	}

        ...

                for (;;) {
                        int state = avr_run(avr);
                        if (state == cpu_Done || state == cpu_Crashed)
                                break;
                }

If needed, the debugging run function avr_callback_run_gdb() can be called directly.

An application might also need to implement an additional AVR peripheral, real or virtual. For that, there are functions to monitor reads and writes to memory and I/O register space, such as avr_register_io_write() and avr_core_watch_read(). Interrupts can be raised with avr_raise_interrupt() and controlled using other functions defined in sim_interrupts.h.

About

simavr is a lean, mean and hackable AVR simulator for linux & OSX

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C 98.9%
  • Makefile 1.1%