Оглавление
§ Вступление
Начну новую главу с того что расскажу о реализации 16-битного процессора 8088, который считывает инструкции из памяти байт за байтом. Несмотря на то что я пишу цикл статей о 386, то крайне важно знать и понимать то, как устроены сами по себе базовые инструкции перед тем, как переходить к более сложным.
Процессоры от x86 архитектуры ранее всегда состояли из микрокода, где в CROM (ПЗУ внутри процессора) были встроены команды исполнения микрооперации, а в моем случае вся логика основана на FSM (Finite State Machine) или синтезируемая логика конечных состоянии машины в RTL-реализации, где RTL означает что мы работаем на уровне регистровой схемы DFF (Data Flip-Flop), то есть, все промежуточные состояния процессора находятся в особых регистрах его состояний, помимо обычных регистров таких как AX или, допустим, ES.
Реализация процессора будет в минимальном 16-битном режиме. После того как он будет создан полностью, мы сможем перейти к разработке 32-битных инструкции от 386.
§ Выбор инструментов
Конечно же, мой выбор падает на проверенный временем инструмент для синтеза icarus verilog, который доступен как для windows (в том числе даже для windows xp), так и для linux. С помощью этой программы из исходного кода на верилоге получается промежуточный файл для симуляции, и с помощью программы vvp эту симуляцию можно запустить и получить новый файл с waveform, который уже можно прочитать с помощью программы gtkwave, и посмотреть то, как работает итоговая схема.
Общая схема синтеза схемы.
- Пишется верхний модуль
testbench
- К нему присоединяются необходимые модули (процессор, контроллеры и прочее)
- Запускается синтез программой
iverilog
- Запускается симулятор при помощи программы
vvp
- Открывается результат работы через
gtkwave
Всё можно автоматизировать через серию команд в makefile или bat-файле, а при обновлении результатов просто пользоваться "горячей клавишей" CTRL+SHIFT+R в gtkwave.
Отладочная плата – неотъемлемый атрибут проверки схемы на работоспособность в реальных условиях. Но с ними есть определенные проблемы: все они разные. Существует просто впечатляющий спектр ПЛИС всех мастей, от совсем крошечных с пару десятков логических элементов до сверхбольших, достигающих миллионов вентилей. То как их программировать – совершенно отдельный разговор, которого я коснусь вскользь в последних главах, так что верификацию схем и правильности работы будем проводить только с помощью двух методов – при помощи синтеза и симуляции с последующей проверкой в gtkwave, либо через синтез в Си++ код с некоторой "обвязкой" в виде модулей видеоадаптера, клавиатуры и контроллера SD-карты.
§ Верхний модуль
Любой тестбенч (и не только он) начинается с описания верхнего модуля, который будет генерировать и связывать все субмодули в единую схему. Можно назвать верхний модуль (topmodule), неким аналогом материнской платы, где выложены микросхемы (модули), связанные друг с другом проводами.
Опишу то, что должно быть реализовано в top-модуле:
- Тактовый генератор (25 мгц и 100 мгц)
- Объявление памяти, основанной на регистрах (для этого придется прибегнуть к
system verilog) и загрузки туда содержания ПЗУ
- Мини-контроллера памяти – как раз 100 мгц тут потребуется для того чтобы работающий на частоте 25 мгц процессор успевал читать и писать в память
- Подключение самого процессора и, возможно, некоторой периферии
- Для тестовых целей – подключение симуляции вызова внешних прерываний или контроллера прерываний
Достаточно много чего нужно. Да, тут есть одно интересное допущение, которое распространяется также и на физическую реализацию в ПЛИС: частота памяти и системная частота процессора. Разность в частоте памяти составляет 25 и 100 мгц, что гораздо выше возможности процессора для обработки из-за внутрисхемных задержек, которые там присутствуют.
Обычно так не делают. В целом можно сказать что в современных процессорах частота памяти никогда не бывает выше частоты процессора, и как минимум, эти две частоты совпадают, образуя системную частоту. В процессорах есть кеши разного уровня – от L0 (сверхоперативная память) до L3 (большой, но достаточно медленный кеш). Ясное дело, что кеши не настолько медленные, как сама по себе оперативная память, которая не может работать на частоте ядра никаким образом из-за особенностей конструкции такой памяти.
В нашем случае, процессор работает гораздо медленнее внутрисхемной памяти и это, конечно, проблема, но, опять-таки, в учебных целях не стоит думать о том, чтобы прямо сейчас подключать медленную память, обвязывая всё вокруг кешами и оптимизируя всё подряд. Конечно же, процессор будет работать медленно за счёт еще и того что читается из памяти всё по одному байту, а не группой, как это делал даже 16-битный 8086.
Приведу пример самого минимального тестбенча.
`timescale 10ns / 1ns
module tb;
reg clock_25 = 0, clock_100 = 1, rst_n = 0;
always #0.5 clock_100 = ~clock_100;
always #2.0 clock_25 = ~clock_25;
initial begin #4.0 rst_n = 1; #100 $finish; end
initial begin $dumpfile("tb.vcd"); $dumpvars(0, tb); end
endmodule
Прокомментирую происходящее в файле.
- Директива
`timescale 10ns / 1ns устанавливает "метрику", которая указывает что единица времени принимается равной 10 нс, а минимальняа дробная часть – 1 нс. Это значит что минимально возможная длительность сигнала равна 1 нс и не меньше.
- Задаем 3 регистра с их первоначальными значениями, управляющих частотой 25, 100 мгц и сигналом "сброс"
rst_n
- Конструкция
always #0.5: Через каждые 0.5 единиц времени (что равно 10 x 0.5 = 5 нс) регистр clock_100 перебрасывается с 0 на 1, и с 1 на 0, что имитирует тактовую частоту 100 Мгц
- Аналогично работает конструкция
always #2.0, имитируя частоту 25 Мгц (в 4 раза медленнее)
- В блоках
initial begin ... end через #4.0 указывается что спустя 4.0 единиц времени (40 нс) устанавливается rst_n=1 вместо rst_n=0
- Директива
$dumpfile выгружает результаты симуляции в файл tb.vcd, а директива $dumpvars указывает, откуда именно будут выгружены результаты – а именно из модуля tb.
Ниже представлен результат симуляции данной схемы.
Сигнал RST_N имеет большое значение для того чтобы сбросить процессор и назначить ему определенное начальное состояние регистров. Кстати говоря, при разработке процессора я все регистры выведу "наружу" – как те, которые нужно будет загрузить при сбросе процессора, так и все регистры общего назначения, сегментные регистры и даже специальные. Это важно для того чтобы позже, при использовании симулятора verilator возможно было посмотреть текущий статус процессора и его значения регистров.
§ Контроллер памяти
Первоначально, процессор в 16-битном режиме имеет доступ к памяти до 1Мб (и немного выше). Поскольку в этой части мы рассматриваем только 16-битный процессор, то максимально доступную память будем определять до ровно 1 мб (от 0 до FFFFFh), а при превышении – вообще никак не использовать старший, 21-й бит. Это значит, что будет задана строго 20-битная шина адреса, если можно так выразиться:
wire [19:0] A;
reg [ 7:0] I;
wire [ 7:0] O;
wire W;
Известно что любой процессор в качестве экономии пинов использует совмещенную шину данных (у нас она сейчас 8-битная), но в верилог-реализации для ПЛИС об этом можно совершенно не беспокоиться и потому данные, которые идут из памяти I и данные, которые идут от процессора в память O я разъединяю.
Помимо объявления самих проводов, необходимо каким-то образом создать массив байт, объединенных в ОЗУ. При помощи средств синтаксиса от system verilog такой метод существует:
Эта конструкция объявляет массив из 8-битных регистров на 1 мегабайт не инициализированных элементов (1024 x 1024). Огромный размер, и, как однажды мудро выразился Билл Гейтс:
640 килобайт хватит на всё
Он был в этом прав. Только современные калькуляторы не то что бы не влезут в этот объем... им нужно мегабайт так 20 для запуска. Для наших немногочисленных задач пока этого объема будет вполне и даже более чем достаточно.
Одно дело просто объявить регистры, необходимо заполнить их данными и для этой цели существует удобная директива для icarus verilog:
initial begin $readmemh("tb.hex", mem, 20'h00000); end
Она указывает синтезатору схем что необходимо загрузить файл tb.hex, который представляет из себя построчную последовательность 16-разрядных чисел (байт) в регистры mem, начиная с индекса20'h00000 (проще говоря, 0).
На самом деле, остался лишь последний штрих.
always @(posedge clock_100)
begin
I <= mem[A];
if (W) mem[A] <= O;
end
Я ранее упоминал о том, что память работает на скоростях выше чем процессор, в 4 раза. Это значит что при каждом новом такте будет забираться из памяти mem по адресу A новое значение I, при этом, если что-то (значение O) процессором будет записываться W=1 в память, то через один такт 100 мгц это значение уже будет доступно в I, поэтому получается такая ситуация:
- Процессор ставит адрес
A, сигнал W=1 и данные O
- Контроллер выдает сначала предыдущее значение из памяти на провод
I, при этом записывая новое значение
- На следующем такте выдает уже новое значение, которое записано от процессора
- На следующем процессорном такте он получает обновленное значение
I из памяти, и процессор выключает запись W=0 (если это нужно)
Эти нюансы важно помнить при разработке в данной среде. Если бы память работала синхронно, то процессор бы, после того как записал что-то в память, позже ожидал бы 1 такт, чтобы память обновилась и выдала новые данные.
И, наконец, завершающий этап, синтез.
iverilog -g2005-sv -o main.qqq tb.v
vvp main.qqq >> /dev/null
gtkwave tb.vcd
- Первая строка, собственно, синтезирует схему, указывая
-g2005-sv использование system verilog конструкции, синтез происходит в файл -o main.qqq с верхнего модуля tb.v.
- Вторая строка запускает симулятор
vvp, используя файл main.qqq, и, согласно коду тестбенча, создается файл tb.vcd с вейвформами
- И третья строка запускает просмотр сигналов через утилиту
gtkwave