habrahabr

Создаём эмулятор легендарной игры «Ну, Погоди» на базе Raspberry Pi Pico

  • пятница, 11 апреля 2025 г. в 00:00:10
https://habr.com/ru/companies/ruvds/articles/889414/


Многие из тех, кому сейчас за 30, и рождённых в СССР или на постсоветском пространстве, помнят электронную игру «Ну, погоди!». Во времена, когда не было ни интернета, ни ноутбуков, ни мобильных телефонов, а из общедоступных электронных развлечений были только аттракционы в парках культуры и видеосалоны, обладание бытовым компьютером, электронными наручными часами Montana или электронной игрой «Ну, погоди!» было мечтой многих детей.

Были ещё и другие электронные игры, но именно «Ну, погоди!» считается классикой.


Игре посвящено много ностальгических статей и видео. На различных торговых площадках можно купить её в различном состоянии от убитого до «с хранения» и даже новодел.


Лет 10 назад и я купил её в идеальном состоянии, поигрался, вспомнил детство и положил в ящик. Но несколько месяцев назад с разочарованием увидел, что «потекла» нижняя часть экрана.


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


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


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


Эмулятор максимально приближен к оригиналу, если не считать экран (он не сегментный, как в оригинале) и корпус (я пока реализовал на беспаечной макетной плате).


Если вам интересно, как за несколько вечеров воссоздать у себя эмулятор «Ну, погоди!» на современном микроконтроллере или просто поностальгировать, добро пожаловать под кат.


Немного истории


Я не застал СССР во взрослом возрасте, но по той информации, что сейчас можно найти в интернете, из-за политической ситуации в мире советская электроника 80-х годов была смесью пиратства, реверс-инжиниринга, уникальных разработок и попыток интегрироваться в мировую экономику. Всё это в полной мере отразилось в электронной игре «Ну, погоди!».


Японская компания Nintendo 9 октября 1981 выпустила две игры из серии Game & Watch Wide Screen, идентичные по геймплею, но из-за лицензионных ограничений отличающиеся по оформлению: Egg и Mickey Mouse. Советский союз не остался в стороне и выпустил клон — «Электроника 24-01. Игра на экране: Микки Маус». Но самой известной игрой, выпущенной в СССР, стала «Электроника ИМ-02» — «Ну, погоди!».


Game & Watch MC-25 «Mickey Mouse»

Game & Watch EG-26 «Egg»

Электроника 24-01 «Игра на экране: Микки Маус»

Электроника ИМ-02 «Ну, погоди!»

Позже были выпущены различные советские клоны линейки Game & Watch, но некоторые игры из линейки Электроника ИМ, например, Электроника ИМ-23 «Автослалом» 1989 года является уникальной и не имеет японского аналога.


При желании можно найти много общей информации по советским и японским играм, но я, чтобы не распыляться, сосредоточусь только на «Ну, погоди!».


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


Введение


Расскажу, как мне пришла идея своего эмулятора, и с чем мне пришлось столкнуться.


Я спаял Мурмулятор и успел насладиться спектрумовскими играми на нём. Мне очень понравилась идея создания эмулятора на микроконтроллере без операционной системы, уж очень быстро устройство готово к работе и очень реалистично эмулирует.


Я задался вопросом, а можно ли подобный эмулятор создать для «Ну, погоди!». Итак, мой первый поисковый запрос был «Эмулятор Ну, Погоди!», который привёл на тему форума. Очень много информации было почерпнуто при просмотре сообщений этой темы, а также темы форума на сайте watch.ru.


Для меня задача решается проще, когда ты уверен в возможности её решения. И в том, что решение задачи возможно, меня убедил отладчик для игр серии Электроника и наличие уже существующих эмуляторов. Отладчик помог мне и в изучении того, как работает игра на низком уровне. У него небольшой размер, но он очень функциональный. Я понял это, когда перерыл уйму информации и написал свой эмулятор. Кроме непосредственной отладки, в нём вы можете посмотреть распиновку микроконтроллеров, разводку сегментов дисплея, получить звук игры в виде wav-файла. Для работы с эмулятором необходимо найти ROM нужной вам игры.


Внешний вид отладчика

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


Кроме 2 тем на форумах и отладчика, я использовал:


  1. Код эмулятора MAME для устройств SM-510.
  2. Документацию по микроконтроллерам Sharp (ссылка 1 и ссылка 2).
  3. Cтатья «Однокристальные ЭВМ серии КБ1013» в журнале «Микропроцессорные средства и системы» 1987г. №5 стр. 5-18. Ссылка на журнал.
  4. Статьи IgorR76 по микроконтроллеру КБ1013ВК1-2 (Статья 1 и Статья 2).
  5. Различные схемы для игры «Ну, погоди!», которые я сумел найти на форумах.
  6. Несколько проектов, помимо MAME, связанных с эмуляцией, на Github:
  7. Урок по жидким кристаллам от Павла Виктора, который доходчиво рассказал, что такое жидкокристаллический дисплей, и как он работает. Были бы такие уроки по физике в моей школе.
  8. Лекции по Raspberry Pi Pico, так как я не был знаком c Raspberry Pi Pico. В частности лекция по PIO.
  9. Беседы с ИИ-помощником, без него сейчас уже никуда. Только часть времени тратится на проверку его утверждений, соглашательство его очень мешает.

Может, кому-то моя статья покажется простой и банальной, но, во-первых, в простоте истина и красота, в простом быстрее разобраться, а, во-вторых, простое всегда можно развить во что-то более сложное и полезное.


Как устроена «Ну, погоди»


С внутренним устройством игры я ознакомился в детстве, когда решил разобрать свою «Ну, погоди!», у которой перестали светиться отдельные сегменты экрана. Естественно, такого вандализма игра не пережила и ещё несколько лет пылилась в ящике для игрушек, пока её не выбросили при очередной разборке ящика.


Основу игры составляют:


  • печатная плата,
  • две токопроводящие резинки,
  • сегментный жидкокристаллический дисплей ИЖМ2-71-01 или ИЖМ13-71,
  • поляризационный фильтр,
  • светоотражающая поляризационная плёнка,
  • плёнка с сюжетом,
  • мембраны для кнопок,
  • кнопки,
  • корпус игры из двух половинок,
  • крышка для отсека батареек,
  • две батарейки,
  • десяток шурупов.

На печатной плате находятся:


  • микроконтроллер КБ1013ВК1-2, или, как тогда принято было называть, однокристальная микроЭВМ
  • пьезоизлучателель,
  • «часовой» кварц на 32768 Гц,
  • несколько конденсаторов,
  • батарейный отсек,
  • контакты для мембранных кнопок,
  • контакты для токопроводящих резинок, передающих сигналы от печатной платы жидкокристаллическому дисплею.

Внешний вид однокристальной микроЭВМ КБ1013ВК1-2

В детстве меня больше всего меня впечатлили токопроводящие резинки, соединяющие плату и сегментный индикатор. Зная из физики, что резина не проводит электричество, я не мог понять, как сигналы передаются на экран. Если резинка проводящая, то как она не замыкает все контакты? Сейчас я знаю, что токопроводящая резинка — композитный материал, который состоит из тонких чередующихся слоёв (проводника и изолятора).


Больше информации вы можете узнать из познавательного видео о внутреннем устройстве игр серии Электроника ИМ.


Приведу интересные факты, которые помогут вам быстрее вникнуть в то, что нужно для создания эмулятора. К сожалению, на 100% быть уверенным в достоверности информации я не могу, так как официальных источников информации практически нет. Но тем не менее, то, что я собрал, позволило мне создать работающий эмулятор «Ну, погоди!» на базе Raspberry Pi Pico.


  1. КБ1013ВК1-2 является советским аналогом японского Sharp SM-5A.
  2. Полноценной спецификации для КБ1013ВК1-2 и Sharp SM-5A я не нашёл, но есть несколько источников, которые позволяют разобраться в их работе.
  3. Мнемоники ассемблера, название регистров портов ввода-вывода для КБ1013ВК1-2 и Sharp SM-5A различные, но микроконтроллеры имеют идентичную архитектуру и бинарно совместимы.
  4. Для КБ1013ВК1-2 многое проясняют несколько статей в журнале «Микропроцессорные средства и системы».
  5. Микроконтроллер Sharp SM-5A задокументирован ещё меньше. Он является одним из серии микроконтроллеров, выпускавшихся Sharp. Для Sharp SM-5A описание инструкций я не нашёл, в источнике 1 присутствует только общая информация о нём, а в источнике 2 описание мнемомоник схожего с SM-5A микроконтроллера SM-510.
  6. Много информации можно получить из эмулятора SM-510 проекта MAME, а также более простых эмуляторов.
  7. Микроконтроллеры имеют масочное ПЗУ, записанное один раз и навсегда на заводе. То есть изменить программу для игры в микроконтроллере нельзя.
  8. Как было получено содержимое ПЗУ для игр, гуляющее на просторах интернета, существует несколько вариантов:
    • методом декапинга,
    • переводом микроконтроллера в специальный режим,
    • у Nintendo могли сохраниться исходные прошивки, так как я находил файлы, датируемые 1996 годом, а информация о двух первых способах появилась гораздо позже.
  9. Обладание прошивкой вовсе не значит, что вы её сможете просто так взять и выполнить инструкция за инструкцией, так как у этих микроконтроллеров используется так называемый полиномиальный счётчик команд.
  10. Игра «Ну, погоди!» не использует возможности сегментного дешифратора.

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


Мне помогло, когда я посмотрел на микроконтроллер с точки зрения ООП. Микроконтроллер хранит состояние в своих регистрах, каждая команда имеет интерфейс в виде машинного кода и реализацию в виде микрокода, который изменяет состояние микроконтроллера. Для эмулятора более сложных микроконтроллеров, вероятно, эта метафора не подойдёт, но для КБ1013ВК1-2 сгодится.


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


Микроконтроллеры КБ1013ВК1-2/SM-5A


Для построения эмулятора игры «Ну, погоди!» важной составляющей является разработка эмулятора самого микроконтроллера (однокристальной микроЭВМ). Важно знать программную модель микроконтроллера, но знание архитектуры микроконтроллера поможет лучше понять программную модель. Структура и стиль изложения в статье «Однокристальные ЭВМ серии КБ1013» в журнале «Микропроцессорные средства и системы» оставляет желать лучшего, но эта статья является, пожалуй, самым полным общедоступным трудом, который задокументировал КБ1013ВК1-2. К сожалению, в ней программная модель описана вперемешку с описанием архитектуры, что только запутывает. Далее я хочу преподнести информацию, на мой взгляд, более доходчиво. Надеюсь, мне это удастся.


▍ Архитектура микроконтроллеров КБ1013ВК1-2/SM-5A


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


Микроконтроллер в СССР называли однокристальной микроЭВМ, и я считаю, что этот термин лучше отражает архитектуру этого устройства.


У однокристальной микроЭВМ есть процессор, обрабатывающий команды, ПЗУ, ОЗУ, набор схем для коммутации с внешними устройствами. В однокристальной микроЭВМ КБ1013ВК1-2 это всё присутствовало. Знал бы я в детстве, что я был обладателем полноценной ЭВМ!


Из устройств у КБ1013ВК1-2 присутствуют:


  • процессор,
  • таймер,
  • контроллер жидкокристаллических дисплеев,
  • дешифратор сегментного кода,
  • устройство управления режимом малой потребляемой мощности (считай спящим режимом).

Для эмуляции нам важны первые три. Замечу, что дешифратор сегментного кода вообще не используется программой, записанной в «Ну, погоди!», но в эмулятор я его всё равно включил.


Есть ещё другие устройства, но думаю, это уже тема более глубокого исследования.


Структурные схемы для SM5A и КБ1013ВК1-2 приведены ниже:


Распиновка SM5A

Схема SM5A

Схема КБ1013ВК1-2

Обозначение микросхемы КБ1013ВК1-2 на чертежах

Распиновка микросхемы КБ1013ВК1-2 в 60-ти выводном планарном корпусе

SM5A выпускался в 60QFP корпусе, КБ1013ВК1-2 в похожем на него планарном 60 выводном корпусе. Как я понимаю, микроконтроллеры не были совместимы по расположению выводов, но совместимы на уровне машинных кодов и присутствующих устройств ввода-вывода.


Я не рассчитываю, что вы сразу поймёте эти схемы, но для общего понимания микроконтроллера КБ1013ВК1-2/SM5A, если к ним добавить программную модель, будет достаточно.


▍ Программная модель микроконтроллеров КБ1013ВК1-2/SM-5A


На основании доступной мне информации я могу предположить, что программная модель SM-5A идентична КБ1013ВК1-2. Различаются они только названиями регистров, портов ввода-вывода и мнемоник машинных команд.


Элемент модели
КБ1013ВК1-2
SM-5A
Кол-во разрядов
Аккумулятор
Acc
Acc
4
Флаг переноса
C
C
1
Счётчик команд
PC
СA, PU, PL
11 (1, 4 и 6)
Стековый регистр
A
S (CS, SU, SL)
11 (1, 4 и 6)
Регистр доступа к ОЗУ
DP (DPH, DPL)
B (BM, BL)
7 (3 и 4)
Порт ввода
D
K
4
Порт вывода
R
R
4
Порт вывода для контроллера ЖК-дисплея
O,O’
W,W’
36 и 36
Таймер
T
DIV
15(4)
Флаг таймера
TIMER
не нашёл информации
1
Флаг прерывания 1
INT1
α
1
Флаг прерывания 2
INT2
β
1

Интересно реализованы прерывания в КБ1013ВК1-2. Вместо привычной push-модели используется pull-модель. Иными словами, прерывание не является событием, которое прерывает выполнение основной программы для вызова обработчика прерывания, а программа должна опросить флаг прерывания, и, если он установлен, выполнить обработку.


Push-модель реализована только при выходе микроконтроллера из спящего режима при переполнении счётчика или наличия информации во входном порту. В этом случае микроконтроллер выполняет переход по адресу 0х0000. Но я не знаю, можно ли это отнести к механизму прерываний.


Для флагов прерывания α и β я сомневаюсь, что японцы использовали в мнемониках греческие буквы, но как они назывались в мнемониках, я не нашёл.


▍ ПЗУ, ОЗУ, ввод-вывод


Организация ПЗУ, у привыкших к современным микроконтроллерам, наверное, вызовет недоумение. ПЗУ имеет страничную организацию. В ПЗУ хранится программа. Счётчик команд PC и стековый регистр A хранят адреса в памяти ПЗУ. К тому же PC явлется полиномиальным счётчиком (то есть он изменяется не на 1 при икрементировании). В общем, я взял готовый код из MAME для полиномиального счётчика.


Организация ОЗУ тоже вызывает удивление, но то были времена, когда 64 килобайт должно было хватить всем. В ОЗУ хранятся данные. Адресуются не байты, а ниблы, или полубайты (4 бита).


Возможности ввода-вывода игрой «Ну, погоди!» используются не полностью. Скажу только, что регистр для ввода данных и регистр для вывода данных используются для организации матричной клавиатуры. Вы поймёте, как она организована, изучив распиновку микроконтроллера и схему электрическую принципиальную «Ну, погоди!».


Доступ к таймеру и регистрам порта ввода-вывода ЖКИ ограниченный. Вы не можете получить из программы доступ ко всем битам таймера, как и вывода для контроллера ЖК-дисплея. Таймер является 15-битным, но из программы можно только манипулировать переполнением и старшими 4 битами. В регистры контроллера ЖКИ данные помещаются по очереди в один регистр со сдвигом, а также есть возможность группу регистров O(W) поместить в O’(W’).


Как всё реализовано — лучше посмотреть на исходный код в MAME или в моём эмуляторе.


▍ Система команд


Система команд состоит из 58 базовых команд. Почти все команды имеют длину в 1 байт. Мнемоники ассемблера, как и названия регистров для SM-5A и КБ1013ВК1-2, имеют разные имена, что немного усложняет понимание. Полноценное описание мнемоник для SM5A, в отличие от КБ1013ВК1-2 я не нашёл. Но для создания эмулятора мне хватило программного кода в MAME.


▍ ЖКИ-дисплей игр серии Электроника ИМ


Я включил этот раздел в статью, поскольку в нём есть ответ на ворос, который долго не давал мне покоя: почему сегмент дисплея темнеет при подаче напряжения, остаётся светлым при отсутствии напряжения, но при вытекании жидких кристаллов повреждённый участок тоже становится тёмным?


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


Экран «Ну, погоди!»

Рисунок на лицевой стороне тыльного стекла ЖКИ-дисплея «Ну, Погоди!»

Рисунок на тыльной стороне лицевого стекла ЖКИ дисплея «Ну, Погоди!»

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


Как же свет проходит, когда на ЖКИ-дисплей не подаётся напряжение? На внутренние поверхности стёкол, помимо токопроводящего рисунка, нанесены параллельные борозды, причём борозды на одной пластине перпендикулярны бороздам на другой. Благодаря этим бороздам жидкие кристаллы выстраиваются в спиральные структуры при осутствии электрического поля, позволяя проходить свету. При подаче напряжения электрическое поле выравнивает кристаллы, нарушая спираль, и свет перестаёт проходить — сегмент темнеет. Схематично это представлено у 7-сегментного ЖКИ на рисунке ниже. Устройство ЖКИ-дисплея «Ну, Погоди» аналогично.


Устройство ЖКИ-дислпея

Жидкие кристаллы деградируют при постоянном напряжении, поэтому на токопроводящий слой подаётся переменное напряжение. Эту задачу выполняет драйвер ЖКИ, например, встроенный в микросхему КБ1013ВК1-2. Кроме того, выводов у ЖКИ меньше, чем сегментов. Для управления используются общие выводы, каждый из которых активирует определённую группу сегментов в заданный момент. Поочерёдная активация этих выводов — ещё одна функция драйвера.


Интересно было бы использовать оригинальный ЖКИ-дисплей из игры «Ну, погоди!». Это возможно в теории, но требует глубоких знаний электроники. Поэтому я остановился на модульном TFT-дисплее с драйвером ILI9341, популярном в DIY-проектах и более простом в реализации.


Отказ от полной эмуляции работы драйвера ЖКИ значительно упрощает задачу.


▍ Схема электрическая принципиальная «Ну, погоди!»


Для лучшего понимания программного кода и схемы моего эмулятора, приведу схему оригинальной «Ну, погоди!» и распиновку КБ1013ВК1-2.


Схема игры «Ну, погоди!»

Распиновка ЖКИ-дисплея «Ну, Погоди!»

Построение эмулятора


▍ Принципы построения эмулятора


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


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


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


Постараюсь в несколько абзацев вместить краткие сведения по архитектуре и схемотехнике.


Микроконтроллер/процессор работает на определённой частоте. Под частотой понимается количество тактовых импульсов (тактов) в секунду. Такты необходимы для синхронизации работы процессора с внутренними схемами и внешними устройствами. Время, которое занимает обработка одной команды, называется командным циклом.


Командный цикл состоит из трёх этапов: Fеtch — Decode — Execute. Этапы инициируются тактовыми сигналами. Этапы Fetch — Decode могут выполняться за один машинный такт.


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


Для эмуляции работы микропроцессора необходимо:


  1. Реализовать работу с памятью программ.
  2. Реализовать работу с памятью данных.
  3. Написать логику работы каждой из команд.
  4. Написать логику для Fetch — Decode — Execute.

В рабочем состоянии процессор обычно выполняет Fetch — Decode — Execute в вечном цикле.


В моём эмуляторе Fetch — Decode — Execute выглядит следущим образом:


void device_run() {
  int remaining_icount = m_icount;

  while (m_icount > 0) {
    m_icount--;

    if (m_halt && !wake_me_up()) {
      div_timer(remaining_icount);
      m_icount = 0;
      return;
    }

    m_prev_op = m_op;
    m_prev_pc = m_pc;
    m_op = read_byte_program(m_pc);

    increment_pc();
    get_opcode_param();

    if (m_skip) {
      m_skip = false;
      m_op = 0;
    } else
      execute_one();

    div_timer(remaining_icount - m_icount);
    remaining_icount = m_icount;
  }
}

▍ Эмуляция ЖКИ дисплея


Суть эмуляции заключается в следующем: изображение разбивается на зоны, соответствующие сегментам на сегментном дисплее, и, в зависимости от состояния регистров порта O (W) драйвера ЖКИ, отображаются или нет. Кроме того, при эмуляции отображается изображение с сюжетной плёнки.


В интернете вместе с прошивкой для «Ну, погоди!» можно найти изображение со всеми сегментами и изображение с сюжетной плёнкой. Называется она ArtWork. К сожалению, именно для «Ну, погоди!» я не нашёл, а воспользовался той, которая была для Egg. Если вы присмотритесь, то сможете увидеть, что она наложилась неидеально, но, по-моему, это добавляет реалистичности в эмуляцию.


Соотношения сторон и размеры TFT-дисплея и сегментного дисплея немного различаются, но я не стал добиваться 100% соответствия.


Графические изображения необходимо предварительно обработать для использования в моём эмуляторе. В этом мне помог проект на Github. Как я понимаю, он используется для микроконтроллера или платы с лучшими характеристиками, чем Raspberry Pi Pico. Для использования с Pico и моим дисплеем я немного модифицировал программу из проекта.


Я не использовал упаковку в архив. Так как размер оперативной памяти Pico не позволял разместить и кадровый буфер для вывода изображения, и распаковать архив, я разместил все данные в ПЗУ без архивации.


Код для эмуляции ЖКИ я взял из проекта, удалив лишний код и адаптировав под мой TFT-дисплей.


В реальном «Ну, погоди!» экран обновляется 60 раз в секунду. Добиться такого на Pico у меня не удалось, но «на глаз» изображение отображается удовлетворительно.


Так как игра «Ну, погоди!» использует инертность сегментного ЖКИ-дисплея, в эмулятор пришлось добавить код для устранения мерцания.


▍ Эмуляция звука


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


В «Ну, погоди!» простейший однобитный звук, такой, какой был в то время. Заключался он в аппроксимации меандром. Вывод 0 и 1 в разряд порта, к которому подключён пьезоизлучатель, заставляет выдавать динамик нужные колебания.


Задача, которая стояла передо мной, была выводить эти 0 и 1 с чёткими таймингами, такими же как и в оригинале. Изначально я хотел это сделать с использованием ШИМ, но потом узнал об интересной возможности PIO в Pico и решил использовать её. Если кратко, то PIO немного напоминает FPGA на минималках.


Ниже я привожу листинги модуля для вывода звука и программу для PIO State Machine


Вывод звука
#include "sound.h"
#include "def.h"
#include "hardware/pio.h"
#include "sound.pio.h"
#include <stdio.h>

#include "hardware/dma.h"

volatile uint8_t *current_buff;

uint8_t buffer1[SOUND_BUFFER_SIZE] __attribute__((aligned(SOUND_BUFFER_SIZE)));
uint8_t buffer2[SOUND_BUFFER_SIZE] __attribute__((aligned(SOUND_BUFFER_SIZE)));

#define PIO_TX_PIN 15

int dma_chan1;
int dma_chan2;

volatile bool emulate = false;
volatile bool draw = false;

volatile uint8 *buffer_start = buffer1;

void dma_handler() {
  static uint32 cnt = 0;
  if (dma_hw->ints0 & 1u << dma_chan1) {
    // Clear the interrupt request.
    dma_hw->ints0 = 1u << dma_chan1;
    emulate = true;
    buffer_start = buffer2;
    if (!(cnt++ % 8)) {
      draw = true;
    }
  } else if (dma_hw->ints0 & 1u << dma_chan2) {
    dma_hw->ints0 = 1u << dma_chan2;
    emulate = true;
    buffer_start = buffer1;
    if (!(cnt++ % 8)) {
      draw = true;
    }
  }
}

int init_sound() {

  const uint sm = 0;

  uint offset = pio_add_program(pio0, &beeper_tx_program);

  beeper_tx_program_init(pio0, sm, offset, PIO_TX_PIN, SYS_FREQ);

  dma_chan1 = dma_claim_unused_channel(true);
  dma_chan2 = dma_claim_unused_channel(true);

  dma_channel_config c1 = dma_channel_get_default_config(dma_chan1);
  channel_config_set_transfer_data_size(&c1, DMA_SIZE_8);
  channel_config_set_read_increment(&c1, true);
  channel_config_set_write_increment(&c1, false);
  channel_config_set_dreq(&c1, DREQ_PIO0_TX0);
  channel_config_set_ring(&c1, false, 8);
  channel_config_set_chain_to(&c1, dma_chan2);
  dma_channel_set_irq0_enabled(dma_chan1, true);
  dma_channel_configure(dma_chan1, &c1, &pio0_hw->txf[0], buffer1,
                        SOUND_BUFFER_SIZE, false);

  dma_channel_config c2 = dma_channel_get_default_config(dma_chan2);
  channel_config_set_transfer_data_size(&c2, DMA_SIZE_8);
  channel_config_set_read_increment(&c2, true);
  channel_config_set_write_increment(&c2, false);
  channel_config_set_dreq(&c2, DREQ_PIO0_TX0);
  channel_config_set_ring(&c2, false, 8);
  channel_config_set_chain_to(&c2, dma_chan1);
  dma_channel_set_irq0_enabled(dma_chan2, true);
  dma_channel_configure(dma_chan2, &c2, &pio0_hw->txf[0], buffer2,
                        SOUND_BUFFER_SIZE, false);

  // Configure the processor to run dma_handler() when DMA IRQ 0 is asserted
  irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);
  irq_set_enabled(DMA_IRQ_0, true);
  dma_channel_start(dma_chan1);
}


Программа для PIO State Machine
.pio_version 0

.program beeper_tx

.wrap_target
out pins, 1 [7]
.wrap

% c-sdk {
#include "hardware/clocks.h"

static inline void beeper_tx_program_init(PIO pio, uint sm, uint offset, uint pin_tx, uint baud) {

    pio_gpio_init(pio, pin_tx);

    pio_sm_config c = beeper_tx_program_get_default_config(offset);

    pio_sm_set_consecutive_pindirs(pio, sm, pin_tx, 1, true);

    sm_config_set_out_shift(&c, true, true, 1);

    sm_config_set_out_pins(&c, pin_tx, 1);

    sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);

    // SM transmits 1 bit per 8 execution cycles.
    float div = (float)clock_get_hz(clk_sys) / (baud * 8);
    sm_config_set_clkdiv(&c, div);

    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);

}

%}


▍ Синхронизация эмуляции микроконтроллера, звука и изображения


Одной из задач я ставил реалистичную эмуляцию звука оригинальной «Ну, погоди!», и это мне удалось, оставалось его синхронизировать с эмуляцией дисплея и эмуляцией работы микроконтроллера. Я поступил следующим образом: так как у RP2040 два ядра, одно ядро я использовал для эмуляции дисплея, а второе для эмуляции микроконтроллера.


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


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


Для непрерывного вывода содержимого буферов я использовал DMA chaining.


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


Проблема оказалась в следующем. Микроконтроллер RP2040 не имеет встроенной памяти, а использует внешнюю флеш-память. Для ускорения доступа используется XIP-кэш. Во флеш-памяти обычно располагаются команды для микроконтроллера, но я разместил и ROM «Ну, погоди!». Поэтому программе приходилось постоянно обращаться к разным областям флеш-памяти, кэш часто оказывался невалидным, что требовало обращения к медленной флеш-памяти, и появлялись провалы в генерировании звука. Я решил проблему, разместив весь программный код в оперативной памяти и принудительно указал, что ROM игры находится во флеш-памяти.


▍ Обработка кнопок


Как вы видели на схеме, в «Ну, погоди!» все кнопки, кроме кнопки сброса, объеденены в матрицу 2x4. Так как я не был ограничен тем, как будет реализована клавиатура в моём эмуляторе, я решил сделать аналогичную клавиатуру.


С целью упрощения и экономии места на плате я не стал добавлять кнопку сброса, присутствующую в оригинальной «Ну, погоди!». Для сброса достаточно просто отключить питание от Raspberry Pi Pico и повторно подключить.


▍ Устройство эмулятора


Я разрабатывал эмулятор на современных компонентах и ставил целью только создание прототипа эмулятора «Ну, погоди!». Для прототипов удобны беспаечные макетные платы. Нам понадобится:


  • Raspberry Pi Pico (я пользовался версией с WiFi, но должен подойти и обычный или даже китайский клон),
  • беспаечная макетная плата,
  • 8 тактовых кнопок без фиксации положения,
  • кнопка с фиксацией положения (это необязательно, но позже я расскажу, зачем понадобится эта кнопка),
  • TFT-дисплей (я использовал с драйвером ILI 9341),
  • пассивный зуммер,
  • резистор 100 Ом,
  • резистор 1 кОм,
  • транзистор 2N222,
  • набор соединительных проводов с Dupont-клемами,
  • набор джамперов для беспаечных макетных плат.

Резисторы и транзисторы нужны, чтобы не спалить выход Raspberry Pi Pico и добиться приемлемой громкости звука.


Компоненты, используемые для создания эмулятора

▍ Схема эмулятора на базе Raspberry Pi Pico


Чтобы вы могли быстро повторить схему, приведу схему эмулятора, созданную в Fritzing.


Схема эмулятора, созданная в Fritzing

Также для удобства привожу распиновки Raspberry Pi Pico W, TFT-дисплеев MSP2806/MSP2807 и соединения компонентов в виде таблиц.


Распиновка Raspberry Pi Pico W
Распиновка дисплеев MSP2806/MSP2807

Дисплей MSP2806 отличается от MSP2807 тем, что у MSP2807 сенсорный экран, а у MSP2806 — нет. Для эмулятора нет необходимости использовать сенсорный экран.


▍ Соединение Raspberry Pi Pico и MSP2806


Raspberry Pi Pico
MSP2806
PIN21 GP16 (SPI0 RX)
SDO(MISO)
PIN36 3V3(OUT)
LED
PIN24 GP18 (SPI0 SCK)
SCK
PIN25 GP19 (SPI0 TX)
SDI(MOSI)
PIN26 GP20
DC
PIN27 GP21
RESET
PIN22 GP17 (SPI0 CSn)
CS
PIN23 GND
GND
PIN36 3V3(OUT)
VCC

▍ Соединение Raspberry Pi Pico и кнопок


Raspberry Pi Pico
Кнопки
PIN9 GP6
Будильник, Игра А, Игра Б, Время
PIN10 GP7
↖️, ↙️, ↘️, ↗️
PIN14 GP10
↖️, Будильник
PIN15 GP11
↙️, Игра А
PIN16 GP12
↗️, Игра Б
PIN17 GP13
↘️, Время
PIN11 GP8
Кнопка вечных жизней
PIN36 3V3(OUT)
Кнопка вечных жизней

▍ Соединение Raspberry Pi Pico и модуля зуммера


Raspberry Pi Pico
Модуль зуммера
PIN36 3V3(OUT)
VCC
PIN18 GND
GND
PIN20 GP15
I/O

У меня не было готового модуля зуммера, поэтому я реализовал его на выводных компонентах, которые у меня были. Ниже приведена его схема. Резистор в 100 Ом используется для уменьшения громкости звука.


Схема модуля зуммера

▍ Исходный код


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


▍ Сборка и запуск эмулятора


Если вы хотите собрать мой проект, то нужно выполнить следующие шаги в Linux (Ubuntu):


  1. Устанавливаем зависимости


    $ sudo apt install cmake python3 build-essential gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib
    
  2. Переходим в рабочую директорию


    $ cd ~
    $ mkdir emulator && cd emulator
    
  3. Клонируем Pico SDK


    $ git clone https://github.com/raspberrypi/pico-sdk
    $ cd pico-sdk
    $ git submodule update --init --recursive
    
  4. Клонируем репозиторий с моим эмулятором


    $ cd ..
    $ git clone git@github.com:artyomsoft/pico-nu-pogodi.git
    $ cd pico-nu-pogodi
    git submodule update --init --recursive
    
  5. Собираем эмулятор


    export PICO_SDK_PATH=$(pwd)/../pico-sdk
    mkdir -p build
    cd build
    cmake .. -DPICO_COPY_TO_RAM=1
    make
    
  6. Подключаем Raspberry Pi Pico в режиме загрузки и копируем на него файл pico_nupogodi.uf2


Если у вас не установлен Linux, а посмотреть работу эмулятора хочется, можете скачать уже собранный эмулятор со страницы релизов и записать его в Raspberry Pi Pico.


▍ Вечные жизни


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


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


Благодаря этой кнопке я проверил миф о мультфильме при достижении 1000 очков на практике — мультфильма не было, чего, конечно, и следовало ожидать.


Живой пример с работой эмулятора «Ну, погоди!»


Внешний вид эмулятора приведён ниже. Несмотря на большое количество проводов, они практически не мешают увлекательной игре. Может, в дальнейшем я соберу его на печатной плате. Но на данный момент моё любопытство было удовлетворено, я получил работающий прототип и насладился игрой.


Внешний вид эмулятора

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


Полезные ссылки


  1. Репозиторий с моим эмулятором
  2. Mame
  3. Примеры кода для Raspberry Pi Pico
  4. Raspberry Pi Pico SDK
  5. Raspberry Pi Pico Examples
  6. Raspberry Pi Pico Playground
  7. Raspberry Pi Pico на МК RP2040: начало и первые шаги. Что есть поесть за $4
  8. Cамоучитель по Pico
  9. Генерация заголовочного файла из бинарного
  10. Тема форума по эмулятору «Ну, погоди!»
  11. Отладчик для «Ну, погоди!» и других игр
  12. Список игр Game and Watch
  13. Статья по КБ1013ВК1-2
  14. Работа с LCD на базе 9341ili
  15. Микропроцессорные средства и системы

Заключение


Удивляет и вызывает восхищение, как раньше программисты умудрялись разрабатывать игры при таких ограниченных ресурсах. В 1856 байт уместить полноценную игру, не утратившую свою актуальность и увлекательность в наше время.


Разбираясь с созданием эмулятора, я лучше ознакомился с современным микроконтроллером Raspberry Pi Pico, узнал много фактов, которые уже давно стали историей, получил навыки работы с программой Fritzing, ставшей де-факто основной программой для документирования электронных схем для DIY Pet проектов.


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


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


Я не включил в статью много интересностей, с которыми столкнулся при написании эмулятора, их вы можете найти в разделе «Полезные ссылки».


Мой эмулятор обладает рядом преимуществ:


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

Но главное — я, и, надеюсь, вы тоже, получили удовольствие от кратковременного возвращения в беззаботное детство, в котором игра «Ну, погоди!» была одним из немногих электронных развлечений, доступных большинству.


© 2025 ООО «МТ ФИНАНС»

Telegram-канал со скидками, розыгрышами призов и новостями IT 💻