Mecrisp Forth

Форт для микроконтроллеров

Решил заняться чем-то новым, точнее хорошо забытым старым, и освоить форт на микроконтроллерах.

Перед компилируемыми ЯП форт имеет преимущество за счёт своей интерактивности - писать и отлаживать код можно непосредственно на целевом устройстве не занимаясь каждый раз сборкой и прошивкой.

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

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

Из-за простоты реализации существует огромное множество форт-машин, и выбор какой-то конкретной - дело не простое. При должном опыте можно даже свою написать ;-)

Поковыряв имеющиеся предложения я пока остановился на Mecrisp Forth изначально созданном для архитектуры MSP430, в дальнейшем он был портирован на ARM Cortex M микроконтроллеры под именем Mecrisp-Stellaris. Не смотря на явное указание на конкретное семейство МК от конкретного производителя список поддерживаемых МК гораздо шире.

Среди поддерживаемых МК и пплат разработки оказалась и Nucleo-F401RE, а их у меня как раз парочка завалялась. На ней и буду ставить эксперименты.

Установка Mecrisp-Stellaris

Качаем свежую версию Mecrisp-stellaris. Потребуется именно версия stellaris для МК на Cortex M ядрах.

Распаковываем архив в удобное место, и среди предложенного обилия процессоров входим в каталог stm32f401.

Нам потребуется прошить бинарный файл с прошивкой mecrisp-stellaris-stm32f401.bin в микроконтроллер.

Для этого воспользуемся утилитой st-flash из Open source ST Link tools.

Прежде всего, нужно полностью стереть микроконтроллер. Это обязательный шаг и необходим для очистки совести флеш-памяти МК, иначе Mecrisp подхватит оставшейся в ней мусор, и завалит вас ошибками.

st-flash erase

Следующим шагом, загружаем прошивку в МК:

st-flash write mecrisp-stellaris-stm32f401.bin 0x08000000

Или можно загрузить её же из hex файла:

st-flash --format ihex write mecrisp-stellaris-stm32f401.hex

В этом случае, не требуется указывать адрес памяти, куда будет прошивка загружена.

Если загрузка прошла без ошибок, то теперь у нас в МК крутится форт-машина.

Консоль

Пока нет связи с форт-машиной, она представляет собой не просто чёрный ящик, а чёрный ящик с наглухо заваренными входами и выходами. Поэтому потребуется терминал умеющий взаимодействовать с последовательным портом.

В том же каталоге, что и прошивка есть шелл-скрипт terminal смысл жизни которого заключается в запуске консольного терминала picocom.

Но для запуска picocom его сперва надо установить. В deb-based системах это делается командой:

apt-get install picocom minicom

Обратите внимание, что я кроме picocom ещё устанавливают minicom. Это более сложный терминал последовательного порта, но сейчас нам от него нужно только консольное приложение ascii-xfr которое позволит передавать файлы по последовательному порту.

Теперь можно запустить терминал ./terminal, что бы подключиться к форт-машине в МК.

На самом деле в терминале мы ничего не увидим, машина совершенно не в курсе, что к ней подключились.

Так что начнём с начала и сбросим МК, либо аппаратной кнопкой RESET на плате, либо набрав в терминале reset и нажав <enter>.

После этого, в терминале выскочит информация о версии форт-машины:

Mecrisp-Stellaris 2.3.9 for STM32F401 by Matthias Koch

Впрочем, никто не мешает воспользоваться любым привычным терминалом, хоть консольным, хоть графическим. Но я рекомендую обратить внимание на такие, которые позволяют отредактировать строку для отправки в отдельном поле ввода и отправить её в терминал всю целиком, а не посимвольно. Ибо протокол обмена по последовательному порту у Mecrips не предполагает обширных возможностей для редактирования: максимум - стереть неправильно введённые символы.

Так же хорошей идеей будет воспользоваться специализированным терминалом Folie, который был написан специально для взаимодействия с Mecrisp Forth.

Хоть он и выглядит так же просто, как picocom, но для ввода команд использует библиотеку readline, что позволяет удобно редактировать вводимую строку, а так же пользоваться историей команд.

“Я у мамы программист”

Теперь попробуем сделать что-нибудь простое. Например посчитать известное 2+2*2.

На форте это может выглядеть так:

2 2 2 * + .

Жмём <enter> и получаем ответ:

6  ok.

Если перевести, то что мы проделали, то получится следующая последовательность:

  • Поместить в стек число 2.
  • Поместить в стек число 2.
  • Поместить в стек число 2.
  • Извлечь из стека два числа, перемножить их и поместить результат в стек.
  • Извлечь из стека два числа, сложить их и поместить результат в стек.
  • Извлечь из стека число и напечатать его.

Да, никаких алгебраических записей арифметических действий. Только простые команды, только хардкор.

Изучить основы форта за Y минут, можно на соответствующей странице.

Список ключевых слов доступных в ядре Mecrisp forth можно найти на странице http://mecrisp.sourceforge.net/glossary.htm .

Запускаем пример

В каталоге с бинарными файлами лежит пример взаимодействия с железом МК - blinky.txt. В нём простейшая программа на форте. Точнее даже не программа, а набор определений слов форта и констант.

Что бы загрузить эти определения в МК, нам достаточно отправить этот файл “как есть” по последовательному порту с паузой между строками порядка 50мс (если слать все строки без пауз, форт не успеет их все обработать и мы получим эпик фейл).

Если мы подключались через скрипт terminal, то для отправки файла нужно нажать последовательно Ctrl+A, Ctrl+S и на запрос ввести имя файла.

Но сейчас подключимся через folie:

folie -p /dev/ttyACM0 -t 50ms

Ключ -p указывает последовательный порт, к которому подключена форт-машина, а ключ -t позволяет сэмулировать паузу между строками файла. По умолчанию она 500мс, и файл будет грузится ну очень медленно.

Для загрузки файлов folie предоставляет встроенную команду include <filename>. Указывать её надо с самого начала строки, без пробелов перед ней. Т.е. если в вашей форт-машине определено слово include вызвать его можно поставив перед ним пробел. Для форта это корректный синтаксис, а folie не распознает её как свою команду, и передаст строку напрямую в порт.

include blinky.txt

В результате увидим следующий вывод:

\       >>> include blinky.txt
\       <<<<<<<<<<< blinky.txt (68 lines)
\ done.

И теперь можем исполнить слово blinky для запуска очень полезной мигалки светодиодом.

Я позволил себе вольность и вычистил пример от неиспользуемого кода и снабдил комментариями:

\ LED and button example
\ Базовые адреса GPIO МК
\ Константы задаются с помощью ключевого слова constant
$40020000 constant PORTA_Base \ Шестнадцатиричные числа начинаются с $
$40020800 constant PORTC_Base

\ Регистры GPIO МК
PORTA_BASE $00 + constant PORTA_MODER    \ Reset 0 Port Mode Register - 00=Input  01=Output  10=Alternate  11=Analog
PORTA_BASE $18 + constant PORTA_BSRR     \ WO      Bit set/reset register   31:16 Reset 15:0 Set
        \ +$1C                                     ... is Lock Register, unused

PORTC_BASE $10 + constant PORTC_IDR      \ RO      Input Data Register

\ User LD2: the green LED is a user LED connected to Arduino signal D13 corresponding to
\ MCU I/O PA5 (pin 21) or PB13 (pin 34) depending on the STM32 target.
\ On the Nucleo 401RE, this is PA5.

\ B1 USER: the user button is connected to the I/O PC13 (pin 2) of the STM32
\ microcontroller.

\ Однострочные комментарии начинаются с обратного слеша.
\ Так же комментарии можно указывать в круглых скобках.
\ Обычно они используются для описания стековых диаграмм слов форта.

: button? ( -- ? ) \ Считываем значение кнопки
  1 13 lshift \ Эквивалентно (1<<13)
  PORTC_IDR bit@ \ Считываем бит из адреса памяти (регистра МК)
  not ; \ Инвертируем результат

: led     ( ? -- )
  if
    1 5 lshift \ Эквивалетно (1<<5)
    PORTA_BSRR ! \ Записываем значение в регистр МК. Это установит бит порта
  else
    1 5 lshift 16 lshift \ Эквивалетно ((1<<5)<<16)
    PORTA_BSRR ! \ Записываем значение в регистр МК. Это сбросит бит порта
  then ;

: led_setup ( -- )
  %01 \ Двоичное число в стек
  5 2* \ 5*2 в стек
  lshift \ Битовый сдвиг
  PORTA_MODER bis! ; \ Устанавливаем биты в ячейке памяти (регистре МК)

: led_blink ( delay -- )
  true led \ Включаем светодиод. Слово true помещает на стек значение -1, что соответствует истине.
  dup 0 do loop \ Дублируем значение задержки и запускаем пустой цикл от 0 до значения задержки
  false led \ Выключаем светодиод. Слово false помещает на стек значение 0, что соответствует лжи.
  0 do loop ; \ На вершине стека осталось значение задержки, запускаем пустой цикл с ним.

: blinky ( -- )
  led_setup \ Настраиваем GPIO на выход.
  begin \ Начинаем условный цикл
    \ Проверяем состояние аппаратной кнопки
    button? if
              \ Если нажата - мигаем быстро
              500000 led_blink
            else
              \ Если отпущена - медленно
              1000000 led_blink
            then
  key? until \ Если есть данные с терминала (key? вернула -1), то завершаем цикл.
;

Сохранение определений слов во flash

Определим новое слово для возведения числа в квадрат:

: sqr ( ? -- ? ) dup * ;

Теперь мы можем возводить числа в квадрат:

5 sqr .
25 ok.

Но если использовать аппаратный сброс МК:

reset

Магия перестаёт работать:

5 sqr .
sqr not found.

Дело в том, что по умолчанию слова компилируются в оперативную память и это правильно, что бы не загрязнять флеш не отлаженными словами.

Переключить компиляцию в режим сохранения во flash можно словом compiletoflash, а обратно словом compiletoram.

compiletoflash
: sqr dup * ;
compiletoram
: sqr2 sqr 2* ;

Теперь после сброса МК слово sqr будет доступно, а слово sqr2 - нет.

Толмач с машинного

Среди файлов поставки Mecrisp forth присутствуют файлы common/disassembler-m0.txt и common/disassembler-m3.txt, что есть ни что иное, как дизассемблер для соответствующих архитектур.

Загрузим его:

include ../common/disassembler-m3.txt

О ужас, целых 474 строки (m0 поменьше - 316).

\       >>> include ../common/disassembler-m3.txt
\       <<<<<<<<<<< ../common/disassembler-m3.txt (474 lines)
\ done.

И для эксперимента определим новое слово:

: delay ( ? -- ) 0 do loop ;

Теперь его можно дизассемблировать.

see delay

Результат исполнения:

20002858: B500  push { lr }
2000285A: B430  push { r4  r5 }
2000285C: 2400  movs r4 #0
2000285E: 0035  lsls r5 r6 #0
20002860: CF40  ldmia r7 { r6 }
20002862: 3401  adds r4 #1
20002864: 42AC  cmp r4 r5
20002866: D1FC  bne 20002862
20002868: BC30  pop { r4  r5 }
2000286A: BD00  pop { pc }

Можно переписать определение слова по другому

: delay1 ( ? -- ) begin 1- dup until ;

выиграв три команды:

see delay1
2000297A: B500  push { lr }
2000297C: 3E01  subs r6 #1
2000297E: F847  str r6 [ r7 #-4 ]!
20002980: 6D04  
20002982: 2E00  cmp r6 #0
20002984: CF40  ldmia r7 { r6 }
20002986: D0F9  beq 2000297C
20002988: BD00  pop { pc }

Библиотеки

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

Во-первых, рекомендую ознакомиться с каталогом common в поставке Mecrisp-stellaris. Там много вкусного.

Опять же, вкусняшки можно найти в каталоге конкретного процессора.

Коллекция кода от JeeLabs https://github.com/jeelabs/embello/tree/master/explore/1608-forth

Неофициальная документация по Mecrisp Stellaris Forth.

 
comments powered by Disqus