11. Память на ПЛИС
Как и в большинстве вычислительных устройств, в ПЛИС есть своя иерархия памяти, построенная вокруг всё того же поиска баланса между скоростью доступа и объёмом (на самом деле ценой).
Нас будет интересовать память, которая доступна на кристале. В основном, это регистры и блочная память (B-SRAM).
Естественно, можно подключать и внешнюю (в том числе энергонезависимую) память, но это уже зависит от платы (в меньшей мере от чипа). Часто доступны DDR, Flash, при желании и какой-нибудь съёмный носитель можно подключить. Для ускорителей характерно использование HBM.
Block SRAM
Block SRAM (B-SRAM, иногда просто SRAM) — энергозависимая память на кристале, организованная не очень большими блоками (десятки килобит), суммарным объёмом порядка сотен килобайт. Например, для наших плат.
-
Для Tang Primer 25K: 56 блоков по 18 килобит (порядка 120 килобайт суммарно)
-
Для Tang Mega 138K Pro Dock: 340 блоков по 18 килобит (порядка 760 килобайт суммарно)
Каждый блок может быть сконфигурирован отдельно или включён в состав хранилища, организованного из нескольких блоков. На что важно обратить внимание.
-
Каждый блок может иметь два порта, каждый из которых может быть сконфигурирован независимо. В частности, каналы могут работать на разных частотах.
-
В блок могут быть загружены какие-либо (задаваемые пользователем) данные при инициализации.
У Gowin есть несколько режимов работы BSRAM:
-
Single port — один порт для чтения и записи.
-
Dual port — два независимых порта: оба могут использоваться и для чтения, и для записи.
-
Semi-Dual port — два порта: один для чтения, другой для записи.
-
Read Only — память, инициализирующаяся изначально и доступная только для чтения.
Блоки можно подключать вручную, но производители рекомендуют использовать возможности соответствующих САПР. Посмотреть, как это делается в нашем случае можно в инструкции.
Пример работы с памятью.
Рассмотрим простейший пример, в котором воспользуемся однопортовой BSRAM. Задача такая: на вход каждый такт поступает пиксель картинки, мы сначала записываем её в память, а после того, как место кончится, с начала выдаём на выход те пиксели, которые были записаны, а после записываем на его место новый пиксель. То есть реализуем простейший line buffer, который потребуется вам в дальнейшем для реализации свёртки изображения.
При работе с памятью больше всего затруднений вызывает именно аккуратная работы с задержками: запись происходит с задержкой, результат чтения (если использовать pipeline) происходит тоже только на следующий такт. После заполнения буфера, нам нужно будет читать пиксель из памяти, и только после этого перезаписывать. Это займёт как минимум два такта (вообще, три, если бы нам гипотетически приходилось бы читать из той же ячейки, в которую мы писали), что при поступлении картинки потактово вызовет неизбежные потери пикселей, подумайте, как можно подходить к решению этой проблемы, мы же перейдём к описанию кода.
Предварительно изучите, как подключать память. |
`timescale 1ns / 1ps (1)
module line_buffer #(
parameter int IMAGE_WIDTH = 640,
parameter int PIXEL_WIDTH = 8
)(
input logic clk,
input logic rst,
input logic [PIXEL_WIDTH - 1:0] pixel_in,
output logic [PIXEL_WIDTH - 1:0] pixel_out,
output logic ready_to_write (2)
);
logic wre;
logic[1:0] state;
logic[9:0] address;
logic ce = 1;
logic oce = 1;
parameter int FILLING = 0; (3)
parameter int WRITE = 1;
parameter int READ = 2;
logic [PIXEL_WIDTH-1:0] din;
Gowin_SP sp( (4)
.dout(pixel_out),
.clk(clk),
.oce(oce),
.ce(ce),
.reset(rst),
.wre(wre),
.ad(address),
.din(din)
);
always_ff @(posedge clk) begin
if (rst) begin
address <= 0;
state <= FILLING;
wre <= 1;
din <= pixel_in;
ready_to_write <= 1;
end
unique case(state)
FILLING: (5)
begin
din <= pixel_in;
if (address == IMAGE_WIDTH - 1) begin
state <= READ;
address <= 10'd0;
wre <= 0;
ready_to_write <= 0;
end
else address <= address + 10'd1;
end
READ: (6)
begin
state <= WRITE;
wre <= 1;
ready_to_write <= 1;
end
WRITE: (7)
begin
state <= READ;
wre <= 0;
ready_to_write <= 0;
din <= pixel_in;
if (address == IMAGE_WIDTH - 1) begin
address <= 10'd0;
end
else address <= address + 10'd1;
end
endcase
end
endmodule
1 | В подключенном нами $FPGA_TOOLS/simlib/gw1n/prim_sim.v есть timescale , для корректной работы, нам тоже это надо прописать.
Этот параметр важен только для симуляции и задаёт первым параметром единицу времени для симуляции (когда задаём задержку через #delay ), а вторым — точность, с которой происходит симуляция. |
2 | Мы будем говорить другим, когда мы готовы писать в память: когда происходит чтение, то пока не готовы. |
3 | Сама логика будет реализована через знакомый нам автомат Мили. |
4 | Собственно, подключаем модуль с памятью. |
5 | В самом начале пишем в память, пока можем. После переходим в состояние для чтения и меняем соответствующий флаг (wre ⇐ 0 ). |
6 | Читаем из памяти и переходим в состояние для записи, не меняя адрес. Меняем флаг для записи wre ⇐ 1 . |
7 | После записи, переходим в состояние чтения, поменяв адрес, по которому будем читать. |
Очень рекомендуем посмотреть, что происходит потактово через gtkwave. |
Обязательно порешайте упражнения. |