Лисья Нора

Оглавление


§ Вступление

Начну новую главу с того что расскажу о реализации 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, и посмотреть то, как работает итоговая схема.
Общая схема синтеза схемы.
Всё можно автоматизировать через серию команд в makefile или bat-файле, а при обновлении результатов просто пользоваться "горячей клавишей" CTRL+SHIFT+R в gtkwave.
Отладочная плата – неотъемлемый атрибут проверки схемы на работоспособность в реальных условиях. Но с ними есть определенные проблемы: все они разные. Существует просто впечатляющий спектр ПЛИС всех мастей, от совсем крошечных с пару десятков логических элементов до сверхбольших, достигающих миллионов вентилей. То как их программировать – совершенно отдельный разговор, которого я коснусь вскользь в последних главах, так что верификацию схем и правильности работы будем проводить только с помощью двух методов – при помощи синтеза и симуляции с последующей проверкой в gtkwave, либо через синтез в Си++ код с некоторой "обвязкой" в виде модулей видеоадаптера, клавиатуры и контроллера SD-карты.

§ Верхний модуль

Любой тестбенч (и не только он) начинается с описания верхнего модуля, который будет генерировать и связывать все субмодули в единую схему. Можно назвать верхний модуль (topmodule), неким аналогом материнской платы, где выложены микросхемы (модули), связанные друг с другом проводами.
Опишу то, что должно быть реализовано в top-модуле:
Достаточно много чего нужно. Да, тут есть одно интересное допущение, которое распространяется также и на физическую реализацию в ПЛИС: частота памяти и системная частота процессора. Разность в частоте памяти составляет 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
Прокомментирую происходящее в файле.
Ниже представлен результат симуляции данной схемы.
CLOCK_100CLOCK_25RST_N
Сигнал RST_N имеет большое значение для того чтобы сбросить процессор и назначить ему определенное начальное состояние регистров. Кстати говоря, при разработке процессора я все регистры выведу "наружу" – как те, которые нужно будет загрузить при сбросе процессора, так и все регистры общего назначения, сегментные регистры и даже специальные. Это важно для того чтобы позже, при использовании симулятора verilator возможно было посмотреть текущий статус процессора и его значения регистров.

§ Контроллер памяти

Первоначально, процессор в 16-битном режиме имеет доступ к памяти до 1Мб (и немного выше). Поскольку в этой части мы рассматриваем только 16-битный процессор, то максимально доступную память будем определять до ровно 1 мб (от 0 до FFFFFh), а при превышении – вообще никак не использовать старший, 21-й бит. Это значит, что будет задана строго 20-битная шина адреса, если можно так выразиться:
wire [19:0] A; // Адрес (Address)
reg [ 7:0] I; // Данные из памяти (In)
wire [ 7:0] O; // Данные в память (Out)
wire W; // Сигнал записи (Write)
Известно что любой процессор в качестве экономии пинов использует совмещенную шину данных (у нас она сейчас 8-битная), но в верилог-реализации для ПЛИС об этом можно совершенно не беспокоиться и потому данные, которые идут из памяти I и данные, которые идут от процессора в память O я разъединяю.
Помимо объявления самих проводов, необходимо каким-то образом создать массив байт, объединенных в ОЗУ. При помощи средств синтаксиса от system verilog такой метод существует:
reg [7:0] mem[1048576];
Эта конструкция объявляет массив из 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, поэтому получается такая ситуация:
Эти нюансы важно помнить при разработке в данной среде. Если бы память работала синхронно, то процессор бы, после того как записал что-то в память, позже ожидал бы 1 такт, чтобы память обновилась и выдала новые данные.
И, наконец, завершающий этап, синтез.
iverilog -g2005-sv -o main.qqq tb.v
vvp main.qqq >> /dev/null
gtkwave tb.vcd
Скачать исходный код