Привет! Меня зовут Илья Мамай, я инженер-программист в группе разработки операционных систем 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 рублей, надеюсь, не обманули.
Из блока питания, пары амперметров, добротного советского тумблера, делителя напряжения и клеевого пистолета я собрал источник питания для своего будущего убийцы рынка микропроцессоров. Достал макетную плату и за несколько вечеров и пару поездок в Митино осилил процессорный модуль (попутно научил паять соседа и вынудил его принять активное участие в процессе). Получил такой вот тестовый стенд:
Собирать все остальное без понимания, как проверить работоспособность, казалось бессмысленной авантюрой. Еще и диплом все-таки пришлось начать писать. И я забросил проект почти на год.
Вторая попытка
На пятки начала наступать очередная сессия, и вместо того, чтобы думать о неприятном, я начал думать о продолжении моей компьютерной авантюры. В этот раз решил действовать поэтапно:
Изучить документацию к процессору.
Собрать минимальную обвязку для его запуска, запустить и увидеть признаки его кремниевой жизни (пациенту все-таки необходима диспансеризация, в его-то годы).
Заставить Arduino симулировать память, чтобы запустить на процессоре простейшую программу и увидеть признаки ее выполнения. Так я смогу гарантировать, что пациент скорее жив, чем мертв.
Собрать память, заставить «ардуину» загружать туда информацию на старте, после чего — стать устройством передачи данных на ПК.
Двигаться дальше. В каком направлении – проблема меня будущего.
Пока я добрался только до третьего пункта, но мне уже есть, что об этом рассказать! Начну я с сердца компьютера «Микро-80» — процессора КР580ВМ80А.
КР580ВМ80А
Как я уже говорил, процессор КР580ВМ80А — это полная советская копия процессора Intel 8080, так что вся (или почти вся) известная информация об Intel 8080 верна и для нашего подопытного. Заостряю на этом внимание только потому, что единой документации на КР580ВМ80А я не нашел. Отыскал лишь несколько сухих страниц в советском справочнике «Микропроцессоры и микропроцессорные комплекты интегральных микросхем». Широкого выбора, с чего начать работу, эти страницы не давали, так что я решил начать с архитектуры и распиновки.
Хоть до появления первого 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, который легко отыскать на разборе компьютерной техники.
Если взглянуть внимательно на разъем ATX, то видно, что он выдает напряжения +5, +12 и –12 В. –5 В присутствует в разъеме ATX, но, как я понял, в наше время оно чаще не реализуется. Мы получим его делителем напряжения, ведь потребление всех микросхем по линии –5 В очень мало, так что излишества нам ни к чему.
Также я добавлю туда тумблер для запуска (между землей и PS_ON), чтобы не ковыряться в контактах скрепкой, и два амперметра по линиям +5 В и +12 В, чтобы до появления белого дыма и запаха гари увидеть, что в работе схемы что-то идет не так. Итак, меньше слов и больше схем!
Номиналы резисторов я рассчитывал исходя из следующих условий. Ток по линии –5 В, согласно документации к процессору, не превышает 1 мА. Если ток, проходящий через два резистора, будет намного больше тока потребления по линии –5 В, то мы добьемся достаточной точности величины напряжения.
За «намного больше» обычно берется ровно в 10 раз. Потому из схемы и правил Кирхгофа…
…при учете, что V0 = –12 В, V1 = –5 В, I = 10 мА, получаем R1 = 500 Ом, R2 = 700Ом. Ну или, если брать ближайшие доступные номиналы, то R1 = 500 Ом, R2 = 680 Ом.
В соответствии с документацией, для долголетия процессора особенно важен порядок включения и выключения напряжений питания при запуске. Первым должно появиться –5 В, потом +5 В, потом +12 В. Отключаться питание должно в обратной последовательности. Из тематических форумов я узнал, что главное — не подавать положительные питания раньше, чем отрицательные, тогда процессор будет жить.
Порядок появления питаний в БП стандарта ATX не регламентирован, за это отвечают схемы на материнской плате. Однако, исходя из типовых схем и тех фактов, что…
все три напряжения берутся с одного и того же трансформатора,
емкость сглаживающих конденсаторов по цепям +5 В и +12 В намного больше емкости по цепи –5 В,
я купил имплант удачи за 4000 крышечек и нам повезет,
…можно предположить, что процессор выживет при запуске, так как –5 В появятся и пропадут первыми. Уверенно срезаем ATX разъем, паяем источник питания и получаем примерно такой DIY-экспонат. Мы с товарищами ласково называем его «электрофорез»:
Для экспериментов этого источника питания будет вполне достаточно, переходим к следующему важному для запуска процессора шагу — генерации тактовых сигналов.
Генератор тактовых сигналов
Документация и журнал «Радио» говорят, что необходимо два тактовых сигнала, их частота не должна быть ниже 500 КГц и выше 2,5 МГц и они должны иметь следующую форму:
Сигнал C2 должен обгонять сигнал C1 на треть периода. Для генерации таких сигналов есть два пути:
использование специализированной микросхемы КР580ГФ24,
сборка генератора из счетчика и микросхем логики.
В процессе радиотехнических изысканий я купил на развес килограмм советских микросхем и нашел там золото и КР580ГФ24. Но вообще отыскать ее бывает непросто, а логические микросхемы серии К155 все еще продаются в каждом хлебном магазине. Поэтому предлагаю рассмотреть оба варианта, хоть я и остановился на первом.
Путь джедая: генератор тактовых сигналов на логических элементах
Первое, что нам необходимо, — это сгенерировать меандр, из которого мы логическими операциями получим два необходимых тактовых сигнала. Для генерации меандра на логических элементах есть старая добрая советская схема на микросхеме К155ЛН1, представляющая собой шесть логических НЕ.
Работает она следующим образом. Конденсатор С9 попеременно заряжается и разряжается двумя инверторами, ведь пока напряжение на нем ниже напряжения высокого логического уровня, второй инвертор на выходе выдает высокий логический уровень, а первый — низкий. Соответственно, конденсатор заряжается выходом второго инвертора через резистор R2. А как только напряжение на нем увеличится до напряжения высокого логического уровня, оба инвертора переключатся и станут его разряжать. Снимая напряжение с выхода второго инвертора, мы и получим желаемый меандр. Кварцевый резонатор стабилизирует частоту переключения триггеров на своей резонансной частоте.
С меандром разобрались. Дальше предлагаю из него получить несколько меандров с частотами, кратными исходной. Давайте подключим к нему счётчик и посмотрим, что из этого выйдет.
Здесь я нарисовал 5-битный счетчик К155ИЕ5, распиновка которого выглядит так:
С0 — вход первого однобитного счетчика,
С1 — вход второго 3-битного счетчика (потому я их замкнул, чтобы в итоге получить 4-битный счетчик),
Q0-Q3 — выходы 4 бит рассчитанного счетчиком двоичного числа.
Входы R1 и R2 обнуляют значения каждого счетчика, а нам обнулять их незачем, потому они притянуты к земле. Используемый таким образом счетчик работает как делитель входной частоты на 2, 4, 8 и 16. Потому на выходах схемы out_2, out_4, out_8 мы получим следующие уровни сигналов.
А теперь настало время решать последнюю задачу, генерацию сигналов С1 и С2, самым простым и надежным методом — методом вглядывания. Вычислим логическое И сигналов out2 и out4, НЕ сигнала out2 и зарисуем результат.
Похоже ли это на требуемый сигнал? Ну, отдаленно да… Периоды, правда, не совпадают, да и смещены они не на треть периода, а на четверть. Но при запуске процессор должен бойко сказать: «С пивом покатит!» И заработать, ведь такая схема и была в «Микро-80». Используем out_4 и out_8 для вычисления тактовых сигналов, так как их вчетверо большая частота может оказаться полезной в будущем.
Осталась одна небольшая деталь. Для КР580ВМ80А необходима амплитуда тактовых сигналов 12 В, а у нас только 5 В. Потому воспользуемся какой-нибудь микросхемой с открытым коллектором типа К155ЛА7 и получим итоговую схему.
Собираем тактовый генератор:
Запускаем и давайте подглядывать: тыкаем осциллографом в ножки 6 и 8 микросхемы К155ЛА7.
Путь ситха: микросхема КР580ГФ24
Все, что мы сделали в предыдущем разделе, умеет одна микросхема — КР580ГФ24. А еще у нее есть типовая схема включения, которая радует глаз своей простотой, в отличие от ранее собранного осьминога:
Здесь все точно так же, просто вся логика собрана в одну микросхему. А цепь из диода, резистора и конденсатора отвечает за инициализацию процессора при старте. Ее мы пока игнорируем, как и все остальные сигналы кроме C1 и С2 — те пугают. Разберемся с ними по мере надобности или когда ничего не заработает, а пока просто подтянем их к земле или питанию в зависимости от того, инвертированы они или нет. Сейчас основная цель — запуститься. Собираем:
Подключаем процессор (питание и C1, C2). Осталось закинуть входные ноги на землю или на питание. Смотрим на осциллограмму:
Работает, ура! Остановимся на этой схеме, так как:
она проще, соответственно, вероятность ошибок меньше,
мама в детстве сказала мне, что джедаев не существует (и вообще, нам больше по нраву фэнтези-рпг).
Работоспособность «джедайского» варианта для всех дальнейших конструкций статьи я все же проверил за кадром. Вдруг кто решит заняться тем же.
Запускаем процессор
Наш тестовый стенд при запуске вроде бы не горит, но как понять, что он работает? Процессор будет пытаться читать инструкции с шины данных, а значит, поднимать сигнал RC (нога 17), чтобы сигнализировать об этом. На ней мы ожидаем что-то вроде меандра — это и будет признаком его успешного запуска. Тыкаем осциллограф одним каналом на 17 ногу, а вторым на 12 ногу К155ИЕ5:
Ура! Работает! Каждый четвертый такт сигнала out_2 процессор пытается читать шину данных, но там ничего нет, там нули. А инструкция с опкодом 0x00 — это NOP. Очень удобно.
Итак, мы сделали большой шаг — обеспечили процессор всем необходимым для работы. Осталось лишь дать ему инструкции. Но прежде необходимо понять, как процессор исполняет инструкции. Почему он читает инструкцию каждый 4 такт, может ли чаще или реже…
Как КР580ВМ80А исполняет инструкции
Процессор имеет переменную длину инструкции, от 1 до 3 байт. Каждая инструкция выполняется за 1–5 машинных циклов, каждый машинный цикл состоит из 3–5 тактов. Такт — это один период сигналов C1 и C2.
В первом машинном цикле М1, который занимает 4–5 тактов (Т), процессор делает следующее.
Т1. Выводит адрес на шину адреса, а на шину данных — информацию о своем состоянии. Здесь указывается, что сейчас собирается делать процессор: читать память, стек, устройства ввода-вывода, обрабатывать прерывание и т. д.
Т2. Проверяет состояние ножек RDY и HLD на предмет необходимости притормозить. Если на одной из ножек есть сигнал, процессор переходит в состояние останова до его исчезновения.
Т3. Читает с шины данных команду.
Т4. Подготавливает себя к исполнению команды или выполняет команду, если для нее не требуется дополнительных действий типа чтения регистров из памяти или портов ввода-вывода (необязательный шаг).
Т5. Продолжает выполнение команды (необязательный шаг).
Далее процессор может переходить в один из девяти вариантов машинных циклов. Среди них интересным пока является только цикл чтения запоминающего устройства, в котором происходит чтение памяти по адресу из PC или одной из пар регистров общего назначения. Проходит этот цикл по тому же сценарию, что и М1. Общая диаграмма состояний процессора для самый пытливых представлена на картинке:
На осциллограмме, полученной при запуске процессора, можно увидеть, что NOP (опкод 0х00) исполняется согласно вышеописанной схеме. За четыре такта он выполняет команду:
выводит свое состояние на шину данных,
проверяет отсутствие необходимости останова,
читает опкод 0х00 с шины данных,
исполняет NOP.
А потом просит следующую команду. Это еще раз подтверждает правильность функционирования камня.
Давайте теперь разберемся, как процессор будет исполнять более длинную инструкцию в три байта, которая не требует дополнительных операндов. Например, инструкцию JMP по адресу 0x0, имеющую опкод 0xC2 0x00 0x00 (последние два байта — адрес перехода), процессор будет выполнять не менее трех машинных циклов. Точнее мы узнаем, когда запустим ее на исполнение. Первым циклом будет описанный выше М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. Делаем это в соответствии со схемой:
И земли соединить не забываем, а то обидно будет за мертвую «ардуину». Собираем и будем писать свиток с проклятиями код…
Учим 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(;;);
}
Компилируем и…
Бинго! На ножке INTE появилась жизнь! Мы включаем прерывание ровно на один машинный цикл, выключаем обратно, потом начинаем сначала. Здесь можно отследить даже порядок чтения байтов инструкции (синий — сигнал запроса на чтение, желтый — INTE). Я подписал на иллюстрации.
1 машинный цикл — читаем инструкцию EI, исполняем, включаются прерывания
2 машинный цикл — читаем инструкцию DI, исполняем — выключаются прерывания
3–5 машинные циклы — читаем команду JMP 0x0, так как ее длина — три байта. На последнем цикле исполняем.
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))
);
}
Попробуем запустить:
Лишний цикл пропал! И теперь процессор работает стабильно многие часы! Получается, зверушка оказалась живой и на ней теперь можно надстраивать всякую периферию.
Заключение
Я очень хочу считать программу с магнитофона. Какую-нибудь игру. И поиграть в нее на этом процессоре. Но для этого нужна полноценная память, клавиатура, монитор, а лучше сразу терминал. И этим предстоит заняться. Если вам понравился формат и статья зайдет, то следующую я планирую посвятить легендам об оперативной памяти и устройствах ввода-вывода процессора. Он, как доблестный воин после левел-апа, обзаведется 4 КБ SRAM и откроет в инвентаре порт для вывода текста в ноутбук (здесь также поможет «ардуина»). Надеюсь, в следующий раз КР580ВМ80А скажет нам «Hello, world!», ну или что там говорили в те времена…
Комментарии (14)
mark_ablov
15.01.2025 09:02Я сразу FPGA начал использовать, чтоб был запас для 8080, который крутится на частоте 3.125 МГц. По моему опыту даже частота в 1МГц с трудом переваривается не самыми мощными микроконтроллерами.
SIISII
15.01.2025 09:02Хотя, может, поздние варианты и поддерживали частоты свыше 3 МГц, но, вроде б, предел везде 2,5 указывается... Впрочем, обычно работает; кажись, в заводском Векторе его на 3 МГц как раз и гоняли.
А ПЛИС -- откопали старую, 5-вольтовую, или преобразователи уровней лепите?
mark_ablov
15.01.2025 09:02Я использовал оригинальный 8080 от Intel'a, а не клоны. Штатная частота Intel 8080A-1 как раз 3.125. Преобразователей накидал - уложился в 5 микросхем, с учётом отдельного чипа для конвертации уровней тактового сигнала в 12в (FPGA так же и тактирует 8080).
SIISII
15.01.2025 09:021) Если говорить строго, то нужно гарантировать порядок подачи питания, причём -5 В идут первыми, иначе проц имеет полное право сдохнуть. Учитывая, что сам БП никакого порядка не гарантирует, единственный способ -- делать свой... скажем так, контроллер питания, который пропускает на проц напряжения в нужном порядке. Правда, я неоднократно встречал слухи, что у советских процов поздних выпусков это требование стало необязательным: в принципе, это вполне возможно, ведь та же Интел через какое-то время выпустила 8085, чуть ли не важнейшим отличием которого стало только одно напряжение питания (отрицательное смещение научились вырабатывать "на борту"), который у нас тоже клонировали -- КР1821ВМ85, если склероз не изменяет.
2) Для отладки Ардуина вполне пойдёт; я сам временами использую её для разборок с логикой работы сложных микросхем, когда документация не даёт полного ответа или допускает двоякое толкование. Но для нормальной работы в качестве периферии, увы, не подходит. Я вот для аналогичной, но несколько более объёмной задачи (эмуляция периферии для СМ ЭВМ, в девичестве PDP-11) рассматриваю какой-нибудь мощный ARM -- скажем, GD32H7 с ядром Cortex-M7 на 600 МГц, ну а память будет настоящей, только на относительно современных (1990-х годов) микросхемах статического ОЗУ.
ПЛИС в роли периферии смотрится лучше, но там сразу кучу геморроя с согласованием уровней, ведь ПЛИС, поддерживающие +5 В, найти сложно, их выпуск прекращён много лет назад. Но такой вариант тоже рассматриваю, но, вероятно, как "продолжение банкета": отлаживаться по понятным причинам проще на микроконтроллере.
3) Достать почти все микросхемы 580-й серии пока ещё возможно. Самый дефицит, похоже, -- КР580ВВ79, которая применяется мало где (из любительских, кажется, только в ЮТ-88).
4) А вот в те годы (года эдак до 1987-88) "достать" означало прямо или косвенно украсть, ибо купить легально даже какую-нибудь К155ЛА3 было проблематично (в магазинах были лампы, транзисторы и несколько типов аналоговых микросхем -- и, по большому счёту, всё). Фактически, так сделали и разработчики Микро-80: где-то было то ли интервью, то ли рассказ о его создании, там и говорилось, что им в контору ошибочно прислали К580ИК80 (раннее название этого проца), которого ещё ни в каких справочниках не было, но они имели доступ к буржуйской литературе, кто-то связала советское название с 8080 -- и они из того, что было, слепили комп. Не по заданию конторы, а по собственному желанию использовали целую кучу государственных микросхем. Как только им не ай-ай-ай :)
DvoiNic
15.01.2025 09:02Самый дефицит, похоже, -- КР580ВВ79, которая применяется мало где
Я вообще не помню применения в самоделках. Только к 1816ВМ48/51 подключали
voldemar_d
15.01.2025 09:02Надо сразу взять Xilinx Spartan и реализовать на нём всё, как создатели ZX Spectrum Next сделали :-) Правда, из-за дороговизны и дефицита в последних реализациях они на другой FPGA перешли.
ash_lm
15.01.2025 09:02Впервые слышу про СБМ-20 в советских светофорах, да и поиск ничего не находит.
DvoiNic
15.01.2025 09:02Решил собрать «Микро-80» на макетных платах по схемам
Это жестоко! Но только за такую безбашенность можно поставить плюсик!
Ну а к высказанным выше в комментах рекомендациям использовать FPGA - присоединяюсь.
зы. Микро-80 у нас в те годы в целом заработал, но дисплейный модуль мы так и не допаяли..
radiolok
Все же использовать Arduino в роли периферийного контроллера - перебор. при своих 16МГц она не будет ничего успевать делать - что и показал ассемблерный дебаг.
Я сейчас делаю похожую задачу - делаю плату для дебажного порта пишущей машинки на базе Z80 на 2.5МГц. Ее задача мониторить адрес/данные системной шины и по usb выплевывать Коната трассу того что там такого выполняет проц. Прошивка явно живая - активничает по периферии, но внешне машинка не работает :)
Так вот смотрю на BlackPill на базе stm32f411 и думаю - а хватит ли мне 100МГц для такого или половину времени буду WAIT держать? В описанном в статье эксперименте отношение частот 1к32, у меня 1к40... Быть может лучше ПЛИС на роль периферии?
mmamayka Автор
Это правда - с Arduino получилось больше мороки, но я этого не понимал с самого начала, так что использовал то, что было под рукой. Дальше, когда буду наращивать периферию, попробую использовать что-нибудь более шустрое. ПЛИС я трогать не хотел, так как опыта разработки очень мало, да и в схемотехнике не очень силен. Не хотел получить второй активный излучатель багов.
Мне кажется (но я не экспертен), что успевать выплевывать plain text трассу, не зажимая WAIT, при таком соотношении частот маловероятно, так как только на формирование строк уйдет пару десятков инструкций. Я хочу попробовать, пока у меня не появилась STM-ка, держать перед Ардуиной логическую схему, которая будет дергать её за прерывания, чтобы избавиться от ветвлений в обработчиках и дать себе большего пространства для маневра. Хотя думаю, что дело в итоге все равно придет к ПЛИС.