Относительно недавно (немногим больше полугода), в университете было получено задание - написать модель простейшего процессора на не синтезируемом подмножестве Verilog, но в связи с личной неприязнью к этому языку, модель была написана на Haskell (в связи с не нулевой вероятностью, что меня попросят переписать, писалась по тактовая модель, что бы можно было скриптами сгенерировать часть кода на Verilog). По скольку тестировать модель как-то надо, а писать в маш кодах довольно уныло, модель писалась вместе с компилятором асм подобного языка, как следствие, в описаниях машинных команд есть фрагменты, необходимые для компилятора.
Описание процессора:
Сплошной линией обозначены линии данных. Пунктиром обозначены управляющие сигналы.
Основные характеристики:
Разрядность процессора: 8 бит.
Организация памяти: гарвардская, с отдельными блоками памяти для команд и данных.
Подсистема обработки прерываний и команды вызова подпрограмм отсутствует.
Внешние устройства (для данной работы это светодиоды, двухпозиционные переключатели и тактовые кнопки) отображаются в адресное пространство данных. Работа с ними выполняется по опросу.
Команды выполняются за 2 или 3 такта (в зависимости от типа команды): 1) Выборка команды 2) Выборка операндов 3) Выполнение команды.
Регистры:
PC — счетчик команд
IR — регистр инструкций
AR — регистр адреса операнда
C — флаг переноса/заёма
Z — флаг нуля
Описание системы команд можно найти в файле Cmd.hs, функция cmd. Функциональность описана в комментариях с использование C синтаксиса. pmem и dmem - память программ и данных соответственно. arg - аргумент команды.
Модель.
Модель создавалась с учётом того, что бы в случае необходимости её можно было относительно легко перетащить в Verilog, а значит вариант с наивным case по командам отпадал вроде такого:
command 0x01 state = someFunctiom1 state
command 0x02 state = someFunctiom2 state
или в без точечном стиле (в основном будет использовать именно он):
command 0x01 = someFunctiom1
command 0x02 = someFunctiom2
Не подходит, так как в данном случае пришлось бы переписывать весь код полностью. Разбираясь с принципами работы процесса, довольно быстро становится очевидно, что наиболее простой способ описания работы команд - потактово задать функции, определяющие изменения состояния процессора после каждого из них. Функции на каждый такт можно получить при помощи композиции нескольких простейших функций, которые были бы в реальном процессоре представлены управляющими сигналами (назовём их микрокомандами, хотя ими они и не являются). К примеру, первый такт программы представляет из себя выборку команд, который может быть описан через композицию сигнала для инкриминирования регистра PC и чтения команды в регистр IR. В моделе это выглядит так:
nextCommand = incPC . pmem2IR
К сожалению, модель вычислений языка Haskell довольно сильно отличается от модели SDF, которая присутствует в Verilog (или отчасти присутствует), и мы вынуждены следить за соблюдением правильной последовательности операций при композиции функций. Использую этот подход, вся система команд может быть описана в одной таблице следующим образом:
cmd = [(0x01, Nop, NoneArg, [nop]), -- nope
(0x00, Hlt, NoneArg, [hlt]), -- halt model
(0x02, Ldm, DmemArg, [incPC . pmem2AR, dmem2acc]), -- acc = dmem[arg]; c = const; z = (dmem[arg] == 0)
(0x03, Ldi, ValueArg, [incPC . pmem2acc]), -- acc = arg; c = const; z = (arg == 0)
...
В данном случае важен только первый (код команды) и последний (поведение) элемент кортежа (остальные необходимы для компилятора). Поведение команды задаётся списком функций, каждая из которых выполняется за такт. Значительная часть функций задаётся композицией микрокоманд которые имеют тип (State -> State).
cmd = [(0x01, Nop, NoneArg, [nop]), -- nope
(0x00, Hlt, NoneArg, [hlt]), -- halt model
(0x02, Ldm, DmemArg, [incPC . pmem2AR, dmem2acc]), -- acc = dmem[arg]; c = const; z = (dmem[arg] == 0)
(0x03, Ldi, ValueArg, [incPC . pmem2acc]), -- acc = arg; c = const; z = (arg == 0)
...
В данном случае важен только первый (код команды) и последний (поведение) элемент кортежа (остальные необходимы для компилятора). Поведение команды задаётся списком функций, каждая из которых выполняется за такт. Значительная часть функций задаётся композицией микрокоманд которые имеют тип (State -> State).
Таким образом, описание микропроцессора сводится к определению:
- типа данных State - хранит состояние процессора в текущий момент;
- таблицы cmd, в которой описываются все команды;
- микрокоманды, путём композиции которых получаются собственно сами команды;
Само моделирование производится следующим небольшим кусочком кода:
step (State {on = False}) = return ()
step state = step $ executeCommand state' commandFlow
where
state'@(State{ir = hexCommand}) = nextCommand state
commandFlow = commandFlowFromInfo $ getCommandInfo hexCommand
executeCommand state [] = state
executeCommand state (f:fs) = executeCommand (f state) fs
В нём происходит следующее, производится выборка команды (PC инкриминируется), из таблицы cmd выбираются команды, список функций команд по очереди применяется к текущему состоянию и далее по рекурсии.
Явные недочёты:
- Внешние устройства "зашиты" в структуру модели. По хорошему их необходимо вынести из модели, и обрабатывать в step. (тут же использование Trace, для функциональности).
- Использование неправильных структур данных (в частности для cmd).
- В модуле отвечающем за моделирование захардкодено всё, что только можно.
В ближайшее время будет описан разработанный в параллель компилятор, тогда же и будет полный код и продемонстрирована его работоспособность.
Cmd.hs - http://gist.github.com/492325
SimpleRiscModel - http://gist.github.com/492327
executeCommand = foldl (flip id)
ReplyDeleteили по-русски foldl (\x f -> f x)