ЛР15. Рефактор методички

This commit is contained in:
Andrei Solodovnikov
2024-07-28 12:01:56 +03:00
parent 566e3c9553
commit 37dd47c0c9
2 changed files with 181 additions and 94 deletions

View File

@@ -13,15 +13,15 @@
3. Описать перезаписываемую память инструкций ([#память инструкций](#перезаписываемая-память-инструкций))
4. Описать и проверить модуль программатора ([#программатор](#программатор))
5. Интегрировать программатор в процессорную систему и проверить её ([#интеграция](#интеграция-программатора-в-riscv_unit))
6. Проверить работу системы в ПЛИС с помощью предоставленного скрипта по прошивкe системы ([#проверка](#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8-%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D1%8B))
6. Проверить работу системы в ПЛИС с помощью предоставленного скрипта, инициализирующего память системы ([#проверка](#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8-%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D1%8B))
## Теория
До этого момента исполняемая процессором программа попадала в память инструкций через магический вызов `$readmemh`. Однако, реальные микроконтроллеры не обладают такими возможностями. Программа из внешнего мира попадает в них посредством так называемого **программатора** — устройства, обеспечивающего запись программы в память микроконтроллера. Программатор записывает данные в постоянное запоминающее устройство (ПЗУ). Для того, чтобы программа попала из ПЗУ в память инструкций (в ОЗУ), после запуска контроллера сперва начинает исполняться **загрузчик** (**bootloader**) — небольшая программа, вшитая в память микроконтроллера на этапе изготовления. Загрузчик отвечает за первичную инициализацию различных регистров и подготовку микроконтроллера к выполнению основной программы, включая её перенос из ПЗУ в память инструкций.
Со временем появилось несколько уровней загрузчиков: сперва запускается **первичный загрузчик** (**first stage bootloader**, **fsbl**), после которого запускается **вторичный загрузчик** (часто в роли вторичного загрузчика исполняется программа под названием **u-boot**). Такая иерархия загрузчиков может потребоваться, например, в случае загрузки операционной системы (которая хранится в файловой системе). Код для работы с файловой системой может попросту не уместиться в первичный загрузчик. В этом случае, целью первичного загрузчика является лишь загрузить вторичный загрузчик, который в свою очередь уже будет способен взаимодействовать с файловой системой и загрузить операционную систему[[1]](https://stackoverflow.com/q/22455153).
Со временем появилось несколько уровней загрузчиков: сперва запускается **первичный загрузчик** (**first stage bootloader**, **fsbl**), после которого запускается **вторичный загрузчик** (часто в роли вторичного загрузчика исполняется программа под названием **u-boot**). Такая иерархия загрузчиков может потребоваться, например, в случае загрузки операционной системы (которая хранится в файловой системе). Код для работы с файловой системой может попросту не уместиться в первичный загрузчик. В этом случае, целью первичного загрузчика является лишь загрузить вторичный загрузчик, который в свою очередь уже будет способен взаимодействовать с файловой системой и загрузить операционную систему.
Кроме того, код вторичного загрузчика может быть изменен, поскольку программируется вместе с основной программой. Первичный загрузчик может быть изменен не во всех случаях.
Кроме того, код вторичного загрузчика может быть изменен, поскольку программируется вместе с основной программой. Первичный же загрузчик не всегда может быть изменен.
В рамках данной лабораторной работы мы немного упростим процесс передачи программы: вместо записи в ПЗУ, программатор будет записывать её сразу в память инструкций, минуя загрузчик.
@@ -35,7 +35,7 @@
- логики, обеспечивающей изменение значения **регистра состояния** (логики перехода между состояниями) в зависимости от его текущего состояния и входных сигналов;
- логики, отвечающей за выходы конечного автомата.
Обычно, конечные автоматы описываются в виде направленного графа переходов между состояниями, где вершины графа — это состояния конечного автомата, а дуги — условия перехода из одного состояния в другое.
Обычно, конечные автоматы описываются в виде направленного графа переходов между состояниями, где вершины графа — это состояния конечного автомата, а рёбра (дуги) — условия перехода из одного состояния в другое.
Простейшим примером конечного автомата может быть турникет. Когда в приёмник турникета опускается подходящий жетон, тот разблокирует вращающуюся треногу. После попытки поворота треноги, та блокируется до следующего жетона.
@@ -56,7 +56,7 @@
![https://upload.wikimedia.org/wikipedia/commons/9/9e/Turnstile_state_machine_colored.svg](https://upload.wikimedia.org/wikipedia/commons/9/9e/Turnstile_state_machine_colored.svg)
_Рисунок 1. Граф переходов конечного автомата для турникета[[2]](https://en.wikipedia.org/wiki/Finite-state_machine)._
_Рисунок 1. Граф переходов конечного автомата для турникета[[1]](https://en.wikipedia.org/wiki/Finite-state_machine)._
Черной точкой со стрелкой в вершину `Locked` обозначен сигнал сброса. Иными словами, при сбросе турникет всегда переходит в заблокированное состояние.
@@ -70,7 +70,7 @@ _Рисунок 1. Граф переходов конечного автомат
Для реализации регистра состояния конечного автомата будет удобно воспользоваться специальным типом языка **SystemVerilog**, который называется `enum` (**перечисление**).
Перечисления позволяют объявить объединенный набор именованных констант. В дальнейшем, объявленные имена можно использовать вместо перечисленных значений, им соответствующих, что повышает читабельность кода. Если не указано иного, первому имени присваивается значение `0`, каждое последующее увеличивается на `1` относительно предыдущего значения.
Перечисления позволяют объявить объединенный набор именованных констант. В дальнейшем, объявленные имена можно использовать вместо перечисленных значений, им соответствующих, что повышает читаемость кода. Если не указано иного, первому имени присваивается значение `0`, каждое последующее увеличивается на `1` относительно предыдущего значения.
```SystemVerilog
module turnstile_fsm(
@@ -83,7 +83,7 @@ module turnstile_fsm(
enum logic {LOCKED=1, UNLOCKED=0} state;
assign is_locked = state;
assign is_locked = state == LOCKED;
always_ff @(posedge clk) begin
if(rst) begin
@@ -103,6 +103,8 @@ module turnstile_fsm(
end
```
_Листинг 1. Пример реализации конечного автомата для турникета._
Кроме того, при должной поддержке со стороны инструментов моделирования, значения объектов перечислений могут выводиться на временную диаграмму в виде перечисленных имен:
![../../.pic/Labs/lab_15_programming_device/fig_02.png](../../.pic/Labs/lab_15_programming_device/fig_02.png)
@@ -123,7 +125,7 @@ module turnstile_fsm(
enum logic {LOCKED=1, UNLOCKED=0} state;
assign is_locked = state;
assign is_locked = ststate == LOCKED;
// (!push) && coin — условие перехода в состояние UNLOCKED
assign green_light = (state == LOCKED) && (!push) && coin;
@@ -146,6 +148,8 @@ module turnstile_fsm(
end
```
_Листинг 2. Пример реализации конечного автомата для усложненного турникета._
Используя сигнал `next_state`, автомат мог бы быть переписан следующим образом:
```SystemVerilog
@@ -186,6 +190,8 @@ module turnstile_fsm(
end
```
_Листинг 3. Пример реализации конечного автомата для усложненного турникета с использованием сигнала next\_state._
На первый взгляд может показаться, что так даже сложнее. Во-первых, появился дополнительный сигнал. Во-вторых, появился еще один `always`-блок. Однако представьте на секунду, что условиями перехода будут не однобитные входные сигналы, а какие-нибудь более сложные условия. И что от них будет зависеть не один выходной сигнал, а множество как выходных сигналов, так и внутренних элементов памяти помимо регистра состояний. В этом случае, сигнал `next_state` позволит избежать дублирования множества условий.
Важно отметить, что объектам типа `enum` можно присваивать только перечисленные константы и объекты того же типа. Иными словами, `state` можно присваивать значения `LOCKED`/`UNLOCKED` и `next_state`, но нельзя, к примеру, присвоить `1'b0`.
@@ -200,10 +206,13 @@ module turnstile_fsm(
### Перезаписываемая память инструкций
Поскольку ранее из памяти инструкций можно было только считывать данные, но не записывать их в нее, программатор не сможет записать принятую из внешнего мира программу. Поэтому необходимо добавить в память инструкций порт на запись. Для того, чтобы различать реализации памяти инструкций, данный модуль будет назвать `rw_instr_mem` со следующим прототипом:
Поскольку ранее из памяти инструкций можно было только считывать данные, но не записывать их в нее, программатор не сможет записать принятую из внешнего мира программу. Поэтому необходимо добавить в память инструкций порт на запись. Для того, чтобы различать реализации памяти инструкций, данный модуль будет называться `rw_instr_mem`:
```SystemVerilog
module rw_instr_mem(
module rw_instr_mem
import memory_pkg::INSTR_MEM_SIZE_BYTES;
import memory_pkg::INSTR_MEM_SIZE_WORDS;
(
input logic clk_i,
input logic [31:0] read_addr_i,
output logic [31:0] read_data_o,
@@ -212,9 +221,21 @@ module rw_instr_mem(
input logic [31:0] write_data_i,
input logic write_enable_i
);
logic [31:0] ROM [INSTR_MEM_SIZE_WORDS];
assign read_data_o = ROM[read_addr_i[$clog2(INSTR_MEM_SIZE_BYTES)-1:2]];
always_ff @(posedge clk_i) begin
if(write_enable_i) begin
ROM[write_addr_i[$clog2(INSTR_MEM_SIZE_BYTES)-1:2]] <= write_data_i;
end
end
endmodule
```
Как вы помните, модуль `instr_mem` отличался от `data_mem` только портом на запись, так что данный модуль будет практически в точности повторять модуль `data_mem` за исключением того, что у этого модуля нет сигнала `req_i` (поскольку память инструкций подключена к процессору в обход системной шины).
_Листинг 4. Модуль rw\_instr\_mem._
### Программатор
@@ -228,7 +249,18 @@ module rw_instr_mem(
_Рисунок 3. Граф перехода между состояниями программатора._
Условия перехода следующие:
Данный автомат реализует следующий алгоритм:
1. Получение команды ("запись очередного блока" / "программирование завершено"). Данная команда представляет собой адрес записи очередного блока, и в случае, если адрес равен 0xFFFFFFFF, это означает команду "программирование завершено".
1. В случае получения команды "программирование завершено", модуль отправляет финальное сообщение и завершает свою работу.
2. В случае получения команды "запись очередного блока" происходит переход к п. 2.
2. Модуль отправляет сообщение о готовности принимать размер очередного блока.
3. Выполняется передача размера очередного блока.
4. Модуль подтверждает получение размера очередного блока и повторяет его значение.
5. Выполняется передача очередного блока, который записывается, начиная с адреса, принятого в п.1.
6. Получив заданное в п.3 количество байт очередного блока, модуль сообщает о завершении записи и переходит к ожиданию очередной команды в п.1.
На графе перехода автомата обозначены следующие условия:
- `send_fin = ( msg_counter == 0) && !tx_busy` — условие завершения передачи модулем сообщения по `uart_tx`;
- `size_fin = ( size_counter == 0) && !rx_busy` — условие завершения приема модулем размера будущей посылки;
@@ -248,16 +280,21 @@ module bluster
output logic [ 31:0] instr_addr_o,
output logic [ 31:0] instr_wdata_o,
output logic instr_write_enable_o,
output logic instr_we_o,
output logic [ 31:0] data_addr_o,
output logic [ 31:0] data_wdata_o,
output logic data_write_enable_o,
output logic data_we_o,
output logic core_reset_o
);
enum logic [2:0] {
import memory_pkg::INSTR_MEM_SIZE_BYTES;
import bluster_pkg::INIT_MSG_SIZE;
import bluster_pkg::FLASH_MSG_SIZE;
import bluster_pkg::ACK_MSG_SIZE;
(* mark_debug = "false" *) enum logic [2:0] {
RCV_NEXT_COMMAND,
INIT_MSG,
RCV_SIZE,
@@ -269,22 +306,18 @@ enum logic [2:0] {
state, next_state;
logic rx_busy, rx_valid, tx_busy, tx_valid;
logic [7:0] rx_data, tx_data;
(* mark_debug = "false" *) logic [7:0] rx_data, tx_data;
logic [5:0] msg_counter;
logic [31:0] size_counter, flash_counter;
logic [3:0] [7:0] flash_size, flash_addr;
(* mark_debug = "false" *)logic [5:0] msg_counter;
(* mark_debug = "false" *)logic [31:0] size_counter, flash_counter;
(* mark_debug = "false" *)logic [3:0] [7:0] flash_size, flash_addr;
logic send_fin, size_fin, flash_fin, next_round;
assign send_fin = (msg_counter == 0) && !tx_busy;
assign size_fin = (size_counter == 0) && !rx_busy;
assign flash_fin = (flash_counter == 0) && !rx_busy;
assign next_round = (flash_addr != '1) && !rx_busy;
localparam INIT_MSG_SIZE = 40;
localparam FLASH_MSG_SIZE = 57;
localparam ACK_MSG_SIZE = 4;
(* mark_debug = "false" *)assign send_fin = (msg_counter == 0) && !tx_busy;
(* mark_debug = "false" *)assign size_fin = (size_counter == 0) && !rx_busy;
(* mark_debug = "false" *)assign flash_fin = (flash_counter == 0) && !rx_busy;
(* mark_debug = "false" *)assign next_round = (flash_addr != '1) && !rx_busy;
logic [7:0] [7:0] flash_size_ascii, flash_addr_ascii;
// Блок generate позволяет создавать структуры модуля цикличным или условным
@@ -296,14 +329,17 @@ logic [7:0] [7:0] flash_size_ascii, flash_addr_ascii;
genvar i;
generate
for(i=0; i < 4; i=i+1) begin
// Данная логика преобразовывает сигналы flash_size и flash_addr,
// которые представляют собой "сырые" двоичные числа в ASCII-символы[1]
// Разделяем каждый байт flash_size и flash_addr на два ниббла.
// Ниббл — это 4 бита. Каждый ниббл можно описать 16-ричной цифрой.
// Ниббл — это 4 бита. Каждый ниббл можно описать 16-битной цифрой.
// Если ниббл меньше 10 (4'ha), он описывается цифрами 0-9. Чтобы представить
// его ascii-кодом, необходимо прибавить к нему число 8'h30
// (ascii-код символа '0').
// Если ниббл больше либо равен 10, он описывается буквами a-f. Для его
// представления в виде ascii-кода, необходимо прибавить число 8'h57
// (ascii-код символа 'a' - 8'h61).
// (это уменьшенный на 10 ascii-код символа 'a' = 8'h61).
assign flash_size_ascii[i*2] = flash_size[i][3:0] < 4'ha ? flash_size[i][3:0] + 8'h30 :
flash_size[i][3:0] + 8'h57;
assign flash_size_ascii[i*2+1] = flash_size[i][7:4] < 4'ha ? flash_size[i][7:4] + 8'h30 :
@@ -317,11 +353,11 @@ generate
endgenerate
logic [INIT_MSG_SIZE-1:0][7:0] init_msg;
// ascii-код строки "ready for flash staring from 0xflash_addr\n"
// ascii-код строки "ready for flash starting from 0xflash_addr\n"
assign init_msg = { 8'h72, 8'h65, 8'h61, 8'h64, 8'h79, 8'h20, 8'h66, 8'h6F,
8'h72, 8'h20, 8'h66, 8'h6C, 8'h61, 8'h73, 8'h68, 8'h20,
8'h73, 8'h74, 8'h61, 8'h72, 8'h69, 8'h6E, 8'h67, 8'h20,
8'h66, 8'h72, 8'h6F, 8'h6D, 8'h20, 8'h30, 8'h78,
8'h73, 8'h74, 8'h61, 8'h72, 8'h74, 8'h69, 8'h6E, 8'h67,
8'h20, 8'h66, 8'h72, 8'h6F, 8'h6D, 8'h20, 8'h30, 8'h78,
flash_addr_ascii, 8'h0a};
logic [FLASH_MSG_SIZE-1:0][7:0] flash_msg;
@@ -341,7 +377,7 @@ uart_rx rx(
.busy_o (rx_busy ),
.baudrate_i (17'd115200 ),
.parity_en_i(1'b1 ),
.stopbit_i (1'b1 ),
.stopbit_i (2'b1 ),
.rx_data_o (rx_data ),
.rx_valid_o (rx_valid )
);
@@ -353,14 +389,17 @@ uart_tx tx(
.busy_o (tx_busy ),
.baudrate_i (17'd115200 ),
.parity_en_i(1'b1 ),
.stopbit_i (1'b1 ),
.stopbit_i (2'b1 ),
.tx_data_i (tx_data ),
.tx_valid_i (tx_valid )
);
endmodule
```
_Листинг 5. Готовая часть программатора._
Здесь уже объявлены:
- `enum`-сигналы `state` и `next_state`;
@@ -385,23 +424,21 @@ endmodule
Для реализации данного модуля, необходимо реализовать все объявленные выше сигналы, кроме сигналов:
- `rx_busy`, `rx_valid`, `rx_data`, `tx_busy` (т.к. те уже подключены к выходам модулей `uart_rx` и `uart_tx`),
- `flash_size_ascii`, `flash_addr_ascii`, `init_msg`, `flash_msg` (т.к. они уже реализованы в представленной выше логике).
- `send_fin`, `size_fin`, `flash_fin`, `next_round`, `flash_size_ascii`, `flash_addr_ascii`, `init_msg`, `flash_msg` (т.к. они уже реализованы в представленной выше логике).
Так же необходимо реализовать выходы модуля программатора:
- `instr_addr_o`;
- `instr_wdata_o`;
- `instr_write_enable_o`;
- `instr_we_o`;
- `data_addr_o`;
- `data_wdata_o`;
- `data_write_enable_o`;
- `data_we_o`;
- `core_reset_o`.
##### Реализация конечного автомата
Для реализации сигналов `state`, `next_state` представлен граф переходов между состояниями. В случае, если не выполняется ни одно из условий перехода, автомат должен остаться в текущем состоянии.
Логика условий переходов уже была представлена сразу после графа переходов.
Для реализации сигналов `state`, `next_state` используйте граф переходов между состояниями, представленный на _рис. 3_. В случае, если не выполняется ни одно из условий перехода, автомат должен остаться в текущем состоянии.
Для работы логики переходов, необходимо реализовать счетчики `size_counter`, `flash_counter`, `msg_counter`.
@@ -409,7 +446,7 @@ endmodule
`flash_counter` должен сбрасываться в значение `flash_size`, а также принимать это значение во всех состояниях кроме `FLASH`. В этом состоянии счетчик должен декрементироваться в случае, если `rx_valid` равен единице.
`msg_counter` должен сбрасываться в значение `INIT_MSG_SIZE-1`.
`msg_counter` должен сбрасываться в значение `INIT_MSG_SIZE-1`_Листинге 5_ объявлены параметры `INIT_MSG_SIZE`, `FLASH_MSG_SIZE` и `ACK_MSG_SIZE`).
Счетчик должен инициализироваться следующим образом:
@@ -439,6 +476,28 @@ endmodule
В остальных состояниях он равен нулю. Для отсчета байт используется счетчик `msg_counter`.
##### Реализация интерфейсов памяти инструкций и данных
Почему программатору необходимо два интерфейса? Дело в том, что в процессорной системе используется две шины: шина инструкций и шина данных. Чтобы не переусложнять логику системы дополнительным мультиплексированием, программатор также будет реализовывать два отдельных интерфейса. При этом необходимо различать, когда выполняется программирование памяти инструкций, а когда — памяти данных. Поскольку обе эти памяти имеют независимые адресные пространства, адреса по которым может вестись программирование могут быть неотличимы. Однако с этой же проблемой мы сталкивались и в ЛР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)`
- `instr_addr_o` становится равен `flash_addr + flash_counter - 1`
- во всех остальных ситуациях `instr_wdata_o` и `instr_addr_o` сохраняют свое значение, а `instr_we_o` сбрасывается в ноль.
Сигналы памяти данных (`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)`
- `data_addr_o` становится равен `flash_addr + flash_counter - 1`
- во всех остальных ситуациях `data_wdata_o` и `data_addr_o` сохраняют свое значение, а `data_we_o` сбрасывается в ноль.
##### Реализация оставшейся части логики
Регистр `flash_size` работает следующим образом:
@@ -450,32 +509,12 @@ endmodule
Регистр `flash_addr` почти полностью повторяет поведение `flash_size`:
- сбрасывается в 0;
- в состоянии `RCV_NEXT_COMMAND` при `rx_valid` равном единице становится равен `{flash_size[2:0], rx_data}` (сдвигается на 1 байт влево и на освободившееся место ставится очередной пришедший байт);
- в состоянии `RCV_NEXT_COMMAND` при `rx_valid` равном единице становится равен `{flash_addr[2:0], rx_data}` (сдвигается на 1 байт влево и на освободившееся место ставится очередной пришедший байт);
- в остальных ситуациях сохраняет свое значение.
Сигнал `core_reset_o` равен единице в случае, если состояние конечного автомата не `FINISH`.
Оставшиеся сигналы (сигналы интерфейса памяти инструкций и памяти данных) работают по схожей логике.
Сигналы памяти инструкций (`instr_addr_o`, `instr_wdata_o`, `instr_write_enable_o`):
- сбрасываются в ноль
- в случае состояния `FLASH` и пришедшего сигнала `rx_valid`, если значение `flash_addr` меньше размера памяти инструкций в байтах:
- `instr_wdata_o` принимает значение `{instr_wdata_o[23:0], rx_data}` (справа вдвигается очередной пришедший байт)
- `instr_write_enable_o` становится равен `(flash_counter[1:0] == 2'b01)`
- `instr_addr_o` становится равен `flash_addr + flash_counter - 1`
- во всех остальных ситуациях `instr_wdata_o` и `instr_addr_o` сохраняют свое значение, а `instr_write_enable_o` сбрасывается в ноль.
Сигналы памяти данных (`data_addr_o`, `data_wdata_o`, `data_write_enable_o`):
- сбрасываются в ноль
- в случае состояния `FLASH` и пришедшего сигнала `rx_valid`, если значение `flash_addr` больше либо равно размеру памяти инструкций в байтах:
- `data_wdata_o` принимает значение `{data_wdata_o[23:0], rx_data}` (справа вдвигается очередной пришедший байт)
- `data_write_enable_o` становится равен `(flash_counter[1:0] == 2'b01)`
- `data_addr_o` становится равен `flash_addr + flash_counter - 1`
- во всех остальных ситуациях `data_wdata_o` и `data_addr_o` сохраняют свое значение, а `data_write_enable_o` сбрасывается в ноль.
> Так как вышесказанное по сути является полным описанием работы программатора на русском языке, то фактически **задача сводится к переводу** текста описания программатора выше **с руского на verilog**
> Так как вышесказанное по сути является полным описанием работы программатора на русском языке, то фактически **задача сводится к переводу** текста описания программатора **с русского на SystemVerilog**.
### Интеграция программатора в riscv_unit
@@ -489,41 +528,78 @@ _Рисунок 3. Интеграция программатора в `riscv_uni
### Пример загрузки программы
Чтобы проверить работу программатора на практике необходимо подготовить скомпилированную программу подобно тому, как это делалось в лабораторной работе 13. Подключить интерфейс последовательного порта к компьютеру также, как это делалось в лабораторной работе 12, после чего необходимо запустить скрипт:
Чтобы проверить работу программатора на практике необходимо подготовить скомпилированную программу подобно тому, как это делалось в ЛР№14 (или взять готовые .mem-файлы вашего варианта из ЛР№13). Однако, в отличие от ЛР№14, удалять первую строчку из файла, инициализирующего память данных не надо — теперь адрес загрузки будет использоваться в процессе загрузки.
Необходимо подключить отладочный стенд к последовательному порту компьютера (в случае платы Nexys A7 — достаточно просто подключить плату usb-кабелем, как это делалось на протяжении всех лабораторных для прошивки). Необходимо будет узнать COM-порт, по которому отладочный стенд подключен к компьютеру. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню пуск. В данном окне необходимо найти вкладку "Порты (COM и LPT)", раскрыть ее, а затем подключить отладочный стенд через USB-порт (если тот еще не был подключен). В списке появится новое устройство, а в скобках будет указан нужный COM-порт.
Подключив отладочный стенд к последовательному порту компьютера и сконфигурировав ПЛИС вашим проектом остается проинициализировать память. Сделать это можно с помощью предоставленного скрипта, пример запуска которого приведен в листинге 6.
```bash
# Пример использования скрипта. Указывается программа для записи и последовательный порт, к которому подключается программатор
python3 flash.py ./path/to/program COM3
# Пример использования скрипта. Сперва указываются опциональные аргументы
# (инициализация памяти данных и различных областей памяти vga-контроллера),
# Затем идут обязательные аргументы: файл для прошивки памяти инструкций и
# COM-порт.
$ python flash.py --help
usage: flash.py [-h] [-d DATA] [-c COLOR] [-s SYMBOLS] [-t TIFF] instr comport
positional arguments:
instr File for instr mem initialization
comport COM-port name
optional arguments:
-h, --help show this help message and exit
-d DATA, --data DATA File for data mem initialization
-c COLOR, --color COLOR
File for color mem initialization
-s SYMBOLS, --symbols SYMBOLS
File for symbols mem initialization
-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
```
_Листинг 6. Пример использования скрипта для инициализации памяти._
## Порядок выполнения задания
1. Напишите модуль перезаписываемой памяти инструкций. Данный модуль будет аналогичен памяти данных, только в нем не будет сигнала `mem_req_i`.
2. Создайте модуль `bluster`, используя предоставленный код.
1. Опишите модуль `rw_instr_mem`, используя код, представленный в _листинге 4_.
2. Добавьте пакет [`bluster_pkg`](bluster_pkg.sv), содержащий объявления параметров, используемых модулем и вспомогательных вызовов, используемых тестбенчем.
3. Опишите модуль `bluster`, используя код, представленный в _листинге 5_. Завершите описание этого модуля.
1. Опишите конечный автомат используя сигналы `state`, `next_state`, `send_fin`, `size_fin`, `flash_fin`, `next_round`.
2. [Реализуйте](#описание-модуля) логику сигналов `send_fin`, `size_fin`, `flash_fin`, `next_round`.
3. [Реализуйте](#реализация-конечного-автомата) логику счетчиков `size_counter`, `flash_counter`, `msg_counter`.
4. [Реализуйте](#реализация-сигналов-подключаемых-к-uart_tx) логику сигналов `tx_valid`, `tx_data`.
2. [Реализуйте](#реализация-конечного-автомата) логику счетчиков `size_counter`, `flash_counter`, `msg_counter`.
3. [Реализуйте](#реализация-сигналов-подключаемых-к-uart_tx) логику сигналов `tx_valid`, `tx_data`.
4. [Реализуйте](#реализация-интерфейсов-памяти-инструкций-и-данных) интерфейсы памяти инструкций и данных.
5. [Реализуйте](#реализация-оставшейся-части-логики) логику оставшихся сигналов.
3. После описания модуля, его необходимо проверить с помощью тестового окружения.
1. Тестовое окружение находится [здесь](tb_bluster.sv).
2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_bluster`).
4. Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!
4. Интегрируйте программатор в модуль `riscv_unit`.
1. Обновите память инструкций.
4. После описания модуля, его необходимо проверить с помощью тестового окружения.
1. Тестбенч находится [здесь](tb_bluster.sv).
2. Для работы тестбенча потребуется потребуется пакет [`peripheral_pkg`](../13.%20Peripheral%20units/peripheral_pkg.sv) из ЛР№13.
3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`lab_15_tb_bluster`).
4. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
5. **По завершению симуляции убедитесь, что в логе есть сообщение о завершении теста!**
5. Интегрируйте программатор в модуль `riscv_unit`.
1. Замените память инструкцией модулем `rw_instr_mem`.
2. Добавьте модуль программатор.
3. Подключите программатор к процессорной системе.
1. Интерфейс памяти инструкций подключается к порту записи обновленной памяти инструкций.
1. Интерфейс памяти инструкций подключается к порту записи модуля `rw_instr_mem`.
2. Интерфейс памяти данных мультиплексируется с интерфейсом памяти данных модуля `LSU`.
3. Замените сигнал сброса модуля `riscv_core` сигналом `core_reset_o`.
4. В случае если у вас есть периферийное устройство `uart_tx` его выход `tx_o` необходимо мультиплексировать с выходом `tx_o` программатора аналогично тому, как был мультиплексирован интерфейс памяти данных.
5. После интеграции модуля, его необходимо проверить с помощью тестового окружения.
6. После интеграции модуля, его необходимо проверить с помощью тестового окружения.
1. Тестовое окружение находится [здесь](tb_top_asic.sv).
2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_top_asic`).
4. Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста!
6. Переходить к следующему пункту можно только после того, как вы полностью убедились в работоспособности модуля на этапе моделирования (увидели, что в память инструкций и данных были записаны корректные данные). Генерация битстрима будет занимать у вас долгое время, а итогом вы получите результат: заработало / не заработало, без какой-либо дополнительной информации, поэтому без прочного фундамента на моделировании далеко уехать у вас не выйдет.
7. Подключите к проекту файл ограничений ([nexys_a7_100t.xdc](nexys_a7_100t.xdc)), если тот еще не был подключен, либо замените его содержимое данными из файла к этой лабораторной работе.
8. Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
1. Для прошивки процессорной системы используется скрипт [flash.py](flash.py).
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` — этот адрес не накладывается на адресное пространство других периферийных устройств, но при этом заведомо больше возможного размера памяти инструкций.
3. Тестбенч будет ожидать завершения инициализации памяти, после чего сформирует те же тестовые воздействия, что и в тестбенче к ЛР№13. А значит, если вы использовали для инициализации те же самые файлы, поведение вашей системы после инициализации не должно отличаться от поведения на симуляции в ЛР№13.
2. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_top_asic`).
3. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md).
4. **По завершению симуляции убедитесь, что в логе есть сообщение о завершении теста!**
7. Переходить к следующему пункту можно только после того, как вы полностью убедились в работоспособности системы на этапе моделирования (увидели, что в память инструкций и данных были записаны корректные данные, после чего процессор стал обрабатывать прерывания от устройства ввода). Генерация битстрима будет занимать у вас долгое время, а итогом вы получите результат: заработало / не заработало, без какой-либо дополнительной информации, поэтому без прочного фундамента на моделировании далеко уехать у вас не выйдет.
8. Подключите к проекту файл ограничений ([nexys_a7_100t.xdc](nexys_a7_100t.xdc)), если тот еще не был подключен, либо замените его содержимое данными из файла к этой лабораторной работе.
9. Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
1. Для инициализации памяти процессорной системы используется скрипт [flash.py](flash.py).
2. Перед инициализацией необходимо подключить отладочный стенд к последовательному порту компьютера и узнать номер этого порта (см. [пример загрузки программы](#пример-загрузки-программы)).
3. Формат файлов для инициализации памяти с помощью скрипта аналогичен формату, использовавшемуся в [тестбенче](tb_top_asic.sv). Это значит что первой строчкой всех файлов должна быть строка, содержащая адрес ячейки памяти, с которой должна начаться инициализация (см. п. 5.1.2).
## Список источников
1. [Finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine).

View File

@@ -16,10 +16,15 @@ parser.add_argument("instr", type=str, help="File for instr mem initialization")
parser.add_argument("comport", type=str, help="COM-port name")
parser.add_argument("-d", "--data", type=str, help="File for data mem initialization")
parser.add_argument("-c", "--color", type=str, help="File for color mem initialization")
parser.add_argument("-s", "--symbols", type=str, help="File for symbols mem initialization")
parser.add_argument("-t", "--tiff", type=str, help="File for tiff mem initialization")
args = parser.parse_args()
INIT_MSG_SIZE = 41
FLASH_MSG_SIZE = 57
ACK_MSG_SIZE = 4
def parse_file(fname: str, base: int = 16, chars_in_byte: int = 2, start_addr: int = None) -> dict:
res_bytes=b''
bytes_map = {}
@@ -30,7 +35,7 @@ def parse_file(fname: str, base: int = 16, chars_in_byte: int = 2, start_addr: i
assert(start_addr is not None)
bytes_map[start_addr] = res_bytes[::-1]
res_bytes = b''
start_addr = int(line[1:], 16)
start_addr = int(line[1:], 16)*4
else:
for word in line.split():
res_bytes += bytes(int(word,base).to_bytes(len(word)//chars_in_byte,"little"))
@@ -43,16 +48,16 @@ def flash(data: bytes, port: serial.Serial, start_addr: int):
addr_bytes = start_addr.to_bytes(4, "big")
port.write(addr_bytes)
ready_msg = port.read(40)
ready_msg = port.read(INIT_MSG_SIZE)
ready_msg_str = ready_msg.decode("ascii")
print(ready_msg_str)
assert(ready_msg_str == "ready for flash staring from 0x{:08x}\n".format(start_addr))
assert(ready_msg_str == "ready for flash starting from 0x{:08x}\n".format(start_addr))
data_len = len(data)
data_len_bytes = data_len.to_bytes(4, "big")
port.write(data_len_bytes)
data_len_ack_bytes = port.read(4)
data_len_ack_bytes = port.read(ACK_MSG_SIZE)
data_len_ack = int.from_bytes(data_len_ack_bytes,"big")
print("0x{:08x}".format(data_len_ack))
assert(data_len_ack == data_len)
@@ -61,7 +66,7 @@ def flash(data: bytes, port: serial.Serial, start_addr: int):
print("Sent {:08x} bytes".format(data_len))
data_flash_ack = port.read(57)
data_flash_ack = port.read(FLASH_MSG_SIZE)
data_flash_ack_str = data_flash_ack.decode("ascii")
print(data_flash_ack_str)
assert(data_flash_ack_str == "finished write 0x{:08x} bytes starting from 0x{:08x}\n".format(data_len, start_addr))
@@ -73,6 +78,7 @@ def flash(data: bytes, port: serial.Serial, start_addr: int):
inst_file = args.instr
data_file = args.data
color_file= args.color
symbol_file= args.symbols
tiff_file = args.tiff
com = args.comport
@@ -88,6 +94,11 @@ if color_file:
else:
color = {}
if symbol_file:
symbols = parse_file(symbol_file)
else:
symbols = {}
if tiff_file:
tiff = parse_file(tiff_file, 2, 8)
else:
@@ -100,10 +111,10 @@ ser = serial.Serial(
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=None
timeout=1
)
for ass_arr in [instr, data, color, tiff]:
for ass_arr in [instr, data, color, symbols, tiff]:
for addr, bytes_list in ass_arr.items():
flash(bytes_list, ser, addr)