§ Объявление пинов процессора

Любой процессор обычно опирается на АЛУ, потому что задача процессора — это обработка данных, произведение операции над данными, так что без особого устройства, который называется Арифметико-Логическое Устройство (АЛУ) дело не пойдет. Это очень важный блок в любом процессоре.
Но вначале разберемся с входящими и выходящими пинами к модулю процессора.
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off CASEX */
/* verilator lint_off CASEOVERLAP */
/* verilator lint_off CASEINCOMPLETE */

module core
(
    input               clock,      // Тактовая частота
    input               reset_n,    // =0 Сброс процессора
    input               ce,         // =1 Процессор активирован
    output      [19:0]  a,          // Адресная шина (20 бит)
    input       [ 7:0]  i,          // Входящие данные
    output reg  [ 7:0]  o,          // Исходящие данные
    output reg          w,          // Разрешение записи в память
    output reg  [11:0]  flags,      // Флаги процессора
    output reg          halt,       // Признак остановки выполнения
    output              m0,         // =1 Выполняется 1-й такт инструкции
    output reg          ud          // =1 Неизвестный опкод
);

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 бит при вычитании. Теория о том как это работает, у меня описана в моей статье.
  • Флаг OF самый сложный для понимания, и он означает "переполнение" при вычислении результата. О том как это работает, тоже можно прочесть в моей статье. Могу лишь сказать что этот флаг совершенно незаменим при сравнениях с числами с дополненным знаком.
Ну и самый финальный аккорд:
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. Они ставятся и снимаются другими инструкциями.