Запускаем советский процессор из журнала «Радио» с помощью Arduino

Привет! Меня зовут Илья Мамай, я инженер-программист в группе разработки операционных систем YADRO. В этой статье я хочу поделиться опытом некромантии сборки советского компьютера по мотивам «Микро-80», схемы которого были опубликованы в журнале «Радио». Но собирать я буду не по этим схемам. Используя их как опору и источник вдохновения, я начну с запуска процессора КР580ВМ80А (советского клона Intel 8080), определения признаков жизни и продолжу постепенным наращиванием функционала и возможностей системы. Здесь мы займемся и радиотехникой, и DIY, и программированием как самого процессора, так и современных микроконтроллеров. Но перед этим поделюсь историей, как я, будучи студентом 4 курса, дошел до этого…

С чего все началось

В прошлый Новый год сосед по общаге спросил, нужна ли мне куча советского радиобарахла, оставшаяся от его деда. Не знаю зачем, но я согласился, как только мне пообещали доставить это все из Ярославля в Москву. Реле, вольтметры, амперметры, тумблеры и счетчик Гейгера-Мюллера СБМ-20, которые, как мне сказали, были скручены с советских светофоров в давние времена. Что ж, знакомство с тем дедом точно стало бы достижением в какой-нибудь RPG, но его, к сожалению, уже не получить. Там же я нашел самодельные печатные платы на микросхемах семейства К155: в них медная фольга была разрезана ножом на квадраты, поверх которых была напаяна схема. А еще там была одна неизвестная мне микросхема-сороконожка — КР580ВВ51А.

КР580ВВ51А оказалась частью советского микропроцессорного комплекта серии КР580. Меня очень заинтересовало словосочетание «советский микропроцессорный комплект», а точнее, то, на что он мог сгодиться. После нескольких статей википедии про советские игрушечные компьютеры «Микроша», «Искра», «Корвет» я оказался на страницах журнала «Радио». Говорят, это был самый популярный журнал у советских радиолюбителей.

В одной из серий статей журнала читателям предлагалось по напечатанным схемам самостоятельно в домашних условиях собрать компьютер «Микро-80» на основе микропроцессора КР580ВМ80А. Подумать только: в те времена люди САМИ доставали детали и по схемам собирали рабочие компьютеры? Без осциллографов, интернета и прочих радостей цивилизации? Тогда и мне стоит попробовать, ведь старый — значит, скорее всего, простой. А у меня и осциллограф есть, и интернет. Точно получится же…

Первая попытка

Я принял вызов только к концу весны. Принял, потому что не хотел писать диплом, а чем только ни займешься, лишь бы его не писать. Решил собрать «Микро-80» на макетных платах по схемам. За шкафом общаги валялся доставшийся мне в наследство блок питания, паяльник и осциллограф. Среди радиодеталей друга было достаточно необходимой рассыпухи. Не хватало только одного — процессора КР580ВМ80А, который при ближайшем рассмотрении оказался клоном процессора Intel 8080. Да и весь комплект оказался клоном микропроцессорного комплекта от Intel.

Я поехал на Митинский радиорынок искать недостающий 40-ногий камень и макетные платы. Камень мне в итоге просто подарили. А потом оказалось, что ему нужно целых три напряжения питания (+5, –5 и +12В), и я поехал туда еще раз выпрашивать на разборе бесплатный компьютерный блок питания. В итоге это «бесплатно» стоило мне 400 рублей, надеюсь, не обманули.

Из блока питания, пары амперметров, добротного советского тумблера, делителя напряжения и клеевого пистолета я собрал источник питания для своего будущего убийцы рынка микропроцессоров. Достал макетную плату и за несколько вечеров и пару поездок в Митино осилил процессорный модуль (попутно научил паять соседа и вынудил его принять активное участие в процессе). Получил такой вот тестовый стенд:

bbae5f0a082a5d921efc108656be1289.png

Собирать все остальное без понимания, как проверить работоспособность, казалось бессмысленной авантюрой. Еще и диплом все-таки пришлось начать писать. И я забросил проект почти на год.

Вторая попытка

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

  1. Изучить документацию к процессору.

  2. Собрать минимальную обвязку для его запуска, запустить и увидеть признаки его кремниевой жизни (пациенту все-таки необходима диспансеризация, в его-то годы).

  3. Заставить Arduino симулировать память, чтобы запустить на процессоре простейшую программу и увидеть признаки ее выполнения. Так я смогу гарантировать, что пациент скорее жив, чем мертв.

  4. Собрать память, заставить «ардуину» загружать туда информацию на старте, после чего — стать устройством передачи данных на ПК.

  5. Двигаться дальше. В каком направлении — проблема меня будущего.

Пока я добрался только до третьего пункта, но мне уже есть, что об этом рассказать! Начну я с сердца компьютера «Микро-80» — процессора КР580ВМ80А.

КР580ВМ80А

Как я уже говорил, процессор КР580ВМ80А — это полная советская копия процессора Intel 8080, так что вся (или почти вся) известная информация об Intel 8080 верна и для нашего подопытного. Заостряю на этом внимание только потому, что единой документации на КР580ВМ80А я не нашел. Отыскал лишь несколько сухих страниц в советском справочнике «Микропроцессоры и микропроцессорные комплекты интегральных микросхем». Широкого выбора, с чего начать работу, эти страницы не давали, так что я решил начать с архитектуры и распиновки.

3a4bef267c87ea77644c507b79e22887.jpeg

Хоть до появления первого x86-процессора должно пройти еще четыре года, но общие основы хорошо знакомой архитектуры х86 уже ясно просматриваются. 50 лет прошло, а они так похожи!

Наш герой имеет очень знакомый набор регистров:

  • 16-разрядные: регистр адреса IP и регистр стека SP, которые за 50 лет с момента выпуска процессора сначала растолстели до 32 бит, став EIP и ESP, а потом, видимо, уже до конца разграбили подземелье и увеличились до 64-бит, став RIP и RSP.

  • Шесть 8-битных регистров общего назначения B, C, D, E, H, L, а также регистр-аккумулятор A, в который записывается результат выполнения большинства операций. В будущем их постигнет та же участь: их части обрастут префиксами и суффиксами, но общая структура сохранится.

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

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

На распиновке выше видно, что процессор имеет 16-битную шину адреса (ножки A0–A15) и 8-битную шину данных (ножки D0–D7). Требует три напряжения питания (Ucc1 = +5 В, Ucc2 = –5 В и UIo = +12 В) и два тактовых сигнала, некие C1 и C2. А также имеет пачку входных сигналов. Пока пройдусь по ним кратко, буду останавливаться подробно, как только какой-нибудь из них нам понадобится:

  • RDY и HLD необходимы для приостановки работы процессора, если какое-либо устройство не успевает предоставить данные в необходимое время или хочет занять шины адреса или данных.

  • INT, высокий уровень на котором инициирует вызов прерывания.

  • SR, он же RESET.

Также мы видим, что процессор имеет несколько выходных сигналов:

  • HLDA, WI — подтверждение приостановки процессора внешним устройством.

  • INTE — имеет высокий уровень, когда прерывания включены.

  • RC и TR — запрос на чтение и запись данных на шине адреса соответственно.

  • SYN — сигнал синхронизации, высоким уровнем которого процессор обозначает начало исполнения машинного цикла.

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

Источник питания

Как я уже говорил, от источника питания требуется три напряжения — +5, –5 и +12 В. Быстро и просто добыть их для тестовых целей можно через обычный компьютерный блок питания стандарта ATX, который легко отыскать на разборе компьютерной техники.

8825d3e8d521f99aeddd9273d92c2cb8.png

Если взглянуть внимательно на разъем ATX, то видно, что он выдает напряжения +5, +12 и –12 В. –5 В присутствует в разъеме ATX, но, как я понял, в наше время оно чаще не реализуется. Мы получим его делителем напряжения, ведь потребление всех микросхем по линии –5 В очень мало, так что излишества нам ни к чему.

Также я добавлю туда тумблер для запуска (между землей и PS_ON), чтобы не ковыряться в контактах скрепкой, и два амперметра по линиям +5 В и +12 В, чтобы до появления белого дыма и запаха гари увидеть, что в работе схемы что-то идет не так. Итак, меньше слов и больше схем!  

848769b2bc8796f64e302f9e5647e44e.jpeg

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

За «намного больше» обычно берется ровно в 10 раз. Потому из схемы и правил Кирхгофа…

f7994b909be1ceacef500482595ae2ac.png

…при учете, что V0 = –12 В, V1 = –5 В, I = 10 мА, получаем R1 = 500 Ом, R2 = 700Ом. Ну или, если брать ближайшие доступные номиналы, то R1 = 500 Ом, R2 = 680 Ом.

8735e436eaa0dbc246c402a32628d6d5.jpeg

В соответствии с документацией, для долголетия процессора особенно важен порядок включения и выключения напряжений питания при запуске. Первым должно появиться –5 В, потом +5 В, потом +12 В. Отключаться питание должно в обратной последовательности. Из тематических форумов я узнал, что главное — не подавать положительные питания раньше, чем отрицательные, тогда процессор будет жить.

Порядок появления питаний в БП стандарта ATX не регламентирован, за это отвечают схемы на материнской плате. Однако, исходя из типовых схем и тех фактов, что…

  • все три напряжения берутся с одного и того же трансформатора,

  • емкость сглаживающих конденсаторов по цепям +5 В и +12 В намного больше емкости по цепи –5 В,

  • я купил имплант удачи за 4000 крышечек и нам повезет,

…можно предположить, что процессор выживет при запуске, так как –5 В появятся и пропадут первыми. Уверенно срезаем ATX разъем, паяем источник питания и получаем примерно такой DIY-экспонат. Мы с товарищами ласково называем его «электрофорез»:

6df4c94eeb54480748fb89e2bdfd6324.png

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

Генератор тактовых сигналов

Документация и журнал «Радио» говорят, что необходимо два тактовых сигнала, их частота не должна быть ниже 500 КГц и выше 2,5 МГц и они должны иметь следующую форму:

0bf6123eb37afc6dfd1edc1c8ff34595.png

Сигнал C2 должен обгонять сигнал C1 на треть периода. Для генерации таких сигналов есть два пути:

  • использование специализированной микросхемы КР580ГФ24,

  • сборка генератора из счетчика и микросхем логики.

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

Путь джедая: генератор тактовых сигналов на логических элементах

Первое, что нам необходимо, — это сгенерировать меандр, из которого мы логическими операциями получим два необходимых тактовых сигнала. Для генерации меандра на логических элементах есть старая добрая советская схема на микросхеме К155ЛН1, представляющая собой шесть логических НЕ.

932cdeacd5f8118714606847ceb84780.jpeg

Работает она следующим образом. Конденсатор С9 попеременно заряжается и разряжается двумя инверторами, ведь пока напряжение на нем ниже напряжения высокого логического уровня, второй инвертор на выходе выдает высокий логический уровень, а первый — низкий. Соответственно, конденсатор заряжается выходом второго инвертора через резистор R2. А как только напряжение на нем увеличится до напряжения высокого логического уровня, оба инвертора переключатся и станут его разряжать. Снимая напряжение с выхода второго инвертора, мы и получим желаемый меандр. Кварцевый резонатор стабилизирует частоту переключения триггеров на своей резонансной частоте.

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

41efa347b86ff8ddf3247b4f24e193e4.jpeg

Здесь я нарисовал 5-битный счетчик К155ИЕ5, распиновка которого выглядит так:

  • С0 — вход первого однобитного счетчика,

  • С1 — вход второго 3-битного счетчика (потому я их замкнул, чтобы в итоге получить 4-битный счетчик),

  • Q0-Q3 — выходы 4 бит рассчитанного счетчиком двоичного числа.

Входы R1 и R2 обнуляют значения каждого счетчика, а нам обнулять их незачем, потому они притянуты к земле. Используемый таким образом счетчик работает как делитель входной частоты на 2, 4, 8 и 16. Потому на выходах схемы out_2, out_4, out_8 мы получим следующие уровни сигналов.

55811f6368285444cdbc36b17bbe9cd7.jpeg

А теперь настало время решать последнюю задачу, генерацию сигналов С1 и С2, самым простым и надежным методом — методом вглядывания. Вычислим логическое И сигналов out2 и out4, НЕ сигнала out2 и зарисуем результат.

79d18ecf00a7e70c18b2c643e8e07ea5.jpeg

Похоже ли это на требуемый сигнал? Ну, отдаленно да… Периоды, правда, не совпадают, да и смещены они не на треть периода, а на четверть. Но при запуске процессор должен бойко сказать: «С пивом покатит!» И заработать, ведь такая схема и была в «Микро-80». Используем out_4 и out_8 для вычисления тактовых сигналов, так как их вчетверо большая частота может оказаться полезной в будущем.

Осталась одна небольшая деталь. Для КР580ВМ80А необходима амплитуда тактовых сигналов 12 В, а у нас только 5 В. Потому воспользуемся какой-нибудь микросхемой с открытым коллектором типа К155ЛА7 и получим итоговую схему.

bc73ee7f02bd25f8c119470314cc8ed1.jpeg

Собираем тактовый генератор:

b0a482280fda84e7726f3a89df32b4e0.jpg

Запускаем и давайте подглядывать: тыкаем осциллографом в ножки 6 и 8 микросхемы К155ЛА7.

Работает! Ура!

Работает! Ура!

Путь ситха: микросхема КР580ГФ24

Все, что мы сделали в предыдущем разделе, умеет одна микросхема — КР580ГФ24. А еще у нее есть типовая схема включения, которая радует глаз своей простотой, в отличие от ранее собранного осьминога:

5d697553eb5a12c7fc28a8159889d364.jpeg

Здесь все точно так же, просто вся логика собрана в одну микросхему. А цепь из диода, резистора и конденсатора отвечает за инициализацию процессора при старте. Ее мы пока игнорируем, как и все остальные сигналы кроме C1 и С2 — те пугают. Разберемся с ними по мере надобности или когда ничего не заработает, а пока просто подтянем их к земле или питанию в зависимости от того, инвертированы они или нет. Сейчас основная цель — запуститься. Собираем:

ed33f4959794c51925a82054511c0b62.jpegb4121e249cfaa576d792797b7a9c746e.jpg

Подключаем процессор (питание и C1, C2). Осталось закинуть входные ноги на землю или на питание. Смотрим на осциллограмму:

3d2e7761027cafa603a51f1fcfde2bf3.png

Работает, ура! Остановимся на этой схеме, так как:

  • она проще, соответственно, вероятность ошибок меньше,

  • мама в детстве сказала мне, что джедаев не существует (и вообще, нам больше по нраву фэнтези-рпг).

Работоспособность «джедайского» варианта для всех дальнейших конструкций статьи я все же проверил за кадром. Вдруг кто решит заняться тем же. 

Запускаем процессор

Наш тестовый стенд при запуске вроде бы не горит, но как понять, что он работает? Процессор будет пытаться читать инструкции с шины данных, а значит, поднимать сигнал RC (нога 17), чтобы сигнализировать об этом. На ней мы ожидаем что-то вроде меандра — это и будет признаком его успешного запуска. Тыкаем осциллограф одним каналом на 17 ногу, а вторым на 12 ногу К155ИЕ5:

3ae87ec3817f0a74b38d9c3c93a814cd.png

Ура! Работает! Каждый четвертый такт сигнала out_2 процессор пытается читать шину данных, но там ничего нет, там нули. А инструкция с опкодом 0×00 — это NOP. Очень удобно.

Итак, мы сделали большой шаг — обеспечили процессор всем необходимым для работы. Осталось лишь дать ему инструкции. Но прежде необходимо понять, как процессор исполняет инструкции. Почему он читает инструкцию каждый 4 такт, может ли чаще или реже…

Как КР580ВМ80А исполняет инструкции

Процессор имеет переменную длину инструкции, от 1 до 3 байт. Каждая инструкция выполняется за 1–5 машинных циклов, каждый машинный цикл состоит из 3–5 тактов. Такт — это один период сигналов C1 и C2.

В первом машинном цикле М1, который занимает 4–5 тактов (Т), процессор делает следующее.

  • Т1. Выводит адрес на шину адреса, а на шину данных — информацию о своем состоянии. Здесь указывается, что сейчас собирается делать процессор: читать память, стек, устройства ввода-вывода, обрабатывать прерывание и т. д.

  • Т2. Проверяет состояние ножек RDY и HLD на предмет необходимости притормозить. Если на одной из ножек есть сигнал, процессор переходит в состояние останова до его исчезновения.

  • Т3. Читает с шины данных команду.

  • Т4. Подготавливает себя к исполнению команды или выполняет команду, если для нее не требуется дополнительных действий типа чтения регистров из памяти или портов ввода-вывода (необязательный шаг). 

  • Т5. Продолжает выполнение команды (необязательный шаг).

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

808943f5692dabd56027b460b282e280.png

На осциллограмме, полученной при запуске процессора, можно увидеть, что NOP (опкод 0×00) исполняется согласно вышеописанной схеме. За четыре такта он выполняет команду:

  • выводит свое состояние на шину данных,

  • проверяет отсутствие необходимости останова,

  • читает опкод 0×00 с шины данных,

  • исполняет NOP.

А потом просит следующую команду. Это еще раз подтверждает правильность функционирования камня.

Давайте теперь разберемся, как процессор будет исполнять более длинную инструкцию в три байта, которая не требует дополнительных операндов. Например, инструкцию JMP по адресу 0×0, имеющую опкод 0xC2 0×00 0×00 (последние два байта — адрес перехода), процессор будет выполнять не менее трех машинных циклов. Точнее мы узнаем, когда запустим ее на исполнение. Первым циклом будет описанный выше М1, так как он является первым при выполнении любой инструкции, а за ним последуют два цикла чтения ЗУ.

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

Обслуживание шины данных

Подключаем КР580ВМ80А к Arduino

Финальная задача — заставить процессор исполнять программу:

0xFB           EI         # включить прерывания
0xF3           DI         # выключить прерывания
0xC2 0x00 0x00 JMP 0x0000 # прыгнуть в начало

Эта программа очень полезна, так как у процессора имеется нога INTE, на которой есть сигнал, когда прерывания включены. Если программа заработает корректно, то на этой ноге мы увидим меандр.

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

План действий у меня такой:

  • Запустить «ардуину» и первым делом подать логическую единицу на вход RST-процессора, чтобы он инициализировался, занулил себе PC.

  • Инициализировать на «ардуине» три порта — один под шину данных, два под шину адреса.

  • Инициализировать два прерывания — на запросы чтения и записи от процессора соответственно.

  • Подать логический 0 на сигнал RST.

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

Есть только один напряженный момент: процессор я заводил на частоте 500 КГц, минимально допустимой. Микроконтроллер Arduino работает на 16 МГц. (Почти) любая инструкция исполняется «ардуиной» за один такт, но микроконтроллер в ней конвейерный 4-стадийный. Соответственно, результата исполнения последней инструкции мы дождемся только через четыре такта после ее начала. Нехитрой арифметикой получаем, что в один цикл КР580ВМ80А Arduino успеет исполнить не более 29 инструкций. Запоминаем это на всякий случай. Вдруг что-то не заработает, будет повод проверить.

Итак, подключаем младшую часть адреса к порту K, старшую к порту A. Шину данных подключаем к порту F. Делаем это в соответствии со схемой:

c529e06cbe799da0f19d071390464825.png

И земли соединить не забываем, а то обидно будет за мертвую «ардуину». Собираем и будем писать свиток с проклятиями код…

5fcbb09e96a57ad9da2b51816dcfcb84.jpegf8544d477a39edef6c1950ab1cd0b4ad.png

Учим Arduino мимикрировать под RAM

Давайте определим порты шины адреса:

#define ADDRL_DDR DDRK
#define ADDRL_PORT PORTK
#define ADDRL_PIN PINK
#define ADDRH_DDR DDRA
#define ADDRH_PORT PORTA
#define ADDRH_PIN PINA

И сразу настроим их на вход без подтягивающего резистора (в рамках паранойи):

static void abus_init() {
    ADDRL_DDR = 0x00;
    ADDRH_DDR = 0x00;

    ADDRL_PORT = 0x00;
    ADDRH_PORT = 0x00;
}

Таким же образом инициализируем порты шины адреса:

#define DATA_DDR DDRF
#define DATA_PORT PORTF
#define DATA_PIN PINF

static void dbus_init() {
    DATA_DDR = 0x00;
    DATA_PORT = 0x00;
}

Шину адреса сам КР580ВМ80А иногда использует на выход. Поэтому очень важно не допускать ситуаций, когда каждый контроллер пытается установить шину в своё состояние. В ходе такого эксперимента один из камней-ветеранов храбро пал на поле боя, за что следует отдать ему дань уважения: ценой своей жизни он рассказал нам, как делать не надо.

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

#define ISR_READ_INT INT0
#define ISR_READ_VECT INT0_vect
#define ISR_READ_ISC0 ISC00
#define ISR_READ_ISC1 ISC01
#define ISR_READ_DDR DDRD
#define ISR_READ_PORT PORTD
#define ISR_READ_PIN PIND
#define ISR_READ_PINNUM 0

И заряжаем это в свои регистры:

static void isr_init() {
    EICRA = _BV(ISR_READ_ISC0) | _BV(ISR_READ_ISC1);
    EIMSK |= _BV(ISR_READ_INT);

    ISR_READ_DDR &= ~_BV(ISR_READ_PINNUM);
    ISR_READ_PORT |= _BV(ISR_READ_PINNUM);
}

Определяем обработчик прерывания:

ISR(ISR_READ_VECT) {
    if(ISR_READ_PIN & _BV(ISR_READ_PINNUM)) {
        uint16_t address = ADDRL_PIN | (ADDRH_PIN << 8);
        DATA_DDR = 0xff;
        DATA_PORT = INTEL8080_RAM[address];
    } else {
        DATA_PORT = 0x0;
        DATA_DDR = 0x0;
    }
}

Здесь в массиве INTEL8080_RAM лежит та самая программа, которую я описывал выше. Только ее нужно скомпилировать в уме:

static uint8_t INTEL8080_RAM[] = {
    0xfb, 0xf3, 0xc3, 0x00, 0x00
};

Также нам понадобится подключить вход RST-процессора к Arduino, чтобы:  

  • позволить ему инициализироваться,

  • позволить инициализироваться «ардуине» и быть уверенным, что без нее процессор никуда не убежит.

#define INTEL8080_RST_DDR DDRD
#define INTEL8080_RST_PORT PORTD
#define INTEL8080_RST_PIN PIND
#define INTEL8080_RST_PINNUM 2

static void init_intel8080_reset() {
    INTEL8080_RST_DDR |= _BV(INTEL8080_RST_PINNUM);
    INTEL8080_RST_PORT |= _BV(INTEL8080_RST_PINNUM);

}
static void intel8080_reset() {
    INTEL8080_RST_PORT |= _BV(INTEL8080_RST_PINNUM);
}
static void intel8080_run() {
    INTEL8080_RST_PORT &= ~_BV(INTEL8080_RST_PINNUM);
}

Вот и все, теперь собираем:

int main() {
    cli();

    init_intel8080_reset();
    intel8080_reset();

    abus_init();
    dbus_init();
    isr_init();

    _delay_us(1000);

    sei();

    intel8080_run();

    for(;;);
}

Компилируем и…

7ca79b4ec0df3855b357d4f10737ecf3.png

Бинго! На ножке INTE появилась жизнь! Мы включаем прерывание ровно на один машинный цикл, выключаем обратно, потом начинаем сначала. Здесь можно отследить даже порядок чтения байтов инструкции (синий — сигнал запроса на чтение, желтый — INTE). Я подписал на иллюстрации.

  • 1 машинный цикл — читаем инструкцию EI, исполняем, включаются прерывания

  • 2 машинный цикл — читаем инструкцию DI, исполняем — выключаются прерывания

  • 3–5 машинные циклы — читаем команду JMP 0×0, так как ее длина — три байта. На последнем цикле исполняем.

  • 7 машинный цикл — начало второй итерации, снова читаем EI и идем по кругу.

Заметили одну странность в моем списке? Откуда-то взялся лишний цикл. Их должно быть пять на одну итерацию. Об этом говорит и документация, и размер команд. Но лишний цикл все равно есть. 

Вроде уже работает, но почему-то не так, как я хотел

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

Оказалось, Arduino не поспевает за 50-летним старичком. Хоть он и работает на своей минимальной частоте в 500 кГц, это все равно слишком быстро. Периодически «ардуина» не успевает сменить команду на шине и процессор исполняет ее еще раз. Это были долгие сутки отладки осциллографом. Я полюбил GDB горячей любовью!

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

ISR(ISR_READ_VECT) {
  60:   1f 92           push    r1
  62:   0f 92           push    r0
  64:   0f b6           in      r0, 0x3f        ; 63
  66:   0f 92           push    r0
  68:   11 24           eor     r1, r1
  6a:   0b b6           in      r0, 0x3b        ; 59
  6c:   0f 92           push    r0
  6e:   8f 93           push    r24
  70:   ef 93           push    r30
  72:   ff 93           push    r31
    if(ISR_READ_PIN & _BV(ISR_READ_PINNUM)) {
  74:   48 9b           sbis    0x09, 0 ; 9
  76:   0e c0           rjmp    .+28            ; 0x94 <__vector_1+0x34>
        uint16_t address = ADDRL_PIN | (ADDRH_PIN << 8);
  78:   80 91 06 01     lds     r24, 0x0106     ; 0x800106 <__TEXT_REGION_LENGTH__+0x700106>
  7c:   e0 b1           in      r30, 0x00       ; 0
  7e:   f0 e0           ldi     r31, 0x00       ; 0
  80:   fe 2f           mov     r31, r30
  82:   ee 27           eor     r30, r30
  84:   e8 2b           or      r30, r24
        DATA_DDR = 0xff;
  86:   8f ef           ldi     r24, 0xFF       ; 255
  88:   80 bb           out     0x10, r24       ; 16
        DATA_PORT = INTEL8080_RAM[address];
  8a:   e0 50           subi    r30, 0x00       ; 0
  8c:   fe 4f           sbci    r31, 0xFE       ; 254
  8e:   80 81           ld      r24, Z
  90:   81 bb           out     0x11, r24       ; 17
  92:   02 c0           rjmp    .+4             ; 0x98 <__vector_1+0x38>
    } else {
        DATA_PORT = 0x0;
  94:   11 ba           out     0x11, r1        ; 17
        DATA_DDR = 0x0;
  96:   10 ba           out     0x10, r1        ; 16
    }
}
  98:   ff 91           pop     r31
  9a:   ef 91           pop     r30
  9c:   8f 91           pop     r24
  9e:   0f 90           pop     r0
  a0:   0b be           out     0x3b, r0        ; 59
  a2:   0f 90           pop     r0
  a4:   0f be           out     0x3f, r0        ; 63
  a6:   0f 90           pop     r0
  a8:   1f 90           pop     r1
  aa:   18 95           reti

О ужас: 38 инструкций и чеховское ружье! Мы не успеваем только потому, что у нас есть calling convention, о которой заботится компилятор. Он не понимает, что после инициализации мы не исполняем какой-либо код, достойный соглашения о вызовах. Придется ему объяснять…

Тут моя программистская душа возрадовалась! Вы когда-нибудь читали в документации к gcc атрибуты вызова функций? Я каждый раз читал и с интересом задавался вопросом: кому, когда и как понадобится бо́льшая их часть? И здесь я нашел naked. Этот атрибут говорит компилятору не создавать пролог и эпилог функции (которые у нас и занимают бо́льшую часть кода!), но взамен требует принести тело функции в жертву — написать ее на ассемблере. Сделаем это.

Заставим наше прерывание срабатывать только по фронту:

static void isr_init() {
    EICRA |= _BV(ISR_READ_ISC1) | _BV(ISR_READ_ISC0);
    EIMSK |= _BV(ISR_READ_INT);
}

Сделаем его naked:

ISR(ISR_READ_VECT, ISR_NAKED) {
    asm volatile(

И начнем переписывать строчку за строчкой. 

Переключаем порт шины данных в выходной режим:

ldi r16, 0xff
out 0x10, r16

Читаем два байта шины адреса со своих портов:

lds r30, 0x0106
in r31, 0x0

Вычисляем смещение байта инструкции в нашем массиве:

add r30, %0
adc r31, %1

Загружаем байт в регистр:

ld r16, Z

И отправляем его в порт шины данных:

out 0x11, r16

Чуть-чуть ждем, чтобы процессор успел прочитать данные:

.rep 20
nop
.endr

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

eor r1, r1
out 0x11, r1
out 0x10, r1

И выходим из прерывания:

reti

Соберем все вместе:

ISR(ISR_READ_VECT, ISR_NAKED) {
    asm volatile(
        "ldi r16, 0xff\n\t"
        "out 0x10, r16\n\t"
        "lds r30, 0x0106\n\t"
        "in r31, 0x0\n\t"
        "add r30, %0\n\t"
        "adc r31, %1\n\t"
        "ld r16, Z\n\t"
        "out 0x11, r16\n\t"
        ".rep 20\n\t"
        "nop\n\t"
        ".endr\n\t"
        "eor r1, r1\n\t"
        "out 0x11, r1\n\t"
        "out 0x10, r1\n\t"
        "reti\n\t"
        ::
        "r" ((uint8_t)(uint16_t)INTEL8080_RAM),
        "r" ((uint8_t)((uint16_t)INTEL8080_RAM >> 8))
    );
}

Попробуем запустить:

a09a90e6b656332501a3240d0c396120.png

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

Заключение

Я очень хочу считать программу с магнитофона. Какую-нибудь игру. И поиграть в нее на этом процессоре. Но для этого нужна полноценная память, клавиатура, монитор, а лучше сразу терминал. И этим предстоит заняться. Если вам понравился формат и статья зайдет, то следующую я планирую посвятить легендам об оперативной памяти и устройствах ввода-вывода процессора. Он, как доблестный воин после левел-апа, обзаведется 4 КБ SRAM и откроет в инвентаре порт для вывода текста в ноутбук (здесь также поможет «ардуина»). Надеюсь, в следующий раз КР580ВМ80А скажет нам «Hello, world!», ну или что там говорили в те времена…

© Habrahabr.ru