Ретро-разработка для первой портативной консоли из далекого 1979 года

41yfickhp7ywna9ls3tj4tigy24.jpeg

В 1974 году Texas Instruments выпускает первые 4-битные микроконтроллеры семейства TMS1000, а Intel в 1976 начинает производство 8-битных микроконтроллеров своей известной серии MCS-48. И тут началось.

Из-за дешевизны и самодостаточности микроконтроллеров (тогда они назывались прямо — однокристальный микрокомпьютер), потребительские электронные устройства резко поумнели, а их количество росло как никогда раньше. С появлением микроконтроллеров возник такой класс устройств как портативные электронные игры, масштабы «бедствия» можно оценить по этой ссылке. Среди всего этого многообразия ранних игр, выделялась одна, о которой я расскажу в этом посте — это Milton Bradley Microvision, первая портативная электронная игра со сменными картриджами, в которой использовались оба упомянутых выше микроконтроллера. Я также постараюсь подробнее остановиться на специфике разработки для этой консоли.

Общее описание


9_vzpzmfre7fck4ey-suxwkrgna.jpeg

Microvision была выпущена в США почти сорок лет назад, в 1979 году. Всего для нее продавалось 12 картриджей с играми, среди которых были — идущий в комплекте с консолью Block Buster (аналог Breakout), спортивные игры — Bowling и Baseball, электронный вариант известной настольной игры Connect Four, Pinball и другие. Большинство игр можно оценить на эмуляторе MVEM, который был сделан на основе интересной серии публикаций, из которых я тоже почерпнул много полезного. Здесь же я не буду подробно останавливаться на описании оригинальных игр консоли, а сразу перейду к ее внутреннему миру.

grgulbct_3jmlu9830qx2upfble.jpeg

Важной особенностью консоли является 2-дюймовый жидкокристаллический дисплей с разрешением 16×16 пикселей. Довольно примитивно по сегодняшним меркам, но в сравнении с используемыми тогда в портативных играх светодиодными сборками и вакуумно-люминесцентными индикаторами, матричный ЖК экран был довольно прогрессивным шагом. Кроме Microvision, в том же году, появилась в продаже серия электронных игр Mego Mini-Vid с похожим экраном, правда 13×20. По-видимому, на тот момент, это были единственные устройства в широкой продаже с матричными ЖК такого разрешения.

gauhtvjy3-ltagudiptipvvl6lm.jpeg

Дисплей управляется микросхемой Hughes SCUS0488 — это драйвер матричного ЖКД. Питание драйвера обеспечивается стабилизатором отрицательного напряжения UA79MG с обвязкой из пары конденсаторов и резисторов — вот и вся нехитрая элементная база.
wslipbri9usvwd5o8eztgddsnry.jpeg

В корпусе консоли располагаются так же органы управления — кнопочная матрица 4×3 и переменный резистор на 10кОм в качестве пэддла. Для воспроизведения звука имеется пьезодинамик.

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

12keqw-kmdbgoo85q50i2yunnjc.jpeg

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

ubodouuncl6f7az9o4v8n2slwuq.jpeg
Плата игры «Block Buster», в корпусе и с обратной стороны

Электронная часть картриджа строилась на одним из двух микроконтроллеров — TMS1100 или Intel 8021 с необходимой обвязкой. Размещение «мозга» консоли в картридже может показаться несколько странным решением, но оно позволяло обойтись только двумя микросхемами на игру. К тому же, это также добавляло универсальности. При этом цена картриджа из-за наличия микроконтроллера возрастала, по-видимому, не сильно (например, стоимость 8021 в крупных партиях в 1976 году составляла около $3).

Питало все это одна или две соединенных параллельно кроны (8021 был довольно прожорлив). Причем в более поздних версиях контакты для второй батареи были убраны, а оставшееся место рекомендовалось использовать для запасной батарейки. По всей видимости, это было связано с тем, что пользователи часто путали полярность, получая довольно опасное КЗ.

Завязка


Я купил эту древнюю консоль с той целью, чтобы написать для нее какую-нибудь игру и сделать картридж, причем по возможности максимально скопировав элементную базу оригинала.
Но здесь есть одна существенная проблема — что TMS1100, что Intel 8021 имели масочное ПЗУ, т.е. программировались на заводе в процессе изготовления. Для микроконтроллера от Intel есть выход: 8021 это урезанная версия 8048, которая тоже имела масочное ПЗУ, но Intel выпускала аналог 8048 с программируемым ПЗУ — 8748, причем как в варианте с ультрафиолетовым стиранием так и более дешевые однократно программируемые.
Для TMS1100 ситуация, к сожалению, гораздо хуже — существовала отладочная версия чипа, работающая с внешним ПЗУ — TMS1098, но купить ее сейчас если и возможно, то очень трудно. К тому же, микросхема была выполнена в корпусе DIP-64, он сам по себе огромный и не влезет в картридж по длине, а еще нужна немаленькая ПЗУ.
В общем, уф-стираемая 8748 это то, что нужно, а окончательный вариант картриджа можно сделать на нестираемой версии.

kggsjpioqodpscq5i9orcdp_0im.jpeg
Сверху однократно-программируемая P8748H, а снизу уф-стираемая D8748H

Intel 8021


Ниже приведу краткое описание именно 8021, а не 8748, т.к. я буду использовать только урезанные возможности 8021 для того, чтобы иметь лишь изначально заложенные разработчиками консоли ресурсы.

Подсистема памяти, как и у всего семейства MCS-48, основана на модифицированной гарвардской архитектуре. Память программ — внутреннее ПЗУ объемом 1024 байт, память данных — 64 байта динамической ОЗУ.

Организация ОЗУ показана на следующем рисунке:

ak9m2y_m9qmrnnwbj8a8f55slf4.png

Ячейки 0–7 занимают прямо адресуемые рабочие регистры R0-R7, причем R0 и R1 могут использоваться как указатели для косвенного доступа ко всем ячейкам ОЗУ. Ячейки 8–23 используются для 8-уровневого стека вызовов, хотя их тоже можно использовать через R0-R1.

Микроконтроллер имеет встроенный тактовый генератор, опорная частота задается внешним кварцем, RC-цепочкой или LC-цепочкой. Машинный цикл длится 10 тактов, а каждый такт занимает 3 периода опорной частоты. Максимальная частота 3.58Мгц, при этом машинный цикл длится 8.38 мкс. Минимальная частота ограничена спецификой работы DRAM и составляет 600 кГц.

8021 содержит два 8-битных порта и один 4-битный, который может так же использоваться для подключения расширителя портов ввода/вывода, микросхемы 8243. Все порты квазидвунаправленные.

Кроме этого микроконтроллер имеет встроенный восьмиразрядный таймер/счетчик. В режиме таймера, счетчик T увеличивается на 1 каждые 32 машинных цикла. При переполнении T устанавливается флаг TF. В режиме счетчика подсчитываются импульсы, поступающие на тестируемый вход T1.

Система команд содержит 64 инструкции, из них 36 выполняются за один цикл, а 28 за два. Большинство инструкций однобайтные.

Список инструкций с краткими описаниями
dylwhdauhna9z62ivd8z2slns-s.png

Подготовка


С покупкой 8748 никаких проблем не возникло, главное обращать внимание, что микросхемы выпускались по разным технологиям. Самые ранние NMOP, требовали для прошивки напряжение 25В, маркировались D8748. К концу 70-х эти микроконтроллеры стали строиться по технологии HMOP-E (улучшенная версия NMOP от Intel), маркировались D8748H и требовали уже 21В. Такое же напряжение требовал более поздний клон от NEC (mPD8748H). Однократно программируемая версия маркировалась P8748H.

УФ-стиратель был куплен самый простой и дешевый китайский с механическим таймером, который, как показала практика, со своей задачей справляется отлично (хотя точность таймера на коротких интервалах никакая). ПЗУ микросхем он надежно стирает за ~2.5 минуты. Позже, я купил за символическую цену с японских рук стиратель ZAX Quick-EII, даже не знаю какого он года, по виду начало 90-х. Он использует ксеноновую вспышку и стирает 8748 за 3(!) секунды, буквально. Видео (не мое) с демонстрацией работы можно посмотреть здесь.

Основной проблемой оказался программатор. Поддержка этого устаревшего семейства Intel у современных программаторов имеется только при стоимости от 300$. Хотя, есть относительно дешевый любительский Willem, который через переходник может работать с MCS-48, но ему нужен LPT, что меня совершенно не устраивало. Пришлось паять самому. Я, скажем так, начинающий радиолюбитель, поэтому провозился с ним около недели, угробив одну из двух приехавших к тому времени 8748 (хотя я тешу себя мыслью, что она изначально была такая). За основу взял опубликованную здесь схему, только адаптировал ее под Atmega и более удобное 24В питание. Все это спаял на макетной плате:

5cttutbzs817thtcpq37vlakkx0.jpeg

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

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

Картридж


Основная масса картриджей работала на TMS1100, т.к. Signetics, которая выпускала 8021 по лицензии Intel, не могла обеспечить нужного MB объема поставок чипов. Даже некоторые игры, которые уже были написаны для 8021, пришлось портировать на TMS1100. Это, кстати, позволило отказаться от опасной связки из двух батарей, ведь потребление микроконтроллера от TI было всего 0.1Вт против 1Вт у Intel. У меня из 6 картриджей только один с 8021, это игра Connect Four, она и была взята за основу.

Я старался сделать плату максимально похожей на оригинал, но, конечно, пришлось вносить изменения. Во-первых, размер микросхемы (DIP-40 против DIP-28) и разная распиновка. Во-вторых, пришлось поменять номиналы в колебательном контуре, задающем тактовую частоту, т.к. машинный цикл у HMOS версий микросхем составляет 15 тактов, а у MOS версий, которые использовались в оригинальных картриджах, 30 тактов. Поэтому, для полной аутентичности мой картридж будет работать на 1.25 МГц против оригинальных 2.5 МГц, обеспечивая при этом ту же производительность.

bz9fzxh_7-9-djp9yamoeog5tfe.jpeg

На фотографии выше готовые печатные платы заказанные в Китае, а пока они шли, я нарисовал и вытравил плату и собрал вот такой «отладочный комплекс»:
yhgxvntybge5njn2cw3dppmogrs.jpeg

Наконец-то хардварная часть готова и можно начинать программировать.

Дисплей


wag6jx-tblcypxvqvgmz-pwau_q.jpeg

Первым делом, надо было разобраться с выводом на ЖК дисплей. Как я писал выше, он управлялся драйвером Hughes 0488 используя мультиплексирование (недавно на Хабре была опубликована интересная статья про этот способ управления ЖК). Именно драйвером, а не контроллером, так что нельзя просто включить какой-то пиксель и идти заниматься своими делами, необходимо постоянно низкоуровневыми командами обновлять содержимое экрана с частотой 30–50Гц.

Схема подключения выглядит следующим образом:

ulymrgc1oyensrme_-wjhprv2_u.png

Распиновка H0488:
Vdd — питание (3–8В)
R1-R16 — Выходы управления строками
C1-C16 — Выходы управления столбцами
DATA0-DATA3 — Шина данных
! Data Clk — Вход тактового сигнала записи данных
Latch Pulse — Сигнал фиксации состояния выводов R0-R15, C0-C15

Смысл работы микросхемы следующий:
Через 4 информационные линии последовательно задается необходимое состояние всех 32 выходов микросхемы (по 16 на строки и на столбцы). Т.е. мы 4-битными порциями указываем состояние выходов для строк 1–4, затем 5–8, 9–12 и наконец для 13–16, и так же 4 раза по 4 бита мы указываем состояние столбцов. Каждая новая порция данных тактируется импульсом по линии ! DATA CLK. После того, как мы отправили все 8 порций данных, импульсом по лини Latch Pulse устанавливаем указанное состояние выходов R1-R16, C1-C16 которое будет удерживаться до следующего импульса по этой линии.
Естественно, за один такой цикл мы не можем указать состояние каждого отдельного пикселя независимо от других, сможем только 16 (по количеству пересечений строк со столбцами). Поэтому для независимого обновления состояния каждого пикселя потребуется выполнить 256/16 = 16 циклов.
Кроме этого на нас лежит ответственность переключать полярность, чтобы исключить постоянное напряжение электродах ЖКД, иначе дисплей начнет быстро деградировать. Переключение полярности происходит по заднему фронту сигнала на Latch Pulse при низком уровне на входе! Data Clock. Полярность рекомендуется переключать с частотой обновления экрана.
Вышесказанное иллюстрируется следующей временной диаграммой из даташита:

58okzuokeat3bg6nf98bppvxpco.png

Все это, реализованное на ассемблере 8021 (который, кстати, существенно урезан по сравнению с ассемблером остального семейства, все для той же аутентичности я использовал только инструкции, поддерживаемые 8021), может выглядеть примерно так:

     mov R0, #32         ;Указатель на изображение в ОЗУ.
        mov r1, #10000000b  ;Положение установленного бита в r1-r2
        mov r2, #00000000b  ;определяет текущую строку.
        mov r4, #00000010b  ;Вторым битом будем дергать !DATA CLK.
        mov r3, #11110000b   
        clr c       
loop:           
        ;Устанавливаем значения выводов r1-r16 для текущей строки.    
        mov a, r1           ;Старшим полубайтом регистра r1
        loadNibble          ;устанавливаем уровни для выводов R1-R4.
        mov a, r1           ;Берем младшие 4 бита регистра r1    
        rrc a               ;и сдвигаем готовя его к следующей итерации,
        xch a, r1           ;а текущее содержимое используем сейчас.
        swap a                
        loadNibble          ;Устанавливаем уровни для выводов R5-R8.
        mov a, r2           ;Старшим полубайтом регистра r2
        loadNibble          ;устанавливаем уровни для выводов R9-R12.
        mov a, r2           ;Берем младшие 4 бита регистра r2
        rrc a               ;и сдвигаем готовя его к следующей итерации, заодно
        xch a, r2           ;устанавливаем флаг C, если у нас последняя строка.
        swap a
        loadNibble          ;Устанавливаем уровни для выводов R13-R16
        
        ;Устанавливаем значения выводов C1-C16 для столбцов на текущей строке.
        mov a, @R0          ;Берем байт содержащий левую половину выводимой строки
        loadNibble          ;и старшими битами устанавливаем уровни для выходов C1-C4.
        mov a, @R0
        swap a              ;Теперь нужны младшие 4 бита.
        loadNibble          ;Устанавливаем ими уровни для выходов C4-C8.
        inc R0              ;Переходим ко второму байту строки
        mov a, @R0          ;Берем байт содержащий правую половину строки,
        loadNibble          ;от него сейчас нужны только старшие 4 бита.
        mov a, @R0
        swap a              ;Теперь нужны младшие 4 бита.
        loadNibble          ;Устанавливаем ими уровни для выходов C9-C12.
        inc R0              ;Переходим к следующей строке
        
        inc a               ;Фиксируем состояние выходов R0-R15 и C0-C15,
        outl p1, a          ;подняв на Latch Pulse логическую 1.
        
        jnc loop            ;Переходим к новой строке если не последняя.
                
        clr a               ;
        outl p1, a          ;Меняем полярность.
        inc a               ;
        outl p1, a          ;
        
loadNibble macro
        anl a, r3           ;Нам нужен только старший полубайт.
        outl p1, a          ;Отправляем его в порт.
        orl a, r4
        outl p1, a          ;Дергаем !DATA CLK.
        endm

Это мой самый быстрый вариант подпрограммы выводящей на дисплей изображение, хранящееся в ОЗУ. Она выполняется за 1152 машинных цикла (который в нашем случае равен ~12 мкс). Максимальная частота обновления экрана получается около 70 кадров в секунду (если микроконтроллер больше ничего не будет делать), что в принципе избыточно, поэтому, для экономии регистров и ПЗУ, я на практике использовал другие подпрограммы — более медленные и более подходящие под конкретные задачи. Зато, при такой частоте, с учетом высокой инертности этого дисплея, можно выводить картинки с 4-мя градациями серого (как на КПДВ), быстро меняя кадры. Или даже короткие четырехцветные анимации:

jl9v1zg4fi77u_67qendhuxbtt0.gif


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

Клавиатура


yh96k4mzlmmxlt7bczu5whiozcg.jpeg

Тут все гораздо проще. Кнопки сгруппированы в матрицу из 4 строк и 3 столбцов. Строки подключены к выводам P0.4-P0.7, столбцы к P0.0-P0.2.

zk2bcyshihgdt_h5ygvmxyu4gqw.png

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

     mov a, #01111111b   ;Отправляем 0 на первую строку а на все столбцы 1,
        outl p0, a          ;для того, чтобы можно было прочитать их состояние.
        in a, p0            ;Читаем порт, 0 на одном из трех младших разрядов
                            ;соответствует нажатой кнопке в сканируемой строке

Пэддл


4nckj1to9wuxdqnp4qbuzr4zrku.jpeg

Или, по-русски, колесо-манипулятор, установленный в нижней части консоли, представляет собой переменный резистор на 10кОм с декоративной ручкой, вращая которую можно чем-то управлять в игре. Конечно, никакого АЦП ни в 8021, ни в TMS1100 не было. Угол поворота определялся по скорости заряда конденсатора, который предусмотрительно был распаян на плате консоли.

hekhk2fa7hrnv1odcm5lxximh7m.png

Работает это все следующим образом: пока на выводах P2.2-P2.3 высокий уровень, конденсатор разряжен и на тестируемом входе T1 логическая единица. После того, как мы установим на выводы P2.2-P2.3 низкий уровень, конденсатор начнет заряжаться и через некоторое время, экспоненциально зависящее от сопротивления переменного резистора, падение напряжения на нем станет таким, что на T1 установится 0. Остается только засечь время до появления нуля на T1, которое будет пропорционально углу поворота пэддла (Подробнее и интереснее о подобных схемах можно почитать у DI HALT’а). В коде это может выглядеть следующим образом:

     clr a
        mov r1, a
        outl p2, a          ;отправляем 0 на P2.2-P2.3
loop:
        inc r1              ;инкрементируем r1
        jt1 loop            ;пока на T1 единица
        mov a, #00110000b   ;отправляем 1 на P2.2-P2.3
        outl p2, a          ;готовя конденсатор к следующему считыванию

ir2c1yukhpzppj3h9jznphe_s-k.gif

Звук


Здесь совсем просто, к двум линиям порта P2.0 и P2.1 подключен пьезодинамик, все, что нам нужно — попеременно дрыгать ножками с нужной частотой.

Что получилось


В итоге я написал две игры — Тетрис и некое подобие Flappy Bird. Для отладки использовал Intel D8748H и клон NEC D8749HD отличающуюся только размером ПЗУ. В качестве ассемблера и дебагера пользовался 8048 Integrated Development Environment. В процессе получил незабываемый опыт — постоянные перетыкания микроконтроллеров из консоли в стиратель, из стирателя в программатор, а оттуда обратно в консоль и все это сопровождается больничным запахом ионизированного ультрафиолетом воздуха…
Готовые программы записывал в однократно программируемые Intel P8748H которые распаивал на приехавших к тому времени печатных платах.

_pf8ool9qniaa7--uotj-o-bx6i.jpeg
Сравнение с оригинальными платами, слева направо: Block Buster, Мой вариант, Connect Four

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

На вторую игру корпуса не хватило:

Заключение


MB Microvision выпускалась до 1981 года, и хотя продажи на старте были довольно успешны, малое количество игр (всего было выпущено 13 картриджей) и проблемы с качеством обычно называют причинами спада продаж. Я бы к этому добавил еще чрезмерно большой размер, особенно по сравнению с появившейся тогда и ставшей хитом серией Nintendo Game&Watch.

Исходные коды и схемы опубликованы на GitHub

© Habrahabr.ru