Оглавление


§ Объявление пинов модуля

Разговор пойдет о том, какие мне потребуются по итогу входящие и исходящие пины. Набор всегда стандартный:
1module KR580VM80ALite
2(
3    input               clock,    // Тактовая частота. У Радио86 частота обычно 2.2 Мгц
4    input               reset_n,  // Сигнал сброса, при 0 сбрасывает процессор
5    input               ce,       // Если =1, то значит, что процессор может выполнять такты
6    output      [15:0]  address,  // Адрес в памяти
7    input       [ 7:0]  in,       // Входящие данные от памяти
8    output reg  [ 7:0]  out,      // Исходящие данные к памяти (я их тут разделил)
9    output reg          we        // Если =1 то происходит запись в память
10);
11endmodule
На самом деле, в процессорах используется "шинная" технология, с помощью которой in и out порты объединяются в один, в зависимости от того, какой установлен WE, с той стороны и приходят данные. Я не привык так делать и поэтому в FPGA-схемах постоянно разделяю единый пин DATA на две части, вход и выход.

§ Регистры

У процессора не так много регистров. Их по сути, всего лишь 8: B,C,D,E,H,L,A и регистр флагов, который тут называется PSW. Он объединяет 8-битный регистр A с регистром флагов, который тоже 8 битный. Не все биты используются в регистре флагов.
7 N  "NEG" Индикатор отрицательного результата, по сути просто копируется 7-й бит
6 Z  "ZERO" Результат операции дал 0
5 U5
4 H  "HALF-CARRY" Полуперенос из разряда 3
3 U3
2 P  "PARITY"
1 U1
0 C  "CARRY" бит переноса
В данном байте состояния бит U1 всегда равен 1, а биты U3, U5 всегда равны 0. Итак, в коде модуля добавляются следующие регистры:
1reg [15:0] pc;             // Program Counter
2reg [15:0] bc, de, hl, sp; // Регистры 16-бит
3reg [ 7:0] a, psw;         // Аккумулятор, регистр статуса PSW (Program Status Word)
Обращаю внимание на то, что я объявил регистры не как B,C,D и т.д., а в виде регистровых пар BC, DE, HL и SP. Это связано больше с тем, чтобы было удобнее с ними далее работать. Ну, по крайней мере, мне. Что есть регистровая пара? По сути это сдвоенный регистр. Например, регистровая пара DE содержит в старшем байте регистр D, а в младшем E, то есть, связаны следующим соотношением:
BC = B*256 + C
DE = D*256 + E
HL = H*256 + L
Регистры A и PSW объявлены как отдельные. Регистр же SP не делится на отдельные регистры, к нему можно получить доступ только как к 16-битному регистру, и возможности записать в старший байт или в младший отдельными инструкциями нет. Да и нигде такого нет, я не видел вообще ни в одном процессоре 80-й серии, и даже 86-й.

§ Объявление управляющих регистров

Помимо регистров, которые содержат какие-то промежуточные данные вычислений, существуют еще несколько регистров, которые управляют поведением процессора в определенный момент времени. Часть из них я приведу сейчас.
1reg         sw;     // =1 Адрес указывает на CP, иначе =0 PC
2reg [15:0]  cp;     // Адрес для считывания данных из памяти
3reg [ 7:0]  opcode; // Сохраненный опкод
4reg [ 4:0]  t;      // Исполняемый такт опкода [0..31]
Первый из регистра управления, sw, показывает, в какую точку памяти в данный момент смотрит сейчас процессор. Это могут быть только два варианта, когда sw=0, то это указатель PC, либо же указатель CP, если sw=1:
1assign address = sw ? cp : pc;
В регистре opcode же находится код операции текущей выполняемой инструкции, а регистр t — это счетчик тактов внутри одного опкода. Новый опкод всегда записывается тогда, когда t = 0, то есть можно считать, что это состояние является признаком записи опкода из памяти.
Каждая инструкция не выполняется одномоментно, она проходит серию микро-операции, к тому же, инструкция не может быть короче чем 4 такта. Это значит, что любая, даже самая простая инструкция всегда содержит в себе 4Т (такта), а то и больше, иногда количество тактов доходит до 19 за одну инструкцию.

§ Исполняемая логика

Мой процессор, который я сейчас разрабатываю в статьях, при запуске ПЛИС всегда будет сбрасывать управляющие регистры через пин reset_n=0:
1always @(posedge clock)
2if (reset_n == 0) begin
3    t  <= 0;        // Установить чтение кода на начало
4    sw <= 0;        // Позиционировать память к PC
5    pc <= 16'hF800; // Указатель на программу "Монитор"
6end
7else if (ce) begin
8    // Выполнение инструкции
9end
В блоке always код выполняется тогда, когда clock переходит из состояния 0 к состоянию 1, что отмечено в списке чувствительности блока как posedge clock. Внутри этого блока, в зависимости от условия, будет записаны новые значения регистров. К примеру, если reset_n = 0, то в этом случае одновременно будут записаны новые значения в регистр t=0, sw=0 и pc=F800h. Сверх этого ничего выполняться не будет в данном участке кода.
В случае, если сигнал сброса процессора установлен в 1, то далее проверяется условие CE=1, что означает активацию тактов процессора извне. Это сделано на тот случай, если внешнему устройству понадобилось остановить такты процессора, к примеру потому что внешнее устройство "арендует" доступ к памяти или память недоступна из-за промаха кеша. Ситуации могут быть разные, потому я и реализовал подобный механизм.

§ Запись опкода

Пришло время для того, чтобы зафиксировать опкод для начала исполнения инструкции. Поэтому внутри блока always, там где выполняется инструкция, можно написать следующий код:
1if (t == 0) begin opcode <= in; pc <= pcn; end
В опкод будет запись только тогда, когда t=0, то есть, начало инструкции. Помимо самой записи опкода в регистр, увеличивается также регистр PC на единицу, поскольку вне блока always объявлен следующий простой сумматор:
1wire [15:0] pcn = pc + 1;
Так что, защелкивая опкод, он уже не потеряется далее.

§ Встраиваем в testbench

После написания простого кода, хочется проверить на предмет того, как он работает, поэтому переходим в файл tb.v и встраиваем объявление свеженаписанной микросхемы.
1wire [15:0] address;   // Объявление "провода" 16-битного адреса
2wire [ 7:0] in;
3wire [ 7:0] out;
4wire        we;        // Сигнал записи
5
6KR580VM80ALite CORE
7(
8    .clock      (clock_25),
9    .reset_n    (reset_n),
10    .ce         (1'b1),
11    .address    (address),
12    .in         (in),
13    .out        (out),
14    .we         (we)
15);
Чтобы icarus знал, откуда брать информацию, надо ему объявить об этом, дописав в makefile название файла, в котором находится модуль KR580VM80ALite:
1ica:
2	iverilog -g2005-sv -DICARUS=1 -o tb.qqq tb.v kr580vm80a.v
3	vvp tb.qqq >> /dev/null

§ Простой контроллер памяти

Несмотря на то, что было успешно подключен сам процессор, он никак не соединен с памятью и не будет выполнять никаких инструкции, кроме разве что NOP (код 00), если запустить его. Синтезатор Quartus вообще даже не синтезирует процессор, он его проигнорирует, поскольку посчитает что зачем синтезировать код, который все равно нигде не используется. Нужно подключить память.
Чтобы это сделать, я воспользуюсь возможностями SystemVerilog и подключу массив регистров memory (8 бит) на 64Кб памяти.
1reg  [ 7:0] memory[65536];
2wire [15:0] address;
3wire [ 7:0] in = memory[address];
4wire [ 7:0] out;
5wire        we;
6
7initial $readmemh("tb.hex", memory, 16'hF800);
8always @(posedge clock_100) if (we) memory[address] <= out;
Как можно заметить, я видоизменил in = memory[address]. Вместо простого провода, который ни к чему не подключен, теперь он будет смотреть каждый раз в нужную ячейку памяти в зависимости от адреса. В реальной схеме это примерно так же, за исключением небольшой задержки в 5 ns, которая необходима для изменения выходящих из блочной памяти (block memory) данных.
Для загрузки дампа памяти в регистры используется оператор $readmemh("tb.hex", memory, 16'hF800);, который загружает контент файла tb.hex (он в 16-ричных кодах) в память memory, начиная с адреса F800, где находится программа "МОНИТОР".
И последним штрихом в этом коде является запись в память по сигналу we. При высоком уровне сигнала =1 в память по адресу address записывается значение out. Естественно то, что когда сигнал we выставляется, значение записанных данных доступно только на следующем такте, то есть, сразу же пользоваться на том же такте этими данными невозможно. Но это и правильно.

§ Отладка

Пусть в файле tb.hex будет следующий текст:
01
55
AA
Который по своей сути означает инструкцию LXI B, 0AA55h. Надо проверить пока что только то, защелкивается ли опкод 01 при первом такте и как сбрасывается процессор при первом запуске. Вначале, компилируем схему через make, после запускаем через make vcd и открывается окно gtkwave с не проставленными пинами.
Clipboard01.png
Щелкнув tb, а потом CORE, выберем все пины, кроме pcn, ce — они не нужны, а потом необходимо нажать Append, чтобы добавить выбранные провода и регистры в окно справа.
Clipboard02.png Clipboard03.png
Отсортируем пины определенным образом, чтобы они шли по смыслу, а не по алфавиту. Также пины можно группировать. Для этого необходимо выделить те (кликая по названиям с зажатой CTRL), которые требует сгруппировать и нажать G, вписать имя группы.
Нажав CTRL+V, можно сохранить симуляцию на будущее, чтобы не добавлять пины постоянно каждый раз. После того же, как icarus снова компилирует схему, в окне отладчика необходимо нажать сочетание клавиш CTRL+SHIFT+R, чтобы обновить данные.
Рассмотрим теперь сигналы.
Clipboard04.png
1. Как видно на графике, на позитивном фронте clock при выключенном reset_n=0, в регистр pc записалось значение F800, в sw=0 и в t=0, что автоматически установило address на pc и на проводе in появилось значение 01. Поэтому сброс процессора был успешен.
2. На следующем такте сработало все как надо, записалось в opcode значение, которое находилось в in, а также pc=pc+1, увеличился до F801, что, соответственно, привело к тому, что на входе in появилось следующее значение, а именно 55. Также сработал автосчетчик микрооперандов t=t+1.
Поскольку процессор пока что ничего не умеет, и обработка инструкции никак не реализована, то на каждом цикле, когда t переполнится и станет равным 0, будет повторяться пункт 2 до бесконечности.
В следующих главах будет рассказано о том, как начать реально исполнять инструкции.
Приложение, файлы проекта