Files
APS/Labs/15. Programming device

Лабораторная работа 15 "Программатор"

Чтобы выпустить микроконтроллер в "дикую природу", то есть, чтобы его можно было использовать не в лабораторных условиях, а независимо от всего этого дополнительного оборудования, необходимо предусмотреть механизм замены исполняемой программы.

Цель

Реализация программатора — части микроконтроллера, обеспечивающего получение исполняемой программы из внешних, по отношению к системе, устройств.

Ход работы

  1. Познакомиться с информацией о программаторах и загрузчиках (#теория)
  2. Изучить информацию о конечных автоматах и способах их реализации (#практика)
  3. Описать перезаписываемую память инструкций (#память инструкций)
  4. Описать и проверить модуль программатора (#программатор)
  5. Интегрировать программатор в процессорную систему и проверить её (#интеграция)
  6. Проверить работу системы в ПЛИС с помощью предоставленного скрипта, инициализирующего память системы (#проверка)

Теория

До этого момента исполняемая процессором программа попадала в память инструкций через магический вызов $readmemh. Однако, реальные микроконтроллеры не обладают такими возможностями. Программа из внешнего мира попадает в них посредством так называемого программатора — устройства, обеспечивающего запись программы в память микроконтроллера. Программатор записывает данные в постоянное запоминающее устройство (ПЗУ). Для того, чтобы программа попала из ПЗУ в память инструкций (в ОЗУ), после запуска контроллера сперва начинает исполняться загрузчик (bootloader) — небольшая программа, вшитая в память микроконтроллера на этапе изготовления. Загрузчик отвечает за первичную инициализацию различных регистров и подготовку микроконтроллера к выполнению основной программы, включая её перенос из ПЗУ в память инструкций.

Со временем появилось несколько уровней загрузчиков: сперва запускается первичный загрузчик (first stage bootloader, fsbl), после которого запускается вторичный загрузчик (часто в роли вторичного загрузчика исполняется программа под названием u-boot). Такая иерархия загрузчиков может потребоваться, например, в случае загрузки операционной системы (которая хранится в файловой системе). Код для работы с файловой системой может попросту не уместиться в первичный загрузчик. В этом случае, целью первичного загрузчика является лишь загрузить вторичный загрузчик, который в свою очередь уже будет способен взаимодействовать с файловой системой и загрузить операционную систему.

Кроме того, код вторичного загрузчика может быть изменен, поскольку программируется вместе с основной программой. Первичный же загрузчик не всегда может быть изменен.

В рамках данной лабораторной работы мы немного упростим процесс передачи программы: вместо записи в ПЗУ, программатор будет записывать её сразу в память инструкций, минуя загрузчик.

Практика

Конечные автоматы (Finite-State Machines, FSM)

Программатор будет представлен в виде модуля с конечным автоматом. Конечный автомат представляет собой устройство, состоящее из:

  • элемента памяти (так называемого регистра состояния);
  • логики, обеспечивающей изменение значения регистра состояния (логики перехода между состояниями) в зависимости от его текущего состояния и входных сигналов;
  • логики, отвечающей за выходы конечного автомата.

Обычно, конечные автоматы описываются в виде направленного графа переходов между состояниями, где вершины графа — это состояния конечного автомата, а рёбра (дуги) — условия перехода из одного состояния в другое.

Простейшим примером конечного автомата может быть турникет. Когда в приёмник турникета опускается подходящий жетон, тот разблокирует вращающуюся треногу. После попытки поворота треноги, та блокируется до следующего жетона.

Иными словами, у турникета есть:

  • два состояния
    • заблокирован (locked)
    • разблокирован(unlocked)
  • два входа (события)
    • жетон принят (coin)
    • попытка поворота треноги (push)
  • один выход
    • блокировка треноги

Для описания двух состояний нам будет достаточно однобитного регистра. Для взаимодействия с регистром, нам потребуются так же сигнал синхронизации и сброса.

Опишем данный автомат в виде графа переходов:

https://upload.wikimedia.org/wikipedia/commons/9/9e/Turnstile_state_machine_colored.svg

Рисунок 1. Граф переходов конечного автомата для турникета[1].

Черной точкой со стрелкой в вершину Locked обозначен сигнал сброса. Иными словами, при сбросе турникет всегда переходит в заблокированное состояние.

Как мы видим, повторное опускание жетона в разблокированном состоянии приводит к сохранению этого состояния (но турникет не запоминает, что было опущено 2 жетона, и после первого же прохода станет заблокирован). В случае попытки поворота треноги в заблокированном состоянии, автомат так и останется в заблокированном состоянии.

Так же необходимо оговорить приоритет переходов: в первую очередь проверяется попытка поворота треноги, в случае если такой попытки не было, проверяется опускание монетки. Такой приоритет можно было бы указать и на графе, показав на ребрах что переход в состояние unlocked возможен только при отсутствии сигнала push.

Реализация конечных автоматов в SystemVerilog

Глядя на описание составляющих конечного автомата, вы могли задаться вопросом: чем автомат отличается от последовательностной логики, ведь она состоит из тех же компонент. Ответом будет: ничем. Конечные автоматы являются математической абстракцией над функцией последовательностной логики[3]. Иными словами — конечный автомат, это просто другой способ представления последовательностной логики, а значит вы уже умеете их реализовывать.

Для реализации регистра состояния конечного автомата будет удобно воспользоваться специальным типом языка 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

Листинг 1. Пример реализации конечного автомата для турникета.

Кроме того, при должной поддержке со стороны инструментов моделирования, значения объектов перечислений могут выводиться на временную диаграмму в виде перечисленных имен:

../../.pic/Labs/lab_15_programming_device/fig_02.png

Рисунок 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

Листинг 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

Листинг 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 в качестве интерфейса для обмена данными с внешним миром, а с другой — интерфейсы для записи полученных данных в память инструкций и память данных.

Описание модуля

В основе работы модуля лежит конечный автомат со следующим графом перехода между состояниями:

../../.pic/Labs/lab_15_programming_device/fig_03.drawio.svg

Рисунок 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 — условие завершения приема модулем размера будущей посылки;
  • 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;

(* mark_debug = "false" *) 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;
(* mark_debug = "false" *) logic [7:0] rx_data, tx_data;

(* 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;

(* 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 позволяет создавать структуры модуля цикличным или условным
// образом. В данном случае, при описании непрерывных присваиваний была
// обнаружена закономерность, позволяющая описать четверки присваиваний в более
// общем виде, который был описан в виде цикла.
// Важно понимать, данный цикл лишь автоматизирует описание присваиваний и во
// время синтеза схемы развернется в четыре четверки непрерывных присваиваний.
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

../../.pic/Labs/lab_15_programming_device/fig_04.drawio.svg

Рисунок 3. Интеграция программатора в 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. Пример использования скрипта для инициализации памяти.

Порядок выполнения задания

  1. Опишите модуль rw_instr_mem, используя код, представленный в листинге 4.
  2. Добавьте пакет bluster_pkg, содержащий объявления параметров, используемых модулем и вспомогательных вызовов, используемых тестбенчем.
  3. Опишите модуль bluster, используя код, представленный в листинге 5. Завершите описание этого модуля.
    1. Опишите конечный автомат используя сигналы state, next_state, send_fin, size_fin, flash_fin, next_round.
    2. Реализуйте логику счетчиков size_counter, flash_counter, msg_counter.
    3. Реализуйте логику сигналов tx_valid, tx_data.
    4. Реализуйте интерфейсы памяти инструкций и данных.
    5. Реализуйте логику оставшихся сигналов.
  4. После описания модуля, его необходимо проверить с помощью тестового окружения.
    1. Тестбенч находится здесь.
    2. Для работы тестбенча потребуется потребуется пакет peripheral_pkg из ЛР№13.
    3. Перед запуском симуляции убедитесь, что в качестве top-level модуля выбран корректный (lab_15_tb_bluster).
    4. Для запуска симуляции воспользуйтесь этой инструкцией.
    5. По завершению симуляции убедитесь, что в логе есть сообщение о завершении теста!
  5. Интегрируйте программатор в модуль riscv_unit.
    1. Замените память инструкцией модулем rw_instr_mem.
    2. Добавьте модуль программатор.
    3. Подключите программатор к процессорной системе.
      1. Интерфейс памяти инструкций подключается к порту записи модуля rw_instr_mem.
      2. Интерфейс памяти данных мультиплексируется с интерфейсом памяти данных модуля LSU.
      3. Замените сигнал сброса модуля riscv_core сигналом core_reset_o.
      4. В случае если у вас есть периферийное устройство uart_tx его выход tx_o необходимо мультиплексировать с выходом tx_o программатора аналогично тому, как был мультиплексирован интерфейс памяти данных.
  6. После интеграции модуля, его необходимо проверить с помощью тестового окружения.
    1. Тестовое окружение находится здесь.
      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. Для запуска симуляции воспользуйтесь этой инструкцией.
    4. По завершению симуляции убедитесь, что в логе есть сообщение о завершении теста!
  7. Переходить к следующему пункту можно только после того, как вы полностью убедились в работоспособности системы на этапе моделирования (увидели, что в память инструкций и данных были записаны корректные данные, после чего процессор стал обрабатывать прерывания от устройства ввода). Генерация битстрима будет занимать у вас долгое время, а итогом вы получите результат: заработало / не заработало, без какой-либо дополнительной информации, поэтому без прочного фундамента на моделировании далеко уехать у вас не выйдет.
  8. Подключите к проекту файл ограничений (nexys_a7_100t.xdc), если тот еще не был подключен, либо замените его содержимое данными из файла к этой лабораторной работе.
  9. Проверьте работу вашей процессорной системы на отладочном стенде с ПЛИС.
    1. Для инициализации памяти процессорной системы используется скрипт flash.py.
    2. Перед инициализацией необходимо подключить отладочный стенд к последовательному порту компьютера и узнать номер этого порта (см. пример загрузки программы).
    3. Формат файлов для инициализации памяти с помощью скрипта аналогичен формату, использовавшемуся в тестбенче. Это значит что первой строчкой всех файлов должна быть строка, содержащая адрес ячейки памяти, с которой должна начаться инициализация (см. п. 5.1.2).

Список источников

  1. Finite-state machine.