В прошлой статье я рассказывал, как делал Arduboy WOW с применением модулей, но помимо Arduboy мне еще почти также сильно интересовала платформа Microsoft MakeCode Arcade и мне захотелось повторить тоже самое, но с ней. Правда пока я решил ограничиться созданием устройства на макетной плате, чтобы подробнее изучить эту платформу и не отвлекаться на остальное. (Или в будущем сделаю на этой базе полноценную приставку и напишу еще одну статью.)

MakeCode Arcade очень сильно отличается от Arduboy, но в то же время они в сущности обо одном. В двух словах, MakeCode Arcade это не одна приставка, а несколько десятков приставок, игры для которых пишутся в своем редакторе, в браузере. В этой статье я постараюсь рассказать как эта система работает изнутри или как у них получилось сделать столь заманчивую идею реальностью на примере моего устройства на макетной плате, которое я назвал MakeCode Arcade Beast.

Рекомендации к чтению

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

Если вы хотите сразу перейти к изучению MakeCode Arcade, то начинайте читать с «Настройка устройства или как обуздать много приставок».

  1. Введение

  2. Разработка

    1. Документация

    2. Модули

    3. Проблемы и трудности макетной платы

    4. Настройка устройства или как обуздать много приставок

    5. Сложности программирования

  3. Видеобзор

  4. Выводы

  5. Заключение

Введение

Arduboy WOW с игрой Karateka
Arduboy WOW с игрой Karateka

В прошлой статье, про Arduboy WOW, я очень подробно расписал разработку DIY приставки, но возможно вы её не читали, поэтому сейчас расскажу необходимое, чтобы мы были на одной волне.

Под DIY приставкой обычно понимается какое-нибудь устройство собранное самому на макетной плате или на сделанной в домашних условиях печатной плате. Получается обычно громоздкое и не до конца проработанное устройство, например, с лишнем usb разъемом, без аккумуляторной батареи, без корпуса или корпус, напечатанный на 3д принтере, с пищалкой, а не динамиком, в основном из всего того, что есть под рукой. И это на самом деле здорово, потому что оценивается не конечный результат, а удовольствие от процесса, которое на самом деле огромное.

В итоге, в 2016 году выходит Arduboy, которые все эти шаги в производстве DIY приставок выполняет на должном уровне и получается очень маленькая, компактная и главное полноценная приставка. Но на самом деле, чтобы я не говорил, но рассуждаю постфактум — приставка просто всем понравилась и кажется поэтому хорошо продалась, хотя конкретики я не знаю.

Как мне кажется ключевое в этом все то, что в таких приставках используется микроконтроллер. Другими словами, если вы начнете искать в сети приставки, сделанные с микроконтроллером, то сразу найдете Arduboy или какие-нибудь DIY приставки на макетной плате. Хотелось бы думать, что такие приставки способны на существование, но по большей части представляют интерес только для разработчиков и они делаются для разработчиков. Игры, мягко говоря, там слабенькие для нынешнего геймера, в основном потому что негде развернутся, но при этом на Arduboy полно игр, которые выжимают максимум из устройства.

Playdate часто сравнивают с Arduboy. Фото взято из промоматериалов
Playdate часто сравнивают с Arduboy. Фото взято из промоматериалов

Еще интересный факт, что часто DIY приставки делают на ESP32 или на мощном микроконтроллере, чтобы запускать на нем эмуляторы, например, NES или Game Boy, но Arduboy пошел иным путем и является оригинальным устройством. Возможно, кому-то покажется это очень важным пунктом и это действительно была в свое время новая приставка, со своими особенностями и главное уникальными играми. Сейчас, наверно, продолжением этой эволюции скорее всего будет Playdate, который, кстати, тоже интересно было создать с помощью подхода из модулей, описанного в данной статье, но там один экран стоил 6 тыс. рублей, поэтому не горю желанием быстрее туда ворваться. (А почему я про экран заговорил, так потому что там он какой-то особенный, очень точный и монохромный, если интересно, можете изучить этот вопрос, потому что это выходит за рамки данной статьи.)

Среда разработки MakeCode Arcade: слева симулятор (приставка), а справа код (блоки)
Среда разработки MakeCode Arcade: слева симулятор (приставка), а справа код (блоки)

Подходя к теме текущей статьи, в какой-то момент успех проектов по типу Arduboy заметили в Microsoft и создали на самом деле, что-то по своему уникальное и отличное, используя обычную формулу DIY приставок. Они сделали платформу для обучения программирования MakeCode Arcade, т.е. не самому приставку, но подождите, сейчас расскажу. Заходите вы на сайт MakeCode Arcade, создаете проект игры, пишете код, а конечный результат проверяете на игровой приставке. Т.е. это обучающая платформа, где вы программируете игры для игровой приставки. В браузере это симулятор, но там же есть кнопку запустить игру на настоящей приставке, её достаточно подключить к компьютеру, выбрать из списка и нажать кнопку “Загрузить”.

Выбор всех приставок на одном экране
Выбор всех приставок на одном экране

Самое главное в этом все, что MakeCode Arcade это проект с десятками приставками, там их действительно много и на любой вкус. Если говорить сразу, то не могу сказать, что это такой же успешный проект как Arduboy и игры что там хорошие, но с технической точки зрения мне он кажется заслуживает внимания, например, хотя бы понять как они сделали возможным запуск одной и той же игры на множестве разных приставок, с разной периферией и микроконтроллерами. А других подобных проектов я и не знаю.

На этом (в)водная часть заканчивается и переходим к погружение в изучение этой платформы. И начнем с разработки.

Разработка

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

Документация

Принципиальная схема примера из pxt-arcade-hardware-designs репозитория очень подробно объясняет нюансы
Принципиальная схема примера из pxt-arcade-hardware-designs репозитория очень подробно объясняет нюансы

Вся необходимая информация, на самом деле есть на сайте MakeCode Arcade (Creating your own Arcade hardware). Там же вы найдете пример готовой приставки (pxt-arcade-hardware-designs).

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

Maketato — DIY приставка для MakeCode Arcade на RP2040
Maketato — DIY приставка для MakeCode Arcade на RP2040

Вот, например, указано, что поддерживаются микроконтроллеры на Cortex M4F и M0+, т.е. можно создать устройство на RP2040, или, если говорить по-другому, можно взять Raspberry Pi Pico и собрать такую приставку, что и сделано в проекте Maketato (по ссылке подробная инструкция, как её повторить).

Мне же было интересно попробовать STM микроконтроллеры, поэтому выбрал STM32F401 и STM32F412, но остановился на первом, а последний не пригодился.

Модули

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

Фото макетной платы, где пописан каждый модуль
Фото макетной платы, где пописан каждый модуль

И так, приставка состоит из следующих модулей:

  1. Главный модуль

  2. Модуль заряда

  3. Дисплей

  4. Клавиатура

  5. Динамик

  6. Акселерометр

  7. Модуль со светодиодами

  8. Вибромотор

Я старался, чтобы модули работали и от 3В и от 5В, чтобы можно было переиспользовать в будущих проектах, и вроде это у меня получилось, если что-то не упустил.

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

Главный модуль

Принципиальная схема главного модуля
Принципиальная схема главного модуля
Фото главного модуля
Фото главного модуля

Идея была сделать модуль только с микроконтроллером, хотел еще добавить регулятор напряжения, но он плохо влезал на плату, поэтому все остальное было решено вынести в другие модули. Но обязательно добавил элементы для программирования: разъем SWD, кнопка BOOT и кнопка RESET. Еще есть светодиод на питании и с ним интересная история, потому что устройство может работать в режиме сна, а этот светодиод всю батарею съест, но это не беда, выпаять его не проблема, а вот плату переделывать — да.

Используется микроконтроллер STM32F401RET6 (в LQFP64 корпусе), потому что так проще паять, но при этом расстояние между ножками 0.2 мм и у меня удачно получилось сделать такую плату на фрезерном станке. Хотя одна дорожка чуть-чуть отвалилась, но это оказалось совершенно не критичным.

Предполагалось, что потом заменю микроконтроллер на STM32F412. Он указан в документации как рекомендуемый микроконтроллер, если собираетесь использовать 320х240 дисплей, но я решил не заниматься этим, потому что по факту разрешение с 160х120 растягивается до 320х240 и не понятно зачем это нужно, если только хочется большой экран и быстрое обновление картинки. А вот прирост производительности от замены микроконтроллера будет небольшой, около 12 МГц, на STM32F401 84 МГц, а на STM32F412 96 МГц, хотя STM32F412 может работать на 100 МГц, но из-за USB частоту снизили до 96 МГц (это два раза по 48, частота используемая для режимов Low-Speed или Full-Speed).

Модуль заряда

Принципиальная схема модуля заряда
Принципиальная схема модуля заряда
Фото модуля заряда. Ему больше всего досталось
Фото модуля заряда. Ему больше всего досталось

Модуль заряда можно было бы взять из проекта Arduboy WOW и я так сделал, но решил его заметно доработать. Здесь используется микросхема TP4056, где ток заряда ограничен до 0.5А; если подключен USB, то используется он, а не аккумулятор (A03401 мосфет в схеме) — это было еще в модуле из Arduboy WOW. В этом модуле добавил выключатель питания, защиту от статики (PRTR5V0U2X), линейный регулятор (AP2112K) и делитель напряжения (с RC-цепочкой) на положительный вывод аккумулятора, чтобы показывать текущей уровень заряда в игре (спойлер, что заявлено, но не поддерживается платформой). Самое главное, это, конечно, выключатель питания, потому что прошлый модуль заряда я боялся куда либо класть из-за того, что контакты могут закоротить, потому что там его не было.

Еще важный момент, это регулятор питания, здесь используется AP2112K, в отличие от AMS1117, так как у него ток покоя 60 мкА, а у AMS1117 13 мА, потому что устройство может входить в глубокий сон и с AMS1117 аккумулятор очень быстро разрядится.

Проговорю еще момент с делителем напряжения. Там используются 47 кОм резисторы, взял побольше, чтобы аккумулятор сильно не разрядился, т.к. они постоянно пропускают ток, даже если питание выключено. Была идея, сделать, чтобы они пропускали ток, только когда питание включено, но не довел эту идею до конца и оставил все как есть, а по-хорошему лучше доработать. Но т.к. программной поддержки показывать напряжения аккумулятора нет, то возникают мысли может и полностью отказаться от этой затеи.

Дисплей

Принципиальная схема дисплея
Принципиальная схема дисплея
Фото дисплея
Фото дисплея

Для MakeCode Arcade можно выбрать из ST7735 или ILI9341 дисплеев, один 160х120, другой 320х240. Я хотел и то и то попробовать, но пока остановился на ST7735, причины я описал выше, в разделе главного модуля.

Здесь с одной стороны просто, но я решил усложнить и добавил возможность, что модуль мог бы работать от 3В или 5В, для этого используется преобразователь уровней 74HC4050D. Еще для регулирования подсветки установлен мосфет.

1.8" TFT Display Breakout and Shield — устройство Adadfruit в схему которого я подглядывал, источник
1.8" TFT Display Breakout and Shield — устройство Adadfruit в схему которого я подглядывал, источник

Для создания этого модуля я подсматривал в модуль от Adafruit. Adafruit еще не раз помогали мне в создании модулей, т.к. они на этом специализируются и выкладывают все в открытый доступ, можете к ним присмотреться.

Клавиатура

Принципиальная схема клавиатуры
Принципиальная схема клавиатуры
Фото клавиатуры
Фото клавиатуры

С одной стороны здесь все просто, но есть нюансы. Это конденсатор между землей и RESET, т.к. из-за длинных проводов сигнал либо затухал, но он просто не работал, возможно стоит везде поставить конденсаторы, но конкретно проблема возникла только с RESET.

Другой момент, это кнопка MENU, которая замыкается на MENU_ACTIVE, это сделано для того, что в MakeCode Arcade кнопка меню может пробуждать устройство из сна и активный уровень это +3V3. У меня не получилось добиться, чтобы по этой кнопке устройство просыпалось, поэтому MENU_ACTIVE у меня тоже GND, и эта кнопка используется как все остальные.

Напишу еще чуть про MENU. Обычно кнопки конфигурируются так, что активный уровень это земля, т.е. подтягиваются программно до единицы и надо немного напрячься, чтобы отдельная кнопка работала наоборот, это можно сделать, но в документации не описана. Эту проблему я подробнее опишу ниже, в программной части.

Динамик

Принципиальная схема динамика
Принципиальная схема динамика
Фото динамика
Фото динамика

Этот модуль полностью перекачивал из Arduboy WOW, но еще добавлена RC-цепочка, чтобы сгладить ШИМ сигнал и добавлен вывод выключения модуля. Здесь используется усилитель звука 8002A, который спокойно выдает звук на динамик до 3Вт.

В MakeCode Arcade есть выбор выводить звук между ЦАП-ом или ШИМ-ом. Из-за просты и отсутствия ЦАП, я выбрал использовать ШИМ сигнал и как раз RC-цепочка добавлена, чтобы все сглаживать.

Добавил вывод выключения для того, чтобы модуль выключался, когда устройство входит в сон. И этот вывод действительно работает: если сделать громкость 0, то модуль полностью выключается через этот вывод.

Акселерометр

Принципиальная схема акселерометра
Принципиальная схема акселерометра
Фото акселерометра
Фото акселерометра

Я еще до этого не работал с акселерометром, поэтому обязательно захотел его добавить. Из особенностей, здесь используется преобразователь уровней на мосфетах для I2C интерфейса, что указывает на то, что модель работает от 3В или 5В. Если посмотрите на схему, то увидите много выводов, но на деле пригодились только SCL, SDA, SD0 (отвечает за выбор адреса I2C), CS, INT1 (прерывание от акселерометра к микроконтроллеру). Вывод типа ADC1, как понимаю, это АЦП встроенный в акселерометр, т.е. у него еще есть 3 канала АЦП.

Стоит добавить, что расстояние между дорожками тут тоже 0.2 мм и у меня получилось удачно фрезеровать эту плату, но возможно не с первой попытки, уже не помню точно.

Акселерометр от Adafruit, источник
Акселерометр от Adafruit, источник

Этот модуль я также подсмотрел у Adafruit.

Модуль со светодиодами

Принципиальная схема модуля со светодиодами
Принципиальная схема модуля со светодиодами
Фото модуля со светодиодами
Фото модуля со светодиодами

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

Вибромотор

Принципиальная схема вибромотора
Принципиальная схема вибромотора
Фото вибромотора
Фото вибромотора

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

Проблемы и трудности макетной платы

Сзади макетной платы, кстати, все аккуратно
Сзади макетной платы, кстати, все аккуратно

За кадром осталось то, как я все спаял на макетной плате, что оказалось на самом деле очень долго и монотонно. Еще при этом что-то не работало, а что-то сделал не так.

Например, кнопка RESET не работала из-за длинных проводов и пришлось добавить конденсатор на 10 мкФ.

Другая проблема длинных проводов, это дисплей, он банально зависал, если указать частоту SPI в 20 МГц. Пришлось выбрать пины близкие к модулю и делать провода короче, но он все равно иногда зависал, но снизив частоту до 8-16 МГц, зависания пропали.

Забыл вывести RESET на главном модуле, поэтому припаялся к нему проводками. Еще забыл вывести кнопку MENU, точнее она должна быть особенной, которая может выводить микроконтроллер из сна, тоже припаялся проводками. Но это со всеми бывает, ничего особенного.

Кусачки для ногтей приспособленные для зачищения проводов
Кусачки для ногтей приспособленные для зачищения проводов

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

Настройка устройства или как обуздать много приставок

С железной частью разобрались, теперь перейдем к изучению платформы. Начнем с того как настроить получившиеся устройство к работе, заодно узнаем как MakeCode Arcade решила проблему поддержки множества приставок.

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

Но немного о загрузчике, чтобы у вас появились нужные ассоциации. Возможно вы встречали плату Raspberry Pi Pico на микроконтроллере RP2040, так вот если зажать кнопку BOOT и подсоединить эту плату к компьютеру, то она определяется как флешка и если перетащить на эту флешку файл в формате uf2, то она запрограммируется этим файлом. В MakeCode Arcade используется именно этот загрузчик, особенно для RP2040, а на микроконтроллерах, где его нет, например STM32F401, написан свой, который работает по тому же самому принципу и с теми же самыми uf2.

Еще один интересный момент, что для самописного загрузчика, который для STM32F401, файлы конфигурации записываются в сам загрузчик, а вот как же быть для RP2040, где так сделать не получится? Для этих целей используется последние 4КБ флеш памяти, которые нужно прописать вручную.

В документации описан формат конфигурационного файла похожего на ini, но я его не использовал, а использовал конфигурацию из исходников загрузчика. Там как раз есть конфигурация для универсальной платы на F401.

Вот такой получился у меня конфиг (немного грязный и не причесанный)
#ifndef BOARD_H
#define BOARD_H

#define OSC_FREQ 8
#define USBDEVICESTRING "Arcade F401"
#define USBMFGSTRING "Contoso Ltd."
#define BOARD_FLASH_SECTORS 8
#define BOARD_FLASH_SIZE (512 * 1024)

#ifdef DEFINE_CONFIGDATA
__attribute__((section(".config"))) __attribute__((used)) //
const uint32_t configData[] = {
    /* CF2 START */
    513675505, 539130489, // magic
    63, 100,  // used entries, total entries
    1, 0x2d, // PIN_ACCELEROMETER_INT = PC13
    2, 0x18, // PIN_ACCELEROMETER_SCL = PB08
    3, 0x17, // PIN_ACCELEROMETER_SDA = PB07
    4, 0x20, // PIN_BTN_A = PC00
    5, 0x21, // PIN_BTN_B = PC01
    13, 0x10, // PIN_LED = PB00
    18, 0x1e, // PIN_MISO = PB14
    19, 0x1f, // PIN_MOSI = PB15
    21, 0x3, // PIN_RX = PA03
    // 23, 0x1d, // PIN_SCK = PB13
    // 24, 0x18, // PIN_SCL = PB08
    // 25, 0x17, // PIN_SDA = PB07
    26, 0x26 | 0x110000, // PIN_SPEAKER_AMP = PC06
    28, 0x2, // PIN_TX = PA02
    32, 0x13, // PIN_DISPLAY_SCK = PB03
    // 33, 0x6, // PIN_DISPLAY_MISO = PA06
    34, 0x15, // PIN_DISPLAY_MOSI = PB05
    35, 0x1c, // PIN_DISPLAY_CS = PB12
    36, 0x14, // PIN_DISPLAY_DC = PB04
    37, 0xa0, // DISPLAY_WIDTH = 160
    38, 0x80, // DISPLAY_HEIGHT = 128
    39, 0x020180, // DISPLAY_CFG0 = 0x80
    40, 0x603, // DISPLAY_CFG1 = 0x603
    41, 0x08, // DISPLAY_CFG2 = 0x16
    43, 0x2c, // PIN_DISPLAY_RST = PC12
    44, 0x19, // PIN_DISPLAY_BL = PB09
    // 45, 0x0, // PIN_SERVO_1 = PIN_D8
    // 46, 0x1, // PIN_SERVO_2 = PIN_D9
    47, 0x4, // PIN_BTN_LEFT = PA04
    48, 0x29, // PIN_BTN_RIGHT = PC09
    49, 0x07, // PIN_BTN_UP = PA07
    50, 0x11, // PIN_BTN_DOWN = PB01
    51, 0x00, // PIN_BTN_MENU = PA00
    55, 0x01, // PIN_LED1 = PA01
    56, 0x02, // PIN_LED2 = PA02
    57, 0x03, // PIN_LED3 = PA03
    58, 0x05, // PIN_LED4 = PA05
    60, 0x16, // PIN_JACK_TX = PB06
    // 64, 0x26, // PIN_JACK_PWREN = PC06
    65, 0x8, // PIN_JACK_SND = PA08
    // 66, 0x10, // PIN_JACK_BUSLED = PB00
    67, 0x11, // PIN_JACK_COMMLED = PB01
    70, 0x32, // ACCELEROMETER_TYPE = LIS3DH
    // 74, 0x23, // PIN_BTN_MENU2 = PC03
    75, 0x24 , // PIN_BATTSENSE = PC04
    76, 0x2e, // PIN_VIBRATION = PC14
    77, 0x25, // PIN_PWREN = PC5
    81, 0x13, // CFG_ACCELEROMETER_SPACE
    // 151, 0x2, // PIN_D1 = PA02
    // 152, 0x3, // PIN_D2 = PA03
    153, 0x1f, // PIN_D3 = PB15
    154, 0x1e, // PIN_D4 = PB14
    // 155, 0x1d, // PIN_D5 = PB13
    156, 0x18, // PIN_D6 = PB08
    157, 0x17, // PIN_D7 = PB07
    // 158, 0x0, // PIN_D8 = PA00
    159, 0x1, // PIN_D9 = PA01
    // 160, 0x25, // PIN_D10 = PC05
    161, 0x2b, // PIN_D11 = PC11
    202, 0,
    204, 0x80000, // FLASH_BYTES = 0x80000
    205, 0x18000, // RAM_BYTES = 0x18000
    208, 0x59ea3b60, // BOOTLOADER_BOARD_ID = 0x59ea3b60
    209, 0x57755a57, // UF2_FAMILY = STM32F401
    210, 0x10, // PINS_PORT_SIZE = PA_16
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
    /* CF2 END */
};
#endif

#endif /* BOARD_H */
Как я компилировал прошивку

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

Проект собирается командой make BOARD=f401, но при чистом проекте возникают ошибки вида:

  BUILD   lib/stm32/f0
  BUILD   lib/stm32/f1
  BUILD   lib/stm32/f2
  BUILD   lib/stm32/f3
/bin/sh: 1: /bin/sh: 1: Syntax error: Unterminated quoted string
Syntax error: Unterminated quoted string
/bin/sh: 1: Syntax error: Unterminated quoted string
make[1]: *** [Makefile:62: lib/stm32/f0] Error 2
make[1]: *** Waiting for unfinished jobs....
make[1]: *** [Makefile:62: lib/stm32/f1] Error 2
make[1]: *** [Makefile:62: lib/stm32/f2] Error 2
/bin/sh: 1: Syntax error: Unterminated quoted string
make[1]: *** [Makefile:62: lib/stm32/f3] Error 2
  BUILD   lib/stm32/f4
/bin/sh: 1: Syntax error: Unterminated quoted string
make[1]: *** [Makefile:63: lib/stm32/f4] Error 2
make: *** [Makefile:146: libopencm3/include/libopencm3/stm32/f4/nvic.h] Error 2

Здесь ошибки в Makefile, вот diff старого и исправленного

m039@DESKTOP:~/Code/uf2-stm32f-new$ diff libopencm3/Makefile ../uf2-stm32f/libopencm3/Makefile
28c28
< SRCLIBDIR:= $(subst $(space),\$(space),$(realpath lib))
---
> SRCLIBDIR:= $(realpath lib)
62,63c62,63
<       @printf "  BUILD   $@\n";
<       $(Q)$(MAKE) --directory=$@ SRCLIBDIR="$(SRCLIBDIR)"
---
>       @printf "  BUILD   $@ $(SRCLIBDIR) $(MAKE) \n";
>       $(Q)$(MAKE) --directory="$@" SRCLIBDIR=$(SRCLIBDIR)

Конфигурационный файл очень простой, первое число параметр, второе его значение. Например49, 0x15, // PIN_BTN_UP = PB05 означает кнопка вверх весит на пине PB05. Эта кнопка будет подтянута к логической единице, а активный уровень будет 0 или земля. Все хорошо, но как сделать наоборот, чтобы активный уровень был 1? Первое число, параметр, что оно значит легко находится в исходниках, а вот второй, значение, практически нет. Что туда записывать вы не узнаете у ИИ-чат ботов, потому что в документации этого нет и поэтому они придумывают ответы из воздуха. Пришлось найти и изучать исходники.

Перед тем, как я дам ссылки на исходники, давайте узнаем, что в итоге у нас используется. Мы уже знаем, что здесь есть загрузчик с поддержкой uf2 файлов, в который зашивается этот конфигурационный файл. А дальше запускается игра написанная с помощью фреймворка CODAL. Другими словами, в конфигурационном файле кнопка верх находится на пине PB05 и CODAL позволяет проверить ли нажата эта кнопка через класс Button в конструктор который передается данные из конфига, т.е. PB05. И, например, проверка, нажата ли эта кнопка происходит через button.isPressed(), внутри есть устранение дребезка, в любом случае получается абстрактно.

Конструктор Button
Button::Button(Pin &pin, uint16_t id, ButtonEventConfiguration eventConfiguration, ButtonPolarity polarity, PullMode mode) : _pin(pin), pullMode(mode)
{
    this->id = id;
    this->eventConfiguration = eventConfiguration;
    this->downStartTime = 0;
    this->sigma = 0;
    this->polarity = polarity;

    // For backward compatibility, always perform demand activation.
    this->isPressed();
}

Другими словами, CODAL это уровень абстракции, он так и расшифровывается Component-Oriented Device Abstraction Layer, и на примере класса Button это легко видно как он может выглядеть. (И легко можно понять, что этот фреймворк может работать и с другим микроконтроллером.) Теперь глядя в исходник вы можете понять, что в конструкторе Button есть еще дополнительные поля, нам нужен ButtonPolarity. Вы наверно подумаете, что дальше все элементарно, но нет. В репозитории CODAL вы не найдете ничего про то, как парсится или составлен конфиг, только функционал классов.

Но ответ близко. Прямых исходников, из которых создается игра нет, но есть исходники редактора (pxt-arcade или pxt-common-packages), в котором она пишется. В этих исходниках, если поискать, то можно найти формат полей из конфигурационного файла:

Код конфигурации получения класса кнопки
Button *getButtonByPin(int pin, int flags) {
    unsigned highflags = (unsigned)pin >> 16;
    if (highflags & 0xff)
        flags = highflags & 0xff;

    pin &= 0xffff;

    auto cpid = DEVICE_ID_FIRST_BUTTON + pin;
    auto btn = (Button *)lookupComponent(cpid);
    if (btn == NULL) {
        auto pull = PullMode::None;
        if ((flags & 0xf0) == 0x10)
            pull = PullMode::Down;
        else if ((flags & 0xf0) == 0x20)
            pull = PullMode::Up;
        else if ((flags & 0xf0) == 0x30)
            pull = PullMode::None;
        else
            oops(3);
        // GCTODO
        btn = new Button(*lookupPin(pin), cpid, DEVICE_BUTTON_ALL_EVENTS,
                                 (ButtonPolarity)(flags & 0xf), pull);
    }
    return btn;
}

Вот тут вы можете видеть, как используется класс Button и откуда берутся эти поля из параметра flags. Так понимаю аргумент flags, это и есть то, что передается в конфиге, вторым параметром. В итоге, чтобы изменить полярность, нужно добавить 0x10000, получаем: 49, 0x15 | 0x10000, // PIN_BTN_UP = PB05.

Подытожим все сказанное. Для поддержки множества приставок используется загрузчик, в который зашивается файл конфигурации и по этой конфигурации игра понимает что и где есть в приставке. Приставка может использовать разный микроконтроллер, а игра может написана на Javascript, Python или на блоках, чтобы все это соединить вместе используется фреймворк CODAL, а точнее уровень абстракции, который транслирует высокоуровневые конструкции языка, например, Javascript в обычный код, но скорее всего это делает не через виртуальную машину, как язык Java, а просто переводит в С++, что я точно не знаю, но додумал самостоятельно.

Если захотите углубить эти знания или проверить мои гипотезы, то можете попробовать написать расширения для MakeCode Arcade, которое добавляет поддержку какого-то функционал железа через С++ в Javascript. Такие расширения я видел.

Сложности программирования

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

Если вам интересна эта тема, то можете самостоятельно зайти на сайт MakeCode Arcade и попробовать написать что-то простое, например вывести на экран спрайт, подвигать его. Можете сделать это с помощью языка блоков, так вы лучше всего поймете как происходит программирование, но я воспользуюсь Javascript и распишу только самое интересно, что мне приглянулось. Ну и все просчеты платформы, само собой.

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

Окно редактора Javascript кода, открыта документация
Окно редактора Javascript кода, открыта документация

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

Окно редактора ресурсов. Открыт рисунок шарика для теста акселерометра
Окно редактора ресурсов. Открыт рисунок шарика для теста акселерометра

Не забудьте обратит внимание не только на код, но там есть разделы, где можно нарисовать спрайт или написать мелодию. Т.е. это полноценная среда, по типу PICO8, где вы можете создать полностью игру не выходя из этой среды. Для детей, думаю очень классно, но на примере создания мелодии видно, что никаких настроек нет, кроме расставить ноты и выбрать нужный звук и поэтому все это серьезно воспринимать не стоит. С другой стороны, если вы захотите большего, то вам так и так пришлось бы это все самостоятельно добавлять. Вот на Arduboy ничего такого нет и там уже выкручивайся как хочешь.

Давайте перейдем к примерам кода. Я, как и говорил, использую Javascript, но скажу большее, здесь есть поддержка Typescript, а это уже круто.

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

interface GameScene {
    create(): void;
    destroy(): void;
    onUpdate(): void;
}

create вызывается, когда экран выбран, onUpdate выполняется каждый кадр, destroy когда экран закрыт. Интерфейс хорош своим минимализмом.

Этот интерфейс используется в небольшой системе меню, которую я написал, но здесь не привожу, чтобы не отвлекать от основной темы, но всё есть в разделе «Исходники» ниже.

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

1. Тест вибромоторчика

Тест 1. Я для наглядности добавил картинки к тестам, толку от них думаю не много, но зато веселее
Тест 1. Я для наглядности добавил картинки к тестам, толку от них думаю не много, но зато веселее

Давайте пробежимся по тестовым экранам. Вот, например, как происходит включение вибромоторчика:

class VibrationScene implements GameScene {
    text: TextSprite | undefined;
    aWasPressed: boolean = false;

    constructor() { }

    public create(): void {
        this.text = textsprite.create("Press A to vibrate", 0, 1);
        this.text.setMaxFontHeight(6);
        this.text.setPosition(160 / 2, 120 / 2);
        this.aWasPressed = true;
    }

    public destroy(): void {
        if (this.text != undefined) {
            this.text.destroy();
        }
    }

    public onUpdate(): void {
        if (!this.aWasPressed && controller.A.isPressed()) {
            this.aWasPressed = true;
            let p = pins.pinByCfg(76)
            if (p != null) {
                p.digitalWrite(true)
                pause(200)
                p.digitalWrite(false)
            }
            return;
        }

        if (this.aWasPressed && controller.A.isPressed()) {
            return;
        }

        this.aWasPressed = false;
    }
}

Здесь на самом деле уже видны проблемы, например, потому что если хотите использовать вибромоторчик, то подключаете расширение controller и используете функцию controller.vibrate. Но я почему-то использую конструкцию из pinByCfg и digitalWrite. Это из-за того, что расширение controller конфликтует с расширением arcade-text, которое используется для вывода текста, через textsprite.create. Вот такие пироги. И что самое неприятное, если нажать на документацию к расширению controller, то выкинет на 404 страницу, а если открыть репозиторий расширения arcade-text, то увидите, что оно в архиве. Другими словами, MakeCode Arcade старая платформа и все уже устарело, а новое не появилось.

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

2. Тест индикации батареи

Тест 2
Тест 2

Возможно вам будет интересно посмотреть на следующий тест, это индикация состояния батареи.

Код теста индикации состояния батареи
class BatteryScene implements GameScene {
    samples: number[]
    welcomeText: TextSprite | undefined;
    batteryText1: TextSprite | undefined;
    batteryText2: TextSprite | undefined;

    enabled: boolean;
    pin: AnalogInOutPin | null;

    public create(): void {
        this.samples = []
        this.pin = pins.pinByCfg(75) as AnalogInOutPin;
        if (this.pin == null) {
            this.welcomeText = textsprite.create("Can't read battery pin", 0, 1);
        } else {
            this.welcomeText = textsprite.create("Measuring battery...", 0, 1);
        }

        this.welcomeText.setMaxFontHeight(6);
        this.welcomeText.setPosition(160 / 2, 120 / 2);
    }

    public destroy(): void {
        if (this.welcomeText != undefined) {
            this.welcomeText.destroy();
            this.welcomeText = undefined;
        }

        if (this.batteryText1 != undefined) {
            this.batteryText1.destroy();
            this.batteryText1 = undefined;
        }

        if (this.batteryText2 != undefined) {
            this.batteryText2.destroy();
            this.batteryText2 = undefined;
        }
    }

    public onUpdate(): void {
        if (this.pin == null)
        {
            return;
        }

        let batteryMV = this.pin.analogRead()
        if (this.samples.length < 50) {
            this.samples.push(batteryMV);
            return;
        }

        if (this.welcomeText != undefined) {
            this.welcomeText.destroy();
            this.welcomeText = undefined;
        }

        this.samples.shift()
        this.samples.push(batteryMV);

        batteryMV = this.samples.reduce((sum, val) => sum + val, 0) / this.samples.length;
        batteryMV = Math.trunc(batteryMV / 1024 * 3.3 * 2 * 1000)

        if (this.batteryText1 != undefined) {
            this.batteryText1.destroy();
            this.batteryText1 = undefined;
        }

        if (this.batteryText2 != undefined) {
            this.batteryText2.destroy();
            this.batteryText2 = undefined;
        }

        let t1 = "BATTSENSE: " + batteryMV + " mV";
        let t2
        if (batteryMV > 0) {
            let percent = Math.map(batteryMV, 3400, 4100, 0, 100)
            percent = Math.clamp(0, 100, Math.floor(percent))
            t2 = "CHARGE: " + percent + "%"
        } else {
            t2 = "STATUS: SIMULATOR (0mV)"
        }

        this.batteryText1 = textsprite.create(t1, 0, 1);
        this.batteryText1.setPosition(160 / 2, 120 / 2 - (this.batteryText1.height / 2 + 5));
        this.batteryText1.left = 10

        this.batteryText2 = textsprite.create(t2, 0, 1);
        this.batteryText2.top = this.batteryText1.bottom + 10;
        this.batteryText2.left = this.batteryText1.left
    }
}

Здесь собираются 50 замеров, затем берется среднее и показывается сколько это в отношении к опорному напряжению. Проблема в том, что у меня не получилось сделать так, что это число было стабильно и оно скачет очень хаотично. Но основная проблема здесь в том, что этот функционал должен был быть встроен в платформу, а его нет, приходится городить каждому разработчику что-то свое, но думаю никто просто ничего не делает. А по-хорошему хотелось бы функцию вида battery.level и все.

Поэтому проблема та же, находится пин через pinByCfg и используется он напрямую, т.е. считывается значение через ацп с помощью pin.analogRead.

3. Тест светодиодов

Тест 3
Тест 3
Код теста светодиодов
class LEDScene implements GameScene {
    text: TextSprite | undefined;
    wasDestroed: boolean = false;

    constructor() { }

    public create(): void {
        this.wasDestroed = false;
        this.text = textsprite.create("LED are blinking", 0, 1);
        this.text.setMaxFontHeight(6);
        this.text.setPosition(160 / 2, 120 / 2);

        let ledIds = [55, 56, 57, 58, 77]
        let loop = true;
        let self = this;

        forever(function () {
            if (!loop) {
                pause(100);
                return;
            }

            for (let id of ledIds) {
                let p = pins.pinByCfg(id)
                if (p != null) {
                    p.digitalWrite(true)
                    pause(200)
                    p.digitalWrite(false)
                }
            }

            pause(500)

            if (self.wasDestroed) {
                loop = false;

                for (let id of ledIds) {
                    let p = pins.pinByCfg(id);
                    if (p != null) {
                        p.digitalWrite(false)
                    }
                }
            }
        })
    }

    public destroy(): void {
        if (this.text != undefined) {
            this.text.destroy();
        }
        this.wasDestroed = true;
    }

    public onUpdate(): void {
    }
}

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

Здесь интересна конструкция forever. Она запускает в отдельном потоке, если можно так сказать, исполнение функции, которая будет на повторе. Другими словами моргание светодиодов будет работать без остановок. Но гвоздь здесь в том, что функция такая есть, но вот функции, чтобы остановить её нету. Поэтому такой вот хак, причем, как я понял, официальный.

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

4. Тест акселерометра

Тест 4
Тест 4
Код теста акселерометра
class AccelerometerScene implements GameScene {
    ball: Sprite;

    constructor() { }

    public create(): void {
        scene.setBackgroundColor(1)
        this.ball = sprites.create(assets.image`ball`, SpriteKind.Player);
        this.ball.setPosition(160 / 2, 120 / 2);
    }

    public destroy(): void {
        sprites.destroy(this.ball);
    }

    public onUpdate(): void {
        let dx = controller.acceleration(ControllerDimension.X) / 10;
        let dy = controller.acceleration(ControllerDimension.Y) / 10;
        this.ball.setVelocity(dx, dy);
        if (this.ball.left < 0) {
            this.ball.left = 0;
            this.ball.setVelocity(0, 0);
        }

        if (this.ball.right > 160) {
            this.ball.right = 160;
            this.ball.setVelocity(0, 0);
        }

        if (this.ball.top < 0) {
            this.ball.top = 0;
            this.ball.setVelocity(0, 0);
        }

        if (this.ball.bottom > 120) {
            this.ball.bottom = 120;
            this.ball.setVelocity(0, 0);
        }
    }
}

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

Здесь вы можете видеть, как используются спрайты. Спрайт я сделал в редакторе, присвоил ему имя и вызвал в коде. На самом деле хороший подход. Спрайт еще можно задавать прямо в коде, используя символы, но мне было это неудобно.

5. Тест музыки

Тест 5
Тест 5
Код теста музыки
    text: TextSprite | undefined;
    aWasPressed: boolean = false;

    constructor() { }

    public create(): void {
        this.text = textsprite.create("Press A to play a song", 0, 1);
        this.text.setMaxFontHeight(6);
        this.text.setPosition(160 / 2, 120 / 2);
        this.aWasPressed = true;
    }

    public destroy(): void {
        if (this.text != undefined) {
            this.text.destroy();
        }

        music.stopAllSounds();
    }

    public onUpdate(): void {
        if (!this.aWasPressed && controller.A.isPressed()) {
            this.aWasPressed = true;
            music.play(music.createSong(assets.song`twinkle`), music.PlaybackMode.InBackground)
            return;
        }

        if (this.aWasPressed && controller.A.isPressed()) {
            return;
        }

        this.aWasPressed = false;
    }

Хотел я сказать, что это тоже хороший тест, но есть нюанс, что запустить музыку можно, но вот остановить конкретную мелодию нельзя, только все разом.

Музыку я также создал в редакторе и здесь вызываю её в коде.

Исходники

Некоторым читателям захочется посмотреть все остальное и это сделать очень просто. Вот ссылка на код. Либо, что более экзотично, вы можете открыть MakeCode Arcade и перетащить в окно браузера картинку ниже, в которой зашит исходник. Да, похоже как в PICO8.

Исходник проекта, картинка, вы можете её перетащить в окно среды разработки, должен импортироваться
Исходник проекта, картинка, вы можете её перетащить в окно среды разработки, должен импортироваться

Видеобзор

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

Выводы

MakeCode Arcade уникальная платформа, хотя бы потому, что объединяет много приставок в одну платформу и на самом деле лояльна или рада видеть новую приставку в своем ряду, потому что добавить такую не сложно. Среда программирования хорошая и в ней все есть, если вы только начинаете изучать программирования. Мне кажется, для детей или школьников классная платформа, но давайте перейдем к отрицательным её качествам:

  • Абстракция железа есть, но она слабо развита. Это видно на примере дисплея: вы можете выбрать дисплей с 320x240, но это по сути 160х120 растянутый на весь экран — можно, но зачем?

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

  • Допустим вы пишете игру на Javascript и я не могу понять, как вы будете её оптимизировать, как выжимать максимум из приставки. Нет инструментария.

  • Многие библиотеки и игры устарели, новых практически нет. Но всё устаревает и с этим ничего не поделать, хочется, правда, чтобы платформа была поживее.

Заключение

Мне понравилось знакомство с Arduboy и MakeCode Arcade, это две платформы с DIY приставками, которые меня больше всего интересовали. Осталось еще сделать для MakeCode Arcade полноценную приставку, где все модули будут на одной печатной плате и напечатать корпус, но эту затею я пока отложил, в пользу других проектов. Но в опросе будет соотвествующий пункт, поэтому если вы этого хотите, мне будет интересно узнать.

Другие DIY приставки я не собирался делать, но если вам интересна эта серия, где я собираю приставку из самодельный модулей и изучаю платформу, то в опрос добавил пункты про ESPBoy и Playdate (интересно, а как ручку делать?). Хотелось, конечно, заказать печатную плату, а не только делать все на макетной плате, но тогда статья растянется еще на 1-3 месяца и энтузиазм может сильно упасть. Поэтому решил все разбивать на две части и делать вторую, если будет интерес.

Если вам интересно, что я делаю, то присоединяйтесь в мою группу Планета M039 во ВКонтакте. Там я размещаю новости и делюсь своими мыслями, правда, стараюсь придерживаться тематики игр и электронных устройств, но подумываю писать обо всем, что мне интересно.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Чтобы вам хотелось видеть в следующих статьях?
0%Сделать полноценную приставку для MakeCode Arcade Beast (заказать печатную плату, напечатать корпус)0
0%Сделать Playdate на макетной плате и изучить его0
0%Сделать ESPBoy на макетной плате и изучить его0
0%Больше хотелось бы полноценную печатную плату, а не только макетную плату с модулями0
0%Если я сделаю полноценную приставку и размещу объявление на Авито, вы бы купили? Arduboy WOW, MakeCode Arcade Beast?0
0%В статье хотелось побольше или подробнее о схемотехники0
0%В статье хотелось побольше или подробнее о программировании0
Никто еще не голосовал. Воздержался 1 пользователь.