Virtual machines and microcontrollers

When developing different devices, you often get a problem: the algorithm from device to device is repeated in places, and the devices themselves are completely different. I have three devices under development that in some places repeat the functionality of each other, they use three different processors (three different architectures), but there is only one algorithm. In order to somehow unify everything, it was planned to write a minimal virtual machine.



In general, I looked towards the bytecode of the Java, Lua and other machines, but I didn’t really want to rewrite all the available baggage into another language. So we decided on the language - C. Although Java or Lua still sounds attractive. [1] [2] [3] [4].

The next criterion was the compiler. In my projects, I most often use "written by students for cookies GCC (c) anonymus." Those. if you describe your architecture, you would have to come up with the whole bunch of GCC (compiler, linker, etc.).

Since I'm a lazy person, I was looking for the smallest possible architecture with GCC support. And it became the MSP430.

Short description


MSP430 is a very simple architecture. It has only 27 instructions [5] and almost any addressing.

The construction of the virtual machine began with the context of the processor. The context of the processor in operating systems is a structure that fully describes the state of the processor. And the state of this virtual processor is described through the following:

  • Current team
  • Registers
  • Optional state of interrupt registers
  • Optional contents of RAM and ROM

The registers of the MSP430 are 16. Of the 16 registers, the first 4 are used as system registers. Say, a null register is responsible for the current pointer to the command being executed from the address space (Command counter).

You can read more about registers in the original user guide msp430x1xxx [6]. In addition to registers, there is also the contents of the address space - RAM, ROM. But since it is easy to keep the “Host Machine” (the machine executing the virtual machine code) in the memory of the virtual machine, for frequent, there is no sense - callback is used.

This solution allows you to execute "completely left" programs on processors with Harvard architecture (read AVR [7] [8]), taking the program from external sources (say, i2c memory or SD card).

Also in the context of the processor is a description of interrupt registers (SFRs). The MSP430 interrupt system is most accurately described in [6], clause 2.2.
But in the described virtual machine, I slightly moved away from the original. In the original processor, interrupt flags are in the peripheral registers. In this case, interrupts are described in SFR registers.

The processor periphery is described in the same way, via callback, which allows you to create your own peripherals at will.

The next processor item is the command multiplexer. The command multiplexer performs a separate function. The multiplexer selects the command itself from the command word, addressing the source and receiver, and performs the action of the selected command.

Separate functions describe source addressing (SRC) and receiver.

How to use it


In the examples folder from the project repository [9] there are examples for the following processors:
  • STM8 for the IAR compiler
  • STM8 for SDCC compiler
  • STM32 for Keil armcc compiler
  • AVR for GCC Compiler


In the Cpu.h file, the processor is configured.

Description of settings below:

  • RAM_USE_CALLBACKS - Indicates whether to use calls (callbacks) instead of individual arrays in the processor context. Whether to use calls for work with RAM (calls cpu.ram_read, cpu.ram_write)
  • ROM_USE_CALLBACKS - Whether to use calls to work with ROM (call cpu.rom_read)
  • IO_USE_CALLBACKS - Whether to use calls to work with the periphery (calls cpu.io_read, cpu.io_write), if 0 then the functions for working with the periphery should be described in the function msp430_io from the file cpu.c
  • RAM_SIZE - RAM size (RAM), the end address is automatically recalculated based on this parameter
  • ROM_SIZE - ROM size (ROM), the starting address is automatically recalculated based on this parameter
  • IRQ_USE - Indicates whether interrupts will be used; if 1, then interrupts are enabled
  • HOST_ENDIANESS - Indicates the byte order of the host controller (the controller that runs the virtual machine). The architectures AVR, X86, STM32 are little-endian, STM8 are big-endian
  • DEBUG_ON - indicates whether debugging will be used. Debugging is done via fprintf - stderr


Using the library starts by connecting cpu.c and cpu.h to the project.

#include "cpu.h"

Next is the announcement of the processor context. Depending on the use of the * _USE_CALLBACKS parameters, the context declaration code will change.

for all * _USE_CALLBACKS = 1 processor context declarations will look like this:

msp430_context_t cpu_context =
    {
        .ram_read_cb = ram_read,
        .ram_write_cb = ram_write,
        .rom_read_cb = rom_read,
        .io_read_cb = io_read,
        .io_write_cb = io_write
    };

Where * _cb variables accept function pointers (see examples).

Conversely, for * _USE_CALLBACKS = 0, the declarations will look like this:

msp430_context_t cpu_context =
    {
         .rom = { /* hex program */ },
    };

Next is the context initialization through the function:

msp430_init(&cpu_context);

And executing one instruction at a time through a function:

while(1)
    msp430_cpu(&cpu_context);

Callbacks for working with address space look like this:

uint16_t io_read(uint16_t address);
void io_write(uint16_t address,uint16_t data);
uint8_t ram_read(uint16_t address);
void ram_write(uint16_t address,uint8_t data);
uint8_t rom_read(uint16_t address);

Addresses for IO are transmitted relative to 0 address space (i.e. if the virtual machine program accesses P1IN, which is assigned to address 0x20, then the address 0x20 will be transferred to the function).

On the contrary, the addresses for RAM and ROM are transmitted relative to the starting points (for example, when accessing the address 0xfc06 and starting the ROM at 0xfc00, the address 0x0006 will be passed to the function. That is, the address is from 0 to RAM_SIZE, 0 - ROM_SIZE)

This allows the use of external memory , for example I2C (which already slows down the processor).

How to complete


Completely project not completed. It works, test firmware works with a bang. But most compilers practically do not use different specific commands (say, Dadd is the decimal addition of source and receiver (with hyphenation)). So there’s no need to talk about 100% compatibility with real processors.

Naturally, there are about two dozen operations of the host machine per one virtual machine command, so it makes no sense to talk about any speed characteristics.

Project sources and a more extensive description are available at bitbucket.org [9].

I will be glad if this project is useful to anyone.

[1] dmitry.gr/index.php?r=05.Projects&proj=12.%20uJ%20-%20a%20micro%20JVM
[2] www.harbaum.org/till/nanovm/index.shtml
[3]www.eluaproject.net
[4] code.google.com/p/picoc
[5] en.wikipedia.org/wiki/MSP430
[6] www.ti.com/lit/ug/slau049f/slau049f.pdf
[7] en.wikipedia.org/wiki/%D0%93%D0%B0%D1%80%D0%B2%D0%B0%D1%80%D0%B4%D1%81%D0%BA%D0%B0%D1 % 8F_% D0% B0% D1% 80% D1% 85% D0% B8% D1% 82% D0% B5% D0% BA% D1% 82% D1% 83% D1% 80% D0% B0
[8] en .wikipedia.org / wiki / AVR
[9] bitbucket.org/intl/msp430_vm

Also popular now: