Самодельный 16-битный ЦПУ в 2023 году: проектирование


Для создания самодельного CPU требуется большое количество чипов логики. И в самом деле разумно, что для реализации регистров, счётчика команд, АЛУ и других компонентов CPU на логике TTL или CMOS действительно необходимо существенное число чипов. Но сколько конкретно?

Я попытался оптимизировать свой самодельный 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 битом памяти, а также простой CPU Дэниела Торнбурга с 1 командой byte-byte-jump (копирует 1 байт из одного участка памяти в другой, а затем выполняет безусловный переход) и памятью, симулируемой на Raspberry PI.

▍ Оборудование

Источником вдохновения для создания архитектуры стали другие проекты CPU наподобие JAM-1 Джеймса Шэрмана, SAP-1 Бена Итера, 4-bit Crazy Small CPU Уоррена, его 8-битная версия и другие. Все они и многие другие подобные архитектуры используют «управляющие» EEPROM, EPROM или ROM для генерации управляющих компонентами CPU-сигналов, потому что это намного проще, чем генерировать их только логическими цепями, а также потому, что это обеспечивает гораздо большую гибкость на будущее. Я тоже решил использовать такую «управляющую» память, а конкретно EPROM. В отличие от упомянутых выше проектов я стремился к наименьшему количеству чипов, поэтому попытался «запихнуть» в память как можно больше обработки данных, чтобы снизить требования к другим компонентам CPU или, того лучше, полностью от них избавиться. Предпринятые мной основные шаги были следующими:

  • Я полностью избавился от АЛУ и реализовал его как таблицу поиска. Так как большинство EPROM имеет всего лишь 8-битный выход, а системе также нужны другие управляющие сигналы, то разрядность данных АЛУ необходимо было существенно ограничить. Но не нужно волноваться, её можно уменьшить вплоть до одного бита: на самом деле, нам достаточно 1-битных вычислений.
  • Чтобы иметь возможность выполнения любых значимых вычислений, результаты работы 1-битного АЛУ должны сериализироваться. Это идеально подходит для использования последовательной SRAM, которая также обеспечивает другие преимущества. Во-первых, она избавляет от необходимости в регистрах, так как все операции с АЛУ могут выполняться напрямую с данными в SRAM. Во-вторых, последовательные SRAM также имеют последовательную адресацию, поэтому нам не нужно защёлкивать исходный и конечный адреса. В-третьих, произвольную разрядность обработки данных можно получить простым выбором периода повторения тактовых импульсов SRAM. Я выбрал 16 битов (16 периодов повторения тактовых импульсов SRAM на 1 операцию АЛУ) как приемлемый компромисс между удобством и скоростью.
  • Требуется как минимум два чипа последовательной SRAM, один из них должен предоставить сериализованный вход для нашего 1-битного АЛУ, а второй в то же время должен сохранять результат.
  • Для операций АЛУ с двумя операндами (например, ADD/AND/XOR…) необходимы два сериализованных входа. Разумеется, можно добавить и третью SRAM (2 для входов АЛУ, 1 для результата), но есть решение получше. Если вместо SRAM использовать последовательную память FLASH, то преимущества сохранятся (уже сериализованные данные, сериализованный адрес), но FLASH можно использовать для хранения команд/программы, а также для обеспечения ввода АЛУ.
  • Необязательно добавлять оборудование для счётчика команд, потому что в SRAM и так уже есть достаточно места для хранения его значения.

Но даже при таких существенных упрощениях всё равно требуется дополнительное оборудование. Однако всё можно собрать всего на 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 Лоренци. Двоичные файлы можно загружать при помощи небольшого приложения на 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

▍ Максимальная частота и критический путь

Согласно спецификациям, суммарная задержка распространения по критическому пути равна:

  • 12 нс в 74HC14 от «Clock_pos» до «Clock_neg»,
  • 54 нс в 74HC393 на пульсацию до последнего 8-го бита (12+3×5+12+3×5 нс),
  • Время доступа 150 нс к EPROM M27C1001-15,
  • 2 нс в 74HC574 на стабилизацию входов до фронта синхроимпульса.

Если соединить всё вместе, то можно прийти к выводу, что схема должна работать только на частоте примерно 4,6 МГц. Однако конкретно моя сборка может без проблем работать на частотах до 10 МГц и становиться нестабильной только при частотах выше примерно 10,5 МГц. Я считаю, что это довольно впечатляющий результат для схемы на макетной плате со множеством паразитных ёмкостей. Максимальную тактовую частоту можно даже увеличить, если использовать более быстрый двоичный счётчик или EPROM.

▍ Заключение и ретроспектива

Я очень доволен получившимся CPU. Он имеет удобный и простой в работе набор команд со всеми базовыми командами. Он достаточно мощный, чтобы передавать видео на небольшой ЖК-дисплей, воспроизводить аудио (благодаря использованию внешней «звуковой карты»), и в целом выполняет простые вычислительные операции ввода-вывода, для которых и предназначался. В конечном итоге, он успешно демонстрирует, что на небольшом количестве интегральных схем можно изготовить функциональный самодельный CPU.

Однако в него можно внести и небольшие улучшения:

  • Счётчик числа колебаний 74HC393 — существенное узкое место на критическом пути. Замена его быстродействующим сумматором (carry-lookahead adder) или счётчиком с буферизацией наподобие 74HC590 увеличит максимальную тактовую частоту.
  • То же самое относится к EPROM M27C1001-15. Использование более быстрой памяти, например, EPROM M27C1001-35 или FLASH SST39SF020A-70 тоже позволит увеличить тактовую частоту.
  • Более крупная EPROM с более чем семнадцатью шинами адреса может использоваться или для увеличения количества команд, или для применения дополнительных шин адреса в качестве цифровых входов общего назначения.
  • Добавление команд для стирания и программирования внутренней памяти FLASH позволило бы создать bootloader, а значить, и избавиться от схемы программирования на Attiny13.
  • Система может исполнять код только из памяти FLASH. Можно создать эмулятор внутри FLASH, чтобы он исполнял код из SRAM, но чтобы CPU мог исполнять код из SRAM нативно, потребовался бы другой процесс получения команд, возможно, с использованием дублирующего набора команд для самого исполнения в SRAM.

Мне придётся подумать, стоит ли реализовывать эти улучшения. Если вам понравился проект и вы хотите изучить его глубже, то просмотрите исходный код, выложенный здесь. Он содержит симулятор, генератор микрокода EPROM, прошивку Attiny13 для программирования и весь мой ассемблерный код.

▍ Дополнение 1

Я реализовал минималистичный движок проецирования каркасных 3D-объектов с использованием 16-битной арифметики с фиксированной запятой. Умножение матриц на моём CPU мощностью 0,012 MIPS выполняется довольно медленно, поэтому вряд ли в ближайшее время стоит ожидать 3D-игр:

Также я постепенно расширяю список оборудования, напрямую поддерживаемого моим CPU. Я добавил алфавитно-цифровой ЖК-дисплей SPI, извлечённый из старого принтера HP:


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

▍ Дополнение 2

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

▍ Веб-кольцо самодельных CPU

Рекомендую вам изучить другие потрясающие архитектуры CPU от Уоррена.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх 🕹️


 

Источник

16битный, году, проектирование, самодельный, ЦПУ

Читайте также