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

В прошлой статье, про Arduboy WOW, я очень подробно расписал разработку DIY приставки, но возможно вы её не читали, поэтому сейчас расскажу необходимое, чтобы мы были на одной волне.
Под DIY приставкой обычно понимается какое-нибудь устройство собранное самому на макетной плате или на сделанной в домашних условиях печатной плате. Получается обычно громоздкое и не до конца проработанное устройство, например, с лишнем usb разъемом, без аккумуляторной батареи, без корпуса или корпус, напечатанный на 3д принтере, с пищалкой, а не динамиком, в основном из всего того, что есть под рукой. И это на самом деле здорово, потому что оценивается не конечный результат, а удовольствие от процесса, которое на самом деле огромное.
В итоге, в 2016 году выходит Arduboy, которые все эти шаги в производстве DIY приставок выполняет на должном уровне и получается очень маленькая, компактная и главное полноценная приставка. Но на самом деле, чтобы я не говорил, но рассуждаю постфактум — приставка просто всем понравилась и кажется поэтому хорошо продалась, хотя конкретики я не знаю.
Как мне кажется ключевое в этом все то, что в таких приставках используется микроконтроллер. Другими словами, если вы начнете искать в сети приставки, сделанные с микроконтроллером, то сразу найдете Arduboy или какие-нибудь DIY приставки на макетной плате. Хотелось бы думать, что такие приставки способны на существование, но по большей части представляют интерес только для разработчиков и они делаются для разработчиков. Игры, мягко говоря, там слабенькие для нынешнего геймера, в основном потому что негде развернутся, но при этом на Arduboy полно игр, которые выжимают максимум из устройства.

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

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

Самое главное в этом все, что MakeCode Arcade это проект с десятками приставками, там их действительно много и на любой вкус. Если говорить сразу, то не могу сказать, что это такой же успешный проект как Arduboy и игры что там хорошие, но с технической точки зрения мне он кажется заслуживает внимания, например, хотя бы понять как они сделали возможным запуск одной и той же игры на множестве разных приставок, с разной периферией и микроконтроллерами. А других подобных проектов я и не знаю.
На этом (в)водная часть заканчивается и переходим к погружение в изучение этой платформы. И начнем с разработки.
Разработка
Изучение платформы можно начать сразу с браузера, написать программу и проверить её на симуляторе — если вы хотите купить одну из таких приставок и присматривается, то это будет хороший вариант перед покупкой, но меня больше всего интересовало сделать свою приставку и проверить все на железе. Поэтому начнем сразу с наиболее интересного, с разработки.
Документация

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

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

И так, приставка состоит из следующих модулей:
Главный модуль
Модуль заряда
Дисплей
Клавиатура
Динамик
Акселерометр
Модуль со светодиодами
Вибромотор
Я старался, чтобы модули работали и от 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. Еще для регулирования подсветки установлен мосфет.

Для создания этого модуля я подсматривал в модуль от 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.
Модуль со светодиодами


А этот модуль из записок сумасшедшего, т.к. по сути это 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. В этом плане среда для программирования сделана хорошо, но, конечно, не удобна для больших проектов.

Не забудьте обратит внимание не только на код, но там есть разделы, где можно нарисовать спрайт или написать мелодию. Т.е. это полноценная среда, по типу PICO8, где вы можете создать полностью игру не выходя из этой среды. Для детей, думаю очень классно, но на примере создания мелодии видно, что никаких настроек нет, кроме расставить ноты и выбрать нужный звук и поэтому все это серьезно воспринимать не стоит. С другой стороны, если вы захотите большего, то вам так и так пришлось бы это все самостоятельно добавлять. Вот на Arduboy ничего такого нет и там уже выкручивайся как хочешь.
Давайте перейдем к примерам кода. Я, как и говорил, использую Javascript, но скажу большее, здесь есть поддержка Typescript, а это уже круто.
Я написал небольшое приложения, демку, чтобы протестировать функционал, который оказался для меня проблемным или спорным. Она работает просто: показывается меню, выбираешь то, что хочешь протестировать, запускается соответствующий экран, тестируешь что хочешь и по нажатию на кнопку, возвращаешься на меню назад. Вот шаблон или интерфейс такого экрана:
interface GameScene { create(): void; destroy(): void; onUpdate(): void; }
create вызывается, когда экран выбран, onUpdate выполняется каждый кадр, destroy когда экран закрыт. Интерфейс хорош своим минимализмом.
Этот интерфейс используется в небольшой системе меню, которую я написал, но здесь не привожу, чтобы не отвлекать от основной темы, но всё есть в разделе «Исходники» ниже.
На самом деле интерфейсы и классы не родные конструкции среды разработки, из-за чего иногда все портится, когда переключаешься на блоки, но мне все это нравится, поэтому почему бы и нет.
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. Тест индикации батареи

Возможно вам будет интересно посмотреть на следующий тест, это индикация состояния батареи.
Код теста индикации состояния батареи
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. Тест светодиодов

Код теста светодиодов
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. Тест акселерометра

Код теста акселерометра
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. Тест музыки

Код теста музыки
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 во ВКонтакте. Там я размещаю новости и делюсь своими мыслями, правда, стараюсь придерживаться тематики игр и электронных устройств, но подумываю писать обо всем, что мне интересно.

