mirror of
https://github.com/MPSU/APS.git
synced 2025-09-16 01:30:10 +00:00
Синхронизация с правками публикуемого издания (#101)
* СП. Обновление предисловия * СП. Обновление введения * СП. Обновление лаб * СП. Обновление доп материалов * СП. Введение * СП. Введение * СП. ЛР№4, 15 * СП. Базовые конструкции Verilog * Update Implementation steps.md * СП. ЛР 4,5,7,8,14 * СП. ЛР№8 * Синхронизация правок * СП. Финал * Исправление ссылки на рисунок * Обновление схемы * Синхронизация правок * Добавление белого фона .drawio-изображениям * ЛР2. Исправление нумерации рисунка
This commit is contained in:
committed by
GitHub
parent
d251574bbc
commit
9739429d6e
@@ -63,7 +63,7 @@
|
||||
|
||||
_Таблица истинности одноразрядного сложения._
|
||||
|
||||
`S` — это младший разряд 2-битного результата суммы, записываемый в столбце сложения под слагаемыми `a` и `b`. `C` (_carry_, перенос) — это старший разряд суммы, записываемый левее, если произошёл перенос разряда. Как мы видим, перенос разряда происходит только в случае, когда оба числа одновременно равны единице. При этом значение `S` обращается в `0`, и результат записывается как `10`, что в двоичной системе означает `2`. Кроме того, значение `S` равно `0` и в случае, когда оба операнда одновременно равны нулю. Вы можете заметить, что `S` равно нулю в тех случаях, когда `а` и `b` равны, и не равно нулю в противоположном случае. Подобным свойством обладает логическая операция **Исключающее ИЛИ** (**eXclusive OR**, **XOR**):
|
||||
`S` — это младший разряд 2-битного результата суммы, записываемый в столбце сложения под слагаемыми `a` и `b`. `C` (_carry_, перенос) — это старший разряд суммы, записываемый левее, если произошёл перенос разряда. Как мы видим, перенос разряда происходит только в случае, когда оба числа одновременно равны единице. При этом значение `S` обращается в `0`, и результат записывается как `10`, что в двоичной системе означает `2`. Кроме того, значение `S` равно `0` и в случае, когда оба операнда одновременно равны нулю. Вы можете заметить, что `S` равно нулю в тех случаях, когда `а` и `b` равны, и не равно нулю в противоположном случае. Подобным свойством обладает логическая операция **Исключающее ИЛИ** (**eXclusive OR**, **XOR**), именно поэтому одно из других названий этой операции — сумма по модулю 2.
|
||||
|
||||

|
||||
|
||||
@@ -75,7 +75,7 @@ _Таблица истинности операции Исключающее И
|
||||
|
||||
_Таблица истинности операции И._
|
||||
|
||||
Давайте нарисуем цифровую схему, связывающую входные и выходные сигналы с помощью логических элементов, соответствующих ожидаемому поведению:
|
||||
На _рис. 1_ представлена цифровая схема, связывающая входные и выходные сигналы с помощью логических элементов, соответствующих ожидаемому поведению.
|
||||
|
||||

|
||||
|
||||
@@ -91,9 +91,8 @@ _Таблица истинности сигналов полного 1-битн
|
||||
|
||||
Поскольку теперь у нас есть и входной и выходной биты переноса, для их различия добавлены суффиксы "in" и "out".
|
||||
|
||||
Как в таком случае описать S? Например, как `а ^ b ^ Cіn`, где `^` — операция исключающего ИЛИ. Давайте сравним такую операцию с таблицей истинности. Сперва вспомним, что Исключающее ИЛИ — ассоциативная операция [`(a^b)^c = a^(b^с)`], т.е. нам не важен порядок вычисления. Предположим, что Cin равен нулю. Исключающее ИЛИ с нулем дает второй операнд (`a^0=a`), значит `(a^b)^0 = a^b`. Это соответствует верхней половине таблицы истинности для сигнала S, когда Cin равен нулю.
|
||||
|
||||
Предположим, что Cin равен единице. Исключающее ИЛИ с единицей дает нам отрицание второго операнда (`a^1=!a`), значит `(a^b)^1=!(a^b)`. Это соответствует нижней половине таблицы истинности, когда Cin равен единице.
|
||||
Как в таком случае описать S? Например, как сумму по модулю 2 этих трёх слагаемых: `а ⊕ b ⊕ Cіn`. Давайте сравним такую операцию с таблицей истинности. Сумма по модуля 2 — это ассоциативная операция [`(a⊕b)⊕c = a⊕(b⊕с)`], т.е. порядок сложения не влияет на результат. Предположим, что Cin равен нулю. Сумма по модулю 2 с нулём даёт второй операнд (`a⊕0=a`), значит `(a⊕b)⊕0 = a⊕b`. Это соответствует верхней половине таблицы истинности для сигнала S, когда Cin равен нулю.
|
||||
Предположим, что Cin равен единице. Сумма по модулю 2 с единицей даёт нам отрицание второго операнда (`a⊕1=!a`), значит `(a⊕b) ⊕1=!(a⊕b)`. Это соответствует нижней половине таблицы истинности, когда Cin равен единице.
|
||||
|
||||
Для выходного бита переноса всё гораздо проще. Он равен единице, когда хотя бы два из трех операндов равны единице, это значит, что необходимо попарно сравнить все операнды, и если найдется хоть одна такая пара, он равен единице. Это утверждение можно записать следующим образом:
|
||||
|
||||
@@ -111,8 +110,6 @@ _Рисунок 2. Цифровая схема полного 1-битного
|
||||
|
||||
Модуль `half_adder` имеет два входных сигнала и два выходных. Входы `a_i` и `b_i` идут на два логических элемента: Исключающее ИЛИ и И, выходы которых подключены к выходам модуля `sum_o` и `carry_o` соответственно.
|
||||
|
||||
|
||||
|
||||
```Verilog
|
||||
module half_adder(
|
||||
input logic a_i, // Входные сигналы
|
||||
@@ -142,7 +139,7 @@ _Рисунок 3. Цифровая схема модуля half_adder, сген
|
||||
|
||||
Подаваемые на схему входные воздействия формируются верификационным окружением. Верификационное окружение (в дальнейшем будет использован термин "**тестбенч**") — это особый несинтезируемый модуль, который не имеет входных или выходных сигналов. Ему не нужны входные сигналы, поскольку он сам является генератором всех своих внутренних сигналов, и ему не нужны выходные сигналы, поскольку этот модуль ничего не вычисляет, только подаёт входные воздействия на проверяемый модуль.
|
||||
|
||||
Внутри тестбенча можно использовать конструкции из несинтезируемого подмножества языка SystemVerilog, в частности программный блок `initial`, в котором команды выполняются последовательно, что делает этот блок чем-то отдаленно похожим на проверяющую программу. Поскольку изменение внутренних цепей происходит с некоторой задержкой относительно изменений входных сигналов, при моделировании есть возможность делать паузы между командами. Это делается с помощью специального символа #, за которым указывается количество времени симуляции, которое нужно пропустить перед следующей командой.
|
||||
Внутри тестбенча можно использовать конструкции из несинтезируемого подмножества языка SystemVerilog, в частности программный блок `initial`, в котором команды выполняются последовательно, что делает этот блок чем-то отдалённо похожим на проверяющую программу. Поскольку изменение внутренних цепей происходит с некоторой задержкой относительно изменений входных сигналов, при моделировании есть возможность делать паузы между командами. Это делается с помощью специального символа #, за которым указывается количество времени симуляции, которое нужно пропустить перед следующей командой.
|
||||
|
||||
Перед тем как писать верификационное окружение, необходимо составить план того, как будет проводиться проверка устройства (составить верификационный план). Ввиду предельной простоты устройства, план будет состоять из одного предложения:
|
||||
|
||||
@@ -193,7 +190,7 @@ _Рисунок 5. Схема 4-битного сумматора._
|
||||
|
||||
Как же реализовать модуль, состоящий из цепочки других модулей? Половину этой задачи мы уже сделали, когда писали тестбенч к 1-битному полусумматору в _Листинге 2_ — мы создавали модуль внутри другого модуля и подключали к нему провода. Теперь надо сделать то же самое, только с чуть большим числом модулей.
|
||||
|
||||
Для того, чтобы описать 4-битный сумматор, необходимо подключить четыре 1-битных подобно тому, как было описано в [`документе`](../../Basic%20Verilog%20structures/Modules.md#иерархия-модулей), который вы изучали перед лабораторной работой.
|
||||
Описание 4-битного сумматора, сводится к описанию межсоединения четырёх экземпляров 1-битного сумматора. Подробней о том, как описывать создание экземпляров модулей рассказано в главе [Описание модулей на языке SystemVerilog](../../Basic%20Verilog%20structures/Modules.md#иерархия-модулей), который вы изучали перед лабораторной работой.
|
||||
|
||||

|
||||
|
||||
@@ -243,13 +240,13 @@ module fulladder4(
|
||||
|
||||
либо же можно создать массив 1-битных сумматоров.
|
||||
|
||||
Создание массива модулей схоже с созданием одного модуля за исключением того, что после имени сущности модуля указывается диапазон, определяющий количество модулей в массиве. При этом подключение сигналов к массиву модулей осуществляется следующим образом:
|
||||
Создание массива модулей схоже с созданием одного модуля за исключением того, что после имени экземпляра модуля указывается диапазон, определяющий количество модулей в массиве. При этом подключение сигналов к массиву модулей осуществляется следующим образом:
|
||||
|
||||
- если разрядность подключаемого сигнала совпадает с разрядностью порта модуля из массива, этот сигнал подключается к каждому из модулей в массиве;
|
||||
- если разрядность подключаемого сигнала превосходит разрядность порта модуля из массива в `N` раз (где `N` — количество модулей в массиве), к модулю подключается соответствующий диапазон бит подключаемого сигнала (диапазон младших бит будет подключён к модулю с меньшим индексом в массиве).
|
||||
- если разрядность подключаемого сигнала не подходит ни под один из описанных выше пунктов, происходит ошибка синтеза схемы, поскольку в этом случае САПР не способен понять каким образом подключать данный сигнал к каждому модулю из массива.
|
||||
|
||||
Далее идёт пример того, как можно создать массив модулей:
|
||||
В _листинге 3_ представлен пример того, как можно создать массив модулей.
|
||||
|
||||
```Verilog
|
||||
module example1(
|
||||
|
@@ -24,35 +24,27 @@
|
||||
|
||||
**Арифметико-логическое устройство** (**АЛУ**, Arithmetic Logic Unit – ALU) – это блок процессора, выполняющий арифметические и поразрядно логические операции. Разница между арифметическими и логическими операциями в отсутствии у последних бита переноса, так как логические операции происходят между 1-битными числами и дают 1-битный результат, а в случае АЛУ (в рамках данной лабораторной работы) одновременно между 32-мя 1-битными парами чисел. В логических операциях результаты значений отдельных битов друг с другом никак не связаны.
|
||||
|
||||
Также, кроме результата операций, АЛУ формирует флаги, которые показывают выполняется ли заданное условие. Например, выведет `1`, если один операнд меньше другого.
|
||||
Также, кроме результата операций, может формировать специальные сигналы-флаги, которые показывают выполняется ли заданное условие. Например, выводить `1`, если один операнд меньше другого, или если в результате суммы произошло переполнение.
|
||||
|
||||
Обычно АЛУ представляет собой комбинационную схему (то есть не имеет элементов памяти), на входы которой поступают информационные (операнды) и управляющие (код операции) сигналы, в ответ на что на выходе появляется результат заданной операции. АЛУ бывает не комбинационной схемой, но это скорее исключение.
|
||||
Обычно АЛУ представляет собой комбинационную схему (то есть не имеет элементов памяти), на входы которой поступают информационные (операнды) и управляющие (код операции) сигналы, в ответ на что на выходе появляется результат заданной операции. АЛУ может быть реализовано и в виде последовательностной логики, но это скорее исключение.
|
||||
|
||||

|
||||
|
||||
_Рисунок 1. Структурное обозначение элемента АЛУ[[1, стр. 305]](https://reader.lanbook.com/book/97336?lms=57991a6f83ced8530d7f0759ce4b95b7)._
|
||||
_Рисунок 1. Структурное обозначение элемента АЛУ[[1, стр. 304]](https://reader.lanbook.com/book/241166?lms=1b8d65a957786d4b32b8201bd30e97f3)._
|
||||
|
||||
На рис. 1 изображен пример АЛУ, используемый в книге "Цифровая схемотехника и архитектура компьютера" Харрис и Харрис. На входы `A` и `B` поступают операнды с разрядностью _N_. На 3-битный вход `F` подается код операции. Например, если туда подать `000`, то на выходе `Y` появится результат операции _логическое И_ между битами операндов `A` и `B`. Если на `F` подать `010`, то на выходе появится результат сложения. Это лишь пример, разрядность и коды могут отличаться в зависимости от количества выполняемых операций и архитектуры.
|
||||
На рис. 1 изображено структурное обозначение АЛУ, используемое в книге "Цифровая схемотехника и архитектура компьютера RISC-V" Харрис и Харрис. На входы `A` и `B` поступают операнды с разрядностью _N_. На 2-битный вход `ALUControl` подается код операции. Например, если туда подать `10`, то на выходе `Y` появится результат операции _логическое И_ между битами операндов `A` и `B`. Если же подать `00`, то на выходе появится результат сложения. Это лишь пример, разрядность и коды могут отличаться в зависимости от количества выполняемых операций и архитектуры.
|
||||
|
||||
Существует несколько подходов к реализации АЛУ, отличающиеся внутренней организацией. В лабораторных работах применяется повсеместно используемый подход мультиплексирования операций, то есть подключения нескольких операционных устройств (которые выполняют какие-то операции, например сложения, логического ИЛИ и т.п.) к мультиплексору, который будет передавать результат нужного операционного устройства на выходы АЛУ.
|
||||
|
||||
Рассмотрим данный подход на примере все того же АЛУ MIPS из книги Харрисов. На рис. 2, в левой его части, изображена внутренняя организация этого АЛУ, справа – таблица соответствия кодов операциям. На выходе схемы (внизу) стоит 4-входовой мультиплексор, управляемый двумя из трех битов `F`. К его входам подключены _N_ логических И (побитовое И _N_-битных операндов), _N_ логических ИЛИ, _N_-битный сумматор и Zero Extend – устройство, дополняющее слева нулями 1-битное число до N-битного.
|
||||
Рассмотрим данный подход на примере всё того же АЛУ из книги Харрисов. На рис. 2, в левой его части, изображена внутренняя организация этого АЛУ, справа – таблица соответствия кодов операциям. На выходе схемы (внизу) стоит 4-входовой мультиплексор, управляемый 2-битным сигналом `ALUControl`. Ко входам мультиплексора подключены выходы `N` логических И (побитовое И N-битных операндов), `N` логических ИЛИ, а выход N-битного сумматора подключён дважды, позволяя выбирать его результат для двух кодов операции.
|
||||
|
||||

|
||||
|
||||
_Рисунок 2. Структурная схема АЛУ MIPS[[1, стр. 305]](https://reader.lanbook.com/book/97336?lms=57991a6f83ced8530d7f0759ce4b95b7)._
|
||||
_Рисунок 2. Структурная схема АЛУ [[1, стр. 305]](https://reader.lanbook.com/book/241166?lms=1b8d65a957786d4b32b8201bd30e97f3)._
|
||||
|
||||
К одному из входов этих операционных устройств подключен без изменений вход `A`, а ко второму подключен выход двухвходового мультиплексора, управляемого оставшимся битом _F_. То есть `F[2]` определяет, что будет вторым операндом: `B` или `~B`. Вдобавок `F[2]` подается на входной перенос сумматора, то есть, когда `F[2] == 1` на выходе сумматора появляется результат операции `A + ~B + 1`, что (с учетом [дополнительного кода](https://ru.wikipedia.org/wiki/Дополнительный_код)) эквивалентно `A – B`.
|
||||
Вход `A` подключается напрямую ко входам этих операционных устройств. Вход `B` тоже — за исключением подключения к сумматору. На вход второго операнда сумматора подаётся результат мультиплексирования сигналов `B` и `~B`. Управляющим сигналом этого мультиплексора является младший бит управляющего сигнала `ALUControl`. Кроме того, этот же младший бит подаётся и на сумматор в качестве входного бита переноса. Это означает, что когда `ALUControl[0] = 0`, вычисляется сумма `A + B + 0`, а когда `ALUControl[0] = 1`, вычисляется сумма `A + ~B + 1`, что (с учётом [дополнительного кода](https://ru.wikipedia.org/wiki/Дополнительный_код) ) эквивалентно `A – B`.
|
||||
|
||||
Посмотрим, что произойдет, если на вход `F` такого АЛУ подать `111`. Будет выполняться операция `SLT`(сокращение от `Set Less Then`) – выдать `1`, если `A` меньше `B`, в противном случае — выдать `0`. Биты `F[1:0]` переключат мультиплексор на выход блока Zero Extend. На вход Zero Extend поступает старший бит выхода сумматора, этот бит отвечает за знак результата. Так как `F[2] == 1`, сумматор вычисляет `A + ~B + 1`, то есть `A – B`. Если `A < B`, то результат вычитания будет отрицательный, а старший бит `Y[N-1] == 1`. Если `A` не меньше `B`, то разность будет неотрицательна, а `Y[N-1] == 0`, как и требуется от этой операции.
|
||||
|
||||
Сравнение двух чисел несколько сложнее чем просто проверка старшего бита разности и зависит от того, сравниваем ли мы знаковые числа или беззнаковые. Если знаковые — то произошло ли переполнение. Для простоты схемы, принято, что схема реализует операцию SLT для знаковых пар чисел, разность которых не вызывает переполнения [[2, 307]](https://reader.lanbook.com/book/241166?lms=1b8d65a957786d4b32b8201bd30e97f3).
|
||||
|
||||

|
||||
|
||||
_Рисунок 3. Пример исполнения операции АЛУ._
|
||||
|
||||
Преимущество такой организации АЛУ в его простой модификации, настройке под нужные коды операций, читаемости кода и масштабируемости. Можно легко добавить или убрать требуемые операции. Подумайте, как бы вы обновили данную схему, если бы от вас потребовалось расширить её функционал операциями XOR (Исключающее ИЛИ) и (SGE операция "больше либо равно")?
|
||||
Преимущество такой организации АЛУ в простоте его модификации, настройке под нужные коды операций, читаемости кода и масштабируемости. Можно легко добавить или убрать требуемые операции. Подумайте, как бы вы обновили данную схему, если бы от вас потребовалось расширить её функционал операциями XOR (Исключающее ИЛИ) и (SGE операция "больше либо равно")?
|
||||
|
||||
## Инструменты
|
||||
|
||||
@@ -64,7 +56,7 @@ _Рисунок 3. Пример исполнения операции АЛУ._
|
||||
|
||||
Очень удобным на практике оказывается использование параметров. Параметры добавляют модулю гибкости, позволяя убрать ["магические"](https://ru.wikipedia.org/wiki/Магическое_число_(программирование)#Плохая_практика_программирования) константы из описания модулей, подставляя вместо них выразительное символьное имя. Параметры отдаленно схожи с макросами `#define` в языке Си, однако стоит понимать, что это не одно и то же. Дефайны представляют собой специальные текстовые макросы, которые автоматически заменяются на этапе препроцессора (как если бы вы прошлись по всем файлам своего кода и вручную заменили бы макросы на их значения). Например, с помощью дефайнов можно писать целые куски кода, а не просто одно какое-то число. При этом у дефайнов глобальная область видимости (объявив их в одном месте, этот макрос будет доступен во всем последующем коде). Параметр в свою очередь может хранить только значение какого-то конкретного типа (т.е. в параметр нельзя поместить фрагмент кода) а область видимости параметра ограничена тем модулем, где он был объявлен.
|
||||
|
||||
Допустим, ваше устройство должно включить тостер, если на вход ему придет сигнал `32'haf3c5bd0`. Человек, не знакомый с устройством, при прочтении этого кода будет недоумевать, что это за число и почему используется именно оно. Однако, скрыв его за параметром `TOASTER_EN`, читающий поймет, что это код включения тостера. Кроме того, если некоторая константа должна использоваться в нескольких местах кода, то определив её через в виде параметра, можно будет менять её в одном месте, и она тут же поменяется везде.
|
||||
Допустим, ваше устройство должно включить тостер, если на вход ему придет сигнал `32'haf3c5bd0`. Человек, не знакомый с устройством, при прочтении этого кода будет недоумевать, что это за число и почему используется именно оно. Однако, скрыв его за параметром `TOASTER_EN`, читающий поймет, что это код включения тостера. Кроме того, если некоторая константа должна использоваться в нескольких местах кода, то определив её в виде параметра, можно будет менять её в одном месте, и она тут же поменяется везде.
|
||||
|
||||
Параметры позволяют влиять на структуру модуля. К примеру, описывая сумматор, можно параметризовать его разрядность и использовать этот параметр при описании модуля (например, в качестве диапазона массива модулей). В этом случае вы сможете создавать множество сумматоров различных разрядностей, подставляя при создании нужное вам значение параметра.
|
||||
|
||||
@@ -83,10 +75,10 @@ module overflow #(parameter WIDTH = 32)(
|
||||
output logic overflow
|
||||
);
|
||||
|
||||
logic [WIDTH : 0] sum;
|
||||
logic [WIDTH : 0] sum;
|
||||
|
||||
assign sum = a + b;
|
||||
assign overflow = sum[WIDTH];
|
||||
assign sum = a + b;
|
||||
assign overflow = sum[WIDTH];
|
||||
|
||||
endmodule
|
||||
```
|
||||
@@ -101,8 +93,8 @@ module toaster(
|
||||
output logic power
|
||||
)
|
||||
|
||||
parameter TOASTER_EN = 32'haf3c5bd0;
|
||||
assign power = command == TOASTER_EN;
|
||||
parameter TOASTER_EN = 32'haf3c5bd0;
|
||||
assign power = command == TOASTER_EN;
|
||||
|
||||
endmodule
|
||||
```
|
||||
@@ -181,19 +173,12 @@ endmodule
|
||||
- `>>` — логический сдвиг вправо
|
||||
- `>>>` — арифметический сдвиг вправо
|
||||
|
||||
<br><br><br><br><br>
|
||||
|
||||
---
|
||||
|
||||
### Особенности реализации сдвига
|
||||
|
||||
**Для ВСЕХ операций сдвига вы должны брать только 5 младших бит операнда B.**
|
||||
|
||||
Сами посмотрите: пятью битами можно описать 32 комбинации [0-31], а у операнда А будет использоваться ровно 32 бита. **Это обязательное требование**, поскольку старшие биты в дальнейшем будут использоваться по другому назначению и, если вы упустите это, ваш будущий процессор станет работать неправильно.
|
||||
|
||||
---
|
||||
|
||||
<br><br><br><br><br>
|
||||
> [!IMPORTANT]
|
||||
> Для **ВСЕХ** операций сдвига вы должны брать только 5 младших бит операнда B.
|
||||
>
|
||||
> Сами посмотрите: выполнять операцию сдвига более чем на 31 для 32-битных чисел не имеет смысла, число полностью заполнится нулями (единицами). Т.е. сдвигая на любое число, большее 31, вы получите один и тот же результат. Для того чтобы закодировать 31 требуется минимум 5 бит, отсюда и это требование. Оно обязательно, поскольку старшие биты в дальнейшем будут использоваться по другому назначению и, если вы упустите это, ваш будущий процессор станет работать неправильно.
|
||||
|
||||
## Задание
|
||||
|
||||
@@ -209,7 +194,7 @@ module alu (
|
||||
output logic [31:0] result_o
|
||||
);
|
||||
|
||||
import alu_opcodes_pkg::*; // импорт параметров, содержащих
|
||||
import alu_opcodes_pkg::*; // импорт параметров, содержащих
|
||||
// коды операций для АЛУ
|
||||
|
||||
endmodule
|
||||
@@ -219,33 +204,33 @@ endmodule
|
||||
|
||||
Для удобства чтения, список инструкций разбит на две таблицы.
|
||||
|
||||
В первой таблице перечислены операции, вычисляющие значение сигнала `result_o`. **При любом коде операции `alu_op_i` не входящим в эту таблицу, сигнал `result_o` должен быть равен нулю**.
|
||||
В первой таблице перечислены операции, вычисляющие значение сигнала `result_o`. **При любом коде операции, не входящим в эту таблицу, сигнал `result_o` должен быть равен нулю**.
|
||||
|
||||
|alu_op_i|={cmp, add/sub, alu_op_i}|result_o |Операция |
|
||||
|--------|-------------------------|-------------------------------------------|-------------------------------------------------------|
|
||||
| ADD | 0 0 000 |result_o = a_i + b_i | Сложение |
|
||||
| SUB | 0 1 000 |result_o = a_i – b_i | Вычитание |
|
||||
| SLL | 0 0 001 |result_o = a_i << b_i | Сдвиг влево |
|
||||
| SLTS | 0 0 010 |result_o = a_i < b_i (знаковое сравнение)| Знаковое сравнение |
|
||||
| SLTU | 0 0 011 |result_o = a_i < b_i | Беззнаковое сравнение |
|
||||
| XOR | 0 0 100 |result_o = a_i ^ b_i | Побитовое исключающее **ИЛИ** |
|
||||
| SRL | 0 0 101 |result_o = a_i >> b_i | Сдвиг вправо |
|
||||
| SRA | 0 1 101 |result_o = a_i >>> b_i | Арифметический сдвиг вправо (операнд `a_i` — знаковый)|
|
||||
| OR | 0 0 110 |result_o = a_i \| b_i | Побитовое логическое **ИЛИ** |
|
||||
| AND | 0 0 111 |result_o = a_i & b_i | Побитовое логическое **И** |
|
||||
|Операция|={cmp, mod, opcode}|Выражение | Действие |
|
||||
|--------|-------------------|-----------------------|-------------------------------------------------------|
|
||||
| ADD | 0 0 000 |result_o = a_i + b_i | Сложение |
|
||||
| SUB | 0 1 000 |result_o = a_i – b_i | Вычитание |
|
||||
| SLL | 0 0 001 |result_o = a_i << b_i | Сдвиг влево |
|
||||
| SLTS | 0 0 010 |result_o = a_i < b_i | **Знаковое** сравнение |
|
||||
| SLTU | 0 0 011 |result_o = a_i < b_i | **Беззнаковое** сравнение |
|
||||
| XOR | 0 0 100 |result_o = a_i ^ b_i | Побитовое исключающее **ИЛИ** |
|
||||
| SRL | 0 0 101 |result_o = a_i >> b_i | Сдвиг вправо |
|
||||
| SRA | 0 1 101 |result_o = a_i >>> b_i | Арифметический сдвиг вправо (операнд `a_i` — знаковый)|
|
||||
| OR | 0 0 110 |result_o = a_i \| b_i | Побитовое логическое **ИЛИ** |
|
||||
| AND | 0 0 111 |result_o = a_i & b_i | Побитовое логическое **И** |
|
||||
|
||||
_Таблица 1. Список вычислительных операций._
|
||||
|
||||
Во второй таблице перечислены операции, вычисляющие значение сигнала `flag_o`. **При любом коде операции `alu_op_i` не входящим в эту таблицу, сигнал `flag_o` должен быть равен нулю**.
|
||||
Во второй таблице перечислены операции, вычисляющие значение сигнала `flag_o`. **При любом коде операции, не входящим в эту таблицу, сигнал flag_o должен быть равен нулю**.
|
||||
|
||||
|alu_op_i|={cmp, add/sub, alu_op_i}| flag_o | Операция |
|
||||
|--------|-------------------------|------------------------------------------|-----------------------------------|
|
||||
| EQ | 1 1 000 | flag_o = (a_i == b_i) | Выставить флаг, если **равны** |
|
||||
| NE | 1 1 001 | flag_o = (a_i != b_i) | Выставить флаг, если **не равны** |
|
||||
| LTS | 1 1 100 | flag_o = a_i < b_i (знаковое сравнение)| Знаковое сравнение **<** |
|
||||
| GES | 1 1 101 | flag_o = a_i ≥ b_i (знаковое сравнение)| Знаковое сравнение **≥** |
|
||||
| LTU | 1 1 110 | flag_o = a_i < b_i | Беззнаковое сравнение **<** |
|
||||
| GEU | 1 1 111 | flag_o = a_i ≥ b_i | Беззнаковое сравнение **≥** |
|
||||
|Операция|={cmp, mod, opcode}| Выражение | Действие |
|
||||
|--------|-------------------|----------------------|-----------------------------------|
|
||||
| EQ | 1 1 000 | flag_o = (a_i == b_i)| Выставить флаг, если **равны** |
|
||||
| NE | 1 1 001 | flag_o = (a_i != b_i)| Выставить флаг, если **не равны** |
|
||||
| LTS | 1 1 100 | flag_o = a_i < b_i | **Знаковое** сравнение **<** |
|
||||
| GES | 1 1 101 | flag_o = a_i ≥ b_i | **Знаковое** сравнение **≥** |
|
||||
| LTU | 1 1 110 | flag_o = a_i < b_i | **Беззнаковое** сравнение **<** |
|
||||
| GEU | 1 1 111 | flag_o = a_i ≥ b_i | **Беззнаковое** сравнение **≥** |
|
||||
|
||||
_Таблица 2. Список операций сравнения._
|
||||
|
||||
@@ -280,16 +265,16 @@ _Таблица 2. Список операций сравнения._
|
||||
Конструкция `$signed` говорит САПР интерпретировать число, переданное в качестве операнда, как знаковое.
|
||||
|
||||
```Verilog
|
||||
assign Result = $signed(A) >>> B[4:0];
|
||||
assign Result = $signed(A) / 10;
|
||||
```
|
||||
|
||||
В этом примере некоторому сигналу `Result` присваивают результат сдвига знакового числа `A` на значение количества бит получаемых из младших 5 бит сигнала `B`.
|
||||
В этом примере некоторому сигналу `Result` присваивают результат деления **знакового** числа `A` на `10`.
|
||||
|
||||
Так как используются не все возможные комбинации управляющего сигнала АЛУ, то **при описании через `case` не забывайте использовать `default`**. Если описать АЛУ как задумано, то получится что-то похожее на _рис. 4_. Но не обязательно, зависит от вашего описания.
|
||||
|
||||

|
||||
|
||||
_Рисунок 4. Пример схемы, реализующей АЛУ._
|
||||
_Рисунок 3. Пример схемы, реализующей АЛУ._
|
||||
|
||||
### Порядок выполнения задания
|
||||
|
||||
|
@@ -33,11 +33,11 @@
|
||||
|
||||
В общем случае `V = 2^a * d`.
|
||||
|
||||
Для объема памяти в 1 KiB ([кибибайт](https://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D0%B1%D0%B8%D0%B1%D0%B0%D0%B9%D1%82), 1024 байта или 8192 бита) разрядность адреса может быть, например, 10 бит (что покрывает 2^10 = 1024 адреса), тогда разрядность хранимых данных должна быть 8 бит. 1024 * 8 = 8192, то есть 1 кибибайт. Если разрядность адреса составляет 8 бит (что покрывает 2^8 = 256 адресов), то разрядность данных `d = V / 2^a` это 8192 / 256 = 32 бита.
|
||||
Для объема памяти в 1 KiB ([кибибайт](https://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D0%B1%D0%B8%D0%B1%D0%B0%D0%B9%D1%82), 1024 байта или 8192 бита) разрядность адреса может быть, например, 10 бит (что покрывает 2<sup>10</sup> = 1024 адреса), тогда разрядность хранимых данных должна быть 8 бит. 1024 * 8 = 8192, то есть 1 кибибайт. Если разрядность адреса составляет 8 бит (что покрывает 2<sup>8</sup> = 256 адресов), то разрядность данных `d = V / 2^a` это 8192 / 256 = 32 бита.
|
||||
|
||||
Однако, может быть такое, что не все ячейки памяти реализованы на кристалле микросхемы, то есть некоторые адреса существуют, но по ним не имеет смысла обращаться, а объем памяти, соответственно, не равен `V ≠ 2^a * d` — он меньше.
|
||||
|
||||
Память можно разделить на категории: ПЗУ (постоянное запоминающее устройство) и ОЗУ (оперативное запоминающее устройство). Из ПЗУ можно только считывать информацию, которая попадает в него до начала использования памяти и не может изменяться в процессе работы. Из ОЗУ можно считывать и записывать информацию. В самом простом случае ПЗУ имеет один вход адреса `addr` и один выход считываемых данных `read_data`. На вход `addr` подается адрес требуемой ячейки памяти, на выходе `read_data` появляются данные, которые хранятся по этому адресу.
|
||||
Память можно разделить на две основные категории: ПЗУ (постоянное запоминающее устройство) и ОЗУ (оперативное запоминающее устройство). ПЗУ предназначено для хранения информации, которая записывается на этапе производства или в процессе программирования (ППЗУ) и используется преимущественно для чтения. В зависимости от типа, ПЗУ доступна или только операция чтения ([PROM](https://ru.wikipedia.org/wiki/PROM)), или также многократная запись (для [EPROM](https://ru.wikipedia.org/wiki/EPROM), [EEPROM](https://ru.wikipedia.org/wiki/EEPROM) и [Flash](https://ru.wikipedia.org/wiki/Флеш-память)). ОЗУ позволяет считывать и записывать информацию в процессе работы устройства, обеспечивая доступ к временным данным, необходимым для текущей работы системы.
|
||||
|
||||
Для ОЗУ требуется больше сигналов. Кроме входного `addr` и выходного `read_data` добавляются: входные данные для записи `write_data`, сигнал синхронизации `clk`, который определяет момент записи данных и сигнал разрешения на запись `write_enable`, который контролирует нужно ли записывать данные или только считывать. Для того, чтобы записать информацию в такую память необходимо:
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
- установить сигнал `write_enable` в состояние разрешения записи (как правило это 1) и
|
||||
- дождаться нужного (положительного, либо отрицательного) фронта `clk` — в этот момент данные будут записаны по указанному адресу.
|
||||
|
||||
Так же возможна реализация, в которой вход `write_data` и выход `read_data` объединены в единый вход/выход `data`. В этом случае операции чтения и записи разделены во времени и используют для этого один единый порт ввода-вывода (`inout`, двунаправленный порт) `data`.
|
||||
Также возможна реализация, в которой вход `write_data` и выход `read_data` объединены в единый вход/выход `data`. В этом случае операции чтения и записи разделены во времени и используют для этого один единый порт ввода-вывода (`inout`, двунаправленный порт) `data`.
|
||||
|
||||

|
||||
|
||||
@@ -58,7 +58,7 @@ _Рисунок 1. Примеры блоков ПЗУ и ОЗУ._
|
||||
|
||||
Регистровый файл, который будет реализован в рамках данной работы, является **трехпортовым**, и имеет 2 порта на чтение и 1 порт на запись.
|
||||
|
||||
С точки зрения аппаратной реализации память в ПЛИС может быть **блочной**, **распределенной** или **регистровой**. **Блочная память** — это аппаратный блок памяти, который можно сконфигурировать под свои нужды. **Распределенная** и **регистровая** память (в отличие от блочной) реализуется на **конфигурируемых логических блоках** (см. [как работает ПЛИС](../../Introduction/How%20FPGA%20works.md)). Такая память привязана к расположению конфигурируемых логических блоков ПЛИС и как бы равномерно распределена по всему кристаллу. Вместо реализации логики конфигурируемые логические блоки используются для нужд памяти. Чтобы понять почему это возможно, рассмотрим структуру логического блока:
|
||||
С точки зрения аппаратной реализации память в ПЛИС может быть **блочной**, **распределенной** или **регистровой**. **Блочная память** — это заранее размещённый аппаратный блок памяти, который можно сконфигурировать под свои нужды. **Распределенная** и **регистровая** память (в отличие от блочной) реализуется на **конфигурируемых логических блоках** (см. [как работает ПЛИС](../../Introduction/How%20FPGA%20works.md)). Такая память привязана к расположению конфигурируемых логических блоков ПЛИС и как бы равномерно распределена по всему кристаллу. Вместо реализации логики конфигурируемые логические блоки используются для нужд памяти. Чтобы понять почему это возможно, рассмотрим структуру логического блока:
|
||||
|
||||

|
||||
|
||||
@@ -70,23 +70,23 @@ _Рисунок 2. Структурная схема логического бл
|
||||
|
||||
Для реализации многопортовой памяти небольшого размера лучше воспользоваться расположенным в логическом блоке D-триггером (**DFF** на _рис. 2_). Несмотря на то, что D-триггер позволяет воспроизвести только 1 разряд элемента памяти, он не ограничивает реализацию по портам.
|
||||
|
||||
Таким образом, плюс распределенной памяти относительно регистровой заключается в лучшей утилизации ресурсов: одним трёхвходовым LUT можно описать до 8 бит распределенной памяти, в то время как одним D-триггером можно описать только один бит регистровой памяти. Предположим, что в ПЛИС размещены логические блоки, структура которых изображена на _рис. 2_ и нам необходимо реализовать 1KiB памяти. Мы можем реализовать распределенную память, используя 64 логических блока (в каждом блоке два трёхвходовых LUT), либо регистровую память, используя 1024 логических блока.
|
||||
Таким образом, преимущество распределенной памяти относительно регистровой заключается в лучшей утилизации ресурсов: одним трёхвходовым LUT можно описать до 8 бит распределенной памяти, в то время как одним D-триггером можно описать только один бит регистровой памяти. Предположим, что в ПЛИС размещены логические блоки, структура которых изображена на _рис. 2_ и нам необходимо реализовать 1KiB памяти. Мы можем реализовать распределенную память, используя 64 логических блока (в каждом блоке два трёхвходовых LUT), либо регистровую память, используя 1024 логических блока.
|
||||
|
||||
Минусом является ограниченность в реализации многопортовой памяти.
|
||||
Недостатком является ограниченность в реализации многопортовой памяти.
|
||||
|
||||
Сравним блочную память с распределенной/регистровой: поскольку большой объем памяти "съест" много логических блоков при реализации распределенной/регистровой памяти, такую память лучше делать в виде блочной.
|
||||
|
||||
В то же время, к плюсам распределенной/регистровой памяти относится возможность синтезировать память с асинхронным портом на чтение, чем мы и воспользуемся при реализации однотактного процессора (если бы порт чтения памяти был синхронным, нам потребовалось ждать один такт, чтобы получить инструкцию из памяти инструкций или данные из регистрового файла, что затруднило бы реализацию однотактного процессора, где каждая инструкция должна выполняться ровно за один такт).
|
||||
В то же время, к преимуществам распределенной/регистровой памяти относится возможность синтезировать память с асинхронным портом на чтение, чем мы и воспользуемся при реализации однотактного процессора (если бы порт чтения памяти был синхронным, нам потребовалось ждать один такт, чтобы получить инструкцию из памяти инструкций или данные из регистрового файла, что затруднило бы реализацию однотактного процессора, где каждая инструкция должна выполняться ровно за один такт).
|
||||
|
||||
Обычно синтезатор сам понимает, какой вид памяти подходит под описанную схему на языке SystemVerilog.
|
||||
|
||||
В случае, если под описанную схему подходит несколько видов памяти, есть возможность выбрать конкретную вручную, причем способы могут различаться от производителя к производителю, поэтому за подробностями лучше обращаться к документации. Например у Xilinx за это отвечает [следующий раздел](https://docs.xilinx.com/r/en-US/ug901-vivado-synthesis/RAM-HDL-Coding-Techniques) документации по синтезу.
|
||||
В случае если под описанную схему подходит несколько видов памяти, есть возможность выбрать конкретный вид вручную, причем способы могут различаться у разных производителей, поэтому за подробностями лучше обращаться к документации. Например у Xilinx за это отвечает [следующий раздел](https://docs.xilinx.com/r/en-US/ug901-vivado-synthesis/RAM-HDL-Coding-Techniques) документации по синтезу.
|
||||
|
||||
## Инструменты для реализации памяти
|
||||
|
||||
### Описание памяти на языке SystemVerilog
|
||||
|
||||
Память на языке SystemVerilog объявляется [подобно регистрам](../../Basic%20Verilog%20structures/Registers.md), используя ключевое слово `logic`. Но, кроме разрядности (разрядности ячеек памяти, в данном случае) после имени регистра (памяти, в данном случае) указывается количество создаваемых ячеек либо в виде натурального числа, либо в виде диапазона адресов этих ячеек.:
|
||||
Память на языке SystemVerilog объявляется [подобно регистрам](../../Basic%20Verilog%20structures/Registers.md), используя ключевое слово `logic`. Но, кроме разрядности (разрядности ячеек памяти, в данном случае) после имени регистра (памяти, в данном случае) указывается количество создаваемых ячеек либо в виде натурального числа, либо в виде диапазона адресов этих ячеек:
|
||||
|
||||
```Verilog
|
||||
logic [19:0] memory1 [16]; // memory1 и memory2 являются полностью
|
||||
@@ -112,15 +112,15 @@ logic [19:0] memory3 [1:16]; // А вот memory3 хоть и совпадае
|
||||
|
||||
_Листинг 1. Пример создания массива ячеек._
|
||||
|
||||
В приведенном листинге `logic [19:0] memory1 [16];` создается память с шестнадцатью (от 0-го до 15-го адреса) 20-битными ячейками памяти. В таком случае говорят, что ширина памяти 20 бит, а глубина 16. Для адресации такой памяти потребуется адрес с разрядностью ceil(log2(16)) = 4 бита (`ceil` — операция округления вверх).
|
||||
В первой строке _листинга 1_ создаётся память с шестнадцатью (от 0-го до 15-го адреса) 20-битными ячейками памяти. В таком случае говорят, что ширина памяти 20 бит, а глубина 16. Для адресации такой памяти потребуется адрес с разрядностью ceil(log2(16)) = 4 бита (`ceil` — операция округления вверх).
|
||||
|
||||
Для обращения к конкретной ячейке памяти используются квадратные скобки с указанием нужного адреса `memory[addr]`. Грубо говоря, то, что указывается в квадратных скобках будет подключено ко входу адреса памяти `memory`.
|
||||
Для обращения к конкретной ячейке памяти используются квадратные скобки с указанием нужного адреса: `memory[addr]`. Грубо говоря, то, что указывается в квадратных скобках будет подключено ко входу адреса памяти `memory`.
|
||||
|
||||
Как уже говорилось, чтение из памяти может быть сделано двумя способами: синхронно и асинхронно.
|
||||
|
||||
Синхронное чтение подразумевает ожидание следующего тактового синхроимпульса для выдачи данных после получения адреса. Иными словами, данные будут установлены на выходе не в тот же такт, когда был выставлен адрес на вход памяти данных, а на следующий. Несмотря на то, что в таком случае на каждой операции чтения "теряется" один такт, память с синхронным чтением имеет значительно меньший критический путь, чем положительно сказывается на временных характеристиках итоговой схемы.
|
||||
|
||||
Память с асинхронным чтением выдает данные в том же такте, что и получает адрес (т.е. ведет себя как комбинационная схема). Несмотря на то, что такой подход кажется быстрее, память с асинхронным чтением обладает длинным критическим путем, причем чем большего объема будет память, тем длиннее будет критический путь.
|
||||
Память с асинхронным чтением выдает данные в том же такте, что и получает адрес (т.е. во время операций чтения ведёт себя как комбинационная схема). Несмотря на то, что такой подход кажется быстрее, память с асинхронным чтением обладает длинным критическим путем, причем чем большего объема будет память, тем длиннее будет критический путь.
|
||||
|
||||
Реализация асинхронного подключения к выходу памяти осуществляется оператором `assign`. А, если требуется создать память с синхронным чтением, то присваивание выходу требуется описать внутри блока`always_ff`.
|
||||
|
||||
@@ -185,7 +185,7 @@ $readmemh("<data file name>",<memory name>,<start address>,<end address>);
|
||||
$readmemh("<data file name>",<memory name>);
|
||||
```
|
||||
|
||||
Пример описанной выше памяти:
|
||||
Пример описанной выше памяти приводится в _листинге 3_.
|
||||
|
||||
```Verilog
|
||||
module rom16_8 (
|
||||
@@ -220,7 +220,8 @@ _Листинг 3. Пример использования инициализи
|
||||
A7
|
||||
```
|
||||
|
||||
**Для того, чтобы при сборке модуля не было проблем с путями, по которым будет искаться данный файл, обычно его необходимо добавить в проект. В случае Vivado, чтобы тот распознал этот файл как инициализирующий память, необходимо чтобы у этого файла было расширение `.mem`.**
|
||||
> [!IMPORTANT]
|
||||
> Для того, чтобы при сборке модуля не было проблем с путями, по которым будет искаться данный файл, обычно его необходимо добавить в проект. В случае Vivado, чтобы тот распознал этот файл как инициализирующий память, необходимо чтобы у этого файла было расширение `.mem`.
|
||||
|
||||
## Задание по реализации памяти
|
||||
|
||||
@@ -231,7 +232,7 @@ _Листинг 3. Пример использования инициализи
|
||||
|
||||
### 1. Память инструкций
|
||||
|
||||
У данного модуля будет два входных/выходных сигнала:
|
||||
У данного модуля будет один входной и один выходной сигнал:
|
||||
|
||||
- 32-битный вход адреса
|
||||
- 32-битный выход данных (асинхронное чтение)
|
||||
@@ -243,7 +244,7 @@ module instr_mem(
|
||||
);
|
||||
```
|
||||
|
||||
Несмотря на разрядность адреса, на практике, внутри данного модуля вы должны будете реализовать память с 512-ю 32-битными ячейками (в ПЛИС попросту не хватит ресурсов на реализации памяти с 2<sup>32</sup> ячеек). Таким образом, реально будет использоваться только 9 бит адреса.
|
||||
Несмотря на разрядность адреса, на практике, внутри данного модуля вы должны будете реализовать память с 512-ю 32-битными ячейками (в ПЛИС попросту не хватит ресурсов для реализации памяти с 2<sup>32</sup> ячеек). Таким образом, реально будет использоваться только 9 бит адреса.
|
||||
|
||||
При этом по спецификации процессор RISC-V использует память с побайтовой адресацией [[2](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), стр. 15]. Байтовая адресация означает, что процессор способен обращаться к отдельным байтам в памяти (за каждым байтом памяти закреплен свой индивидуальный адрес).
|
||||
|
||||
@@ -260,7 +261,7 @@ _Рисунок 3. Связь адреса байта и индекса слов
|
||||
Для этого можно, например, создать параметр: `INSTR_MEM_SIZE_BYTES`, показывающий размер памяти инструкций в байтах. Однако, поскольку у данной памяти 32-битные ячейки, нам было бы удобно иметь и параметр `INSTR_MEM_SIZE_WORDS`, который говорит сколько в памяти 32-битных ячеек.
|
||||
При этом `INSTR_MEM_SIZE_WORDS = INSTR_MEM_SIZE_BYTES / 4` (т.е. в 32-битном слове 4 байта).
|
||||
|
||||
В случае подобной параметризации, необходимо иметь возможность подстраивать количество используемых бит адреса. Для 512 ячеек памяти мы использовали 9 бит адреса, для 1024 ячеек нам потребуется уже 10 бит. Нетрудно заметить, что нам нужно такое число бит данных, возведя в степень которого `2`, мы получим размер нашей памяти (либо число, превышающее этот размер в случае, если размер памяти не является степенью двойки). Иными словами, нам нужен логарифм по основанию 2 от размера памяти, с округлением до целого вверх. И неудивительно, что в SystemVerilog есть специальная конструкция, которая позволяет считать подобные числа. Эта конструкция называется `$clog2` (`с` означает "ceil" — операцию округления вверх).
|
||||
В случае подобной параметризации, необходимо иметь возможность подстраивать количество используемых бит адреса. Для 512 ячеек памяти мы использовали 9 бит адреса, для 1024 ячеек нам потребуется уже 10 бит. Нетрудно заметить, что нам нужно такое число бит данных, возведя в степень которого `2`, мы получим размер нашей памяти (либо число, превышающее этот размер в случае, если размер памяти не является степенью двойки). Иными словами, нам нужен логарифм по основанию 2 от размера памяти, с округлением до целого вверх. И неудивительно, что в SystemVerilog есть специальная функция, которая позволяет считать подобные числа. Эта функция называется `$clog2` (`с` означает "ceil" — операцию округления вверх).
|
||||
|
||||
Поскольку реализация памяти состоит буквально из нескольких строчек, но при этом использование параметров может вызвать некоторые затруднения, код памяти инструкций предоставляется в готовом виде:
|
||||
|
||||
@@ -282,11 +283,11 @@ import memory_pkg::INSTR_MEM_SIZE_WORDS;
|
||||
end // файла program.mem
|
||||
|
||||
// Реализация асинхронного порта на чтение, где на выход идёт ячейка памяти
|
||||
// инструкций, расположенная по адресу read_addr_i, в котором обнулены два
|
||||
// инструкций, расположенная по адресу read_addr_i, в котором отброшены два
|
||||
// младших бита, а также биты, двоичный вес которых превышает размер памяти
|
||||
// данных в байтах.
|
||||
// Два младших бита обнулены, чтобы обеспечить выровненный доступ к памяти,
|
||||
// в то время как старшие биты обнулены, чтобы не дать обращаться в память
|
||||
// Два младших бита отброшены, чтобы обеспечить выровненный доступ к памяти,
|
||||
// в то время как старшие биты отброшены, чтобы не дать обращаться в память
|
||||
// по адресам несуществующих ячеек (вместо этого будут выданы данные ячеек,
|
||||
// расположенных по младшим адресам).
|
||||
assign read_data_o = ROM[read_addr_i[$clog2(INSTR_MEM_SIZE_BYTES)-1:2]];
|
||||
@@ -298,18 +299,18 @@ _Листинг 4. SystemVerilog-описание памяти инструкц
|
||||
|
||||
### 3. Регистровый файл
|
||||
|
||||
На языке SystemVerilog необходимо реализовать модуль регистрового файла для процессора с архитектурой RISC-V, представляющего собой трехпортовое ОЗУ с двумя портами на чтение и одним портом на запись и состоящей из 32-х 32-битных регистров, объединенных в массив с именем `rf_mem`.
|
||||
Необходимо описать на языке SystemVerilog модуль регистрового файла для процессора с архитектурой RISC-V, представляющего собой трехпортовое ОЗУ с двумя портами на чтение и одним портом на запись и состоящей из 32-х 32-битных регистров, объединенных в массив с именем `rf_mem`.
|
||||
|
||||
У данного модуля будет восемь входных/выходных сигналов:
|
||||
|
||||
- вход тактового синхроимпульса
|
||||
- вход сигнала разрешения записи
|
||||
- 5-битный вход первого адреса чтения
|
||||
- 5-битный вход второго адреса чтения
|
||||
- 5-битный вход адреса записи
|
||||
- 32-битный вход данных записи
|
||||
- 32-битный выход данных асинхронного чтения по первому адресу
|
||||
- 32-битный выход данных асинхронного чтения по второму адресу
|
||||
- вход тактового синхроимпульса;
|
||||
- вход сигнала разрешения записи;
|
||||
- 5-битный вход первого адреса чтения;
|
||||
- 5-битный вход второго адреса чтения;
|
||||
- 5-битный вход адреса записи;
|
||||
- 32-битный вход данных записи;
|
||||
- 32-битный выход данных асинхронного чтения по первому адресу;
|
||||
- 32-битный выход данных асинхронного чтения по второму адресу.
|
||||
|
||||
```Verilog
|
||||
module register_file(
|
||||
@@ -327,10 +328,10 @@ module register_file(
|
||||
|
||||
```
|
||||
|
||||
По адресу `0` должно всегда считываться значение `0` вне зависимости от того, какое значение в этой ячейке памяти, и есть ли она вообще. Такая особенность обусловлена тем, что при выполнении операций очень часто используется ноль (сравнение с нулем, инициализация переменных нулевым значением, копирование значения одного регистра в другой посредством сложения с нулем и записи результата и т.п.). Эту особенность регистрового файла можно реализовать несколькими способами:
|
||||
По адресу `0` должно всегда считываться значение `0` вне зависимости от того, какое значение в этой ячейке памяти, и есть ли она вообще. Такая особенность обусловлена тем, что при выполнении операций очень часто используется ноль (сравнение с нулём, инициализация переменных нулевым значением, копирование значения одного регистра в другой посредством сложения с нулём и записи результата и т.п.). Эту особенность регистрового файла можно реализовать несколькими способами:
|
||||
|
||||
- можно решить эту задачу с помощью мультиплексора, управляющим сигналом которого является сигнал сравнения адреса на чтение с нулем;
|
||||
- либо же можно проинициализировать нулевую ячейку памяти нулем с запретом записи в неё каких-либо значений. В этом случае в ячейке всегда будет ноль, а значит и считываться с нулевого адреса будет только он.
|
||||
- с помощью мультиплексора, управляющим сигналом которого является результат сравнения адреса на чтение с нулём;
|
||||
- либо же можно проинициализировать нулевую ячейку памяти нулём с запретом записи в неё каких-либо значений. В этом случае в ячейке всегда будет ноль, а значит и считываться с нулевого адреса будет только он.
|
||||
|
||||
Инициализация ячейки памяти может быть осуществлена (только при проектировании под ПЛИС) с помощью присваивания в блоке `initial`.
|
||||
|
||||
|
@@ -36,7 +36,7 @@
|
||||
4. изменяется значение `PC`;
|
||||
5. цикл повторяется с `п.1`.
|
||||
|
||||
Любая инструкция приводит к изменению состояния памяти. В случае процессора с архитектурой `CYBERcobra 3000 Pro 2.1` есть два класса инструкций: одни изменяют содержимое регистрового файла — это инструкции записи. Другие изменяют значение `PC` — это инструкции перехода. В первом случае используются вычислительные инструкции и инструкции загрузки данных из других источников. Во-втором случае используются инструкции перехода.
|
||||
Любая инструкция приводит к изменению состояния памяти. В случае процессора с архитектурой `CYBERcobra 3000 Pro 2.1` есть два класса инструкций: одни изменяют содержимое регистрового файла — это инструкции записи. Другие изменяют значение `PC` — это инструкции перехода. В первом случае используются вычислительные инструкции и инструкции загрузки данных из других источников. Во втором случае используются инструкции перехода.
|
||||
|
||||
Если процессор обрабатывает вычислительную инструкцию, то `PC` перейдет к следующей по порядку инструкции. В ЛР№3 мы реализовали память инструкций с [побайтовой адресацией](../03.%20Register%20file%20and%20memory/README.md#1-память-инструкций). Это означает, что каждый байт памяти имеет свой собственный адрес. Поскольку длина инструкции составляет `4 байта`, для перехода к следующей инструкции `PC` должен быть увеличен на `4` (`PC = PC + 4`). При этом, регистровый файл сохранит результат некоторой операции на АЛУ или данные с порта входных данных.
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
Так как операции будут выполняться только над данными в регистровом файле, то его можно сразу подключить к АЛУ, соединив порты чтения `read_data1_o` и `read_data2_o` со входами операндов АЛУ, а результат операции АЛУ подключив к порту на запись `write_data_i`. Полученный результат изображен на _рис. 0_.
|
||||
|
||||
> Для того, чтобы номера таблиц и рисунков лучше соотносились друг с другом и сопутствующим текстом, первая схема разрабатываемой микроархитектуры будет обозначена как _Рисунок 0_. Все последующие схемы будут совпадать по нумерации с таблицами, обозначающими способ кодирования инструкций.
|
||||
> Для того чтобы номера таблиц и рисунков лучше соотносились друг с другом и сопутствующим текстом, первая схема разрабатываемой микроархитектуры будет обозначена как _Рисунок 0_. Все последующие схемы будут совпадать по нумерации с таблицами, обозначающими способ кодирования реализуемого типа инструкций.
|
||||
|
||||

|
||||
|
||||
@@ -107,14 +107,24 @@ _Таблица 1. Кодирование вычислительных инст
|
||||
|
||||
### Реализация вычислительных инструкций
|
||||
|
||||
Чтобы процессор правильно реагировал на эти инструкции, требуется подключить ко входам адреса регистрового файла и управляющему входу АЛУ соответствующие биты выхода `read_data_o` памяти инструкции (**Instruction Memory**). В таком случае, когда `PC` будет указывать на ячейку памяти, в которой лежит, например, следующая 32-битная инструкция:
|
||||
Чтобы процессор правильно реагировал на эти инструкции, требуется подключить ко входам адреса регистрового файла и управляющему входу АЛУ соответствующие биты выхода `read_data_o` памяти инструкции (**Instruction Memory**). Допустим, программный счётчик указывает на ячейку памяти, в которой хранится следующая 32-битная инструкция:
|
||||
|
||||
```text
|
||||
0000 00111 00100 01000 00000000 11100
|
||||
0000|00111 |00100|01000|00000000|11100
|
||||
|alu_op| RA1 | RA2 | | WA
|
||||
```
|
||||
|
||||
будет выполнена операция `reg_file[28] = reg_file[4] | reg_file[8]`, потому что `alu_op = 00111`, что соответствует операции **логического ИЛИ** (см ЛР№2), `WA = 11100`, то есть запись произойдёт в 28-ой регистр, `RA1 = 00100` и `RA2 = 01000` — это значит что данные для АЛУ будут браться из 4-го и 8-го регистров соответственно.
|
||||
В этом случае, будет выполнена операция:
|
||||
|
||||
```text
|
||||
reg_file[28] = reg_file[4] | reg_file[8]
|
||||
```
|
||||
|
||||
Здесь:
|
||||
|
||||
- `alu_op = 00111`, что соответствует операции **логического ИЛИ** (см ЛР№2);
|
||||
- `WA = 11100`, то есть запись произойдёт в 28-ой регистр;
|
||||
- `RA1 = 00100` и `RA2 = 01000` — это значит что данные для АЛУ будут браться из 4-го и 8-го регистров соответственно.
|
||||
|
||||
_Рис. 1_ иллюстрирует фрагмент микроархитектуры, поддерживающий вычислительные операции на АЛУ. Поскольку другие инструкции пока что не поддерживаются, то вход `WE` регистрового файла просто равен `1` (это временно).
|
||||
|
||||
@@ -124,7 +134,7 @@ _Рисунок 1. Подключение АЛУ и регистрового ф
|
||||
|
||||
### Реализация загрузки константы в регистровый файл
|
||||
|
||||
Информация как-то должна попадать в регистровый файл, для этого добавим инструкцию загрузки константы по адресу `WA`. Чтобы аппаратура могла различать, когда ей нужно выполнять операцию на АЛУ, а когда загружать константу, назначим один бит инструкции определяющим "что именно будет записано в регистровый файл": результат с АЛУ или константа из инструкции. За это будет отвечать 28-ой бит инструкции `WS` (**Write Source**). Если `WS == 1`, значит выполняется вычислительная инструкция, а если `WS == 0`, значит нужно загрузить константу в регистровый файл.
|
||||
Обрабатываемая информация как-то должна попадать в регистровый файл, для этого добавим инструкцию загрузки константы по адресу `WA`. Чтобы аппаратура могла различать, когда ей нужно выполнять операцию на АЛУ, а когда загружать константу, назначим один бит инструкции определяющим "что именно будет записано в регистровый файл": результат с АЛУ или константа из инструкции. За это будет отвечать 28-ой бит инструкции `WS` (**Write Source**). Если `WS == 1`, значит выполняется вычислительная инструкция, а если `WS == 0`, значит нужно загрузить константу в регистровый файл.
|
||||
|
||||
Сама константа имеет разрядность **23 бита** ([27:5] биты инструкции) и должна быть **знакорасширена** до 32-х бит, то есть к 23-битной константе нужно приклеить слева 9 раз 23-ий знаковый бит константы (см. [оператор конкатенации](../../Basic%20Verilog%20structures/Concatenation.md)).
|
||||
|
||||
@@ -169,7 +179,7 @@ _Рисунок 2. Добавление константы из инструкц
|
||||
|
||||
### Реализация загрузки в регистровый файл данных с внешних устройств
|
||||
|
||||
Чтобы процессор мог взаимодействовать с внешним миром добавим возможность загрузки данных с внешних устройств в регистр по адресу `WA`. Появляется третий тип инструкции, который определяет третий источник ввода для регистрового файла. Одного бита `WS` для выбора одного из трех источников будет недостаточно, поэтому расширим это поле до 2 бит. Теперь, когда `WS == 0` будет загружаться константа, когда `WS == 1` – будет загружаться результат вычисления АЛУ, а при `WS == 2` будут загружаться данные с внешних устройств. Остальные поля в данной инструкции не используются.
|
||||
Чтобы процессор мог взаимодействовать с внешним миром добавим возможность загрузки данных с внешних устройств в регистр по адресу `WA`. Появляется третий тип инструкции, который определяет третий источник ввода для регистрового файла. Одного бита `WS` для выбора одного из трех источников будет недостаточно, поэтому расширим это поле до 2 бит. Теперь, когда `WS == 0` будет загружаться константа, когда `WS == 1` – будет загружаться результат вычисления АЛУ, а при `WS == 2` будут загружаться данные с внешних устройств. Остальные поля (кроме `WA`) в данной инструкции не используются.
|
||||
|
||||

|
||||
|
||||
@@ -183,7 +193,7 @@ _Таблица 3. Кодирование в инструкции большег
|
||||
|
||||
По аналогии с загрузкой констант увеличиваем входной мультиплексор до 4 входов и подключаем к нему управляющие сигналы – `[29:28]` биты инструкции. Последний вход используется, чтобы разрешить неопределённость на выходе при `WS == 3`(`default`-вход, см. [мультиплексор](../../Basic%20Verilog%20structures/Multiplexors.md)).
|
||||
|
||||
Выход OUT подключается к первому порту на чтение регистрового файла. Значение на выходе OUT будет определяться содержимым ячейки памяти по адресу `RA1`.
|
||||
Выход модуля `out_o` подключается к первому порту на чтение регистрового файла. Значение на выходе `out_o` будет определяться содержимым ячейки памяти по адресу `RA1`.
|
||||
|
||||

|
||||
|
||||
@@ -199,9 +209,9 @@ _Таблица 4.Кодирование условного перехода._
|
||||
|
||||
Для вычисления результата условного перехода, нам необходимо выполнить операцию на АЛУ и посмотреть на сигнал `flag`. Если он равен 1, переход выполняется, в противном случае — не выполняется. А значит, нам нужны операнды `A`, `B`, и `alu_op`. Кроме того, нам необходимо указать насколько мы сместимся относительно текущего значения `PC` (константу смещения, `offset`). Для передачи этой константы лучше всего подойдут незадействованные биты инструкции `[12:5]`.
|
||||
|
||||
Обратим внимание на то, что `PC` 32-битный и должен быть всегда кратен четырем (`PC` не может указывать кроме как в начало инструкции, а каждая инструкция длиной в 32 бита). Кратные четырем двоичные числа всегда будут иметь в конце два нуля (так же, как и кратные ста десятичные числа). Поэтому для более эффективного использования бит константы смещения, эти два нуля будут неявно подразумеваться при её описании. При этом, перед увеличением программного счётчика на значение константы смещения, эти два нуля нужно будет к ней приклеить справа. Кроме того, чтобы разрядность константы совпадала с разрядностью `PC`, необходимо знакорасширить её до 32 бит.
|
||||
Обратим внимание на то, что `PC` 32-битный и должен быть всегда кратен четырем (`PC` не может указывать кроме как в начало инструкции, а каждая инструкция длиной в 32 бита). Кратные четырем двоичные числа всегда будут иметь в конце два нуля (так же, как и кратные ста десятичные числа). Поэтому для более эффективного использования битов константы смещения, эти два нуля будут неявно подразумеваться при её описании. При этом, перед увеличением программного счётчика на значение константы смещения, эти два нуля нужно будет к ней приклеить справа. Кроме того, чтобы разрядность константы совпадала с разрядностью `PC`, необходимо знакорасширить её до 32 бит.
|
||||
|
||||
Предположим, мы хотим переместиться на две инструкции вперед. Это означает, что программный счётчик должен будет увеличиться на 8 ([2 инструкции] * [4 байта — размер одной инструкции в памяти]). Умножение на 4 константы смещения произойдет путем добавления к ней двух нулей справа, поэтому в поле `offset` мы просто записываем число инструкций, на которые мы переместим программный счётчик (на две): `0b00000010`.
|
||||
Предположим, мы хотим переместиться на две инструкции вперед. Это означает, что программный счётчик должен будет увеличиться на 8 ([2 инструкции] * [4 байта — размер одной инструкции в памяти]). Умножение константы смещения на 4 произойдет путем добавления к ней двух нулей справа, поэтому в поле `offset` мы просто записываем число инструкций, на которое мы переместим программный счётчик (на две): `0b00000010`.
|
||||
|
||||
Данный Си-подобный псевдокод (далее мы назовем его псевдоассемблером) демонстрирует кодирование инструкций с новым полем `B`:
|
||||
|
||||
@@ -214,7 +224,7 @@ _Таблица 4.Кодирование условного перехода._
|
||||
|
||||
Так как второй вход сумматора счётчика команд занят числом 4, то для реализации условного перехода этот вход надо мультиплексировать с константой. Мультиплексор при этом управляется 30-ым битом `B`, который и определяет, что будет прибавляться к `PC`.
|
||||
|
||||
Сигнальные линии, которые управляют АЛУ и подают на его входы операнды уже существуют. Поэтому на схему необходимо добавить только логику управления мультиплексором на входе сумматора счётчика команд так. Эта логика работает следующим образом:
|
||||
Сигнальные линии, которые управляют АЛУ и подают на его входы операнды уже существуют. Поэтому на схему необходимо добавить только логику управления мультиплексором на входе сумматора счётчика команд. Эта логика работает следующим образом:
|
||||
|
||||
1. если сейчас инструкция условного перехода
|
||||
2. и если условие перехода выполнилось,
|
||||
@@ -241,11 +251,10 @@ _Таблица 5. Кодирование безусловного перехо
|
||||
|
||||
Для реализации безусловного перехода, нам необходимо добавить дополнительную логику управления мультиплексором перед сумматором. Итоговая логика его работы звучит так:
|
||||
|
||||
1. Если сейчас инструкция безусловного перехода
|
||||
2. ИЛИ если сейчас инструкция условного перехода
|
||||
3. И если условие перехода выполнилось
|
||||
1. если сейчас инструкция безусловного перехода, _или_
|
||||
2. если сейчас инструкция условного перехода _и_ условие перехода выполнилось
|
||||
|
||||
Кроме того, при безусловном переходе в регистровый файл так же ничего не пишется. А значит, необходимо обновить логику работы сигнала разрешения записи `WE`, который будет равен 0 если сейчас инструкция условного или безусловного перехода.
|
||||
Кроме того, при безусловном переходе в регистровый файл также ничего не пишется. А значит, необходимо обновить логику работы сигнала разрешения записи `WE`, который будет равен 0 если сейчас инструкция условного или безусловного перехода.
|
||||
|
||||
На _рис. 5_ приводится итоговый вариант микроархитектуры процессора `CYBERcobra 3000 Pro 2.1`.
|
||||
|
||||
@@ -255,7 +264,7 @@ _Рисунок 5. Реализация безусловного переход
|
||||
|
||||
### Финальный обзор
|
||||
|
||||
Итого, архитектура `CYBERcobra 3000 Pro 2.1` поддерживает 5 типов инструкций, которые кодируются следующим образом (иксами помечены биты, которые не задействованы в данной инструкции):
|
||||
Итого, архитектура `CYBERcobra 3000 Pro 2.1` поддерживает 5 типов инструкций, которые кодируются следующим образом (символами `x` помечены биты, которые не задействованы в данной инструкции):
|
||||
|
||||
1. 10 вычислительных инструкций `0 0 01 alu_op RA1 RA2 xxxx xxxx WA`
|
||||
2. Инструкция загрузки константы `0 0 00 const WA`
|
||||
@@ -265,9 +274,9 @@ _Рисунок 5. Реализация безусловного переход
|
||||
|
||||
При кодировании инструкций используются следующие поля:
|
||||
|
||||
- J – однобитный сигнал, указывающий на выполнение безусловного перехода;
|
||||
- B – однобитный сигнал, указывающий на выполнение условного перехода;
|
||||
- WS – двухбитный сигнал, указывающий источник данных для записи в регистровый файл:
|
||||
- J – 1-битный сигнал, указывающий на выполнение безусловного перехода;
|
||||
- B – 1-битный сигнал, указывающий на выполнение условного перехода;
|
||||
- WS – 2-битный сигнал, указывающий источник данных для записи в регистровый файл:
|
||||
- 0 – константа из инструкции;
|
||||
- 1 – результат с АЛУ;
|
||||
- 2 – внешние данные;
|
||||
@@ -281,9 +290,9 @@ _Рисунок 5. Реализация безусловного переход
|
||||
Напишем простую программу для этого процессора, которая циклично увеличивает значение первого регистра на 1 до тех пор, пока его значение не превысит число, введенное на переключателях. Сначала напишем программу на псевдоассемблере (используя предложенную мнемонику):
|
||||
|
||||
``` C
|
||||
reg_file[1] ← -1 // загрузить константу `-1` регистр 1
|
||||
reg_file[1] ← -1 // загрузить константу -1 в регистр 1
|
||||
reg_file[2] ← sw_i // загрузить значение с входа sw_i в регистр 2
|
||||
reg_file[3] ← 1 // загрузить константу `1` регистр 3
|
||||
reg_file[3] ← 1 // загрузить константу 1 в регистр 3
|
||||
|
||||
reg_file[1] ← reg_file[1] + reg_file[3] // сложить регистр 1 с регистром 3 и
|
||||
// поместить результат в регистр 1
|
||||
@@ -344,17 +353,17 @@ endmodule
|
||||
1. В первую очередь, необходимо создать счётчик команд и все вспомогательные провода. При создании, **следите за разрядностью**.
|
||||
2. Затем, необходимо создать экземпляры модулей: памяти инструкции, АЛУ, регистрового файла и сумматора. При подключении сигналов сумматора, надо **обязательно** надо подать нулевое значение на входной бит переноса. Выходной бит переноса подключать не обязательно. Объекту памяти инструкций нужно дать имя `imem`.
|
||||
3. После этого, необходимо описать оставшуюся логику:
|
||||
1. Программного счётчика
|
||||
1. Программного счётчика. Счётчик должен сбрасываться, когда сигнал _rst_i == 1_.
|
||||
2. Сигнала управления мультиплексором, выбирающим слагаемое для программного счётчика
|
||||
3. Сигнала разрешения записи в регистровый файл
|
||||
4. Мультиплексор, выбирающий слагаемое для программного счётчика
|
||||
5. Мультиплексор, выбирающий источник записи в регистровый файл.
|
||||
3. Проверьте модуль с помощью верификационного окружения, представленного в файле [`lab_04.tb_cybercobra.sv`](lab_04.tb_cybercobra.sv).
|
||||
1. Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в `Simulation Sources`.
|
||||
2. В этот раз, в конце не будет сообщения о том, работает ли ваше устройство или в нем есть ошибки. Вам необходимо самостоятельно проверить работу модуля, перенеся его внутренние сигналы на временную диаграмму, и [изучив](../../Vivado%20Basics/05.%20Bug%20hunting.md) их поведение.
|
||||
2. В этот раз, в конце не будет сообщения о том, работает ли ваше устройство или в нём есть ошибки. Вам необходимо самостоятельно проверить работу модуля, перенеся его внутренние сигналы на временную диаграмму, и [изучив](../../Vivado%20Basics/05.%20Bug%20hunting.md) их поведение.
|
||||
3. По сути, проверка сводится к потактовому изучению временной диаграммы, во время которого вам нужно циклично ответить на следующие вопросы (после чего необходимо сравнить предсказанный ответ со значением сигналов на временной диаграмме):
|
||||
1. Какое сейчас значение программного счётчика?
|
||||
2. Какое должно быть значение у ячейки памяти инструкций с адресом, соответствующим значению программного счётчика. Какой инструкции соответствует значение этой ячейки памяти?
|
||||
2. Какая инструкция должна быть считана при данном значении программного счётчика?
|
||||
3. Как должно обновиться содержимое регистрового файла в результате выполнения этой инструкции: должно ли записаться какое-либо значение? Если да, то какое и по какому адресу?
|
||||
4. Как должен измениться программный счётчик после выполнения этой инструкции?
|
||||
4. Проверьте работоспособность вашей цифровой схемы в ПЛИС.
|
||||
|
@@ -1,18 +1,18 @@
|
||||
# Написание программы под процессор CYBERcobra
|
||||
|
||||
Чтобы максимально "прочувствовать" принцип работы созданного вами процессора, вам необходимо написать один из [вариантов программ](#индивидуальные-задания). Вариант выдается преподавателем.
|
||||
Чтобы максимально "прочувствовать" принцип работы созданного вами процессора, вам необходимо написать один из [вариантов программ](#индивидуальные-задания). Вариант выдаётся преподавателем.
|
||||
|
||||
Порядок выполнения задания следующий:
|
||||
|
||||
1. В первую очередь необходимо ознакомиться с заданием и изучить пример, приведенный в конце задания. Если возникли вопросы по заданию или примеру, свяжитесь с преподавателем. Чем лучше вы понимаете что от вас ожидают, тем проще будет выполнить задание.
|
||||
1. В первую очередь необходимо ознакомиться с заданием и изучить пример, приведённый в конце задания. Если возникли вопросы по заданию или примеру, свяжитесь с преподавателем. Чем лучше вы понимаете, что от вас ожидают, тем проще будет выполнить задание.
|
||||
2. Составьте алгоритм работы программы (буквально возьмите листочек, и нарисуйте блок-схему). Прежде чем вы погрузитесь в увлекательное приключение с ноликами и единицами, вам нужно составить четкую карту вашего путешествия.
|
||||
3. Проверьте вашу блок-схему на данных из примера. Если все сошлось, проверьте вашу блок-схему на других данных. Не забывайте про краевые случаи (отрицательные числа, деление на ноль, переполнения и прочее — для каждого задания они могут быть разными).
|
||||
4. После того как вы убедились в работоспособности алгоритма на всех возможных данных, наступает время претворить его в виде двоичной программы.
|
||||
5. Программа описывается в текстовом файле. Для удобства был написан специальный конвертер, который будет принимать на вход текстовый файл с комментариями и двоичным кодом, разделенным пробелами, а на выход выдавать текстовый файл, которым можно будет проинициализировать память инструкций. Подробнее о конвертере смотрите в разделе [cyberconverter](#cyberconverter). Пример текстового файла, который сможет принять конвертер:
|
||||
5. Программа описывается в текстовом файле. Для удобства был написан специальный конвертер, который будет принимать на вход текстовый файл с комментариями и двоичным кодом, разделённым пробелами, а на выход выдавать текстовый файл, которым можно будет проинициализировать память инструкций. Подробнее о конвертере смотрите в параграфе [cyberconverter](#cyberconverter). Пример текстового файла, который сможет принять конвертер:
|
||||
|
||||
```text
|
||||
//J B WS ALUop RA1 RA2 const WA
|
||||
0 0 00 11111111111111111111111 00001 // загрузить константу -1 регистр 1
|
||||
0 0 00 11111111111111111111111 00001 // загрузить константу -1 в регистр 1
|
||||
0 0 10 00000000000000000000000 00010 // загрузить значение с входа sw_i в регистр 2
|
||||
0 0 00 00000000000000000000001 00011 // загрузить константу 1 регистр 3
|
||||
0 0 01 00000 00001 00011 00000000 00001 // сложить регистр 1 с регистром 3 и поместить результат в регистр 1
|
||||
@@ -25,11 +25,11 @@
|
||||
1. инструкции блока `if` (если условие выполнилось)
|
||||
2. инструкции блока `else` (если условие не выполнилось)
|
||||
|
||||
При этом сразу за инструкцией ветвления описываются инструкции блока `else` (т.к. в случае не выполнения условия перехода, `PC` перейдет к следующей инструкции).
|
||||
При этом сразу за инструкцией ветвления описываются инструкции блока `else` (т.к. в случае невыполнения условия перехода, `PC` перейдёт к следующей инструкции).
|
||||
Для того, чтобы после выполнения инструкций блока `else` не начались исполняться инструкции блока `if`, в конце блока этих инструкций необходимо добавить безусловный переход на инструкцию, следующую за инструкциями блока `if`.
|
||||
2. Если вы реализуете не ветвление (аналог блока `if/else`), а только проверку условия для выполнения какого-то блока инструкций (аналог блока `if` без блока `else`), вы должны помнить, что блок `else` все равно есть, просто в этом блоке нет инструкций. Однако вы, как и в прошлом правиле, должны добавить безусловный переход на инструкцию, следующую за инструкциями блока `if`.
|
||||
Этого можно избежать, если инвертировать ваше условие. В этом случае, если ваше инвертированное условие выполнится, вы сможете сразу пропустить нужное количество инструкций и начать исполнять инструкцию за пределами вашего блока `if`. Если инвертированное условие не выполнится (т.е. выполнится исходное условие), `PC` перейдет к следующей инструкции, где и будут находиться ваши инструкции блока `if`.
|
||||
Звучит достаточно запутанно, поэтому давайте рассмотрим пару примеров. Сначала мы запишем нашу идею на языке Си, а затем перенесем её в двоичный код под архитектуру CYBERcobra:
|
||||
2. Если вы реализуете не ветвление (аналог блока `if/else`), а только проверку условия для выполнения какого-то блока инструкций (аналог блока `if` без блока `else`), вы должны помнить, что блок `else` всё равно есть, просто в этом блоке нет инструкций. Однако вы, как и в прошлом правиле, должны добавить безусловный переход на инструкцию, следующую за инструкциями блока `if`.
|
||||
Этого можно избежать, если инвертировать ваше условие. В этом случае, если ваше инвертированное условие выполнится, вы сможете сразу пропустить нужное количество инструкций и начать исполнять инструкцию за пределами вашего блока `if`. Если инвертированное условие не выполнится (т.е. выполнится исходное условие), `PC` перейдёт к следующей инструкции, где и будут находиться ваши инструкции блока `if`.
|
||||
Звучит достаточно запутанно, поэтому давайте рассмотрим пару примеров. Сначала мы запишем нашу идею на языке Си, а затем перенесём её в двоичный код под архитектуру CYBERcobra:
|
||||
|
||||
```C
|
||||
if(reg[1]==reg[5])
|
||||
@@ -54,7 +54,7 @@
|
||||
```text
|
||||
//J B WS ALUop RA1 RA2 const WA
|
||||
0 1 00 11000 00001 00101 00000011 00000 // Если регистры 1 и 5 равны,
|
||||
// перемести PC на 3 инструкции вперед
|
||||
// перемести PC на 3 инструкции вперёд
|
||||
// (перешагни через две
|
||||
// инструкции блока else)
|
||||
//---------------------------------------
|
||||
@@ -133,8 +133,8 @@
|
||||
//---------------------------------------
|
||||
```
|
||||
|
||||
7. В двоичном программировании, реализация циклов лучше всего делается аналогом `do while` в Си (если вы уверены, что первая итерация цикла гарантированно пройдет условие выхода из цикла). В этом случае, вы сперва описываете тело цикла, а затем через условный переход возвращаетесь обратно к началу тела цикла. Если условие не выполнилось, вы автоматически выйдете из цикла.
|
||||
8. Для того, чтобы в конце выполнения программы было легко увидеть результат выполнения, в конец программы необходимо добавить инструкцию безусловного перехода, поле `const` которой равно нулю. В этом случае, будет выполняться `PC=PC+0` что приведет к повторению этой инструкции снова и снова. При этом в поле `RA1` необходимо указать адрес регистра, где хранится результат. На временной диаграмме это отобразится так, что в какой-то момент все сигналы процессора "замрут", а на выходе `out_o` окажется результат, вычисленный вашей программой.
|
||||
7. В двоичном программировании, реализация циклов лучше всего делается аналогом `do while` в Си (если вы уверены, что первая итерация цикла гарантированно пройдёт условие выхода из цикла). В этом случае, вы сперва описываете тело цикла, а затем через условный переход возвращаетесь обратно к началу тела цикла. Если условие не выполнилось, вы автоматически выйдете из цикла.
|
||||
8. Для того, чтобы в конце выполнения программы было легко увидеть результат выполнения, в конец программы необходимо добавить инструкцию безусловного перехода, поле `const` которой равно нулю. В этом случае, будет выполняться `PC=PC+0` что приведёт к повторению этой инструкции снова и снова. При этом в поле `RA1` необходимо указать адрес регистра, где хранится результат. На временной диаграмме это отобразится так, что в какой-то момент все сигналы процессора "замрут", а на выходе `out_o` окажется результат, вычисленный вашей программой.
|
||||
9. После того, как вы написали программу, её необходимо проверить. Для этого сперва необходимо преобразовать её к формату, принимаемому памятью инструкций с помощью программы [`cyberconverter`](#cyberconverter). При необходимости, заменить данные в файле, инициализирующем память инструкций актуальными данными.
|
||||
10. Если ваша программа использует данные с внешних устройств, нужно выставить проверяемое вами значение в модуле `testbench` на вход `sw_i` в месте подключения модуля `CYBERcobra`.
|
||||
11. Проверка работы программы осуществляется аналогично проверке модуля `CYBERcobra` — вы достаете внутренние сигналы модуля, и смотрите за поведением сигналов: `PC`, `read_data` памяти инструкций, `flag` АЛУ, содержимым регистрового файла. Проверяете, что в конце на выходе `out_o` размещено корректное значение.
|
||||
@@ -175,9 +175,9 @@ cyberconverter принимает до двух аргументов. Поряд
|
||||
|
||||
## Индивидуальные задания
|
||||
|
||||
В приведенных ниже заданиях под `a` будет подразумеваться некоторое число, заданное в программе (например в программе прописано `a=10`), под `sw_i` — вход с внешних устройств. "Вывести в `out_o`" — означает, что в конце программы необходимо реализовать бесконечный цикл, с указанием в `RA1` адреса регистра, хранящего результат (см. пункт 8 раздела "[Написание программы под процессор CYBERcobra](#написание-программы-под-процессор-cybercobra)").
|
||||
В приведённых ниже заданиях под `a` будет подразумеваться некоторое число, заданное в программе (например в программе прописано `a=10`), под `sw_i` — вход с внешних устройств. "Вывести в `out_o`" — означает, что в конце программы необходимо реализовать бесконечный цикл, с указанием в `RA1` адреса регистра, хранящего результат (см. пункт 8 параграфа "[Написание программы под процессор CYBERcobra](#написание-программы-под-процессор-cybercobra)").
|
||||
|
||||
В случае, если задание используется для написания программы на ассемблере, `sw_i` будет обозначать еще одно число, заданное в программе (как и `a`), а под "Вывести в `out_o`" — запись результата в регистр `x10` (в назначение этого регистра входит возврат результата функции) в конце выполнения программы.
|
||||
В случае, если задание используется для написания программы на ассемблере, `sw_i` будет обозначать ещё одно число, заданное в программе (как и `a`), а под "Вывести в `out_o`" — запись результата в регистр `x10` (в назначение этого регистра входит возврат результата функции) в конце выполнения программы.
|
||||
|
||||
1. Вычислить [циклический сдвиг](https://ru.wikipedia.org/wiki/Битовый_сдвиг#Циклический_сдвиг) вправо `a >> sw_i`.
|
||||
Пример: `a = 0...01011`, `sw_i = 0...010`.
|
||||
@@ -229,7 +229,7 @@ cyberconverter принимает до двух аргументов. Поряд
|
||||
Пример: `sw_i = 0...011_1011_1000`.
|
||||
Результат вычислений `out_o = 0...01_0100`.
|
||||
|
||||
13. Найти количество единиц в двоичном представлении числа `sw_i` (обрати внимание, `sw_i` – знаковое число). Вывести результат в `out_o`.
|
||||
13. Найти количество единиц в двоичном представлении числа `sw_i` (обратите внимание, `sw_i` – знаковое число). Вывести результат в `out_o`.
|
||||
Пример: `sw_i = 0...0101_0110`.
|
||||
Результат вычислений: `out_o = 0...0100`.
|
||||
|
||||
@@ -312,6 +312,6 @@ cyberconverter принимает до двух аргументов. Поряд
|
||||
|
||||
<!-- 25. *Зажечь все светодиоды на 50% яркости ([подсказка](http://wiki.amperka.ru/конспект-arduino:шим)) -->
|
||||
|
||||
29. Удалить все вхождения `sw_i[2:0]` из `a` со сдвигом вправо (заполняя удаленные области).
|
||||
29. Удалить все вхождения `sw_i[2:0]` из `a` со сдвигом вправо (заполняя удалённые области).
|
||||
Пример: `a = 0...010011010`, `sw_i[2:0] = 101`.
|
||||
Результат вычислений: `out_o = 0...010010`
|
||||
|
@@ -43,7 +43,7 @@ _Рисунок 1. Микроархитектура будущего проце
|
||||
- `mem_size_o` — размер данных при обращении в память;
|
||||
- `mem_we_o` — сигнал разрешения записи во внешнюю память;
|
||||
- `mem_wd_o` — данные для записи во внешнюю память;
|
||||
- `mem_rd_i` — считанные из внешней памяти данные;
|
||||
- `mem_rd_i` — считанные из внешней памяти данные.
|
||||
Эти сигналы используются при выполнении инструкций загрузки (сохранения) информации из (в) памяти данных.
|
||||
- еще у процессора появился вход `stall_i`, приостанавливающий обновление программного счётчика.
|
||||
|
||||
@@ -84,7 +84,7 @@ _Рисунок 1. Микроархитектура будущего проце
|
||||
|
||||
_Таблица 1. Описание портов декодера инструкций._
|
||||
|
||||
У данного модуля будет лишь один вход: `fetched_instr_i` — декодируемая в данный момент инструкция. Все остальные сигналы — это выходы модуля, которые можно классифицировать следующим образом:
|
||||
У данного модуля будет лишь один вход: `fetched_instr_i` — декодируемая в данный момент инструкция. Все остальные сигналы — это выходы модуля, которые можно сгруппировать по нескольким классам.
|
||||
|
||||
### Сигналы кода операции
|
||||
|
||||
@@ -105,7 +105,7 @@ _Таблица 1. Описание портов декодера инструк
|
||||
- `b_sel_o`,
|
||||
- `wb_sel_o`.
|
||||
|
||||
Сигналы `a_sel_o` и `b_sel_o` определяют откуда пойдут данные на операнды АЛУ `a_i`, `b_i` соответственно. К примеру, если мы хотим, чтобы оба операнда брались из регистрового файла, нам необходимо подать значение `0` на оба соответствующих мультиплексора.
|
||||
Сигналы `a_sel_o` и `b_sel_o` определяют откуда пойдут данные на операнды АЛУ `a_i`, `b_i` соответственно. К примеру, если мы хотим, чтобы оба операнда брались из регистрового файла, нам необходимо подать значение `0` на входы управляющих сигналов обоих мультиплексоров.
|
||||
|
||||
Сигнал `wb_sel_o` определяет источник данных для записи в регистровый файл: это либо результат операции на АЛУ, считанные данные из памяти данных, либо же данные, полученные из модуля регистров контроля и статуса.
|
||||
|
||||
@@ -149,7 +149,7 @@ _Таблица 2. Значения сигнала `mem_size_o` при пере
|
||||
|
||||
### Сигнал нелегальной инструкции
|
||||
|
||||
Это сигнал, который должен принять значение `1`, в случае если пришла инструкция, которая не входит в список поддерживаемых процессором.
|
||||
Сигнал, который должен принять значение `1`, в случае если пришла инструкция, которая не входит в список поддерживаемых процессором.
|
||||
|
||||
Это не единственное, что должен сделать декодер в подобной ситуации. Давайте разберем подробней, что должно происходить по приходу нелегальной инструкции.
|
||||
|
||||
@@ -162,7 +162,7 @@ _Таблица 2. Значения сигнала `mem_size_o` при пере
|
||||
- намеренная вставка неподдерживаемой инструкции (например для эксплуатации какой-нибудь уязвимости);
|
||||
- инструкция, которая на самом деле поддерживается процессором, но требует большего уровня привилегий и потому не может быть выполнена.
|
||||
|
||||
В случае появления инструкции, которая не поддерживается процессором, устройство управления должно обеспечить стабильность системы. В самом простом случае, такую инструкцию необходимо пропустить, сохранив так называемое **архитектурное состояние** процессора — т.е. сохранив значение всех элементов системы, характеризующих состояние системы в текущий момент. К таким элементам относятся: содержимое регистрового файла, основой памяти, содержимое регистров контроля и статуса и т.п. Значение программного счетчика также входит в архитектурное состояние процессора, однако в контексте пропуска инструкции с сохранением архитектурного состояния, его значение нужно изменить, иначе система оказалась бы в бесконечном цикле (неизменный счетчик бы указывал на ту же самую инструкцию, которая не должна менять архитектурного состояния).
|
||||
В случае появления инструкции, которая не поддерживается процессором, устройство управления должно обеспечить стабильность системы. В самом простом случае, такую инструкцию необходимо пропустить, сохранив так называемое **архитектурное состояние** процессора — т.е. сохранив значение всех элементов, характеризующих состояние системы в текущий момент. К таким элементам относятся: содержимое регистрового файла, основой памяти, содержимое регистров контроля и статуса и т.п. Значение программного счетчика также входит в архитектурное состояние процессора, однако в контексте пропуска инструкции с сохранением архитектурного состояния, его значение нужно изменить, иначе система оказалась бы в бесконечном цикле (неизменный счетчик бы указывал на ту же самую инструкцию, которая не должна менять архитектурного состояния).
|
||||
|
||||
Иными словами, в случае появления нелегальной инструкции, устройство управления (роль которого в нашей системе по большей части играет декодер) должно проследить за тем, чтобы в системе не изменилось ничего кроме программного счетчика. К сигналам, влияющим на изменение архитектурного состояния, относятся:
|
||||
|
||||
@@ -177,8 +177,6 @@ _Таблица 2. Значения сигнала `mem_size_o` при пере
|
||||
|
||||
то есть, должны быть запрещены все запросы на запись, обращения в память и любые "прыжки" программного счетчика.
|
||||
|
||||
|
||||
|
||||
Давайте теперь разберемся с тем, какие именно инструкции должен будет поддерживать наш процессор.
|
||||
|
||||
### Набор поддерживаемых инструкций **RISC-V** и способы их кодирования
|
||||
@@ -200,11 +198,11 @@ _Таблица 2. Значения сигнала `mem_size_o` при пере
|
||||
|
||||
В _Таблице 3_ приводится фрагмент из `спецификации RISC-V`. В верхней её части приводится 6 форматов кодирования инструкций: **R**, **I**, **S**, **B**, **U** и **J** (описание типов представлено в _таблице 4_). Затем список всех инструкций с конкретными значениями полей, соответствующих формату кодирования инструкции данного типа.
|
||||
|
||||
Под `rd` подразумевается 5-битный адрес регистра назначения (**r**egister **d**estination), `rs1` и `rs2` — 5-битные адреса регистров источников (**r**egister **s**ource), `imm` — непосредственный (immediate, задающийся прямиком в инструкции) операнд (константа), расположение и порядок битов которого указывается в квадратных скобках. Обратите внимание, что в разных форматах кодирования константы имеют различную разрядность, а их биты расположены по-разному. Для знаковых операций константу предварительно знаково расширяют до 32 бит. Для беззнаковых расширяют нулями до 32 бит.
|
||||
Под `rd` подразумевается 5-битный адрес регистра назначения (**r**egister **d**estination), `rs1` и `rs2` — 5-битные адреса регистров источников (**r**egister **s**ource), `imm` — непосредственный (immediate, задающийся прямиком в инструкции) операнд (константа), расположение и порядок битов которого указывается в квадратных скобках. Обратите внимание, что в разных форматах кодирования константы имеют различную разрядность, а их биты расположены по-разному. Непосредственные операнды всех типов интерпретируются как знаковые и требуют знакового расширения [[1](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), стр. 23]. Исключение составляют 5-битные константы CSR-инструкций.
|
||||
|
||||

|
||||
|
||||
_Таблица 3. Базовый набор инструкций из спецификации RISC-V[[1, стр. 130]](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf), Стандартное расширение Zicsr[[1, стр. 131]](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf), а также привилегированная инструкция mret[[2, стр. 138]](https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf)._
|
||||
_Таблица 3. Базовый набор инструкций из спецификации RISC-V[[1, стр. 554]](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), Стандартное расширение Zicsr[[1, стр.556]](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), а также привилегированная инструкция mret[[2, стр.51]](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/priv-isa-asciidoc.pdf)._
|
||||
|
||||
| Кодирование | Описание |
|
||||
|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
@@ -221,13 +219,13 @@ _Таблица 4. Описание типов форматов кодирова
|
||||
|
||||
Как уже описывалось в дополнительных материалах, декодирование инструкции начинается с поля `opcode` (**op**eration **code**, опкод). По этому полю определяется группа инструкций одного типа. Далее (для большинства типов кодирования) инструкция доопределяется через поля `func3` и `func7` (при наличии). Обратите внимание, что расположение этих полей одинаково для всех типов инструкций (см. верхнюю часть _таблицы 3_).
|
||||
|
||||
Поля `rs1`/`rs2`/`imm` и `rd` декодеру не нужны и используются напрямую для адресации в регистровом файле / использования непосредственного операнда в АЛУ.
|
||||
Поля `rs1`/`rs2`/`imm` и `rd` декодеру не нужны и используются напрямую для адресации регистров / задания констант.
|
||||
|
||||
Существуют особые инструкции, не имеющие никаких переменных полей (к примеру инструкция ECALL в _таблице 3_). Такие инструкции необходимо проверять целиком (нужно убедиться, что инструкция совпадает вплоть бита).
|
||||
|
||||
В _Таблице 5_ представлены все опкоды реализуемых нами инструкций. Представленные в ней коды операций 5-битные потому, что 2 младших бита полноценного 7-битного кода операции в реализуемых нами инструкциях должны всегда быть равны `11`. Если это не так, то вся инструкция уже запрещенная и не нуждается в дальнейшем декодировании.
|
||||
|
||||
Для удобства значения кодов операций определены в виде параметров в пакете `decoder_pkg`.
|
||||
Для удобства, значения кодов операций определены в виде параметров в пакете `decoder_pkg`.
|
||||
|
||||
|Параметр|Opcode| Описание группы операций | Краткая запись |
|
||||
|--------|------|-------------------------------------------------------------------------------------------------------|------------------------------------|
|
||||
@@ -241,7 +239,7 @@ _Таблица 4. Описание типов форматов кодирова
|
||||
|JALR |11001 |Записать в `rd` следующий адрес счетчика команд, в счетчик команд записать `rs1+imm` |`rd = PC + 4; PC = rs1+imm` |
|
||||
|AUIPC |00101 |Записать в `rd` результат сложения непосредственного операнда U-типа `imm_u` и счетчика команд |`rd = PC + (imm << 12)` |
|
||||
|MISC-MEM|00011 |Не производить операцию | `-` |
|
||||
|SYSTEM |11100 |Записать в `rd` значение `csr`. Обновить значение `csr` с помощью `rs1`. (либо `mret`/`ecall`/`ebreak`)|`csr = csr_op(rs1); rd = csr` |
|
||||
|SYSTEM |11100 |Записать в `rd` значение `csr`. Обновить значение `csr` с помощью `rs1`. (либо инструкция `mret`/`ecall`/`ebreak`)|`csr = csr_op(rs1); rd = csr` |
|
||||
|
||||
_Таблица 5. Описание кодов операций._
|
||||
|
||||
@@ -258,13 +256,13 @@ SYSTEM-инструкции используются для доступа к с
|
||||
|
||||
Кроме того, для возврата управления основному потоку инструкций, нужна дополнительная `SYSTEM`-инструкция привилегированного набора команд `MRET`.
|
||||
|
||||
Перечисленные выше инструкции являются "дополнительными" — их намеренно их добавили сверх стандартного набора инструкций, чтобы обеспечить требуемый нашей системе функционал. Однако осталось ещё две SYSTEM-инструкции, которые мы должны уметь декодировать, поскольку они есть в стандартном наборе инструкций.
|
||||
Перечисленные выше инструкции являются "дополнительными" — их добавили сверх стандартного набора инструкций, чтобы обеспечить требуемый нашей системе функционал. Однако осталось ещё две SYSTEM-инструкции, которые мы должны уметь декодировать, поскольку они есть в стандартном наборе инструкций.
|
||||
|
||||
Инструкции `ECALL` и `EBREAK` вызывают исключение. Подробнее исключения и прерывания будут разобраны в ЛР№10.
|
||||
|
||||
#### MISC-MEM инструкция
|
||||
#### MISC-MEM инструкции
|
||||
|
||||
В базовом наборе инструкций **RISC-V** к `MISC-MEM`-операции относится инструкция `FENCE`. В реализуемом процессорном ядре эта инструкция не должны приводить ни к каким изменениям. Инструкция `FENCE` в **RISC-V** необходима при работе с несколькими аппаратными потоками, или "хартами" (hart – «hardware thread»). В **RISC-V** используется расслабленная модель памяти (**relaxed memory model**): потоки «видят» все инструкции чтения и записи, которые исполняются другими потоками, однако видимый порядок этих инструкций может отличаться от реального. Инструкция `FENCE`, использованная между двумя инструкциями чтения и/или записи гарантирует, что остальные потоки увидят первую инструкцию перед второй. Реализация `FENCE` является опциональной в **RISC-V** и в данном случае в ней нет необходимости, так как в системе не предполагается наличия нескольких аппаратных потоков. Данная инструкция должна быть реализована как `NOP` (**n**o **op**eration).
|
||||
В базовом наборе инструкций **RISC-V** к `MISC-MEM`-операциям относятся инструкции `FENCE`, `FENCE.TSO`, `PAUSE` (которые объединены в таблице 5 в одну инструкцию `FENCE`). В реализуемом процессорном ядре эта инструкция не должна приводить к изменениям. Инструкция `FENCE` в **RISC-V** необходима при работе с несколькими аппаратными потоками, или "хартами" (hart – «hardware thread»). Она помогает согласовать доступ к данным между ними. В **RISC-V** используется "расслабленная модель" памяти (**relaxed memory model**): которая позволяет потокам видеть операции других потоков, но не обязательно в том порядке, в каком они были записаны в коде программы. Инструкция `FENCE`, использованная между двумя инструкциями чтения и/или записи гарантирует, что остальные потоки увидят первую инструкцию перед второй. Реализация `FENCE` является опциональной в **RISC-V** и в данном случае в ней нет необходимости, так как в системе не предполагается наличия нескольких аппаратных потоков. Данная инструкция должна быть реализована как `NOP` (**n**o **op**eration).
|
||||
|
||||
В _таблице 6_ представлены инструкции из таблицы 3 с приведением их типов, значениями полей `opcode`, `func3`, `func7`, функциональным описанием и примерами использования.
|
||||
|
||||
@@ -272,15 +270,17 @@ SYSTEM-инструкции используются для доступа к с
|
||||
|
||||
_Таблица 6. Расширенное описание инструкций RV32IZicsr._
|
||||
|
||||
Обратите внимание на операции `slli`, `srli` и `srai` (операции сдвига на константную величину). У этих инструкций немного измененный формат кодирования **I\***. Формат кодирования **I** предоставляет 12-битную константу. Сдвиг 32-битного числа более, чем на 31 не имеет смысла. Для кодирования числа 31 требуется всего 5 бит. Выходит, что из 12 бит константы используется только 5 бит для операции сдвига (в виде поля `shamt`, сокращение от **sh**ift **am**oun**t** — "сколько раз сдвигать"), а оставшиеся 7 бит – не используются. А, главное (какое совпадение!), эти 7 бит находятся ровно в том же месте, где у других инструкций находится поле `func7`. Поэтому, чтобы у инструкций `slli`, `srli` и `srai` использующих формат **I** не пропадала эта часть поля, к ней относятся как к полю `func7`.
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на операции `slli`, `srli` и `srai` (операции сдвига на константную величину). У этих инструкций немного измененный формат кодирования **I\***. Формат кодирования **I** предоставляет 12-битную константу. Сдвиг 32-битного числа более, чем на 31 не имеет смысла. Для кодирования числа 31 требуется всего 5 бит. Выходит, что из 12 бит константы для операции сдвига используется только 5 бит (которые обозначены в виде поля `shamt`, сокращение от **sh**ift **am**oun**t** — "сколько раз сдвигать"), а оставшиеся 7 бит – не используются. А, главное (какое совпадение!), эти 7 бит находятся ровно в том же месте, где у других инструкций находится поле `func7`. Поэтому, чтобы у инструкций `slli`, `srli` и `srai` использующих формат **I** не пропадала эта часть поля, к ней относятся как к полю `func7`.
|
||||
|
||||
Также обратите внимание на инструкции `ecall`, `ebreak` и `mret`. Все эти инструкции I-типа имеют поле func3, равное нулю. С точки зрения декодирования инструкции I-типа, это одна и та же инструкция с разными значениями поля `imm`. Однако конкретно в данном случае (SYSTEM_OPCODE и `func3 == 0`) эти инструкции должны рассматриваться как совокупность всех 32-бит сразу (см. _таблицу 3_).
|
||||
> [!IMPORTANT]
|
||||
> Также обратите внимание на инструкции `ecall`, `ebreak` и `mret`. Все эти инструкции I-типа имеют поле func3, равное нулю. С точки зрения декодирования инструкции I-типа, это одна и та же инструкция с разными значениями поля `imm`. Однако конкретно в данном случае (SYSTEM_OPCODE и `func3 == 0`) эти инструкции должны рассматриваться как совокупность всех 32-бит сразу (см. _таблицу 3_).
|
||||
|
||||
### Формирование управляющих сигналов
|
||||
|
||||
Как говорилось ранее, декодер инструкций в процессоре служит для преобразования инструкции в набор управляющих сигналов, необходимых для ее исполнения. Таким образом, для каждой инструкции из _таблицы 3_ декодер должен поставить в соответствие конкретное значение для каждого из выходов, перечисленных в _таблице 1_.
|
||||
|
||||
Пример: для выполнения инструкции записи 32-бит данных из регистрового файла во внешнюю память `sw`, дешифратор должен направить в АЛУ два операнда (базовый адрес и смещение) вместе с кодом операции АЛУ (сложения) для вычисления адреса. Базовый адрес берется из регистрового файла, а смещение является непосредственным операндом инструкции S-типа. Таким образом для вычисления адреса записи декодер должен выставить следующие значения на выходах:
|
||||
Пример: для выполнения инструкции записи 32-бит данных из регистрового файла во внешнюю память (инструкции `sw`), дешифратор должен направить в АЛУ два операнда (базовый адрес и смещение) вместе с кодом операции АЛУ (сложения) для вычисления адреса записи. Базовый адрес берется из регистрового файла, а смещение является непосредственным операндом инструкции S-типа. Таким образом для вычисления адреса записи декодер должен выставить следующие значения на выходах:
|
||||
|
||||
- `a_sel_o = 2'd0`,
|
||||
- `b_sel_o = 3'd1`,
|
||||
@@ -336,15 +336,15 @@ _Таблица 7. Описание портов дешифратора кома
|
||||
|
||||
## Инструменты
|
||||
|
||||
В первую очередь язык описания аппаратуры **SystemVerilog** – это язык. С помощью этого языка человек объясняет либо синтезатору какое он хочет получить устройство, либо симулятору – как он хочет это устройство проверить. Синтезатор – это программа, которая создает из логических элементов цифровое устройство по описанию, предоставляемому человеком. Синтезатору внутри **Vivado** нужно объяснить, что от него нужно. Например, чтобы спросить дорогу у испанца, придется делать это на испанском языке, иначе он ничем не сможет помочь. А если вы знаете испанский, то скорее всего сможете это сделать еще и разными способами. В **SystemVerilog** точно также – одно и то же устройство можно описать разным кодом, но результат синтеза будет одним и тем же. Однако, часто два разных кода одинаковые по смыслу могут синтезироваться в разную аппаратуру, хотя функционально они будут идентичны, но могут отличаться, например, скоростью работы. Или одни и те же специальные языковые конструкции могут применяться для синтезирования разных цифровых элементов.
|
||||
**SystemVerilog** – это язык описания аппаратуры. С помощью этого языка человек объясняет либо синтезатору какое он хочет получить устройство, либо симулятору – как он хочет это устройство проверить. Синтезатор – это программа, которая создает из логических элементов цифровое устройство по описанию, предоставляемому человеком. Синтезатору внутри **Vivado** нужно объяснить, что от него нужно. Например, чтобы спросить дорогу у испанца, придется делать это на испанском языке, иначе он ничем не сможет помочь. А если вы хорошо знаете испанский, то скорее всего сможете это сделать еще и разными способами. В **SystemVerilog** точно также – одно и то же устройство можно описать разным кодом, но результат синтеза будет одним и тем же. Однако, часто два разных кода, одинаковые по смыслу, могут синтезироваться в разную аппаратуру, хотя функционально они будут идентичны, но могут отличаться, например, скоростью работы. Или одни и те же специальные языковые конструкции могут применяться для синтезирования разных цифровых элементов.
|
||||
|
||||
Декодер – комбинационная схема. Это значит, что каждый раз подавая на вход одни и те же значения, вы будете получать на выходе один и тот же результат, потому что комбинационные схемы не содержат элементов памяти.
|
||||
|
||||
Можно по-разному описывать комбинационные схемы. Например — через конструкцию `assign`. Для описания декодера отлично подойдет конструкция `case`, которая превратится не в мультиплексор, а в комбинационную схему с оптимальными параметрами критического пути. В доверилоговую эпоху разработчикам пришлось бы строить гигантские таблицы истинности и [карты Карно](https://ru.wikipedia.org/wiki/Карта_Карно), искать оптимальные схемы реализации. Сегодня эту задачу решает синтезатор, по описанию устройства сам находит наиболее эффективное решение.
|
||||
Можно по-разному описывать комбинационные схемы. Например — через конструкцию `assign`. Для описания декодера отлично подойдет конструкция `case`, которая превратится не в мультиплексор, а в комбинационную схему с оптимальными параметрами критического пути. В доверилоговую эпоху разработчикам пришлось бы строить гигантские таблицы истинности и [карты Карно](https://ru.wikipedia.org/wiki/Карта_Карно), искать оптимальные схемы реализации. Сегодня эту задачу решает синтезатор, по описанию устройства он сам находит наиболее эффективное решение.
|
||||
|
||||
Разница с реализацией мультиплексора в том, что в этом случае справа от знака равно всегда стоит константа. Получается это такой способ описать таблицу истинности. В такой код легко вносить правки и искать интересующие фрагменты.
|
||||
|
||||
Рассмотрим пример ниже. Внутри конструкции `always_comb`, перед конструкцией `case` указываются значения по умолчанию. Благодаря этому пропадает необходимость указывать все сигналы внутри каждого обработчика `case`, достаточно указать только те, что имеют значение отличное от значения по умолчанию. Представленный пример реализует комбинационную схему, которая при `control_signal== 4'b1100` будет выставлять сигнал `c == 1'b0`, то есть отличное, от значения по умолчанию. Сигнал `a` никак не меняется, поэтому он не указан в соответствующем обработчике. Если сигнал `size == 1'b0`, то `b` будет равен 1, а `d` равен 0. Если сигнал `size == 1'b1`, то наоборот – `b` будет равен 0, а `d` равен 1.
|
||||
Рассмотрим _листинг 1_. Внутри конструкции `always_comb`, перед конструкцией `case` указываются значения по умолчанию. Благодаря этому пропадает необходимость указывать все сигналы внутри каждого обработчика `case`, достаточно указать только те, что имеют значение отличное от значения по умолчанию. Представленный пример реализует комбинационную схему, которая при `control_signal== 4'b1100` будет выставлять сигнал `c == 1'b0`, то есть отличное, от значения по умолчанию. Сигнал `a` никак не меняется, поэтому он не указан в соответствующем обработчике. Если `sub_signal == 1'b0`, то `b` будет равен 1, а `d` равен 0. Если `sub_signal == 1'b1`, то наоборот – `b` будет равен 0, а `d` равен 1.
|
||||
|
||||
```Verilog
|
||||
module example (
|
||||
@@ -380,13 +380,15 @@ module example (
|
||||
endmodule
|
||||
```
|
||||
|
||||
Имейте в виду, что значения по умолчанию, описанные в начале блока `always_comb` можно использовать таким образом при помощи **блокирующих присваиваний** (которые следует использовать только в комбинационных блоках).
|
||||
_Листинг 1. Пример описания декодера._
|
||||
|
||||
Имейте в виду, что значения по умолчанию, описанные в начале блока `always_comb` можно использовать таким образом при помощи **блокирующих присваиваний** (которые [следует](../../Basic%20Verilog%20structures/Assignments.md) использовать только в комбинационных блоках).
|
||||
|
||||
Кроме того, использование вложенных блоков `case` обосновано только в ситуации создания блока декодера (т.е. в случаях, когда справа от всех присваиваний будут использованы константы, а не другие сигналы). В случае описания мультиплексора, вложенные блоки `case` могут быть синтезированы в каскад мультиплексоров, что негативно скажется на временных характеристиках схемы.
|
||||
|
||||
## Задание
|
||||
|
||||
Необходимо реализовать на языке **SystemVerilog** модуль декодера инструкций однотактного процессора RISC-V в соответствии с предложенной микроархитектурой. Далее приводится прототип разрабатываемого модуля.
|
||||
Необходимо реализовать на языке **SystemVerilog** модуль декодера инструкций однотактного процессора RISC-V в соответствии с предложенной микроархитектурой. В _листинге 2_ приводится прототип разрабатываемого модуля.
|
||||
|
||||
```Verilog
|
||||
module decoder (
|
||||
@@ -412,7 +414,9 @@ module decoder (
|
||||
endmodule
|
||||
```
|
||||
|
||||
В зависимости от стиля оформления, модуль может занимать больше сотни строк кода, но это не делает его реализацию сложной. По сути, дешифратор — это просто большой `case` с описанием того, в каком случае, какие сигналы и чему должны быть равны. Работа требует внимательности, немного усидчивости и понимания выполняемых действий. С огромной вероятностью в коде будут ошибки и их нужно будет исправлять. Ошибки — это нормально (не ошибается тот, кто ничего не делает), а исправление ошибок дает бесценный опыт разработки. Возможно, реализация этого модуля в какой-то момент покажется рутинной, но по окончании следующей лабораторной работы удовольствие от результата покажет, что оно того стоило.
|
||||
_Листинг 2. Прототип декодера инструкций._
|
||||
|
||||
В зависимости от стиля оформления, модуль может занимать больше сотни строк кода, но это не делает его реализацию сложной. По сути, декодер — это просто большой блок `case` с описанием того, в каком случае, какие сигналы и чему должны быть равны. Работа требует внимательности, немного усидчивости и понимания выполняемых действий. С огромной вероятностью в коде будут ошибки и их нужно будет исправлять. Ошибки — это нормально (не ошибается тот, кто ничего не делает), а исправление ошибок дает бесценный опыт разработки. Возможно, реализация этого модуля в какой-то момент покажется рутинной, но по окончании лабораторной работы №7 удовольствие от результата покажет, что оно того стоило.
|
||||
|
||||
## Порядок выполнения задания
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
## Цель
|
||||
|
||||
Описать память данных, с побайтовой адресацией.
|
||||
Описать память данных с побайтовой адресацией.
|
||||
|
||||
## Материалы для подготовки к лабораторной работе
|
||||
|
||||
@@ -97,7 +97,7 @@ import memory_pkg::DATA_MEM_SIZE_WORDS;
|
||||
|
||||
_Рисунок 2. Операции запросов на чтение._
|
||||
|
||||
Если `mem_req_i == 1` и `write_enable_i == 1`, то происходит запрос на запись в память. В этом случае, необходимо записать значение `write_data_i` в ячейку по, на которую указывает `addr_i`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится. Запись необходимо производить только в те байты указанной ячейки, которым соответствуют биты сигнала `byte_enable_i`, равные 1.
|
||||
Если `mem_req_i == 1` и `write_enable_i == 1`, то происходит запрос на запись в память. В этом случае, необходимо записать значение `write_data_i` в ячейку, на которую указывает `addr_i`. Во всех других случаях (любой из сигналов `mem_req_i`, `write_enable_i` равен нулю), запись в память не производится. Запись необходимо производить только в те байты указанной ячейки, которым соответствуют биты сигнала `byte_enable_i`, равные 1.
|
||||
|
||||
На _рис. 3_ показан пример записей по различным адресам. Т.к. деление на 4 любого из приведенных на _рис. 3_ адресов даёт результат 2, на рисунке показано только содержимое второй 32-битной ячейки памяти и то, как оно менялось в зависимости от комбинации сигналов `write_data_i` и `byte_enable_i`.
|
||||
|
||||
@@ -105,7 +105,7 @@ _Рисунок 2. Операции запросов на чтение._
|
||||
|
||||
_Рисунок 3. Операции запросов на запись._
|
||||
|
||||
Выход `ready_o` в данном модуле должен всегда быть равен 1, поскольку данные всегда будут выдаваться на следующий такт. В реальности, обращение в память может занимать сотни тактов процессора, причём их число бывает недетерминированным (нельзя заранее предсказать сколько тактов займёт очередной запрос в память). Именно поэтому стандартные интерфейсы обычно используют такие сигналы как `ready` или `valid`, позволяющие синхронизировать разные блоки системы. Сигнал `ready_o` в нашем интерфейсе используется сигнала о задержке в выдаче данных. В случае, если устройству нужно больше одного такта, чтобы выдать данные, он устанавливает на данный сигнал значение `0` до тех пор, пока данные не будут готовы.
|
||||
Выход `ready_o` в данном модуле должен всегда быть равен 1, поскольку данные всегда будут выдаваться на следующий такт. В реальности, обращение в память может занимать сотни тактов процессора, причём их число бывает недетерминированным (нельзя заранее предсказать сколько тактов займёт очередной запрос в память). Именно поэтому стандартные интерфейсы обычно используют такие сигналы как `ready` или `valid`, позволяющие синхронизировать разные блоки системы. Сигнал `ready_o` в нашем интерфейсе используется в качестве сигнала о задержке в выдаче данных. В случае, если устройству нужно больше одного такта, чтобы выдать данные, он устанавливает на данный сигнал значение `0` до тех пор, пока данные не будут готовы.
|
||||
|
||||
## Порядок выполнения работы
|
||||
|
||||
|
@@ -13,11 +13,11 @@
|
||||
3. Подготовить программу по индивидуальному заданию и загрузить ее в память инструкций
|
||||
4. Сравнить результат работы процессора на модели в **Vivado** и в симуляторе программы ассемблера
|
||||
|
||||
## Микроархитектура RISC-V
|
||||
## Микроархитектура процессорной системы
|
||||
|
||||
### processor_core
|
||||
|
||||
Рассмотрим микроархитектуру процессорного ядра `processor_core`. Данный модуль обладает следующим прототипом и микроархитектурой:
|
||||
Рассмотрим микроархитектуру процессорного ядра `processor_core`. Микроархитектура данного модуля представлена на _рис. 1_.
|
||||
|
||||
```Verilog
|
||||
module processor_core (
|
||||
@@ -56,17 +56,18 @@ _Рисунок 1. Микроархитектура ядра процессор
|
||||
- `mem_wd_o` — данные для записи во внешнюю память;
|
||||
- `mem_rd_i` — считанные из внешней памяти данные;
|
||||
Эти сигналы используются при выполнении инструкций загрузки (сохранения) информации из (в) памяти данных.
|
||||
- еще у процессора появился вход `stall_i`, приостанавливающий обновление программного счётчика.
|
||||
- ещё у процессора появился вход `stall_i`, приостанавливающий обновление программного счётчика.
|
||||
|
||||
Кроме того, в данной микроархитектуре используется пять различных видов констант (соответствующих определенным типам инструкций).
|
||||
|
||||
Константы `I`, `U`, `S` используются для вычисления адресов и значений. Поэтому все эти константы должны быть подключены к АЛУ. А значит теперь, для выбора значения для операндов требуются мультиплексоры, определяющие, что именно будет подаваться на АЛУ.
|
||||
|
||||
Обратите внимание на константу `imm_U`. В отличие от всех остальных констант, она не знакорасширяется, вместо этого к ней "приклеивается" справа 12 нулевых бит.
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на константу `imm_U`. В отличие от всех остальных констант, она не знакорасширяется, вместо этого к ней "приклеивается" справа 12 нулевых бит.
|
||||
|
||||
Константы `B` и `J` используются для условного и безусловного перехода (в киберкобре для этого использовалась одна константа `offset`).
|
||||
|
||||
Программный счётчик (`PC`) теперь также изменяется более сложным образом. Поскольку появился еще один вид безусловного перехода (`jalr`), программный счётчик может не просто увеличиться на значение константы из инструкции, но и получить совершенно новое значение в виде суммы константы и значения из регистрового файла (см. на самый левый мультиплексор _рис. 1_). Обратите внимание, что младший бит этой суммы должен быть обнулен — таково требование спецификации [[1](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), стр. 28].
|
||||
Программный счётчик (`PC`) теперь также изменяется более сложным образом. Поскольку появился ещё один вид безусловного перехода (`jalr`), программный счётчик может не просто увеличиться на значение константы из инструкции, но и получить совершенно новое значение в виде суммы константы и значения из регистрового файла (см. на самый левый мультиплексор _рис. 1_). Обратите внимание, что младший бит этой суммы должен быть обнулен — таково требование спецификации [[1](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf), стр. 28].
|
||||
|
||||
Поскольку обращение во внешнюю память требует времени, необходимо приостанавливать программный счётчик, чтобы до конца обращения в память не начались исполняться последующие инструкции. Для этого у программного счётчика появился управляющий сигнал `stall_i`. Программный счётчик может меняться только когда этот сигнал равен нулю (иными словами, инверсия этого сигнала является сигналом `enable` для регистра `PC`).
|
||||
|
||||
@@ -87,11 +88,14 @@ endmodule
|
||||
|
||||
_Рисунок 2. Микроархитектура процессорной системы._
|
||||
|
||||
Обратите внимание на регистр `stall`. Этот регистр и будет управлять разрешением на запись в программный счётчик `PC`. Поскольку мы используем блочную память, расположенную прямо в ПЛИС, доступ к ней осуществляется за 1 такт, а значит, что при обращении в память, нам необходимо "отключить" программный счётчик ровно на 1 такт. Если бы использовалась действительно "внешняя" память (например чип DDR3), то вместо этого регистра появилась бы другая логика, выставляющая на вход ядра `stall_i` единицу пока идет обращение в память.
|
||||
Обратите внимание на регистр `stall` (_рисунок 2_). Этот регистр и будет управлять разрешением на запись в программный счётчик. Поскольку мы используем блочную память, расположенную прямо в ПЛИС, доступ к ней осуществляется за 1 такт, а значит, что при обращении в память, нам необходимо "отключить" программный счётчик ровно на 1 такт. Если бы использовалась действительно "внешняя" память (например память типа DDR3), то вместо этого регистра появилась бы другая логика, выставляющая на вход ядра `stall_i` единицу пока идет обращение в память.
|
||||
|
||||
## Задание
|
||||
|
||||
Реализовать ядро процессора `processor_core` архитектуры RISC-V по предложенной микроархитектуре. Подключить к нему память инструкций и память данных в модуле `processor_system`. Проверить работу процессора с помощью программы, написанной на ассемблере RISC-V по индивидуальному заданию, которое использовалось для написания программы для процессора архитектуры CYBERcobra.
|
||||
1. Реализовать ядро процессора RISC-V по предложенной микроархитектуре (`processor_core`).
|
||||
2. Подключить к нему память инструкций и память данных в модуле `processor_system`.
|
||||
3. Проверить работу системы, с помощью тестовой программы из _листинга 1_.
|
||||
4. Написать собственную программу на ассемблере RISC-V по индивидуальному заданию, которое было выбрано в ЛР№4, и проверить выполнение программы на вашей процессорной системе.
|
||||
|
||||
Напишем простую программу, которая использует все типы инструкций для проверки нашего процессора. Сначала напишем программу на ассемблере:
|
||||
|
||||
@@ -168,7 +172,7 @@ _Листинг 2. Программа из Листинга 1, представ
|
||||
1. Внимательно ознакомьтесь микроархитектурной реализацией процессорного ядра. В случае возникновения вопросов, проконсультируйтесь с преподавателем.
|
||||
2. Замените файл `program.mem` в `Design Sources` проекта новым файлом [program.mem](program.mem), приложенном в данной лабораторной работе. Данный файл содержит программу из _листинга 1_.
|
||||
3. Опишите модуль процессорного ядра с таким же именем и портами, как указано в задании.
|
||||
1. Процесс реализации модуля очень похож на процесс описания модуля cybercobra, однако теперь появляется:
|
||||
1. Процесс реализации модуля похож на процесс описания модуля cybercobra, однако теперь появляется:
|
||||
1. декодер
|
||||
2. дополнительные мультиплексоры и знакорасширители.
|
||||
2. Сперва рекомендуется создать все провода, которые будут подключены к входам и выходам каждого модуля на схеме.
|
||||
@@ -176,13 +180,15 @@ _Листинг 2. Программа из Листинга 1, представ
|
||||
4. Также необходимо создать 32-разрядные константы I, U, S, B и J-типа и программный счётчик.
|
||||
5. После необходимо описать логику, управляющую созданными в п. 3.2 проводами.
|
||||
6. В конце останется описать логику работы программного счётчика.
|
||||
4. Опишите модуль процессорной системы, объединяющий ядро процессора (`processor_core`) с памятями инструкция и данных.
|
||||
4. Опишите модуль процессорной системы, объединяющий ядро процессора (`processor_core`) с памятью инструкций и памятью данных.
|
||||
1. Опишите модуль с таким же именем и портами, как указано в задании.
|
||||
2. **При создании объекта модуля `processor_core` в модуле `processor_system` вы должны использовать имя сущности `core` (т.е. создать объект в виде: `processor_core core(...`)**.
|
||||
5. Проверьте модуль с помощью верификационного окружения, представленного в файле [`lab_07.tb_processor_system.sv`](lab_07.tb_processor_system.sv).
|
||||
1. Перед запуском симуляции убедитесь, что выбран правильный модуль верхнего уровня в `Simulation Sources`.
|
||||
2. Как и в случае с проверкой процессора архитектуры CYBERcobra, вам не будет сказано пройден тест или нет. Вам необходимо самостоятельно, такт за тактом проверить что процессор правильно выполняет описанные в _Листинге 1_ инструкции (см. порядок выполнения задания ЛР№4). Для этого, необходимо сперва самостоятельно рассчитать что именно должна сделать данная инструкция, а потом проверить что процессор сделал именно это.
|
||||
6. Проверьте работоспособность вашей цифровой схемы в ПЛИС.
|
||||
6. После проверки работы процессора программой из _листинга 1_, вам необходимо написать собственную программу по варианту индивидуального задания, полученного в ЛР№4.
|
||||
1. Для того, чтобы перевести код из ассемблера RISC-V в двоичный код, вы можете воспользоваться онлайн-компилятором, например: https://venus.kvakil.me. Напишите код во вкладке "Editor", затем, перейдя на вкладку "Simulator", нажмите кнопку "Dump". Двоичный код будет скопирован в буфер обмена в том формате, который подходит для инициализации памяти в Vivado, замените этим кодом содержимое файла program.mem в вашем проекте. С помощью кнопок "Step" и "Run" вы можете отладить вашу программу в онлайн-симуляторе, прежде чем перейти к запуску этой программы на собственной процессорной системе. В ЛР№14 вам будет рассказано, как скомпилировать программу самостоятельно, без помощи сторонних сервисов.
|
||||
7. Проверьте работоспособность вашей цифровой схемы в ПЛИС.
|
||||
|
||||
---
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Лабораторная работа №8 "Блок загрузки и сохранения"
|
||||
|
||||
Итогом шестой лабораторной работы стал практически завершенный процессор архитектуры RISC-V. Особенностью реализации процессора было отсутствие поддержки инструкций `LB`, `LBU`, `SB`, `LH`, `LHU`, `SH`. Тому было две причины:
|
||||
Итогом седьмой лабораторной работы стал практически завершенный процессор архитектуры RISC-V. Особенностью реализации процессора было отсутствие поддержки инструкций `LB`, `LBU`, `SB`, `LH`, `LHU`, `SH`. Тому было две причины:
|
||||
|
||||
- подключенный к памяти данных сигнал `byte_enable_i` был аппаратно зафиксирован на значении `4'b1111`, но на самом деле этим сигналом должен кто-то управлять;
|
||||
- необходимо подготовить считанные из памяти полуслова / байты для записи в регистровый файл.
|
||||
@@ -35,9 +35,9 @@ _Рисунок 1. Место LSU в микроархитектуре RISC-пр
|
||||
|
||||
### Интерфейс процессора и блока загрузки/сохранения
|
||||
|
||||
На входной порт `core_addr_i` от процессора поступает адрес ячейки памяти, к которой будет произведено обращение. Намеренье процессора обратиться к памяти (и для чтения, и для записи) отражается выставлением сигнала `core_req_i` в единицу. Если процессор собирается записывать в память, то сигнал `core_we_i` выставляется в единицу, а сами данные, которые следует записать, поступают от него на вход `core_wd_i`. Если процессор собирается читать из памяти, то сигнал `core_we_i` находится в нуле, а считанные данные подаются для процессора на выход `core_rd_o`.
|
||||
На входной порт `core_addr_i` от процессора поступает адрес ячейки памяти, к которой будет произведено обращение. Намеренье процессора обратиться к памяти (как для чтения, так и для записи) отражается выставлением сигнала `core_req_i` в единицу. Если процессор собирается записывать в память, то сигнал `core_we_i` выставляется в единицу, а сами данные, которые следует записать, поступают от него на вход `core_wd_i`. Если процессор собирается читать из памяти, то сигнал `core_we_i` находится в нуле, а считанные данные подаются на выход `core_rd_o`.
|
||||
|
||||
Инструкции `LOAD` и `STORE` в **RV32I** поддерживают обмен 8-битными, 16-битными или 32-битными значениями, однако в самом процессоре происходит работа только с 32-битными числами, поэтому загружаемые из памяти байты и послуслова необходимо предварительно расширить до 32-битного значения. Расширять значения можно либо знаковым битом, либо нулями — в зависимости от того как должно быть интерпретировано загружаемое число: как знаковое или беззнаковое. Во время записи данных в память, они не расширяются, поскольку в отличие от регистрового файла, основная память имеет возможность обновлять отдельные байты. Таким образом, различать знаковые и беззнаковые числа необходимо только на этапе загрузки, но не сохранения.
|
||||
Инструкции `LOAD` и `STORE` в **RV32I** поддерживают обмен 8-битными, 16-битными или 32-битными значениями, однако в самом процессоре происходит работа только с 32-битными числами, поэтому загружаемые из памяти байты и послуслова необходимо предварительно расширить до 32-битного значения. Расширять значения можно либо знаковым битом, либо нулями — в зависимости от того, как должно быть интерпретировано загружаемое число: как знаковое или беззнаковое. Во время записи данных в память, они не расширяются, поскольку в отличие от регистрового файла, основная память имеет возможность обновлять отдельные байты. Таким образом, различать знаковые и беззнаковые числа необходимо только на этапе загрузки, но не сохранения.
|
||||
|
||||
Для выбора разрядности и формата представления числа, на вход **LSU** подаётся сигнал `core_size_i`, принимающий следующие значения (для удобства использования, данные значения определены в виде параметров в пакете `decoder_pkg`):
|
||||
|
||||
@@ -55,7 +55,7 @@ _Рисунок 1. Место LSU в микроархитектуре RISC-пр
|
||||
|
||||
### Интерфейс блока загрузки/сохранения и памяти
|
||||
|
||||
Память данных имеет 32-битную разрядность ячейки памяти и поддерживает побайтовую адресацию. Это значит, что существует возможность обновить любой байт пределах одного слова (4-байтовой ячейки памяти), не изменяя слова целиком. Для указания на обновляемые байты интерфейс к памяти предусматривает использование 4-битного сигнала `mem_be_o`, подаваемого вместе с адресом слова `mem_addr_o`. Позиции битов 4-битного сигнала соответствуют позициям байт в слове. Если конкретный бит `mem_be_o` равен 1, то соответствующий ему байт в памяти будет обновлен. Данные для записи подаются на выход `mem_wd_o`. На результат чтения из памяти состояние `mem_be_o` не влияет, так как чтение производится всегда по 32-бита.
|
||||
Память данных имеет 32-битную разрядность ячейки памяти и поддерживает побайтовую адресацию. Это значит, что существует возможность обновить любой байт в пределах одного слова (4-байтовой ячейки памяти), не изменяя слова целиком. Для указания на обновляемые байты интерфейс к памяти предусматривает использование 4-битного сигнала `mem_be_o`, подаваемого вместе с адресом слова `mem_addr_o`. Позиции битов 4-битного сигнала соответствуют позициям байт в слове. Если конкретный бит `mem_be_o` равен 1, то соответствующий ему байт в памяти будет обновлен. Данные для записи подаются на выход `mem_wd_o`. На результат чтения из памяти состояние `mem_be_o` не влияет, так как чтение производится всегда по 32-бита.
|
||||
|
||||
После получения запроса на чтение/запись из ядра **LSU** перенаправляет запрос в память данных, взаимодействие осуществляется следующими сигналами:
|
||||
|
||||
@@ -110,7 +110,7 @@ _Рисунок 1. Место LSU в микроархитектуре RISC-пр
|
||||
|
||||
### mem_be_o
|
||||
|
||||
Данный сигнал принимает ненулевые значения только по запросу на запись (`core_req_i == 1`, `core_we_i == 1`), во время которого происходит мультиплексирование сигнала `core_size_i`. Если `core_size_i` соответствует инструкции записи байта (`LDST_B`, 3'd0), то в сигнале `mem_be_o` бит с индексом равным значению двух младших бит адреса `core_addr_i` должен быть равен единице.
|
||||
Данный сигнал является результатом мультиплексирования с управляющим сигналом `core_size_i`. Если `core_size_i` соответствует инструкции записи байта (`LDST_B`, 3'd0), то в сигнале `mem_be_o` бит с индексом равным значению двух младших бит адреса `core_addr_i` должен быть равен единице.
|
||||
|
||||
Допустим, пришёл запрос на запись байта по адресу 18:
|
||||
|
||||
@@ -164,7 +164,7 @@ _Рисунок 3. Временна́я диаграмма запросов на
|
||||
|
||||
Предположим, по адресам `16-19` лежит слово `32'hA55A_1881` (см. _рис. 4_). Чтение по любому из адресов 16, 17, 18, 19 вернет это слово на входном сигнале `mem_rd_i`. В случае инструкции `LB` (чтение байта, который интерпретируется как знаковое число, во время которого `core_size_i == LDST_B`) по адресу 19, в регистровый файл должно быть записано значение `32'hFFFF_FFA5`, поскольку по 19-ому адресу лежит байт `A5`, который затем будет знакорасширен. В случае той же самой инструкции, но по адресу 18, в регистровый файл будет записано значение `32'h0000_005A` (знакорасширенный байт `5A`, расположенный по 18ому адресу).
|
||||
|
||||
Получить нужный байт можно из входного сигнала `mem_rd_i`, но чтобы понять какие биты этого сигнала нас интересуют, необходимо посмотреть на входные сигналы `core_size_i` и `core_addr_i[1:0]`. `core_size_i` сообщит конкретный тип инструкции (сколько нужно взять байт из считанного слова), а `core_addr_i[1:0]` укажет номер начального байта, который нужно взять из `mem_rd_i`.
|
||||
Получить нужный байт можно из входного сигнала `mem_rd_i`, но чтобы понять какие биты этого сигнала нас интересуют, необходимо посмотреть на входные сигналы `core_size_i` и `core_addr_i[1:0]`. `core_size_i` сообщит размер порции данных (сколько нужно взять байт из считанного слова), а `core_addr_i[1:0]` укажет номер байта в `mem_rd_i`, с которого эта порция данных начинается..
|
||||
|
||||
В случае инструкции `LH` будет все тоже самое, только знакорасширяться будет не байт, а полуслово.
|
||||
|
||||
|
@@ -19,5 +19,6 @@ _Рисунок 1. Подключение LSU в процессорную сис
|
||||
1. Интегрируйте модули `lsu` и `data_mem` в модуль `processor_system`.
|
||||
1. Обратите внимание, что из модуля `processor_system` необходимо убрать логику сигнала `stall`, т.к. она была перемещена внутрь модуля `lsu`.
|
||||
2. После интеграции модулей, проверьте процессорную систему с помощью [программы](../07.%20Datapath/#Задание) и верификационного окружения из ЛР№7.
|
||||
1. Обратите внимание на то, как теперь исполняются инструкции `sw`, `sh`, `sb`, `lw`, `lh`, `lb`, `lhu`, `lbu`.
|
||||
1. Как и в случае с проверкой процессора архитектуры CYBERcobra, вам не будет сказано пройден тест или нет. Вам необходимо самостоятельно, такт за тактом проверить, что процессор правильно выполняет описанные в [_листинге 1_](../07.%20Datapath/#Задание) ЛР№7 инструкции (см. порядок выполнения задания ЛР№4). Для этого, необходимо сперва самостоятельно рассчитать что именно должна сделать данная инструкция, а потом проверить что процессор сделал именно это.
|
||||
2. Обратите внимание на то, как теперь исполняются инструкции `sw`, `sh`, `sb`, `lw`, `lh`, `lb`, `lhu`, `lbu`.
|
||||
3. Данная лабораторная работа не предполагает проверки в ПЛИС.
|
||||
|
@@ -23,9 +23,9 @@
|
||||
|
||||
С компьютером постоянно происходят события, на которые он должен реагировать, запуская соответствующие подпрограммы. Например, при движении мышки нужно перерисовать её курсор на новом месте или нужно среагировать на подключение флешки и т.п. Возможность запускать нужные подпрограммы в ответ на различные события, возникающие внутри или снаружи компьютера, существенно расширяют его возможности. События, требующие внимания процессора называются **прерываниями** (**interrupt**). Происходящие события формируют запрос на прерывание процессору.
|
||||
|
||||
С.А. Орлов, Б.Я. Цилькер в учебнике "Организация ЭВМ и систем" дают следующее определение системе прерывания:
|
||||
С.А. Орлов в учебнике "Организация ЭВМ и систем" даёт следующее определение системе прерывания:
|
||||
|
||||
> **Система прерывания** – это совокупность программно-аппаратных средств, позволяющая процессору (при получении соответствующего запроса) на время прервать выполнение текущей программы, передать управление программе обслуживания поступившего запроса, по завершению которой и продолжить прерванную программу с того места, где она была остановлена[1, стр. 155].
|
||||
> **Система прерывания** – это совокупность аппаратных и программных средств, позволяющая вычислительной машине (при получении соответствующего запроса) на время прервать выполнение текущей программы, передать управление программе обслуживания поступившего запроса, а по завершении последней продолжить прерванную программу с того места, где она была прервана[1, стр. 133].
|
||||
|
||||
Прерывания делятся на **маски́руемые** — которые при желании можно игнорировать (на которые можно наложить [**битовую маску**](https://ru.wikipedia.org/wiki/Битовая_маска), отсюда ударение на второй слог), и **немаски́руемые** — которые игнорировать нельзя (например сбой генератора тактового синхроимпульса в микроконтроллерах семейства PIC24FJ512GU410[[2, стр. 130]](https://ww1.microchip.com/downloads/aemDocuments/documents/MCU16/ProductDocuments/DataSheets/PIC24FJ512GU410-Family-Data-Sheet-DS30010203D.pdf)). Прерывание похоже на незапланированный вызов функции, вследствие события в аппаратном обеспечении. Программа (функция), запускаемая в ответ на прерывание, называется **обработчиком прерывания**.
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
Как и любой другой вызов функции, при возникновении прерывания или исключения необходимо сохранить адрес возврата, перейти к программе обработчика, выполнить свою работу, восстановить контекст (не оставить никаких следов работы обработчика прерывания) и вернуться к программе, которую прервали.
|
||||
|
||||
Благодаря исключениям можно реализовать имитацию наличия каких-то аппаратных блоков программными средствами. Например, при отсутствии аппаратного умножителя, можно написать программу обработчика исключения неподдерживаемой инструкции умножения, реализующую алгоритм умножения через сложение и сдвиг. Тогда, каждый раз, когда в программе будет попадаться инструкция умножения, будет возникать исключение, приводящее к запуску обработчика, перемножающего числа и размещающего результат в нужные ячейки памяти. После выполнения обработчика управление возвращается программе, которая даже не поймёт, что что-то произошло и умножитель «ненастоящий».
|
||||
Благодаря исключениям можно реализовать имитацию наличия каких-то аппаратных блоков программными средствами. Например, при отсутствии аппаратного умножителя, можно написать программу обработчика исключения неподдерживаемой инструкции умножения, реализующую алгоритм умножения через сложение и сдвиг. Тогда, каждый раз, когда в программе попадётся инструкция умножения, будет возникать исключение, приводящее к запуску обработчика, перемножающего числа и размещающего результат в нужные ячейки памяти. После выполнения обработчика управление возвращается программе, которая даже не поймёт, что что-то произошло и умножитель «ненастоящий».
|
||||
|
||||
---
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
1. Под **исключением** будут подразумеваться нетипичные условия, произошедшие во время исполнения программы, связанные с инструкцией в текущем харте (_hart_, сокращение от **har**dware **t**hread — аппаратном потоке).
|
||||
2. Под **прерыванием** будут подразумеваться внешние асинхронные события, которые могут стать причиной непредвиденной передачи управления внутри текущего харта.
|
||||
3. Под **перехватом** (вариант глагольного использования слова _trap_, которое обычно переводят как "ловушка", что по мнению автора совершенно не раскрывает сути этого понятия) будет подразумеваться передача управления **обработчику перехватов** (_trap handler_), вызванная либо прерыванием, либо исключением.
|
||||
3. Под **перехватом** (вариант глагольного использования слова _trap_, которое обычно переводят как "ловушка", что по мнению авторов совершенно не раскрывает сути этого понятия) будет подразумеваться передача управления **обработчику перехватов** (_trap handler_), вызванная либо прерыванием, либо исключением.
|
||||
|
||||
Иными словами, **прерываниями** мы будем называть исключительно аппаратные (внешние, асинхронные) события, которые могут привести к **перехвату** (передаче управления обработчику). Под **исключениями** мы будем подразумевать исключительно программные (являющиеся следствием какой-то инструкции, синхронные) события, которые могут привести к **перехвату**.
|
||||
|
||||
@@ -134,7 +134,10 @@ _Таблица 2. Кодирование причины перехвата в
|
||||
- задать адрес вершины стека прерываний `mscratch`,
|
||||
- задать маску прерывания `mie`.
|
||||
|
||||
После чего уже можно переходить к исполнению основного потока инструкций. Обратите внимание, что маску прерываний следует задавать в последнюю очередь, т.к. в противном случае система может начать реагировать на прерывания, не имея в регистре `mtvec` корректного адреса вектора прерываний.
|
||||
После чего уже можно переходить к исполнению основного потока инструкций.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание, что маску прерываний следует задавать в последнюю очередь, т.к. в противном случае система может начать реагировать на прерывания, не имея в регистре `mtvec` корректного адреса вектора прерываний.
|
||||
|
||||
### Реализация прерываний в архитектуре RISC-V
|
||||
|
||||
@@ -150,9 +153,9 @@ _Таблица 2. Кодирование причины перехвата в
|
||||
|
||||
После выполнения программы-обработчика перехвата, возвращение в программу выполняется командой возврата `mret`, которая помещает в `PC` значение регистра `mepc`. Сохранение `PC` инструкции при прерывании в `mepc` аналогично использованию регистра `ra` для хранения обратного адреса во время инструкции `jal`. Поскольку обработчики перехватов могут использовать для своей работы регистровый файл, для хранения и восстановления значений его регистров им нужен отдельный стек, на который указывает `mscratch`.
|
||||
|
||||
Контроллер прерываний – это блок процессора, обеспечивающий взаимодействие с устройствами, запрашивающими прерывания, формирование кода причины прерывания для процессора, маскирование прерываний. В некоторых реализация, контроллер прерываний может реагировать на прерывания в соответствии с приоритетом.
|
||||
Контроллер прерываний – это блок процессора, обеспечивающий взаимодействие с устройствами, запрашивающими прерывания, формирование кода причины прерывания для процессора, маскирование прерываний. В некоторых реализациях, контроллер прерываний может реагировать на прерывания в соответствии с приоритетом.
|
||||
|
||||
Периферийное устройство, которое может генерировать прерывание, подключается к контроллеру прерывания парой проводов: "запрос на прерывание" (`irq_req_i`) и "прерывание обслужено" (`irq_ret_o`). Предположим, к контроллеру прерываний подключили клавиатуру. Когда на ней нажимают клавишу, код этой клавиши попадает в буферный регистр с дополнительным управляющим битом, выставленным в единицу, который подключён к входу запроса на прерывание. Если прерывание не замаскировано (в нашем процессоре это означает, что нулевой бит регистра `mie` выставлен в 1), то контроллер прерывания сгенерирует код причины прерывания (в нашем случае — это константа `0x80000010`). Кроме этого, контроллер прерывания подаст сигнал `irq_o`, чтобы устройство управления процессора узнало, что произошло прерывание и разрешило обновить содержимое регистра причины `mcause`, сохранило адрес прерванной инструкции в `mepc` и загрузило в `PC` вектор прерывания `mtvec`.
|
||||
Периферийное устройство, которое может генерировать прерывание, подключается к контроллеру прерывания парой проводов: "запрос на прерывание" (`irq_req_i`) и "прерывание обслужено" (`irq_ret_o`). Предположим, к контроллеру прерываний подключили клавиатуру. Когда на ней нажимают клавишу, код этой клавиши попадает в буферный регистр с дополнительным управляющим битом, выставленным в единицу, который подключён к входу запроса на прерывание. Если прерывание не замаскировано (в нашем процессоре это означает, что 16-ый бит регистра `mie` выставлен в 1), то контроллер прерывания сгенерирует код причины прерывания (в нашем случае — это константа `0x80000010`). Кроме этого, контроллер прерывания подаст сигнал `irq_o`, чтобы устройство управления процессора узнало, что произошло прерывание и разрешило обновить содержимое регистра причины `mcause`, сохранило адрес прерванной инструкции в `mepc` и загрузило в `PC` вектор прерывания `mtvec`.
|
||||
|
||||
Когда будет выполняться инструкция `mret`, устройство управления подаст сигнал контроллеру прерывания, чтобы тот, в свою очередь, направил его в виде сигнала «прерывание обслужено» для соответствующего устройства. После этого периферийное устройство обязано снять сигнал запроса прерывания хотя бы на один такт. В нашем примере сигнал «прерывание обслужено» может быть подключён непосредственно к сбросу буферного регистра клавиатуры.
|
||||
|
||||
@@ -195,7 +198,7 @@ _Рисунок 5. Структурная схема контроллера пр
|
||||
Контроллер состоит из логики:
|
||||
|
||||
- обработки вложенных прерываний, частью которой являются регистры отслеживания обработки прерывания и исключения (`irq_h` и `exc_h` соответственно),
|
||||
- установки и сброса этих регистров (которая вместе с этими регистрами заключена в штрихованные прямоугольники),
|
||||
- установки и сброса этих регистров (которая вместе с этими регистрами заключена в прямоугольники, выделенные пунктиром),
|
||||
- приоритета исключений над прерываниями,
|
||||
- маскирования запросов на прерывание.
|
||||
|
||||
@@ -215,11 +218,11 @@ _Рисунок 5. Структурная схема контроллера пр
|
||||
|
||||
Логика приоритета исключений над прерываниями заключается в том, что сигнал `exception_i` является частью логики обработки вложенных прерываний. Пройдя через два логических ИЛИ и последующий инвертор, этот сигнал обнулит запрос на прерывание на логическом И в правом верхнем углу.
|
||||
|
||||
Логика маскирования запросов на прерывания заключается в простейшем И между запросом на прерывания (`irq_req_i`) и сигналом разрешения прерывания (`mie_i`).
|
||||
Логика маскирования запросов на прерывания заключается в побитовом И между запросом на прерывания (`irq_req_i`) и сигналом разрешения прерывания (`mie_i`).
|
||||
|
||||
## Пример обработки перехвата
|
||||
|
||||
В _листинге 1_ представлен пример программы с обработчиком перехватов. Программа начинается с инициализации начальных значений регистров управления, указателя на верхушку стека и глобальную область данных, после чего уходит в бесконечный цикл ничего не делая, до тех пор, пока не произойдёт перехват.
|
||||
В _листинге 1_ приведён пример программы с обработчиком перехватов. Программа начинается с инициализации начальных значений регистров управления, указателя на верхушку стека и глобальную область данных, после чего уходит в бесконечный цикл ничего не делая, до тех пор, пока не произойдёт перехват.
|
||||
|
||||
Алгоритм работы обработчика перехвата (`trap handler`) выглядит следующим образом:
|
||||
|
||||
@@ -242,7 +245,7 @@ _start:
|
||||
|
||||
0С: la x5, trap_handler # псевдоинструкция la аналогично li загружает число,
|
||||
10: # только в случае la — это число является адресом
|
||||
# указанного места (адреса обработчика перехвата)
|
||||
# указанного места (адресом обработчика перехвата)
|
||||
# данная псевдоинструкция будет разбита на две
|
||||
# инструкции: lui и addi
|
||||
|
||||
@@ -276,7 +279,7 @@ main:
|
||||
|
||||
# ОБРАБОТЧИК ПЕРЕХВАТА
|
||||
# Без стороннего вмешательства процессор никогда не перейдет к инструкциям ниже,
|
||||
# однако в случае прерывания в программный счетчик будет загружен адрес первой
|
||||
# однако в случае перехвата в программный счетчик будет загружен адрес первой
|
||||
# нижележащей инструкции.
|
||||
|
||||
# Сохраняем используемые регистры на стек
|
||||
@@ -306,7 +309,7 @@ exc_handler: # Проверяем произошло ли иск
|
||||
78: bne x6, x7, done # если это не оно, то выходим
|
||||
|
||||
# Обработчик исключения
|
||||
7С: csrr x6, mepc # Узнаем значение PC (адреса инструкции,
|
||||
7С: csrr x6, mepc # Узнаём значение PC (адреса инструкции,
|
||||
# вызвавшей исключение)
|
||||
80: lw x7, 0x0(x6) # Загружаем эту инструкцию в регистр x7.
|
||||
# В текущей микроархитектурной реализации это
|
||||
@@ -403,9 +406,9 @@ endmodule
|
||||
|
||||
## Список использованной литературы
|
||||
|
||||
1. С.А. Орлов, Б.Я. Цилькер / Организация ЭВМ и систем: Учебник для вузов. 2-е изд. / СПб.: Питер, 2011.
|
||||
1. Орлов С.А. Организация ЭВМ и систем: Учебник для вузов. 4-е изд. дополненное и переработанное / С.А. Орлов. - Санкт-Петербург : Питер, 2021. - 688 с.
|
||||
2. [PIC24FJ512GU410 Family Data Sheet](https://ww1.microchip.com/downloads/aemDocuments/documents/MCU16/ProductDocuments/DataSheets/PIC24FJ512GU410-Family-Data-Sheet-DS30010203D.pdf)
|
||||
3. [The Art of Assembly Language](https://flint.cs.yale.edu/cs422/doc/art-of-asm/pdf/)
|
||||
3. [Hyde, Randall. The Art of Assembly Language. DOS/16-bit edition.](https://flint.cs.yale.edu/cs422/doc/art-of-asm/pdf/)
|
||||
4. [The RISC-V Instruction Set Manual Volume I: Unprivileged ISA](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/unpriv-isa-asciidoc.pdf)
|
||||
5. [Pillai, V.P., Megalingam, R.K. (2022). System Partitioning with Virtualization for Federated and Distributed Machine Learning on Critical IoT Edge Systems. In: Saraswat, M., Sharma, H., Balachandran, K., Kim, J.H., Bansal, J.C. (eds) Congress on Intelligent Systems. Lecture Notes on Data Engineering and Communications Technologies, vol 111. Springer, Singapore.](https://doi.org/10.1007/978-981-16-9113-3_33)
|
||||
6. [The RISC-V Instruction Set Manual Volume II: Privileged Architecture](https://github.com/riscv/riscv-isa-manual/releases/download/20240411/priv-isa-asciidoc.pdf)
|
||||
|
@@ -76,12 +76,13 @@ _Рисунок 2. Структурная схема блока приорите
|
||||
|
||||
_Рисунок 3. Структурная схема блока приоритетных прерываний._
|
||||
|
||||
Обратите внимание, что разрядность сигналов `irq_req_i`, `mie_i`, `irq_ret_o` изменилась. Теперь это 16-разрядные сигналы. Сигнал, который ранее шёл на выход к `irq_ret_o` теперь идёт на вход `irq_ret_i` модуля `daisy_chain`. Формирование кода причины прерывания `irq_cause_o` перенесено в модуль `daisy_chain`.
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание, что разрядность сигналов `irq_req_i`, `mie_i`, `irq_ret_o` изменилась. Теперь это 16-разрядные сигналы. Сигнал, который ранее шёл на выход к `irq_ret_o` теперь идёт на вход `irq_ret_i` модуля `daisy_chain`. Формирование кода причины прерывания `irq_cause_o` перенесено в модуль `daisy_chain`.
|
||||
|
||||
## Порядок выполнения работы
|
||||
|
||||
1. Опишите модуль `daisy_chain`.
|
||||
1. При формировании верхнего массива элементов И с _рис. 2_, вам необходимо воспользоваться сформировать 16 непрерывных присваиваний через блок `generate for`.
|
||||
1. При формировании верхнего массива элементов И с _рис. 2_, вам необходимо сформировать 16 непрерывных присваиваний через блок `generate for`.
|
||||
2. Формирование нижнего массива элементов И можно сделать с помощью одного непрерывного присваивания посредством операции побитовое И.
|
||||
2. Проверьте модуль `daisy_chain` с помощью верификационного окружения, представленного в файле [`lab_12.tb_daisy_chain`](lab_12.tb_daisy_chain.sv). В случае, если в TCL-консоли появились сообщения об ошибках, вам необходимо [найти](../../Vivado%20Basics/05.%20Bug%20hunting.md) и исправить их.
|
||||
1. Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в `Simulation Sources`.
|
||||
|
@@ -20,7 +20,7 @@
|
||||
|
||||
## Теория
|
||||
|
||||
Помимо процессора и памяти, третьим ключевым элементом вычислительной системы является система ввода/вывода, обеспечивающая обмен информации между ядром вычислительной машины и периферийными устройствами [1, стр. 364].
|
||||
Помимо процессора и памяти, третьим ключевым элементом архитектуры вычислительной машины является **система ввода/вывода**, обеспечивающая обмен информацией между ядром вычислительной машины и разнообразными периферийными устройствами [1, стр. 351].
|
||||
|
||||
Любое периферийное устройство со стороны вычислительной машины видится как набор ячеек памяти (регистров). С помощью чтения и записи этих регистров происходит обмен информации с периферийным устройством, и управление им. Например, датчик температуры может быть реализован самыми разными способами, но для процессора он в любом случае ячейка памяти, из которой он считывает число – температуру.
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
|
||||
_Рисунок 1. Итоговая структура процессорной системы._
|
||||
|
||||
Обратите внимание на то, что на вход `mem_ready_i` модуля `lsu` подаётся единица. Вообще говоря, каждый модуль-контроллер периферийного устройства должен содержать выходной сигнал `ready_o`, который должен мультиплексироваться с остальными подобно тому, как мультиплексируются сигналы read_data_o. На вход `lsu` должен подаваться выход мультиплексора. Однако, поскольку все модули достаточно просты, чтобы, как и у памяти данных, выходной сигнал `ready_o` был всегда равен единице (а также для упрощения _рис. 1_), эти сигналы были убраны из микроархитектуры. В случае, если вы решите добавить в процессорную систему периферийное устройство, сигнал `ready_o` которого не будет равен константной единице, логику управления входом `mem_ready_i` модуля `lsu` будет необходимо обновить описанным выше способом.
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на то, что на вход `mem_ready_i` модуля `lsu` подаётся единица. Вообще говоря, каждый модуль-контроллер периферийного устройства должен содержать выходной сигнал `ready_o`, который должен мультиплексироваться с остальными подобно тому, как мультиплексируются сигналы read_data_o. На вход `lsu` должен подаваться выход мультиплексора. Однако, поскольку все модули достаточно просты, чтобы, как и у памяти данных, выходной сигнал `ready_o` был всегда равен единице (а также для упрощения _рис. 1_), эти сигналы были убраны из микроархитектуры. В случае, если вы решите добавить в процессорную систему периферийное устройство, сигнал `ready_o` которого не будет равен константной единице, логику управления входом `mem_ready_i` модуля `lsu` будет необходимо обновить описанным выше способом.
|
||||
|
||||
### Активация выбранного устройства
|
||||
|
||||
@@ -57,7 +58,7 @@ _Рисунок 1. Итоговая структура процессорной
|
||||
|
||||
Реализация унитарного кодирования предельно проста:
|
||||
|
||||
- Нулевой сигнал этой шины будет равен единице только если `data_addr_o[31:24] = 8'd0`.
|
||||
- Нулевой бит этой шины будет равен единице только если `data_addr_o[31:24] = 8'd0`.
|
||||
- Первый бит этой шины будет равен единице только если `data_addr_o[31:24] = 8'd1`.
|
||||
- ...
|
||||
- Двести пятьдесят пятый бит шины будет равен единице только если `data_addr_o[31:24] = 8'd255`.
|
||||
@@ -100,9 +101,10 @@ module processor_system(
|
||||
endmodule
|
||||
```
|
||||
|
||||
Эти порты нужно подключить к одноименным портам ваших контроллеров периферии (**речь идёт только о реализуемых вами контроллерах, остальные порты должны остаться неподключенными**). Иными словами, в описании модуля должны быть все указанные входы и выходы. Но использовать вам нужно только порты, связанные с теми периферийными устройствами, реализацию которых вам необходимо подключить к процессорной системе в рамках индивидуального задания.
|
||||
Эти порты нужно подключить к одноимённым портам ваших контроллеров периферии (**речь идёт только о реализуемых вами контроллерах, остальные порты должны остаться неподключенными**). Иными словами, в описании модуля должны быть все указанные входы и выходы. Но использовать вам нужно только порты, связанные с теми периферийными устройствами, реализацию которых вам необходимо подключить к процессорной системе в рамках индивидуального задания.
|
||||
|
||||
Обратите внимание на то, что изменился сигнал сброса (`resetn_i`). Буква `n` на конце означает, что сброс работает по уровню `0` (в таком случае говорят, что **активный уровень** данного сигнала `0`: когда сигнал равен нулю — это сброс, когда единице — не сброс).
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на то, что изменился сигнал сброса (`resetn_i`). Буква `n` на конце означает, что сброс работает по уровню `0` (в таком случае говорят, что **активный уровень** данного сигнала `0`: когда сигнал равен нулю — это сброс, когда единице — не сброс).
|
||||
|
||||
Помимо прочего, необходимо подключить к вашему модулю `блок делителя частоты`. Поскольку в данном курсе лабораторных работ вы выполняли реализацию однотактного процессора, инструкция должна пройти через все ваши блоки за один такт. Из-за этого критический путь схемы не позволит использовать тактовый сигнал частотой в `100 МГц`, от которого работает отладочный стенд. Поэтому, необходимо создать отдельный сигнал с пониженной тактовой частотой, от которого будет работать ваша схема.
|
||||
|
||||
@@ -134,7 +136,7 @@ _Таблица 1. Карта памяти периферийных устрой
|
||||
- **(R)** означает что разрешён доступ только на чтение (операция записи по этому адресу должна игнорироваться вашим контроллером).
|
||||
- **"Выставленное на переключателях значение"** означает ровно то, что и означает. Если процессор выполняет операцию чтения по адресу `0x01000000` (`0x01` [старшая часть адреса переключателей] + `0x000000` [младшая часть адреса для получения выставленного на переключателях значения]), то контроллер должен выставить на выходной сигнал `RD` значение на переключателях (о том, как получить это значение будет рассказано чуть позже).
|
||||
|
||||
Рассмотрим ещё один пример. При обращении по адресу `0x02000024` (`0x02` [старшая часть адреса контроллера светодиодов] + `0x000024` [младшая часть адреса для доступа на запись к регистру сброса] ) должна произойти запись в регистр сброса, который должен сбросить значения в регистре управления зажигаемых светодиодов и регистре управления режимом "моргания" светодиодов (подробнее о том как должны работать эти регистры будет ниже).
|
||||
Рассмотрим ещё один пример. При обращении по адресу `0x02000024` (`0x02` [старшая часть адреса контроллера светодиодов] + `0x000024` [младшая часть адреса для доступа на запись к регистру сброса] ) должна произойти запись в регистр сброса, который должен сбросить значения в регистре управления зажигаемых светодиодов и регистре управления режимом "моргания" светодиодов (подробнее о том как должны работать эти регистры будет далее).
|
||||
|
||||
Таким образом, каждый контроллер периферийного устройства должен выполнять две вещи:
|
||||
|
||||
@@ -152,7 +154,7 @@ _Таблица 1. Карта памяти периферийных устрой
|
||||
3. Добавьте в проект пакет [`peripheral_pkg`](peripheral_pkg.sv). Данный пакет содержит старшие части адресов периферии в виде параметров, а также вспомогательные вызовы, используемые верификационным окружением.
|
||||
4. Реализуйте модули контроллеров периферии. Имена модулей и их порты будут указаны в [описании контроллеров](#описание-контроллеров-периферийных-устройств). Пример разработки контроллера приведен в [примере описания модуля контроллера](../../Basic%20Verilog%20structures/Controllers.md).
|
||||
1. Готовые модули периферии, управление которыми должны осуществлять модули-контроллеры хранятся в папке `peripheral modules`.
|
||||
5. Обновите модуль `processor_system` в соответствии с разделом ["Дополнительные правки модуля processor_system"](#дополнительные-правки-модуля-processor_system).
|
||||
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_.
|
||||
@@ -162,7 +164,7 @@ _Таблица 1. Карта памяти периферийных устрой
|
||||
2. Во время интеграции, вам необходимо использовать старшую часть адреса, представленную в карте памяти для формирования сигнала `req_i` для ваших модулей-контроллеров.
|
||||
7. Проверьте работу процессорной системы с помощью моделирования.
|
||||
1. Для моделирования используйте тестбенч `lab_13_tb_system`.
|
||||
2. Для каждой пары контроллеров в папке `firmware/mem_files` представлены файлы, инициализирующие память инструкций. Содержимым одного из файлов, соответствующих паре периферийных устройств вашего варианта необходимо заменить содержимое файла `program.mem` в `Design Sources` проекта. Обратите внимание, что для пары "PS2-VGA" также необходим файл, инициализирующий память данных (в модуле `data_mem` необходимо добавить вызов инициализирующей функции `$readmemh` в блоке `initial`).
|
||||
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. После чего, необходимо убедиться, что процессор среагировал на данное прерывание, и в процессе его обработки в контроллер устройства вывода были поданы выходные данные.
|
||||
@@ -176,7 +178,7 @@ _Таблица 1. Карта памяти периферийных устрой
|
||||
10. Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
|
||||
1. Обратите внимание, что в данной лабораторной уже не будет модуля верхнего уровня `nexys_...`, так как ваш модуль процессорной системы уже полностью самостоятелен и взаимодействует непосредственно с ножками ПЛИС через модули, управляемые контроллерами периферии.
|
||||
2. Для проверки периферии переключателей и светодиодов будет достаточно одного лишь отладочного стенда. Для проверки всей остальной периферии может могут потребоваться: компьютер (для uart_rx / uart_tx), клавиатура (для контроллера клавиатуры) и VGA-монитор для VGA-контроллера.
|
||||
1. Чтобы проверить работоспособность контроллеров UART, необходимо запустить на компьютере программу Putty, в настройках программы указать настройки, которыми будет сконфигурирован программой ваш контроллер (либо указать значения, которыми сбрасываются регистры, если программа ничего не настраивает) и COM-порт, через который компьютер будет общаться с контроллером. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню пуск.
|
||||
1. Чтобы проверить работоспособность контроллеров UART, необходимо запустить на компьютере программу Putty, во вкладке `Connections->Serial` указать настройки, которыми будет сконфигурирован программой ваш контроллер (либо указать значения регистров после сброса) и COM-порт, через который компьютер будет общаться с контроллером. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню кнопки "Пуск".
|
||||
В данном окне необходимо найти вкладку "Порты (COM и LPT)", раскрыть её, а затем подключить отладочный стенд через USB-порт (если тот ещё не был подключён). В списке появится новое устройство, а в скобках будет указан нужный COM-порт.
|
||||
2. Несмотря на то, что описанный контроллер клавиатуры позволяет управлять клавиатурой с интерфейсом PS/2, некоторые платы (например, Nexys A7) позволяют подключать вместо них клавиатуры с USB-интерфейсом. Дело в том, что PS/2 уже давно устарел и найти клавиатуры с таким интерфейсом — задача непростая. Однако протокол передачи по этому интерфейсу очень удобен для образовательных целей, поэтому некоторые производители просто ставят на платы переходник с USB на PS/2, позволяя объединить простоту разработки с удобством использования.
|
||||
|
||||
@@ -195,7 +197,8 @@ _Таблица 1. Карта памяти периферийных устрой
|
||||
3. На входе `write_enable_i` выставлено значение `0`.
|
||||
4. На входе `addr_i` выставлено значение `0xАДРЕС`
|
||||
|
||||
Обратите внимание на то, что **запрос на чтение** должен обрабатываться **синхронно** (выходные данные должны выдаваться по положительному фронту `clk_i`) так же, как был реализован порт на чтение памяти данных в [ЛР№6](../06.%20Main%20memory/).
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на то, что **запрос на чтение** должен обрабатываться **синхронно** (выходные данные должны выдаваться по положительному фронту `clk_i`) так же, как был реализован порт на чтение памяти данных в [ЛР№6](../06.%20Main%20memory/).
|
||||
|
||||
При описании поддерживаемых режимов доступа по данному адресу используются следующее обозначения:
|
||||
|
||||
@@ -294,7 +297,7 @@ endmodule
|
||||
|
||||
Регистр `led_mode` отвечает за режим вывода данных на светодиоды. Когда этот регистр равен единице, светодиоды должны "моргать" выводимым значением. Под морганием подразумевается вывод значения из регистра `led_val` на выход `led_o` на одну секунду (загорится часть светодиодов, соответствующие которым биты шины `led_o` равны единице), после чего на одну секунду выход `led_o` необходимо подать нули. Запись и чтение регистра `led_mode` осуществляется по адресу `0x04`.
|
||||
|
||||
Отсчёт времени можно реализовать простейшим счётчиком, каждый такт увеличивающимся на 1 и сбрасывающимся по достижении определенного значения, чтобы продолжить считать с нуля. Зная тактовую частоту, нетрудно определить до скольки должен считать счётчик. При тактовой частоте в 10 МГц происходит 10 миллионов тактов в секунду. Это означает, что при такой тактовой частоте через секунду счётчик будет равен `10⁷-1` (счёт идёт с нуля). Тем не менее удобней будет считать не до `10⁷-1` (что было бы достаточно очевидным и тоже правильным решением), а до `2*10⁷-1`. В этом случае старший бит счётчика каждую секунду будет инвертировать своё значение, что может быть использовано при реализации логики "моргания".
|
||||
Отсчёт времени можно реализовать простейшим счётчиком, каждый такт увеличивающимся на 1 и сбрасывающимся по достижении определенного значения, чтобы продолжить считать с нуля. Зная тактовую частоту, нетрудно определить до скольки должен считать счётчик. При тактовой частоте в 10 МГц происходит 10 миллионов тактов в секунду. Это означает, что при такой тактовой частоте через секунду счётчик будет равен `10⁷-1` (счёт идёт с нуля). Тем не менее удобней будет считать не до `10⁷-1` (что было бы достаточно очевидным и тоже правильным решением), а до `2*10⁷-1`. В этом случае результат сравнения значения счётчика с 10⁷ будет меняться каждую секунду, что может быть использовано при реализации логики "моргания".
|
||||
|
||||
Важно отметить, что счётчик должен работать только при `led_mode == 1`, в противном случае счётчик должен быть равен нулю.
|
||||
|
||||
@@ -598,7 +601,7 @@ endmodule
|
||||
|
||||
Выходной сигнал `busy_o` на каждом такте `clk_i` должен записываться в регистр `busy`, доступ на чтение к которому осуществляется по адресу `0x08`.
|
||||
|
||||
Значения входных сигналов `baudrate_i`, `parity_en_i`, `stopbit_i` берутся из соответствующих регистров, доступ на запись к которым осуществляется по адресам `0x0C`, `0x10`, `0x14` соответственно, но только в моменты, когда выходной сигнал `busy_o` равен нулю. Иными словами, изменение настроек передачи возможно только в моменты, когда передача не происходит. Доступ на чтение этих регистров может осуществляться в любой момент времени.
|
||||
Значения входных сигналов `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`.
|
||||
|
||||
@@ -610,7 +613,7 @@ endmodule
|
||||
|
||||
На вход `tx_valid_i` модуля `uart_tx` подаётся единица в момент выполнения **запроса на запись** по адресу `0x00` (при сигнале `busy` равном нулю). В остальное время на вход этого сигнала подаётся `0`.
|
||||
|
||||
В случае **запроса на запись** значения `1` по адресу `0x24` (адресу сброса), все регистры модуля-контроллера должны сброситься. При этом регистр `baudrate` должен принять значение `9600`, регистр, `stopbit` должен принять значение `1`. Остальные регистры должны принять значение `0`.
|
||||
В случае **запроса на запись** значения `1` по адресу `0x24` (адресу сброса), все регистры модуля-контроллера должны сброситься. При этом регистр `baudrate` должен принять значение `9600`, регистр `stopbit` должен принять значение `1`. Остальные регистры должны принять значение `0`.
|
||||
|
||||
Адресное пространство контроллера `uart_rx_sb_ctrl` представлено в _таблице 6_.
|
||||
|
||||
@@ -655,7 +658,7 @@ _Рисунок 2. Пример игры с использованием сим
|
||||
|
||||
_Таблица 8. Адресное пространство контроллера VGA._
|
||||
|
||||
Для того, чтобы вывести символ на экран, необходимо использовать адрес этого символа на сетке `80x30` (диапазон адресов `char_map`). К примеру, мы хотим вывести символ в верхнем левом углу (т.е. нулевой символ нулевой строки). Это нулевой символ в диапазоне адресов `char_map`. Поскольку данный диапазон начинается с адреса `0x0000_0000`, запись по этому адресу приведёт к отображению символа, соответствующего [ASCII-коду](https://www.asciitable.com/), пришедшему на `write_data_i`.
|
||||
Для того, чтобы вывести символ на экран, необходимо использовать адрес этого символа на сетке `80x30` (диапазон адресов `char_map`). К примеру, мы хотим вывести символ в верхнем левом углу (т.е. нулевой символ нулевой строки). Эта позиция расположена по адресу "0" в диапазоне адресов `char_map`. Поскольку данный диапазон начинается с адреса `0x0000_0000`, запись по этому адресу приведёт к отображению символа, соответствующего [ASCII-коду](https://www.asciitable.com/), пришедшему на `write_data_i`.
|
||||
|
||||
Если мы хотим вывести нулевой (левый) символ в первой строке (счёт ведётся с нуля), то необходимо произвести запись по адресу `1*80 + 0 = 80 = 0x0000_0050`.
|
||||
|
||||
@@ -683,7 +686,7 @@ _Рисунок 3. Цветовая палитра vga-модуля._
|
||||
|
||||
К примеру, мы хотим установить черный фоновый цвет и белый цвет в качестве цвета символа для верхней левой позиции. В этом случае, мы должны записать значение `f0` (f(15) — код белого цвета, 0 — код черного цвета) по адресу `0x0000_1000` (нулевой адрес в диапазоне `color_map`).
|
||||
|
||||
Для отрисовки символов, мы условно поделили экран на сетку `80х30`, и для каждой позиции в этой сетке определили фоновый и активный цвет. Чтобы модуль мог отрисовать символ на очередной позиции (которая занимает `16х8` пикселей), ему необходимо знать в какой цвет необходимо окрасить каждый пиксель для каждого ascii-кода. Для этого используется память шрифтов.
|
||||
Для отрисовки символов, мы условно поделили экран на сетку `80х30`, и для каждой позиции в этой сетке определили фоновый и активный цвет. Чтобы модуль мог отрисовать символ на очередной позиции (которая занимает `16х8` пикселей), ему необходимо знать какой пиксель в этой позиции для заданного ASCII-кода является пикселем фона, а какой — пикселем символа. Для этого используется память шрифтов.
|
||||
|
||||
Допустим, нам необходимо отрисовать символ `F` (ascii-код `0x46`).
|
||||
|
||||
@@ -691,9 +694,9 @@ _Рисунок 3. Цветовая палитра vga-модуля._
|
||||
|
||||
_Рисунок 4. Отрисовка символа `F` в разрешении 16х8 пикселей._
|
||||
|
||||
Данный символ состоит из 16 строчек по 8 пикселей. Каждый пиксель кодируется одним битом (горит/не горит, цвет символа/фоновый цвет). Каждая строчка кодируется одним байтом (8 бит на 8 пикселей). Таким образом, каждый сканкод требует 16 байт памяти.
|
||||
Данный символ состоит из 16 строчек по 8 пикселей. Каждый пиксель кодируется одним битом (горит/не горит, цвет символа/фоновый цвет). Каждая строчка кодируется одним байтом (8 бит на 8 пикселей). Таким образом, каждый символ в шрифте требует 16 байт памяти.
|
||||
|
||||
Данный модуль поддерживает 256 сканкодов. Следовательно, для хранения шрифта под каждый из 256 сканкодов требуется 16 * 256 = 4KiB памяти.
|
||||
Данный модуль поддерживает 256 символов. Следовательно, для хранения шрифта под каждый из 256 символов требуется 16 * 256 = 4KiB памяти.
|
||||
|
||||
Для хранения шрифтов в модуле отведён диапазон адресов `0x00002000-0x00002FFF`. В отличие от предыдущих диапазонов адресов, где каждый адрес был закреплён за соответствующей позицией символа в сетке `80x30`, адреса данного диапазона распределены следующим образом:
|
||||
|
||||
@@ -787,6 +790,8 @@ module vga_sb_ctrl (
|
||||
);
|
||||
```
|
||||
|
||||
При интеграции модуля в процессорную систему, ко входу `clk100m_i` необходимо подключить провод `clk_i` (а ко входу `clk_i` — провод `sysclk`).
|
||||
|
||||
Реализация данного модуля исключительно простая. В первую очередь необходимо подключить одноименные сигналы напрямую:
|
||||
|
||||
- `clk_i`,
|
||||
@@ -835,6 +840,6 @@ module vga_sb_ctrl (
|
||||
|
||||
## Список использованной литературы
|
||||
|
||||
1. С.А. Орлов, Б.Я. Цилькер / Организация ЭВМ и систем: Учебник для вузов. 2-е изд. / СПб.: Питер, 2011.
|
||||
1. Орлов С.А. Организация ЭВМ и систем: Учебник для вузов. 4-е изд. дополненное и переработанное / С.А. Орлов. - Санкт-Петербург : Питер, 2021. - 688 с.
|
||||
2. [Rebelstar](https://en.wikipedia.org/wiki/Rebelstar)
|
||||
3. [Easycode](https://fontstruct.com/fontstructions/show/346317/easycode)
|
||||
|
@@ -31,13 +31,13 @@
|
||||
|
||||
> — Но разве в процессе компиляции исходного кода на языке Си мы не получаем программу, написанную на языке ассемблера? Получится ведь тот же код, что мы могли написать и сами.
|
||||
|
||||
Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто лупили по случайным адресам и все получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Ведь если вы будете пользоваться одними и теми же регистрами, вызов одной функции может испортить данные в другой. Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится ещё один коллега — придётся делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#соглашение-о-вызовах) (calling convention).
|
||||
Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто использовали случайные адреса и всё получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Ведь если вы будете пользоваться одними и теми же регистрами, вызов одной функции может испортить данные в другой. Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится ещё один коллега — придётся делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#соглашение-о-вызовах) (calling convention).
|
||||
|
||||
Таким образом, генерируя ассемблерный код, компилятор не может так же, как это делали вы, использовать все ресурсы без каких-либо ограничений — он должен следовать ограничениям, накладываемым на него соглашением о вызовах, а также ограничениям, связанным с тем, что он ничего не знает о памяти устройства, в котором будет исполняться программа — а потому он не может работать с памятью абы как. Работая с памятью, компилятор следует некоторым правилам, благодаря которым после компиляции компоновщик сможет собрать программу под ваше устройство с помощью специального скрипта.
|
||||
|
||||
### Соглашение о вызовах
|
||||
|
||||
Соглашение о вызовах [устанавливает](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases/download/v1.0/riscv-abi.pdf) порядок вызова функций: где размещаются аргументы при вызове функций, где находятся указатель на стек и адрес возврата и т.п.
|
||||
Соглашение о вызовах [устанавливает](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases/download/v1.0/riscv-abi.pdf) порядок вызова функций: где размещаются аргументы при вызове функций, где находятся указатель на стек, адрес возврата и т.п.
|
||||
|
||||
Кроме того, соглашение делит регистры регистрового файла на две группы: оберегаемые и необерегаемые регистры.
|
||||
|
||||
@@ -71,7 +71,7 @@ _Таблица 1. Ассемблерные мнемоники для целоч
|
||||
|
||||
Несмотря на то, что указатель на стек помечен как Callee-saved регистр, это не означает, что вызываемая функция может записать в него что заблагорассудится, предварительно сохранив его значение на стек. Ведь как вы вернёте значение указателя на стек со стека, если в регистре указателя на стек лежит что-то не то?
|
||||
|
||||
Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путём сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функции все сохранённые на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на то что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из неё, его значение в итоге останется тем же.
|
||||
Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путём сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функции все сохранённые на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на, то что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из неё, его значение в итоге останется тем же.
|
||||
|
||||
### Скрипт для компоновки (linker_script.ld)
|
||||
|
||||
@@ -85,7 +85,7 @@ _Таблица 1. Ассемблерные мнемоники для целоч
|
||||
|
||||
Кроме того, вы в любой момент можете изменить значение счетчика адресов. Например, если адресное пространство памяти поделено на две части: под инструкции отводится 512 байт, а под данные 1024 байта. Таким образом, выделенный диапазон адресов для инструкций: `[0:511]`, а для данных: `[512:1535]`. Предположим при этом, что общий объем секций `.text` составляет 416 байт. В этом случае, вы можете сперва разместить секции `.text` так же, как было описано в предыдущем примере, а затем, выставив значение на счетчике адресов равное `512`, описать размещение секций данных. Тогда, между секциями появится разрыв в 96 байт, а данные окажутся в выделенном для них диапазоне адресов.
|
||||
|
||||
В нашей процессорной системе гарвардская архитектура. Это значит, что память инструкций и данных у нас независимы друг от друга. Это физически разные устройства, с разными шинами и разным адресным пространством. Однако обе эти памяти имеют общие значения младших адресов: самый младший имеет адрес ноль, следующий адрес 1 и т.д. Таким образом, происходит наложение адресных пространств памяти инструкций и памяти данных. Компоновщику трудно работать в таких условиях: "как я записать что по этому адресу будет размещаться секция данных, когда здесь уже размещена секция инструкций".
|
||||
В нашей процессорной системе гарвардская архитектура. Это значит, что память инструкций и данных у нас независимы друг от друга. Это физически разные устройства, с разными шинами и разным адресным пространством. Однако обе эти памяти имеют общие значения младших адресов: самый младший имеет адрес ноль, следующий адрес 1 и т.д. Таким образом, происходит наложение адресных пространств памяти инструкций и памяти данных. Компоновщику трудно работать в таких условиях: "как я могу разместить по этому адресу секцию данных, когда здесь уже размещена секция инструкций".
|
||||
|
||||
Есть два механизма для решения этого вопроса. Первый: компоновать секции инструкций и данных по отдельности. В этом случае будет два отдельных скрипта компоновщика. Однако, компоновка секций инструкций зависит от компоновки секций данных (в частности, от того по каким адресам будут размещены стек и .bss-секция, а также указатель на глобальную область данных), поскольку в часть инструкций необходимо прописать конкретные адреса. В этом случае, придётся делать промежуточные операции в виде экспорта глобальных символов в отдельный объектный файл, который будет использован при компоновке секции инструкций, что кажется некоторым переусложнением.
|
||||
|
||||
@@ -100,7 +100,7 @@ _Таблица 1. Ассемблерные мнемоники для целоч
|
||||
|
||||
Помимо прочего, в скрипте компоновщика необходимо прописать, каков [порядок следования байт](https://en.wikipedia.org/wiki/Endianness), где будет находиться стек, и какое будет значение у указателя на глобальную область памяти.
|
||||
|
||||
Все это с подробными комментариями описано в файле `linker_script.ld`.
|
||||
Всё это с подробными комментариями описано в файле `linker_script.ld`.
|
||||
|
||||
```ld
|
||||
OUTPUT_FORMAT("elf32-littleriscv") /* Указываем порядок следования байт */
|
||||
@@ -301,11 +301,12 @@ SECTIONS
|
||||
|
||||
_Листинг 1. Пример скрипта компоновщика с комментариями._
|
||||
|
||||
Обратите внимание на указанные размеры памяти инструкций и данных. Они отличаются от размеров, которые использовались ранее в пакете `memory_pkg`. Дело в том, что пока система и исполняемые ей программы были простыми, в большом объеме памяти не было нужды и меньший размер значительно сокращал время синтеза системы. Однако в данный момент, чтобы обеспечить программе достаточно места под инструкции, а также программный стек и стек прерываний, необходимо увеличить объемы памяти инструкций и памяти данных. Для этого необходимо обновить значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` на 32'd1024 и 32'd2048 соответственно. В зависимости от сложности вашего проекта, в будущем вам может снова потребоваться изменять размер памяти в вашей системе. Помните, все изменения в memory_pkg должны отражаться и в скрипте компоновщика для вашей системы.
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на указанные размеры памяти инструкций и данных. Они отличаются от размеров, которые использовались ранее в пакете `memory_pkg`. Дело в том, что пока система и исполняемые ей программы были простыми, в большом объеме памяти не было нужды и меньший размер значительно сокращал время синтеза системы. Однако в данный момент, чтобы обеспечить программе достаточно места под инструкции, а также программный стек и стек прерываний, необходимо увеличить объемы памяти инструкций и памяти данных. Для этого необходимо обновить значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` на 32'd1024 и 32'd2048 соответственно. В зависимости от сложности вашего проекта, в будущем вам может снова потребоваться изменять размер памяти в вашей системе. Помните, все изменения в memory_pkg должны отражаться и в скрипте компоновщика для вашей системы.
|
||||
|
||||
### Файл первичных команд при загрузке (startup.S)
|
||||
|
||||
В стартап-файле хранятся инструкции, которые обязательно необходимо выполнить перед началом исполнения любой программы. Это инициализация регистров указателей на стек и глобальную область данных, контрольных регистров системы прерывания и т.п.
|
||||
В стартап-файле хранятся инструкции, которые обязательно необходимо выполнить перед началом исполнения любой программы. Это инициализация регистров указателей на стек и глобальную область данных, контрольных регистров системы прерывания, инициализация .bss-секции и т.п.
|
||||
|
||||
По завершению инициализации, стартап-файл выполняет процедуру передачи управления точке входа в запускаемую программу.
|
||||
|
||||
@@ -456,7 +457,8 @@ _int_handler:
|
||||
|
||||
_Листинг 2. Пример содержимого файла первичных команд с поясняющими комментариями._
|
||||
|
||||
Обратите внимание на строки `call main` и `call int_handler`. Компоновка объектного файла, полученного после компиляции `startup.S` будет успешной только в том случае, если в других компонуемых файлах будут функции именно с такими именами.
|
||||
> [!IMPORTANT]
|
||||
> Обратите внимание на строки `call main` и `call int_handler`. Компоновка объектного файла, полученного после компиляции `startup.S` будет успешной только в том случае, если в других компонуемых файлах будут функции именно с такими именами.
|
||||
|
||||
## Практика
|
||||
|
||||
@@ -487,7 +489,7 @@ _Листинг 2. Пример содержимого файла первичн
|
||||
|
||||
### Компоновка объектных файлов в исполняемый
|
||||
|
||||
Далее необходимо выполнить компоновку объектных файлов. Это можно выполнить командной следующего формата:
|
||||
Далее необходимо выполнить компоновку объектных файлов. Это можно сделать командой следующего формата:
|
||||
|
||||
```text
|
||||
<исполняемый файл компилятора> <флаги компоновки> <входные объектные файлы> -o <выходной объектный файл>
|
||||
@@ -551,9 +553,9 @@ FF5FF06F 04400293 FFF00313 30529073
|
||||
...
|
||||
```
|
||||
|
||||
Обратите внимание что байты не просто склеились в четверки, изменился так же и порядок следования байт. Это важно, т.к. в память данные должны лечь именно в таком (обновленном) порядке байт (см. первую строчку скрипта компоновщика). Когда-то `objcopy` содержал [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=25202), из-за которого порядок следования байт не менялся. В каких-то версиях тулчейна (отличных от представленного в данной лабораторной работе) вы все ещё можете столкнуться с подобным поведением.
|
||||
Обратите внимание что байты не просто склеились в четверки, изменился так же и порядок следования байт. Это важно, т.к. в память данные должны лечь именно в таком (обновленном) порядке байт (см. первую строчку скрипта компоновщика). Когда-то `objcopy` содержал [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=25202), из-за которого порядок следования байт не менялся. В каких-то версиях тулчейна (отличных от представленного в данной лабораторной работе) вы всё ещё можете столкнуться с подобным поведением.
|
||||
|
||||
Вернемся к первой строке: `@00000000`. Как уже говорилось, число, начинающееся с символа `@` говорит САПР, что с этого момента инициализация идет начиная с ячейки памяти, номер которой совпадает с этим числом. Когда вы будете экспортировать секции данных, первой строкой будет: `@20000000`. Так произойдет, поскольку в скрипте компоновщика сказано, указано инициализировать память данных с `0x80000000` адреса (значение которого было поделено на 4, чтобы получить номер 32-битной ячейки памяти). Это было сделано, чтобы не произошло наложения адресов памяти инструкций и памяти данных (см раздел [скрипт для компоновки](#скрипт-для-компоновки-linker_scriptld)). **Чтобы система работала корректно, эту строчку необходимо удалить.**
|
||||
Вернемся к первой строке: `@00000000`. Как уже говорилось, число, начинающееся с символа `@` говорит САПР, что с этого момента инициализация идет начиная с ячейки памяти, номер которой совпадает с этим числом. Когда вы будете экспортировать секции данных, первой строкой будет: `@20000000`. Так произойдет, поскольку в скрипте компоновщика сказано инициализировать память данных с `0x80000000` адреса (значение которого было поделено на 4, чтобы получить номер 32-битной ячейки памяти; когда-то в objcopy был ещё один [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=27214), в результате которого деление на 4 не производилось). Это было сделано, чтобы не произошло наложения адресов памяти инструкций и памяти данных (см параграф [скрипт для компоновки](#скрипт-для-компоновки-linker_scriptld)). **Чтобы система работала корректно, эту строчку необходимо удалить.**
|
||||
|
||||
### Дизассемблирование
|
||||
|
||||
@@ -616,7 +618,7 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
|
||||
Следующая за адресом строка, записанная в шестнадцатеричном виде — это та инструкция (или данные), которая размещена по этому адресу. С помощью этого столбца вы можете проверить, что считанная инструкция на временной диаграмме (сигнал `instr`) корректна.
|
||||
|
||||
В правом столбце находится ассемблерный (человекочитаемый) аналог инструкции из предыдущего столбца. Например, инструкция `00001197` — это операция `auipc gp,0x1`, где `gp` — это синоним (ABI name) регистра `x3` (см. раздел [Соглашение о вызовах](#соглашение-о-вызовах)).
|
||||
В правом столбце находится ассемблерный (человекочитаемый) аналог инструкции из предыдущего столбца. Например, инструкция `00001197` — это операция `auipc gp,0x1`, где `gp` — это синоним (ABI name) регистра `x3` (см. параграф [Соглашение о вызовах](#соглашение-о-вызовах)).
|
||||
|
||||
Обратите внимание на последнюю часть листинга: дизасм секции `.data`. В этой секции адреса могут увеличиваться на любое число, шестнадцатеричные данные могут быть любого размера, а на ассемблерные инструкции в правом столбце и вовсе не надо обращать внимание.
|
||||
|
||||
@@ -627,7 +629,7 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
Для того, чтобы произвести дизасемблирование, необходимо выполнить следующую команду:
|
||||
|
||||
```text
|
||||
<исполняемый файл дизасемблера> -D (либо -d) <входной исполняемый файл> > <выходной файл на языке ассемблер>
|
||||
[исполняемый файл дизасемблера] -D (либо -d) [входной исполняемый файл] > [выходной файл на языке ассемблер]
|
||||
```
|
||||
|
||||
Для нашего примера, командной будет
|
||||
@@ -642,7 +644,7 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
|
||||
## Задание
|
||||
|
||||
Вам необходимо написать программу для вашего [индивидуального задания](../04.%20Primitive%20programmable%20device/Индивидуальное%20задание#индивидуальные-задания) к 4-ой лабораторной работе на языке C или C++ (в зависимости от выбранного языка необходимо использовать соответствующий компилятор: gcc для C, g++ для C++).
|
||||
Написать программу для вашего [индивидуального задания](../04.%20Primitive%20programmable%20device/Индивидуальное%20задание#индивидуальные-задания) к 4-ой лабораторной работе на языке C или C++ (в зависимости от выбранного языка необходимо использовать соответствующий компилятор: gcc для C, g++ для C++).
|
||||
|
||||
Для того чтобы ваша программа собралась, необходимо описать две функции: `main` и `int_handler`. Аргументы и возвращаемые значения могут быть любыми, но использоваться они не смогут. Функция `main` будет вызвана в начале работы программы (после исполнения .boot-секции startup-файла), функция `int_handler` будет вызываться автоматически каждый раз, когда ваш контроллер устройства ввода будет генерировать запрос прерывания (если процессор закончил обрабатывать предыдущий запрос).
|
||||
|
||||
@@ -651,7 +653,7 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
- При вводе данных с клавиатуры, отправляется скан-код клавиши, а не значение нажатой цифры (и не ascii-код нажатой буквы). Более того, при отпускании клавиши, генерируется скан-код `FO`, за которым следует повторная отправка скан-кода этой клавиши.
|
||||
- Работая с uart через программу Putty, вы отправляете ascii-код вводимого символа.
|
||||
|
||||
Таким образом, для этих двух устройств ввода, вам необходимо продумать протокол, по которому вы будете вводить числа в вашу программу. В простейшем случае можно обрабатывать данные "как есть". Т.е. в случае клавиатуры, нажатие на клавишу `1` в верхнем горизонтальном ряду на клавиатуры со скан-кодом 0x16 интерпретировать как число `0x16`. А в случае отправки по uart символа `1` с ascii-кодом `0x31` интерпретировать его как `0x31`. Однако вывод в Putty осуществляется в виде символов принятого ascii-кода, поэтому высок риск получить непечатный символ.
|
||||
Для этих двух устройств ввода, вам необходимо продумать протокол, по которому вы будете вводить числа в вашу программу. В простейшем случае можно обрабатывать данные "как есть". Т.е. в случае клавиатуры, нажатие на клавишу `1` в верхнем горизонтальном ряду на клавиатуры со скан-кодом 0x16 интерпретировать как число `0x16`. А в случае отправки по uart символа `1` с ascii-кодом `0x31` интерпретировать его как `0x31`. Однако вывод в Putty осуществляется в виде символов принятого ascii-кода, поэтому высок риск получить непечатный символ.
|
||||
|
||||
Функция main может быть как пустой, содержать один лишь оператор return или бесконечный цикл — ход работы в любом случае не сломается, т.к. в стартап-файле прописан бесконечный цикл после выполнения main. Тем не менее, вы можете разместить здесь и какую-то логику, получающую данные от обработчика прерываний через глобальные переменные.
|
||||
|
||||
@@ -676,7 +678,7 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
#include "platform.h"
|
||||
|
||||
/*
|
||||
Создаем заголовочном файле "platform.h" объявлены collider_ptr — указатель на
|
||||
В заголовочном файле "platform.h" объявлены collider_ptr — указатель на
|
||||
структуру SUPER_COLLIDER_HANDLE и collider_obj — экземпляр аналогичной
|
||||
структуры.
|
||||
Доступ к полям этой структуры через указатель можно осуществлять посредством
|
||||
@@ -723,11 +725,27 @@ extern "C" void int_handler()
|
||||
|
||||
_Листинг 4. Пример кода на C++, взаимодействующего с выдуманным периферийным устройством через указатели на структуру и массив, объявленные в platform.h._
|
||||
|
||||
При написании программы на языках высокого уровня может возникнуть соблазн использовать все блага высокоуровневого программирования: использовать `scanf`/`cin` для консольного ввода, `printf`/`cout` для консольного вывода, а также использовать динамические массивы, или контейнеры библиотеки STL. Вам следует избегать подобных позывов, поскольку "из коробки" весь этот функционал вам не будет доступен по следующим причинам:
|
||||
|
||||
1. **Консольный ввод-вывод**. Для того чтобы `printf` мог вывести сообщение в ваше устройство вывода, необходимо "рассказать" ему как это сделать. Для этого необходимо переопределить функцию `write`, используемую функцией `printf`.
|
||||
2. **Динамическая память**. Использование динамических массивов и контейнеров, неявно использующих динамическую память ограничено сложной механикой контроля за этой памятью. В системах общего назначения за это отвечает операционная система, однако в нашей встраиваемой системе её не будет. Для использования динамической памяти, вам потребуется написать **аллокатор** — специальную функцию, или класс, обеспечивающий выделение и освобождение памяти.
|
||||
3. **Размер стандартных библиотек**. Даже если вы реализуете весь требуемый функционал, любая из перечисленных выше функций стандартных библиотек C/C++ потянет за собой включение огромного числа кода, который в итоге попросту не уместится в вашу память инструкций.
|
||||
|
||||
Однако, если вы всё ещё горите желанием писать по-настоящему высокоуровневый код, вы можете воспользоваться сторонними библиотеками, написанными специально для встраиваемых систем. К примеру, для `printf` можно использовать следующий репозиторий: https://github.com/mpaland/printf. Для использования этой библиотеки вам потребуется реализовать одну лишь функцию со следующим прототипом:
|
||||
|
||||
```C
|
||||
void _putchar(char character);
|
||||
```
|
||||
|
||||
В этой функции вам необходимо рассказать, как вывести один ASCII-символ в ваше устройство вывода.
|
||||
|
||||
Динамических массивов вам всё же стоит избегать, заменяя их статическими массивами заведомо большего размера. Однако, не всегда можно решить проблему одним лишь статическим массивом. К примеру, вы можете захотеть использовать словарь (ассоциативный массив). В этом случае вы можете использовать следующий репозиторий: https://github.com/mpaland/avl_array. Он не будет использовать динамическую память, в основе данного контейнера будет также лежать статический массив, размер которого вам необходимо будет задать вручную.
|
||||
|
||||
---
|
||||
|
||||
### Порядок выполнения задания
|
||||
|
||||
1. Внимательно изучите разделы теории и практики.
|
||||
1. Внимательно изучите параграфы теории и практики.
|
||||
2. Разберите принцип взаимодействия с контрольными и статусными регистрами периферийного устройства на примере _Листинга 4_.
|
||||
3. Обновите значения параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` в пакете `memory_pkg` на 32'd1024 и 32'd2048 соответственно. Поскольку пакеты не являются модулями, вы не увидите их во вкладке `Hierarchy` окна исходников, вместо этого вы сможете найти их во вкладках `Libraries` и `Compile order`.
|
||||
4. Напишите программу для своего индивидуального задания и набора периферийных устройств на языке C или C++. В случае написания кода на C++ помните о необходимости добавления `extern "C"` перед определением функции `int_handler`.
|
||||
@@ -749,10 +767,10 @@ _Листинг 4. Пример кода на C++, взаимодействую
|
||||
|
||||
## Список источников:
|
||||
|
||||
1. [ISC-V ABIs Specification, Document Version 1.0', Editors Kito Cheng and Jessica
|
||||
1. [RISC-V ABIs Specification, Document Version 1.0, Editors Kito Cheng and Jessica
|
||||
Clarke, RISC-V International, November 2022](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases/download/v1.0/riscv-abi.pdf);
|
||||
2. [Using LD, the GNU linker — Linker Scripts](https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html#IDX338);
|
||||
3. [Google Gropus — "gcc gp (global pointer) register"](https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ?pli=1);
|
||||
3. [Google Groups — "gcc gp (global pointer) register"](https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ?pli=1);
|
||||
4. [Wikipedia — .bss](https://en.wikipedia.org/wiki/.bss).
|
||||
|
||||
|
||||
|
@@ -17,7 +17,7 @@
|
||||
|
||||
## Теория
|
||||
|
||||
До этого момента исполняемая процессором программа попадала в память инструкций через магический вызов `$readmemh`. Однако, реальные микроконтроллеры не обладают такими возможностями. Программа из внешнего мира попадает в них посредством так называемого **программатора** — устройства, обеспечивающего запись программы в память микроконтроллера. Программатор записывает данные в постоянное запоминающее устройство (ПЗУ). Для того, чтобы программа попала из ПЗУ в память инструкций (в ОЗУ), после запуска контроллера сперва начинает исполняться **загрузчик** (**bootloader**) — небольшая программа, вшитая в память микроконтроллера на этапе изготовления. Загрузчик отвечает за первичную инициализацию различных регистров и подготовку микроконтроллера к выполнению основной программы, включая её перенос из ПЗУ в память инструкций.
|
||||
До этого момента исполняемая процессором программа попадала в память инструкций через магический вызов `$readmemh`. Однако, реальные микроконтроллеры не обладают такими возможностями. Программа из внешнего мира попадает в них посредством так называемого **программатора** — устройства, обеспечивающего запись программы в память микроконтроллера. Программатор записывает данные в постоянное запоминающее устройство (ПЗУ). Для того, чтобы программа попала из ПЗУ в память инструкций (в ОЗУ), после запуска микроконтроллера сперва начинает исполняться **загрузчик** (**bootloader**) — небольшая программа, "вшитая" в память микроконтроллера на этапе изготовления. Загрузчик отвечает за первичную инициализацию различных регистров и подготовку микроконтроллера к выполнению основной программы, включая её перенос из ПЗУ в память инструкций.
|
||||
|
||||
Со временем появилось несколько уровней загрузчиков: сперва запускается **первичный загрузчик** (**first stage bootloader**, **fsbl**), после которого запускается **вторичный загрузчик** (часто в роли вторичного загрузчика исполняется программа под названием **u-boot**). Такая иерархия загрузчиков может потребоваться, например, в случае загрузки операционной системы (которая хранится в файловой системе). Код для работы с файловой системой может попросту не уместиться в первичный загрузчик. В этом случае, целью первичного загрузчика является лишь загрузить вторичный загрузчик, который в свою очередь уже будет способен взаимодействовать с файловой системой и загрузить операционную систему.
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
- один выход
|
||||
- блокировка треноги
|
||||
|
||||
Для описания двух состояний нам будет достаточно однобитного регистра. Для взаимодействия с регистром, нам потребуются так же сигнал синхронизации и сброса.
|
||||
Для описания двух состояний нам будет достаточно однобитного регистра. Для взаимодействия с регистром, нам потребуются также сигнал синхронизации и сброса.
|
||||
|
||||
Опишем данный автомат в виде графа переходов:
|
||||
|
||||
@@ -126,7 +126,7 @@ module turnstile_fsm(
|
||||
|
||||
enum logic {LOCKED=1, UNLOCKED=0} state;
|
||||
|
||||
assign is_locked = ststate == LOCKED;
|
||||
assign is_locked = state == LOCKED;
|
||||
|
||||
// (!push) && coin — условие перехода в состояние UNLOCKED
|
||||
assign green_light = (state == LOCKED) && (!push) && coin;
|
||||
@@ -166,7 +166,7 @@ module turnstile_fsm(
|
||||
|
||||
enum logic {LOCKED=1, UNLOCKED=0} state, next_state;
|
||||
|
||||
assign is_locked = state;
|
||||
assign is_locked = state == LOCKED;
|
||||
|
||||
assign green_light = (state == LOCKED) && (next_state == UNLOCKED);
|
||||
|
||||
@@ -175,7 +175,7 @@ module turnstile_fsm(
|
||||
state <= LOCKED;
|
||||
end
|
||||
else begin
|
||||
state <= next_state
|
||||
state <= next_state;
|
||||
end
|
||||
end
|
||||
|
||||
@@ -195,7 +195,7 @@ endmodule
|
||||
|
||||
_Листинг 3. Пример реализации конечного автомата для усложнённого турникета с использованием сигнала next\_state._
|
||||
|
||||
На первый взгляд может показаться, что так даже сложнее. Во-первых, появился дополнительный сигнал. Во-вторых, появился ещё один `always`-блок. Однако представьте на секунду, что условиями перехода будут не однобитные входные сигналы, а какие-нибудь более сложные условия. И что от них будет зависеть не один выходной сигнал, а множество как выходных сигналов, так и внутренних элементов памяти помимо регистра состояний. В этом случае, сигнал `next_state` позволит избежать дублирования множества условий.
|
||||
На первый взгляд может показаться, что так даже сложнее. Во-первых, появился дополнительный сигнал. Во-вторых, появился ещё один `always`-блок. Однако представьте на секунду, что условиями перехода будет что-то сложнее, чем 1-битный входной сигнал. И что от этих условий будет зависеть не один выходной сигнал, а множество как выходных сигналов, так и внутренних элементов памяти помимо регистра состояний. В этом случае, сигнал `next_state` позволит избежать дублирования множества условий.
|
||||
|
||||
Важно отметить, что объектам типа `enum` можно присваивать только перечисленные константы и объекты того же типа. Иными словами, `state` можно присваивать значения `LOCKED`/`UNLOCKED` и `next_state`, но нельзя, к примеру, присвоить `1'b0`.
|
||||
|
||||
@@ -205,7 +205,7 @@ _Листинг 3. Пример реализации конечного авто
|
||||
|
||||
- описать перезаписываемую память инструкций;
|
||||
- описать модуль-программатор;
|
||||
- заменить в `riscv_unit` память инструкций на новую, и интегрировать в `riscv_unit` программатор.
|
||||
- заменить в `processor_system` память инструкций на новую, и интегрировать программатор.
|
||||
|
||||
### Перезаписываемая память инструкций
|
||||
|
||||
@@ -449,15 +449,15 @@ _Листинг 5. Готовая часть программатора._
|
||||
|
||||
`msg_counter` должен сбрасываться в значение `INIT_MSG_SIZE-1` (в _Листинге 5_ объявлены параметры `INIT_MSG_SIZE`, `FLASH_MSG_SIZE` и `ACK_MSG_SIZE`).
|
||||
|
||||
счётчик должен инициализироваться следующим образом:
|
||||
`msg_counter` должен инициализироваться следующим образом:
|
||||
|
||||
- в состоянии `FLASH` счётчик должен принимать значение `FLASH_MSG_SIZE-1`,
|
||||
- в состоянии `RCV_SIZE` счётчик должен принимать значение `ACK_MSG_SIZE-1`,
|
||||
- в состоянии `RCV_NEXT_COMMAND` счётчик должен принимать значение `INIT_MSG_SIZE-1`.
|
||||
- в состоянии `FLASH` должен принимать значение `FLASH_MSG_SIZE-1`,
|
||||
- в состоянии `RCV_SIZE` должен принимать значение `ACK_MSG_SIZE-1`,
|
||||
- в состоянии `RCV_NEXT_COMMAND` должен принимать значение `INIT_MSG_SIZE-1`.
|
||||
|
||||
В состояниях: `INIT_MSG`, `SIZE_ACK`, `FLASH_ACK` счётчик должен декрементироваться в случае, если `tx_valid` равен единице.
|
||||
В состояниях: `INIT_MSG`, `SIZE_ACK`, `FLASH_ACK` `msg_counter` должен декрементироваться в случае, если `tx_valid` равен единице.
|
||||
|
||||
Во всех остальных ситуациях счётчик должен сохранять свое значение.
|
||||
Во всех остальных ситуациях `msg_counter` должен сохранять свое значение.
|
||||
|
||||
##### Реализация сигналов, подключаемых к uart_tx
|
||||
|
||||
@@ -479,11 +479,11 @@ _Листинг 5. Готовая часть программатора._
|
||||
|
||||
##### Реализация интерфейсов памяти инструкций и данных
|
||||
|
||||
Почему программатору необходимо два интерфейса? Дело в том, что в процессорной системе используется две шины: шина инструкций и шина данных. Чтобы не переусложнять логику системы дополнительным мультиплексированием, программатор также будет реализовывать два отдельных интерфейса. При этом необходимо различать, когда выполняется программирование памяти инструкций, а когда — памяти данных. Поскольку обе эти памяти имеют независимые адресные пространства, адреса по которым может вестись программирование могут быть неотличимы. Однако с этой же проблемой мы сталкивались и в ЛР14 во время описания скрипта компоновщика. Тогда было решено дать секции данных специальный заведомо большой адрес загрузки. Это же решение отлично ложится и в логику программатора: если мы будет использовать при программировании системы те адреса загрузки, по их значению мы сможем понимать назначение текущего блока данных: если адрес записи этого блока больше либо равен размеру памяти инструкций в байтах — этот блок не предназначен для памяти инструкций и будет отправлен на запись по интерфейсу памяти данных, в противном случае — наоборот.
|
||||
Почему программатору необходимо два интерфейса? Дело в том, что в процессорной системе используется две шины: шина инструкций и шина данных. Чтобы не переусложнять логику системы дополнительным мультиплексированием, программатор также будет реализовывать два отдельных интерфейса. При этом необходимо различать, когда выполняется программирование памяти инструкций, а когда — памяти данных. Поскольку обе эти памяти имеют независимые адресные пространства, адреса по которым может вестись программирование могут быть неотличимы. Однако с этой же проблемой мы сталкивались и в ЛР№14 во время описания скрипта компоновщика. Тогда было решено дать секции данных специальный заведомо большой адрес загрузки. Это же решение отлично ложится и в логику программатора: если мы будет использовать при программировании системы заведомо большие адреса загрузки, по их значению мы сможем понимать назначение текущего блока данных: если адрес записи этого блока больше либо равен размеру памяти инструкций в байтах — этот блок не предназначен для памяти инструкций и будет отправлен на запись по интерфейсу памяти данных, в противном случае — наоборот.
|
||||
|
||||
Сигналы памяти инструкций (регистры `instr_addr_o`, `instr_wdata_o`, `instr_we_o`):
|
||||
|
||||
- сбрасываются в ноль
|
||||
- по сигналу сброса — сбрасываются в ноль
|
||||
- в случае состояния `FLASH` и пришедшего сигнала `rx_valid`, если значение `flash_addr` **меньше** размера памяти инструкций в байтах:
|
||||
- `instr_wdata_o` принимает значение `{instr_wdata_o[23:0], rx_data}` (справа вдвигается очередной пришедший байт)
|
||||
- `instr_we_o` становится равен `(flash_counter[1:0] == 2'b01)`
|
||||
@@ -492,7 +492,7 @@ _Листинг 5. Готовая часть программатора._
|
||||
|
||||
Сигналы памяти данных (`data_addr_o`, `data_wdata_o`, `data_we_o`):
|
||||
|
||||
- сбрасываются в ноль
|
||||
- по сигналу сброса — сбрасываются в ноль
|
||||
- в случае состояния `FLASH` и пришедшего сигнала `rx_valid`, если значение `flash_addr` **больше либо равно** размеру памяти инструкций в байтах:
|
||||
- `data_wdata_o` принимает значение `{data_wdata_o[23:0], rx_data}` (справа вдвигается очередной пришедший байт)
|
||||
- `data_we_o` становится равен `(flash_counter[1:0] == 2'b01)`
|
||||
@@ -503,13 +503,13 @@ _Листинг 5. Готовая часть программатора._
|
||||
|
||||
Регистр `flash_size` работает следующим образом:
|
||||
|
||||
- сбрасывается в 0;
|
||||
- по сигналу сброса — сбрасывается в 0;
|
||||
- в состоянии `RCV_SIZE` при `rx_valid` равном единице становится равен `{flash_size[2:0], rx_data}` (сдвигается на 1 байт влево и на освободившееся место ставится очередной пришедший байт);
|
||||
- в остальных ситуациях сохраняет свое значение.
|
||||
|
||||
Регистр `flash_addr` почти полностью повторяет поведение `flash_size`:
|
||||
|
||||
- сбрасывается в 0;
|
||||
- по сигналу сброса — сбрасывается в 0;
|
||||
- в состоянии `RCV_NEXT_COMMAND` при `rx_valid` равном единице становится равен `{flash_addr[2:0], rx_data}` (сдвигается на 1 байт влево и на освободившееся место ставится очередной пришедший байт);
|
||||
- в остальных ситуациях сохраняет свое значение.
|
||||
|
||||
@@ -531,7 +531,7 @@ _Рисунок 4. Интеграция программатора в `riscv_uni
|
||||
|
||||
Чтобы проверить работу программатора на практике необходимо подготовить скомпилированную программу подобно тому, как это делалось в ЛР№14 (или взять готовые .mem-файлы вашего варианта из ЛР№13). Однако, в отличие от ЛР№14, удалять первую строчку из файла, инициализирующего память данных не надо — теперь адрес загрузки будет использоваться в процессе загрузки.
|
||||
|
||||
Необходимо подключить отладочный стенд к последовательному порту компьютера (в случае платы Nexys A7 — достаточно просто подключить плату usb-кабелем, как это делалось на протяжении всех лабораторных для прошивки). Необходимо будет узнать COM-порт, по которому отладочный стенд подключен к компьютеру. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню пуск. В данном окне необходимо найти вкладку "Порты (COM и LPT)", раскрыть ее, а затем подключить отладочный стенд через USB-порт (если тот еще не был подключен). В списке появится новое устройство, а в скобках будет указан нужный COM-порт.
|
||||
Необходимо подключить отладочный стенд к последовательному порту компьютера (в случае платы Nexys A7 — достаточно просто подключить плату usb-кабелем, как это делалось на протяжении всех лабораторных для прошивки). Необходимо будет узнать COM-порт, по которому отладочный стенд подключен к компьютеру. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню пуск. В данном окне необходимо найти вкладку "Порты (COM и LPT)", раскрыть её, а затем подключить отладочный стенд через USB-порт (если тот еще не был подключен). В списке появится новое устройство, а в скобках будет указан нужный COM-порт.
|
||||
|
||||
Подключив отладочный стенд к последовательному порту компьютера и сконфигурировав ПЛИС вашим проектом, остается проинициализировать память. Сделать это можно с помощью предоставленного скрипта, пример запуска которого приведен в листинге 6.
|
||||
|
||||
@@ -557,7 +557,7 @@ optional arguments:
|
||||
-t TIFF, --tiff TIFF File for tiff mem initialization
|
||||
|
||||
python3 flash.py -d /path/to/data.mem -c /path/to/col_map.mem \
|
||||
-s /path/to/char_map.mem -t /path/to/tiff_map.mem /path/to/program COM3
|
||||
-s /path/to/char_map.mem -t /path/to/tiff_map.mem /path/to/program COM
|
||||
```
|
||||
|
||||
_Листинг 6. Пример использования скрипта для инициализации памяти._
|
||||
@@ -584,19 +584,18 @@ _Листинг 6. Пример использования скрипта для
|
||||
4. В случае если у вас есть периферийное устройство `uart_tx` его выход `tx_o` необходимо мультиплексировать с выходом `tx_o` программатора аналогично тому, как был мультиплексирован интерфейс памяти данных.
|
||||
6. Проверьте процессорную систему после интеграции программатора с помощью верификационного окружения, представленного в файле [`lab_15.tb_processor_system.sv`](lab_15.tb_processor_system.sv).
|
||||
1. Данный тестбенч необходимо обновить под свой вариант. Найдите строки со вспомогательным вызовом `program_region`, первыми аргументами которого являются "YOUR_INSTR_MEM_FILE" и "YOUR_DATA_MEM_FILE". Обновите эти строки под имена файлов, которыми вы инициализировали свои память инструкций и данных в ЛР№13. Если память данных вы не инициализировали, можете удалить/закомментировать соответствующий вызов. При необходимости вы можете добавить столько вызовов, сколько вам потребуется.
|
||||
2. В .mem-файлах, которыми вы будете инициализировать вашу память необходимо сделать доработку. Вам необходимо указать адрес ячейки памяти, с которой необходимо начать инициализировать память. Это делается путем добавления в начало файла строки вида: `@hex_address`. Пример `@FA000000`. Строка обязательно должна начинаться с символа `@`, а адрес обязательно должен быть в шестнадцатеричном виде. Для памяти инструкций нужен нулевой адрес, а значит можно использовать строку `@00000000`. Для памяти данных необходимо адрес, превышающий размер памяти инструкций, но не попадающий в адресное пространство других периферийных устройств (старший байт адреса должен быть равен нулю). Поскольку система использует байтовую адресацию, адрес ячеек будет в 4 раза меньше адреса по которому обратился бы процессор. Это значит, что если бы вы хотели проинициализировать память VGA-контроллера, вам нужно было бы использовать не адрес `@07000000`, а `@01C00000` (`01C00000 * 4 = 07000000`). Таким образом, для памяти данных оптимальным адресом инициализации будет `@00200000`, поскольку эта ячейка с адресом `00200000` соответствует адресу `00800000` — этот адрес не накладывается на адресное пространство других периферийных устройств, но при этом заведомо больше возможного размера памяти инструкций. Примеры использования начальных адресов вы можете посмотреть в файлах [`lab_15_char.mem`](lab_15_char.mem), [`lab_15_data.mem`](lab_15_data.mem), [`lab_15_instr.mem`](lab_15_instr.mem) из папки [mem_files](mem_files).
|
||||
2. В .mem-файлах, которыми вы будете инициализировать вашу память необходимо сделать доработку. Вам необходимо указать адрес ячейки памяти, с которой необходимо начать инициализировать память. Это делается путём добавления в начало файла строки вида: `@hex_address`. Пример `@FA000000`. Строка обязательно должна начинаться с символа `@`, а адрес обязательно должен быть в шестнадцатеричном виде. Для памяти инструкций нужен нулевой адрес, а значит нужно использовать строку `@00000000`. Для памяти данных необходимо использовать адрес, превышающий размер памяти инструкций, но не попадающий в адресное пространство других периферийных устройств (старший байт адреса должен быть равен нулю). Поскольку система использует байтовую адресацию, адрес ячеек будет в 4 раза меньше адреса по которому обратился бы процессор. Это значит, что если бы вы хотели проинициализировать память VGA-контроллера, вам нужно было бы использовать не адрес `@07000000`, а `@01C00000` (`01C00000 * 4 = 07000000`). Таким образом, для памяти данных оптимальным адресом инициализации будет `@00200000`, поскольку эта ячейка с адресом `00200000` соответствует адресу `00800000` — этот адрес не накладывается на адресное пространство других периферийных устройств, но при этом заведомо больше возможного размера памяти инструкций. Примеры использования начальных адресов вы можете посмотреть в файлах [`lab_15_char.mem`](lab_15_char.mem), [`lab_15_data.mem`](lab_15_data.mem), [`lab_15_instr.mem`](lab_15_instr.mem) из папки [mem_files](mem_files).
|
||||
3. Тестбенч будет ожидать завершения инициализации памяти, после чего сформирует те же тестовые воздействия, что и в тестбенче к ЛР№13. А значит, если вы использовали для инициализации те же самые файлы, поведение вашей системы после инициализации не должно отличаться от поведения на симуляции в ЛР№13.
|
||||
4. Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в `Simulation Sources`.
|
||||
7. Переходить к следующему пункту можно только после того, как вы полностью убедились в работоспособности системы на этапе моделирования (увидели, что в память инструкций и данных были записаны корректные данные, после чего процессор стал обрабатывать прерывания от устройства ввода). Генерация битстрима будет занимать у вас долгое время, а итогом вы получите результат: заработало / не заработало, без какой-либо дополнительной информации, поэтому без прочного фундамента на моделировании далеко уехать у вас не выйдет.
|
||||
8. Подключите к проекту файл ограничений ([nexys_a7_100t.xdc](nexys_a7_100t.xdc)), если тот еще не был подключен, либо замените его содержимое данными из файла к этой лабораторной работе.
|
||||
8. Подключите к проекту файл ограничений ([nexys_a7_100t.xdc](../13.%20Peripheral%20units/nexys_a7_100t.xdc)), если тот ещё не был подключён, либо замените его содержимое данными из файла, представленного в ЛР№13.
|
||||
9. Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
|
||||
1. Для инициализации памяти процессорной системы используется скрипт [flash.py](flash.py).
|
||||
2. Перед инициализацией необходимо подключить отладочный стенд к последовательному порту компьютера и узнать номер этого порта (см. [пример загрузки программы](#пример-загрузки-программы)).
|
||||
3. Формат файлов для инициализации памяти с помощью скрипта аналогичен формату, использовавшемуся в [тестбенче](lab_15_tb_bluster.sv). Это значит что первой строчкой всех файлов должна быть строка, содержащая адрес ячейки памяти, с которой должна начаться инициализация (см. п. 5.1.2).
|
||||
3. Формат файлов для инициализации памяти с помощью скрипта аналогичен формату, использовавшемуся в [тестбенче](lab_15_tb_bluster.sv). Это значит что первой строчкой всех файлов должна быть строка, содержащая адрес ячейки памяти, с которой должна начаться инициализация (см. п. 6.2).
|
||||
10. В текущем исполнении, инициализировать память системы можно только 1 раз с момента сброса, что может оказаться не очень удобным при отладке программ. Подумайте, как можно модифицировать конечный автомат программатора таким образом, чтобы получить возможность в неограниченном количестве инициализаций памяти без повторного сброса всей системы.
|
||||
|
||||
## Список источников
|
||||
|
||||
1. [Finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine).
|
||||
2. [All about circuits — Finite State Machines](https://www.allaboutcircuits.com/textbook/digital/chpt-11/finite-state-machines/)
|
||||
3.
|
||||
|
@@ -104,15 +104,15 @@ logic [63:0] system_counter_at_start;
|
||||
- `delay` — регистр, ассоциированный с адресами `0x08` (младшие 32 бита) и `0x0c` (старшие 32 бита). Число тактов, спустя которое таймер (когда тот будет включен) сгенерирует прерывание. Данный регистр изменяется только сбросом, либо запросом на запись.
|
||||
- `mode` — регистр, ассоциированный с адресом `0x10`. Режим работы таймера:
|
||||
- `OFF` — отключен (не генерирует прерывания)
|
||||
- `NTIMES` — включен до тех пор, пока не сгенерирует N прерываний (Значение N хранится в регистре `repeat_counter` и обновляется после каждого сгенерированного прерывания). После генерации N прерываний, переходит в режим `OFF`.
|
||||
- `NTIMES` — включен до тех пор, пока не сгенерирует N прерываний (значение N хранится в регистре `repeat_counter` и обновляется после каждого сгенерированного прерывания). После генерации N прерываний, переходит в режим `OFF`.
|
||||
- `FOREVER` — бесконечная генерация прерываний. Не отключится, пока режим работы прерываний не будет изменен.
|
||||
- `next_mode` — комбинационный сигнал, который подается на вход записи в регистр `mode` (аналог `next_state` из ЛР№15). Данный сигнал меняется только запросами на запись по адресу `0x10` или в случае, если `repeat_counter == 0` в режиме `NTIMES`. Поскольку этому сигналу можно присваивать только значения сигналов такого же типа (`timer_mods`), либо константы из перечисления, запросы на запись можно реализовать через блок `case` (где перебираются 3 возможных значения `write_data_i`).
|
||||
- `next_mode` — комбинационный сигнал, который подается на вход записи в регистр `mode` (аналог `next_state` из ЛР№15). Данный сигнал меняется только запросами на запись по адресу `0x10` или в случае, если `repeat_counter == 0` в режиме `NTIMES`. Поскольку этому сигналу можно присваивать только сигналы того же типа (`mode`/`next_mode`), либо константы из перечисления, запросы на запись можно реализовать через блок `case` (где перебираются 3 возможных значения `write_data_i`).
|
||||
- `repeat_counter` — регистр, ассоциированный с адресом `0x14`. Количество повторений для режима `NTIMES`. Уменьшается в момент генерации прерывания в этом режиме в случае, если еще не равен нулю.
|
||||
- `system_counter_at_start` — неархитектурный регистр, хранящий значение системного счётчика на момент начала отсчета таймера. Обновляется при генерации прерывания (если это не последнее прерывание в режиме `NTIMES`) и при запросе на запись в регистр `mode` значения не `OFF`.
|
||||
|
||||
Выходной сигнал interrupt_request_o должен быть равен единице, если текущий режим работы не `OFF`, а сумма `system_counter_at_start` и `delay` равна `system_counter`.
|
||||
|
||||
Для подключения данного таймера к системной шине, мы воспользуемся первым свободным базовым адресом, оставшимся после ЛР13: `0x08`. Таким образом, для обращения к системному счётчику, процессор будет использовать адрес `0x08000000` для обращения к регистру `delay` `0x08000008` и т.п.
|
||||
Для подключения данного таймера к системной шине, мы воспользуемся первым свободным базовым адресом, оставшимся после ЛР№13: `0x08`. Таким образом, для обращения к системному счётчику, процессор будет использовать адрес `0x08000000` для обращения к регистру `delay` `0x08000008` и т.п.
|
||||
|
||||
### Настройка Coremark
|
||||
|
||||
@@ -144,7 +144,7 @@ barebones_clock()
|
||||
|
||||
_Листинг 2. Код функции `barebones_clock`._
|
||||
|
||||
После ЛР14 вы уже должны представлять, что здесь происходит. Мы создали указатель с абсолютным адресом `0x08000000` — адресом системного счётчика. Разыменование данного указателя вернет текущее значение системного счётчика, что и должно быть результатом вызова этой функции. Поскольку тест закончится менее чем за секунду, не обязательно загружать значение старших 32 бит (они будут не равны нулю только спустя 2³²тактов / 10⁶тактов/с ≈ 429c).
|
||||
После ЛР№14 вы уже должны представлять, что здесь происходит. Мы создали указатель с абсолютным адресом `0x08000000` — адресом системного счётчика. Разыменование данного указателя вернет текущее значение системного счётчика, что и должно быть результатом вызова этой функции. Поскольку тест закончится менее чем за секунду, не обязательно загружать значение старших 32 бит (они будут не равны нулю только спустя 2³²тактов / 10⁶тактов/с ≈ 429c).
|
||||
|
||||
Для того, чтобы корректно преобразовать тики системного счётчика во время, используется функция [`time_in_secs`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.c#L117), которая уже реализована, но для работы которой нужно определить макрос `CLOCKS_PER_SEC`, характеризующий тактовую частоту, на которой работает процессор. Давайте определим данный макрос сразу над макросом [`EE_TICKS_PER_SEC`](https://github.com/eembc/coremark/blob/d5fad6bd094899101a4e5fd53af7298160ced6ab/barebones/core_portme.c#L62):
|
||||
|
||||
@@ -178,7 +178,7 @@ uart_send_char(char c)
|
||||
|
||||
_Листинг 3. Код функции `uart_send_char_`._
|
||||
|
||||
`0x06000000` — базовый адрес контроллера UART TX из ЛР13 (и адрес передаваемых этим контроллером данных).
|
||||
`0x06000000` — базовый адрес контроллера UART TX из ЛР№13 (и адрес передаваемых этим контроллером данных).
|
||||
`0x08` — смещение до адреса регистра `busy` в адресном пространстве этого контроллера.
|
||||
|
||||
#### 3. Реализация функции первичной настройки
|
||||
@@ -268,7 +268,7 @@ _Листинг 5. Последовательность команд для ко
|
||||
|
||||
### Запуск моделирования
|
||||
|
||||
Программирование 32KiB по UART займет ощутимое время, поэтому вам предлагается проинициализировать память инструкций и данных "по-старинке" через системные функции `$readmemh`.
|
||||
Программирование 34KiB по UART займет ощутимое время, поэтому вам предлагается проинициализировать память инструкций и данных "по-старинке" через системные функции `$readmemh`.
|
||||
|
||||
Если все было сделано без ошибок, то примерно через `300ms` после снятия сигнала сброса с ядра процессора выход `tx_o` начнет быстро менять свое значение, сигнализируя о выводе результатов программы, которые отобразятся в `tcl console` примерно еще через `55ms` в виде _листинга 6_ (вывод сообщения будет завершен приблизительно на `355ms` времени моделирования).
|
||||
|
||||
@@ -301,7 +301,7 @@ _Листинг 6. Лог вывода результатов coremark. Знач
|
||||
1. Ко входу `rst_i` модуля подключите сигнал `core_reset_o` программатора. Таким образом, системный счётчик начнет работать только когда память системы будет проинициализирована.
|
||||
2. Сигнал прерывания этого модуля подключать не обязательно, т.к. coremark будет осуществлять чтение путем опроса системного счётчика, а не по прерыванию.
|
||||
4. В случае, если до этого в Л№Р13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине готовый модуль [uart_tx_sb_ctrl](../Made-up%20modules/lab_13.uart_tx_sb_ctrl.sv).
|
||||
5. Получите исходники программы coremark. Для этого можно либо склонировать [репозиторий](https://github.com/eembc/coremark/tree/d5fad6bd094899101a4e5fd53af7298160ced6ab), либо скачать его в виде архива.
|
||||
5. Получите исходный код программы coremark. Для этого можно либо склонировать [репозиторий](https://github.com/eembc/coremark/tree/d5fad6bd094899101a4e5fd53af7298160ced6ab), либо скачать его в виде архива.
|
||||
6. Добавьте реализацию платформозависимых функций программы coremark. Для этого в папке `barebones` необходимо:
|
||||
1. в файле `core_portme.c`:
|
||||
1. [реализовать](#1-реализация-функции-измеряющей-время) функцию `barebones_clock`, возвращающую текущее значение системного счётчика;
|
||||
@@ -311,7 +311,7 @@ _Листинг 6. Лог вывода результатов coremark. Знач
|
||||
7. Добавьте с заменой в корень программы файлы [Makefile](Makefile), [linker_script.ld](linker_script.ld) и [startup.S](../14.%20Programming/startup.S).
|
||||
8. Скомпилируйте программу вызовом `make`.
|
||||
1. Если кросскомпилятор расположен не в директории `C:/riscv_cc`, перед вызовом `make` вам необходимо соответствующим образом отредактировать первую строчку в `Makefile`.
|
||||
2. В случае отсутствия на компьютере утилиты `make`, вы можете самостоятельно скомпилировать программу вызовом команд, представленных в разделе ["Компиляция"](#компиляция).
|
||||
2. В случае отсутствия на компьютере утилиты `make`, вы можете самостоятельно скомпилировать программу вызовом команд, представленных в параграфе ["Компиляция"](#компиляция).
|
||||
9. Временно измените размер памяти инструкций до 64KiB, а памяти данных до 16KiB, изменив значение параметров `INSTR_MEM_SIZE_BYTES` и `DATA_MEM_SIZE_BYTES` в пакете `memory_pkg` на `32'h10_000` и `32'h4_000` соответственно.
|
||||
10. Проинициализируйте память инструкций и память данных файлами `coremark_instr.mem` и `coremark_data.mem`, полученными в ходе компиляции программы.
|
||||
1. Память можно проинициализировать двумя путями: с помощью вызова системной функции `$readmemh`, либо же с помощью программатора. Однако имейте в виду, что инициализация памятей с помощью программатора будет достаточно долго моделироваться в виду большого объема программы.
|
||||
@@ -361,7 +361,7 @@ Iterations/Sec : 3.446111
|
||||
|
||||
Обычно, для сравнения между собой нескольких реализаций микроархитектур, более достоверной считается величина "кормарк / МГц", т.е. число кормарков, поделённое на тактовую частоту процессора, поскольку время прохождения теста напрямую зависит от тактовой частоты. Это значит, что чип с менее удачной микроархитектурной реализацией может выиграть по кормарку просто потому, что он был выпущен по лучшей технологии, позволяющей запустить его на больших частотах. Кормарк/МГц нормализует результаты, позволяя сравнивать микроархитектурные решения, не заботясь о том, на какой частоте был получен результат.
|
||||
|
||||
Более того, при сравнении с другими результатами, необходимо учитывать флаги оптимизации, которые использовались при компиляции программы, поскольку они также влияют на результат. Например, если собрать coremark с уровнем оптимизаций `-O1`, результат нашей системы скакнёт до 11.23 кормарков, что всего лишь является следствием того, что программа стала меньше обращаться к памяти вследствие оптимизаций. Именно поэтому результаты coremark указываются вместе с опциями, с которыми тот был собран.
|
||||
Более того, при сравнении с другими результатами, необходимо учитывать флаги оптимизации, которые использовались при компиляции программы, поскольку они также влияют на результат. Например, если собрать coremark с уровнем оптимизаций `-O1`, результат нашей системы скакнёт до 11.23 кормарков, что всего лишь является следствием того, что программа стала меньше обращаться к памяти после оптимизаций. Именно поэтому результаты coremark указываются вместе с опциями, с которыми тот был собран.
|
||||
|
||||
Мы не будем уходить в дебри темных паттернов маркетинга и вместо этого будет оценивать производительность в лоб: сколько кормарков в секунду смог прогнать наш процессор без каких-либо оптимизаций в сравнении с представленными результатами других систем вне зависимости от их оптимизаций.
|
||||
|
||||
@@ -371,7 +371,7 @@ Iterations/Sec : 3.446111
|
||||
|
||||

|
||||
|
||||
На что мы можем обратить внимание? Ну, во-первых, мы видим, что ближайший к нам микроконтроллер по кормарку — это `ATmega2560` с результатом `4.25` кормарка. Т.е. наш процессор по производительности схож с микроконтроллерами Arduino.
|
||||
На что мы можем обратить внимание? Ну, во-первых, мы видим, что соседний с нашим микроконтроллер по кормарку — это `ATmega2560` с результатом `4.25` кормарка. Т.е. наш процессор по производительности схож с микроконтроллерами Arduino.
|
||||
|
||||
Есть ли здесь еще что-нибудь интересное? Посмотрим в верх таблицы, мы можем увидеть производителя Intel с их микропроцессором [Intel 80286](https://ru.wikipedia.org/wiki/Intel_80286). Как написано на вики, данный микропроцессор был в 3-6 раз производительней [Intel 8086](https://ru.wikipedia.org/wiki/Intel_8086), который соперничал по производительности с процессором [Zilog Z80](https://en.wikipedia.org/wiki/Zilog_Z80), который устанавливался в домашний компьютер [TRS-80](https://en.wikipedia.org/wiki/TRS-80).
|
||||
|
||||
|
@@ -25,11 +25,11 @@ import memory_pkg::INSTR_MEM_SIZE_WORDS;
|
||||
end // файла program.mem
|
||||
|
||||
// Реализация асинхронного порта на чтение, где на выход идет ячейка памяти
|
||||
// инструкций, расположенная по адресу read_addr_i, в котором обнулены два
|
||||
// инструкций, расположенная по адресу read_addr_i, в котором отброшены два
|
||||
// младших бита, а также биты, двоичный вес которых превышает размер памяти
|
||||
// данных в байтах.
|
||||
// Два младших бита обнулены, чтобы обеспечить выровненный доступ к памяти,
|
||||
// в то время как старшие биты обнулены, чтобы не дать обращаться в память
|
||||
// Два младших бита отброшены, чтобы обеспечить выровненный доступ к памяти,
|
||||
// в то время как старшие биты отброшены, чтобы не дать обращаться в память
|
||||
// по адресам несуществующих ячеек (вместо этого будут выданы данные ячеек,
|
||||
// расположенных по младшим адресам).
|
||||
assign read_data_o = ROM[read_addr_i[$clog2(INSTR_MEM_SIZE_BYTES)-1:2]];
|
||||
|
Reference in New Issue
Block a user