ZX Spectrum из коронавируса и палок (на самом деле, не совсем)

Самоизоляция — бич современного человечества. Вот, к примеру, в соседнем с нашим городе по пятницам и субботам, после традиционного хлопанья в ладоши в 8 вечера, устраивают балконные концерты. Им хорошо, у них дома высокие и соседи молодые. У нас же соседи пожилые, концертов не хотят. И дома невысоки, что тоже не способствует праздности. Поэтому, спасаемся, как можем.

Днем, на удаленке, не так и плохо. Как и вечером, пока не уснут дети. Как и в первые несколько дней, пока не кончатся книги и не надоедят сериалы. Но проходит месяц, за ним другой. Душа требует старого железа. Но не просто, а чтоб с извратом. И я порылся в ящиках с мусором и обнаружил там процессор Zilog Z80:

image
Надо сказать, я этот процессор очень люблю. Наверное, больше него мне нравится только 486й чип, но до него руки дойдут еще нескоро, ибо вставить его в макетку сложно и бессмысленно. Придется паять. А вот паять пока не хочется. И еще больше, чем сам Z80, я люблю построенный на его основе компьютер ZX Spectrum. Но родной спектрум страдает бедой в виде микросхемы кастомной логики ULA, а клоны его на рассыпухе хотя и не особо сложны в постройке и доработке, но все же не для макетки, да и вообще, зачем столько забот, когда есть ардуино?

Умный, уравновешенный и адекватный читатель тут уже либо прекратит чтение, либо бросит что-то типа »1 микросхема FPGA вместит компьютерный класс Спектрумов», перед тем, как прекратить. Я же не умный, не адекватный, хотя и уравновешен, но про FPGA знаю только то, что это круто. Я умею только в ардуино. Но очень хотется потыкать проводочками в Z80. Очень.

Начнем?

Конечно, начнем. Но сначала — Дисклеймер. Пожалуйста, относитесь ко всему, что я пишу, с изрядной долей скепсиса. Я — любитель в самом плохом смысле этого слова. У меня нет никакого соответствующего тому, о чем я пишу, образования. Если вы вдруг решите повторить то, что сделал я (нет, ну, а вдруг?), знайте, что почти все, что сделано тут, будь то хард или софт, сделано неправильно. Выкладываю я это на всеобщее обозрение потому, что убиться этим сложно, а детали, используемые в поделке, стоят сущие копейки, и их не жалко.

Начнем с того, что такое адекватный 8-битный компьютер. Это, собственно, процессор, соединенный с ПЗУ и ОЗУ, а сбоку — пара-тройка счетчиков, чтобы на экран по композиту выводить. Иногда, таймер, чтобы пищало. ZX Spectrum ничем от традиционной схемы не отличается, кроме одного но. Там есть ULA. Это, собственно, «чипсет» спектрума. ULA заведует периферией, типа магнитофона, пищалки, клавиатуры (частично), вывода на экран (да, да, в чипсете спектрума интегрированная видяха появилась еще до того, как это стало мэйнстримом). Там же была и шаред-мемори, первые 16 КиБ ОЗУ (адреса с 0×4000 по 0×5В00). Из нее ULA рисовала композитом на экран, а чтобы Z80 не шарился там, когда не надо, ULA могла остановить процессор, если надо, ведь тактовый сигнал на Z80 шел именно от нее. То есть, если ULA работала с памятью, и детектила, что процессор тоже лезет в эту память (для этого она постоянно мониторила MREQ и линии A15 и A14), она просто останавливала тактирование процессора пока сама не закончит делать то, что ей надо. Кстати, дабы избежать порчи данных на шине, части шины со стороны процессора и со стороны ULA были разграничены… резисторами… Причем память сидела на шине со стороны ULA и, соответственно, в случае коллизии, полностью игнорировала данные и адрес со стороны процессора.

Кроме того, в спектруме было ПЗУ (адреса 0×0000 — 0×3FFF) и собственная память процессора (0×8000 — 0xFFFF), к которым ULA доступа не имела, и которые работали быстрее, чем 16 КиБ разделяемой памяти, так как процессор не мешал ULA в этой области. Но это было только на 48К версии компьютера. В базовой версии были только ПЗУ и совместная с ULA память в 16 КиБ. С нее и начнем.

Удобно, что процессор Z80 умеет регенирировать DRAM, но мне как-то не хочется с ней возится, ибо SRAM найти проще и у меня нет мультиплексора (или я не могу его найти). Так что, будем использовать именно SRAM. Для начала соберем основной скелет, на который потом можно будет навесить все остальное. Скелетом будет процессор, ПЗУ с прошивкой, замапленое на адреса ПЗУ спектрума, ОЗУ, замапленое на первые 16 КиБ после ПЗУ и немного микросхем, чтобы все заврте… Надо сказать, что у меня долго все не хотело вертеться, так как макетки у меня китайские по $1 за 2 штуки на ибее. Но, по мне, возня того стоит. Если не хотите возиться долго, берите хорошие макетки.

Итак, установим Z80.
Как видно из даташита,

een6i0aw3zjgv2b4gah5oo9t2ja.png

У процессора 40 выводов, разделенных на группы: шина адреса, шина данных, контроль системы, контроль процессора, контроль шины процессора, ну и питание и тактовый сигнал. Далеко не все из этих выводов используются в реальных системах, таких, как ZX Spectrum, , как видно из схемы. Из группы «контроль процессора» в самом Спектруме используются только сигналы INT и RESET. Из группы «контроль системы» не используется сигнал M1, группа «контроль шины» не используется совсем. Тому есть причина. Старые 8-битные системы были очень просты, а Спектрум создавался с идеей быть максимально простым и все, что можно было игнорировать, игнорировалось. Конечно, производители периферии могли использовать прерывания (сигналы INT и NMI), они были разведены на слот расширения, но в самом спектруме NMI не использовался. Как видно из вышеприведенной схемы, сигналы NMI, WAIT, BUSREQ подтянуты резисторами к питанию, так как это входы, активируемые низким уровнем (об этом говорит черта над названием сигнала), и там должна быть логическая единица (то есть +5В), чтобы не дай бог не сработал ненужный сигнал. А вот выводы, BUSACK, HALT, M1, так и висят в воздухе, ни к чему не подключенные. Кстати, обратите внимание, что кнопки сброса в спектруме нет. Вывод сброса подключен через RC-цепочку к питанию (RESET тоже активируется низким уровнем), так как, согласно даташита, после включения RESET должен быть активен по меньшей мере 3 такта, чтобы процессор перешел в рабочий режим. Эта RC-цепочка держит низкий уровень, пока конденсатор не зарядится до высокого уровня через резистор.

Давайте кратко пробежимся по остальным сигналам:
M1. Нам не нужен. Он сообщает, что процессор начал выполнять очередную инструкцию.
MREQ. Нужен. Он сообщает, что процессор обращается к памяти. Если этот сигнал становится низким (то есть соединенным с землей питания), то нам надо будет активировать память, подключенную к процессору.
IOREQ. Нужен. Он сообщает, что процессор обращается к периферийному устройству. Например, к клавиатуре.
RD. Нужен. Сообщает, что процессор будет читать данные из памяти (если активен MREQ) или периферии (IOREQ).
WR. Нужен. Сообщает, что процессор будет писать данные в память/периферию.
RFSH. Нужен. Вообще, этот сигнал нужен для динамической памяти (DRAM). Её использовать я не планирую, так как и адресация у нее сложнее (матричная, а не линейная, то есть надо будет мультиплексор ставить), и вообще, в наше время микросхемы SRAM малой емкости добыть легче. Но так как процессор сам регенерирует DRAM, перебирая адреса на шине памяти, этот сигнал позволит нам игнорировать циклы регенерации и не активировать память при активном RFSH.
HALT. Не нужен. Сообщает, что процессор остановлен.
WAIT. Не нужен. Этот сигнал нужен, чтобы попросить процессор остановиться и подождать немного. Используется обычно медленной периферией или памятью. Но не в спектруме. Когда в спектруме периферия (ULA) решает остановить процессор, то она просто перестает подавать на него тактовый сигнал. Это надежнее, так как после получения WAIT процессор не сразу остановится.
INT. Прерывание. Пока непонятно. Будем считать, что не нужен пока. Потом разберемся.
NMI. Немаскируемое прерывание. Супер-прерывание. Не нужно.
RESET. Без него не полетит.
BUSREQ. Не нужен. Просит процессор отключиться от шин данных/адреса, а так же контрольных сигналов. Нужен, если какое-то устройство хочет заполучить контроль над шиной.
BUSACK. Не нужен. Служит для того, чтобы сообщить устройству, выполнившему BUSREQ, что шина свободна.
CLOCK. Тактовый сигнал. Понятно, он нужен.
Питание тоже нужно. Слава разработчикам, только +5V/GND. Никаких тебе 3х напряжений.
A0-A15 — шина адреса. На нее процессор выводит либо адрес памяти (MREQ активен), либо адрес порта ввода-вывода (IOREQ активен) при соответствующих обращениях. Как видно, шина в 16 бит шириной, что позволяет напрямую адресовать 64 КиБ памяти.
D0-D7 — шина данных. На нее процессор выводит (WR активен), либо читает с нее (RD активен) запрошенные данные.

Итак, разместим процессор на макетке. Вот так его выводы расположены физически:
image

Подключим питание (пин 11 и 29). Я на всякий случай поставил еще и конденсатор на 10 пФ между этими ногами. Но он мне не помог в итоге. Пины 27, 23, 18 пусть останутся ни к чему не подключенными. Пины 26, 25, 24, 17, 16 подключим через резисторы (я использовал 10 кОм) к питанию. Шину адреса (пины 1–5 и 30–40) я вывел на противоположную сторону макетки, а шину данных (пины 7–10 и 12–15) — на сделаную из макеточных шин питания отдельную шину данных.
Пины 6 (тактовый сигнал) и 26 (RESET) подключим (потом) к Ардуине, чтобы с нее можно было управлять процессором.

Получилось вот так:

jminrxjfjixmm4pod6jhxw3kzfy.png

Пока не обращайте внимания на провода сверху, они идут от ПЗУ, к нему перейдем немного позже. Также, на фото рядом с процессором видно еще одну микросхему. Она нам нужна, чтобы декодировать верхние биты адреса. Как я уже говорил выше, в спектруме есть 3 типа памяти. Нижние 16 КиБ адресного пространства — это ПЗУ. Соответственно, если выводы A14 и A15 в низком состоянии (0 Вольт), нам надо отключить от шины все, кроме микросхемы ПЗУ. Далее идет 16 КиБ разделяемой памяти. Соответственно, эту память нам надо подключить к шине (а остальное отключить), если вывод A15 в низком состоянии, а A14 — в высоком (+5 Вольт). Ну и потом идет 32 КиБ быстрой памяти. Эту память мы приделаем попозже, и активировать будем, если вывод A15 в высоком состоянии. Кроме того, не стоит забывать, что память мы активируем только при активном (здесь активный — низкий, 0 Вольт) выводе MREQ и неактивном (здесь, неактивный — высокий, +5В) RFSH. Все это довольно просто реализовать на стандартной логике, на тех же NAND, типа 74HC00, или православных К155ЛА3, и я понимаю, задача эта — примерно для подготовительной группы детского сада, однако я думать в таблицах истинности могу только на воле, а в неволе у меня вот там лежит полная схема Harlequin’а, из которой можно просто взять часть, где нарисована U4 (74HC138, благо у меня таких около сотни). U11 пока проигнорируем для ясности, так как верхние 32КиБ пока нас не интересуют.

Подключаем очень просто.

2hmyp2xuiiuejpixie4e1mnoo1w.png

Как видно из краткого описания, микросхема представляет из себя декодер, принимающий на выводы с 1 го по 3й двоичное число от 000 до 111 и активирующее соответствующий этому числу один из 8 выводов (ноги 7 и с 9 по 15). Так как в 3х битах можно хранить лишь 8 разных чисел, выводов тоже всего восемь. Как можно заметить, выводы инвертированы, то есть тот, который будет активен, будет иметь уровень 0В, а все остальные +5В. Кроме того, в микросхему встроен ключ в виде 3-х вводного гейта типа «И», и два из трех его входов также инвертированы.
В нашем случае, мы подключаем сам декодер следующим образом: старший бит (3я нога) к земле, там всегда будет 0. Средний бит — к линии А15. Там будет 1 только если процессор будет обращаться к верхним 32КиБ памяти (адреса 0×8000 — 0хFFFF, или 1000000000000000 — 1111111111111111 в двоичной записи, когда старший бит всегда выставлен в 1). Младший бит мы подключаем в линии А14, там высокий уровень будет в случае обращения либо к памяти после первых 16 КиБ, но до верхних 32 КиБ (адреса 0×4000 — 0×7FFF, или 0100000000000000 — 0111111111111111 в двоичном виде), либо к самым последним 16 КиБ адресного пространства (адреса 0хB000 — 0xFFFF, или 1100000000000000 — 1111111111111111 в двоичном виде).

Давайте посмотрим, какой вывод будет в каждом из случаев:

  • Если линии А14 и А15 обе в низком состоянии, то есть процессор обращается к нижним 16 КиБ памяти, где у Спектрума ПЗУ, на входе декодера будет 000, или 0 в двоичном формате (все входы в низком состоянии), и будет активен вывод Y0 (15й пин). Его мы и подключим к ПЗУ, чтобы оно включалось в этом случае.
  • Если линия А14 в высоком состоянии, а линия А15 — в низком, то есть процессор обращается к памяти после первых 16 КиБ, но до 32 КиБ, на входе будет 001, или 1 в двоичном формате, и будет активен вывод Y1 (14й пин). К нему мы подключим разделяемое ОЗУ, первые 16 КиБ, где находится в том числе экранная память.
  • Если линия А14 в низком состоянии, а линия А15 — в высоком, процессор обращается к памяти где-то от 32 КиБ до 48 КиБ, на входе микросхемы 010, то есть активен вывод Y2 (13й пин). У нас пока этой памяти нет, так что вывод в воздухе.
  • Если обе линии (А14 и А15) активны, процессор обращается к верхним 16 КиБ памяти, от 48 до 64 КиБ, её у нас нет, так что вывод Y3 (12й пин) тоже в воздухе.

Кроме того, благодаря еще одному элементу, микросхема будет активировать свои выводы только в том случае, если вводы 4 и 5 в низком состоянии, а 6 — в высоком. 4й ввод у нас всегда в низком состоянии (он подключен напрямую к земле), 5й будет в низком только когда процессор обращается к памяти (помните, MREQ в низком состоянии означает обращение к памяти), а 6й будет в высоком когда процессор не выполняет цикл обновления DRAM (у нас SRAM, так что циклы обновления DRAM безопаснее всего просто игнорировать). Получается здорово.

Дальше ставим ПЗУ.

Я взял W27С512 так как дешево, сердито, все помеcтится и еще и банковать можно. 64КиБ! 4 прошивки можно залить. Ну и у меня этих микросхем примерно миллион. Я решил, что буду шить только верхнюю половину, так как на Арлекине нога А15 привязана к +5В, а А14 перемычкой регулируется. Таким образом, смогу протестировать на Арлекине прошивку, чтобы долго не возиться. Сморим даташит. Ставим микросхему на макетку. Я опять поставил в правый угол, чтобы шину адреса разместить слева. Притягиваем ногу А15 к питанию, А14 проводком к земле. Проводком — это для того, чтобы можно было менять банки памяти. Так как на А15 всегда будет высокий уровень, нам будут доступны лишь верхние 32 КиБ флешки. Из них линией А14 будем выбирать верхние (+5В) или нижние (земля) 16 КиБ. В них я программатором залил тестовый образ и прошивку 48К бейсик.
Остальные 14 адресных линий (А0 — А13) подключаем к шине адреса слева. Шину данных (Q0 — Q7) подключаем к нашей импровизированной шине в виде шин питания от макеток. Не забываем про питание!

Теперь управляющие сигналы. OE — это output enable. Нам надо, чтобы ПЗУ отдавало данные на шину данных, когда процессор их читает. Так что подключаем напрямую к выводу RD процессора. Удобно, что оба вывода, и OE у ПЗУ и RD у процессора активны в низком состоянии. Это важно, ничего инвертировать не надо. Кроме того, ПЗУ имеет ввод CS, также активный в низком состоянии. Если этот ввод не активен, ПЗУ будет игнорировать все остальные сигналы и ничего не будет выводить на шину данных. Этот ввод мы подключим к выводу Y0 (15 му пину) микросхемы 74HC138, который тоже активен в низком состоянии. На схеме Арлекина этот сигнал, почему-то, подключен через резистор. Сделаем так же. Зачем, я не знаю. Может, умные люди подскажут в комментариях…

1kn7h3nfgruh121jefz2qfhucts.png

Все.

Теперь ОЗУ.

С ним сложнее, так как с ОЗУ (с нашими 16 КиБ) работает не только процессор, но и ULA, или, в нашем случае, Ардуино. Так как надо же чем-нибудь читать то, что выводится на экран. Поэтому нам надо уметь отключать управляющие сигналы и шину адреса ОЗУ от процессора. Шину данных не будем отключать, поступим, как в оригинальном спектруме (и в Арлекине): разделим шину резисторами (470–500 Ом). С одной стороны резисторов будут процессор и ПЗУ, с другой — ОЗУ и Ардуино. Таким образом, в случае конфликта на шине данных, она будет работать как 2 отдельные шины. А вот для остального используем 74HC245, как в Арлекине (U43, U44 на схеме), хотя в настоящем Спекки тоже были резисторы (между IC1 с одной стороны, это ULA, и IC3, IC4 — с другой).

74HC245 представляет из себя буфер шины на 8 бит. У нас же есть 2 сигнала управления (RD — в случае чтения из памяти и CЕ для активации самого ОЗУ. С WR в случае записи в память разберемся позже) и 14 бит адреса: помните, выше мы уже генерируем сигнал на память с помощью 74HC138 только в том случае, если процессор активировал A14 при неактивной А15, так что никакого дополнительного декодирования адреса нам делать не надо, память будет работать только при обращении в первые 16 КиБ после ПЗУ. Ну и, конечно, чтобы адресовать 16 КиБ нужно как раз 14 линий адреса (А0-А13). Всего получается 16 сигналов, так что нам надо 2 микросхемы 74HC245. Подключим их на макетку слева, на место шины адреса.

Из даташита на 74HC245 видно, что, в общем то, не важно, какой стороной подключать микросхемы, но, так как я начал наращивать макетки снизу вверх, и все остальные микросхемы установлены первым пином налево, шина адреса будет подключаться к стороне А (выводы 2–9 микросхемы, в даташите обозначены как A0-А7). Направление передачи всегда — от процессора к ОЗУ, так как ОЗУ никогда не устанавливает адрес, а лишь принимает его. В 74HC245 за направление передачи отвечает пин 1 (DIR). Согласно даташита, чтобы на стороне B появился вывод равный вводу со стороны А, DIR должен быть установлен в HIGH. Так что подключим 1й пин обеих микросхем к +5В. OE (20й пин, активируется низким уровнем) подключим с помощью проводка к земле, чтобы можно было его переставить быстро на +5В и отключить память от процессора. Дальше проще. Подключим питание для обеих микросхем. Самые правые пины правой микросхемы (8й и 9й пины, вводы A6 и A7) будут управляющими сигналами. А7 я подключил к выводу RD процессора, а A6 — к выводу Y1 микросхемы 74HC138, так как там низкий уровень будет только в том случае, когда процессор обращается к нашему ОЗУ. Остальные выводы со стороны А обеих микросхем (ноги 2–9 для левой и ноги 2–7 для правой) я подключил к адресной шине, выводам А13-А0. Старшие 2 бита адреса нам не нужны, ведь они уже декодированы в сигнале с 74HC138. Теперь про само ОЗУ. Естественно, я использовал то, что у меня уже было: микросхему кэш-памяти из старой материнки. Мне попалась IS61C256 на 20 нс, но подойдет любая. Спекки работал на частоте 3,5 МГц, а мы пока вообще с Ардуинки трактовать будем. Если выйдет 100 кГц, будет счастье! Итак, подключаем. Само собой, не надо забывать про питание. Выводы I/O0 — I/O7 подключаем на макетку шины данных ПОСЛЕ резисторов. Мне повезло (на самом деле, нет), на моих китайских макетаках шины питания разделены ровно посередине. Эту фичу я и использовал для разделения шины резисторами. Если на ваших макетах не так, придется извращаться делать вторую шину данных, и соединять её резисторами с первой. Выводы А0-А13 кидаем на соответствующие B-выводы микросхем 74HC245, не забывая, что самые правые у нас подключены не к шине данных, а к управляющим сигналам. А14 — по выбору, либо к земле, либо к +5В. Микросхема на 32 КиБ, так что этот вывод определит, какую половину мы будем использовать. Если вы найдете SRAM на 16 КиБ, линии A14 в ней не будет. Остаются выводы WE (write enable), CE (chip enable) и OE (output enable). Все активируются низким уровнем. OE надо подключить к RD процессора, но, естественно, не напрямую, а через правую 74HC245, где RD у меня приходит на ногу A7, соответственно выходит с ноги B7 (11й пин). Туда и подключим. CE надо подключить к Y1 от 74HC138, которая декодирует адрес. Её сигнал приходит у меня на A6 правой микросхемы 74HC245, соотвественно, выходит из ноги B6 (12 пин). WE я напрямую подключил к выводу WR процессора. Еще я установил провод-перемычку от сигнала OE и воткнул его просто в неиспользуемую часть макетки. Подключив этот провод к земле питания, я смогу принудительно активировать ОЗУ, когда буду читать его с Ардуинки. Еще, я притянул все управляющие сигналы ОЗУ к +5В с помощью резисторов в 10 кОм. На всякий случай. Получилось вот так:

9y0k3ht6ehvrsvzoehp70h428mo.png

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

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

В общем, это почти все. Теперь только надо понять, как читать данные из ОЗУ в Ардуино. Для начала, посчитаем, сколько выводов Ардуинки нам понадобится. Нам надо отдавать тактовый сигнал и управлять RESETом, это 2 вывода. 8 бит шины данных — еще 8 выводов. Плюс 13 бит адреса, итого, 23 вывода. Кроме того, надо с Ардуинкой общаться, будем делать это через её последовательный интерфейс, это еще 2 пина. К сожалению, на моей УНО всего 20 выводов.

Ну да не беда. Я не знаю ни одного человека, у которого есть Ардуино и нет 74HC595. Мне кажется, их продают только в комплекте. По крайне мере, у меня только микросхем типа 74HC00 больше, чем 595х. Так что их и используем. Тем более, что я сэкономлю место в статье, ведь работа 595х с ардуино прекрасно описана тут. 595 ми будем генерировать адрес. Микросхем понадобится 2 штуки (так как у нас 13 бит адреса, а 595я имеет 8 выводов). Как соединить несколько 595х для расширения шины, подробно описано по ссылке выше. Замечу лишь, что в примерах по той ссылке OE (пин 13) 595х притягивают к земле. Мы же делать этого категорически не будем, мы пошлем туда сигнал с Ардуинки, так как выводы 595х будут подключены непосредственно к шине адреса ОЗУ, и нам не надо никакого паразитного сигнала там. После подключения выводов 595х к шине адреса ОЗУ, больше на макетах ничего делать не надо. Время подключать ардуинку. Но сначала напишем скетч:

// CPU defines
#define CPU_CLOCK_PIN 2
#define CPU_RESET_PIN 3

// RAM defines
#define RAM_OUTPUT_ENABLE_PIN 4
#define RAM_WRITE_ENABLE_PIN 5
#define RAM_CHIP_ENABLE_PIN 6
#define RAM_BUFFER_PIN 7

// Shift Register defines
#define SR_DATA_PIN 8
#define SR_OUTPUT_ENABLE_PIN 9
#define SR_LATCH_PIN 10
#define SR_CLOCK_PIN 11

//////////////////////////////////////////////////////////////////////////

void setup() {
  // All CPU and RAM control signals need to be configured as inputs by default
  // and only changed to outputs when used.
  // Shift register control signals may be preconfigured

  // CPU controls seetup
  DDRC = B00000000;
  pinMode(CPU_CLOCK_PIN, INPUT);
  pinMode(CPU_RESET_PIN, INPUT);

  // RAM setup
  pinMode(RAM_WRITE_ENABLE_PIN, INPUT);
  pinMode(RAM_OUTPUT_ENABLE_PIN, INPUT);
  pinMode(RAM_CHIP_ENABLE_PIN, INPUT);
  pinMode(RAM_BUFFER_PIN, OUTPUT);
  digitalWrite(RAM_BUFFER_PIN, LOW);

  // SR setup
  pinMode(SR_LATCH_PIN, OUTPUT);
  pinMode(SR_CLOCK_PIN, OUTPUT);
  pinMode(SR_DATA_PIN, OUTPUT);
  pinMode(SR_OUTPUT_ENABLE_PIN, OUTPUT);
  digitalWrite(SR_OUTPUT_ENABLE_PIN, HIGH); // active low

  // common setup
  Serial.begin(9600);
  Serial.println("Hello");
}// setup

//////////////////////////////////////////////////////////////////////////

void shiftReadValueFromAddress(uint16_t address, uint8_t *value) {
  // disable RAM output
  pinMode(RAM_WRITE_ENABLE_PIN, OUTPUT);
  digitalWrite(RAM_WRITE_ENABLE_PIN, HIGH); // active low
  pinMode(RAM_OUTPUT_ENABLE_PIN, OUTPUT);
  digitalWrite(RAM_OUTPUT_ENABLE_PIN, HIGH); // active low
  // set address
  digitalWrite(SR_LATCH_PIN, LOW);
  shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, address>>8); 
  shiftOut(SR_DATA_PIN, SR_CLOCK_PIN, MSBFIRST, address);  
  digitalWrite(SR_LATCH_PIN, HIGH);
  digitalWrite(SR_OUTPUT_ENABLE_PIN, LOW); // active low
  // write value to RAM
  digitalWrite(RAM_OUTPUT_ENABLE_PIN, LOW); // active low
  delay(1);
  DDRC = B00000000;
  *value = PINC;
  digitalWrite(RAM_OUTPUT_ENABLE_PIN, HIGH); // active low
  // disable SR
  digitalWrite(SR_OUTPUT_ENABLE_PIN, HIGH); // active low
  pinMode(RAM_WRITE_ENABLE_PIN, INPUT);
  pinMode(RAM_OUTPUT_ENABLE_PIN, INPUT);
}// shiftWriteValueToAddress

//////////////////////////////////////////////////////////////////////////

void runClock(uint32_t cycles) {
  uint32_t currCycle = 0;
  pinMode(CPU_CLOCK_PIN, OUTPUT);
  while(currCycle < cycles) {
    digitalWrite(CPU_CLOCK_PIN, HIGH);
    digitalWrite(CPU_CLOCK_PIN, LOW);
    currCycle++;
  }
  pinMode(CPU_CLOCK_PIN, INPUT);
}// runClock

//////////////////////////////////////////////////////////////////////////

void trySpectrum() {
  pinMode(RAM_WRITE_ENABLE_PIN, INPUT);
  pinMode(RAM_OUTPUT_ENABLE_PIN, INPUT);
  pinMode(CPU_RESET_PIN, OUTPUT);
  digitalWrite(CPU_RESET_PIN, LOW);
  runClock(30);
  digitalWrite(CPU_RESET_PIN, HIGH);
  runClock(12500000);
}// trySpectrum

//////////////////////////////////////////////////////////////////////////

void readDisplayLines() {
  uint8_t value;
  digitalWrite(RAM_BUFFER_PIN, HIGH);
  pinMode(RAM_CHIP_ENABLE_PIN, OUTPUT);
  digitalWrite(RAM_CHIP_ENABLE_PIN, LOW);
  for(uint16_t i=16384; i<16384+6144;i++) {
    shiftReadValueFromAddress(i, &value);
    Serial.println(value);
  }
  pinMode(RAM_CHIP_ENABLE_PIN, INPUT);
}// readDisplayLines

//////////////////////////////////////////////////////////////////////////

void loop() {
  trySpectrum();
  Serial.println("Hope we are ok now. Please set up memory for reading");
  delay(40000);
  Serial.println("Reading memory");
  readDisplayLines();
  Serial.println("Done");
  delay(100000);
}// loop

Как видно из скетча (ну правда, вдруг, кто-то прочитал), я читаю шину данных в порт C. Как может помнить Ардуищик, в УНО порт C — это 6 пинов. То есть я читаю только 6 бит. Да, для простоты процесса, я игнорирую 2 старших бита в каждом байте экранного буфера. Это выльется в то, что каждые 2 пикселя через 6 будут всегда цвета фона. Пока прокатит, потом поправим. Это же скелет.

Теперь по самому подключению. В принципе, все расписано в самом верху скетча:

// CPU defines
#define CPU_CLOCK_PIN 2 — пин 2 ардуины подключаем к пину 6 процессора (тактовый сигнал)
#define CPU_RESET_PIN 3 — пин 3 ардуины подключаем к пину 26 процессора (RESET)

// RAM defines
#define RAM_OUTPUT_ENABLE_PIN 4 — пин 4 аруины подключаем к пину 22 ОЗУ (OE)
#define RAM_WRITE_ENABLE_PIN 5 — пин 5 ардуины не подключаем никуда. Это остаток от старых скетчей.
#define RAM_CHIP_ENABLE_PIN 6 — пин 6 ардуины тоже не подключаем никуда. Я пытался сделать управление памятью полностью с Ардуины, но на макетке это у меня не заработало. То ли из-за питания, то ли из-за качества макетки. Но скорее всего, по обеим причинам.
#define RAM_BUFFER_PIN 7 — так же, как и пин 6, пока никуда не идет.

// Shift Register defines
#define SR_DATA_PIN 8 — пин 8 ардуины подключаем к пину 14 «младшей» 595й. У старшей этот пин подключен к пину 9 младшей, так что перепутать не получится.
#define SR_OUTPUT_ENABLE_PIN 9 — к пинам 13 обеих 595х
#define SR_LATCH_PIN 10 — к пинам 12 обеих 595х
#define SR_CLOCK_PIN 11 — к пинам 11 обеиз 595х.

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

pjq5siebaxafimb_nqnjxyry6_u.png

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

Теперь надо ручками переставить проводок, подсоединенный к пинам 19 обеих 74HC245 c земли на +5В. Таким образом мы отключаем процессор от ОЗУ. Пин 22 самой микросхемы ОЗУ надо подключить к земле (я выше писал про проводок, который я воткнул просто в макетку пока, в неиспользуемое место). Таким образом мы принудительно включаем ОЗУ.

После этого, подождав немного, Ардуинка начнет читать содержимое памяти и выводить его в столбик в последовательный порт. Будет много-много цифр. Теперь можно эти данные скопировать оттуда и вставить, скажем, в текстовый файл, не забыв подчистить весь ненужный текст (сверху пара строк, и «Done» внизу), нам нужны только цифры. Это то, что наш Спекки записал в видеопамять. Остается только посмотреть, что же там было, в видеопамяти. А видеопамять у Спектрума непростая…

Как видно, сами пикселы хранятся отдельно от цвета. Цвет пока будем игнорировать, давайте читать только сами пикселы. Но и их не так просто раскодировать. После долгих мучений Visual Studio, я пришел вот к такому элегантному решению:


#include "stdafx.h"
#include 
#include 
#include 

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
uint8_t *scrData;

VOID OnPaint(HDC hdc) {
	size_t arrSize = 6144;//sizeof(scrData) / sizeof(scrData[0]);
	//int currRow = 0, currX = 0, currBlock = 0, currY = 0, currBase = 0;
	for (size_t arrPos = 0; arrPos < arrSize; arrPos++) {
		int blockPos = arrPos % 2048;
		int currBase = (blockPos % 256) / 32;
		int currX = blockPos % 32;
		int currBlock = arrPos / 2048;
		int currRow = blockPos / 256;
		int currY = currBlock * 64 + currBase * 8 + currRow;
		for (int trueX = 0; trueX < 8; trueX++) {
			char r = ((scrData[arrPos] >> trueX) & 1)*255;
			SetPixel(hdc, currX * 8 + (8-trueX), currY, RGB(r, r, r));
		}
	}
}

void loadData() {
	FILE *file;
	errno_t err;
	if ((err = fopen_s(&file, "data.txt", "r"))) {
		MessageBox(NULL, L"Unable to oopen the file", L"Error", 1);
	}
	scrData = (uint8_t*)malloc(6144);
	int currDataPos = 0;
	char buffer[256];
	char currChar = 0;
	int currLinePos = 0;
	while (currChar != EOF) {
		currChar = getc(file);
		buffer[currLinePos++] = currChar;
		if (currChar == '\n') {
			buffer[currLinePos] = 0;
			scrData[currDataPos++] = (uint8_t)atoi(buffer);
			currLinePos = 0;
		}
	}
	fclose(file);
}

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, PSTR, INT iCmdShow) {
	HWND                hWnd;
	MSG                 msg;
	WNDCLASS            wndClass;
	wndClass.style = CS_HREDRAW | CS_VREDRAW;
	wndClass.lpfnWndProc = WndProc;
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = hInstance;
	wndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = TEXT("GettingStarted");
	RegisterClass(&wndClass);
	hWnd = CreateWindow(
		TEXT("GettingStarted"),   // window class name
		TEXT("Getting Started"),  // window caption
		WS_OVERLAPPEDWINDOW,      // window style
		CW_USEDEFAULT,            // initial x position
		CW_USEDEFAULT,            // initial y position
		CW_USEDEFAULT,            // initial x size
		CW_USEDEFAULT,            // initial y size
		NULL,                     // parent window handle
		NULL,                     // window menu handle
		hInstance,                // program instance handle
		NULL);                    // creation parameters
	loadData();
	ShowWindow(hWnd, iCmdShow);
	UpdateWindow(hWnd);
	while (GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}  // WinMain

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
	HDC          hdc;
	PAINTSTRUCT  ps;
	switch (message) {
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		OnPaint(hdc);
		EndPaint(hWnd, &ps);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
} // WndProc

Программа открывает файл data.txt из своей директории. В этом файле текстовый вывод ардуинки (после удаления всех лишних строк, как сказано выше.)

Скормим ей полученный файл, и что в итоге:

tvai3fcvvg-y53dlrcxogjo9o-c.png

Да, пока результат очень далек от идеала, но это определенно вывод на экран. Причем, именно тот, что нужен. От ПЗУ с диагностической прошивкой.

Ну, скелет компьютера готов. Да, пока использовать его никак нельзя, но зато видно, как предельно просто были устроены старые 8-битные компьютеры. Я еще побился немного над макеткой, но вывод стал только хуже. Похоже, следующий шаг — паять на нормальной, распаячной макетке, с нормальным питанием.

Вот только надо ли?

© Habrahabr.ru