Оглавление
§ Объявление пинов процессора
Любой процессор обычно опирается на АЛУ, потому что задача процессора – это обработка данных, произведение операции над данными, так что без особого устройства, который называется Арифметико-Логическое Устройство (АЛУ) дело не пойдет. Это очень важный блок в любом процессоре.
Но вначале разберемся с входящими и выходящими пинами к модулю процессора.
module core
(
input clock,
input reset_n,
input ce,
output [19:0] a,
input [ 7:0] i,
output reg [ 7:0] o,
output reg w,
output reg [11:0] flags,
output reg halt,
output m0,
output reg ud
);
endmodule
Теперь подробнее рассмотрю каждый из пинов, который представлен тут.
- Тактовая частота. Обычно я выбираю 25 мгц, это стандарт для меня. Он совпадает с VGA пиксель-клоком и достаточен для всех задач, поскольку скорость распространения сигнала максимально составляет 40 нс, при этом около 15 нс расходуется на извлечение данных из памяти и 25 нс на вычисление результатов, чего вполне хватает.
- Сброс процессора через reset_n необходим всегда при запуске ПЛИС. Я подключаю его к выходу locked из PLL (генератора частот). Когда ПЛИС только запускается, то какое-то время генератор частот настраивает частоту, установив locked=0, и как только конфигурация будет закончена, то locked становится равным 1, но в то время пока он был 0, то тактовые сигналы на clock все равно отсылались, что дает возможность процессору сброситься до начального состояния.
- Сигнал CE по умолчанию установлен в 1 и это означает, что процессор будет выполнять инструкции. Если там 0, то процессор инструкции не выполняет. Этот сигнал фактически аналогичен сигналу WAIT, который стопает процессор на какое-то время. По сути, в данный момент этот сигнал не нужен, но он будет полезен при использовании контроллера памяти, который будет временно останавливать процессор при не готовых данных из памяти, например.
- Пины a, i, o, w – это стандартный способ подключить процессор к памяти. В процессорах, на самом деле, для экономии пинов, используется i + o в одном и то же пине, например d, но я всегда разделяю их. Пин
I требуется для получения значения данных из памяти по адресу A, а пины O и W необходимы для записи в память по адресу A, если W=1. Если же W=0, то данные только читаются, а не пишутся
- Флаги flags выведены на внешние пины только для отладочных целей, ни для чего более.
- Также это касается и HALT, M0 и UD. Они необязательны, но без них никуда, если я хочу подключить их к симулятору. Эти пины нужны для отладчика, как и флаги. Также, как и набор регистров, но на данный момент я не вывел их на внешние пины, пока что в этом нет необходимости.
§ АЛУ
У этого процессора не так много арифметико-логических операции, не считая операции сдвига, а также например, деления или различных инструкции FPU (Floating Point Unit). Всего их восемь: ADD, SUB, ADC, SBB, XOR, OR, AND, CMP. Да, всего лишь восемь.
localparam
ADD = 0, OR = 1, ADC = 2, SBB = 3, AND = 4, SUB = 5, XOR = 6, CMP = 7;
wire [16:0] alu_r =
alu == ADD ? op1 + op2 :
alu == OR ? op1 | op2 :
alu == ADC ? op1 + op2 + flags[CF] :
alu == SBB ? op1 - op2 - flags[CF] :
alu == AND ? op1 & op2:
alu == XOR ? op1 ^ op2: op1 - op2;
Порядок следования команд здесь не случайно именно такой. Эти инструкции участвуют в разных видах опкодов. Существуют групповые инструкции, когда вместо операнда выбирается номер функции, и он должен совпадать с тем, что написано выше. То есть номер функции АЛУ=5 отведен для SUB, и никак иначе.
Работают такие инструкции очень просто, если рассмотреть выше приведенный код, который по своей сути, является мультиплексором. Могу отметить то что функция CMP и SUB вычисляются одинаково, но в первом случае, при выполнении инструкции CMP сохраняются только флаги, а при выполнении функции SUB сохраняются и флаги, и записывается результат выполнения.
Также flags[CF] – это бит флага Carry, то есть, инструкция ADC это сложение + заём флага, а SBB – вычитание с заёмом флага переноса. Этот флаг имеет большое значение и используется в вычислениях.
С самими вычислениями вроде как все понятно, но этого недостаточно. После того как процессор выполняет инструкцию АЛУ, он записывает еще и флаги, которых достаточно много и они нужны для дальнейших инструкции ветвления. Флаги после исполнения инструкции показывают, что далее делать процессору с результатами.
wire [ 3:0] alu_top = size ? 15 : 7;
wire [ 4:0] alu_up = alu_top + 1'b1;
wire is_add = alu == ADD || alu == ADC;
wire is_lgc = alu == XOR || alu == AND || alu == OR;
Для провода alu_top устанавливается либо 15, либо 7, это номер старшего бита в результате. Как видно, может быть 2 варианта результата, либо 16, либо 8 бит. Самый старший бит alu_up (там где бит переноса) устанавливается в 8 и 16 соответственно. В регистре size, если установлено 0, то используется 8 битные операнды op1, op2, а противном случае, эти операнды классифицируются как 16-битные.
То как формируются операнды, будет рассказано в следующей главе.
Также для того чтобы сделать правильные вычисления, необходимо знать тип инструкции. Если инструкция – это инструкция сложения (ADD, ADC), то на проводе is_add=1, иначе 0. Аналогично, если инструкция – это инструкция логики (XOR, AND или OR), то на проводе is_lgc=1.
Теперь же самое важное. Это вычисление флагов. Дальнейший код показывает то, как это происходит.
wire _of = !is_lgc &
(op1[alu_top] ^ op2[alu_top] ^ is_add) & (op1[alu_top] ^ alu_r[alu_top]);
wire _sf = alu_r[alu_top];
wire _zf = (size ? alu_r[15:0] : alu_r[7:0]) == 0;
wire _af = !is_lgc & (op1[4] ^ op2[4] ^ alu_r[4]);
wire _pf = ~^alu_r[7:0];
wire _cf = !is_lgc & alu_r[alu_up];
В данном коде сложно будет разобраться без пояснительной бригады. Для начала скажем так, что для любой логической инструкции (XOR, AND, OR) результат флагов OF, AF и CF будет одинаково равен 0, так что там и записано вначале !is_lgc. При наличии логической инструкции данная конструкция сбрасывает результат в 0 для данных флагов.
Теперь переходим к флагам, если это не логическая инструкция (все остальные – ADD, ADC, SUB, SBB, CMP).
- Флаг PF равен 1, если количество бит в результате четно (младших 8 битах). Например если количество бит 0,2,4 или 6, то будет 1, иначе будет 0. Четность бит подсчитать крайне просто, применив побитовое XOR к каждому биту. Теория о том, как это происходит, выходит за пределы этой скромной статьи.
- Флаг CF устанавливается в 1, если произошел либо перенос при сложении, либо же заем при вычитании. В зависимости от того, какая была выбрана битность (8 или 16), бит просто копируется из самого старшего бита alu_r (результата вычисления)
- Флаг SF аналогичен флагу CF, но бит копируется с последнего бита результата (7 или 15 бит), а не с бита переноса
- Флаг ZF – это флаг нулевого результата. В зависимости от битности (16 или 8) проверяется либо младшие 8 бит, либо же все 16 бит результата на 0. Если там 0, то ставится флаг ZF=1
- Флаг AF вычисляется на основе побитового XOR над запрошенными операндами и результатом в бите 4. Это флаг полупереноса, который проверяет, был ли перенос из 3 в 4 бит при сложении или был ли заем из 4 в 3 бит при вычитании. [[/cpu/flag_half Теория]] о том как это работает, у меня описана в моей статье.
- Флаг OF самый сложный для понимания, и он означает "переполнение" при вычислении результата. О том [[/cpu/flag_overflow как это работает]], тоже можно прочесть в моей статье. Могу лишь сказать что этот флаг совершенно незаменим при сравнениях с числами с дополненным знаком.
Ну и самый финальный аккорд:
wire [11:0] alu_f = {_of, flags[10:8], _sf, _zf, 1'b0, _af, 1'b0, _pf, 1'b1, _cf};
На этом проводе будет записано то самое итоговое значение флагов, которое будет сохранено после вычисления. Как видно, биты номер 1, 3 и 5 содержат фиксированные значения 1,0,0, а флаги с 8 по 10 бит просто копируются без изменений. Там находятся флаги управления DF,IF,TF. Они ставятся и снимаются другими инструкциями.