[Из песочницы] Виртуальные машины и микроконтроллеры
Разрабатывая разные устройства, очень часто получаешь проблему: алгоритм от устройства к устройству местами повторяется, а сами устройства полностью разные. У меня три разрабатываемых устройства, которые местами повторяют функционал друг друга, в них используются три разных процессора (три разные архитектуры), но алгоритм один. Чтобы хоть как-то все унифицировать, было задумано написать минимальную виртуальную машину.
В целом, я смотрел в сторону байт-код машин 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