Оглавление
§ Назначение пинов
В предыдущей главе я описал то, как именно подключить первый тестбенч и запустить его, посмотреть тактовую генерацию, а в этой главе мы разберём основные моменты того, как создать модуль процессора, пока что безо всякой функциональности, а также как связать его с тестбенчем, запустить и проверить, что всё работает корректно.
Начнем с первых шагов. Для начала, требуется создать файл cpu.v, где и будет в одном файле расписана вся логика работы процессорного модуля. На самом же деле, процессор занимает примерно 1500 строк, что сравнительно немного.
module cpu
(
input clock,
input rst_n,
input ce,
output m0
);
endmodule
Это, конечно, далеко не всё. В модуле объявляется пин (провод) на вход, на котором будет периодически перебрасываться тактовая частота 25 миллионов тактов в секунду. Не так много, но и не мало. Первые процессоры Intel вообще работали на частотах около 4 Мгц и ниже.
Второй пин rst_n предназначен для сброса процессора в изначальное состояние. Когда он равен 0, это значит, что процессор на следующем системном такте должен сбросить регистры до заданных извне значений. В реальных процессорах осуществляется сброс только нескольких регистров – CS/EIP и некоторых системных, но в нашем случае при сбросе будут заранее устанавливаться некоторые начальные значения всех регистров вообще, которые используются в самом процессоре. Это сделано исключительно в отладочных целях с возможностью полного контроля за состоянием внутренних регистров в любой момент времени.
Третий пин ce предназначен для того чтобы на время останавливать процессор, когда это нужно. В большинстве случаев, этот пин равен 1, что означает "разрешить процессору выполнять что-либо", но если некоторой периферии или контроллеру памяти потребуется дочитать или задержать исполнение процессора, то тогда извне на этот пин подается значение 0, и пока это значение там, процессор ничего не делает, не исполняет инструкции и ничего не обрабатывает.
И последний (исходящий) пин m0 устанавливается процессором в 1 на этапе первого такта, когда происходит начало чтения инструкции. Этот сигнал показывает тот момент, что процессор закончил исполнение предыдущей инструкции и начал исполнение новой. Он предназначен для реализации отладчика, который должен точно знать, где именно процессор читает инструкцию, чтобы потом выдать либо на экран, либо в файловый лог. Опять-таки, этот пин нужен для отладки, а в реальном исполнении его можно даже ни к чему не подключать по итогу.
Добавим основные пины для извлечения и записи данных в памяти.
output [19:0] a,
input [ 7:0] i,
output reg [ 7:0] o,
output reg w,
Как видно, здесь названия пинов в нижнем регистре, я так сделал намеренно чтобы не мелькали лишний раз. Верхний регистр обычно ассоциируется с определенными константами, чем с регистрами или переменными.
Некоторые пины обозначены на вход или выход. В основном, на выход. Замечу еще одну деталь: у нас есть пин w, который отвечает за запись в память, но нет пина r, который бы отвечал за чтение. Всё дело в том что процессор практически 100% времени занимается только чтением из памяти.
§ Подключаем регистры
Самое, пожалуй, многочисленное множество входов и выходов – это регистры.
output reg [15:0] ax,
output reg [15:0] bx,
output reg [15:0] cx,
output reg [15:0] dx,
output reg [15:0] sp,
output reg [15:0] bp,
output reg [15:0] si,
output reg [15:0] di,
output reg [15:0] es,
output reg [15:0] cs,
output reg [15:0] ss,
output reg [15:0] ds,
output reg [15:0] ip,
output reg [11:0] flags,
Да, тут перечисляются все регистры, 8 общего назначения, 4 сегментных, 1 регистр программного счетчика IP и регистр флагов FLAGS. В более ранних реализациях процессоров, которые я делал, регистры обычно я размещал в виде перечисления reg, но так как мне часто требовалась отладка, при встраивании кода процессора в Си++ реализацию, то я стал выводить регистры "наружу". Это позволяет просматривать текущий их статус.
Из-за этой же причины я также подключаю и регистры, которые будут устанавливаться "извне" через тот же отладочный инструмент.
input [15:0] _ax,
input [15:0] _bx,
input [15:0] _cx,
input [15:0] _dx,
input [15:0] _sp,
input [15:0] _bp,
input [15:0] _si,
input [15:0] _di,
input [15:0] _es,
input [15:0] _cs,
input [15:0] _ss,
input [15:0] _ds,
input [15:0] _ip,
input [11:0] _flags
Смотря на это громадное количество проводов, приходит осознание что в реальном процессоре такое было бы абсолютно невозможно. Это слишком расточительно! Но не забываем, что это не реальный процессор, а учебный SoC (System on Chip) без конвейера, кешей и буферов, с 8-битной шиной данных и 20-битным адресом, и что тут это совершенно нормально.
§ Схема сброса
Давайте хотя бы что-то реализуем в схеме помимо подключения регистров и пинов. В первую очередь, нам необходима реализация схемы сброса. Из предыдущей главы сигнал сброса был уже подключен и он управлялся пином rst_n, но перед этим я хочу рассказать о принципах функционирования процессора в целом.
Схема процессора – это конечный автомат, что значит, что следующее состояние в регистрах-защелках (DFF-триггеры) будет вычисляться из предыдущего состояния. Если говорить на конкретном примере, то приведу простейшую схему конечного автомата:
reg [1:0] a;
always @(posedge c) a <= a + 1'b1;
Пример выше к процессору никакого отношения не имеет, он для иллюстрации. Что значит конечный автомат здесь? Это значит, что на позитивном фронте сигнала на проводе c (он подключен к тактовому генератору) из регистра a будет извлечено и прибавлено значение +1 и записано в регистр снова. Это значит что каждый такт регистр a будет последовательно принимать значения от 0 до 3, и так по кругу, что говорит о том, количество состоянии такой схемы ограничено. Из этого сделаем вывод, что если a=2, то следующий цикл (или такт) приведет к тому что a станет равным 3 и никакому другому значению.
Применительно к разработке процессора это значит что на каждом новом такте, на основе предыдущего состояния процессора будет вычисляться новое. Но не будем сильно забегать вперед, однако к этой теме вернемся сравнительно скоро. Все смены состояния происходят только на позитивном фронте clock.
always @(posedge clock)
if (rst_n == 0) begin
ax <= _ax; bx <= _bx; cx <= _cx; dx <= _dx;
sp <= _sp; bp <= _bp; si <= _si; di <= _di;
es <= _es; cs <= _cs; ss <= _ss; ds <= _ds;
ip <= _ip; flags <= _flags;
end
else if (ce) begin
end
Код выше демонстрирует первоначальный сброс процессора.
- Вне зависимости от состояния пина
ce при наличии низкого уровня на пине rst_n=0, на позитивном фронте clock будет выполнено неблокирующее (<=) присваивание новых состоянии регистров и флагов. Это важный момент, поскольку тут сбрасываются вообще все регистры до того значения, которое установлено из верхнего модуля.
- В случае когда
rst_n=1 и ce=1, схема сброса неактивна, но активна другая ветка выполнения инструкции.
На самом деле, эти условные операторы не являются теми условными операторами, которые мы привыкли видеть в программировании, но выполняют ту же функцию. Здесь, в схемотехнике, условные операторы реализуют мультиплексор, который выбирает одну из веток и присваивает новое значение регистру или регистрам. И всё. К этому сложно поначалу привыкнуть, но со временем и с опытом понимание такого процесса становится привычным.
Несмотря на то, что в схемотехнике всё выполняется одновременно, но в реализации в RTL (Register Transfer Level) каждое новое состояние регистра получается путем выборки из двух или более плечей альтернативы, так что создается ощущение будто за раз исполняется только один участок кода, причем, сразу же отмечу – все действия там, в этом участке, происходят одновременно.
Это значит, что при исполнении альтернативы, отвечающей за сброс регистров (см. код выше) все регистры установятся за 1 такт, то есть, сразу все, не последовательно (частично также из-за указания неблокирующего присваивания).