
Некоторое время назад я поделился первыми впечатлениями от знакомства с Ардуино-совместимой платой ELBEAR ACE-UNO на базе отечественного микроконтроллера MIK32 «Амур». Материал нашёл хороший отклик среди читателей, и это подогрело моё желание развить тему. Правда, подогрев слегка перешёл в фазу медленного бурления, и достиг точки закипания только сейчас. Но лучше поздно, чем никогда.
В прошлый раз я входил в эту воду совершенно без подготовки, и почти все мои тесты работали ужасно медленно. Но я верю, что «Амур» может лучше, и сегодня сделаю второй подход к снаряду: всё-таки попытаюсь продемонстрировать художественный фильм «Плохое яблоко», рассказывающий о негативном влиянии продукции компании Apple на моральный облик японских девочек. Попутно расскажу про несколько важнейших практических моментов при работе с «Амуром».
Готовлю нападение
Несмотря на не особо впечатляющие результаты, в первый подход я получил ценный опыт, на основе которого получше подготовился, и сейчас я вам покажу, как готовлю нападение на выявленные проблемы.

По результатам предыдущего подхода задача кажется весьма амбициозной: ведь я не смог получить хоть сколько-нибудь плавную анимацию даже на микроскопическом монохромном OLED-дисплее с разрешением 128 на 64 точки и объёмом буфера кадра всего один килобайт. Да и с качественным звуком тоже не задалось, несмотря на наличие двухканального 12-битного ЦАП. Что-то явно пошло не так.
Однако, первые тесты дали понимание слабых мест, с которыми предстоит разобраться. А комментарии к статье, давшие полезные подсказки и наводки, плюс чтение различных публикаций на тему, позволили наметить пути решения возникших сложностей.
Основных проблем наметилось ровно три штуки:
Очень низкая скорость передачи данных в экран. Нужно убедиться, что аппаратный SPI действительно работает. По скорости работы прошлых тестов было очень похоже, что что-то идёт не так, и фактически работает через программный SPI, по крайней мере на OLED-дисплее.
Очень большой оверхед (сопутствующая нагрузка на процессор) при использовании прерываний по таймеру даже на частотах порядка десяти килогерц. Это одновременно и проблема местной реализации обработчика прерываний, и самого подхода с выборкой сэмплов звука по таймеру: лучше бы задействовать DMA.
Очень медленная загрузка скетча в плату: до восьми минут на один мегабайт. Нужно выяснить, почему она такая, и можно ли её как-то поднять до адекватных значений.
Прежде всего предстоит разобраться с этими вопросами, а потом реализовать простейший видеокодек для более эффективного хранения анимации и частичного обновления экрана, чтобы трёхминутный ролик поместился в имеющуюся память. Благо, памяти на моей плате ACE-UNO аж 32 мегабайта.
План-минимум на сегодня — показать монохромную анимацию на маленьком OLED-дисплее, а план-максимум — цветную на большом TFT, с цифровым звуком, возможно, даже в стерео. Как говорится, хотеть не вредно, вредно не хотеть.
Обновления
Так как с моего предыдущего подхода к плате ACE-UNO прошло уже немало времени — девять месяцев, надо бы обновиться. Ведь разработчики платы поддерживают свой проект. Ведь поддерживают же?
Сразу при возвращении к проектам в среде Arduino у меня возникла проблема: перестала работать ссылка на пакет поддержки плат (Board Support Package, BSP), на котором я делал свои первые тесты. В документации она не поменялась, пишет ошибку:

При этом вне IDE она открывалась, и в JSON-файле видно, что версия пакета поменялась с 0.5.1 на 0.5.3. Я нагуглил изменения и захотел обновиться: в новой версии были учтены некоторые моменты, которые иначе пришлось бы решать вручную. К тому же, обновился загрузчик, что давало надежду на решение проблемы очень низкой скорости загрузки скетчей.
Довольно быстро я догадался, почему IDE не открывает ссылку. Если предыдущий пакет работал в Arduino IDE 1.8.x, которым до сих пор пользуются многие энтузиасты, новый работает исключительно в IDE 2.x.x. Довольно сомнительное решение, которое сразу закрыло возможность ��родолжать работу на компьютере с Windows 7. А в прошлый-то раз прокатило. Ушла эпоха!

Пришлось закрывать тему ретроградства и открывать ноутбук с Windows 10. Поставил на него IDE 2.3.8, поставил пакет поддержки платы. Теперь всё прошло без проблем. Нужно отдать должное: начало работы с платой не требует плясок с бубном: поставил пакет в IDE, подключил плату, залил Blink, светодиод мигает. Всё как у взрослых.
Увы, в остальном всё осталось без изменений. Я снова собрал стандартный тест Adafruit GFX для OLED-дисплея на контроллере SH1106. 170-килобайтный скетч загружался мучительно долго, и как и прежде — целых полторы минуты, и работает старый код всё так же медленно. То есть автоматически никакие проблемы не решаются, решать их предстоит врукопашную.

Я всё же лелеял надежду, что новый загрузчик будет побыстрее, и решил его прошить в плату. Для этого производитель предлагает использовать ELJTAG — собственный JTAG-программатор. Из прочитанных материалов я узнал, что это не обязательно, подойдёт любой на чипе FT2232H. Но у меня под рукой был только старенький Segger J-Link, и меня не сильно вдохновляла перспектива экспериментировать с ним и самодельными кабелями, рискуя окирпичить дорогостоящую плату ACE-UNO. К слову, цены ��а платы за прошедшее время особо не изменились: 3 тысячи за ACE-NANO и 6 тысяч за ACE-UNO 8MB с ELJTAG в комплекте.

На Озоне отдельный ELJTAG стоил 2781 рубль, а в некоторых других самостоятельных Ардуино-ориентированных магазинах и вовсе 4000-5500. Импортозамещение, что поделать. Покряхтев, я совершил ещё одно обновление: отслюнявил в поддержку отечественного производителя необходимую сумму и заказал родной программатор. Благо, он может мне пригодиться и для других RISC-V контроллеров. Как говорится, у богатых свои причуды. Причуды уже есть, осталось только разбогатеть. Ну а пока программатор ехал, я начал проводить новые тесты.
Экран и SPI
Для начала я решил побороть проблему медленной работы OLED-дисплея. На этот раз я не стал гадать, и применил внешние средства контроля. Для начала измерил тактовую частоту SPI на пине SCK дисплея с помощью простенького цифрового осциллографа FNIRSI 5012H с частотой сэмплирования 100 МГц — он терпимо показывает цифровые сигналы в единицы мегагерц, этого хватает для моих упражнений со старой 8-битной техникой.

Осциллограф увидел тактовые импульсы, они оказались достаточно прямоугольные, что указывает на их низкую частоту. Автоматическое измерение показывает около 31 килогерца, и значение такого же порядка видно на сетке 20 микросекунд: импульс немного меньше, чем одна клетка. И это катастрофически мало для шины SPI: с такой скоростью передаётся порядка 4000 байт в секунду, а с объёмом буфера кадра в 1024 байта получается порядка 4 кадров в секунду. Скорость анимации примерно того же порядка я наблюдаю в тесте и визуально.
Вывод: аппаратный SPI в моём старом коде не работает, действует программная реализация через bitbang. Для моей же затеи абсолютно необходим аппаратный SPI, тогда скорость передачи данных будет на порядок больше. Ведь контроллер этого OLED-экранчика может работать с частотами около 10 МГц и обеспечивать скорость обновления более ста кадров в секунду.

Проблема решилась удивительно просто. Просто настроил ещё раз, как обычно делаю на Ардуинах, и аппаратный SPI заработал. Почему не работал ранее — я не знаю. Возможно, я делал что-то не так. А возможно, что-то было исправлено в пакете поддержки, так как на Хабре можно найти более старую статью, в которой говорится, что аппаратный SPI не работал, если задействовать десятый пин. Я использовал его и ранее, и сейчас. В первый подход не работало, теперь работает.
Так или иначе, правильная инициализация библиотеки Adafruit SH110X для работы с аппаратным SPI выглядит так:
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &SPI, OLED_DC, OLED_RST, OLED_CS, 10000000);
Чтобы убедиться в успехе, я снова проконтролировал тактовую частоту осциллографом, и она всегда высокая. Мой осциллограф не может нормально увидеть 10 МГц, видна только синусоида, и она присутствует и в начальных сценах, и при падающих звёздочках. Правда, почему-то идёт она редкими пакетами, а не сплошным потоком. Видимо, тест обновляет сцену по фрагментам, а не шлёт весь буфер экрана целиком.
Однако, не всё так хорошо. Да, аппаратный SPI работает, и первые тесты в стандартной библиотеке Adafruit_GFX рисуются очень быстро, как и должны. Но финальная анимация со звёздочками снова еле-еле шевелится. На всякий случай я поменял номера управляющих пинов, чтобы не трогать десятый пин, но ничего не поменялось. Из этого я сделал вывод, что дальше тормозит уже не SPI, а код рисования графики.
Скорость работы кода
У MIK32 «Амур» и, соответственно, плат на нём есть два больших подводных камня, прямо-таки скалы, связанных со скоростью работы. Несмотря на 32-битное ядро RISC-V, работающее на 32 мегагерцах, фактическая производительность легко может налететь на эти рифы и оказаться хуже, чем у завалящей 8-битной классической Ардуины на AVR ATmega.

Как и большинство современных микроконтроллеров, «Амур» хранит код во внешней Flash-памяти. К контроллеру она подключена последовательной шиной, которая работает сильно медленнее, чем ядро контроллера. По мере надобности код запрашивается из внешней памяти и тем или иным способом кэшируется в специальном ОЗУ, где и выполняется. Это позволяет коду внутри циклов, где обычно и происходит основная вычислительная нагрузка, выполняться довольно быстро. Ну а если код длинный и ветвистый, тогда он медленно и печально подгружается из Flash-памяти по мере выполнения.
Разумеется, на практике всё несколько сложнее и разнообразнее, но общая идея остаётся прежней: быстрое ядро, быстрое небольшое ОЗУ, медленная последовательная Flash-память. Этой схемы придерживаются и ESP32 на всех ядрах, включая RISC-V, и STM32, и SAMD, и некоторые другие современные микроконтроллеры, и это не мешает им обеспечивать довольно хорошую производительность. При этом с точки зрения кода всё работает совершенно прозрачно, и средний Ардуинщик вполне может даже не углубляться в такие дебри, и не догадываться о наличии подобного механизма.

К сожалению, именно на «Амуре» углубляться в дебри придётся. Flash-память, физически расположенная в отдельной микросхеме (на моей плате Winbond W25Q256), подключена к контроллеру шиной Quad SPI, работающей на частоте 16 мегагерц. Это значительно медленнее скорости работы ядра (шина-то п��следовательная). К счастью, предусмотрена кэш-память. К сожалению, её объём составляет всего 1024 байта, и работает она блоками — 8 страниц по 128 байт. Если код и его данные какого-то цикла не поместятся в кэш, будет значительная просадка производительности.
Есть способ обойти проблему: можно размещать часть кода в ОЗУ либо в EEPROM. Они расположены непосредственно на кристалле микроконтроллера, подключены к быстрой параллельной шине, и код оттуда работает существенно быстрее. Для этого в линкере предусмотрены специальные атрибуты. Заодно можно включить и максимальную оптимизацию:
void __attribute__((noinline, section(".ram_text"), optimize("O3"))) test_code_in_ram() { … }
Однако, объём ОЗУ на «Амуре» составляет всего 16 килобайт, а EEPROM вообще 8 килобайт, и много кода туда не поместится. К тому же, часть кода уже находится там. Как минимум, в ОЗУ располагается часть кода обработки прерываний, потому что на них необходимо реагировать максимально быстро.
Я сделал тест выполнения одинакового фрагмента кода из ОЗУ, куда он был помещён атрибутами для линкера, и из Flash. Чтобы код гарантировано не влез в кэш, я по совету ИИ сгенерировал макросами 2000 опкодов NOP (8 килобайт), а чтобы компилятор при оптимизации не убрал бесполезный цикл, объявил переменную цикла как volatile:
#define NOP __asm__ volatile ("nop") #define NOP10 NOP; NOP; NOP; NOP; NOP; NOP; NOP; NOP; NOP; NOP; #define NOP100 NOP10; NOP10; NOP10; NOP10; NOP10; NOP10; NOP10; NOP10; NOP10; NOP10; #define NOP1000 NOP100; NOP100; NOP100; NOP100; NOP100; NOP100; NOP100; NOP100; NOP100; NOP100; #define NOP2000 NOP1000; NOP1000; void test_code_in_flash() { volatile uint32_t j = 0; for (volatile uint32_t i = 0; i < 100000; ++i) { NOP2000 } } void __attribute__((noinline, section(".ram_text"), optimize("O3"))) test_code_in_ram() { for (volatile uint32_t i = 0; i < 100000; ++i) { NOP2000 } } void setup() { Serial.begin(115200); char buf[256]; uint32_t t1 = millis(); test_code_in_flash(); sprintf(buf, "code in flash %u ms", millis() - t1); Serial.println(buf); uint32_t t2 = millis(); test_code_in_ram(); sprintf(buf, "code in ram %u ms", millis() - t2); Serial.println(buf); } void loop() { }
Результат соответствует теории — крупнокалиберный код, размер которого превышает размер кэша, в ОЗУ работает почти в десять раз быстрее, чем из Flash-памяти:
23:42:53.981 -> code in flash 59369 ms
23:43:00.281 -> code in ram 6291 ms
Обработка прерываний
Другая большая проблема на «Амуре» — обработка прерываний. У контроллера есть всего один обработчик прерываний, который срабатывает при приходе любого прерывания. В нём нужно последовательно разобрать флаги всех возможных источников, чтобы понять, какое же прерывание сработало. Так как среда Arduino обеспечивает универсальность, она проверяет все возможные источники, и это работает очень медленно.
Единственное решение, с помощью которого можно повысить скорость обработки прерываний — заменить обработчик на свой собственный, в котором реагировать только на нужные источники и в нужном порядке. Такая возможность была предусмотрена изначально, по стандартной для платформы Arduino схеме:
extern "C" __attribute__((section(".ram_text"))) bool ISR(void) { // прерывание от пина GPIO? if (EPIC_CHECK_GPIO_IRQ()) { // проверка конкретного пина if (HAL_GPIO_LineInterruptState(GPIO_LINE_2)) { // необходимые действия при прерывании от пина } // сброс прерываний от пинов HAL_GPIO_ClearInterrupts(); } // сброс всех флагов прерываний HAL_EPIC_Clear(0xFFFFFFFF); }
Однако, раньше вместе с пользовательским ISR выполнялся и весь стандартный код проверки источников прерываний, что тратило кучу времени зря — ведь в реальном проекте наверняка будет использоваться лишь несколько заранее известных источников прерываний. В частности, в моём пробном проекте эмулятора звукового чипа AY-3-8910, работающего на прерывании от 16-битного таймера, большая часть времени уходила на эти накладные расходы, поэтому тогда мне пришлось понизить частоту дискретизации аж до 6 килогерц.
К счастью, в пакете поддержки 0.5.3 была предусмотрена возможность отключения стандартного кода и проверки только необходимых для проекта источников прерываний. Код пользовательского обработчика выглядит практически так же:
extern "C" __attribute__((section(".ram_text"))) bool ISR(void) { // прерывание от 16-битного таймера? if (EPIC_CHECK_TIMER16_1()) { // проверка первого таймера if (TIM16_GET_ARRM_INT_STATUS(htimer16_1_)) { // необходимые действия при переполнении таймера } // очистить флаги прерывания от таймера TIM16_CLEAR_INT_MASK(htimer16_1_, 0xFFFFFFFF); } return false; }
Ключевое отличие в том, что теперь обработчик может возвращать булевый флаг. Если вернуть true, стандартный код проверки источников прерываний выполняться не будет.
Загрузка скетча
Пока я делал редкие небольшие тесты, терпеть очень медленную загрузку скетчей было как-то возможно. Но далее я собираюсь загружать скетчи с анимацией, занимающие несколько мегабайт, и от этого зависит успех мероприятия. Ведь с такой скоростью один мегабайт загружается восемь минут. В таких условиях осуществить задуманное очень сложно: одна попытка будет занимать часы времени.

Есть простое обходное решение: подключить SD-карту и выполнять загрузку данных с неё. Но загрузка с SD сама по себе значительно медленнее, чем доступ к встроенной Flash-памяти, а со скоростью работы кода и без этого наблюдаются большие проблемы. Да и просто это неспортивно — использовать внешнюю память, когда на плате установлены безумные 32 мегабайта памяти, куда всё могло бы прекрасно поместиться. За них ведь уплочено! Значит, нужно бы разобраться.
Как я выяснил ранее, низкая скорость загрузки не является какой-то моей местной проблемой. Она воспроизводится на всех моих компьютерах с разными ОС. К счастью, проблема медленной загрузки пользователям известна, и напрягает не только меня. Есть два варианта: попробовать обновить загрузчик и попробовать загружать скетч непосредственно через JTAG. А У меня теперь как раз совершенно случайно есть ELJTAG. За него ведь уплочено!

Но для начала нужно его правильно подключить. Разъём на самом JTAG-адаптере имеет пластиковый ключ, провод в него вставить можно только одним способом. А вот на плате нет ни ключа, ни каких-либо надписей. И в Интернете найти хотя бы фотку, как правильно подключать, с наскоку не удалось. Вставляй как хочешь. Но если получится неправильно, есть вероятность, что плате настанут кранты, потому что через этот провод идёт и питание платы.
Соединять на самом деле нужно пины по схеме первый-к-первому: тот, что с квадратной площадкой к такому же. Убедиться в правильности подключения можно, прозвонив между собой экраны USB-разъёмов на плате и адаптере: при правильном подключении звонилка запищит. Только после этого можно вставлять ELJTAG в USB.

Далее я быстренько настроил адаптер через Zadig согласно официальному мануалу, и, радуясь, что хоть и заплатил немало денег, теперь-то точно смогу стремительно заливать свои мегабайты, попробовал с ходу загрузить всё тот же тестовый 170-килобайтный скетч непосредственно через JTAG, так как в теории это должно быть быстрее. Делается это просто: в IDE в инструментах выбираем программатор «mik32 uploader», потом в меню скетча «загрузка через программатор» (Ctrl+Shift+U).
Изменения в скорости загрузки оказались впечатляющими: 185 секунд, три минуты. Средняя скорость загрузки через JTAG оказалась около 900 байт в секунду. Я также попробовал прошить через JTAG обновлённый загрузчик от производителя, и снова загрузить скетч через USB-провод. С ним всё те же полторы минуты, как и раньше.

Далее начались пл��ски с бубном в условиях частичной неопределённости. Я не очень знаком ни с RISC-V, ни с JTAG и процедурами загрузки подобных контроллеров, понимаю только общие идеи. Конкретной информации на эту тему, касающейся именно «Амура», почти нет, и судя по небольшим её обрывкам, то, что есть сейчас — это уже значительно лучше, чем было раньше (раньше, видимо, слали скетчи голубиной почтой).

По советам от ИИ попробовал поменять в конфигурационном скрипте mik32-link.cfg скорость для JTAG — с 500 кГц на 2000 и на 3200. openocd принимает эти параметры, что видно в детальном логе загрузки, но по фактической скорости никаких изменений не наблюдается. Я поигрался и другими параметрами в скриптах, не особо понимая, что делаю, и ничего не дало никакого эффекта. Или работает как прежде, или перестаёт работать загрузка.

Я также попытался изменить настройки загрузки через USB. Если поменять upload.speed в boards.txt для моей платы с дефолтного значения 230400 на любое другое, загрузка перестаёт работать. Дефолтное значение, к слову, указывается в битах в секунду и подразумевает скорость передачи данных порядка 28 килобайт в секунду, чего не наблюдается даже отдалённо.
Всё вышесказанное намекает, что дело не в самих интерфейсах, а в медленных реализациях загрузчиков. JTAG тоже реализует загрузку в SPIFI память посредством некоторого протокола и исполняемого внутри микроконтроллера кода, который сначала загружается в ОЗУ или EEPROM. И похоже, там алгоритм записи в память по какой-то причине работает ещё хуже. Вероятно, потому что он не задействует ОЗУ для выполнения загрузчика целиком и работает фрагментами кода через progbuf.

Из любопытства я заглянул в код официального загрузчика, elbear_fw_bootloader (bootloader.c). UART в нём действительно настроен на 230400 бод. Но загрузчик принимает данные и пишет их во Flash-память 256-байтными пакетами, проверяя их на чтение после записи. При этом пакеты идут в текстовом HEX-формате и парсятся на стороне контроллера. Вероятно, это работает недостаточно быстро. Вряд ли узкое место в самой используемой Flash-памяти. Даже в самом худшем случае она должна обеспечивать скорость записи от 20 килобайт в секунду.
Существует альтернативный загрузчик ex_loader_2 от fabmicro. Он обещает более быструю загрузку, блоками по 4 килобайта. Но я пока не смог опробовать его на практике: он не связан с инфраструктурой Arduino, его надо собирать из исходников, а также собирать коммуникационную программу. Даже если я провернул бы данный фокус, рекомендовать этот путь Ардуинщикам было бы сложно.
На данном этапе я понял, что с наскока проблему не победить, а её сложность выходит за пределы моей текущей компетенции. Компетенцию надо наращивать, а это займёт время. Возможно, позже я осилю написать свой альтернативный загрузчик для интеграции в Arduino IDE. Ну а пока пришлось поумерить аппетиты и сфокусироваться на программе-минимум.
Хреновое яблоко
На данном этапе у меня уже есть все необходимые для рецепта успеха ингредиенты, с помощью которых можно реализовать вариант проекта попроще: монохромное видео на OLED с какой-то примитивной озвучкой.
Изначально я планировал сделать такую версию в довесок к лучшему финальному результату, просто для полноты и интересности материала. Но теперь она стала основным блюдом. И чтобы загрузить её в плату за обозримое время, хотя бы за время жизни Вселенной, а лучше за пару часов, пришлось пересмотреть некоторые моменты первоначального плана.

Для начала я подготовил саму анимацию к последующей конверсии. При помощи VirtualDub разобрал исходное видео на кадры с уменьшением до 128 на 64, и сохранил их в 24-битные BMP-файлы. Но результат был в оттенках серого, а для OLED-дисплея мне нужны однобитные изображения.
Чтобы не сильно мудрить, прогнал файлы с помощью пакетной обработки в IrfanView, получив готовые двухцветные изображения. В опциях включил дизеринг по Флойд-Стейнбергу, чтобы смотрелось побогаче, хотя решение это довольно сомнительное.

Потом точно таким же образом преобразовал все файлы в 16-цветные BMP. Это потребовалось для того, чтобы я мог задействовать в качестве основы для конвертера анимации свой старый код, который делал ещё для проекта hway. Переконвертировать файлы лишний раз было быстрее, чем добавлять поддержку 2-цветных изображений в мою библиотечку на Питоне.
Изначально я планировал хранить кадры без какого-либо сжатия, прямо в формате буфера кадра OLED-дисплея. Один кадр имеет размер 1024 байта, всего в анимации, проигрываемой со скоростью 30 кадров в секунду, 6572 кадра, то есть около 7 мегабайт данных. Такой объём прекрасно помещается во Flash-память, а сами кадры достаточно небольшие, что позволяет пересылать их в дисплей целиком.
Я отладил конвертер и проигрыватель в таком виде, тестируя их на фрагменте из десятка кадров. Однако, когда стало ясно, что скорость загрузки скетчей не победить, и загрузка варианта с несжатой анимацией полной длительности грозит занять буквально два часа, мне пришлось внедрить упаковку кадров.
Так как исходная анимация силуэтная и содержит много залитых одним цветом областей, я решил использовать простейший вариант: конвертер отслеживает изменения между соседними кадрами в блоках размером 8x4, и в данные кадра добавляются только изменившиеся блоки. Размер блока был подобран экспериментально, чтобы получить наименьший размер выходных данных. Эта простая схема обеспечила сжатие примерно в 4.5 раза.
Для работы с дисплеем я решил отказаться от библиотеки Adafruit SH110X, потому что она очень жирная: тащит за собой шрифт и заставку, и на этапе первоначальных тестов всё это добро каждый раз грузилось в плату мучительно долго. Вместо неё я поддержал отечественного производителя и взял GyverOLED от Алекса Гайвера. Но библиотека используется только для инициализации дисплея, а пересылку в буфер я делаю вручную, чтобы избежать лишних преобразований и копирований. Всё просто:
void oled_send_frame(const unsigned char* data) { oled.sendCommand(0x00); oled.sendCommand(0x10); oled.sendCommand(0x40); for (int i = 0; i < 64 / 8; ++i) { oled.sendCommand(0xB0 + i + 0); // set page address oled.sendCommand(2 & 0xf); // set lower column address oled.sendCommand(0x10); // set higher column address oled.beginData(); for (int j = 0; j < 128; ++j) SPI.transfer(data[i * 128 + j]); oled.endTransm(); } }
Текущий кадр собирается из изменившихся блоков в буфере в ОЗУ в формате дисплея, и собранный кадр каждый пересылается в дисплей целиком. Это быстрее и проще, чем проводить серию блочных обновлений, слишком много накладных расходов.

Отлаживал код я на обычном небольшом OLED-дисплейчике с диагональю 1.3 дюйма и контроллером SSD1306/SH1106, а для финальной демонстрации решил перейти на крупнокалиберный 2.42-дюймовый дисплей на контроллере SSD1309. Вроде бы эти контроллеры совместимы, однако, так просто ничего не заработало, картинка была неправильной: вертикальная полоса слева и фрагмент изображения сверху. Пришлось поразбираться, в итоге поддержал оба типа дисплеев. У них отличается и формат передачи данных, и одна команда в инициализации.

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

Для подготовки мелодии я задействовал свой VST-плагин PETCB2, предназначенный для создания музыки для Commodore PET с помощью современных цифровых студий. В Reaper собрал монофоническую аранжировку, экспортировал данные, потом преобразовал их программкой на C в более компактный и удобный для проигрывания формат, чтобы сэкономить ещё несколько килобайт и немного выиграть в скорости работы. Далее написал простенький проигрыватель, синтезирующий нужный звук.
В остальном код проекта очевидный, ничего особенного не требуется: строим текущий кадр по данным изменений блоков из массива данных анимации, и 30 раз в секунду отправляем результат на дисплей. На прерываниях таймера синтезируем звук, играем музыку и отслеживаем время. Код подготовки кадра, пересылки в дисплей и синтеза звука на всякий случай размещается в ОЗУ, потому что почему бы и нет.
Размер итогового скетча со сжатой анимацией полной длительности и музыкальным сопровождением составил 1450400 байт, то есть почти полтора мегабайта. Это всё ещё довольно много, и грузилось в плату аж 16 с половиной минут. Но это хотя бы в пределах возможностей человеческого терпения.
И вот вам, пожалуйста, действительно плохое яблоко на «Амуре» готово к употреблению:
Я также собирал проект с BSP 0.5.1, и с его менее эффективной схемой обработки прерываний максимальная частота дискретизации звука, при которой анимация ещё не подтормаживает, и работает на стабильных 30 кадрах в секунду, составила 32000 гц, что в общем-то неплохо. Ну а в сборке для BSP 0.5.3, которая и показана на видео, экспериментальным путём было подобрано значение в 55000 гц (в 1.7 раз больше).
Заключение
Плохому яблоку на «Амуре» живётся несладко. Пока что оно смогло достичь лишь своей самой базовой формы — монохромное силуэтное видео низкого разрешения на небольшом экранчике с простенькой одноголосой интерпретацией мелодии, пиликающей на фоне. Но для начала хотя бы так. Русская Ардуина всё-таки что-то да может. Электрический медведь только расправляет крылья.
Нельзя не отметить, что опыт на данном этапе не самый приятный. На три изделия отечественного производителя я потратил уже 11 тысяч рублей, и едва ли получил удовлетворительный результат. Отдам должное: всё это хотя бы как-то работает из коробки: прошивается, загружается, запускается, и это уже большое достижение. Но цикл тестирования скетчей пока получается очень медленный, а воспользоваться всеми мегабайтами Flash-памяти на плате в таком виде просто невозможно.
Но плохое яблоко ещё может стать лучше! Да и другие интересные проекты на «Амуре» давно уже мной задуманы и близятся к реализации. Продолжение последует.
P.S. Пока материал ожидал публикации, «Микрон» выпустил в продажу отладочный набор «Игровая консоль», имитирующий формат некогда популярной самодельной портативной игровой приставки Arduboy, но на базе «Амура». А значит, будут ещё траты и ещё проекты.
P.P.S. Исходники пока что выложил в посте на моём Бусти.
© 2026 ООО «МТ ФИНАНС»

