Маленькие Python для маленьких embedded-программистов: CircuitPython и MicroPython для MeowBit
Энтузиасты из Шэньчжэня, взявшие себе название KittenBot, решили заполнить пустующую нишу «обучающий микрокомпьютер, который как micro: bit, но со всем необходимым для нескучных проектов — уже внутри». Их плата MeowBit, выпущенная в 2018, стоит $40; сохраняет edge connector, совместимый с micro: bit; и добавляет четыре кнопки-«джойстик», полноцветный TFT-экран 160×128, динамик, и силиконовую оболочку с отсеком для аккумулятора — всё, что нужно для создания мини-«геймбоя» размером с кредитную карточку. У MeowBit 96 КБ ОЗУ — до выхода micro: bit v2 это было ещё одним его существенным превосходством — и 2 МБ флеш-памяти, по сравнению с 256 КБ у micro: bit v1 и 512 КБ у micro: bit v2. Игры для MeowBit можно писать на MakeCode Arcade (диалект Scratch от Microsoft), Kittenblock (собственный диалект Scratch от KittenBot), или на MicroPython. На сайте KittenBot есть туториалы по использованию этих трёх языков, но весьма бедные, и увы, только на китайском.
MicroPython создавался для тех, кто привык программировать микроконтроллеры на Си, но хотел быиспользовать при этом синтаксис Python. Процесс разработки организован соответствующим образом: после загрузки MicroPython выполняет файл main.py из флеш-памяти, затем запускает REPL (интерактивную оболочку) на последовательном интерфейсе. Единственный способ запустить новый скрипт — перезагрузка с новым main.py. Единственный способ удалить из памяти переменные и импорты, оставшиеся от отработавшего скрипта — перезагрузка. Единственный способ прервать выполнение скрипта, вошедшего в бесконечный цикл — перезагрузка. Вдобавок, стандартный вывод MicroPython направляется на последовательный интерфейс, а не на экран; чтобы напечатать что-либо на экране, вместо стандартного print
надо использовать FrameBuffer: fb.text(s, x, y); pyb.SCREEN().show(fb)
— с явным заданием координат, без автоматического разбиения на строки и без автоматической прокрутки экрана. Для разработки на Си всё это естественно, но программисты на Python привыкли к намного большему комфорту.
Осознавая это, в 2017 нью-йоркский стартап Adafruit начал разработку собственного форка MicroPython, получившего название CircuitPython. Эта реализация выполняет main.py каждый раз, когда файл с таким названием копируется через USB, и затем «обнуляет» среду, так что переменные из main.py не мешают ни REPL, ни следующему запускаемому скрипту. Чтобы остановить выполнение скрипта, достаточно удалить main.py через USB. В MicroPython нужно проявлять осторожность, чтобы во флеш-память не писали одновременно скрипт и компьютер через USB, иначе файловая система могла повредиться. В CircuitPython же перезапись main.py во время его выполнения — основной сценарий использования, так что ради предосторожности нужно выбрать один из двух вариантов — доступ ко флешу из Python только на чтение и через USB на чтение и запись, либо из Python на чтение и запись и через USB только на чтение, без возможности перезаписи либо удаления main.py. И ещё одна приятная фича — что до перехода в графический режим стандартный вывод дублируется и на последовательный интерфейс, и на экран. С другой стороны, MicroPython позволяет писать на Python обработчики прерываний (с рядом ограничений — например, в них нельзя создавать/удалять объекты на куче и нельзя бросать исключения), а CircuitPython — не позволяет, и не собирается добавлять такую
возможность.
Разница в подходе проявляется и в API. MicroPython стремится к краткости, CircuitPython — к абстракции:
С другой стороны, CircuitPython навязывает программисту многоуровневую абстракцию: Bitmap
произвольной цветовой глубины, позволяющий сэкономить память, когда одновременно используемых цветов не так много; Palette
, превращающая значения Bitmap в конкретные значения цвета, задаваемые в стандартном 24-битном формате, и автоматически конвертируемые в тот формат, с которым работает экран; затем TileGrid
, позволяющая сдвигать и масштабировать несколько спрайтов как одно целое; и наконец Group
, позволяющая переключаться между «стопками спрайтов». Для простых задач, типа отрисовки графика функции, все эти дополнительные абстракции совершенно лишние;, но для разработки игр, скорее всего, программисту на MicroPython пришлось бы самостоятельно разрабатывать нечто аналогичное этой иерархии абстракций.
Самое удивительное в CircuitPython — то, что он занимает не больше памяти, чем MicroPython:
свежезагруженной в MeowBit программе остаётся для работы 55 КБ и 53 КБ соответственно. Одна из причин — то, что большинство стандартных модулей CircuitPython ожидаются во флеш-памяти отдельными файлами, и не загружаются, пока не востребованы. (Таков, например, модуль adafruit_framebuf
, предоставляющий интерфейс стандартного framebuf
из MicroPython.) Полный набор стандартных внешних модулей занимает больше 2 МБ и даже не помещается целиком во флеш-память MeowBit.
Один из моментов, вызванный разницей в подходах, хотелось бы разобрать подробнее: сложно представить игру без музыки и/или звуковых эффектов, но если на время проигрывания звуков игра будет приостанавливаться (как в примерах выше с delay
и sleep
), то играть будет очень неудобно. Как же реализовать фоновый звук в двух вариантах Python?
В MicroPython можно напрямую пернести напрашивающееся низкоуровневое решение — обрабатывать прерывание от таймера: функция play
будет добавлять записи в список music
, а обработчик handle_music
будет обрабатывать их по одной. Ограничения MicroPython не позволяют укорачивать список music
прямо в handle_music
по мере обработки записей, так что придётся пользоваться более низкоуровневыми средствами: продвигать в обработчике указатель next
, и удалять из списка обработанные записи лишь при следующем вызове play
.
# `tim` и `ch` как в примере выше
music = []
countdown = 0
next = 0
# понимает подмножество синтаксиса QBasic PLAY:
# https://en.wikibooks.org/wiki/QBasic/Appendix#PLAY
def play(m):
global music, next
music = music[next:]
next = 0
octave = 1
duration = 75
n = 0
while n < len(m):
note = m[n]
if note >= 'A' and note <= 'G':
freq = [440, 494, 262, 294, 330, 349, 392][ord(note)-ord('A')]
music.append((freq * 2 ** (octave-1), duration * 7 / 8))
music.append((0, duration / 8))
elif note == 'O':
n += 1
octave = int(m[n])
elif note == 'L':
n += 2
l = int(m[n-1:n+1])
duration = 1500 / l
n += 1
def handle_music(t):
global countdown, next
if countdown:
countdown -= 1
if countdown:
return
ch.pulse_width_percent(0)
if next < len(music):
(freq, countdown) = music[next]
next += 1
if freq:
tim.freq(freq)
ch.pulse_width_percent(50)
bg_tim = Timer(1, freq=1000)
bg_tim.callback(handle_music)
CircuitPython же не позволяет писать обработчики прерываний, так что понадобится намного более высокоуровневая реализация.
handle_music
из повторно вызываемого обработчика превращается в генератор — это ещё и упрощает логику кода: включение динамика, задержка, и выключение динамика теперь идут в коде последовательно, так что можно обойтись без глобального countdown
. Кроме того, генератор может сам удалять из music
обработанные записи, так что упрощается и функция play
.# `pwm` как в примере выше
def sleep(duration):
until = monotonic_ns() + duration
while monotonic_ns() < until:
yield
def handle_music():
global music
while True:
if music:
(freq, d) = music[0]
music = music[1:]
if freq:
pwm.frequency = int(freq)
pwm.duty_cycle = 2**15
yield from sleep(d * 1.e6)
pwm.duty_cycle = 0
yield
Но теперь фоновый звук будет проигрываться не сам собой, а только при регулярном «дёрганьи» генератора. Это склоняет к тому, чтобы и остальные игровые процессы реализовать
в виде генераторов; например, заставка игры, прокручивающаяся вверх-вниз до нажатия любой
кнопки, реализуется следующим образом:
play("L08CDEDCDL04ECC")
def scroll():
while True:
while splash.y > max_scroll:
splash.y -= 1
yield from sleep(3.e8)
while splash.y < 0:
splash.y += 1
yield from sleep(3.e8)
def handle_input():
while all(button.value for button in buttons):
yield
for _ in zip(scroll(), handle_input(), handle_music()):
pass
С одной стороны, реализация на MicroPython даёт заметно более качественный звук, потому что обработчик прерывания вызывается точно в заданное время, тогда как в CircuitPython на время перерисовки экрана (порядка 0.15 с) звук «подвисает». С другой стороны, код на CircuitPython легче писать, легче отлаживать и легче расширять, а реализация игровых процессов в виде сопрограмм-генераторов естественна и в отрыве от требований ко звуку.