Виртуальные машины и микроконтроллеры

в 8:38, , рубрики: arduino, diy или сделай сам, mcu, msp430, virtual machine, vm, Виртульная машина, программирование микроконтроллеров, метки: ,

Разрабатывая разные устройства, очень часто получаешь проблему: алгоритм от устройства к устройству местами повторяется, а сами устройства полностью разные. У меня три разрабатываемых устройства, которые местами повторяют функционал друг друга, в них используются три разных процессора (три разные архитектуры), но алгоритм один. Чтобы хоть как-то все унифицировать, было задумано написать минимальную виртуальную машину.

Виртуальные машины и микроконтроллеры - 1

В целом, я смотрел в сторону байт-код машин Java, Lua и других, но весь имеющийся багаж особо переписывать на другой язык не хотелось. Так что с языком определились — Си. Хотя Java или Lua все еще заманчиво звучит. [1][2][3][4].

Следующим критерием шел компилятор. Я в своих проектах чаще всего использую «написанный студентами за печеньки GCC (с) анонимус». Т.е. если описывать свою какую-то архитектуру, к ней бы пришлось еще придумывать всю связку из GCC (Компилер, линковщик и т.д.).

Так как я человек ленивый, искал минимально возможную архитектуру с поддержкой GCC. И ею стала MSP430.

Краткое описание

MSP430 — очень простая архитектура. Она имеет всего 27 инструкций [5] и практически любую адресацию.

Постройку виртуальной машины начал с контекста процессора. Контекстом процессора в операционных системах называют структуру, которая полностью описывает состояние процессора. А состояние данного виртуального процессора описывается через следующее:

  • Текущую команду
  • Регистры
  • Опционально состояние регистров прерываний
  • Опционально содержимое ОЗУ и ПЗУ

Регистров у MSP430 — 16. Из этих 16 регистров первые 4 используются как системные регистры. Скажем, нулевой регистр отвечает за текущий указатель на выполняемую команду из адресного пространства (Счетчик команд).

Более детально про регистры можно почитать в оригинальном user guide msp430x1xxx [6]. Кроме регистров есть еще содержимое адресного пространства — ОЗУ, ПЗУ. Но так как просто держать в памяти «Хост-машины» (машина, выполняющая код виртуальной машины) память виртуальной машины, за частую, нету смысла — используются callback.

Данное решение позволяет исполнять «совершенно левые» программы на процессорах с гарвардской архитектурой (читай AVR [7][8]), беря программу из внешних источников (Скажем, i2c память или SD карта).

Также в контексте процессора имеется описание регистров прерываний (SFR). Наиболее точно система прерываний MSP430 описана в [6] п. 2.2.
Но в описываемой виртуальной машине я немного отошел от оригинала. В оригинальном процессоре флаги прерываний находятся в регистрах периферии. В данном случае прерывания описывается в SFR регистрах.

Периферия процессора описывается так же, через callback-и, что позволяет создавать свою собственную периферию по желанию.

Следующим пунктом процессора является мультиплексор команд. Мультиплексор команд выполняет отдельная функция. Мультиплексор выбирает из слова команды саму команду, адресацию источника и приемника и выполняет действие выбранной команды.

Отдельными функциями описывается адресация источника (SRC) и приемника.

Как этим пользоватся

В папке examples из репозитория проекта [9] есть примеры для следующих процессоров:

  • STM8 для компилятора IAR
  • STM8 для компилятора SDCC
  • STM32 для компилятора Keil armcc
  • AVR для компилятора GCC

В файле Cpu.h выполняется настройка процессора.

Описание настроек ниже:

  • RAM_USE_CALLBACKS — Указывает, использовать ли вызовы (callbacks) вместо отдельных массивов в контексте процессора. Использовать ли вызовы для работы с RAM (Вызовы cpu.ram_read, cpu.ram_write)
  • ROM_USE_CALLBACKS — Использовать ли вызовы для работы с ROM (вызов cpu.rom_read)
  • IO_USE_CALLBACKS — Использовать ли вызовы для работы с переферией (вызовы cpu.io_read, cpu.io_write), если 0 то функции работы с переферией должны быть описаны в функции msp430_io из файла cpu.c
  • RAM_SIZE — Размер ОЗУ (RAM), конечный адрес автоматически пересчитывается, исходя из этого параметра
  • ROM_SIZE — Размер ПЗУ (ROM), начальный адрес автоматически пересчитывается, исходя из этого параметра
  • IRQ_USE — Указывает, будут ли использованы прерывания; если 1, то прерывания включены
  • HOST_ENDIANESS — Указывает на порядок байт хост-контроллера (контроллера который выполняет виртуальную машину). Архитектуры AVR,X86,STM32 являются little-endian, STM8 — big-endian
  • DEBUG_ON — указывает будет ли использоваться отладка. Отладка выполняется через fprintf — stderr

Использование библиотеки начинается с подключения cpu.c и cpu.h в проект.

#include "cpu.h"

Далее идет обьявление контекста процессора. В зависимости от использования параметров *_USE_CALLBACKS будет меняться код объявления контекста.

для всех *_USE_CALLBACKS = 1 объявления контекста процессора будет выглядеть следующим образом:

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

Где переменные *_cb принимают указатели на функции (см. примеры).

Наоборот же, для *_USE_CALLBACKS = 0, объявления будут выглядеть так:

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

Далее идет инициализация контекста через функцию:

msp430_init(&cpu_context);

И выполнение по одной инструкции за раз через функцию:

while(1)
    msp430_cpu(&cpu_context);

Callback-и для работы с адресным пространством выглядят следующим образом:

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

Адреса для IO передаются относительно 0 адресного пространства (т.е. если в программа виртуальной машины обратится к P1IN, который назначен на адрес 0x20, то и в функцию будет передан адрес 0x20).

Напротив, адреса для RAM и ROM передаются относительно начальных точек (например, при обращение по адресу 0xfc06 и началом ПЗУ по адресу 0xfc00 в функцию будет передан адрес 0x0006. Т.е адрес от 0 до RAM_SIZE, 0 — ROM_SIZE)

Это позволяет использовать внешнюю память, к примеру I2C (что и без того замедляет процессор).

Как завершение

Полностью проект не завершен. Он работает, тестовые прошивки работают на ура. Но большинство компиляторов практически не используют разные специфические команды (скажем, Dadd — десятичное сложение источника и приёмника (с переносом)). Так что говорить о 100% совместимости с реальными процессорами не приходится.

Естественно, на одну команду виртуальной машины приходится с два десятка операций хост-машины, поэтому говорить о каких-либо скоростных характеристиках бессмысленно.

Исходники проекта и более расширенное описание доступно на bitbucket.org [9].

Буду рад, если кому-нибудь пригодится данный проект.

[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] ru.wikipedia.org/wiki/MSP430
[6] www.ti.com/lit/ug/slau049f/slau049f.pdf
[7] ru.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] ru.wikipedia.org/wiki/AVR
[9] bitbucket.org/intl/msp430_vm

Автор: intl

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js