diff --git a/Labs/15. Programming device/README.md b/Labs/15. Programming device/README.md index 2c2e235..813256a 100644 --- a/Labs/15. Programming device/README.md +++ b/Labs/15. Programming device/README.md @@ -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). diff --git a/Labs/15. Programming device/flash.py b/Labs/15. Programming device/flash.py index 41c1901..2563013 100644 --- a/Labs/15. Programming device/flash.py +++ b/Labs/15. Programming device/flash.py @@ -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)