В ноябре 2019 года я уволился с работы и решил посвятить несколько месяцев изучению нового навыка, которому я уже давно хотел научиться. В то время я работал веб-разработчиком. До этого я изучал разработку ПО. А ещё раньше, детстве, я постоянно экспериментировал с электроникой и программами. Поэтому я достаточно уверенно ощущал себя в создании ПО.
Однако всегда существовала эта волшебная штука под названием «железо», которую я использую ежедневно, но понятия не имею, как она на самом деле работает. Разработчику ПО (и в особенности веб-разработчику) нет необходимости разбираться в оборудовании. Так как весь код веб-разработки очень высокоуровневый, редко оказывается, что проблема нашего ПО связана с аппаратной частью.
Поэтому я никогда не занимался «железом» и не имел никаких общих знаний об электротехнике, кроме тех, которые нам давали в старшей школе. И я хотел изменить эту ситуацию. Я поставил перед собой нелепую цель, которая в то время казалась очень далёкой от моего набора знаний. А потом… я просто начал экспериментировать. Я планировал проверить, как далеко смогу зайти, прежде чем у меня иссякнет запас мотивации. Не ожидал, что достигну своей цели, но каким-то образом мне это удалось.
Вот мой результат:
Моя невероятная цель заключалась в создании с нуля аркадной игровой консоли на четырёх игроков. В качестве мозгов в ней используется микроконтроллер, в качестве дисплея — светодиодные ленты. При помощи одного джойстика и четырёх кнопок на каждого игрока, расположенных по краям стола, игроки управляют игрой, которая динамически загружается в систему с чипа памяти. Благодаря этому игровой процесс на устройстве напоминает старые добрые консоли. Только теперь нам не нужно дуть на картридж, чтобы избавиться от багов.
К моему удивлению… я действительно смог завершить проект. У меня ушло примерно три полных рабочих месяца на циклы экспериментов, провалов, чтения документации, отчаяний и повторных попыток. Надо было бы фиксировать количество рабочих часов, чтобы знать, сколько я потратил на это времени. Предположу, что около 800-900 часов. (Если считать, что я работал по 6/7 дней в неделю в течение 8–10 часов в день, что довольно близко к правде.)
Я пытался документировать процесс своих экспериментов от начала до самого конца, делая фотографии и записывая видео. Этим постом я буду документировать проект и свой опыт для себя. Надеюсь, что для вас он станет источником вдохновения, если вы тоже решились взяться за столь же невероятный проект.
Приступаем к работе
Как и сказал в начале статьи, приступая к проекту, я не обладал никакими знаниями по электронике, кроме неправильно запомненной формулы закона Ома. Поэтому во-первых мне нужно было изучить, как на практике работает электроника, а уже потом пытаться собрать игровую консоль. Я даже не знал, как включить светодиод при помощи микроконтроллера. Итак, мой первый шаг: заказ простой электроники и микроконтроллера для экспериментов. Для начала я попробую запитать светодиод. Я почти уверен, что моя первая попытка включения светодиода привела к его сгоранию. Но это не было задокументировано на камеру, так что притворимся, что этого не было.
Эксперименты с микроконтроллером, резисторами, транзистором и светодиодом.
Это стало первой серьёзной вехой. Теперь мы знаем, как создать код, выполняемый в микроконтроллере. Также мы узнали, как можно использовать транзисторы как простой электрический переключатель для включения светодиода. Восхитительно!
Приключение с EEPROM
Рис. 1. Использование регистров сдвига для управления 16 выходными линиями при помощи всего 3 выходных контактов.
Частью проекта была возможность записи игровой логики на чипы памяти (EEPROM). Они должны были считываться, когда картридж с игрок вставляется в систему.
В долговременной перспективе мне навредило то, что я изучил недостаточно информации о том, какой тип EEPROM нужно использовать в этом проекте. Я был настолько увлечён, что купил старые параллельные чипы EEPROM (W27C512).
Для записи на них или считывания с них мне нужна была возможность устанавливать высокий или низкий сигнал на всех 16 линиях. Если я не хотел занимать все выходные контакты микроконтроллера, логично было бы научиться тому, как использовать регистры сдвига.
Рис. 2. Схема, использованная в неудачной попытке считывания/записи данных на параллельный чип EEPROM.
При помощи всего пары контактов мы можем отдельно управлять всеми 16 линиями.
А теперь давайте применим полученные знания к EEPROM! Да… но нет. Мне так и не удалось стабильно записывать данные в EEPROM. Этому типу чипа требуется 12–14 В для удаления или записи байтов. Я попытался обеспечить их, использовав транзисторы как переключатели для подачи повышенного напряжения. И насколько я понимаю, это ДОЛЖНО сработать. Но не сработало. Я по-прежнему не понимаю, в чём конкретно проблема. На рис. 2 показана схема, которую я пытался использовать для считывания/записи байтов с/на EEPROM.
Рис. 3. Схема, которой успешно удалось считывать/записывать байты на последовательный чип EEPROM
Наконец, изучив больше информации по этой теме, я узнал, что сегодня параллельные EEPROM используются не так часто. Обычно разработчики предпочитают последовательные EEPROM I2C, а следовательно, по ним гораздо проще найти туториалы. (Например, этот потрясающий туториал на Sparkfun).
На рис. 3 показана моя схема для чтения и записи данных на последовательные EEPROM… Она НАМНОГО проще и гораздо надёжнее. Это научило меня, что не стоит слишком уж радоваться заказу деталей и стараться развивать проект слишком быстро. Мы потратили на параллельные EEPROM около 1–2 недель, и в конечном итоге вообще их не использовали. Однако в процессе мы многому научились в электронике и чтении спецификаций (что само по себе является навыком). Усилия не были полностью потрачены впустую.
Так как мы всё-таки это сделаем?
Теперь мы можем выполнять чтение и запись в чипы памяти. Каким будет следующий шаг? На этом этапе будет логично подробнее описать мой план консоли на примере нескольких чертежей.
Общий план. Это будет игровая консоль с контроллерами для четырёх игроков. У каждого игрока есть один джойстик и несколько кнопок.
Красным кругом отмечен разьём для игрового картриджа. У этого разъёма будет четыре металлических контакта, которые консоль использует для чтения последовательных EEPROM, чтобы загружать игры в память.
Зелёным показано, как мы планировали создать экран из светодиодных полос. Мы хотели, чтобы пиксель экрана был квадратным светящимся участком цвета, а не пятном, смешивающимся с другими пятнами.
Чтобы добиться нужного эффекта, мы использовали для отделения источников света друг от друга такую схему. Одновременно она рассеивает освещение, падающее сверху экрана. (Белые полосы под решёткой обозначают светодиодные ленты)
Я решил сделать дисплей размером 42×42 светодиода. То есть в сумме получается 1764 светодиодов. Я выбрал число 42, потому что благодаря этому размер стола будет как раз подходить для четырёх человек.
Подача питания на такое количество светодиодов — сама по себе сложная задача. При максимальной яркости RGB-светодиод потребляет ток 60 мА. (что даёт белый с максимальной яркостью). Если умножить на общее количество светодиодов, то мы получим максимальный потребляемый ток 105,84 А при 5 В! Чтобы приблизиться к такой величине тока, мы приобрели источник питания SE-600–5 MEAN WELL. Он может подавать ток до 100 А. Что меньше, чем теоретически возможные 105 А. Но я не стремлюсь отображать в играх полностью белые экраны с максимальной яркостью. Кроме того, мы сможем ограничить яркость программно. Также мы добавим предохранители, чтобы случайно не превзойти это ограничение в 100 А.
Рис. 4. Картонный прототип для дисплея размером 3×3 светодиода.
К сборке этого экрана я вернусь позже. А сначала нужно заняться прототипированием. Давайте посмотрим, сможем ли мы заставить работать маленький светодиодный экран.
Этот прототип 3×3 собран из нескольких кусков картона, разделяющих пиксели, и куска обычной бумаги для принтеров, рассеивающей свет. Отлично заработало! Мы получили замечательное подтверждение работоспособности концепции.
Для управления светодиодами с помощью микроконтроллера мы использовали библиотеку FastLED. Однако выяснилось, что если светодиодами управляет одна шина данных, то нельзя обновлять 1764 светодиода с достаточной для игр скоростью. Учитывая частоту 800 кГц, мы сможем достичь частоты кадров примерно 17 кадров в секунду. Не совсем то, к чему мы стремимся. Лично мне нравится играть с не менее чем 60FPS. К счастью, это возможно. Достаточно использовать не одну шину данных. Дисплей 42×42 можно удобно разделить на 7 равных частей. Каждой из этих частей можно управлять отдельно, собственной шиной данных. Это возможно благодаря использованию функции параллельного вывода библиотеки FastLED в микроконтроллере Teensy 4. Благодаря использованию 7 шин данных мы можем достичь максимальной частоты обновления в примерно 120FPS!
Я этого не планировал, но по чистой случайности в самом начале проекта выбрал именно этот микроконтроллер. Мне казалось, что для плавной работы игр потребуется быстрое время обработки, обеспечиваемое данным МК. Прототип на рис. 4 уже управляется при помощи функции параллельного вывода. Однако в этом прототипе мы используем только 2 шины данных, просто чтобы протестировать, действительно ли всё будет работать, как заявлено.
Как мы будем исполнять код с EEPROM?
Итак, на данном этапе мы можем считывать и записывать в EEPROM, а также имеем работающее доказательство концепции светодиодного дисплея. Что дальше?
Осталось всего две серьёзные технические задачи: организация способа ввода всех игроков (кнопок и джойстиков). А ещё нам нужно разобраться, как исполнять игровую логику из EEPROM и отображать результаты этих инструкций на дисплее.
Заказанные кнопки и джойстики пока ещё не приехали. Поэтому сначала мы займёмся исполнением игровой логики из EEPROM.
Существует множество способов исполнения инструкций (кода) из EEPROM. Однако сначала нам нужно задать определённые требования:
1) Код должен быть прост в написании.
2) Код должен быть маленьким (размер файла).
3) Код должен уметь взаимодействовать с уже готовыми библиотеками, например, с FastLED, чтобы снизить объём моей работы.
4) Код должен уметь как можно быстрее загружаться в оперативную память, чтобы снизить время загрузки.
5) Необходима возможность эмуляции кода на обычном PC для тестирования игр.
6) Наше решение должно быть высокопроизводительным (обеспечивать как минимум 60FPS).
7) Для решения не должна требоваться покупка большого количества дополнительного оборудования.
8) Решение должно работать на МК Teensy 4.0.
9) Решение должно быть простым в реализации.
Я придумал четыре разных способа выполнения кода из EEPROM:
— Записываем на EEPROM обычный скомпилированный код Arduino. Затем заставляем внешний программатор Arduino считывать скомпилированный код с EEPROM и перепрограммировать Arduino на лету каждый раз, когда загружается новая игра.
— Загружаем ассемблерный код в ОЗУ и исполняем его оттуда.
— Используем готовый интерпретатор кода, например, ArduinoBASIC или Bitlash.
— Пишем собственный интерпретатор кода.
Вот краткое сравнение преимуществ и недостатков четырёх решений для нашего проекта:
(1) Размер файла для решения с внешним программатором ужасен. Вместе с игровой логикой в EEPROM должны храниться все библиотеки кода, которые мы используем в этом проекте. Для отображения состояния игры в каждом EEPROM должна храниться собственная копия библиотеки FastLED. Это совсем не здорово. Вероятно, эту проблему можно обойти, каким-то образом добавив базовый код во внешний программатор, который затем перед программированием Arduino объединяется с кодом в EEPROM. Это была бы задача с высокими рисками, потому что по ней не так легко найти туториалы в Интернете. Вероятно, мы потратили бы на неё слишком много времени.
(2) Выполнение ассемблера из ОЗУ — отличный вариант. Именно поэтому я решил его рассмотреть. Сложность написания кода можно снизить благодаря использованию какого-нибудь высокоуровневого языка, например, C, который затем компилируется в правильный код ассемблера. Однако было непонятно, насколько легко его будет заставить взаимодействовать с другими библиотеками на Arduino, поэтому я решил от него отказаться.
(3) Использование готового интерпретатора — тоже довольно неплохое решение. Однако, насколько я знаю все эти интерпретаторы выполняются на основании строк символов. Эти длинные строки должны записываться в EEPROM. Это не такая уж большая проблема, но определённо не лучший способ минимизации размера файла. Кроме того, было непонятно, могут ли эти интерпретаторы взаимодействовать с библиотеками Arduino. Поэтому я решил, что использование готовых интерпретаторов — не лучшая идея.
(4) Наконец, у нас есть решение 4: создание собственного интерпретатора кода. Оно удовлетворяет всем требованиям, потому что способ реализации интерпретатора целиком зависит от меня. Он будет иметь высокую скорость, если я добьюсь высокой скорости самого интерпретатора. Да, размер файла будет маленьким, и код будет легко писать… если я обеспечу это сам. Другими словами, у нас будет полный контроль над всем. И если я приложу усилия, то всё получится идеально. Единственным серьёзным недостатком такого решения будет долгое время разработки. Возможно, вы помните, что я потратил на этот проект 800–900 часов. То есть очевидно, что я решил выбрать решение 4 — создать собственный интерпретатор кода. Время не было для меня проблемой. Я воспользовался этой возможностью узнать, как создаются языки программирования. Я узнаю, почему в этих языках было принято то или иное архитектурное решение.
Рождение ArcadableScript
Общий принцип работы интерпретатора довольно прост. Я не буду вдаваться в подробности его внутреннего устройства, потому что на момент написания статьи планировал полностью переписать интерпретатор, чтобы он стал более эффективным и для него было проще писать код. Однако основы остаются теми же. Интерпретатор будет исполнять для каждого такта игры простой цикл кода:
Большой недостаток такой схемы заключается в том, что ввод и состояние игры проверяются только раз в кадр. Для игр, не требующих быстрой реакции, это нормально. Однако в других играх игрок не сможет реагировать между кадрами. Эту проблему я решу в следующей версии интерпретатора.
Рис. 5
На диаграмме с рис. 5 показано, как я планировал модернизировать интерпретатор в ближайшем будущем, создав два отдельных цикла для игрового состояния и игровых кадров. Это позволит нам обновлять состояние игры сотни раз в секунду, а дисплей — только 60 раз в секунду.
К сожалению, на Teensy 4.0 невозможно выполнять аппаратный многопоточный код. Поэтому мы не сможем выполнять эти два цикла параллельно. Но я уверен, что что-нибудь придумаю.
Спустя какое-то время мне удалось написать две простые программы, использующие мой собственный придуманный язык байт-кода. Они используют в качестве входящих данных сырые числа, чтобы максимально снизить размер файла. То есть я завершил написание этих двух программ, в буквальном смысле записывая списки чисел, которые может понять интерпретатор. Чтобы дать представление о том, насколько читаем этот байт-код, я продемонстрирую настоящие инструкции, используемые в примере программы, двигающей точку по экрану:
// TODO: Write/read all untyped... data to/from ROM int untypedGamestate[] = { 0, // Previous time, to keep track of game time/framerate. 0, // Player position x. 0, // Player position y. 0, // Player R color value. 0, // Player G color value. 255, // Player B color value. }; int untypedValues[][3] = { // ID, Type, Value {0, 0, 0}, // Move up button value {1, 0, 1}, // Move right button value {2, 0, 2}, // Move down button value {3, 0, 3}, // Move left button value {4, 3, 1}, // True/1 {5, 3, 0}, // False/0 {6, 4, 0}, // Current millis since game start {7, 2, 0}, // Gamestate previous time {8, 2, 1}, // Gamestate player x {9, 2, 2}, // Gamestate player y {10, 2, 3}, // Gamestate player r color {11, 2, 4}, // Gamestate player g color {12, 2, 5}, // Gamestate player b color {13, 3, 1}, // Move player up after button press boundary check {14, 1, 0}, // System config screen width {15, 1, 1}, // System config screen height {16, 3, 3}, // Move player down after button press boundary check {17, 3, 5}, // Move player left after button press boundary check {18, 3, 7}, // Move player right after button press boundary check } int untypedCalculations[][5] = { // ID, ending, valueLeftID, calculationRightID, calculationOperator {0, 0, 9, 1, 1}, // Current player y position - 1 {1, 1, 4, 0, 0}, // True/1 {2, 1, 0, 0, 0}, // Up button {3, 1, 2, 0, 0}, // Down button {4, 1, 5, 0, 0}, // False/0 {5, 1, 9, 0, 0}, // Current player y position {6, 0, 15, 1, 1} // screenheight - 1 {7, 0, 9, 1, 0}, // Current player y position + 1 {8, 1, 3, 0, 0}, // Left button {9, 1, 1, 0, 0}, // Right button {10, 1, 8, 0, 0}, // Current player x position {11, 0, 8, 1, 0}, // Current player x position + 1 {12, 0, 8, 1, 1}, // Current player x position - 1 {13, 0, 14, 1, 1} // screenwidth - 1 } int untypedInstructions[][10] = { // ID, rootInstruction, conditionCalculationLeftID, conditionCalculationRightID, conditionOperator, conditionSuccesValueLeftID, // conditionSuccessCalculationRightID, hasFailedCondition, conditionFailedValueLeftID, conditionFailedCalculationRightID {0, 1, 2, 1, 0, 13, 0, 0, 0, 0}, // move player up when up button is pressed. {1, 0, 5, 4, 1, 9, 0, 0, 0, 0}, // move the player up when the boundary is not reached. {2, 1, 3, 1, 0, 16, 0, 0, 0, 0}, // move player down when down button is pressed. {3, 0, 5, 6, 1, 9, 7, 0, 0, 0} // move the player down when the boundary is not reached. {4, 1, 8, 1, 0, 17, 0, 0, 0, 0}, // move player left when left button is pressed. {5, 0, 10, 4, 1, 8, 12, 0, 0, 0}, // move the player left when the boundary is not reached. {6, 1, 9, 1, 0, 18, 0, 0, 0, 0}, // move player right when right button is pressed. {7, 0, 10, 13, 1, 8, 11, 0, 0, 0}, // move the player right when the boundary is not reached. };
Если вы уделите время внимательному изучению кода, то сможете понять, как работала раннаяя версия этого интерпретатора. Общий принцип использования «значений», «вычислений» и «инструкций» сохраняется и в текущей версии. Однако много изменилось. У нас больше нет фиксированного списка значений «gamestate», теперь они являются просто частью обычного списка значений. Кроме того, мы отделили «условия» от инструкций. Благодаря этому мы можем многократно использовать инструкции в коде, что снижает размер файла.
Давайте не будем вдаваться в подробности работы интерпретатора, потому что в переделанной версии всё это вскоре изменится.
Теперь мы находимся на этапе, когда знаем, как читать/записывать в EEPROM, и можем выполнять инструкции на основании массивов обычных чисел. Следующим логичным шагом станет устранение из кода жёстко заданных списков инструкций и запись их в EEPROM. С этого момента мы сможем пробовать считывать и выполнять инструкции, сохранённые в EEPROM.
На этом этапе у нас есть все доказательства работоспособности концепций, которые необходимы нам, чтобы приступить к созданию окончательного результата!
Очевидно, каждая созданная нами отдельная часть прототипа по-прежнему требует много работы. Но, по моему мнению, самые сложные задачи уже решены. Всё, что мне оставалось делать — развить то, что у меня уже было. Всё просто!
Сборка дисплея из светодиодных лент
На сборку дисплея потребовалось много труда. ОЧЕНЬ много труда. Начал я с того, что пришлось припаивать провода к 42 отдельным полосам. Для каждой полосы требуется три провода с одной стороны и два с другой. То есть всего около 200 проводов. До этого проекта у меня было не так много практики в пайке, так что вы чётко можете заметить, что чем больше я занимаюсь пайкой, тем сильнее растёт качество. Разумеется, в конечном итоге пришлось переделать множество первых светодиодных полос, потому что мне не понравился результат. Всё это часть процесса учёбы!
Следующий этап: мы прикрепляем все светодиодные полосы на большую деревянную панель. Оглядываясь назад, я думаю, что возможно, ради лучшей теплопроводности стоило использовать металлический лист, потому что после часа работы дисплей сейчас становится тёплым (40–50°C). Но такое всё равно случается не так часто, поэтому большой проблемы не представляет. Однако если бы я решил повторить проект, то исправил бы этот аспект.
Далее мы крепим к положительным и отрицательным проводникам светоиодных полос провода большего диаметра. Надо убедиться, что провода большого диаметра надёжно выдерживают максимальный ток 100 А. Для дополнительной защиты мы также добавили в каждую часть дисплея предохранители на 15 А.
На этом этапе мы готовы предпринимать первые попытки управления дисплеем. После множества неудач и манипуляций с параметрами ПО и проводами, мне наконец удалось отобразить на дисплее один цвет без искажений и помех. На это потребовалось много времени, и результат всё равно был далёк от совершенства. У меня долгое время по-прежнему возникали проблемы с искажёнными сигналами, пока я не опубликовал вопрос об этом на electronics.stackexchange.com. Замечательные люди с этого сайта помогли мне диагностировать проблему. Я всё ещё не до конца понял, в чём она заключается, но присоединив отрицательные выводы непосредственно вдоль шин данных от заземления МК к заземляющему выводу рядом с входом данных, я смог её решить. С тех пор у меня не было никаких проблем с искажениями на дисплее.
Как мы видели из показанной выше иллюстрации, поверх светодиодных полос наложено два слоя материала.
В качестве материала для этого покрытия нам нужно что-то прочное, прозрачное, но рассеивающее свет. Однако я не хотел использовать стекло, потому что оно дорогое и хрупкое. Поэтому я решил взять стандартный прозрачный полистирол. Чтобы он рассеивал освещение, я обработал его мелкой шкуркой до такой степени, чтобы сквозь нельзя было смотреть. Всё очень просто.
Гораздо больше времени ушло на создание решётки, которая будет находиться непосредственно поверх полос. Я думал просто заказать на заказ решётку с нужными параметрами, но мне назвали цену примерно 500 евро. По моему мнению, она того не стоила. Поэтому пришлось собирать её самим. Для этого нам потребуются тонкие (не более 2 мм толщиной), прочные, непрозрачные белые пластмассовые полосы. Довольно много требований. Поэтому подбор нужного материала занял много времени. Как выяснилось, идеально подходят полосы от оконных жалюзи (ламели).
Сначала мы изготовили небольшой держатель, чтобы сделать на широких полосах равномерные ровные отрезы. При этом получились достаточно мелкие пластмассовые полоски. Затем нам понадобилось сделать надрезы до середины каждой полоски точно в нужных местах. Это необходимо, чтобы соединить куски пластмассы и собрать решётку. Этим я и занимаюсь на третьей фотографии. Прежде чем делать это, нужно убедиться, что ширина пилы такая же или чуть больше, чем ширина полоски, которую вы надрезаете. В противном случае решётку собрать не удастся. В моём случае оказалось, что идеальную толщину имеет ножовка по металлу.
На четвёртой фотографии виден результат сборки всех полосок в решётку. Это была очень раздражающая и монотонная работа, но решётка получилась очень прочной. В целом похоже, что все ячейки одинакового размера. Большого разброса между размерами ячеек нет, иначе бы дисплей выглядел странно, поэтому всё оказалось довольно неплохо. Я доволен этой решёткой.
Затем я заключил решётку в деревянную раму, выполняющую несколько задач. Во-первых, она удерживает лист полистирола наверху решётки. Она гарантирует, что решётка не сдвинется. Кроме того, деревянная рама прижимает все провода, чтобы они держались надёжнее и не могли случайно оторваться (что случалось пару раз, когда я двигал дисплей).
На этой фотографии показан зашкуренный лист полистирола, положенный на решётку. Заметьте, что решётка теперь едва видна, этого мы и хотели.
Потратив долгие часы на эксперименты с правильной реализацией управления дисплеем, нам наконец удалось заставить его работать. Огромной проблемой стало сохранение целостности сигналов. Но как я сказал выше. сообщество с сайта electronics.stackexchange помогло мне с этим!
Дисплей яркий, намного ярче, чем я ожидал. Надо было это предвидеть, заказывая для его питания блок на 100 А.
Как я уже несколько раз говорил, для управления светодиодами мы используем библиотеку FastLED. Но я не упоминал того, что также для простой отрисовки фигур на дисплее я использую изменённую версию библиотеки FastLED-GFX Юргена Скроцки (которая сама является портом библиотеки Adafruit-GFX-Library). Изменения в этой библиотеке незначительны, но они были необходимы для того, чтобы интерпретатор мог общаться с библиотекой удобным образом.
Мозгиии…
Одна из последних технических задач, которую нужно решить для завершения этого проекта — доделка мозгов консоли. Нужно превратить их в один удобный и компактный набор. Но прежде нам нужно разобраться, как обрабатывать все сигналы ввода игроков. Напомню, что у каждого игрока есть по 4 кнопки и одному джойстику. То есть всего 16 цифровых сигналов ввода и 8 аналоговых. У МК никогда не бывает достаточное для такого ввода контактов; кроме того, два контакта шин данных уже используются для чтения EEPROM, а 7 контактов параллельных шин данных необходимы для управления дисплеем.
Чтобы решить эту проблему, мы используем для цифрового ввода пару регистров сдвига. А для всех аналоговых вводов мы возьмём аналоговый мультиплексор.
Схема для одновременных чтения/записи EEOPROM, обработки ввода игроков и управления дисплеем.
И тут начинается неразбериха. Позвольте мне объяснить, что происходит на этой фотографии, слева направо. В верхней части левой макетной платы находятся 7 шин данных, используемых для управления дисплеем. Ниже расположена область, куда может вставляться EEPROM и где он считывается.
В верхней части средней макетной платы расположен аналоговый мультиплексор. Этот мультиплексор может получать до 16 аналоговых сигналов ввода, и в зависимости от входящих сигналов на мультиплексоре подключать один из аналоговых сигналов к МК. Таким образом, нам нужен только один контакт аналогового ввода и пара контактов цифрового ввода для обработки 16 аналоговых вводов. Похоже, на этой фотографии к мультиплексору не подключен ни один аналоговый ввод.
Под мультиплексором находится МК Teensy.
На правой макетной плате мы обрабатываем цифровые сигналы ввода. На этой фотографии 16 цифровых сигналов просто подключены к заземлению. Я всё ещё ждал прибытия кнопок, поэтому так тестировал все 16 цифровых сигналов. Благодаря регистрам сдвига для управления всеми цифровыми сигналами потребовалось всего 4 контакта.
Вся система представляет собой огромный ненадёжный хаос. Нужно сделать её более компактной, чтобы она не разваливалась от одного неосторожного выдоха. Поэтому я заказал несколько плат для прототипирования и снова приступил к пайке!
И это оказалось трудной задачей! Однако это был отличный повод научиться точной пайке.
Чтобы придумать эту схему, я воспользовался в качестве примера хаосом из проводов на макетных платах. Затем я попытался придумать более умную схему, чтобы компоненты, которые должны общаться друг с другом, находились как можно ближе. Таким образом я пытался избавиться от необходимости тянуть слишком много проводов. Теперь я думаю, что для проектирования схемы можно было воспользоваться какими-нибудь инструментами моделирования. Но всё равно вышло неплохо.
Я решил спроектировать двухслойную схему. На нижней прототипной плате обрабатывается весь ввод. На каждом из 4 входных соединений у меня есть 8 линий ввода (1 — заземление, 1 — положительная, 4 — цифровой ввод, 2 — аналоговый ввод). МК общается с нижним слоем при помощи четырёх соединений рядом с регистрами сдвига и четырёх соединений рядом с мультиплексором.
На верхнем слое расположены подача питания, справа находятся 7 выходных шин данных для дисплея, а снизу находятся контакты для считывания EEPROM. И наконец, в центре, разумеется, расположен микроконтроллер.
Между основными управляющими платами и вводом игроков я разместил промежуточные платы, чтобы снизить количество необходимых проводов с обеих сторон с 12 до 8, потому что необязательно использовать все 5 проводов заземления, идущих от основной платы к плате ввода. Вполне достаточно одного заземления, используемого для всех кнопок и джойстика. Поэтому всего есть четыре такие промежуточные платы. Мне нравится, что можно заметить повышение качества каждой следующей собираемой платы.
Так и не получив заказанные кнопки, я заказал у другого продавца новых набор и всего спустя пару дней получил его. Это позволило наконец начать тестировать все кнопки.
На этом этапе я также написал первую игру, в которую можно сыграть на консоли. Да, теперь я называю её консолью.
Мне это удалось, потому что в процессе работы над дисплеем я параллельно совершенствовал интерпретатор. В частности, у меня получилось улучшить сам процесс разработки при написании кода. Этот процесс не так интересен внешне, поэтому я решил не особо его документировать.
Я создал простую среду разработки для написания кода, который может выполняться на консоли.
Эта среда работает в браузере, и она позволяет писать код для консоли, не связываясь с хаотичными списками чисел, которые мы видели выше. Кроме того, я создал эмулятор консоли, запускаемый в том же веб-приложении.
Возможность эмуляции игр для консоли в веб-приложении позволила сэкономить много времени, потому что отладка на PC выполняется гораздо проще, чем на МК.
Эмулятор не идеален. Присутствуют некоторые баги, отличающие эмуляцию от игры на реальной версии консоли. Я попробую устранить их в будущем, когда буду писать новую версию интерпретатора. Необходимость поддержки двух версий интерпретатора (одна написана на C++, другая — на TS) немного напрягала, но дело того стоило.
Собираем всё вместе
Всё готово. Мы можем отображать изображения, на основании взятой из EEPROM игровой логики, и обрабатывать ввод игроков с кнопок и джойстиков. Теперь нам нужно соединить всё это в одно прочное устройство.
Я начал с просверливания отверстий в алюминиевых пластинах, которыми будут крепиться кнопки и джойстики. Затем можно было вставить эти пластины в пазы деревянного корпуса, в которых они удобно утапливались. Благодаря этому игроки не смогут случайно поцарапаться об металл.
Собираем всё вокруг дисплея и добавляем надёжные складываемые ножки (для удобства хранения). Покрашенные чёрной краской металлические пластины выглядят очень красиво. Появляется ощущение, что проект действительно движется к завершению.
Но я конечно ошибался, полагая, что проект почти закончен. Вот как выглядела консоль примерно в течение целого месяца.
Собираем всё, понимаем, что кнопка не работает. Снова разбираем, устраняем проблему с кнопкой. Похоже, всё работает. Понимаем, что не работает джойстик. Снова разбираем. Рано или поздно все кнопки и джойстики начинают работать. Но тут перестают работать светодиоды. Чтобы починить их, нужно разобрать консоль целиком и заново собрать с самого начала. Так продолжалось около месяца, пока, наконец, всё не заработало.
Результат
Всё готово! После начала работы над проектом уже прошло 3-4 месяца. Поэтому логично, что моя мотивация продолжать улучшать интерпретатор, красить дерево и разрабатывать новые игры уже иссякла. На момент написания статьи уже прошёл месяц, когда я последний раз занимался проектом. Я наконец-то набрался сил для написания этого документирующего поста для себя и тебя, мой читатель. Хоть я и не ожидаю, что кто-то в здравом уме на самом деле прочитает всё то, что я написал, надеюсь, вам понравилось рассматривать фотографии постепенного развития проекта.
Но если вы всё это прочитали, то, возможно, вас заинтересует и Github-репозиторий проекта. Разумеется, весь мой код имеет открытые исходники! https://github.com/Arcadable
Дальнейшие планы развития проекта:
— Во-первых, нужно сделать более красивый картридж для игры в Pong на четверых, которая показана в последнем видео. Пока он представляет собой кусок картона с загнутыми кусками алюминиевой фольги, которые используются в качестве контактных поверхностей.
— Кроме того, я хочу качественнее покрасить консоль. Изначально я планировал красить её с очень опытными друзьями, которым нравится красить. Однако сейчас из-за пандемии это невозможно.
— Наконец, как я уже пару раз говорил, я собираюсь переписать интерпретатор более эффективным образом. Также я усовершенствую среду разработки, чтобы создавать игры было проще. Сейчас дизайн веб-приложения мешает продуктивности.
Котокомпенсация за запись вертикальных видео.