Máquinas virtuales y microcontroladores.

Al desarrollar diferentes dispositivos, a menudo aparece un problema: el algoritmo de dispositivo a dispositivo se repite en algunos lugares, y los dispositivos en sí son completamente diferentes. Tengo tres dispositivos en desarrollo que en algunos lugares repiten la funcionalidad entre ellos, usan tres procesadores diferentes (tres arquitecturas diferentes), pero solo hay un algoritmo. Para unificar de alguna manera todo, se planeó escribir una máquina virtual mínima.



En general, miré hacia el código de bytes de Java, Lua y otras máquinas, pero realmente no quería reescribir todo el equipaje disponible en otro idioma. Entonces decidimos el idioma - C. Aunque Java o Lua todavía suena atractivo. [1] [2] [3] [4].

El siguiente criterio fue el compilador. En mis proyectos uso con mayor frecuencia "escrito por estudiantes para cookies GCC (c) anonymus". Aquellos. si describe su arquitectura, tendría que crear un montón de GCC (compilador, enlazador, etc.).

Como soy una persona perezosa, estaba buscando la arquitectura más pequeña posible con soporte GCC. Y se convirtió en el MSP430.

Breve descripción


MSP430 es una arquitectura muy simple. Tiene solo 27 instrucciones [5] y casi cualquier direccionamiento.

La construcción de la máquina virtual comenzó con el contexto del procesador. El contexto del procesador en los sistemas operativos es una estructura que describe completamente el estado del procesador. Y el estado de este procesador virtual se describe a través de lo siguiente:

  • Equipo actual
  • Registros
  • Estado opcional de registros de interrupción
  • Contenidos opcionales de RAM y ROM

Los registros del MSP430 son 16. De los 16 registros, los primeros 4 se utilizan como registros del sistema. Digamos, un registro nulo es responsable del puntero actual al comando que se ejecuta desde el espacio de direcciones (Contador de comandos).

Puede leer más sobre los registros en la guía de usuario original msp430x1xxx [6]. Además de los registros, también está el contenido del espacio de direcciones: RAM, ROM. Pero dado que es fácil mantener la "máquina host" (la máquina que ejecuta el código de la máquina virtual) en la memoria de la máquina virtual, por lo general, no tiene sentido: se utiliza la devolución de llamada.

Esta solución le permite ejecutar programas "completamente a la izquierda" en procesadores con arquitectura Harvard (lea AVR [7] [8]), tomando el programa de fuentes externas (por ejemplo, memoria i2c o tarjeta SD).

También en el contexto del procesador hay una descripción de los registros de interrupción (SFR). El sistema de interrupción MSP430 se describe con mayor precisión en [6], cláusula 2.2.
Pero en la máquina virtual descrita, me alejé un poco del original. En el procesador original, los indicadores de interrupción están en los registros periféricos. En este caso, las interrupciones se describen en registros SFR.

La periferia del procesador se describe de la misma manera, mediante devolución de llamada, que le permite crear sus propios periféricos a voluntad.

El siguiente elemento del procesador es el comando multiplexor. El comando multiplexor realiza una función separada. El multiplexor selecciona el comando en sí de la palabra de comando, direccionando la fuente y el receptor, y realiza la acción del comando seleccionado.

Las funciones separadas describen el direccionamiento de origen (SRC) y el receptor.

Cómo usarlo


En la carpeta de ejemplos del repositorio del proyecto [9] hay ejemplos para los siguientes procesadores:
  • STM8 para el compilador IAR
  • STM8 para el compilador SDCC
  • STM32 para el compilador Keil armcc
  • AVR para el compilador GCC


En el archivo Cpu.h, el procesador está configurado.

Descripción de la configuración a continuación:

  • RAM_USE_CALLBACKS: indica si se deben usar llamadas (devoluciones de llamada) en lugar de matrices individuales en el contexto del procesador. Si se deben usar llamadas para trabajar con RAM (llamadas cpu.ram_read, cpu.ram_write)
  • ROM_USE_CALLBACKS: si se deben usar llamadas para trabajar con ROM (llame a cpu.rom_read)
  • IO_USE_CALLBACKS: si se deben usar llamadas para trabajar con la periferia (llamadas cpu.io_read, cpu.io_write), si es 0, las funciones para trabajar con la periferia se deben describir en la función msp430_io del archivo cpu.c
  • RAM_SIZE: tamaño de RAM (RAM), la dirección final se recalcula automáticamente en función de este parámetro
  • ROM_SIZE: tamaño de ROM (ROM), la dirección de inicio se recalcula automáticamente en función de este parámetro
  • IRQ_USE - Indica si se usarán interrupciones; si 1, entonces las interrupciones están habilitadas
  • HOST_ENDIANESS: indica el orden de bytes del controlador host (el controlador que ejecuta la máquina virtual). Las arquitecturas AVR, X86, STM32 son little-endian, STM8 son big-endian
  • DEBUG_ON: indica si se utilizará la depuración. La depuración se realiza a través de fprintf - stderr


El uso de la biblioteca comienza conectando cpu.c y cpu.h al proyecto.

#include "cpu.h"

El siguiente es el anuncio del contexto del procesador. Dependiendo del uso de los parámetros * _USE_CALLBACKS, el código de declaración de contexto cambiará.

para todas las declaraciones de contexto de procesador * _USE_CALLBACKS = 1 se verán así:

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
    };


Donde * _cb las variables aceptan punteros de función (ver ejemplos).

Por el contrario, para * _USE_CALLBACKS = 0, las declaraciones se verán así:

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

Lo siguiente es la inicialización del contexto a través de la función:

msp430_init(&cpu_context);

Y ejecutando una instrucción a la vez a través de una función:

while(1)
    msp430_cpu(&cpu_context);

Las devoluciones de llamada para trabajar con espacio de direcciones se ven así:

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);

Las direcciones para IO se transmiten en relación con el espacio de direcciones 0 (es decir, si el programa de máquina virtual accede a P1IN, que se asigna a la dirección 0x20, entonces la dirección 0x20 también se transferirá a la función).

Por el contrario, las direcciones para RAM y ROM se transmiten en relación con los puntos de inicio (por ejemplo, al acceder a la dirección 0xfc06 e iniciar la ROM en 0xfc00, la dirección 0x0006 se pasará a la función. Es decir, la dirección es de 0 a RAM_SIZE, 0 - ROM_SIZE)

Esto permite el uso de memoria externa , por ejemplo I2C (que ya ralentiza el procesador).

Como completar


Completamente el proyecto no está terminado. Funciona, prueba de firmware funciona con una explosión. Pero la mayoría de los compiladores prácticamente no utilizan diferentes comandos específicos (por ejemplo, Dadd es la suma decimal de la fuente y el receptor (con guiones)). Por lo tanto, no hay necesidad de hablar de compatibilidad 100% con procesadores reales.

Naturalmente, hay alrededor de dos docenas de operaciones de la máquina host por cada comando de máquina virtual, por lo que no tiene sentido hablar de ninguna característica de velocidad.

Las fuentes del proyecto y una descripción más extensa están disponibles en bitbucket.org [9].

Me alegraría si este proyecto es útil para alguien.

[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] es .wikipedia.org / wiki / AVR
[9] bitbucket.org/intl/msp430_vm

Source: https://habr.com/ru/post/es385009/


All Articles