# Лабораторная работа №13 "Периферийные устройства"

В [ЛР№11](../11.%20Interrupt%20integration/) вы закончили реализовывать свой собственный RISC-V процессор. Однако пока что он находится "в вакууме" и никак не связан с внешним миром. Для исправления этого недостатка вами будет реализована системная шина, через которую к процессору смогут подключаться различные периферийные устройства.

## Цель

Интегрировать периферийные устройства в процессорную систему.

## Материалы для подготовки к лабораторной работе

- [Пример описания модуля-контроллера](../../Basic%20Verilog%20structures/Controllers.md);

## Ход работы

1. Изучить теорию об адресном пространстве.
2. Получить индивидуальный вариант со своим набором периферийных устройств.
3. Интегрировать контроллеры периферийных устройств в адресное пространство вашей системы.
4. Собрать финальную схему вашей системы.
5. Проверить работу системы в ПЛИС с помощью демонстрационного ПО, загружаемого в память инструкций.

## Теория

Помимо процессора и памяти, третьим ключевым элементом архитектуры вычислительной машины является **система ввода/вывода**, обеспечивающая обмен информацией между ядром вычислительной машины и разнообразными периферийными устройствами [1, стр. 351].

Любое периферийное устройство со стороны вычислительной машины видится как набор ячеек памяти (регистров). С помощью чтения и записи этих регистров происходит обмен информации с периферийным устройством, и управление им. Например, датчик температуры может быть реализован самыми разными способами, но для процессора он в любом случае ячейка памяти, из которой он считывает число – температуру.

Система ввода/вывода может быть организована одним из двух способов: с **выделенным адресным пространством** устройств ввода/вывода, или с **совместным адресным пространством**. В первом случае система ввода/вывода имеет отдельную шину для подключения к процессору (и отдельные инструкции для обращения к периферии), во втором – шина для памяти и системы ввода/вывода общая (а обращение к периферии осуществляется теми же инструкциями, что и обращение к памяти).

### Адресное пространство

Архитектура RISC-V подразумевает использование совместного адресного пространства — это значит, что в лабораторной работе будет использована единая шина для подключения памяти и регистров управления периферийными устройствами. При обращении по одному диапазону адресов процессор будет попадать в память, при обращении по другим – взаимодействовать с регистрами управления/статуса периферийного устройства. Например, можно разделить 32-битное адресное пространство на 256 частей, отдав старшие 8 бит адреса под указание конкретного периферийного устройства. Тогда каждое из периферийных устройств получит 24-битное адресное пространство (16&nbsp;MiB). Допустим, мы распределили эти части адресного пространства в следующем порядке (от младшего диапазона адресов к старшему):

0. Память данных
1. Переключатели
2. Светодиоды
3. Клавиатура PS/2
4. Семисегментные индикаторы
5. UART-приёмник
6. UART-передатчик
7. Видеоадаптер

В таком случае, если мы захотим обратиться к первому байту семисегментных индикаторов, мы должны будем использовать адрес `0x04000001`. Старшие 8 бит (`0x04`) определяют выбранное периферийное устройство, оставшиеся 24 бита определяют конкретный адрес в адресном пространстве этого устройства.

На _рис. 1_ представлен способ подключения процессора к памяти инструкций и данных, а также 255 периферийным устройствам.

![../../.pic/Labs/lab_13_periph/fig_01.drawio.svg](../../.pic/Labs/lab_13_periph/fig_01.drawio.svg)

_Рисунок 1. Итоговая структура процессорной системы._

> [!IMPORTANT]
> Обратите внимание на то, что на вход `mem_ready_i` модуля `lsu` подаётся единица. Вообще говоря, каждый модуль-контроллер периферийного устройства должен содержать выходной сигнал `ready_o`, который должен мультиплексироваться с остальными подобно тому, как мультиплексируются сигналы read_data_o. На вход `lsu` должен подаваться выход мультиплексора. Однако, поскольку все модули достаточно просты, чтобы, как и у памяти данных, выходной сигнал `ready_o` был всегда равен единице (а также для упрощения _рис. 1_), эти сигналы были убраны из микроархитектуры. В случае, если вы решите добавить в процессорную систему периферийное устройство, сигнал `ready_o` которого не будет равен константной единице, логику управления входом `mem_ready_i` модуля `lsu` будет необходимо обновить описанным выше способом.

### Активация выбранного устройства

В зависимости от интерфейса используемой шины, периферийные устройства либо знают какой диапазон адресов им выделен (например, в интерфейсе I²C), либо нет (интерфейс APB). В первом случае, устройство понимает что к нему обратились непосредственно по адресу в данном обращении, во втором случае — по специальному сигналу.

На _рис. 1_ используется второй вариант — устройство понимает, что к нему обратились по специальному сигналу `req_i`. Данный сигнал формируется из двух частей: сигнала `req` исходящего из процессорного ядра (сигнал о том, что обращение в память вообще происходит) и специального сигнала-селектора исходящего из 256-разрядной шины. Формирование значения на этой шине происходит с помощью [унитарного](https://ru.wikipedia.org/wiki/Унитарный_код) ([one-hot](https://en.wikipedia.org/wiki/One-hot)) кодирования. Процесс кодирования достаточно прост. В любой момент времени на выходной шине должен быть **ровно один** бит, равный единице. Индекс этого бита совпадает со значением числа, формируемого из старших восьми бит адреса. Поскольку для восьмибитного значения существует 256 комбинаций значений, именно такая разрядность будет на выходе кодировщика. Это означает, что в данной системе можно связать процессор с 256 устройствами (одним из которых будет память данных).

Реализация унитарного кодирования предельно проста:

- Нулевой бит этой шины будет равен единице только если `data_addr_o[31:24] = 8'd0`.
- Первый бит этой шины будет равен единице только если `data_addr_o[31:24] = 8'd1`.
- ...
- Двести пятьдесят пятый бит шины будет равен единице только если `data_addr_o[31:24] = 8'd255`.

Для реализации такого кодирования достаточно выполнить сдвиг влево константы `255'd1` на значение `data_addr_o[31:24]`.

### Дополнительные правки модуля processor_system

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

Для этого необходимо добавить в модуль `processor_system` дополнительные входы и выходы, которые подключены посредством файла ограничений к входам и выходам ПЛИС (см. документ "[Как работает ПЛИС](../../Introduction/How%20FPGA%20works.md)").

```Verilog
module processor_system(
  input  logic        clk_i,
  input  logic        resetn_i,

  // Входы и выходы периферии
  input  logic [15:0] sw_i,       // Переключатели

  output logic [15:0] led_o,      // Светодиоды

  input  logic        kclk_i,     // Тактирующий сигнал клавиатуры
  input  logic        kdata_i,    // Сигнал данных клавиатуры

  output logic [ 6:0] hex_led_o,  // Вывод семисегментных индикаторов
  output logic [ 7:0] hex_sel_o,  // Селектор семисегментных индикаторов

  input  logic        rx_i,       // Линия приёма по UART
  output logic        tx_o,       // Линия передачи по UART

  output logic [3:0]  vga_r_o,    // Красный канал vga
  output logic [3:0]  vga_g_o,    // Зелёный канал vga
  output logic [3:0]  vga_b_o,    // Синий канал vga
  output logic        vga_hs_o,   // Линия горизонтальной синхронизации vga
  output logic        vga_vs_o    // Линия вертикальной синхронизации vga

);
//...
endmodule
```

Эти порты нужно подключить к одноимённым портам ваших контроллеров периферии (**речь идёт только о реализуемых вами контроллерах, остальные порты должны остаться неподключенными**). Иными словами, в описании модуля должны быть все указанные входы и выходы. Но использовать вам нужно только порты, связанные с теми периферийными устройствами, реализацию которых вам необходимо подключить к процессорной системе в рамках индивидуального задания.

> [!IMPORTANT]
> Обратите внимание на то, что изменился сигнал сброса (`resetn_i`). Буква `n` на конце означает, что сброс работает по уровню `0` (в таком случае говорят, что **активный уровень** данного сигнала `0`: когда сигнал равен нулю — это сброс, когда единице — не сброс).

Помимо прочего, необходимо подключить к вашему модулю `блок делителя частоты`. Поскольку в данном курсе лабораторных работ вы выполняли реализацию однотактного процессора, инструкция должна пройти через все ваши блоки за один такт. Из-за этого критический путь схемы не позволит использовать тактовый сигнал частотой в `100 МГц`, от которого работает отладочный стенд. Поэтому, необходимо создать отдельный сигнал с пониженной тактовой частотой, от которого будет работать ваша схема.

Для этого необходимо:

1. Подключить файл [`sys_clk_rst_gen.sv`](sys_clk_rst_gen.sv) в ваш проект.
2. Создать экземпляр этого модуля в начале описания модуля `processor_system` следующим образом:

```Verilog
logic sysclk, rst;
sys_clk_rst_gen divider(.ex_clk_i(clk_i),.ex_areset_n_i(resetn_i),.div_i(5),.sys_clk_o(sysclk), .sys_reset_o(rst));
```

_Листинг 1. Пример создания экземпляра блока делителя частоты._

3. После вставки данных строк в начало описания модуля `processor_system` вы получите тактовый сигнал `sysclk` с частотой в 10 МГц и сигнал сброса `rst` с активным уровнем `1` (как и в предыдущих лабораторных). Все ваши внутренние модули (`processor_core`, `data_mem` и контроллеры периферии) должны работать от тактового сигнала `sysclk`. На модули, имеющие входной сигнал сброса (`rst_i`) необходимо подать ваш сигнал `rst`.

## Задание

В рамках данной лабораторной работы необходимо реализовать модули-контроллеры двух периферийных устройств, реализующих управление в соответствии с приведенной в _таблице 1_ картой памяти и встроить их в процессорную систему, используя [_рис. 1_](../../.pic/Labs/lab_13_periph/fig_01.drawio.svg). На карте приведено семь периферийных устройств, вам необходимо взять только два из них. Какие именно — сообщит преподаватель.

![../../.pic/Labs/lab_13_periph/tab_01.png](../../.pic/Labs/lab_13_periph/tab_01.png)

_Таблица 1. Карта памяти периферийных устройств._

Работа с картой осуществляется следующим образом. Под названием каждого периферийного устройства указана старшая часть адреса (чему должны быть равны старшие 8 бит адреса, чтобы было сформировано обращение к данному периферийному устройству). Например, для переключателей это значение равно `0x01`, для светодиодов `0x02` и т.п.
В самом левом столбце указаны используемые/неиспользуемые адреса в адресном пространстве данного периферийного устройства. Например для переключателей есть только один используемый адрес: `0x000000`. Его функциональное назначение и разрешения на доступ указаны в столбце соответствующего периферийного устройства. Возвращаясь к адресу `0x000000`, для переключателей мы видим следующее:

- **(R)** означает что разрешён доступ только на чтение (операция записи по этому адресу должна игнорироваться вашим контроллером).
- **"Выставленное на переключателях значение"** означает ровно то, что и означает. Если процессор выполняет операцию чтения по адресу `0x01000000` (`0x01` [старшая часть адреса переключателей] + `0x000000` [младшая часть адреса для получения выставленного на переключателях значения]), то контроллер должен выставить на выходной сигнал `RD` значение на переключателях (о том, как получить это значение будет рассказано чуть позже).

Рассмотрим ещё один пример. При обращении по адресу `0x02000024` (`0x02` [старшая часть адреса контроллера светодиодов] + `0x000024` [младшая часть адреса для доступа на запись к регистру сброса] ) должна произойти запись в регистр сброса, который должен сбросить значения в регистре управления зажигаемых светодиодов и регистре управления режимом "моргания" светодиодов (подробнее о том как должны работать эти регистры будет далее).

Таким образом, каждый контроллер периферийного устройства должен выполнять две вещи:

1. При получении сигнала `req_i`, записать в регистр или вернуть значение из регистра, ассоциированного с переданным адресом (адрес передаётся с обнуленной старшей частью). Если регистра, ассоциированного с таким адресом нет (например, для переключателей не ассоциировано ни одного адреса кроме `0x000000`), игнорировать эту операцию.
2. Выполнять управление периферийным устройством с помощью управляющих регистров.

Подробное описание периферийных устройств их управления и назначение управляющих регистров описано **после порядка выполнения задания**.

---

## Порядок выполнения задания

1. Ознакомьтесь с [примером описания модуля контроллера](../../Basic%20Verilog%20structures/Controllers.md).
2. Ознакомьтесь со спецификацией контроллеров периферии своего варианта. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
3. Добавьте в проект пакет [`peripheral_pkg`](peripheral_pkg.sv). Данный пакет содержит старшие части адресов периферии в виде параметров, а также вспомогательные вызовы, используемые верификационным окружением.
4. Реализуйте модули контроллеров периферии. Имена модулей и их порты будут указаны в [описании контроллеров](#Описание-контроллеров-периферийных-устройств). Пример разработки контроллера приведен в [примере описания модуля контроллера](../../Basic%20Verilog%20structures/Controllers.md).
   1. Готовые модули периферии, управление которыми должны осуществлять модули-контроллеры хранятся в папке `peripheral modules`.
5. Обновите модуль `processor_system` в соответствии с параграфом ["Дополнительные правки модуля processor_system"](#Дополнительные-правки-модуля-processor_system).
   1. Подключите в проект файл `sys_clk_rst_gen.sv`.
   2. Добавьте в модуль `processor_system` входы и выходы периферии, а также замените вход `rst_i` входом `resetn_i`. **Необходимо добавить порты даже тех периферийных устройств, которые вы не будете реализовывать**.
   3. Создайте в начале описания модуля `processor_system` экземпляр модуля `sys_clk_rst_gen`, скопировав фрагмент кода, приведённый в _листинге 1_.
   4. Замените подключение тактового сигнала исходных подмодулей `processor_system` на появившийся сигнал `sysclk`. Убедитесь, что на модули, имеющие сигнал сброса, приходит сигнал `rst`.
6. Интегрируйте модули контроллеров периферии в процессорную систему по схеме представленной на _рис. 1_, руководствуясь старшими адресами контроллеров, представленными на карте памяти ([_таблицы 1_](../../.pic/Labs/lab_13_periph/tab_01.png)). Это означает, что если вы реализуете контроллер светодиодов, на его вход `req_i` должна подаваться единица в случае, если `mem_req_o == 1` и старшие 8 бит адреса равны `0x02`.
   1. При интеграции вам необходимо подключить только модули-контроллеры вашего варианта. Контроллеры периферии других вариантов подключать не надо.
   2. Во время интеграции, вам необходимо использовать старшую часть адреса, представленную в карте памяти для формирования сигнала `req_i` для ваших модулей-контроллеров.
7. Проверьте работу процессорной системы с помощью моделирования.
   1. Для моделирования используйте тестбенч `lab_13_tb_system`.
   2. Для каждой пары контроллеров в папке `firmware/mem_files` представлены файлы, инициализирующие память инструкций. Содержимым одного из файлов, соответствующих паре периферийных устройств вашего варианта необходимо заменить содержимое файла `program.mem` в `Design Sources` проекта. Обратите внимание, что для пары "PS2-VGA" также необходим файл, инициализирующий память данных — [lab_13_ps2_ascii_data.mem](./firmware/mem_files/lab_13_ps2ascii_data.mem) (в модуле `data_mem` необходимо добавить вызов инициализирующей функции `$readmemh` в блоке `initial`).
   3. Для проверки тестбенч имитирует генерацию данных периферийных устройств ввода. Перед проверкой желательно найти в тестбенче `initial`-блок своего устройства ввода (`sw_block`, `ps2_block`, `uart_block`) — по этому блоку будет понятно, какие данные будет передавать устройство ввода. Именно эти данные в итоге должны оказаться на шине `mem_rd_i`.
   4. Для того, чтобы понять, что устройство работает должным образом, в первую очередь необходимо убедиться, что контроллер устройства ввода успешно осуществил прием данных (сгенерированные тестбенчем данные оказались в соответствующем регистре контроллера периферийного устройства) и выполнил запрос на прерывание.
   5. После чего, необходимо убедиться, что процессор среагировал на данное прерывание, и в процессе его обработки в контроллер устройства вывода были поданы выходные данные.
   6. Для того, чтобы лучше понимать как именно процессор будет обрабатывать прерывание, рекомендуется ознакомиться с исходным кодом исполняемой программы, расположенным в папке `firmware/software`.
      1. Общая логика программ для всех периферий сводится к ожиданию в бесконечном цикле прерывания от устройства ввода, после чего в процессе обработки прерывания процессор загружает данные от устройства ввода и (возможно преобразовав их) выдаёт их на устройство вывода.
      2. В случае правильной работы программы на временной диаграмме это будет отображено следующим образом: сразу после поступления прерывания от устройства ввода, на системной шине начинается операция чтения из устройства ввода (это легко определить по старшей части адреса, к которому обращается процессор), после чего выполняются операции записи в устройство вывода (аналогично, обращение к устройству вывода можно определить по адресу, к которому обращается процессор).
   7. При моделировании светодиодов лучше уменьшить значение, до которого считает счётчик в режиме "моргания" в 1000 раз, чтобы сократить время моделирования до очередного переключения светодиодов. Перед генерацией битстрима это значение будет необходимо восстановить, иначе моргание станет слишком быстрым и его нельзя будет воспринять невооружённым взглядом.

8. Переходить к следующему пункту можно только после того, как вы полностью убедились в работоспособности модуля на этапе моделирования (увидели корректные значения на выходных сигналах периферии, либо (если по сигналам периферии сложно судить о работоспособности), значениям в контрольных/статусных регистрах модуля-контроллера этой периферии). Генерация битстрима будет занимать у вас долгое время, а итогом вы получите результат: заработало / не заработало, без какой-либо дополнительной информации, поэтому без прочного фундамента на моделировании далеко уехать у вас не выйдет.
9. Подключите к проекту файл ограничений ([nexys_a7_100t.xdc](nexys_a7_100t.xdc)), если тот ещё не был подключён, либо замените его содержимое данными из файла к этой лабораторной работе.
10. Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
    1. Обратите внимание, что в данной лабораторной уже не будет модуля верхнего уровня `nexys_...`, так как ваш модуль процессорной системы уже полностью самостоятелен и взаимодействует непосредственно с ножками ПЛИС через модули, управляемые контроллерами периферии.
    2. Для проверки периферии переключателей и светодиодов будет достаточно одного лишь отладочного стенда. Для проверки всей остальной периферии может могут потребоваться: компьютер (для uart_rx / uart_tx), клавиатура (для контроллера клавиатуры) и VGA-монитор для VGA-контроллера.
       1. Чтобы проверить работоспособность контроллеров UART, необходимо запустить на компьютере программу Putty, во вкладке `Connections->Serial` указать настройки, которыми будет сконфигурирован программой ваш контроллер (либо указать значения регистров после сброса) и COM-порт, через который компьютер будет общаться с контроллером. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню кнопки "Пуск".  
      В данном окне необходимо найти вкладку "Порты (COM и LPT)", раскрыть её, а затем подключить отладочный стенд через USB-порт (если тот ещё не был подключён). В списке появится новое устройство, а в скобках будет указан нужный COM-порт.
       2. Несмотря на то, что описанный контроллер клавиатуры позволяет управлять клавиатурой с интерфейсом PS/2, некоторые платы (например, Nexys A7) позволяют подключать вместо них клавиатуры с USB-интерфейсом. Дело в том, что PS/2 уже давно устарел и найти клавиатуры с таким интерфейсом — задача непростая. Однако протокол передачи по этому интерфейсу очень удобен для образовательных целей, поэтому некоторые производители просто ставят на платы переходник с USB на PS/2, позволяя объединить простоту разработки с удобством использования.

## Описание контроллеров периферийных устройств

Для того, чтобы избежать избыточности в контексте описания контроллеров периферийных устройств будет использоваться два термина:

1. Под "**запросом на запись** по адресу `0xАДРЕС`" будет пониматься совокупность следующих условий:
   1. Происходит восходящий фронт `clk_i`.
   2. На входе `req_i` выставлено значение `1`.
   3. На входе `write_enable_i` выставлено значение `1`.
   4. На входе `addr_i` выставлено значение `0xАДРЕС`
2. Под "**запросом на чтение** по адресу `0xАДРЕС`" будет пониматься совокупность следующих условий:
   1. Происходит восходящий фронт `clk_i`.
   2. На входе `req_i` выставлено значение `1`.
   3. На входе `write_enable_i` выставлено значение `0`.
   4. На входе `addr_i` выставлено значение `0xАДРЕС`

> [!IMPORTANT]
> Обратите внимание на то, что **запрос на чтение** должен обрабатываться **синхронно** (выходные данные должны выдаваться по положительному фронту `clk_i`) так же, как был реализован порт на чтение памяти данных в [ЛР№6](../06.%20Main%20memory/).

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

- R — доступ **только на чтение**;
- W — доступ **только на запись**;
- RW — доступ на **чтение и запись**.

В случае отсутствия **запроса на чтение**, на выходе `read_data_o` не должно меняться значение (тоже самое было сделано в процессе разработки памяти данных).

Если пришёл **запрос на запись** или **чтение**, это ещё не значит, что контроллер должен его выполнить. В случае, если запрос происходит по адресу, не поддерживающему этот запрос (например **запрос на запись** по адресу, поддерживающему только чтение), данный запрос должен игнорироваться. В случае **запроса на чтение** по недоступному адресу, на выходе `read_data_o` должно остаться прежнее значение.

В случае осуществления записи по принятому запросу, необходимо записать данные с сигнала `write_data_i` в регистр, ассоциированный с адресом `addr_i` (если разрядность регистра меньше разрядности сигнала `write_data_i`, старшие биты записываемых данных отбрасываются).

В случае осуществления чтения по принятому запросу, необходимо по положительному фронту `clk_i` выставить данные с сигнала, ассоциированного с адресом `addr_i` на выходной сигнал `read_data_o` (если разрядность сигнала меньше разрядности выходного сигнала `read_data_o`, возвращаемые данные должны дополниться нулями в старших битах).

### Переключатели

Переключатели являются простейшим устройством ввода на отладочном стенде `Nexys A7`. Соответственно и контроллер, осуществляющий доступ процессора к ним так же будет очень простым. Рассмотрим прототип модуля, который вам необходимо реализовать:

```Verilog
module sw_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic        clk_i,
  input  logic        rst_i,
  input  logic        req_i,
  input  logic        write_enable_i,
  input  logic [31:0] addr_i,
  input  logic [31:0] write_data_i,  // не используется, добавлен для
                                     // совместимости с системной шиной
  output logic [31:0] read_data_o,

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

  output logic        interrupt_request_o,
  input  logic        interrupt_return_i,

/*
    Часть интерфейса модуля, отвечающая за подключение к периферии
*/
  input logic [15:0]  sw_i
);

endmodule
```

По сути, логика работы контроллера сводится к тому, выдавать на шину `read_data_o` данные со входа `sw_i` каждый раз, когда приходит **запрос на чтение** по нулевому адресу. Поскольку разрядность `sw_i` в два раза меньше разрядности выхода `read_data_o` его старшие биты необходимо дополнить нулями.

Адресное пространство контроллера представлено в _таблице 2_.

|Адрес|Режим доступа|           Функциональное назначение             |
|-----|-------------|-------------------------------------------------|
|0x00 | R           | Чтение значения, выставленного на переключателях|

_Таблица 2. Адресное пространство контроллера переключателей._

При этом, будучи устройством ввода, данный модуль может генерировать прерывание, чтобы сообщить процессору о том, что данные на переключателях были изменены. Если на очередном такте `clk_i` данные на входе `sw_i` изменились (т.е. отличаются от тех, что были на предыдущем такте), модуль должен выставить значение `1` на выходе `interrupt_request_o` и удерживать его до получения сигнала о завершении обработки прерывания `interrupt_return_i`.

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

### Светодиоды

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

```Verilog
module led_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic        clk_i,
  input  logic        rst_i,
  input  logic        req_i,
  input  logic        write_enable_i,
  input  logic [31:0] addr_i,
  input  logic [31:0] write_data_i,
  output logic [31:0] read_data_o,

/*
    Часть интерфейса модуля, отвечающая за подключение к периферии
*/
  output logic [15:0] led_o
);

logic [15:0]  led_val;
logic         led_mode;

endmodule
```

Данный модуль должен подавать на выходной сигнал `led_o` данные с регистра `led_val`. Запись и чтение регистра `led_val` осуществляется по адресу `0x00`.

Регистр `led_mode` отвечает за режим вывода данных на светодиоды. Когда этот регистр равен единице, светодиоды должны "моргать" выводимым значением. Под морганием подразумевается вывод значения из регистра `led_val` на выход `led_o` на одну секунду (загорится часть светодиодов, соответствующие которым биты шины `led_o` равны единице), после чего на одну секунду выход `led_o` необходимо подать нули. Запись и чтение регистра `led_mode` осуществляется по адресу `0x04`.

Отсчёт времени можно реализовать простейшим счётчиком, каждый такт увеличивающимся на 1 и сбрасывающимся по достижении определенного значения, чтобы продолжить считать с нуля. Зная тактовую частоту, нетрудно определить до скольки должен считать счётчик. При тактовой частоте в 10 МГц происходит 10 миллионов тактов в секунду. Это означает, что при такой тактовой частоте через секунду счётчик будет равен `10⁷-1` (счёт идёт с нуля). Тем не менее удобней будет считать не до `10⁷-1` (что было бы достаточно очевидным и тоже правильным решением), а до `2*10⁷-1`. В этом случае результат сравнения значения счётчика с 10⁷ будет меняться каждую секунду, что может быть использовано при реализации логики "моргания".

Важно отметить, что счётчик должен работать только при `led_mode == 1`, в противном случае счётчик должен быть равен нулю.

Обратите внимание на то, что адрес `0x24` является адресом сброса. В случае **запроса на запись** по этому адресу значения `1`, вам необходимо сбросить регистры `led_val`, `led_mode` и все вспомогательные регистры, которые вы создали. Для реализации сброса вы можете как создать отдельный регистр `led_rst`, в который будет происходить запись, а сам сброс будет происходить по появлению единицы в этом регистре (в этом случае необходимо не забыть сбрасывать и этот регистр тоже), так и создать обычный провод, формирующий единицу в случае выполнения всех указанных условий (условий **запроса на запись**, адреса сброса и значения записываемых данных равному единице).

Адресное пространство контроллера представлено в _таблице 3_.

|Адрес|Режим доступа|Допустимые значения|                       Функциональное назначение                                   |
|-----|-------------|-------------------|-----------------------------------------------------------------------------------|
|0x00 | RW          | [0:65535]         | Чтение и запись в регистр `led_val` отвечающий за вывод данных на светодиоды      |
|0x04 | RW          | [0:1]             | Чтение и запись в регистр `led_mode`, отвечающий за режим "моргания" светодиодами |
|0x24 | W           |  1                | Запись сигнала сброса                                                             |

Таблица 3. Адресное пространство контроллера светодиодов.

### Клавиатура PS/2

Клавиатура [PS/2](https://ru.wikipedia.org/wiki/PS/2_(порт)) осуществляет передачу [скан-кодов](https://ru.wikipedia.org/wiki/Скан-код), нажатых на этой клавиатуре клавиш.

В рамках данной лабораторной работы вам будет дан готовый модуль, осуществляющий приём данных с клавиатуры. От вас требуется написать лишь модуль, осуществляющий контроль предоставленным модулем. У готового модуля будет следующий прототип:

```Verilog
module PS2Receiver(
    input        clk_i,          // Сигнал тактирования
    input        rst_i,          // Сигнал сброса
    input        kclk_i,         // Тактовый сигнал, приходящий с клавиатуры
    input        kdata_i,        // Сигнал данных, приходящий с клавиатуры
    output [7:0] keycode_o,      // Сигнал полученного с клавиатуры скан-кода клавиши
    output       keycode_valid_o // Сигнал готовности данных на выходе keycodeout
    );
endmodule
```

Вам необходимо реализовать модуль-контроллер со следующим прототипом:

```Verilog
module ps2_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic         clk_i,
  input  logic         rst_i,
  input  logic [31:0]  addr_i,
  input  logic         req_i,
  input  logic [31:0]  write_data_i,
  input  logic         write_enable_i,
  output logic [31:0]  read_data_o,

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

  output logic        interrupt_request_o,
  input  logic        interrupt_return_i,

/*
    Часть интерфейса модуля, отвечающая за подключение к модулю,
    осуществляющему приём данных с клавиатуры
*/
  input  logic kclk_i,
  input  logic kdata_i
);

logic [7:0] scan_code;
logic       scan_code_is_unread;

endmodule
```

В первую очередь, вам необходимо создать экземпляр модуля `PS2Receiver` внутри вашего модуля-контроллера, соединив соответствующие входы. Для подключения к выходам необходимо создать дополнительные провода.

По каждому восходящему фронту сигнала `clk_i` вам необходимо проверять выход `keycode_valid_o` и, если тот равен единице, записать значение с выхода `keycode_o` в регистр `scan_code`. При этом значение регистра `scan_code_is_unread` необходимо выставить в единицу.

В случае, если произошёл **запрос на чтение** по адресу `0x00`, необходимо выставить на выход `read_data_o` значение регистра `scan_code` (дополнив старшие биты нулями), при этом значение регистра `scan_code_is_unread` необходимо обнулить. В случае, если одновременно с **запросом на чтение** пришёл сигнал `keycode_valid_o`, регистр `scan_code_is_unread` обнулять не нужно (в этот момент в регистр `scan_code` уже записывается новое, ещё непрочитанное значение).

Обнуление регистра `scan_code_is_unread` должно происходить и в случае получения сигнала `interrupt_return_i` (однако опять же, если в этот момент приходит сигнал `keycode_valid_o`, обнулять регистр не нужно).

В случае **запроса на чтение** по адресу `0x04` необходимо вернуть значение регистра `scan_code_is_unread`.

В случае **запроса на запись** значения `1` по адресу `0x24`, необходимо осуществить сброс регистров `scan_code` и `scan_code_is_unread` в `0`.

Регистр `scan_code_is_unread` необходимо подключить к выходу `interrupt_request_o`. Таким образом процессор может узнавать о нажатых клавишах как посредством программного опроса путём чтения значения регистра `scan_code_is_unread`, так и посредством прерываний через сигнал `interrupt_request_o`.

Адресное пространство контроллера представлено в _таблице 4_.

|Адрес|Режим доступа|Допустимые значения|                       Функциональное назначение                                                                   |
|-----|-------------|-------------------|-------------------------------------------------------------------------------------------------------------------|
|0x00 | R           | [0:255]           | Чтение из регистра `scan_code`, хранящего скан-код нажатой клавиши                                                |
|0x04 | R           | [0:1]             | Чтение из регистра `scan_code_is_unread`, сообщающего о том, что есть непрочитанные данные в регистре `scan_code` |
|0x24 | W           |  1                | Запись сигнала сброса                                                                                             |

_Таблица 4. Адресное пространство контроллера клавиатуры._

### Семисегментные индикаторы

Семисегментные индикаторы позволяют выводить арабские цифры и первые шесть букв латинского алфавита, тем самым позволяя отображать шестнадцатеричные цифры. На отладочном стенде `Nexys A7` размещено восемь семисегментных индикаторов. Для вывода цифр на эти индикаторы, вам будет предоставлен модуль `hex_digits`, вам нужно лишь написать модуль, осуществляющий контроль над ним. Прототип модуля `hex_digits` следующий:

```Verilog
module hex_digits(
  input  logic       clk_i,
  input  logic       rst_i,
  input  logic [3:0] hex0_i,    // Цифра, выводимой на нулевой (самый правый) индикатор
  input  logic [3:0] hex1_i,    // Цифра, выводимая на первый индикатор
  input  logic [3:0] hex2_i,    // Цифра, выводимая на второй индикатор
  input  logic [3:0] hex3_i,    // Цифра, выводимая на третий индикатор
  input  logic [3:0] hex4_i,    // Цифра, выводимая на четвёртый индикатор
  input  logic [3:0] hex5_i,    // Цифра, выводимая на пятый индикатор
  input  logic [3:0] hex6_i,    // Цифра, выводимая на шестой индикатор
  input  logic [3:0] hex7_i,    // Цифра, выводимая на седьмой индикатор
  input  logic [7:0] bitmask_i, // Битовая маска для включения/отключения
                                // отдельных индикаторов

  output logic [6:0] hex_led_o, // Сигнал, контролирующий каждый отдельный
                                // светодиод индикатора
  output logic [7:0] hex_sel_o  // Сигнал, указывающий на какой индикатор
                                // выставляется hex_led
);
endmodule
```

Для того, чтобы вывести шестнадцатеричную цифру на любой из индикаторов, необходимо выставить двоичное представление этой цифры на соответствующий вход `hex0-hex7`.

За включение/отключение индикаторов отвечает входной сигнал `bitmask_i`, состоящий из 8 бит, каждый из которых включает/отключает соответствующий индикатор. Например, при `bitmask_i == 8'b0000_0101`, включены будут нулевой и второй индикаторы, остальные будут погашены.

Выходные сигналы `hex_led` и `hex_sel` необходимо просто подключить к соответствующим выходным сигналам модуля-контроллера. Они пойдут на выходы ПЛИС, соединённые с семисегментными индикаторами.

Для управления данным модулем, необходимо написать модуль-контроллер со следующим прототипом:

```Verilog
module hex_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic        clk_i,
  input  logic        rst_i,
  input  logic [31:0] addr_i,
  input  logic        req_i,
  input  logic [31:0] write_data_i,
  input  logic        write_enable_i,
  output logic [31:0] read_data_o,

/*
    Часть интерфейса модуля, отвечающая за подключение к модулю,
    осуществляющему вывод цифр на семисегментные индикаторы
*/
  output logic [6:0] hex_led,
  output logic [7:0] hex_sel
);

  logic [3:0] hex0, hex1, hex2, hex3, hex4, hex5, hex6, hex7;
  logic [7:0] bitmask;
endmodule
```

Регистры `hex0-hex7` отвечают за вывод цифры на соответствующий семисегментный индикатор. Регистр `bitmask` отвечает за включение/отключение семисегментных индикаторов. Когда в регистре `bitmask` бит, индекс которого совпадает с номером индикатора равен единице — тот включён и выводит число, совпадающее со значением в соответствующем регистре `hex0-hex7`. Когда бит равен нулю — этот индикатор гаснет.

Доступ на чтение/запись регистров `hex0-hex7` осуществляется по адресам `0x00-0x1c` (см. таблицу адресного пространства).

Доступ на чтение/запись регистра `bitmask` осуществляется по адресу `0x20`.

При **запросе на запись** единицы по адресу `0x24` необходимо выполнить сброс всех регистров. При этом регистр `bitmask` должен сброситься в значение `0xFF` (т.е. после сброса все семисегментные индикаторы должны загореться с цифрой `0`).

Адресное пространство контроллера представлено в _таблице 5_.

|Адрес|Режим доступа|Допустимые значения|                 Функциональное назначение               |
|-----|-------------|-------------------|---------------------------------------------------------|
|0x00 | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex0           |
|0x04 | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex1           |
|0x08 | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex2           |
|0x0C | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex3           |
|0x10 | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex4           |
|0x14 | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex5           |
|0x18 | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex6           |
|0x1C | RW          | [0:15]            | Регистр, хранящий значение, выводимое на hex7           |
|0x20 | RW          | [0:255]           | Регистр, управляющий включением/отключением индикаторов |
|0x24 | W           |  1                | Запись сигнала сброса                                   |

_Таблица 5. Адресное пространство контроллера семисегментных индикаторов._

### UART

[UART](https://ru.wikipedia.org/wiki/Универсальный_асинхронный_приёмопередатчик) — это последовательный интерфейс, использующий для приёма и передачи данных по одной независимой линии с поддержкой контроля целостности данных.

Для того, чтобы передача данных была успешно осуществлена, приёмник и передатчик на обоих концах одного провода должны договориться о параметрах передачи:

- её скорости (бодрейт);
- контроля целостности данных (использовать или нет [бит чётности](https://en.wikipedia.org/wiki/Parity_bit));
- длины стопового бита.

Вам будут предоставлены модули, осуществляющие приём и передачу данных по этому интерфейсу, от вас лишь требуется написать модули, осуществляющие управление предоставленными модулями.

```Verilog
module uart_rx (
  input  logic            clk_i,      // Тактирующий сигнал
  input  logic            rst_i,      // Сигнал сброса
  input  logic            rx_i,       // Сигнал линии, подключённой к выводу ПЛИС,
                                      // по которой будут приниматься данные
  output logic            busy_o,     // Сигнал о том, что модуль занят приёмом данных
  input  logic [16:0]     baudrate_i, // Настройка скорости передачи данных
  input  logic            parity_en_i,// Настройка контроля целостности через бит чётности
  input  logic [1:0]      stopbit_i,  // Настройка длины стопового бита
  output logic [7:0]      rx_data_o,  // Принятые данные
  output logic            rx_valid_o  // Сигнал о том, что прием данных завершён

);
endmodule
```

```Verilog
module uart_tx (
  input  logic            clk_i,      // Тактирующий сигнал
  input  logic            rst_i,      // Сигнал сброса
  output logic            tx_o,       // Сигнал линии, подключённой к выводу ПЛИС,
                                      // по которой будут отправляться данные
  output logic            busy_o,     // Сигнал о том, что модуль занят передачей данных
  input  logic [16:0]     baudrate_i, // Настройка скорости передачи данных
  input  logic            parity_en_i,// Настройка контроля целостности через бит чётности
  input  logic [1:0]      stopbit_i,  // Настройка длины стопового бита
  input  logic [7:0]      tx_data_i,  // Отправляемые данные
  input  logic            tx_valid_i  // Сигнал о старте передачи данных
);
endmodule
```

Для управления этими модулями вам необходимо написать два модуля-контроллера со следующими прототипами

```Verilog
module uart_rx_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic          clk_i,
  input  logic          rst_i,
  input  logic [31:0]   addr_i,
  input  logic          req_i,
  input  logic [31:0]   write_data_i,
  input  logic          write_enable_i,
  output logic [31:0]   read_data_o,

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

  output logic        interrupt_request_o,
  input  logic        interrupt_return_i,

/*
    Часть интерфейса модуля, отвечающая за подключение передающему,
    входные данные по UART
*/
  input  logic          rx_i
);

  logic busy;
  logic [16:0] baudrate;
  logic parity_en;
  logic [1:0]  stopbit;
  logic [7:0]  data;
  logic valid;

endmodule
```

```Verilog
module uart_tx_sb_ctrl(
/*
    Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
  input  logic          clk_i,
  input  logic          rst_i,
  input  logic [31:0]   addr_i,
  input  logic          req_i,
  input  logic [31:0]   write_data_i,
  input  logic          write_enable_i,
  output logic [31:0]   read_data_o,

/*
    Часть интерфейса модуля, отвечающая за подключение передающему,
    выходные данные по UART
*/
  output logic          tx_o
);

  logic busy;
  logic [16:0] baudrate;
  logic parity_en;
  logic [1:0] stopbit;
  logic [7:0]  data;

endmodule
```

У обоих предоставленных модулей схожий прототип, различия заключаются лишь в направлениях сигналов `data` и `valid`.

Взаимодействие модулей `uart_rx` и `uart_tx` с соответствующими модулями-контроллерами осуществляется следующим образом.

Сигналы `clk_i` и `rx_i`/`tx_o` подключаются напрямую к соответствующим сигналам модулей-контроллеров.

Сигнал `rst_i` модулей `uart_rx` / `uart_tx` должен быть равен единице при **запросе на запись** единицы по адресу `0x24`, а также в случае, когда сигнал `rst_i` модуля-контроллера равен единице.

Выходной сигнал `busy_o` на каждом такте `clk_i` должен записываться в регистр `busy`, доступ на чтение к которому осуществляется по адресу `0x08`.

Значения входных сигналов `baudrate_i`, `parity_en_i`, `stopbit_i` берутся напрямую из соответствующих регистров. Доступ на запись к этим регистрам осуществляется по адресам `0x0C`, `0x10`, `0x14` соответственно, но только в моменты, когда выходной сигнал `busy_o` равен нулю. Иными словами, изменение настроек передачи возможно только в моменты, когда передача не происходит. Доступ на чтение этих регистров может осуществляться в любой момент времени.

В регистр `data` модуля `uart_rx_sb_ctrl` записывается значение одноименного выхода модуля `uart_rx` в моменты положительного фронта `clk_i`, когда сигнал `rx_valid_o` равен единице. Доступ на чтение этого регистра осуществляется по адресу `0x00`.

В регистр `valid` модуля `uart_rx_sb_ctrl` записывается единица по положительному фронту clk_i, когда выход `rx_valid_o` равен единице. Данный регистр сбрасывается в ноль при выполнении **запроса на чтение** по адресу `0x00`, а также при получении сигнала `interrupt_return_i`. Сам регистр доступен для чтения по адресу `0x04`. Регистр `valid` подключается к выходу `interrupt_request_o`. Что позволяет узнать о пришедших данных и посредством прерывания.

Доступ на запись в регистр `data` модуля `uart_tx_sb_ctrl` происходит по адресу `0x00` в моменты положительного фронта `clk_i`, когда сигнал `busy_o` равен нулю. Доступ на чтение этого регистра может осуществляться в любой момент времени.

На вход `tx_data_i` модуля `uart_tx` непрерывно подаётся младший байт входа `write_data_i`.

На вход `tx_valid_i` модуля `uart_tx` подаётся единица в момент выполнения **запроса на запись** по адресу `0x00` (при сигнале `busy` равном нулю). В остальное время на вход этого сигнала подаётся `0`.

В случае **запроса на запись** значения `1` по адресу `0x24` (адресу сброса), все регистры модуля-контроллера должны сброситься. При этом регистр `baudrate` должен принять значение `9600`, регистр `stopbit` должен принять значение `1`. Остальные регистры должны принять значение `0`.

Адресное пространство контроллера `uart_rx_sb_ctrl` представлено в _таблице 6_.

|Адрес|Режим доступа|Допустимые значения|                 Функциональное назначение                                                                  |
|-----|-------------|-------------------|------------------------------------------------------------------------------------------------------------|
|0x00 | R           | [0:255]           | Чтение из регистра `data`, хранящего значение принятых данных                                              |
|0x04 | R           | [0:1]             | Чтение из регистра `valid`, сообщающего о том, что есть непрочитанные данные в регистре `data`             |
|0x08 | R           | [0:1]             | Чтение из регистра `busy`, сообщающего о том, что модуль находится в процессе приема данных                |
|0x0C | RW          | [0:131072]        | Чтение/запись регистра `baudrate`, отвечающего за скорость передачи данных                                 |
|0x10 | RW          | [0:1]             | Чтение/запись регистра `parity_en`, отвечающего за включение отключение проверки данных через бит чётности |
|0x14 | RW          | [1:2]             | Чтение/запись регистра `stopbit`, хранящего длину стопового бита                                           |
|0x24 | W           |  1                | Запись сигнала сброса                                                                                      |

_Таблица 6. Адресное пространство приёмника UART._

Адресное пространство контроллера `uart_tx_sb_ctrl` представлено в _таблице 7_.

|Адрес|Режим доступа|Допустимые значения|                 Функциональное назначение                                                                  |
|-----|-------------|-------------------|------------------------------------------------------------------------------------------------------------|
|0x00 | RW          | [0:255]           | Чтение и запись регистра `data`, хранящего значение отправляемых данных                                    |
|0x08 | R           | [0:1]             | Чтение из регистра `busy`, сообщающего о том, что модуль находится в процессе передачи данных              |
|0x0C | RW          | [0:131072]        | Чтение/запись регистра `baudrate`, отвечающего за скорость передачи данных                                 |
|0x10 | RW          | [0:1]             | Чтение/запись регистра `parity_en`, отвечающего за включение отключение проверки данных через бит чётности |
|0x14 | RW          | [1:2]             | Чтение/запись регистра `stopbit`, хранящего длину стопового бита                                           |
|0x24 | W           |  1                | Запись сигнала сброса                                                                                      |

_Таблица 7. Адресное пространство передатчика UART._

В случае установки регистра `parity_en` в значение `1`, модуль uart_tx будет дополнять посылку битом чётности (который вычисляется как исключающее ИЛИ по всем битам передаваемого байта). Модуль `uart_rx` же будет выполнять проверку этого бита с тем, что он рассчитает самостоятельно. Однако в случае появления ошибки, внешне его поведение никак не изменится (поскольку выход `err_o` данного модуля закомментирован ради простоты системы).

### Видеоадаптер

Видеоадаптер позволяет выводить информацию на экран через интерфейс **VGA**. Предоставляемый в данной лабораторной работе vga-модуль способен выводить `80×30` символов (разрешение символа `8×16`). Таким образом, итоговое разрешение экрана, поддерживаемого vga-модулем будет `80*8 × 30*16 = 640×480`. VGA-модуль поддерживает управление цветовой схемой для каждого поля символа в сетке `80×30`. Это значит, что каждый символ (и фон символа) может быть отрисован отдельным цветом из диапазона 16-ти цветов.

![https://upload.wikimedia.org/wikipedia/commons/c/cf/RebelstarII.png](https://upload.wikimedia.org/wikipedia/commons/c/cf/RebelstarII.png)

_Рисунок 2. Пример игры с использованием символьной графики[[2]](https://en.wikipedia.org/wiki/Rebelstar)._

Для управления выводимым на экран содержимым, адресное пространство модуля разделено на диапазоны, представленные в _таблице 8_.

![../../.pic/Labs/lab_13_periph/tab_08.png](../../.pic/Labs/lab_13_periph/tab_08.png)

_Таблица 8. Адресное пространство контроллера VGA._

Для того, чтобы вывести символ на экран, необходимо использовать адрес этого символа на сетке `80×30` (диапазон адресов `char_map`). К примеру, мы хотим вывести символ в верхнем левом углу (т.е. нулевой символ нулевой строки). Эта позиция расположена по адресу "0" в диапазоне адресов `char_map`. Поскольку данный диапазон начинается с адреса `0x0000_0000`, запись по этому адресу приведёт к отображению символа, соответствующего [ASCII-коду](https://www.asciitable.com/), пришедшему на `write_data_i`.

Если мы хотим вывести нулевой (левый) символ в первой строке (счёт ведётся с нуля), то необходимо произвести запись по адресу `1*80 + 0 = 80 = 0x0000_0050`.

Вывод символа в правом нижнем углу осуществляется записью по адресу `0x0000_095F` (80*30-1)

Установка цветовой схемы осуществляется по тем же самым адресам, к которым прибавлено значение `0x0000_1000`:

- верхний левый символ — `0x0000_1000`
- нулевой символ первой строки — `0x0000_1050`
- нижний правый символ — `0x0000_195F`

Цветовая схема каждой позиции состоит из двух цветов: цвета фона и цвета символа. Оба эти цвета выбираются из палитры 8 цветов, каждый из которых содержит два оттенка: цвет на полной яркости и цвет на уменьшенной яркости (см. _рис. 5_). Один из цветов — черный, оба его оттенка представляют собой один и тот же цвет. На _рис. 5_ приведены коды цветов их rgb-значения:

![../../.pic/Labs/lab_13_periph/fig_03.png](../../.pic/Labs/lab_13_periph/fig_03.png)

_Рисунок 3. Цветовая палитра vga-модуля._

Код цвета формируется следующим образом: старший бит определяет яркость оттенка цвета. Оставшиеся 3 бита кодируют используемый канал:

- 0 бит кодирует использование синего канала;
- 1 бит кодирует использование зелёного канала;
- 2 бит кодирует использование красного канала.

Таким образом, для установки цветовой схемы, необходимо выбрать два цвета из палитры, склеить их (в старших разрядах идёт цвет символа, в младших — цвет фона) и записать получившееся 8-битное значение по адресу выбранной позиции в диапазоне адресов цветовой схемы (color_map).

К примеру, мы хотим установить черный фоновый цвет и белый цвет в качестве цвета символа для верхней левой позиции. В этом случае, мы должны записать значение `f0` (f(15) — код белого цвета, 0 — код черного цвета) по адресу `0x0000_1000` (нулевой адрес в диапазоне `color_map`).

Для отрисовки символов мы условно поделили экран на сетку `80×30`, и для каждой позиции в этой сетке определили фоновый и активный цвет. Чтобы модуль мог отрисовать символ на очередной позиции (которая занимает `16×8` пикселей), ему необходимо знать какой пиксель в этой позиции для заданного ASCII-кода является пикселем фона, а какой — пикселем символа. Для этого используется память шрифтов.

Допустим, нам необходимо отрисовать символ `F` (ascii-код `0x46`).

![../../.pic/Labs/lab_13_periph/fig_04.png](../../.pic/Labs/lab_13_periph/fig_04.png)

_Рисунок 4. Отрисовка символа `F` в разрешении 16х8 пикселей._

Данный символ состоит из 16 строчек по 8 пикселей. Каждый пиксель кодируется одним битом (горит/не горит, цвет символа/фоновый цвет). Каждая строчка кодируется одним байтом (8 бит на 8 пикселей). Таким образом, каждый символ в шрифте требует 16 байт памяти.

Данный модуль поддерживает 256 символов. Следовательно, для хранения шрифта под каждый из 256 символов требуется 16 * 256 = 4&nbsp;KiB памяти.

Для хранения шрифтов в модуле отведён диапазон адресов `0x00002000-0x00002FFF`. В отличие от предыдущих диапазонов адресов, где каждый адрес был закреплён за соответствующей позицией символа в сетке `80×30`, адреса данного диапазона распределены следующим образом:

- 0-ой байт — нулевая (верхняя) строчка символа с кодом 0;
- 1-ый байт — первая строчка символа с кодом 0;
- ...
- 15-ый байт — пятнадцатая (нижняя) строчка символа с кодом 0;
- 16-ый байт — нулевая (верхняя) строчка символа с кодом 1;
- ...
- 4095-ый байт — пятнадцатая (нижняя) строчка символа с кодом 255.

Для простоты работы с модулем вам будут даны файлы, которыми можно проинициализировать память шрифтов и цветов. В этом случае вам будет достаточно только записывать выводимые символы по нужным адресам.

Прототип vga-модуля следующий:

```Verilog
module vgachargen (
  input  logic        clk_i,             // системный синхроимпульс
  input  logic        clk100m_i,         // клок с частотой 100МГц
  input  logic        rst_i,             // сигнал сброса

  /*
      Интерфейс записи выводимого символа
  */
  input  logic        char_map_req_i,    // запрос к памяти выводимых символов
  input  logic [ 9:0] char_map_addr_i,   // адрес позиции выводимого символа
  input  logic        char_map_we_i,     // сигнал разрешения записи кода
  input  logic [ 3:0] char_map_be_i,     // сигнал выбора байтов для записи
  input  logic [31:0] char_map_wdata_i,  // ascii-код выводимого символа
  output logic [31:0] char_map_rdata_o,  // сигнал чтения кода символа

  /*
      Интерфейс установки цветовой схемы
  */
  input  logic        col_map_req_i,     // запрос к памяти цветов символов
  input  logic [ 9:0] col_map_addr_i,    // адрес позиции устанавливаемой схемы
  input  logic        col_map_we_i,      // сигнал разрешения записи схемы
  input  logic [ 3:0] col_map_be_i,      // сигнал выбора байтов для записи
  input  logic [31:0] col_map_wdata_i,   // код устанавливаемой цветовой схемы
  output logic [31:0] col_map_rdata_o,   // сигнал чтения кода схемы

  /*
      Интерфейс установки шрифта.
  */
  input  logic        char_tiff_req_i,   // запрос к памяти шрифтов символов
  input  logic [ 9:0] char_tiff_addr_i,  // адрес позиции устанавливаемого шрифта
  input  logic        char_tiff_we_i,    // сигнал разрешения записи шрифта
  input  logic [ 3:0] char_tiff_be_i,    // сигнал выбора байтов для записи
  input  logic [31:0] char_tiff_wdata_i, // отображаемые пиксели в текущей позиции шрифта
  output logic [31:0] char_tiff_rdata_o, // сигнал чтения пикселей шрифта

  output logic [3:0]  vga_r_o,           // красный канал vga
  output logic [3:0]  vga_g_o,           // зеленый канал vga
  output logic [3:0]  vga_b_o,           // синий канал vga
  output logic        vga_hs_o,          // линия горизонтальной синхронизации vga
  output logic        vga_vs_o           // линия вертикальной синхронизации vga
);
```

Файлы модуля:

- [peripheral modules/vgachargen.sv](peripheral%20modules/vgachargen.sv)
- [peripheral modules/vgachargen_pkg.sv](peripheral%20modules/vgachargen_pkg.sv)
- [firmware/mem_files/lab_13_ps2_vga_instr.mem](firmware/mem_files/lab_13_ps2_vga_instr.mem) — этим файлом необходимо проинициализировать память инструкций
- [firmware/mem_files/lab_13_ps2ascii_data.mem](firmware/mem_files/lab_13_ps2ascii_data.mem) — этим файлом необходимо проинициализировать память данных
- [firmware/mem_files/lab_13_vga_ch_t.mem](firmware/mem_files/lab_13_vga_ch_t.mem)
- [firmware/mem_files/lab_13_vga_ch_map.mem](firmware/mem_files/lab_13_vga_ch_map.mem)
- [firmware/mem_files/lab_13_vga_col_map.mem](firmware/mem_files/lab_13_vga_col_map.mem)

Вам необходимо добавить в проект все эти файлы. Последние три файла отвечают за инициализацию памятей шрифтов[[3](https://fontstruct.com/fontstructions/show/346317/easycode)], символов и цветов. Инициализация будет выполнена автоматически. Главное, чтобы файлы были добавлены в проект.

Для управления данным модулем, необходимо написать модуль-контроллер со следующим прототипом:

```Verilog
module vga_sb_ctrl (
  input  logic        clk_i,
  input  logic        rst_i,
  input  logic        clk100m_i,
  input  logic        req_i,
  input  logic        write_enable_i,
  input  logic [3:0]  mem_be_i,
  input  logic [31:0] addr_i,
  input  logic [31:0] write_data_i,
  output logic [31:0] read_data_o,

  output logic [3:0]  vga_r_o,
  output logic [3:0]  vga_g_o,
  output logic [3:0]  vga_b_o,
  output logic        vga_hs_o,
  output logic        vga_vs_o
);
```

При интеграции модуля в процессорную систему, ко входу `clk100m_i` необходимо подключить провод `clk_i` (а ко входу `clk_i` — провод `sysclk`).

Реализация данного модуля исключительно простая. В первую очередь необходимо подключить одноименные сигналы напрямую:

- `clk_i`,
- `rst_i`,
- `clk100m_i`,
- `vga_r_o`,
- `vga_g_o`,
- `vga_b_o`,
- `vga_hs_o`,
- `vga_vs_o`

Кроме того, необходимо:

1. подключить напрямую сигнал `write_data_i` ко входам:
   1. `char_map_wdata_i`,
   2. `col_map_wdata_i`,
   3. `char_tiff_wdata_i`,
2. подключить биты `addr_i[11:2]` ко входам:
   1. `char_map_addr_i`,
   2. `col_map_addr_i`,
   3. `char_tiff_addr_i`,
3. сигнал `mem_be_i` подключить ко входам:
   1. `char_map_be_i`,
   2. `col_map_be_i`,
   3. `char_tiff_be_i`.

Остается только разобраться с сигналами `req_i`, `write_enable_i` и `read_data_o`.

Все эти сигналы мультиплексируются / демультиплексируются с помощью одного и того же управляющего сигнала: `addr_i[13:12]` в соответствии с диапазонами адресов (рис. 4):

- `addr_i[13:12] == 2'b00`
  - `req_i` подаётся на вход `char_map_req_i`,
  - `write_enable_i` поступает на вход `char_map_we_i`,
  - `char_map_rdata_o` подаётся на выход `read_data_o`;
- `addr_i[13:12] == 2'b01`
  - `req_i` поступает на вход `col_map_req_i`,
  - `write_enable_i` поступает на вход `col_map_we_i`,
  - `col_map_rdata_o` подаётся на выход `read_data_o`;
- `addr_i[13:12] == 2'b10`
  - `req_i` поступает на вход `char_tiff_req_i`,
  - `write_enable_i` поступает на вход `char_tiff_we_i`,
  - `char_tiff_rdata_o` подаётся на выход `read_data_o`.

> [!Important]
> Обратите внимание на то, что контроллер vga является единственным контроллером, для которого не нужно реализовывать регистр перед выходом read_data_o для реализации синхронного чтения. Данная особенность обусловлена тем, что внутри модуля `vgachargen` уже находится блочная память с синхронным портом на чтение. Добавление ещё одного регистра приведёт к тому, что данные будут "опаздывать" на один такт. Таким образом, данные на выход `read_data_o` необходимо подавать с помощью чисто комбинационной логики.

## Список использованной литературы

1. Орлов С.А. Организация ЭВМ и систем: Учебник для вузов. 4-е изд. дополненное и переработанное / С.А. Орлов. - Санкт-Петербург : Питер, 2021. - 688 с.
2. [Rebelstar](https://en.wikipedia.org/wiki/Rebelstar)
3. [Easycode](https://fontstruct.com/fontstructions/show/346317/easycode)
