Arduino Nano - эмулятор компьютера под управлением ОС CP/M

В далекие 90-е годы у меня был "бытовой" (как он тогда назывался) компьютер "Байт" - брестский клон популярнейшего в те времена ZX Spectrum 48K. Но на нем невозможно было запустить операционную систему CP/M, бывшую де-факто стандартом для восьмибитных компьютеров (в середине 1980-х годов эта ОС запускалась на свыше 300 моделях микрокомпьютеров). И вот я решил восполнить это упущение и создать свой компьютер под управлением ОС CP/M 2.2 на базе ... Arduino Nano 3.0 (с микроконтроллером ATmega328)!

Вот как выглядит первый quick-and-dirty :-) прототип моего устройства:
cpm4nano Mk I
эмулятор 8080 CP/M

Во втором варианте прототипа я использовал адаптер для подключения microSD-карточки:
cpm4nano Mk II
эмулятор Intel 8080 cpm4nano

В процессе его создания я решил две задачи - создал эмулятор процессора Intel 8080, оперативной памяти и дисковой системы и адаптировал к созданному устройству ОС CP/M.

GitHub
Исходный код
(распространяется по лицензии GPL 3.0) - https://github.com/Dreamy16101976/cpm4nano

Эмулятор процессора i8080

Я создал эмулятор для Arduino Nano, способный воспроизводить все команды процессора i8080, включая команду DAA :-)
При этом под i8080 я понимаю  все семейство  X8080Y, не имевшее программных отличий.

Тесты эмулятора
Эмулятор проходит проверку с помощью теста MICROCOSM ASSOCIATES 8080/8085 CPU DIAGNOSTIC VERSION 1.0 (C) 1980 ("Kelly Smith Test"):
MCTEST

От ввода команды MCTEST до выдачи сообщения об успешном окончании теста проходит время около 18 секунд.

Также мой эмулятор процессора 8080 успешно прошел предварительный тест 8080PRE.COM от Ian Bartholomew:
8080PRE.COM

В тесте DIAGNOSTICS II V1.2 - CPU TEST проходятся все основные тесты, но затем выдается ошибка:
DIAGNOSTICS II V1.2

Эмуляция оперативной памяти

Главная проблема в этом проекте - нехватка оперативной памяти, которой в ATmega328 только два килобайта! Для эмуляции оперативной памяти размером несколько десятков Кбайт я использую четырехгигабайтную SDHC-карточку, подключенную к Arduino Nano через самодельный переходник.
кэширование в эмуляторе

Для "общения" с карточкой используется шина SPI:
MISO - прием данных от карточки (D12)
MOSI - передача данных карточке (D11)
SCK - тактовый сигнал для карточки (D13)
SS - выбор устройства (D10)
3V3 - питание 3,3 В
GND - "земля"

ICSP-разъем Arduino Nano:
ICSP Arduino

В первом варианте моего устройства карточка вставляется через адаптер в разъем кабеля для подключения пятидюймового флоппи-дисковода, а адаптер соединяется с Arduino Nano через резистивный делитель напряжения:
подключение SD карточки к  Arduino

Во втором варианте я подключил microSD-карточку посредством специального адаптера:
адаптер для SD-карточки

Для ускорения работы с эмулируемой памятью я использую кэш в ОЗУ Arduino Nano, состоящий из шести линий, каждая размером 64 байта.
Для хранения содержимого оперативной памяти я выделил на карточке 2048 секторов от 0x000400 до 0x000BFF (в начале каждого 512-байтного сектора карточки размещаются байты, соответствующие одной линии кэша). В конце этой цепочки байт записывается байт LRC-контрольной суммы (выполняется операция "Исключающее ИЛИ" над всеми байтами цепочки), который считывается и проверяется при чтении данных с карточки:
LRC контрольная сумма

В качестве алгоритма кэширования я использую LRU (Least recently used). Использованные прежде данные вытесняются выше, к началу кэша - к линии 0, а самая нижняя линия кэша заполняется при промахе новыми данными. При попадании в кэш используемые данные смещаются ниже, обмениваясь позицией с находящимися там данными.

Тестирование быстродействия эмулируемой памяти

По результатам тестов время случайного доступа к эмулируемой картой оперативной памяти составило около 7 миллисекунд (при половинной скорости работы карты) и около 6 миллисекунд (при полной скорости).

Для генерации случайного адреса памяти в таком тесте я использую XOR-алгоритм генерации псевдослучайных чисел в диапазоне 0...65535 с начальным значением (seed) 0xABCD:

rnd ^= rnd << 2
rnd ^= rnd >> 7
rnd ^= rnd << 7

Из-за применения кэширования среднее время доступа при последовательном доступе к памяти составляет около 80 микросекунд.

Я использовал тест на Бейсике (в интерпретаторе TINYBASIC) для оценки влияния кэша на быстродействие в реальной задаче:
BASIC тест быстродействия
(размер линии кэша составляет 64 байта)

кол-во линий кэша время выполнения теста (секунды)
2 345
4 155
6 80
8 60

Тестирование памяти

Популярным способом проверки памяти является запись/чтение в ячейки памяти/из ячеек памяти сначала байтов 0x55 (01010101), а затем байтов 0xAA (10101010), либо байтов 0xA5 (10100101)  и 0x5A (01011010).
В статье "Ram Tests" (http://www.ganssle.com/articles/ramtest.htm) советуется записывать последовательно в ячейки памяти и проверять при чтении случайную последовательность из, например, 257 байт. Для ускорения теста можно писать/читать, например, каждую третью или пятую последовательность.
Я использую этот метод с длиной последовательности 33 - на единицу больше размера линии кэша - из байтов:

0x3D 0x55 0x5F 0x15 0x23    0x47 0x1C 0x31 0x48 0x60  
0x35 0x11 0x4F 0x2F 0x2E    0x14 0x20 0x5B 0x39 0x26  
0x09 0x61 0x34 0x30 0x50    0x2B 0x4B 0x0F 0x63 0x1F   0x10 0x1E 0x36

(случайная последовательность сгенерирована с помощью сервиса RANDOM.ORG).

Сначала я выполняю проход теста с исходной последовательностью, а затем - с инвертированной.

Я выполнил 7400 проходов теста памяти (с помощью команды монитора "M") - ошибок обнаружено не было:
тест памяти

Также успешно было пройден тест памяти UMPIRE.COM:
тест памяти UMPIRE

При тестировании карт памяти  Cromemco 16KZ RAM использовались следующие тесты:
1) Peak - смещает "единицу" в поле нолей
2) Valley - смещает "ноль" в поле единиц
3) Delay - проверка долговременного (в течение 6 секунд) хранения данных в памяти
4) MI - проверяет возможность чтения карты во время машинного цикла MI
5) Banks -  определяет, в какие банки отображается карта

Переключение банков памяти

Я планирую сделать эмуляцию устройства управления памятью (Memory Management Unit - MMU), осуществляющего переключение банков памяти.

Cromemco 16KZ RAM
Подобная технология была использована в картах памяти Cromemco 16KZ RAM  для микрокомпьютерной шины S-100 (она была использована в микрокомьютерах Cromemco, Altair 8800, Imsai 8080). 2 DIP-переключателя (Block Select) управляли битами A14, A15 и позволяли разместить каждую карту в одном из 4 блоков 64 КБайт адресного пространства. Специальное средство Bank Select позволяло организовать из этих карт 8 банков по 64 КБайт в каждой. 8 DIP-переключателей на каждой карте позволяли разместить каждую карту в одном из восьми банков, а сами банки памяти выбирались программно - 8  битов порта вывода 0x40 управляли включением ("1")/выключением ("0") соответствующего банка (после сброса активен банк 0).

MEM8 Plus
При использовании карт памяти MEM8 Plus (для шины S-100) выделялась общая область размером MMU Common Size (8, 16, 24 или 32 КБайт), при этом возможны были два режима работы MMU:
Normal Cromemco style bank select - единичный бит (использовались 4 младших бита) в регистре Bank Select Register (BSR), доступном через порт ввода-вывода Bank Select Port, выбирал соответствующий банк из четырех (например, 0x04 выбирал банк 3), причем после сброса до выбора банка процессором все биты нулевые;
Encoded bank select - для выбора одного из 16 банков использовалась комбинация 4 младших битов).

CP/M 3
В CP/M 3 (CP/M Plus) и MP/M часть адресного пространства (верхняя) является общей для всех банков (Common Memory, resident RAM), а нижняя - разделяется между банками (Bank):
DR рекомендует выделять общую память размером 16 КБайт и несколько банков размером 48 КБайт:

 .........................  040000
|                         |
|        Bank 4           |
|         48K             |
|                         |
|_________________________| 034000
|                         |
|                         |
|        Bank 3           |
|         48K             |
|                         |
|_________________________| 028000
|                         |
|                         |
|        Bank 2           |
|         48K             |
|                         |
|_________________________| 01C000
|                         |
|                         |
|        Bank 1           |
|         48K             |
|                         |
|_________________________| 010000
|                         |
|     Common Memory       |
|         16K             |
|_________________________| 00C000
|                         |
|                         |
|        Bank 0           |
|         48K             |
|                         |
|_________________________| 000000
 

Моя реализация переключения банков памяти
Я разделил адресное пространство 0x0000...0xFFFF на 16 блоков (с номерами от 0 до 15) размером по 4 КБайта. Каждый блок может быть отображен в один из 8 банков (размер банка - 64 КБайта). Соответствие банков блокам определяется значениями (номерами банков от 0 до 7) в наборе из 16 регистров (при запуске эмулятора активен только банк 0).
Для задания номера банка для определенного блока используются две команды вывода в порт:

OUT D0, номер_блока
OUT D1, номер_банка

Также для переключения блока памяти X на банк памяти Y можно использовать команду монитора YXY.

Для определения текущего банка для определенного блока используется команда:

IN D0, номер_блока

При запуске эмулятора возможно выполнить тестирование всех восьми банков памяти:
тестирование банков памяти

Использование в качестве памяти микросхем FRAM

Я планирую применить в качестве оперативной памяти (как альтернативу эмуляции с помошью SD-карточки) две микросхемы FRAM FM24C256-G:
FM24C256-G
Для удобства применения таких микросхем используются SOIC-DIP адаптеры:
SOIC-DIP адаптер

Эмуляция консоли

Сейчас для связи Arduino Nano с компьютером через последовательный порт (для ввода-вывода команд и данных) я использую преобразователь USB-Serial и терминальную программу TerraTermTerraTerm версии 4.94 (https://osdn.net/projects/ttssh2/releases/).
Для последовательного порта я отключил буферы FIFO и управление потоком.
В дальнейшем я планирую подключить к Arduino Nano PS/2-клавиатуру для ввода и экран для отображения информации.

Я сделал эмуляцию коммуникационных портов в соответствии с архитектурой компьютера "Altair 8800" , созданного Micro Instrumentation Telemetry Systems (MITS) :

основной последовательный порт Arduino отображается одновременно на порты двух плат:
плата SIO-A (88SIO) (чип COM2502):
порт 0x00 (0): порт статуса (TTS) (R - бит 5 = 1 (0x20) - есть символы для ввода, бит 1 = 1 (0x02) - терминал готов к выводу символа)
порт 0x01 (1): порт данных  (TTI/TTO) (R/W)

первый порт платы 2SIO (88-2SIO) (чип EF6850 - 8-битный асинхронный адаптер коммуникационного интерфейса):
порт 0x10 (16): порт статуса (TTS) (R - бит 0 = 1 (0x01) - есть символы для ввода, бит 1 = 1 (0x02) - терминал готов к выводу символа)
порт 0x11 (17): порт данных  (TTI/TTO) (R/W)

Эмуляция дисководов

Я эмулирую поведение дисковода гибких дисков на уровне обращений чтения/записи к портам.

Порты контроллера дисковода/DMA:
порт 0xE0: порт статуса (R)/ команды (W)
порт 0xE1: порт  дорожки (R/W)
порт 0xE2: порт сектора (R/W)
порт 0xE3: порт выбора диска (R/W)
порт 0xE4: порт младшего байта адреса DMA-буфера (W)
порт 0xE5: порт старшего байта адреса DMA-буфера (W)

Для эмуляции дисководов гибких дисков (на дискете 77 дорожек с 26 128-байтными секторами на каждой = 2002 (0x7D2) сектора = 256256 байт = 250,25 Кбайт) я  использую ту же SDHC-карточку, выделив в ней области, начиная с сектора  0x0001000, для эмуляции 100 дискет (128-байтный сектор эмулируемой дискеты размещен в начале 512-байтного сектора карточки, для каждой дискеты отводится 0x1000 секторов).
С помощью команды Z монитора я могу "вставить" дискету в любой из четырех дисководов A, B, C или D:
(например, команда ZB02 монтирует дискету с номером 02 в дисковод B)
эмуляция дисководов

Номера "вставленных" дискет запоминаются в EEPROM Arduino и восстанавливаются при повторном запуске эмулятора.
Дискету в дисководе можно отформатировать (заполнить байтами 0xE5) с помощью команды X монитора (например, XA - форматирование дискеты в дисководе A).

Список дискет, записанных мной:

0 - системный диск
1 - тестовые программы
2 - BASIC
3 - ADA
...

Файлы могут быть скопирован с одного диска на другой с помощью утилиты PIP:
например, PIP B:=A:TEST.COM копирует файл TEST.COM с диска A на диск B.

Эмуляция видеоадаптера

Я планирую сделать эмуляцию видеоадаптера TV Dazzler, разработанного компанией Cromemco в 1976 году:
TV Dazzler

В этом адаптере в качестве видеопамяти испоьзуется фрагмент системной оперативной памяти:
TV Dazzler карта памяти
Для связи с адаптером используются порты:

порт 0x10 (16) (W):
бит 7 - включение/выключение; биты 6-0 - биты 15...9 адреса видеобуфера в ОЗУ
порт 0x11 (17) (W):
бит 7 - не используется
бит 6 - разрешение X4 (1) - цвет и интенсивность задаются битами 4...0 - включение/выключение точки на экране определяется битом в видеопамяти (64x64 для 512 байт или 128x128 для 2 Кбайт):
TV Dazzler
          обычное разрешение (0) - 32x32 для 512 байт, 64x64 для 2 Кбайт - цвета и интенсивность задаются 4-хбитовыми словами в видеопамяти (2 точки кодируются одним байтом):
TV Dazzler
бит 5 - 2 Кбайт (1)/512 байт (0) видеопамяти; бит 4 - цветная (1)/черно-белая (1) картинка
бит 3 - повышенная (1)/нормальная (0) интенсивность цвета для черно-белого режима
бит 2 - синий (1); бит 1 - зеленый (1)
; бит 0 - красный (1)
порт 0x10 (16) (R):
бит 7 - включен (1)/выключен (0)
; бит 6 - конец кадра (0)

Адаптация операционной системы CP/M

Операционная система CP/M ("Control Program for Microcomputers") версии 2.2 (однопользовательская и однозадачная) была выпущена компанией  Digital Research Inc. (DRI) под руководством Гарри Килдалла (Gary Kildall) и его жены  Дороти МакЭвен (Dorothy McEwen) в 1979 году.
Гарри Килдалл
Гарри Килдалл (Gary Kildall)Руководство по ОС CP/M версии 2.2 (317 страниц!) можно загрузить здесь - http://www.cpm.z80.de/manuals/cpm22-m.pdf .

Система CP/M должна настраиваться под размер оперативной памяти конкретного компьютера - этот процесс называется "CP/M Alteration".

Для адаптации операционной системы под актуальный размер памяти я создал проект на Go getcpm (проект доступен на GitHub - https://github.com/Dreamy16101976/getcpm).

Для компиляции программы на Go в исполняемый файл getcpm.exe я использую команду:

go build getcpm.go

При запуске программы getcpm.exe требуется указать размер памяти в килобайтах (XX) и желаемый серийный номер системы CP/M (6 байтов в 16-ричном виде, YYYYYYYYYYYY), после выполнения настройки создается файл CPMXXK.SYS и указывается его восьмибитная контрольная сумма:
настройка CP/M

Программа, используя файл CPMDIFF.SYS, корректирует адреса в файле CPM00K.SYS, выполняя настройку на заданный объем оперативной памяти:
настройка CP/M

Размер получаемого файла, содержащего CCP и BDOS операционной системы CP/M версии 2.2, равен 5632 байта.

Для записи операционной системы CP/M на SD-карточку я использую утилиту dd:

dd bs=512 count=11 if=CPM64K.SYS of=\\?\Device\Harddisk1\DR1 seek=256

\\?\Device\Harddisk1\DR1 - идентификатор диска, соответствующий SD-карточке (для определения можно использовать команду dd --list).

CCP и BDOS располагаются на SDHC-карточке, начиная с сектора 256, откуда их  загружает написанный мной начальный загрузчик.

Я выбрал вариант с 64 Кбайтами ОЗУ (64K system).

Карта памяти такой системы:

0xFFFF
BIOS - базовая система ввода-вывода
0xFA00
BDOS - базовая дисковая операционная система
0xEC06
CCP - интерпретатор командной строки (командный процессор)
0xE400
TPA (Transient Program Area) -
область программ пользователя
0x0100
cлужебная область операционной системы
0x0000

Я сделал перехват функций BIOS (при обращении по соответствующим адресам памяти) и их реализацию:
(смещения указаны относительно адреса 0xFA00)

Смещение Функция
0x00 BOOT - "холодная" перезагрузка
0x03 WBOOT - "горячая" перезагрузка
0x06 CONST - получение состояния консоли
0x09 CONIN - ввод из консоли
0x0C CONOUT - вывод в консоль
0x0F LIST
(не реализовано)
0x12 PUNCH
(не реализовано)
0x15 READER
(не реализовано)
0x18 HOME - переход в начало диска
0x1B SELDSK - выбор диска
0x1E SETTRK - выбор дорожки
0x21 SETSEC - выбор сектора
0x24 SETDMA - установка адреса DMA-буфера
0x27 READ - чтение сектора
0x2A WRITE - запись сектора
0x2D LISTST - получение состояния устройства вывода листинга
(не реализовано)
0x30 SECTRAN - трансляция логических (с 0)/физических (с 1) секторов

Для ввода/вывода информации в CP/M используются такие логические устройства:
CONSOLE - консоль;
LIST - устройство для вывода листинга (обычно принтер);
READER - устройство ввода c перфоленты;
PUNCH - устройство вывода на перфоленту.

Эксплуатация эмулятора

Карта памяти SD-карточки (сектора (512 байт)):

0x100 ... 0x10A  - CCP & BDOS (загружаемая ОС CP/M) (11 секторов)
0x1000 ... 0x64FFF - данные эмулируемых дискет (100, на дискету выделено 0x1000 секторов) (128- байтный сектор дискеты в одном секторе)
0x70000 ...  0x71FFF - банки (8) памяти (1 линия кэша (64 байта) в одном секторе)
--- 228 МБайт

После включения Arduino Nano происходит проверка оперативной памяти:
i8080 тест памяти

Затем запускается монитор и отображается его приглашение к вводу команд ">".
Я реализовал для монитора следующие команды:
DXXXX - отображение байта памяти по адресу XXXX (hex)
LXXXXYY - запись байта YY (hex) в память по адресу XXXX (hex)
IXX - чтение байта из порта XX (hex)
OXXYY - запись байта YY (hex) в порт XX (hex)
W - включение/выключение режима отладки
G - запуск программы с адреса XXXX (hex)
QXXXX - установка точки останова (breakpoint) на адрес XXXX (hex)
F
- загрузка двоичной информации из файла формата Intel HEX
TXXXX - загрузка текстового файла в память с адреса XXXX
BXXXX - загрузка двоичного файла в память с адреса XXXX
ZXYY - "вставка" дискеты с номером YY в дисковод X
XX - форматирование диска X (X = A,B,C,D) - область на карточке, отведенная для диска, заполняется байтами 0xE5
C - загрузка ОС CP/M
R - перезагрузка Arduino
M
- тест памяти
YXY - переключение блока памяти X на банк памяти Y
V
- вывод информации о текущем состоянии эмулятора

При вводе команды "C" происходит загрузка операционной системы с SD-карточки (длится около 9 секунд):
загрузка CP/M
32K SYSTEM - размер памяти, используемый системой, в килобайтах
CBASE - адрес начала CCP
FBASE - адрес начала BDOS
BIOS - адреса процедур BIOS
После загрузки отображается приглашение операционной системы "A>".

В ОС CP/M встроено несколько команд:
DIR - вывод списка файлов
REN - переименование файла
ERA - удаление файлов
SAVE - сохранение области памяти в файл
TYPE - вывод содержимого текстового файла
USER - задание текущего номера пользовательской области

Пример результата выполнения команды DIR:
CP/M команда DIR
При нажатии клавиш CTRL + / при запросе ввода с консоли происходит возврат из ОС CP/M в монитор.

Загрузка файлов в эмулятор

Для загрузки двоичного файла я использую команду монитора Bxxxx , где xxxx - начальный адрес в памяти для загрузки файла (в 16-ричной форме), а затем закрываю терминальную программу и использую программу bin2nano.go:

bin2nano com file.ext

(com - номер COM-порта, file.ext - имя и расширение загружаемого файла).

Для загрузки в память данных из файла формата Intel HEX я использую команду монитора F, а затем закрываю терминальную программу и использую программу hex2nano.go:

hex2nano com file.ext

(com - номер COM-порта, file.ext - имя и расширение загружаемого файла)

Затем я запускаю терминальную программу и загружаю ОС CP/M командой монитора C и записываю содержимое памяти, начиная с адреса 0x100 на диск в файл FILE.COM командой
SAVE n FILE.COM , где n = [ размер файла FILE.COM / 256 ] + 1.

Программы hex2nano.go и bin2nano.go можно скачать в моем репозитарии на Github: https://github.com/Dreamy16101976/cpm4nano
Для компиляции программы в исполнимый exe-файл следует выполнить команду:

go build file.go

Системный диск

Я записал на эмулируемый флоппи-диск стандартные системные программы:
ASM.COM - ассемблер
DDT.COM - отладчик
DUMP.COM - вывод файла в 16-ричном виде
ED.COM - текстовый редактор
LOAD.COM - создание COM-файла из HEX-файла
PIP.COM - копирование файлов
STAT.COM - вывод состояния диска
SUBMIT.COM - выполнение SUB-файла
XSUB.COM - выполнение расширенной программы SUBmit

Также я разместил на диске интерпретатор бейсика TINYBAS.COM и тестовые программы TEST.COM, 8080PRE.COM, 8080EX1.COM.

Ссылка на скачивание zip-архива с образом этого диска:
https://acdc.foxylab.com/sites/default/files/DISK_1.ZIP

Для записи образа диска на карточку как дискеты в дисководе A: необходимо использовать утилиту dd:

dd bs=512 count=2002 if=DISK_1.BIN of=\\?\Device\Harddisk1\DR1  seek=4096
dd запись

Идентификатор SD-карточки \\?\Device\Harddisk1\DR1 можно посмотреть в выводе команды

dd --list
dd список

Программирование в эмуляторе

Для программирования в ОС CP/M доступно множество интерпретаторов и компиляторов.

BASIC
Я запустил интерпретатор бейсика TinyBASIC (SHERRY BROTHERS TINY BASIC VER. 3.1) и выполнение простейшую программу:
TinyBASIC CP/M

Ada
Я записал на эмулируемую дискету компилятор языка программирования Ada версии 2.10 от компаний Supersoft Inc. и Maranatha Software Systems.
Архив с компилятором можно скачать здесь - acdc.foxylab.com/ada.zip .
Компиляцию программы TOWERS.ADA решения задачи о "Ханойской башне" я выполнил командой:

ADA TOWERS
компилятор Ada

В процессе компиляции создаются временные файлы TOWERS.INT и TOWERS.OPT и результирующий файл TOWERS.COM.

Затем я запустил скомпилированную программу TOWERS.COM на выполнение командой

TOWERS

и получил решение задачи для трех колец:
задача о Ханойской башне

В качестве теста производительности я использовал программу PRIMES.ADA.
Для выполнения программы на эмуляторе потребовалось около 8 часов (!), при этом было найдено 1899 простых чисел:
PRIMES.ADA

YouTube
Видео работы проекта в реальном времени:
https://youtu.be/LHFmt3qWAuY

Планы дальнейшего развития:

  • подключение FRAM  в качестве ОЗУ
  • подключение PS/2 клавиатуры
  • подключение телевизора или LCD-экрана
Яндекс.Метрика