# Лабораторная работа 15 "Программатор" Чтобы выпустить микроконтроллер в "дикую природу", то есть, чтобы его можно было использовать не в лабораторных условиях, а независимо от всего этого дополнительного оборудования, необходимо предусмотреть механизм замены исполняемой программы. ## Цель Реализация программатора — части микроконтроллера, обеспечивающего получение исполняемой программы из внешних, по отношению к системе, устройств. ## Ход работы 1. Познакомиться с информацией о программаторах и загрузчиках ([#теория](#теория)) 2. Изучить информацию о конечных автоматах и способах их реализации ([#практика](#практика)) 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)) ## Теория До этого момента исполняемая процессором программа попадала в память инструкций через магический вызов `$readmemh`. Однако, реальные микроконтроллеры не обладают такими возможностями. Программа из внешнего мира попадает в них посредством так называемого **программатора** — устройства, обеспечивающего запись программы в память микроконтроллера. Программатор записывает данные в постоянное запоминающее устройство (ПЗУ). Для того, чтобы программа попала из ПЗУ в память инструкций (в ОЗУ), после запуска контроллера сперва начинает исполняться **загрузчик** (**bootloader**) — небольшая программа, вшитая в память микроконтроллера на этапе изготовления. Загрузчик отвечает за первичную инициализацию различных регистров и подготовку микроконтроллера к выполнению основной программы, включая её перенос из ПЗУ в память инструкций. Со временем появилось несколько уровней загрузчиков: сперва запускается **первичный загрузчик** (**first stage bootloader**, **fsbl**), после которого запускается **вторичный загрузчик** (часто в роли вторичного загрузчика исполняется программа под названием **u-boot**). Такая иерархия загрузчиков может потребоваться, например, в случае загрузки операционной системы (которая хранится в файловой системе). Код для работы с файловой системой может попросту не уместиться в первичный загрузчик. В этом случае, целью первичного загрузчика является лишь загрузить вторичный загрузчик, который в свою очередь уже будет способен взаимодействовать с файловой системой и загрузить операционную систему[[1]](https://stackoverflow.com/q/22455153). Кроме того, код вторичного загрузчика может быть изменен, поскольку программируется вместе с основной программой. Первичный загрузчик может быть изменен не во всех случаях. В рамках данной лабораторной работы мы немного упростим процесс передачи программы: вместо записи в ПЗУ, программатор будет записывать её сразу в память инструкций, минуя загрузчик. ## Практика ### Конечные автоматы (Finite-State Machines, FSM) Программатор будет представлен в виде модуля с [конечным автоматом](https://en.wikipedia.org/wiki/Finite-state_machine). Конечный автомат представляет собой устройство, состоящее из: - элемента памяти (так называемого **регистра состояния**); - логики, обеспечивающей изменение значения **регистра состояния** (логики перехода между состояниями) в зависимости от его текущего состояния и входных сигналов; - логики, отвечающей за выходы конечного автомата. Обычно, конечные автоматы описываются в виде направленного графа переходов между состояниями, где вершины графа — это состояния конечного автомата, а дуги — условия перехода из одного состояния в другое. Простейшим примером конечного автомата может быть турникет. Когда в приёмник турникета опускается подходящий жетон, тот разблокирует вращающуюся треногу. После попытки поворота треноги, та блокируется до следующего жетона. Иными словами, у турникета есть: - два состояния - заблокирован (`locked`) - разблокирован(`unlocked`) - два входа (события) - жетон принят (`coin`) - попытка поворота треноги (`push`) - один выход - блокировка треноги Для описания двух состояний нам будет достаточно однобитного регистра. Для взаимодействия с регистром, нам потребуются так же сигнал синхронизации и сброса. Опишем данный автомат в виде графа переходов: ![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)._ Черной точкой со стрелкой в вершину `Locked` обозначен сигнал сброса. Иными словами, при сбросе турникет всегда переходит в заблокированное состояние. Как мы видим, повторное опускание жетона в разблокированном состоянии приводит к сохранению этого состояния (но турникет не запоминает, что было опущено 2 жетона, и после первого же прохода станет заблокирован). В случае попытки поворота треноги в заблокированном состоянии, автомат так и останется в заблокированном состоянии. Так же необходимо оговорить приоритет переходов: в первую очередь проверяется попытка поворота треноги, в случае если такой попытки не было, проверяется опускание монетки. Такой приоритет можно было бы указать и на графе, показав на ребрах что переход в состояние unlocked возможен только при отсутствии сигнала `push`. ### Реализация конечных автоматов в SystemVerilog Глядя на описание составляющих конечного автомата, вы могли задаться вопросом: чем автомат отличается от последовательностной логики, ведь она состоит из тех же компонент. Ответом будет: ничем. Конечные автоматы являются математической абстракцией над функцией последовательностной логики[[3]](https://www.allaboutcircuits.com/textbook/digital/chpt-11/finite-state-machines/). Иными словами — конечный автомат, это просто другой способ представления последовательностной логики, а значит вы уже умеете их реализовывать. Для реализации регистра состояния конечного автомата будет удобно воспользоваться специальным типом языка **SystemVerilog**, который называется `enum` (**перечисление**). Перечисления позволяют объявить объединенный набор именованных констант. В дальнейшем, объявленные имена можно использовать вместо перечисленных значений, им соответствующих, что повышает читабельность кода. Если не указано иного, первому имени присваивается значение `0`, каждое последующее увеличивается на `1` относительно предыдущего значения. ```SystemVerilog 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; 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 ``` Кроме того, при должной поддержке со стороны инструментов моделирования, значения объектов перечислений могут выводиться на временную диаграмму в виде перечисленных имен: ![../../.pic/Labs/lab_15_programming_device/fig_02.png](../../.pic/Labs/lab_15_programming_device/fig_02.png) _Рисунок 2. Вывод значений объекта `enum` на временную диаграмму._ Для описания регистра состояния часто используют отдельный комбинационный сигнал, который подается непосредственно на его вход (часто именуемый как `next_state`). Приведенный выше автомат турникета слишком простой, чтобы показать преимущества такого подхода. Предположим, что в момент перехода из состояния `locked` в состояние `unlocked` мы хотим, чтобы загоралась и сразу гасла зеленая лампочка. Без сигнала `next_state` подобный модуль можно было бы описать как: ```SystemVerilog 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 = state; // (!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 ``` Используя сигнал `next_state`, автомат мог бы быть переписан следующим образом: ```SystemVerilog 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 ``` На первый взгляд может показаться, что так даже сложнее. Во-первых, появился дополнительный сигнал. Во-вторых, появился еще один `always`-блок. Однако представьте на секунду, что условиями перехода будут не однобитные входные сигналы, а какие-нибудь более сложные условия. И что от них будет зависеть не один выходной сигнал, а множество как выходных сигналов, так и внутренних элементов памяти помимо регистра состояний. В этом случае, сигнал `next_state` позволит избежать дублирования множества условий. Важно отметить, что объектам типа `enum` можно присваивать только перечисленные константы и объекты того же типа. Иными словами, `state` можно присваивать значения `LOCKED`/`UNLOCKED` и `next_state`, но нельзя, к примеру, присвоить `1'b0`. ## Задание Для выполнения данной лабораторной работы необходимо: - описать перезаписываемую память инструкций; - описать модуль-программатор; - заменить в `riscv_unit` память инструкций на новую, и интегрировать в `riscv_unit` программатор. ### Перезаписываемая память инструкций Поскольку ранее из памяти инструкций можно было только считывать данные, но не записывать их в нее, программатор не сможет записать принятую из внешнего мира программу. Поэтому необходимо добавить в память инструкций порт на запись. Для того, чтобы различать реализации памяти инструкций, данный модуль будет назвать `rw_instr_mem` со следующим прототипом: ```SystemVerilog module rw_instr_mem( 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 ); ``` Как вы помните, модуль `instr_mem` отличался от `data_mem` только портом на запись, так что данный модуль будет практически в точности повторять модуль `data_mem` за исключением того, что у этого модуля нет сигнала `req_i` (поскольку память инструкций подключена к процессору в обход системной шины). ### Программатор Необходимо реализовать модуль программатора, использующий с одной "стороны" `uart` в качестве интерфейса для обмена данными с внешним миром, а с другой — интерфейсы для записи полученных данных в память инструкций и память данных. #### Описание модуля В основе работы модуля лежит конечный автомат со следующим графом перехода между состояниями: ![../../.pic/Labs/lab_15_programming_device/fig_03.drawio.svg](../../.pic/Labs/lab_15_programming_device/fig_03.drawio.svg) _Рисунок 3. Граф перехода между состояниями программатора._ Условия перехода следующие: - `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 != 0) && !rx_busy` — условие записи блока данных через системную шину; Ниже представлен прототип модуля с частично реализованной логикой: ```SystemVerilog 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_write_enable_o, output logic [ 31:0] data_addr_o, output logic [ 31:0] data_wdata_o, output logic data_write_enable_o, output logic core_reset_o ); enum logic [2:0] { RCV_NEXT_COMMAND, INIT_MSG, RCV_SIZE, SIZE_ACK, FLASH, FLASH_ACK, WAIT_TX_DONE, 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; localparam INIT_MSG_SIZE = 40; localparam FLASH_MSG_SIZE = 57; localparam ACK_MSG_SIZE = 4; 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 на два ниббла. // Ниббл — это 4 бита. Каждый ниббл можно описать 16-ричной цифрой. // Если ниббл меньше 10 (4'ha), он описывается цифрами 0-9. Чтобы представить // его ascii-кодом, необходимо прибавить к нему число 8'h30 // (ascii-код символа '0'). // Если ниббл больше либо равен 10, он описывается буквами a-f. Для его // представления в виде ascii-кода, необходимо прибавить число 8'h57 // (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 staring 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, 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 (1'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 (1'b1 ), .tx_data_i (tx_data ), .tx_valid_i (tx_valid ) ); endmodule ``` Здесь уже объявлены: - `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`), - `flash_size_ascii`, `flash_addr_ascii`, `init_msg`, `flash_msg` (т.к. они уже реализованы в представленной выше логике). Так же необходимо реализовать выходы модуля программатора: - `instr_addr_o`; - `instr_wdata_o`; - `instr_write_enable_o`; - `data_addr_o`; - `data_wdata_o`; - `data_write_enable_o`; - `core_reset_o`. ##### Реализация конечного автомата Для реализации сигналов `state`, `next_state` представлен граф переходов между состояниями. В случае, если не выполняется ни одно из условий перехода, автомат должен остаться в текущем состоянии. Логика условий переходов уже была представлена сразу после графа переходов. Для работы логики переходов, необходимо реализовать счетчики `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`. Счетчик должен инициализироваться следующим образом: - в состоянии `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`. ##### Реализация оставшейся части логики Регистр `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_size[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** ### Интеграция программатора в riscv_unit ![../../.pic/Labs/lab_15_programming_device/fig_04.drawio.svg](../../.pic/Labs/lab_15_programming_device/fig_04.drawio.svg) _Рисунок 3. Интеграция программатора в `riscv_unit`._ В первую очередь, необходимо заменить память инструкций и добавить новый модуль. После чего подключить программатор к памяти инструкций и мультиплексировать выход интерфейса памяти данных программатора с интерфейсом памяти данных LSU. Сигнал сброса процессора необходимо заменить на выход `core_reset_o`. В случае, если использовалось периферийное устройство `uart_tx`, необходимо мультиплексировать его выход `tx_o` с одноименным выходом программатора аналогично тому, как это было сделано с сигналами интерфейса памяти данных. ### Пример загрузки программы Чтобы проверить работу программатора на практике необходимо подготовить скомпилированную программу подобно тому, как это делалось в лабораторной работе 13. Подключить интерфейс последовательного порта к компьютеру также, как это делалось в лабораторной работе 12, после чего необходимо запустить скрипт: ```bash # Пример использования скрипта. Указывается программа для записи и последовательный порт, к которому подключается программатор python3 flash.py ./path/to/program COM3 ``` ## Порядок выполнения задания 1. Напишите модуль перезаписываемой памяти инструкций. Данный модуль будет аналогичен памяти данных, только в нем не будет сигнала `mem_req_i`. 2. Создайте модуль `bluster`, используя предоставленный код. 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`. 5. [Реализуйте](#реализация-оставшейся-части-логики) логику оставшихся сигналов. 3. После описания модуля, его необходимо проверить с помощью тестового окружения. 1. Тестовое окружение находится [здесь](tb_bluster.sv). 2. Для запуска симуляции воспользуйтесь [`этой инструкцией`](../../Vivado%20Basics/Run%20Simulation.md). 3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (`tb_bluster`). 4. Во время симуляции, вы должны прожать "Run All" и убедиться, что в логе есть сообщение о завершении теста! 4. Интегрируйте программатор в модуль `riscv_unit`. 1. Обновите память инструкций. 2. Добавьте модуль программатор. 3. Подключите программатор к процессорной системе. 1. Интерфейс памяти инструкций подключается к порту записи обновленной памяти инструкций. 2. Интерфейс памяти данных мультиплексируется с интерфейсом памяти данных модуля `LSU`. 3. Замените сигнал сброса модуля `riscv_core` сигналом `core_reset_o`. 4. В случае если у вас есть периферийное устройство `uart_tx` его выход `tx_o` необходимо мультиплексировать с выходом `tx_o` программатора аналогично тому, как был мультиплексирован интерфейс памяти данных. 5. После интеграции модуля, его необходимо проверить с помощью тестового окружения. 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).