В целом, я смотрел в сторону байт-код машин 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
Комментарии (10)
A1ien
08.10.2015 14:43+2Есть аналогичный проект под PIC, с компилятором из C- подобного языка, для виртуальной машины заточенной для работы с битовыми операциями, виртуалка реализована посредством стековой машины, из обвязки, для виртуалки присутствуют даже таймера:) Проект сейчас хоститься на assembla.com, но думаю чnо надо перезалить на Github.
intl
08.10.2015 15:55В виртуалке главное ядро процессора, а обвязку вы сами можете описать. Скажем, просто переназначив адресное пространство виртуалки на регистры хардварного таймера. Разные задачи — разная периферия.
К примеру я использую драйвера дисплея и i2c на хост машине, а пользуюсь ими на виртуалке. При том i2c сугубо софтовый.
И это, в целом, касается любого проекта. Скажем именно так в smart.js (esp8266) выполнены модули того же i2c или gpio.A1ien
08.10.2015 16:11Все верно, но есть еще плюшки — например инициализация таймеров из кода виртуалки, плюс — биндить реальный хардварный таймер к виртуалке — чистое расточительство — она в десятки раз медленнее процессора, и времена с которыми она может оперировать тоже несколько отличаются от порядка времен в хост процессоре, так для моих задач нужны были таймера которые отсчитывают секунды, тут одного хардварного таймера достаточно для эмуляции 4х-8и таймеров виртуальных.
Joric
08.10.2015 17:24А интерпретатор какого-нибудь скриптового языка на такой архитектуре вообще невозможно было использовать?
intl
08.10.2015 21:12Да можно все, если вы можете это себе представить.
А по делу, это «все» влазит в 5кб на AVR. Скриптовый интерпретатор, имхо, будет больше.
К примеру Cesanta V7 JS, ест значительно больше.
Micropython думаю тоже значительно больше 5кб, так как для его запуска используют stm32f4
Задача стояла унифицировать куски кода на разных платформах. В данный момент один и тот же код отлично работает как на stm32f030 так и на atmega8 и stm8s003. Разница лишь в скорости.Godless
08.10.2015 22:21И все-таки, каков порядок падения скорости?
в 2 раза? в 10? в 100?
Хотя бы примерно…intl
08.10.2015 22:36+1Ответ хз, не устроит? верно?
Пример с миганием простого светодиода с задержкой из цикла (i=10000; while(--i)) дает частоту мигания:
- AVR gcc -o2 16 Mhz ~ 0.4~0.5 гц
- STM8S0 — iar -os 16 Mhz ~ 0.3~0.4 гц
- STM32F0 keil -o3 96 Mhz ~ 10 гц
Все это «очень» на глаз. Так что ближе всего ответ «хз».
Очень много зависит от архитектуры хост-проца, килограмма оптимизаций, итд.
В целом медленно, но если требуется описать алгоритм «работы с пользователем» этого оказалось достаточно.
Alexeyslav
09.10.2015 14:14+1Ну да, всё правильно. классика. Каждый программист в своей жизни изобретает как минимум один новый язык программирования.
Rumlin
Дороговат TI, к сожалению. Много чего интересного есть, но кусается ценой. Те же простые msp430g2553 по 3$.
А софт них действительно богат и документирован. Много примеров, разобраться не трудно.