E4 Симуляция и верификация процессора

Вы уже научились использовать Verilog с помощью онлайн-платформы для обучения HDLBits. Аналогично, чтобы спроектировать собственный процессор, вам нужно будет использовать Linux в качестве среды разработки.

RTL-симуляция — функциональная верификация

Разрабатывать RTL-код в Linux не сложно, если у нас есть текстовый редактор. Однако после завершения разработки RTL-кода нам также нужен симулятор, чтобы проверить, работает ли схема, описанная этим RTL-кодом, так, как ожидается.

STFW + RTFM, чтобы настроить среду симуляции Verilator

Verilator — это симулятор Verilog с открытым исходным кодом, который вы будете использовать для функциональной RTL-симуляции.

Код фреймворка по умолчанию предоставляет каталог npc, где npc означает New Processor Core. В этом каталоге вы в дальнейшем спроектируете собственный процессор. Процессоры, спроектированные всеми участниками, будут совместно называться NPC, но вы, конечно, можете дать своему процессору более индивидуальное название. Однако для того, чтобы задать переменную окружения NPC_HOME, вам нужно выполнить следующую команду:

cd ysyx-workbench
bash init.sh npc

Эта переменная окружения будет использоваться в дальнейшем. В каталоге npc есть несколько простых файлов:

ysyx-workbench/npc
├── csrc
│   └── main.cpp
├── Makefile
└── vsrc
    └── example.v

Сейчас эти три файла почти пусты. Мы проведём вас через настройку среды симуляции Verilator и написание двух простых модулей цифровых схем для симуляции.

Даже каркаса для симуляции нет? Отстой!

Причина, по которой мы включили эту часть эксперимента, — дать всем понять, что все детали проекта имеют к вам отношение. В предыдущих учебных экспериментах все в той или иной степени считали, что каркас, естественно, должен предоставляться ассистентами преподавателя. Выполнение эксперимента сводилось к написанию соответствующего кода в отведённых местах, а весь остальной код/файлы считались не имеющими отношения к делу и не требующими внимания. На самом деле такой подход к экспериментам очень опасен. Он не только не тренирует вас стать настоящим профессионалом, но и делает невозможным ваше выживание в реальных проектах:

  1. Столкнувшись с системными ошибками, вы точно не сможете их исправить. Ведь даже модули, которые вызывают ваш код, считаются не имеющими к вам отношения, не говоря уже о чётком понимании архитектуры всего проекта и каждой детали внутри него.
  2. Без хендаута вы не можете сделать ничего. Потому что вы всегда ждёте, пока кто-то другой чётко скажет вам, что делать дальше и как это делать — так, как это делает данный хендаут, — вместо того чтобы самостоятельно анализировать, что нужно сделать, исходя из логики проекта.

Весьма реалистичный сценарий: когда в будущем вы придёте работать в компанию или в исследовательскую группу, у вас больше не будет ни хендаутов, ни каркасного кода, которые бы вам помогали. Если ваш руководитель скажет «попробуй Verilator», вам придётся самостоятельно разобраться и запустить Verilator, написать отчёт об использовании и представить свою работу руководителю на групповом собрании на следующей неделе. Поэтому мы хотим дать вам более реалистичную тренировку: поставить цель и научить вас разбивать её на шаги и достигать её пошагово с помощью собственных навыков. Настройка среды симуляции Verilator на самом деле — цель, которую легко достичь, поэтому она отлично подходит и как небольшая тренировка для проверки ваших способностей.

Если вы хотите использовать Chisel

Chisel может сгенерировать функционально эквивалентный код Verilog, который затем можно симулировать с помощью Verilator. Пока что мы сосредоточимся на использовании Verilator. Если вы хотите использовать Chisel, мы также рекомендуем сначала настроить рабочий процесс Verilog, как описано в хендауте, а затем переключиться.

Начнём.

Познакомьтесь с Verilator

Вероятно, вы впервые слышите о Verilator, и это совершенно нормально. Также нормально, что вам захочется узнать больше о различных аспектах Verilator. Однако неправильно, если ваша первая реакция — спросить кого-то. На самом деле инструмент Verilator настолько известен в области симуляции, что вы легко можете найти о нём информацию в интернете. Вам нужно с помощью STFW найти его официальный сайт, а затем прочитать соответствующее введение.

После того как вы найдёте и прочитаете соответствующую информацию, настанет время попробовать его запустить. Но перед этим нам сначала нужно его установить.

Установите Verilator

Найдите на официальном сайте шаги по установке Verilator и выполните соответствующие шаги установки через git. Причина, по которой мы не используем для установки apt-get, в том, что предоставляемая им версия сравнительно старая. Кроме того, чтобы унифицировать версию, вам нужно установить версию stable через git. Для этого вам также потребуется выполнить несколько простых операций с git. Если вы с этим не знакомы, вам может понадобиться найти какие-нибудь туториалы по git и изучить их. Кроме того, вам лучше выполнять эту операцию в каталоге вне ysyx-workbench/. Иначе git будет отслеживать исходный код Verilator, тем самым занимая ненужное место на диске.

После успешной установки выполните следующую команду, чтобы проверить, успешно ли прошла установка и правильна ли версия.

verilator --version

Verilator компилирует код в файлы C++, которые затем компилируются в исполняемые файлы. Симуляция выполняется путём запуска этих исполняемых файлов.

Запустите пример

В руководстве Verilator есть пример на C++. Вам нужно найти этот пример в руководстве и выполнить действия по его шагам. Вы уже изучили язык C. Для использования Verilator вам не нужно понимать сложный синтаксис C++ — достаточно знать некоторые базовые способы использования классов. С этой точки зрения многие материалы в интернете подойдут для ваших нужд.

Пример: проходной выключатель (комбинационная логическая схема)

Пример в руководстве очень простой и даже не может считаться настоящим модулем схемы. Далее мы напишем настоящий модуль схемы — проходной выключатель — для тестирования. Напишите следующий код Verilog:

module top(
  input a,
  input b,
  output f
);
  assign f = a ^ b;
endmodule

Одно из применений проходного выключателя — совместное управление состоянием включения/выключения (f) одной и той же лампы с помощью двух выключателей (a и b). В отличие от примера в руководстве, у этого модуля есть входные и выходные порты. Чтобы подать сигналы на входные порты и получить результаты с выходных портов, нам нужно изменить цикл while в файле C++:

// The following is pseudocode

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

while (???) {
  int a = rand() & 1;
  int b = rand() & 1;
  top->a = a;
  top->b = b;
  top->eval();
  printf("a = %d, b = %d, f = %d\n", a, b, top->f);
  assert(top->f == (a ^ b));
}

За одну итерацию цикла код случайным образом сгенерирует два 1-битных сигнала для подачи на два входных порта. Затем он обновит состояние схемы с помощью функции eval(), что позволит нам считать и вывести значения с выходного порта. Чтобы автоматически проверить корректность результатов, мы будем проверять вывод с помощью выражений assert().

Симулируйте модуль проходного выключателя

Попробуйте симулировать модуль проходного выключателя в Verilator. Поскольку имя модуля верхнего уровня отличается от примера в руководстве, вам нужно внести соответствующие изменения в файл C++. Кроме того, в этом проекте нет оператора, указывающего на завершение симуляции. Чтобы выйти из симуляции, вам нужно нажать Ctrl+C.

Что означает приведённый выше код?

Если вы не знаете, как его изменить, значит, вы не очень хорошо знакомы с написанием программ на C. Вам стоит вернуться к предыдущему разделу, чтобы повторить язык C.

Вывод и просмотр временных диаграмм

Просмотр файлов временных диаграмм — один из распространённых способов отладки RTL. Verilator поддерживает генерацию временных диаграмм, а просматривать их можно с помощью просмотрщика временных диаграмм с открытым исходным кодом GTKWave.

Сгенерируйте и просмотрите временные диаграммы

В руководстве Verilator уже описан способ генерации временных диаграмм. Вам нужно прочитать руководство, найти соответствующее содержимое, затем по шагам из руководства сгенерировать файл временной диаграммы и установить GTKWave с помощью

apt-get install gtkwave

чтобы просмотреть временные диаграммы.

В руководстве так много материала — как там что-то найти?

Попробуйте нажать Ctrl+F.

Не генерируйте временные диаграммы долго

Файлы временных диаграмм обычно занимают много места на диске. Долгая генерация временных диаграмм может привести к исчерпанию места на диске, что может вызвать сбой системы.

Генерируйте временные диаграммы в формате FST

Размер файлов временных диаграмм формата FST составляет примерно 1/50 от размера формата VCD, но поддерживается он только в GTKWave. Тем не менее мы всё равно рекомендуем вам его использовать. Подробнее о том, как генерировать временные диаграммы формата FST, вы можете узнать из руководства Verilator.

Написание Makefile

Симуляция в один клик

Многократно вводить команды компиляции и запуска неудобно. Попробуйте написать правило sim для npc/Makefile, чтобы реализовать симуляцию в один клик — так, чтобы ввод make sim выполнял описанную выше симуляцию.

Не забудьте сохранить команды отслеживания git

Код фреймворка уже предоставляет правило sim по умолчанию в npc/Makefile, которое включает команду отслеживания git: $(call git_commit, "sim RTL"). При написании Makefile будьте осторожны и не изменяйте эту команду, поскольку она влияет на функцию отслеживания разработки, которая является важным основанием для фиксации оригинальности результатов «One Student One Chip». Поэтому после написания Makefile и его запуска вам также нужно убедиться, что git правильно отследил записи симуляции.

Интеграция NVBoard

NVBoardоткрыть в новом окне (NJU Virtual Board) — это проект виртуальной платы FPGA, разработанный Нанкинским университетом для учебных целей. Он может предоставить интерфейс виртуальной платы в среде RTL-симуляции, поддерживая такие функции, как DIP-переключатели, светодиоды, VGA-дисплеи и т.д. В сценариях, где не требуется высокая скорость, он может полностью заменить настоящую плату FPGA (в конце концов, не у всех есть FPGA под рукой). Получите код NVBoard с помощью следующей команды:

cd ysyx-workbench
bash init.sh nvboard

Запустите пример NVBoard

Прочитайте README.md проекта NVBoard и попробуйте запустить предоставленный пример.

Не понимаете, как работает NVBoard?

Попробуйте начать с команды make, чтобы увидеть, как всё происходит. С учётом знаний, полученных в ходе предыдущего обучения, у вас уже достаточно базы, чтобы понять, как работает NVBoard: сюда входит использование Makefile-ов и базовое использование классов в C и C++. Теперь попробуйте почитать код (Makefile — это тоже код), чтобы увидеть, как связаны между собой порты верхнего уровня Verilog, файлы ограничений и NVBoard.

Реализуйте проходной выключатель на NVBoard

Прочитайте инструкции проекта NVBoard, затем, взяв за образец файлы C++ и Makefile из примера, попробуйте изменить свой файл C++, назначить выводы входу и выходу проходного выключателя и изменить npc/Makefile, чтобы подключить его к переключателям и светодиодам на NVBoard.

История NVBoard

Хотя NVBoard — учебный проект Нанкинского университета, он имеет особую связь со всеми участниками «One Student One Chip»:

Среди списка студентов, изготовивших чипы в третьем наборе «One Student One Chip», было двое особенных. Когда они зарегистрировались, они были всего лишь первокурсниками. И один из них, sjr, — первый автор NVBoard.

На самом деле именно способность самостоятельно решать проблемы и уверенность в себе, которые sjr развил, участвуя в «One Student One Chip», помогли ему успешно разработать проект NVBoard. Теперь же проект NVBoard, в свою очередь, помогает «One Student One Chip» повысить эффективность обучения. Помимо функции виртуальной платы FPGA, NVBoard также несёт в себе идею самостоятельного решения проблем, которую отстаивает «One Student One Chip». Всё это не так уж далеко от вас. Когда вы будете готовы учиться самостоятельно, вместо того чтобы ждать, пока кто-то другой даст вам ответы, ваше будущее тоже будет полно безграничных возможностей.

Пример: бегущие огни (последовательностная логическая схема)

Бегущие огни — это набор ламп, которые последовательно включаются и выключаются. Ниже приведена эталонная реализация бегущих огней:

module light(
  input clk,
  input rst,
  output reg [15:0] led
);
  reg [31:0] count;
  always @(posedge clk) begin
    if (rst) begin led <= 1; count <= 0; end
    else begin
      if (count == 0) led <= {led[14:0], led[15]};
      count <= (count >= 5000000 ? 32'b0 : count + 1);
    end
  end
endmodule

Каждый бит его выходного сигнала led соответствует светодиоду на виртуальной плате. Поскольку код содержит последовательностные логические компоненты, которым требуется сброс, нам нужно изменить код симуляции Verilator:

// Below is the pseudocode

void single_cycle() {
  top->clk = 0; top->eval();
  top->clk = 1; top->eval();
}

void reset(int n) {
  top->rst = 1;
  while (n -- > 0) single_cycle();
  top->rst = 0;
}

...
reset(10);  // reset for 10 cycles
while(???) {
  ...
  single_cycle();
  ...
}

Подключите бегущие огни к NVBoard

Напишите модуль бегущих огней, затем подключите его к NVBoard и назначьте выводы. Если ваша реализация верна, вы увидите, как огни последовательно загораются и гаснут справа налево.

Статическая проверка кода

Verilator также может работать как lint-инструмент для статической проверки кода. Передав verilator в командной строке параметр --lint-only, вы заставите Verilator выполнять только проверку кода и указывать на потенциально проблемный код в виде предупреждений, не генерируя файлы C++. В частности, вы также можете добавить опцию -Wall, чтобы включить в Verilator все типы проверок, что поможет ему найти больше потенциальных проблем.

В главе Errors and Warnings руководства Verilator перечислены объяснения для всех предупреждений. Прочитав их, вы поймёте, как возникают эти предупреждения, и, соответственно, будете знать, как их исправить. Предупреждения, связанные с логикой кода, следует устранять, изменяя код; но для некоторых предупреждений, связанных со стилем кода, если вы уверены, что они не влияют на логику кода, вы можете отключить предупреждение xxx с помощью дополнительной опции -Wno-xxx, например -Wno-DECLFILENAME.

Выполните статическую проверку кода с помощью Verilator

Попробуйте проверить свой код с помощью Verilator и по возможности исправить все предупреждения. Мы рекомендуем вам и в дальнейшем всегда включать функцию статической проверки кода Verilator. С одной стороны, это помогает вырабатывать хорошие привычки написания кода и тем самым писать код более высокого качества. С другой стороны, как можно более раннее обнаружение потенциальных проблем в коде также помогает избежать лишней отладочной работы: по мере роста масштаба кода вам, возможно, придётся потратить несколько дней на отладку из-за ошибки разрядности какого-нибудь сигнала, но предупреждения Verilator позволят вам сразу заметить эту проблему и легко устранить соответствующую ошибку.

Углублённое изучение Verilator

нажмите сюдаоткрыть в новом окне

Несколько стилей и стандартов кодирования

В предыдущих фазах OSOC мы обнаружили, что определённые нестандартные стили кодирования могут создавать дополнительные проблемы на этапе интеграции SoC. Чтобы не влиять на ход будущих интеграций SoC, мы рекомендуем всем придерживаться следующих стандартов кодирования.

1. Если вы не разобрались глубоко в событийной модели Verilog, не используйте поведенческое моделирование.

На самом деле в материалах лабораторной работы по цифровым схемам Нанкинского университета также упоминается, что «поведенческое моделирование вредит начинающим в выработке схемотехнического мышления». Здесь мы приводим соответствующее описание:

новичкам настоятельно рекомендуется НЕ проектировать схемы с помощью поведенческого моделирования.

Verilog изначально не предназначался для проектирования синтезируемых схем; по своей сути это язык моделирования схем, основанный на модели очереди событий. Поэтому поведенческое моделирование легко может увести новичков от изначального замысла описания схемы: разработчику нужно посмотреть на принципиальную схему, мысленно представить поведение схемы, а затем преобразовать его в модель очереди событий, в итоге используя поведенческое моделирование для описания поведения схемы, из которого синтезатор выводит соответствующую схему. В этом процессе не только нет необходимости, но и очень легко внести ошибки:

  • Если у разработчика уже есть принципиальная схема в голове, самое удобное — описать её напрямую.
  • Если у разработчика уже есть принципиальная схема в голове, но его понимание поведенческого моделирования ошибочно, он может выбрать неверный способ описания, что приведёт к неожиданному результату проектирования схемы.
  • Если у разработчика нет принципиальной схемы, но он ожидает, что синтезатор сгенерирует схему с определённым поведением через поведенческое моделирование, это уже отклонение от сути «описания схем». Многие студенты легко допускают эту ошибку, воспринимая поведенческое моделирование как процедурный код на C и пытаясь отобразить в схему любое сложное поведение, что в итоге приводит к тому, что синтезатор генерирует низкокачественные схемы с большой задержкой, площадью и энергопотреблением, а то и вовсе к схеме, которая ведёт себя неожиданно из-за гонок данных в коде.

Поэтому, пока каждый не овладеет мышлением «описания схем», не поддаваясь влиянию поведенческого моделирования, мы настоятельно рекомендуем новичкам держаться подальше от поведенческого моделирования и описывать схемы напрямую с помощью потокового моделирования данных и структурного моделирования. Следующие вопросы помогут проверить, ухватили ли вы суть Verilog:

  • Что именно означает «выполнение» в языках описания аппаратуры?
  • Кто выполняет операторы Verilog? Схема, синтезатор или что-то ещё?
  • Если условие оператора if выполнено, операторы после else не выполняются; что здесь означает «не выполняются»? Как это связано с описанием схем?
  • Есть «параллельные выполнения», «последовательные выполнения» и «выполнения, запускаемые изменением любой переменной», а также «выполнения при любых обстоятельствах»; как они отражаются в спроектированной схеме?

Если вы не можете чётко ответить на эти вопросы, мы настоятельно рекомендуем вам воздержаться от использования поведенческого моделирования. Если вы действительно хотите в этом разобраться, вам нужно прочитать стандарт Verilogоткрыть в новом окне.

истинное описание схемы = инстанцирование + соединение.

Если забыть о поведенческом моделировании, можно прямо вернуться к простой сути описания схемы. Представьте, что у вас есть принципиальная схема; как бы вы описали её содержимое другим людям? Скорее всего, вы бы сказали что-то вроде: «Есть компонент/модуль A, и его вывод x соединён с выводом y другого компонента/модуля B» — ведь это самый естественный способ описать схему. Проектирование схем с помощью HDL заключается в использовании HDL для описания принципиальной схемы — то, что на схеме, вы описываете напрямую. Таким образом, описание схемы с помощью HDL по сути состоит из двух задач:

  • Инстанцирование: размещение компонента/модуля на плате схемы, которым может быть логический элемент или модуль, состоящий из логических элементов.
  • Соединение: корректное соединение выводов компонентов/модулей проводниками.

Вы можете оценить, как потоковое моделирование данных и структурное моделирование воплощают эти две задачи, тогда как поведенческое моделирование усложняет эти простые по своей сути задачи.

Таким образом, мы не рекомендуем новичкам писать в коде Verilog какие-либо операторы always. Чтобы облегчить использование триггеров и мультиплексоров, мы предоставляем следующие шаблоны Verilog для вашего использования:

// Flip-Flop Template
module Reg #(WIDTH = 1, RESET_VAL = 0) (
  input clk,
  input rst,
  input [WIDTH-1:0] din,
  output reg [WIDTH-1:0] dout,
  input wen
);
  always @(posedge clk) begin
    if (rst) dout <= RESET_VAL;
    else if (wen) dout <= din;
  end
endmodule

// Example of using the Flip-Flop Template
module example(
  input clk,
  input rst,
  input [3:0] in,
  output [3:0] out
);
  // width of 1 bit, reset value of 1’b1, write enable always active
  Reg #(1, 1'b1) i0 (clk, rst, in[0], out[0], 1'b1);
  // width of 3 bits, reset value of 3’b0, write enable is out[0]
  Reg #(3, 3'b0) i1 (clk, rst, in[3:1], out[3:1], out[0]);
endmodule
// Internal Implementation of the Multiplexer Template
module MuxKeyInternal #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1, HAS_DEFAULT = 0) (
  output reg [DATA_LEN-1:0] out,
  input [KEY_LEN-1:0] key,
  input [DATA_LEN-1:0] default_out,
  input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);

  localparam PAIR_LEN = KEY_LEN + DATA_LEN;
  wire [PAIR_LEN-1:0] pair_list [NR_KEY-1:0];
  wire [KEY_LEN-1:0] key_list [NR_KEY-1:0];
  wire [DATA_LEN-1:0] data_list [NR_KEY-1:0];

  genvar n;
  generate
    for (n = 0; n < NR_KEY; n = n + 1) begin
      assign pair_list[n] = lut[PAIR_LEN*(n+1)-1 : PAIR_LEN*n];
      assign data_list[n] = pair_list[n][DATA_LEN-1:0];
      assign key_list[n]  = pair_list[n][PAIR_LEN-1:DATA_LEN];
    end
  endgenerate

  reg [DATA_LEN-1 : 0] lut_out;
  reg hit;
  integer i;
  always @(*) begin
    lut_out = 0;
    hit = 0;
    for (i = 0; i < NR_KEY; i = i + 1) begin
      lut_out = lut_out | ({DATA_LEN{key == key_list[i]}} & data_list[i]);
      hit = hit | (key == key_list[i]);
    end
    if (!HAS_DEFAULT) out = lut_out;
    else out = (hit ? lut_out : default_out);
  end
endmodule

// Multiplexer Template without Default Value
module MuxKey #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1) (
  output [DATA_LEN-1:0] out,
  input [KEY_LEN-1:0] key,
  input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
  MuxKeyInternal #(NR_KEY, KEY_LEN, DATA_LEN, 0) i0 (out, key, {DATA_LEN{1'b0}}, lut);
endmodule

// Multiplexer Template with Default Value
module MuxKeyWithDefault #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1) (
  output [DATA_LEN-1:0] out,
  input [KEY_LEN-1:0] key,
  input [DATA_LEN-1:0] default_out,
  input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
  MuxKeyInternal #(NR_KEY, KEY_LEN, DATA_LEN, 1) i0 (out, key, default_out, lut);
endmodule

В нём модуль MuxKey реализует функцию «выбора по ключу-значению», которая устанавливает out в данные, соответствующие переданному ключу key, из списка пар (key, data) lut. Если в списке нет данных со значением ключа key, out будет равен 0. В частности, модуль MuxKeyWithDefault может предоставить значение по умолчанию default_out, и когда ни одна пара ключ-значение не соответствует key, out будет равен default_out.

При инстанцировании этих двух модулей обратите внимание на следующее:

  • Пользователю нужно указать количество пар ключ-значение NR_KEY, разрядность ключа KEY_LEN и разрядность данных DATA_LEN, при этом убедившись, что разрядность сигналов портов соответствует переданным параметрам, иначе результаты будут неверными.
  • Если в списке есть несколько записей данных с одинаковым значением ключа, значение out не определено, и ответственность за уникальность значений ключей в списке лежит на пользователе.

Реализация модуля MuxKeyInternal использует различные продвинутые возможности, такие как generate и циклы for, а для удобства применено поведенческое моделирование. Здесь мы не будем подробно на этом останавливаться: благодаря абстракции структурного моделирования пользователи могут не обращать внимания на эти детали.

Следующий код использует шаблоны мультиплексора для реализации мультиплексора 2-в-1 и мультиплексора 4-в-1:

module mux21(a,b,s,y);
  input   a,b,s;
  output  y;

  // Implement the following always code through MuxKey
  // always @(*) begin
  //  case (s)
  //    1'b0: y = a;
  //    1'b1: y = b;
  //  endcase
  // end
  MuxKey #(2, 1, 1) i0 (y, s, {
    1'b0, a,
    1'b1, b
  });
endmodule

module mux41(a,s,y);
  input  [3:0] a;
  input  [1:0] s;
  output y;

  // Implement the following always code through MuxKeyWithDefault
  // always @(*) begin
  //  case (s)
  //    2'b00: y = a[0];
  //    2'b01: y = a[1];
  //    2'b10: y = a[2];
  //    2'b11: y = a[3];
  //    default: y = 1'b0;
  //  endcase
  // end
  MuxKeyWithDefault #(4, 2, 1) i0 (y, s, 1'b0, {
    2'b00, a[0],
    2'b01, a[1],
    2'b10, a[2],
    2'b11, a[3]
  });
endmodule

если вы используете Chisel, также рекомендуется не использовать `when` и `switch`.

В Chisel семантика when и switch очень похожа на поведенческое моделирование в Verilog, поэтому новичкам также не рекомендуется их использовать. Вместо этого вы можете использовать библиотечные функции вроде Mux1H для реализации функциональности мультиплексора. Подробности можно найти в соответствующих материалах по Chisel.

2. если вы всё же настаиваете на использовании поведенческого моделирования Verilog, не используйте negedge.

Смешивание posedge и negedge может усложнить сходимость по времени и увеличить сложность физической реализации на бэкенде. Если вы не уверены, как поддерживать хорошие тайминги при смешивании обоих, мы рекомендуем использовать только posedge. В противном случае, если ваш процессор серьёзно ухудшит общие тайминги SoC, команда проекта OSOC уберёт ваш процессор из списка на тейп-аут в условиях сжатых сроков тейп-аута.

Если вы используете предоставленные нами выше шаблоны Verilog или используете Chisel, вам не нужно беспокоиться об этой проблеме.

если вы всё же настаиваете на использовании поведенческого моделирования Verilog, не используйте защёлки.

Изменения в защёлках не управляются тактовым сигналом, из-за чего их сложно анализировать инструментам анализа таймингов. Если вы не уверены, как избежать защёлок, мы рекомендуем вам не использовать поведенческое моделирование.

Если вы используете предоставленные нами выше шаблоны Verilog или используете Chisel, вам не нужно беспокоиться об этой проблеме.

4. вам нужно добавить префикс с номером студента перед именем модуля.

Например, module IFU нужно изменить на module ysyx_22040000_IFU. Это потому, что когда все интегрируют свои процессоры в SoC, модули с одинаковыми именами приведут к ошибкам повторного определения, о которых сообщат инструменты.

Если вы используете Chisel, вам пока не нужно добавлять префикс с номером студента к именам модулей при написании кода.

5. если вы используете Verilog, вам нужно добавить префикс с номером студента перед идентификаторами определений макросов.

Например, define SIZE 5` нужно изменить на define ysyx_22040000_SIZE 5`. Это потому, что когда все интегрируют свои процессоры в SoC, макросы с одинаковыми именами приведут к ошибкам повторного определения, о которых сообщат инструменты.

Если вы используете Chisel, вам не нужно беспокоиться об этой проблеме.

Завершение экспериментов с цифровыми схемами

Ранее вы уже выполнили несколько проектов цифровых схем на онлайн-платформе для обучения HDLBits. После настройки среды симуляции мы теперь можем поддержать относительно полный процесс проектирования цифровых схем:

Новые требования -> Архитектурное проектирование -> Логическое проектирование -> Функциональная верификация -> Оценка схемы

Здесь Архитектурное проектирование означает «продумывание того, как реализовать новые требования через функциональность схемы», Логическое проектирование означает «реализацию плана проектирования с помощью RTL-кода», Функциональная верификация в настоящее время достигается через симуляцию в Verilator, чтобы проверить, соответствует ли функциональность, реализованная RTL-кодом, ожиданиям, а Оценка схемы использует EDA-инструменты с открытым исходным кодом для оценки производительности схемы, площади, энергопотребления и других метрик.

Далее вы попробуете выполнить несколько экспериментов с цифровыми схемами в соответствии с описанным выше процессом, тем самым углубив своё понимание этого процесса.

Изучение гибкого языка разработки Chisel

Для некоторых сложных цифровых схем использование Chisel может сделать процесс проектирования более удобным. Вы постепенно осознаете это в ходе обучения на этапе B. Однако «One Student One Chip» не ограничивает вас в выборе языка для проектирования процессора.

Если вы планируете изучать Chisel, мы всё же рекомендуем вам сначала освоить основы Verilog, а затем следовать такой рекомендуемой последовательности изучения:

  1. Chisel Users Guideоткрыть в новом окне — хорошее введение в chisel, поскольку оно более систематично излагает возможности chisel.
  2. Chisel cheatsheetоткрыть в новом окне — краткий список типичных случаев использования языка chisel.
  3. Digital Design with Chiselоткрыть в новом окне — справочная книга, объединяющая концепции проектирования цифровой логики с Chisel.
  4. Chisel APIоткрыть в новом окне — здесь подробно перечислены все API библиотеки chisel для справки.

Если вы хотите присоединиться к группе обсуждения Chisel, вы можете отсканировать QR-код ниже с помощью WeChat и обратиться к TA, чтобы запросить доступ:

QRCode-Photo

если вы хотите использовать Chisel

Выполните следующую команду:

cd ysyx-workbench
bash init.sh npc-chisel

Эта команда заменит файлы в каталоге npc средой разработки Chisel; подробности можно найти в README.md внутри неё.

Для кода Verilog, сгенерированного Chisel, предупреждения статического анализа кода Verilator могут быть трудноисправимыми, но вы можете их игнорировать, если уверены, что эти предупреждения не влияют на корректность кода. Тем не менее мы всё равно рекомендуем вам всегда включать функцию статической проверки кода Verilator, поскольку при просмотре этих предупреждений вы можете обнаружить некоторые проблемы, связанные с логикой кода.

Выполните эксперименты с цифровыми схемами с помощью NVBoard

В первую очередь мы рекомендуем Digital Circuit and Computer Composition Experimentоткрыть в новом окне Нанкинского университета.

Нанкинский университет провёл образовательную реформу, объединив «Цифровые схемы» и «Принципы организации ЭВМ» в единый курс, с содержанием экспериментов, охватывающим всё — от основ цифровых схем до проектирования простого процессора. С NVBoard вы можете воспринимать её как FPGA для выполнения экспериментов, требующих поддержки FPGA.

Вам нужно выполнить следующее обязательное содержание:

  • Experiment 2: Decoders and Encoders
  • Experiment 3: Adders and ALUs
  • Experiment 6: Shift Registers and Barrel Shifters
  • Experiment 7: State Machines and Keyboard Input
  • Experiment 8: VGA Interface Controller Implementation

Если вы планируете использовать Chisel для выполнения описанных выше экспериментов с цифровыми схемами, вам просто нужно подключить скомпилированный код Verilog к Verilator и NVBoard.

Реализация простого процессора на RTL

Наконец настал момент спроектировать ваш первый процессор с помощью RTL! Вы уже реализовали sCPU в Logisim, и теперь, чтобы реализовать sCPU на RTL, вы опишете структуру схемы каждого модуля с помощью RTL-кода на основе принципиальной схемы в Logisim. С вашим опытом выполнения цифровых схем это не должно вызвать у вас затруднений.

Реализуйте sCPU на RTL

Попробуйте сделать sCPU целью проектирования NPC. На основе sCPU, спроектированного вами в Logisim, перепроектируйте его на RTL для вычисления 1+2+...+10. Чтобы увидеть результат, вы можете вывести результат вычисления на светодиоды NVBoard с помощью инструкции io.

Эффективный метод верификации процессора——DiffTest

Сейчас в sISA всего 4 инструкции, а программа суммирования, выполняемая на sCPU, тоже очень простая — даже если реализация RTL неверна, отладка с помощью временных диаграмм не составляет труда. Однако по мере того как процессор и программы усложняются, поиск ошибок в огромном объёме данных временных диаграмм станет очень непростой задачей. Представьте, что в будущем спроектированный вами NPC будет иметь 1000 сигналов. Если через 100 000 тактов вы обнаружите, что результат сложной программы неверен, файл временной диаграммы будет содержать 1000 × 100000 = 100 миллионов значений сигналов. Как быстро найти среди них несколько ключевых значений сигналов, чтобы помочь диагностировать проблему в RTL? Очевидно, что в таком сложном сценарии полагаться исключительно на временные диаграммы при отладке крайне неэффективно.

Чтобы найти эффективный способ отладки, нам нужно заново взглянуть на то, как работает процессор. Выполнение программы на процессоре — это просто процесс последовательного выполнения инструкций одна за другой. По сути, этот процесс можно описать с помощью модельной машины ISA — процессор — это всего лишь цифровая схема, реализующая эту модельную машину ISA. Так можем ли мы реализовать эту модельную машину ISA более простым способом и сравнить, корректны ли процессы выполнения инструкций у обоих?

Действительно, можем!Это на самом деле очень эффективная методология тестирования, известная в области тестирования программного обеспечения как дифференциальное тестированиеоткрыть в новом окне (далее — DiffTest). Обычно DiffTest выполняется путём предоставления REF (Reference, эталонной реализации), которая имеет ту же функциональность, что и DUT (Design Under Test, тестируемая схема), но с другой реализацией, а затем подачи на них одних и тех же заданных входных данных, чтобы посмотреть, ведут ли они себя одинаково. Когда поведение этих двух отличается, это обычно указывает на то, что у DUT есть ошибка при соответствующем входном воздействии.

Очевидно, что процессор, реализованный с помощью цифровых схем, следует рассматривать как DUT. Чтобы верифицировать DUT с помощью DiffTest, нам также нужен REF.

Симулятор ISA — программа, которая умеет выполнять программы

На самом деле мы попытаемся реализовать процесс выполнения программы на языке C. Такая программа, используемая для выполнения других программ, называется «симулятор». В промышленном процессе проектирования процессоров симуляторы играют очень важную роль. Как вы, возможно, уже догадались, этот симулятор может послужить REF для DiffTest! Но пока сосредоточимся на том, как реализовать простой симулятор.

Вы уже пробовали спроектировать CPU — будь то с помощью Logisim или RTL, — и этот процесс заключается в использовании автомата состояний цифровой схемы для реализации автомата состояний ISA. Чтобы реализовать симулятор на C, нам нужно продумать, как использовать автомат состояний программы на C для реализации автомата состояний ISA. Поэтому мы сначала рассмотрим программу на C и ISA с точки зрения автоматов состояний:

Программа на CISA
Состояние{PC, V}{PC,R,M}
СобытиеПравило перехода состоянийВыполнение инструкции
Правило перехода состоянийСемантика оператораСемантика инструкции

Чтобы с помощью автомата состояний программы на C реализовать автомат состояний ISA, нам нужно разработать программу на C, включающую следующую функциональность:

  • Использовать состояние программы на C для представления состояния ISA, то есть использовать переменные программы на C для представления PC, GPR и памяти ISA.
  • Использовать правила перехода состояний программы на C для реализации правил перехода состояний ISA, то есть использовать операторы языка C для реализации семантики инструкций.

Симулятор, который реализует только поведение инструкций с точки зрения ISA, называется симулятором системы команд. Аналогично существуют симуляторы архитектуры и симуляторы схем, но пока мы их касаться не будем.

Продемонстрируем, как реализовать симулятор системы команд, на примере sISA, называя соответствующий симулятор системы команд sEMU (simple EMUlator). Подробности о sISA см. в предыдущих лекциях.

sEMU нужно использовать переменные программы на C для представления PC, GPR и памяти ISA, что не составляет труда:

// sEMU.c

#include <stdint.h>
uint8_t PC = 0;
uint8_t R[4];
uint8_t M[256];

При этом PC имеет разрядность 8 бит, а значит, программы sISA могут содержать не более 256 инструкций. Поэтому размер массива M, используемого для реализации памяти, нужно задать равным всего лишь 256.

sEMU нужно использовать операторы языка C для реализации семантики инструкций, что тоже не составляет труда. С точки зрения цикла инструкции, нам нужно написать функцию inst_cycle(), реализующую следующую функциональность:

  • Выборка — напрямую проиндексировать память M по PC, чтобы получить инструкцию.
  • Декодирование — с помощью побитовых операций языка C извлечь поле opcode инструкции и определить, к какой инструкции она относится; затем извлечь поля операндов согласно формату инструкции и получить соответствующие операнды.
  • Выполнение — если выполняемая инструкция не bner0, записать результат обратно в регистр-приёмник; иначе решить, выполнять ли переход, на основе условия.
  • Обновление PC — если перехода нет, увеличить PC на 1.

После реализации inst_cycle() нам просто нужно постоянно вызывать её в sEMU:

while (1) { inst_cycle(); }

Выше были учтены все детали sISA, но чтобы запустить программу в sEMU, нам также нужно продумать, как поместить программу в M. Однако поскольку sEMU предполагается использовать только для запуска программы суммирования, мы можем напрямую инициализировать M:

uint8_t M[16] = { ... };

Кроме того, поскольку у sEMU пока нет абстракции устройств, она не может корректно выполнять программы, содержащие инструкцию io. Мы можем не реализовывать конкретное поведение инструкции io — если нужно выполнить инструкцию io, достаточно просто обновить PC. Однако, поскольку пока мы собираемся запускать только программу суммирования, нам не нужно беспокоиться о запуске других программ.

Реализуйте sEMU

Основываясь на изложенных выше идеях, реализуйте sEMU в коде C и запустите предыдущую программу суммирования. Поскольку сама программа суммирования не завершается, вы можете изменить условие цикла в операторе while так, чтобы выйти из него после определённого числа итераций, а затем проверить, соответствует ли результат суммирования ожиданиям.

Сравните sEMU и sCPU

Для sISA вы реализовали и sEMU, и sCPU. Попробуйте сравнить их и посмотреть, какие есть различия.

Вы могли заметить, что даже при реализации одного и того же набора команд sISA разработка симулятора на C гораздо проще, чем проектирование процессора на RTL. Нам нужно лишь продумать, как реализовать поведение каждой инструкции на C, не беспокоясь о её реализации в схеме или деталях соединений. На самом деле реализация настоящего процессора требует учёта множества дополнительных факторов, включая тайминги, площадь и энергопотребление. Вы глубже разберётесь в этих аспектах на более поздних этапах. Однако с точки зрения обучения именно потому, что реализация симулятора не требует от нас учёта этих факторов, она даёт отличный способ понять систему команд и поведение программ. В промышленных потоках проектирования процессоров симуляторы также играют важную роль в функциональной верификации, анализе программ и отладке. Более детальные симуляторы архитектуры также являются важной составляющей исследования пространства проектных решений процессора. По мере продолжения обучения вы будете постепенно всё глубже понимать ценность и значимость симуляторов.

Запустите DiffTest между sCPU и sEMU

Взяв sEMU в качестве REF, мы можем рассмотреть запуск DiffTest между sCPU и sEMU. А именно: во время симуляции, после того как sCPU выполнит одну инструкцию, сразу же заставим sEMU тоже выполнить одну инструкцию. Затем получим значения GPR как у sCPU, так и у sEMU и проверим, совпадают ли они. Если значения GPR отличаются, выведем сообщение об ошибке и остановим симуляцию.

// The following is pseudocode
while (???) {
  dut_single_cycle();
  ref_inst_cycle();
  uint8_t *dut_regs = &top->rootp->NPC__DOT__gpr_ext__DOT__Memory;
  uint8_t *ref_regs = ref_get_regs();
  int is_diff = check_regs(dut_regs, ref_regs);
  if (is_diff) {
    printf("GPR different\n");
    printf("Simulation stop\n");
    break;
  }
}

Чтобы реализовать описанную выше функциональность, вам нужно:

  1. Удалить функцию main() из sEMU и изменить расширение исходного файла с .c на .cpp. Это нужно для удобства связывания sEMU со средой симуляции sCPU.
  2. Добавить sEMU.cpp в список файлов команды verilator.
  3. После того как вы разберётесь в приведённом выше псевдокоде, переписать цикл while процесса симуляции и реализовать недостающие функции.
  • Обратите внимание, что &top->rootp->NPC__DOT__gpr_ext__DOT__Memory в псевдокоде используется для доступа к GPR через файлы C++, скомпилированные Verilator. Конкретные имена переменных C++ связаны с именами модулей и переменных в Verilog, и их можно найти, прочитав скомпилированные заголовочные файлы C++. Однако имена переменных C++ могут меняться при изменении RTL-кода или переключении версии Verilator, поэтому их нужно синхронизировать вручную.

Запустите DiffTest между sCPU и sEMU

Следуя описанным выше шагам, добавьте механизм DiffTest в среду симуляции sCPU. После добавления попробуйте внести в sCPU несколько ошибок и понаблюдайте, сообщает ли DiffTest об ошибках, как ожидается.

Улучшите сообщения об ошибках

Приведённые выше сообщения об ошибках очень расплывчаты и не способствуют диагностике проблем. Попробуйте изменить код так, чтобы выводить значение PC на момент ошибки, а также GPR, хранимое значение которого отличается.

Сравните значение PC в DiffTest

Поскольку приведённый выше код проверяет только совпадение значений GPR, а инструкция bner0 изменяет только PC, при неверном адресе перехода инструкции bner0 приведённый выше код может вовремя не обнаружить ошибку.

Попробуйте изменить код так, чтобы включить PC в сравнение. Затем попробуйте внести несколько ошибок в реализацию инструкции bner0 и посмотрите, сможет ли DiffTest вовремя их обнаружить.

Поздравляем, вы, по сути, завершили полный процесс проектирования простого CPU!

Новые требования -> Архитектурное проектирование -> Логическое проектирование -> Функциональная верификация -> Оценка схемы
  • Требование к проектированию sCPU — реализовать процессор sISA на RTL.
  • Материал лекций этапа F уже помог всем понять, как реализовать sCPU с помощью функциональности цифровых схем, что по сути завершает архитектурное проектирование, результатом которого является принципиальная схема в Logisim.
  • Процесс проектирования sCPU на RTL — это процесс логического проектирования.
  • Проверка того, может ли ваш RTL-код успешно выполнить 1+2+...+10 с помощью Verilator, — это функциональная верификация; в то время как запуск DiffTest между sCPU и sEMU выполняет функциональную верификацию на более детальном уровне, инструкция за инструкцией.
  • Что касается этапа оценки схемы, он пока не рассматривался. Мы введём его в конце этапа E.

Конечно, функциональность sCPU пока далека от того, чего мы хотим добиться в CPU, поэтому сейчас мы не требуем от всех сосредотачиваться на качестве реализации sCPU. С точки зрения процесса проекта нам нужно сначала спроектировать относительно полноценный CPU, прежде чем задумываться о том, как его оптимизировать, что также соответствует принципу «сначала полнота, потом совершенство». Последующий материал лекций будет следовать этому процессу, направляя всех в том, как проектировать более мощные процессоры для выполнения более сложных программ.