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



В целом, я смотрел в сторону байт-код машин 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)


  1. Rumlin
    08.10.2015 13:26

    Дороговат TI, к сожалению. Много чего интересного есть, но кусается ценой. Те же простые msp430g2553 по 3$.

    А софт них действительно богат и документирован. Много примеров, разобраться не трудно.


  1. A1ien
    08.10.2015 14:43
    +2

    Есть аналогичный проект под PIC, с компилятором из C- подобного языка, для виртуальной машины заточенной для работы с битовыми операциями, виртуалка реализована посредством стековой машины, из обвязки, для виртуалки присутствуют даже таймера:) Проект сейчас хоститься на assembla.com, но думаю чnо надо перезалить на Github.


    1. intl
      08.10.2015 15:55

      В виртуалке главное ядро процессора, а обвязку вы сами можете описать. Скажем, просто переназначив адресное пространство виртуалки на регистры хардварного таймера. Разные задачи — разная периферия.

      К примеру я использую драйвера дисплея и i2c на хост машине, а пользуюсь ими на виртуалке. При том i2c сугубо софтовый.
      И это, в целом, касается любого проекта. Скажем именно так в smart.js (esp8266) выполнены модули того же i2c или gpio.


      1. A1ien
        08.10.2015 16:11

        Все верно, но есть еще плюшки — например инициализация таймеров из кода виртуалки, плюс — биндить реальный хардварный таймер к виртуалке — чистое расточительство — она в десятки раз медленнее процессора, и времена с которыми она может оперировать тоже несколько отличаются от порядка времен в хост процессоре, так для моих задач нужны были таймера которые отсчитывают секунды, тут одного хардварного таймера достаточно для эмуляции 4х-8и таймеров виртуальных.


  1. Joric
    08.10.2015 17:24

    А интерпретатор какого-нибудь скриптового языка на такой архитектуре вообще невозможно было использовать?


    1. intl
      08.10.2015 21:12

      Да можно все, если вы можете это себе представить.

      А по делу, это «все» влазит в 5кб на AVR. Скриптовый интерпретатор, имхо, будет больше.
      К примеру Cesanta V7 JS, ест значительно больше.
      Micropython думаю тоже значительно больше 5кб, так как для его запуска используют stm32f4

      Задача стояла унифицировать куски кода на разных платформах. В данный момент один и тот же код отлично работает как на stm32f030 так и на atmega8 и stm8s003. Разница лишь в скорости.


      1. Godless
        08.10.2015 22:21

        И все-таки, каков порядок падения скорости?
        в 2 раза? в 10? в 100?
        Хотя бы примерно…


        1. 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 гц


          Все это «очень» на глаз. Так что ближе всего ответ «хз».

          Очень много зависит от архитектуры хост-проца, килограмма оптимизаций, итд.
          В целом медленно, но если требуется описать алгоритм «работы с пользователем» этого оказалось достаточно.


          1. Godless
            09.10.2015 08:27

            Да, спасибо, именно это и хотел услышать.


  1. Alexeyslav
    09.10.2015 14:14
    +1

    Ну да, всё правильно. классика. Каждый программист в своей жизни изобретает как минимум один новый язык программирования.