Маленькие Python для маленьких embedded-программистов: CircuitPython и MicroPython для MeowBit

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


На Хабре уже немало писали про обучающий микрокомпьютер BBC micro:bit, который в 2016 раздали всем британским школьникам, и сейчас он продаётся по $15. С прошлого года появились в продаже и micro:bit v2, в которых ОЗУ расширена с 16 КБ до 128 КБ. Неизменным остался форм-фактор: две кнопки для ввода, матрица 5х5 светодиодов для вывода, всё что сверх этого – подключайте через 25-контактный edge connector. Очевидно, что создатели задумывали micro:bit не как самостоятельное устройство, а как «мозг» для более сложного проекта со внешними датчиками, индикаторами, релюшками, сервоприводами и т.п. – этакий «детский Arduino». Энтузиасты из Шэньчжэня, взявшие себе название 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 – к абстракции:
MicroPython CircuitPython
from pyb import Pin pin = Pin(«BTNA», pull=Pin.PULL_UP) print(pin.value())
import board, digitalio pin = digitalio.DigitalInOut(board.BTNA) pin.pull = digitalio.Pull.UP print(pin.value)
from pyb import Pin, Timer, delay # нужные номера таймера и канала # берутся из даташита tim = Timer(4, freq=440) ch = tim.channel(3, Timer.PWM,                  pin=Pin('BUZZ'),                  pulse_width=15909)                  # 50% от 14 MHz / 440 delay(100) ch.pulse_width_percent(0)
import board, pwmio, time pwm = pwmio.PWMOut(board.BUZZ,                    frequency=440,                    duty_cycle=2**15)                    # 50% от  2**16 time.sleep(0.1) pwm.duty_cycle = 0
import pyb, framebuf fbuf = bytearray(160*128*2) fb = framebuf.FrameBuffer(fbuf, 160, 128,                           framebuf.RGB565) tft = pyb.SCREEN() fb.pixel(123, 45, 0x07E0) # blue tft.show(fb)
import board, displayio fb = displayio.Bitmap(160, 128, 2) palette = displayio.Palette(2) palette[1] = 0xFF0000 # blue splash = displayio.Group() splash.append(displayio.TileGrid(fb,               pixel_shader=palette)) tft = board.DISPLAY tft.show(splash) fb[123, 45] = 1 tft.refresh()
Во втором примере разница в подходе наиболее наглядна: MicroPython предоставляет очень простую и прозрачную модель «создал массив значений пикселей, отправил его целиком на экран», навязывающую программисту довольно неудобный формат RGB565, потому что именно с таким форматом работает экран MeowBit (ST7735). Недостаток этой модели в том, что буфер 160х128х2 занимает 40 КБ – почти всю память, остающуюся свободной после загрузки MicroPython. Держать два таких буфера и отображать их поочерёдно – нет никакой возможности. С другой стороны, 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 легче писать, легче отлаживать и легче расширять, а реализация игровых процессов в виде сопрограмм-генераторов естественна и в отрыве от требований ко звуку.

Источник: habr.com

Комментарии: