- PVSM.RU - https://www.pvsm.ru -

Я попытался оптимизировать свой самодельный CPU, минимизировав количество чипов логики, чтобы ответить на вопрос: какое минимальное число интегральных схем требуется для полного по Тьюрингу CPU без CPU?
Мой ответ: для создания 16-битного последовательного CPU нужно всего 8 интегральных схем, включая память и тактовый генератор. Он имеет 128 КБ SRAM, 768 КБ FLASH и его можно разгонять до 10 МГц. Он содержит только 1-битное АЛУ, однако большинство из его 52 команд работает с 16-битными значениями (последовательно). На своей максимальной скорости он исполняет примерно 12 тысяч команд в секунду (0,012 MIPS) и, среди прочего, способен выполнять потоковую передачу видео на ЖК-дисплей на основе PCD8544 (Nokia 5110) с частотой примерно 10 FPS.
Если выбрать подходящую классификацию разделения конечных автоматов и CPU, то моя 16-битная система может считаться CPU с наименьшим количеством интегральных схем. Другими претендентами на это звание могут быть 1-битный компьютер Джеффа Лофтона [1] с 1 командой и 1 битом памяти, а также простой CPU Дэниела Торнбурга [2] с 1 командой byte-byte-jump (копирует 1 байт из одного участка памяти в другой, а затем выполняет безусловный переход) и памятью, симулируемой на Raspberry PI.
Источником вдохновения для создания архитектуры стали другие проекты CPU наподобие JAM-1 Джеймса Шэрмана [3], SAP-1 Бена Итера [4], 4-bit Crazy Small CPU Уоррена [5], его 8-битная версия [6] и другие. Все они и многие другие подобные архитектуры используют «управляющие» EEPROM, EPROM или ROM для генерации управляющих компонентами CPU-сигналов, потому что это намного проще, чем генерировать их только логическими цепями, а также потому, что это обеспечивает гораздо большую гибкость на будущее. Я тоже решил использовать такую «управляющую» память, а конкретно EPROM. В отличие от упомянутых выше проектов я стремился к наименьшему количеству чипов, поэтому попытался «запихнуть» в память как можно больше обработки данных, чтобы снизить требования к другим компонентам CPU или, того лучше, полностью от них избавиться. Предпринятые мной основные шаги были следующими:
Но даже при таких существенных упрощениях всё равно требуется дополнительное оборудование. Однако всё можно собрать всего на 8 чипах в соответствии с показанной ниже схемой:

Схема построена на основе 128-килобитной EPROM M27C1001-15, работающей на 5 В, которая сочетает конечный автомат управления с 1-битным АЛУ. Её выходные линии защёлкиваются 74HC574 каждый период повторения тактовых импульсов и управляют двумя последовательными SRAM 23LCV512 на 64 КБ и одной последовательной FLASH W25Q80 на 1 МБ. Выходов недостаточно для управления каждой памятью по отдельности, поэтому они имеют общую шину данных, а также частично линию выбора чипа. Разделёнными остаются только линии синхронизирующих импульсов. Я не смог найти последовательную память FLASH на 5 В, поэтому резисторы R3, R4 и R5 ограничивают ток и образуют мост с 5 В на 3,3 В. Я не считаю регулятор напряжения MCP1703 на 3,3 В частью CPU (я учёл его, но только как часть источника питания), но если учитывать его, то CPU содержит 9 чипов.
Текущая команда хранится в буферизированном регистре сдвига 74HC595, линии управления которого также частично являются общими с чипами памяти. На выполнение каждой команды необходима пара тактов, так что прогресс выполнения команды отслеживается счётчиком «микрокода» 74HC393. После завершения команды линия «Counter_reset» выполняет сброс счётчика «микрокода» и начинает исполнение следующей команды, буферизированной в 74HC595.
74HC574 и счётчик «микрокода» 74HC393 используют противоположные фронты синхроимпульса, поэтому тактовый генератор 74HC14 передаёт на 74HC393 инвертированный сигнал синхронизации, чтобы они были синхронизованы.

Чего я не смог реализовать в своём CPU разумно — так это самопрограммирование памяти FLASH. Следовательно, bootloader невозможен, а загрузку новой программы в последовательную FLASH необходимо выполнять снаружи. Для этого я использовал микроконтроллер Attiny13, прослушивающий по UART последовательность команд, поэтому для загрузки нового кода достаточно любого адаптера USB-UART. При программировании он отключает выход 74HC574 через линию «Prog_en» и начинает напрямую программировать память FLASH. Микроконтроллер используется только для загрузки новой программы, и CPU замечательно работает без него.
Единственные доступные выходы — это два верхних бита регистра сдвига команд 74HC595. Я использовал одну из этих инвертированных линий для выбора чипа, что позволило CPU подключаться к устройствам наподобие SPI. Например, к нему можно напрямую подключить ЖК-дисплей SPI на основе PCD8544 напряжением 3,3 В (Nokia 5110), а второй старший бит команд используется как селектор данных/команд ЖК-дисплея. Также можно вместо ЖК-дисплея подключить дополнительный регистр сдвига 74HC595, чтобы получить классические линии цифрового вывода.
Единственные доступные входы — это два сигнала данных/входа памяти, подключённые к адресным шинам EPROM (A9, A11). Чипы последовательной памяти удерживают высокий импеданс этих сигналов, когда они не используются, чтобы их можно было сэмплировать как общие цифровые входы, когда чипы памяти находятся в состоянии простоя. Важно отметить, что входной сигнал не должен создавать помехи данным памяти, поэтому требуется высокое сопротивление между входным сигналом и входной шиной памяти (R6, R7). Примечание: чтение входного сигнала на шинах данных памяти работает только для тактовых частот до примерно 8 МГц. При более высоких частотах сэмплируемые данные становятся ошибочными и работа CPU может приостановиться.

Выше уже было видео о том, как мой CPU воспроизводит музыкальное видео «Bad Apple!!» на ЖК-дисплее PCD8544. В видео ниже я покажу возможность управления общими цифровыми выходами после добавления ещё одного 74HC595. Ту же схему можно использовать для создания 8-битной музыки с частотой до 4300 сэмплов/с, если вместо светодиодов бы использовалась резисторная матрица R-2R, и именно эту схему я использовал для создания саундтрека к видео «Bad Apple!!».
У CPU нет отдельных регистров, но есть две SRAM, из которых можно выполнять чтение и запись. Недостаток заключается в том, что каждый раз, когда CPU хочет получить доступ к данным, он должен выполнить запись в полный 16-битный адрес последовательной SRAM. Плюс заключается в том, что поскольку ему всё равно нужно записывать полный 16-битный адрес, CPU (и команды в целом) может иметь доступ ко всем 64 КБ SRAM с постоянным временем.
Я выбрал одну SRAM (U8/RAM1) для хранения данных программ, а все арифметические и логические операции должны выполняться со значениями внутри этой памяти. Вторая SRAM (U7/RAM2) должна использоваться для стека, поэтому считывать и изменять её содержимое могут лишь некоторые команды. Первые несколько байтов обоих чипов памяти зарезервированы под хранение внутреннего состояния CPU (счётчика команд, бита флага, указателя стека, промежуточного результата, исходного/конечного адресов и других используемых внутри значений). Приблизительная таблица распределения памяти:
| Адрес: | 0x0 | 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7 | 0x8 | 0x9 | 0xA | 0xB | 0xC | 0xD | 0x000E~0xFFFF |
| RAM1: | Флаг и ввод | Счётчик команд (PC) | Обратный счётчик команд | Указатель стека (SP) | Значение стека (SPVAL) | Регистры и пользовательские данные | |||||||||
| RAM2: | Flag | Счётчик команд (PC) | Конечный адрес | Результат команды | Стек и пользовательские данные | ||||||||||
Стоит также упомянуть о способе использования памяти FLASH в качестве второго входа АЛУ. Так как FLASH довольно велика (1 МБ), внутрь неё можно поместить полную 16-битную таблицу поиска, содержащую идентичные 16-битные значения. Имея эту таблицу поиска на 128 КБ, можно записывать 16-битное значение в FLASH как адрес и считывать те же 16-битные значения как данные, чтобы использовать их как вход АЛУ.
Небольшое неудобство в использовании последовательных чипов памяти заключается в том, что их адресация происходит в формате MSB-first, а 1-битное АЛУ выполняет вычисления в формате LSB-first. Чтобы адресация памяти работала, нам нужно обратить биты из формата LSB-first, с которым работает CPU, в формат MSB-first, с которым работают чипы памяти. Обращение битов при помощи 1-битного АЛУ — не такая простая задача, поэтому я зарезервировал ещё 128 КБ памяти FLASH под таблицу поиска «обращённых значений», чтобы ускорить операцию. Всё работает так же, как и предыдущая таблица — значение записывается в память FLASH как адрес, и в обращённом виде считывается как данные.
Именно из-за этих таблиц поиска у моего CPU всего 768 КБ памяти FLASH, а счётчик команд (PC) начинается с адреса 0x040000, а не с нуля.
Из-за слабого оборудования набор команд имеет определённые ограничения. CPU способен выполнять только 64 уникальных команд/операций, каждая из которых должна уместиться в 256 этапов микрокоманд и должна исполняться при помощи только 1-битного АЛУ и 1 бита флага. Но даже при наличии этих ограничений, как ни удивительно, можно создать вполне удобный набор команд:
| Опкод | Имя | Операнды | Разрядность | Флаг | Такты | Всего | Описание |
| 0x00 | INIT | - | - | сброс | 256 | 256 | Ожидание стабилизации синхросигнала, затем инициализация интегральных схем ОЗУ в последовательном режиме |
| 0x01 | RESET | - | - | сброс | 235 | 235 | Установка счётчика команд PC = 0x040000 и указателя стека SP = 0x000A |
| 0x02 | - | - | - | - | 158 | 414 | Теневая команда: получение |
| 0x03 | - | - | - | - | 256 | 414 | Теневая команда: продолжение получения |
| 0x04 | - | - | - | - | 129 | 129 | Теневая команда: инкремент счётчика команд PC = PC + 3 |
| 0x05 | - | - | - | - | 129 | 129 | Теневая команда: инкремент счётчика команд PC = PC + 5 |
| 0x06 | - | - | - | - | 129 | 129 | Теневая команда: инкремент счётчика команд PC = PC + 7 |
| 0x07 | - | - | - | - | 129 | 129 | Теневая команда: инкремент счётчика команд PC = PC + 8 |
| 0x08 | - | - | - | - | 162 | 291 | Теневая команда: копирование 32-битного результата |
| 0x09 | - | - | - | - | 130 | 259 | Теневая команда: копирование 16-битного результата |
| 0x0A | - | - | - | - | 113 | 113 | Теневая команда: копирование счётчика команд |
| 0x0B | - | - | - | - | 167 | 296 | Теневая команда: сохранение в ОЗУ косвенное |
| 0x0C | - | - | - | - | 151 | 280 | Теневая команда: сохранение в ОЗУ косвенное |
| 0x0D | - | - | - | - | 173 | 587 | Теневая команда: отправка арифметической команды |
| 0x0E | STF | - | - | установка | 132 | 546 | Установка FLAG |
| 0x0F | CLF | - | - | сброс | 132 | 546 | Сброс FLAG |
| 0x10 | NOP | - | - | - | 132 | 546 | Нет операции |
| 0x11 | MOV | addr16 <- addr16 | 16 | - | 231 | 774 | Передача 16-битного значения |
| 0x12 | MOVW | addr16 <- addr16 | 32 | - | 146 | 851 | Передача 32-битного значения |
| 0x13 | INC | addr16 <- addr16 | 16 | переполнение | 231 | 774 | Инкремент |
| 0x14 | DEC | addr16 <- addr16 | 16 | переполнение | 231 | 774 | Декремент |
| 0x15 | COM | addr16 <- addr16 | 16 | ноль | 231 | 774 | Обратный код (NOT) |
| 0x16 | NEG | addr16 <- addr16 | 16 | ноль | 231 | 774 | Дополнительный код |
| 0x17 | LSL | addr16 <- addr16 | 16 | переполнение | 233 | 776 | Сдвиг влево (<<) |
| 0x18 | LSR | addr16 <- addr16 | 16 | переполнение | 233 | 776 | Сдвиг вправо (>>) |
| 0x19 | ROL | addr16 <- addr16 | 16 | переполнение | 233 | 776 | Сдвиг влево с переносом |
| 0x1A | ROR | addr16 <- addr16 | 16 | переполнение | 255 | 798 | Сдвиг вправо с переносом |
| 0x1B | ASR | addr16 <- addr16 | 16 | переполнение | 235 | 778 | Арифметический сдвиг вправо (с сохранением бита знака) |
| 0x1C | REV | addr16 <- addr16 | 16 | - | 238 | 781 | Инвертирование бита |
| 0x1D | ADDI | addr16 <- addr16, val16 | 16 | переполнение | 231 | 774 | Непосредственное сложение |
| 0x1E | ADCI | addr16 <- addr16, val16 | 16 | переполнение | 231 | 774 | Непосредственное сложение с переносом |
| 0x1F | SUBI | addr16 <- addr16, val16 | 16 | переполнение | 231 | 774 | Непосредственное вычитание |
| 0x20 | SBCI | addr16 <- addr16, val16 | 16 | переполнение | 231 | 774 | Непосредственное вычитание с переносом |
| 0x21 | ANDI | addr16 <- addr16, val16 | 16 | ноль | 231 | 774 | Логическое AND с непосредственным значением |
| 0x22 | ORI | addr16 <- addr16, val16 | 16 | ноль | 231 | 774 | Логическое OR с непосредственным значением |
| 0x23 | XORI | addr16 <- addr16, val16 | 16 | ноль | 231 | 774 | Логическое XOR с непосредственным значением |
| 0x24 | ADD | addr16 <- addr16, addr16 | 16 | переполнение | 171 | 887 | Прибавление регистра |
| 0x25 | ADC | addr16 <- addr16, addr16 | 16 | переполнение | 171 | 887 | Прибавление регистра с переносом |
| 0x26 | SUB | addr16 <- addr16, addr16 | 16 | переполнение | 171 | 887 | Вычитание регистра |
| 0x27 | SBC | addr16 <- addr16, addr16 | 16 | переполнение | 171 | 887 | Вычитание регистра с переносом |
| 0x28 | AND | addr16 <- addr16, addr16 | 16 | ноль | 171 | 887 | Логическое AND с регистром |
| 0x29 | OR | addr16 <- addr16, addr16 | 16 | ноль | 171 | 887 | Логическое OR с регистром |
| 0x2A | XOR | addr16 <- addr16, addr16 | 16 | ноль | 171 | 887 | Логическое XOR с регистром |
| 0x2B | JMP | addr24 | - | - | 197 | 611 | Переход к адресу |
| 0x2C | CALL | addr24 | 32 | - | 221 | 748 | Копирование адреса следующей команды (PC + 4) и текущего FLAG в SPVAL, затем переход |
| 0x2D | RET | - | 32 | восстановление | 138 | 552 | Передача SPVAL в PC и FLAG (по сути, выполняет возврат из CALL и восстанавливает предыдущий FLAG) |
| 0x2E | BRFS | addr24 | - | - | 160 | 625|574 | Ветвление, если FLAG установлен |
| 0x2F | BRFC | addr24 | - | - | 160 | 625|574 | Ветвление, если FLAG сброшен |
| 0x30 | BREQ | addr16, addr24 | 16 | - | 243 | 708|657 | Ветвление, если регистр равен нулю |
| 0x31 | BRNE | addr16, addr24 | 16 | - | 243 | 708|657 | Ветвление, если регистр не равен нулю |
| 0x32 | LDI | addr16 <- value16 | 16 | - | 81 | 624 | Загрузка 16-битного непосредственного значения |
| 0x33 | LDIW | addr16 <- value32 | 32 | - | 113 | 656 | Загрузка 32-битного непосредственного значения |
| 0x34 | LD | addr16 <- [addr16] | 16 | - | 238 | 911 | Косвенная загрузка 16 битов из адреса |
| 0x35 | LDB | addr16 <- [addr16] | 8 | - | 238 | 911 | Косвенная загрузка 8 битов из адреса, верхним 8 битам присваивается 0 |
| 0x36 | ST | [addr16] <- addr16 | 16 | - | 163 | 873 | Косвенное сохранение 16 битов по адресу |
| 0x37 | STB | [addr16] <- addr16 | 8 | - | 163 | 857 | Косвенное сохранение 8 битов по адресу |
| 0x38 | LD2W | [addr16] | 32 | - | 256 | 799 | Косвенная загрузка 32 битов из адреса в RAM2 в регистр SPVAL |
| 0x39 | LD2 | [addr16] | 16 | - | 224 | 767 | Косвенная загрузка 16 битов из адреса в RAM2 в регистр SPVAL |
| 0x3A | ST2W | [addr16] | 32 | - | 256 | 799 | Косвенное сохранение 32 битов из регистра SPVAL в адрес RAM2 |
| 0x3B | ST2 | [addr16] | 16 | - | 224 | 767 | Косвенное сохранение 16 битов из регистра SPVAL в адрес RAM2 |
| 0x3C | LPM | addr16 <- [addr16] | 16 | - | 211 | 884 | Косвенная загрузка 16 битов из адреса FLASH |
| 0x3D | LPB | addr16 <- [addr16] | 8 | - | 211 | 884 | Косвенная загрузка 8 битов из адреса FLASH, верхним 8 битам присваивается 0 |
| 0x3E | OUT | addr16 | 8 | - | 252 | 795 | Вывод 8 битов по SPI |
| 0x3F | HALT | - | - | clear | 14 | 428 | Остановка исполнения |
Первые команды (INIT и RESET) исполняются при включении питания или при нажатии кнопки RESET. «Теневые» команды недоступны для пользователя и в основном используются для повторяющихся операций, например, получения команды, инкремента счётчика команд, записи обратно результата и так далее.
Арифметические и логические операции используют один бит флага как флаг переноса/переполнения, или как флаг нуля. Как говорилось выше, при доступе к полному пространству адресов скорость не снижается, так что во всех этих командах можно указывают любой исходный/конечный адрес в пределах пространства адресов SRAM (64 КБ). Косвенная адресация для арифметических операций не поддерживается напрямую, а должна выполняться командами LD/ST (загрузки/сохранения).
Второй набор команд LD2/ST2 получает доступ ко второй SRAM. Она должна использоваться для стека, но в ней могут храниться любые данные. Команды PUSH м POP не реализованы, но их можно собрать из команд LD2/ST2 и INC/DEC.
В среднем исполнение команды занимает примерно 800 тактов с учётом операции получения и с инкрементом счётчика команд. При максимальной тактовой частоте (10 МГц) CPU может исполнять примерно 12 тысяч команд в секунду.
Для генерации двоичных файлов из исходного ассемблерного кода я использую customasm Лоренци [8]. Двоичные файлы можно загружать при помощи небольшого приложения на python3 в программирующий микроконтроллер Attiny13, который записывает двоичный файл во FLASH.
Ниже приведены два примера небольших процедур, написанных на ассемблере для моего CPU. Первая процедура возвращает 32-битный результат перемножения двух 16-битных значений. Вторая выводит на ЖК-дисплей ascii-строку, хранящуюся внутри памяти FLASH.
| Multiply32_16x16 | LCD_WriteStrF |
; Возвращает FA32 = FA16 * FB16
; Ожидается, что FB - меньшее из чисел
Multiply32_16x16:
;PUSH_PC ; Необязательно
LDIW FC, 0 ; Сброс результата
LDI FA+2, 0 ; Преобразование FA16 в FA32
.loop:
ANDI TMP, FB, 1
BRFS .skip_add
ADD FC, FA ; Сложение FC32 += FA32
ADC FC+2, FA+2 ; Сложение FC32 += FA32
.skip_add:
LSL FA ; Сдвиг FA32 << 1
ROL FA+2 ; Сдвиг FA32 << 1
LSR FB ; Сдвиг FB16 >> 1
BRNE FB, .loop
MOVW FA, FC ; Копируем результат
;POP_PC ; Необязательно
RET
|
; Записываем строку во Flash
; input: FA32 <- Адрес строки во Flash
LCD_WriteStrF:
PUSH_PC ; Сохраняем адрес возврата
PUSHW RA ; Сохраняем RA 32 бит
MOVW RA, FA
.loop:
LPB FA, RA ; Загружаем символ из Flash
BREQ FA, .stop ; Проверяем символ "\0"
REV FA ; MSB-first -> LSB-first
ANDI FA, FA+1, 0xFF ; Преобразование в 8 бит
CALL LCD_WriteChar ; Записываем символ
ADDI RA, 1 ; Увеличиваем 32-битный указатель
ADCI RA+2, 0 ; Увеличиваем 32-битный указатель
JMP .loop
.stop:
POPW RA ; Восстанавливаем RA 32 бит
POP_PC ; Восстанавливаем адрес возврата
RET
|
Согласно спецификациям, суммарная задержка распространения по критическому пути равна:
Если соединить всё вместе, то можно прийти к выводу, что схема должна работать только на частоте примерно 4,6 МГц. Однако конкретно моя сборка может без проблем работать на частотах до 10 МГц и становиться нестабильной только при частотах выше примерно 10,5 МГц. Я считаю, что это довольно впечатляющий результат для схемы на макетной плате со множеством паразитных ёмкостей. Максимальную тактовую частоту можно даже увеличить, если использовать более быстрый двоичный счётчик или EPROM.
Я очень доволен получившимся CPU. Он имеет удобный и простой в работе набор команд со всеми базовыми командами. Он достаточно мощный, чтобы передавать видео на небольшой ЖК-дисплей, воспроизводить аудио (благодаря использованию внешней «звуковой карты»), и в целом выполняет простые вычислительные операции ввода-вывода, для которых и предназначался. В конечном итоге, он успешно демонстрирует, что на небольшом количестве интегральных схем можно изготовить функциональный самодельный CPU.
Однако в него можно внести и небольшие улучшения:
Мне придётся подумать, стоит ли реализовывать эти улучшения. Если вам понравился проект и вы хотите изучить его глубже, то просмотрите исходный код, выложенный здесь [9]. Он содержит симулятор, генератор микрокода EPROM, прошивку Attiny13 для программирования и весь мой ассемблерный код.
Я реализовал минималистичный движок проецирования каркасных 3D-объектов с использованием 16-битной арифметики с фиксированной запятой. Умножение матриц на моём CPU мощностью 0,012 MIPS выполняется довольно медленно, поэтому вряд ли в ближайшее время стоит ожидать 3D-игр:
Также я постепенно расширяю список оборудования, напрямую поддерживаемого моим CPU. Я добавил алфавитно-цифровой ЖК-дисплей SPI, извлечённый из старого принтера HP:

И мне удалось выполнить bit-banging последовательного интерфейса таймера реального времени DS1302. Для создания необходимых сигналов программному обеспечению требуется использовать особые последовательности команд, но это возможно и не требует дополнительного оборудования.

Теперь CPU поддерживает драйвер LCD PCF8833, хотя для рендеринга одного кадра требуется примерно 96 секунд.

Рекомендую вам изучить другие потрясающие архитектуры CPU [10] от Уоррена.
Автор:
ru_vds
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/cpu/386910
Ссылки в тексте:
[1] 1-битный компьютер Джеффа Лофтона: https://laughtonelectronics.com/Arcana/One-bit%20computer/One-bit%20computer.html
[2] простой CPU Дэниела Торнбурга: https://mysterymath.github.io/simple_cpu/
[3] JAM-1 Джеймса Шэрмана: https://www.youtube.com/watch?v=3iHag4k4yEg&list=PLFhc0MFC8MiCDOh3cGFji3qQfXziB9yOw
[4] SAP-1 Бена Итера: https://eater.net/8bit/
[5] 4-bit Crazy Small CPU Уоррена: https://minnie.tuhs.org/Programs/CrazySmallCPU/
[6] его 8-битная версия: https://github.com/DoctorWkt/CSCvon8/
[7] 1-битных вычислений: https://en.wikipedia.org/wiki/1-bit_computing
[8] customasm Лоренци: https://github.com/hlorenzi/customasm
[9] здесь: https://www.jiristepanovsky.cz/projects/23cpu/cpu.zip
[10] другие потрясающие архитектуры CPU: https://www.homebrewcpuring.org/
[11] Источник: https://habr.com/ru/companies/ruvds/articles/757854/?utm_source=habrahabr&utm_medium=rss&utm_campaign=757854
Нажмите здесь для печати.