Программатор для 8-битного компьютера на макетной плате с Arduino UNO – Сборка и программирование
Компоненты и материалы
| Arduino UNO | 1 |
О проекте
Что это за проект?
Это расширение проекта 8-битного компьютера на макетной плате Бена Итера (Ben Eater). Я с большим удовольствием следил за видео Бена о построении компьютера на макетной плате и собрал свою версию. Если вы не знакомы с работой Бена, настоятельно рекомендую посмотреть его видео перед изучением этого проекта — без контекста он будет непонятен.
После сборки компьютера стало очевидно: если программу приходится вводить вручную через DIP-переключатели при каждом включении, компьютер будет использоваться крайне редко. Даже ввод всего 16 байт быстро утомляет. Кроме того, я планировал расширить память до 256 байт, что имело бы смысл только при возможности загрузки и сохранения программ.
Что можно сделать с этим проектом?
- Сохранять программу из ОЗУ компьютера на макетной плате
- Загружать программу в ОЗУ компьютера на макетной плате
- Автоматически запускать сохранённую программу при старте компьютера
- Просматривать и изменять содержимое памяти через последовательное соединение
- Программировать компьютер на языке ассемблера
- Дизассемблировать содержимое памяти
- Выполнять пошаговое выполнение инструкций
- Устанавливать точки останова и запускать выполнение до точки останова
Видео с демонстрацией этих функций доступно ниже (прошу простить качество — я не профессиональный видеоблогер).
Как это работает?
При создании проекта я ставил две цели:
- Минимизировать необходимое оборудование
- Минимизировать изменения в самом компьютере на макетной плате
Изначально я думал использовать EEPROM для хранения программ и логику для переноса программ из EEPROM в ОЗУ компьютера. Однако проектирование такой логики оказалось слишком сложным для меня (я больше программист, хотя и люблю работать «впритык» к железу). Поскольку я большой фанат Arduino, в какой-то момент я подумал о замене логики переноса на Arduino. Понадобилось время, чтобы примириться с мыслью, что использование Arduino — не «обман» (в конце концов, даже Arduino UNO намного мощнее самого компьютера на макетной плате), но я доволен результатом.
Что должен делать Arduino? Он должен уметь читать данные из памяти и записывать их туда. Самый простой и наименее инвазивный способ — использовать тот же интерфейс, что и модуль памяти: шину и управляющие сигналы. Цифровые входы/выходы Arduino двунаправленны, поэтому подключение 8 из них напрямую к шине позволяет Arduino читать и писать в шину. Чтобы заставить модуль ОЗУ читать из шины или писать в неё, достаточно установить сигналы MI/RI/RO соответствующим образом. Обычно эти сигналы управляются EEPROM логики управления, поэтому если Arduino тоже будет ими управлять, возникнут конфликты и возможные короткие замыкания. Однако AT28C16 EEPROM, используемые Беном, имеют вход Chip Enable (CE), который переводит все выходы данных в высокоомное состояние — это позволяет Arduino манипулировать сигналами.
Для чтения ОЗУ Arduino должен выполнить следующее:
- Установить сигнал CE EEPROM в высокое состояние (отключить логику управления)
- Вывести первый адрес для чтения на шину
- Установить сигнал MI в высокое состояние и дождаться перехода тактового сигнала из низкого в высокое
- Установить MI в низкое, RO в высокое и дождаться перехода тактового сигнала
- Прочитать байт данных с шины
- Повторить со шага 2 для всех 16 адресов
- Установить сигнал CE EEPROM в низкое состояние (включить логику управления обратно)
Запись в ОЗУ выполняется похожим образом — Arduino просто пишет в шину и устанавливает RI вместо RO. Есть технические нюансы, но описанный механизм — основа проекта.
Какие изменения нужны в компьютере на макетной плате?
Необходимы два изменения:
- Добавить подтягивающие к земле резисторы на выходы данных EEPROM логики управления
- Отключить (или постоянно сбрасывать) кольцевой счётчик пока EEPROM отключен
Подтягивающие резисторы
Они нужны, потому что когда EEPROM отключен, подтягивающие к питанию резисторы логических элементов, к которым подключены управляющие сигналы (AO, AI, EO…), поднимут эти сигналы в высокое состояние. Это приведёт к тому, что несколько регистров одновременно будут писать в шину, вызывая конфликты.
Подтягивающие резисторы на входах 74LS составляют около 10 кОм. Подтягивающие к земле резисторы должны быть достаточно малы, чтобы опустить напряжение в логический ноль. Для большинства линий я использовал резисторы 3,3 кОм. Есть два исключения: сигнал «SU» подключён к 8 элементам XOR, поэтому эффективное подтягивание составляет 10кОм/8 = 1,25 кОм. Резистор подтяжки к нулю должен быть значительно меньше 1 кОм. К счастью, сигнал SU (вычитание) не управляет взаимодействием с шиной, поэтому можно просто не ставить на него подтяжку. Во-вторых, сигнал CE (разрешение счётчика) потребовал резистор 1 кОм — большие значения вызывали случайное поведение счётчика команд в некоторых случаях.
Самым удобным местом для размещения резисторов оказалась макетная плата со всеми синими светодиодами — между анодами светодиодов (подключёнными к выходам EEPROM) и землёй. (Для сигналов HLT/MI/RI резисторы пришлось разместить в других местах на плате).
Сброс кольцевого счётчика
Это изменение не строго необходимо для сохранения/загрузки программ, но позволяет плавно передать управление от Arduino обратно к логике управления. Суть — удерживать кольцевой счётчик в нуле, пока CE высокий (логика управления отключена). Когда Arduino переключает CE обратно в ноль (включая логику управления), счётчик будет в нуле, и компьютер начнёт выполнять следующую инструкцию.
В моей сборке я использовал один выход EEPROM для сброса кольцевого счётчика. Это повышает производительность, сбрасывая счётчик сразу после завершения инструкции. Также это автоматически даёт сброс счётчика при отключении EEPROM логики управления: подтягивающий резистор на выходе EEPROM тянет сигнал в ноль, сбрасывая счётчик.
Если вы используете реализацию Бена с фиксированным 5-шаговым кольцевым счётчиком, следующее расширение его схемы сброса должно сбрасывать счётчик, когда CE высокий (нажмите стрелки влево/вправо для переключения между оригинальной схемой Бена и расширенной версией):

Требуется 3 дополнительных элемента NAND, то есть ещё одна микросхема 74LS00. Я не тестировал этот подход, но он должен работать.
Это изменение не обязательно — можно опустить его в начале. Сохранение, загрузка, монитор, ассемблер и дизассемблер будут работать нормально. Однако любое действие, требующее передачи управления от Arduino к логике управления, не сработает. Главное — автозапуск сохранённых программ при старте, а также пошаговое выполнение и запуск в отладчике.
Как настроить Arduino?
Загрузите скетч из GIT-репозитория в Arduino и подключите его к компьютеру на макетной плате следующим образом:
- Вывод 5V Arduino (НЕ Vin!) к шине 5V на макетной плате
- Вывод GND Arduino к GND на макетной плате
- Цифровые выводы Arduino 2–9 к шинам 0–7
- Цифровой вывод 10 к выходу EEPROM логики управления, управляющему RO
- Цифровой вывод 11 к выходу EEPROM логики управления, управляющему RI
- Цифровой вывод 12 к выходу EEPROM логики управления, управляющему MI
- Аналоговый вход 0 к сигналу CLOCK
- Аналоговый вход 3 к CE (вывод 18) всех EEPROM логики управления и через резистор 10 кОм к +5V
Кроме того, нужно подключить аналоговые входы 1 и 2 Arduino к DIP-переключателям и кнопкам согласно схемам (подробности см. во вложенном файле Fritzing).


Для абсолютно минимальной (но рабочей) версии можно сделать следующее:
- Добавить подтягивающие к земле резисторы 3,3 кОм на выводы EEPROM, управляющие AO, CO, EO, IO, RO
- Пропустить инструкции по «Сбросу кольцевого счётчика» выше
- Подключить Arduino к компьютеру на макетной плате как показано выше (резистор 10 кОм в последнем шаге можно опустить)
- Соединить оба аналоговых входа 1 и 2 с землёй
Для использования кнопок загрузки/сохранения достаточно подключить кнопки, DIP-переключатели и соответствующие резисторы к аналоговым входам 1 и 2 по схемам.
Для функции автозапуска Arduino должен удерживать счётчик команд и кольцевой счётчик в нуле во время сброса и передачи программы. Подтягивающий резистор между аналоговым входом 3 и +5V держит логику управления отключенной (а значит счётчик команд в нуле) пока Arduino перезагружается. Для кольцевого счётчика следуйте инструкциям «Сброс кольцевого счётчика» выше.
Как загружать и сохранять программы?
Минимальная настройка выше позволяет управлять Arduino через последовательный интерфейс. Для связи понадобится терминальная программа вроде PuTTY или TeraTerm. Последовательный монитор Arduino IDE тоже сработает, но разделение зон ввода/вывода делает его неудобным в этом сценарии.
- Включите компьютер на макетной плате
- Подключите ПК к Arduino через USB
- Запустите терминал и настройте: 9600 бод, 8 бит, без паритета, 1 стоп-бит
- Нажмите ESC в терминале для входа в режим монитора
- Должен появиться приглашение «.»
- Введите «h» и нажмите Enter для списка команд
С минимальной настройкой доступны команды «m», «M», «C», «l» и «s» — они позволяют просматривать память, изменять её, загружать и сохранять программы.
Для сохранения/загрузки программы кнопками:
- Выключите тактовый генератор компьютера на макетной плате
- Установите DIP-переключатели на номер файла для сохранения
- Нажмите кнопку «save» или «load». Светодиод, подключённый к CE, загорится — Arduino взял управление
- Включите тактовый генератор. Вы увидите, как Arduino перебирает адреса (смотрите по светодиодам регистра адреса памяти)
- Дождитесь, когда светодиоды шины перестанут мигать, а регистр адреса покажет 1111
- Выключите тактовый генератор. Светодиод CE погаснет — управление вернулось к логике управления
Для автозапуска программы при включении (при наличии всей необходимой схемы) установите DIP-переключатели на номер файла с программой и включите компьютер (или нажмите сброс). Два особых случая: если все DIP выключены — компьютер стартует нормально, без автозапуска. Если все DIP включены — Arduino сразу входит в режим монитора.
Как пользоваться ассемблером и дизассемблером?
Для использования ассемблера/дизассемблера и отладчика нужно сначала изменить программу в Arduino под свою конкретную схему. Найдите в исходном коде раздел, определяющий структуру opcodes_4bit:
struct opcodes_struct opcodes_4bit [] = {
{"NOP ", B00000000, 0, false},
{"LDA ", B00010000, 2, true},
...
{".OR ", B11111110, 0, true}, // установить начальный адрес
{".BY ", B11111111, 0, true}, // определить один байт данных
{NULL, 0, 0, false}
};
Каждая строка задаёт один опкод:
- Первое поле — мнемоника («LDA»). Для немедленной адресации включите «#» в мнемонику. То, что Бен называет «LDI», здесь будет «LDA #». (Видно, что я вырос на ассемблере 6510 на C64?)
- Второе поле — сам опкод. Младшие 4 бита всегда должны быть 0 (кроме специальных опкодов .OR и .BY).
- Третье поле — количество тактов выполнения опкода (помимо тактов выборки). Например, в моей реализации LDA имеет опкод 0001 и выполняется за 4 такта, 2 из которых — выборка. Если вы следовали инструкциям Бена (все опкоды 5 тактов), это значение всегда 3.
- Последнее поле — требует ли опкод аргумент. Например, LDA требует аргумент, а OUT — нет.
Вам нужно скорректировать этот список под опкоды вашего компьютера. Последние две строки — специальные опкоды для ассемблера, их оставьте как есть.
После ввода всех опкодов загрузите скетч в Arduino. Подключите терминал и войдите в режим монитора (ESC в терминале или все DIP включены). Теперь можно дизассемблировать программу. Ввод «d» в мониторе начнёт дизассемблирование с адреса 0.
Ассемблер минималистичен, но работает хорошо. Введите «a» для начала ассемблирования с адреса 0. Соглашения:
- Если строка не начинается с пробела, она должна начинаться с определения метки. Метки начинаются с буквы, затем буквы/цифры, максимум 3 символа, регистрозависимы.
- После первого пробела ассемблер ждёт мнемонику (LDA, STA, OUT…).
- Специальная мнемоника «.BY» напрямую задаёт байт данных для сохранения в текущем адресе.
- Специальная мнемоника «.OR» говорит ассемблеру продолжить ассемблирование с нового адреса.
- Если аргумент начинается с буквы — это метка.
- Числовой аргумент — десятичное число. Для шестнадцатеричного поставьте перед ним «$». Например, «LDA #$FF» загрузит FF в регистр A.
- Все после «;» — комментарий, игнорируется.
Пример кода Фибоначчи:
rst LDA #0 ; x = 0
STA x
LDA #1 ; y = 1
STA y
lp ADD x ; z = y + x
STA z
JC rst ; перезапуск при переполнении
OUT ; вывод z
LDA y ; x = y
STA x
LDA z ; y = z
STA y
JMP lp ; цикл
x .BY 0
y .BY 0
z .BY 0
Каковы ограничения?
Для экономии ОЗУ Arduino ассемблер работает как однопроходный (иначе пришлось бы буферизировать весь исходный код). Ассемблер записывает опкоды в память компьютера по мере ввода. Это значит, что скорость ассемблирования ограничена тактовой частотой компьютера на макетной плате. Если копировать и вставлять текст в терминал, могут теряться символы, так как Arduino не успевает обрабатывать ввод на 9600 бод (слишком много времени тратит на ожидание тактового сигнала). Обходные пути: снизить скорость порта или использовать TeraTerm с настройкой задержки между отправляемыми символами. Либо повысить тактовую частоту компьютера. Мои часы идут до 160 кГц, и на этой скорости можно копировать код при 9600 бод без проблем.
В конфигурации по умолчанию скетч работает с частотами до ~1–2 кГц (возможно чуть больше). Часы Бена в базовой конфигурации не превышают 500 Гц. Если ваши часы быстрее, найдите в коде переключатель #ifdef FAST_IO. Включение FAST_IO позволит работать с частотами до 250 кГц. Я тестировал до 160 кГц. Теоретически можно поддерживать более высокие скорости, реализовав критичные по времени циклы на ассемблере, но 160 кГц уже слишком быстро для компьютера на макетной плате с его ограниченными возможностями. Обязательно прочитайте комментарии в коде перед включением FAST_IO.
У Arduino 1 кБ EEPROM, значит можно хранить 1024/16 = 64 разных программы. На самом деле 63, так как 16 байт зарезервированы под конфигурацию. Мало, но, вероятно, достаточно для всех придумываемых программ. Только программы 0–15 выбираются через DIP (1–14 для автозапуска), но команды «s» и «l» работают со всем диапазоном 0–62.
Выглядит немного беспорядочно. Можно сделать аккуратнее?
Да! В финальной версии я использовал голый чип Atmega 328P (с кварцем 16 МГц и конденсаторами) вместо Arduino UNO. Стабилизатор напряжения на UNO не нужен, так как Arduino берёт 5V от нашего блока питания. Единственная потеря — теперь нужен отдельный USB-UART преобразователь (FTDI или похожий) для связи с Atmega. Общий вид намного лучше вписывается в остальной компьютер на макетной плате:


Ещё одна оптимизация — убрать загрузчик из Arduino/Atmega. Это устранит 2-секундную задержку запуска загрузчика. Если есть интерес — напишу инструкцию. Дайте знать в комментариях!
Код
Скетч Arduino для программатора компьютера на макетной плате
https://github.com/dhansel/programmer
Схемы
drive_pD8k28E85v.fzz
Производственный процесс
- Эволюция компьютерной мыши: дизайн, производство и будущие тренды
- Создание аудио-эффектов, управляемых датчиками, с набором Satellite CCRMA
- Сборка Bluetooth-робота-шпиона на Arduino с управлением с телефона
- FlickMote: жестовое IR-пульт для умного дома на Arduino 101 и ультразвуковом датчике
- DIY Arduino TV-B-Gone: универсальный пульт для выключения любого телевизора
- Создание пользовательских LED-часов с будильником — видно с 12 метров
- Find Me: Умный поисковик вещей на Arduino и Bluetooth
- Оптимизированные решения питания для проектов на Arduino
- Крестики-нолики на Arduino с матрицей MAX7219 и картонным корпусом
- Пошаговое руководство: сборка умного четырёхногого робота на Arduino