Лабораторная работа №15 "Программатор"
Чтобы выпустить микроконтроллер в "дикую природу", то есть, чтобы его можно было использовать не в лабораторных условиях, а независимо от всего этого дополнительного оборудования, необходимо предусмотреть механизм замены исполняемой программы.
Цель
Реализация программатора — части микроконтроллера, обеспечивающего получение исполняемой программы из внешних, по отношению к системе, устройств.
Ход работы
- Познакомиться с информацией о программаторах и загрузчиках (#теория)
- Изучить информацию о конечных автоматах и способах их реализации (#практика)
- Описать перезаписываемую память инструкций (#память инструкций)
- Описать и проверить модуль программатора (#программатор)
- Интегрировать программатор в процессорную систему и проверить её (#интеграция)
- Проверить работу системы в ПЛИС с помощью предоставленного скрипта, инициализирующего память системы (#проверка)
Теория
До этого момента исполняемая процессором программа попадала в память инструкций через магический вызов $readmemh
. Однако, реальные микроконтроллеры не обладают такими возможностями. Программа из внешнего мира попадает в них посредством так называемого программатора — устройства, обеспечивающего запись программы в память микроконтроллера. Программатор записывает данные в постоянное запоминающее устройство (ПЗУ). Для того, чтобы программа попала из ПЗУ в память инструкций (в ОЗУ), после запуска контроллера сперва начинает исполняться загрузчик (bootloader) — небольшая программа, вшитая в память микроконтроллера на этапе изготовления. Загрузчик отвечает за первичную инициализацию различных регистров и подготовку микроконтроллера к выполнению основной программы, включая её перенос из ПЗУ в память инструкций.
Со временем появилось несколько уровней загрузчиков: сперва запускается первичный загрузчик (first stage bootloader, fsbl), после которого запускается вторичный загрузчик (часто в роли вторичного загрузчика исполняется программа под названием u-boot). Такая иерархия загрузчиков может потребоваться, например, в случае загрузки операционной системы (которая хранится в файловой системе). Код для работы с файловой системой может попросту не уместиться в первичный загрузчик. В этом случае, целью первичного загрузчика является лишь загрузить вторичный загрузчик, который в свою очередь уже будет способен взаимодействовать с файловой системой и загрузить операционную систему.
Кроме того, код вторичного загрузчика может быть изменен, поскольку программируется вместе с основной программой. Первичный же загрузчик не всегда может быть изменен.
В рамках данной лабораторной работы мы немного упростим процесс передачи программы: вместо записи в ПЗУ, программатор будет записывать её сразу в память инструкций, минуя загрузчик.
Практика
Конечные автоматы (Finite-State Machines, FSM)
Программатор будет представлен в виде модуля с конечным автоматом. Конечный автомат представляет собой устройство, состоящее из:
- элемента памяти (так называемого регистра состояния);
- логики, обеспечивающей изменение значения регистра состояния (логики перехода между состояниями) в зависимости от его текущего состояния и входных сигналов;
- логики, отвечающей за выходы конечного автомата.
Обычно, конечные автоматы описываются в виде направленного графа переходов между состояниями, где вершины графа — это состояния конечного автомата, а рёбра (дуги) — условия перехода из одного состояния в другое.
Простейшим примером конечного автомата может быть турникет. Когда в приёмник турникета опускается подходящий жетон, тот разблокирует вращающуюся треногу. После попытки поворота треноги, та блокируется до следующего жетона.
Иными словами, у турникета есть:
- два состояния
- заблокирован (
locked
) - разблокирован(
unlocked
)
- заблокирован (
- два входа (события)
- жетон принят (
coin
) - попытка поворота треноги (
push
)
- жетон принят (
- один выход
- блокировка треноги
Для описания двух состояний нам будет достаточно однобитного регистра. Для взаимодействия с регистром, нам потребуются так же сигнал синхронизации и сброса.
Опишем данный автомат в виде графа переходов:
Рисунок 1. Граф переходов конечного автомата для турникета[1].
Черной точкой со стрелкой в вершину Locked
обозначен сигнал сброса. Иными словами, при сбросе турникет всегда переходит в заблокированное состояние.
Как мы видим, повторное опускание жетона в разблокированном состоянии приводит к сохранению этого состояния (но турникет не запоминает, что было опущено 2 жетона, и после первого же прохода станет заблокирован). В случае попытки поворота треноги в заблокированном состоянии, автомат так и останется в заблокированном состоянии.
Так же необходимо оговорить приоритет переходов: в первую очередь проверяется попытка поворота треноги, в случае если такой попытки не было, проверяется опускание монетки. Такой приоритет можно было бы указать и на графе, показав на рёбрах что переход в состояние unlocked возможен только при отсутствии сигнала push
.
Реализация конечных автоматов в SystemVerilog
Глядя на описание составляющих конечного автомата, вы могли задаться вопросом: чем автомат отличается от последовательностной логики, ведь она состоит из тех же компонент. Ответом будет: ничем. Конечные автоматы являются математической абстракцией над функцией последовательностной логики[2]. Иными словами — конечный автомат, это просто другой способ представления последовательностной логики, а значит вы уже умеете их реализовывать.
Для реализации регистра состояния конечного автомата будет удобно воспользоваться специальным типом языка SystemVerilog, который называется enum
(перечисление).
Перечисления позволяют объявить объединенный набор именованных констант. В дальнейшем, объявленные имена можно использовать вместо перечисленных значений, им соответствующих, что повышает читаемость кода. Если не указано иного, первому имени присваивается значение 0
, каждое последующее увеличивается на 1
относительно предыдущего значения.
module turnstile_fsm(
input logic clk,
input logic rst,
input logic push,
input logic coin,
output logic is_locked
);
enum logic {LOCKED=1, UNLOCKED=0} state;
assign is_locked = state == LOCKED;
always_ff @(posedge clk) begin
if(rst) begin
state <= LOCKED;
end
else begin
if(push) begin
state <= LOCKED;
end
else if (coin) begin
state <= UNLOCKED;
end
else begin
state <= state;
end
end
end
endmodule
Листинг 1. Пример реализации конечного автомата для турникета.
Кроме того, при должной поддержке со стороны инструментов моделирования, значения объектов перечислений могут выводиться на временную диаграмму в виде перечисленных имен:
Рисунок 2. Вывод значений объекта enum
на временную диаграмму.
Для описания регистра состояния часто используют отдельный комбинационный сигнал, который подаётся непосредственно на его вход (часто именуемый как next_state
). Приведённый выше автомат турникета слишком простой, чтобы показать преимущества такого подхода. Предположим, что в момент перехода из состояния locked
в состояние unlocked
мы хотим, чтобы загоралась и сразу гасла зелёная лампочка. Без сигнала next_state
подобный модуль можно было бы описать как:
module turnstile_fsm(
input logic clk,
input logic rst,
input logic push,
input logic coin,
output logic is_locked,
output logic green_light
);
enum logic {LOCKED=1, UNLOCKED=0} state;
assign is_locked = ststate == LOCKED;
// (!push) && coin — условие перехода в состояние UNLOCKED
assign green_light = (state == LOCKED) && (!push) && coin;
always_ff @(posedge clk) begin
if(rst) begin
state <= LOCKED;
end
else begin
if(push) begin
state <= LOCKED;
end
else if (coin) begin
state <= UNLOCKED;
end
else begin
state <= state;
end
end
end
endmodule
Листинг 2. Пример реализации конечного автомата для усложнённого турникета.
Используя сигнал next_state
, автомат мог бы быть переписан следующим образом:
module turnstile_fsm(
input logic clk,
input logic rst,
input logic push,
input logic coin,
output logic is_locked,
output logic green_light
);
enum logic {LOCKED=1, UNLOCKED=0} state, next_state;
assign is_locked = state;
assign green_light = (state == LOCKED) && (next_state == UNLOCKED);
always_ff @(posedge clk) begin
if(rst) begin
state <= LOCKED;
end
else begin
state <= next_state
end
end
always_comb begin
if(push) begin
next_state = LOCKED;
end
else if (coin) begin
next_state = UNLOCKED;
end
else begin
next_state = state;
end
end
endmodule
Листинг 3. Пример реализации конечного автомата для усложнённого турникета с использованием сигнала next_state.
На первый взгляд может показаться, что так даже сложнее. Во-первых, появился дополнительный сигнал. Во-вторых, появился ещё один always
-блок. Однако представьте на секунду, что условиями перехода будут не однобитные входные сигналы, а какие-нибудь более сложные условия. И что от них будет зависеть не один выходной сигнал, а множество как выходных сигналов, так и внутренних элементов памяти помимо регистра состояний. В этом случае, сигнал next_state
позволит избежать дублирования множества условий.
Важно отметить, что объектам типа enum
можно присваивать только перечисленные константы и объекты того же типа. Иными словами, state
можно присваивать значения LOCKED
/UNLOCKED
и next_state
, но нельзя, к примеру, присвоить 1'b0
.
Задание
Для выполнения данной лабораторной работы необходимо:
- описать перезаписываемую память инструкций;
- описать модуль-программатор;
- заменить в
riscv_unit
память инструкций на новую, и интегрировать вriscv_unit
программатор.
Перезаписываемая память инструкций
Поскольку ранее из памяти инструкций можно было только считывать данные, но не записывать их в неё, программатор не сможет записать принятую из внешнего мира программу. Поэтому необходимо добавить в память инструкций порт на запись. Для того, чтобы различать реализации памяти инструкций, данный модуль будет называться 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,
input logic [31:0] write_addr_i,
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
Листинг 4. Модуль rw_instr_mem.
Программатор
Необходимо реализовать модуль программатора, использующий с одной "стороны" uart
в качестве интерфейса для обмена данными с внешним миром, а с другой — интерфейсы для записи полученных данных в память инструкций и память данных.
Описание модуля
В основе работы модуля лежит конечный автомат со следующим графом перехода между состояниями:
Рисунок 3. Граф перехода между состояниями программатора.
Данный автомат реализует следующий алгоритм:
- Получение команды ("запись очередного блока" / "программирование завершено"). Данная команда представляет собой адрес записи очередного блока, и в случае, если адрес равен 0xFFFFFFFF, это означает команду "программирование завершено".
- В случае получения команды "программирование завершено", модуль завершает свою работу, снимая сигнал сброса с процессора.
- В случае получения команды "запись очередного блока" происходит переход к п. 2.
- Модуль отправляет сообщение о готовности принимать размер очередного блока.
- Выполняется передача размера очередного блока.
- Модуль подтверждает получение размера очередного блока и повторяет его значение.
- Выполняется передача очередного блока, который записывается, начиная с адреса, принятого в п.1.
- Получив заданное в п.3 количество байт очередного блока, модуль сообщает о завершении записи и переходит к ожиданию очередной команды в п.1.
На графе перехода автомата обозначены следующие условия:
send_fin = ( msg_counter == 0) && !tx_busy
— условие завершения передачи модулем сообщения поuart_tx
;size_fin = ( size_counter == 0) && !rx_busy
— условие завершения приема модулем размера будущей посылки;flash_fin = (flash_counter == 0) && !rx_busy
— условие завершения приема модулем блока записываемых данных;next_round = (flash_addr !='1) && !rx_busy
— условие записи блока данных через системную шину;
Ниже представлен прототип модуля с частично реализованной логикой:
module bluster
(
input logic clk_i,
input logic rst_i,
input logic rx_i,
output logic tx_o,
output logic [ 31:0] instr_addr_o,
output logic [ 31:0] instr_wdata_o,
output logic instr_we_o,
output logic [ 31:0] data_addr_o,
output logic [ 31:0] data_wdata_o,
output logic data_we_o,
output logic core_reset_o
);
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;
enum logic [2:0] {
RCV_NEXT_COMMAND,
INIT_MSG,
RCV_SIZE,
SIZE_ACK,
FLASH,
FLASH_ACK,
FINISH}
state, next_state;
logic rx_busy, rx_valid, tx_busy, tx_valid;
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;
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;
logic [7:0] [7:0] flash_size_ascii, flash_addr_ascii;
// Блок generate позволяет создавать структуры модуля цикличным или условным
// образом. В данном случае, при описании непрерывных присваиваний была
// обнаружена закономерность, позволяющая описать четверки присваиваний в более
// общем виде, который был описан в виде цикла.
// Важно понимать, данный цикл лишь автоматизирует описание присваиваний и во
// время синтеза схемы развернется в четыре четверки непрерывных присваиваний.
genvar i;
generate
for(i=0; i < 4; i=i+1) begin
// Данная логика преобразовывает сигналы flash_size и flash_addr,
// которые представляют собой "сырые" двоичные числа в ASCII-символы[1]
// Разделяем каждый байт flash_size и flash_addr на два ниббла.
// Ниббл — это 4 бита. Каждый ниббл можно описать 16-битной цифрой.
// Если ниббл меньше 10 (4'ha), он описывается цифрами 0-9. Чтобы представить
// его ascii-кодом, необходимо прибавить к нему число 8'h30
// (ascii-код символа '0').
// Если ниббл больше либо равен 10, он описывается буквами a-f. Для его
// представления в виде ascii-кода, необходимо прибавить число 8'h57
// (это уменьшенный на 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 :
flash_size[i][7:4] + 8'h57;
assign flash_addr_ascii[i*2] = flash_addr[i][3:0] < 4'ha ? flash_addr[i][3:0] + 8'h30 :
flash_addr[i][3:0] + 8'h57;
assign flash_addr_ascii[i*2+1] = flash_addr[i][7:4] < 4'ha ? flash_addr[i][7:4] + 8'h30 :
flash_addr[i][7:4] + 8'h57;
end
endgenerate
logic [INIT_MSG_SIZE-1:0][7:0] init_msg;
// 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'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;
//ascii-код строки: "finished write 0xflash_size bytes starting from 0xflash_addr\n"
assign flash_msg = {8'h66, 8'h69, 8'h6E, 8'h69, 8'h73, 8'h68, 8'h65, 8'h64,
8'h20, 8'h77, 8'h72, 8'h69, 8'h74, 8'h65, 8'h20, 8'h30,
8'h78, flash_size_ascii, 8'h20, 8'h62, 8'h79,
8'h74, 8'h65, 8'h73, 8'h20, 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};
uart_rx rx(
.clk_i (clk_i ),
.rst_i (rst_i ),
.rx_i (rx_i ),
.busy_o (rx_busy ),
.baudrate_i (17'd115200 ),
.parity_en_i(1'b1 ),
.stopbit_i (2'b1 ),
.rx_data_o (rx_data ),
.rx_valid_o (rx_valid )
);
uart_tx tx(
.clk_i (clk_i ),
.rst_i (rst_i ),
.tx_o (tx_o ),
.busy_o (tx_busy ),
.baudrate_i (17'd115200 ),
.parity_en_i(1'b1 ),
.stopbit_i (2'b1 ),
.tx_data_i (tx_data ),
.tx_valid_i (tx_valid )
);
endmodule
Листинг 5. Готовая часть программатора.
Здесь уже объявлены:
enum
-сигналыstate
иnext_state
;- сигналы,
send_fin
,size_fin
,flash_fin
,next_round
, используемые в качестве условий переходов между состояниями; - счетчики
msg_counter
,size_counter
,flash_counter
, необходимые для реализации условий переходов; - сигналы, необходимые для подключения модулей
uart_rx
иuart_tx
:rx_busy
,rx_valid
,tx_busy
,tx_valid
,rx_data
,tx_data
;
- модули
uart_rx
,uart_tx
; - сигналы
init_msg
,flash_msg
, хранящие ascii-код ответов программатора, а также логику и сигналы, необходимые для реализации этих ответов:flash_size
,flash_addr
,flash_size_ascii
,flash_addr_ascii
.
Реализация модуля программатора
Для реализации данного модуля, необходимо реализовать все объявленные выше сигналы, кроме сигналов:
rx_busy
,rx_valid
,rx_data
,tx_busy
(т.к. те уже подключены к выходам модулейuart_rx
иuart_tx
),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_we_o
;data_addr_o
;data_wdata_o
;data_we_o
;core_reset_o
.
Реализация конечного автомата
Для реализации сигналов state
, next_state
используйте граф переходов между состояниями, представленный на рис. 3. В случае, если не выполняется ни одно из условий перехода, автомат должен остаться в текущем состоянии.
Для работы логики переходов, необходимо реализовать счетчики size_counter
, flash_counter
, msg_counter
.
size_counter
должен сбрасываться в значение 4
, а также принимать это значение во всех состояниях кроме: RCV_SIZE
, RCV_NEXT_COMMAND
. В данных двух состояниях счётчик должен декрементироваться в случае, если rx_valid
равен единице.
flash_counter
должен сбрасываться в значение flash_size
, а также принимать это значение во всех состояниях кроме FLASH
. В этом состоянии счётчик должен декрементироваться в случае, если rx_valid
равен единице.
msg_counter
должен сбрасываться в значение INIT_MSG_SIZE-1
(в Листинге 5 объявлены параметры INIT_MSG_SIZE
, FLASH_MSG_SIZE
и ACK_MSG_SIZE
).
счётчик должен инициализироваться следующим образом:
- в состоянии
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
равен единице.
Во всех остальных ситуациях счётчик должен сохранять свое значение.
Реализация сигналов, подключаемых к uart_tx
Сигнал tx_valid
должен быть равен единице только когда tx_busy
равен нулю, а конечный автомат находится в одном из следующих состояний:
INIT_MSG
,SIZE_ACK
,FLASH_ACK
Иными словами, tx_valid
равен единице, когда автомат находится в состоянии, отвечающем за передачу сообщений от программатора, но в данный момент программатор не отправляет очередной байт сообщения.
Сигнал tx_data
должен нести очередной байт одного из передаваемых сообщений:
- в состоянии
INIT_MSG
передаётся очередной байт сообщенияinit_msg
- в состоянии
SIZE_ACK
передаётся очередной байт сообщенияflash_size
- в состоянии
FLASH_ACK
передаётся очередной байт сообщенияflash_msg
.
В остальных состояниях он равен нулю. Для отсчёта байт используется счётчик 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
работает следующим образом:
- сбрасывается в 0;
- в состоянии
RCV_SIZE
приrx_valid
равном единице становится равен{flash_size[2:0], rx_data}
(сдвигается на 1 байт влево и на освободившееся место ставится очередной пришедший байт); - в остальных ситуациях сохраняет свое значение.
Регистр flash_addr
почти полностью повторяет поведение flash_size
:
- сбрасывается в 0;
- в состоянии
RCV_NEXT_COMMAND
приrx_valid
равном единице становится равен{flash_addr[2:0], rx_data}
(сдвигается на 1 байт влево и на освободившееся место ставится очередной пришедший байт); - в остальных ситуациях сохраняет свое значение.
Сигнал core_reset_o
равен единице в случае, если состояние конечного автомата не FINISH
.
Так как вышесказанное по сути является полным описанием работы программатора на русском языке, то фактически задача сводится к переводу текста описания программатора с русского на SystemVerilog.
Интеграция программатора в riscv_unit
Рисунок 4. Интеграция программатора в riscv_unit
.
В первую очередь, необходимо заменить память инструкций и добавить новый модуль. После чего подключить программатор к памяти инструкций и мультиплексировать выход интерфейса памяти данных программатора с интерфейсом памяти данных LSU. Сигнал сброса процессора необходимо заменить на выход core_reset_o
.
В случае, если использовалось периферийное устройство uart_tx
, необходимо мультиплексировать его выход tx_o
с одноименным выходом программатора аналогично тому, как это было сделано с сигналами интерфейса памяти данных.
Пример загрузки программы
Чтобы проверить работу программатора на практике необходимо подготовить скомпилированную программу подобно тому, как это делалось в ЛР№14 (или взять готовые .mem-файлы вашего варианта из ЛР№13). Однако, в отличие от ЛР№14, удалять первую строчку из файла, инициализирующего память данных не надо — теперь адрес загрузки будет использоваться в процессе загрузки.
Необходимо подключить отладочный стенд к последовательному порту компьютера (в случае платы Nexys A7 — достаточно просто подключить плату usb-кабелем, как это делалось на протяжении всех лабораторных для прошивки). Необходимо будет узнать COM-порт, по которому отладочный стенд подключен к компьютеру. Определить нужный COM-порт на операционной системе Windows можно через "Диспетчер устройств", который можно открыть через меню пуск. В данном окне необходимо найти вкладку "Порты (COM и LPT)", раскрыть ее, а затем подключить отладочный стенд через USB-порт (если тот еще не был подключен). В списке появится новое устройство, а в скобках будет указан нужный COM-порт.
Подключив отладочный стенд к последовательному порту компьютера и сконфигурировав ПЛИС вашим проектом, остается проинициализировать память. Сделать это можно с помощью предоставленного скрипта, пример запуска которого приведен в листинге 6.
# Пример использования скрипта. Сперва указываются опциональные аргументы
# (инициализация памяти данных и различных областей памяти 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. Пример использования скрипта для инициализации памяти.
Порядок выполнения задания
- Опишите модуль
rw_instr_mem
, используя код, представленный в листинге 4. - Добавьте пакет
bluster_pkg
, содержащий объявления параметров и вспомогательных вызовов, используемых модулем и тестбенчем. - Опишите модуль
bluster
, используя код, представленный в листинге 5. Завершите описание этого модуля.- Опишите конечный автомат используя сигналы
state
,next_state
,send_fin
,size_fin
,flash_fin
,next_round
. - Реализуйте логику счетчиков
size_counter
,flash_counter
,msg_counter
. - Реализуйте логику сигналов
tx_valid
,tx_data
. - Реализуйте интерфейсы памяти инструкций и данных.
- Реализуйте логику оставшихся сигналов.
- Опишите конечный автомат используя сигналы
- Проверьте модуль с помощью верификационного окружения, представленного в файле
lab_15.tb_bluster.sv
. В случае, если в TCL-консоли появились сообщения об ошибках, вам необходимо найти и исправить их.- Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в
Simulation Sources
. - Для работы тестбенча потребуется пакет
peripheral_pkg
из ЛР№13, а также файлыlab_15_char.mem
,lab_15_data.mem
,lab_15_instr.mem
из папки mem_files.
- Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в
- Интегрируйте программатор в модуль
processor_system
.- В модуле
processor_system
замените память инструкцией модулемrw_instr_mem
. - Добавьте в модуль
processor_system
экземпляр модуля-программатора.- Интерфейс памяти инструкций подключается к порту записи модуля
rw_instr_mem
. - Интерфейс памяти данных мультиплексируется с интерфейсом памяти данных модуля
LSU
. - Замените сигнал сброса модуля
riscv_core
сигналомcore_reset_o
. - В случае если у вас есть периферийное устройство
uart_tx
его выходtx_o
необходимо мультиплексировать с выходомtx_o
программатора аналогично тому, как был мультиплексирован интерфейс памяти данных.
- Интерфейс памяти инструкций подключается к порту записи модуля
- В модуле
- Проверьте процессорную систему после интеграции программатора с помощью верификационного окружения, представленного в файле
lab_15.tb_processor_system.sv
.- Данный тестбенч необходимо обновить под свой вариант. Найдите строки со вспомогательным вызовом
program_region
, первыми аргументами которого являются "YOUR_INSTR_MEM_FILE" и "YOUR_DATA_MEM_FILE". Обновите эти строки под имена файлов, которыми вы инициализировали свои память инструкций и данных в ЛР№13. Если память данных вы не инициализировали, можете удалить/закомментировать соответствующий вызов. При необходимости вы можете добавить столько вызовов, сколько вам потребуется. - В .mem-файлах, которыми вы будете инициализировать вашу память необходимо сделать доработку. Вам необходимо указать адрес ячейки памяти, с которой необходимо начать инициализировать память. Это делается путем добавления в начало файла строки вида:
@hex_address
. Пример@FA000000
. Строка обязательно должна начинаться с символа@
, а адрес обязательно должен быть в шестнадцатеричном виде. Для памяти инструкций нужен нулевой адрес, а значит можно использовать строку@00000000
. Для памяти данных необходимо адрес, превышающий размер памяти инструкций, но не попадающий в адресное пространство других периферийных устройств (старший байт адреса должен быть равен нулю). Поскольку система использует байтовую адресацию, адрес ячеек будет в 4 раза меньше адреса по которому обратился бы процессор. Это значит, что если бы вы хотели проинициализировать память VGA-контроллера, вам нужно было бы использовать не адрес@07000000
, а@01C00000
(01C00000 * 4 = 07000000
). Таким образом, для памяти данных оптимальным адресом инициализации будет@00200000
, поскольку эта ячейка с адресом00200000
соответствует адресу00800000
— этот адрес не накладывается на адресное пространство других периферийных устройств, но при этом заведомо больше возможного размера памяти инструкций. Примеры использования начальных адресов вы можете посмотреть в файлахlab_15_char.mem
,lab_15_data.mem
,lab_15_instr.mem
из папки mem_files. - Тестбенч будет ожидать завершения инициализации памяти, после чего сформирует те же тестовые воздействия, что и в тестбенче к ЛР№13. А значит, если вы использовали для инициализации те же самые файлы, поведение вашей системы после инициализации не должно отличаться от поведения на симуляции в ЛР№13.
- Перед запуском моделирования, убедитесь, что у вас выбран корректный модуль верхнего уровня в
Simulation Sources
.
- Данный тестбенч необходимо обновить под свой вариант. Найдите строки со вспомогательным вызовом
- Переходить к следующему пункту можно только после того, как вы полностью убедились в работоспособности системы на этапе моделирования (увидели, что в память инструкций и данных были записаны корректные данные, после чего процессор стал обрабатывать прерывания от устройства ввода). Генерация битстрима будет занимать у вас долгое время, а итогом вы получите результат: заработало / не заработало, без какой-либо дополнительной информации, поэтому без прочного фундамента на моделировании далеко уехать у вас не выйдет.
- Подключите к проекту файл ограничений (nexys_a7_100t.xdc), если тот еще не был подключен, либо замените его содержимое данными из файла к этой лабораторной работе.
- Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
- Для инициализации памяти процессорной системы используется скрипт flash.py.
- Перед инициализацией необходимо подключить отладочный стенд к последовательному порту компьютера и узнать номер этого порта (см. пример загрузки программы).
- Формат файлов для инициализации памяти с помощью скрипта аналогичен формату, использовавшемуся в тестбенче. Это значит что первой строчкой всех файлов должна быть строка, содержащая адрес ячейки памяти, с которой должна начаться инициализация (см. п. 5.1.2).
- В текущем исполнении, инициализировать память системы можно только 1 раз с момента сброса, что может оказаться не очень удобным при отладке программ. Подумайте, как можно модифицировать конечный автомат программатора таким образом, чтобы получить возможность в неограниченном количестве инициализаций памяти без повторного сброса всей системы.