Память в FPGA: block RAM
Разбираем встроенную память FPGA и учимся описывать её так, чтобы синтезатор использовал готовые блоки BRAM.
Block RAM (BRAM) — встроенные в FPGA блоки памяти на килобиты, предназначенные для хранения массивов данных: буферов, таблиц, очередей; гораздо экономичнее, чем память на триггерах.
Хранить пару чисел можно в регистрах. Но что, если нужно буфер на 1024 значения или таблица синуса на тысячи точек? Строить это на триггерах безумно расточительно — ушли бы тысячи драгоценных ячеек. Поэтому в FPGA встроены специальные блоки памяти — block RAM. Понимание, как их «попросить» у синтезатора, отличает наивный дизайн от эффективного.
Регистры против block RAM
Сравним два способа хранить массив:
| Критерий | Память на регистрах | Block RAM |
| Объём | десятки слов (дорого) | тысячи слов (дёшево) |
| Доступ | ко всем словам сразу | 1-2 слова за такт (по адресу) |
| Ресурс | триггеры (LUT-логика) | выделенные блоки BRAM |
| Когда применять | малые быстрые регистры | буферы, таблицы, FIFO |
Ключевой компромисс: регистры дают доступ ко всем элементам одновременно, но их мало; BRAM хранит много, но за такт можно прочитать/записать лишь по одному-двум адресам. Для потоковых данных этого обычно достаточно.
Как описать BRAM на Verilog
Хитрость в том, что в Verilog нет ключевого слова «block RAM». Вместо этого пишут память в особом стиле, и синтезатор сам выводит (инферит) BRAM. Главное условие — синхронное чтение (через регистр адреса по такту). Однопортовая память:
module bram #(
parameter WIDTH = 8, // разрядность слова
parameter DEPTH = 1024 // число слов
)(
input wire clk,
input wire we, // разрешение записи
input wire [$clog2(DEPTH)-1:0] addr, // адрес
input wire [WIDTH-1:0] din, // данные на запись
output reg [WIDTH-1:0] dout // данные на чтение
);
reg [WIDTH-1:0] mem [0:DEPTH-1]; // массив = память
always @(posedge clk) begin
if (we)
mem[addr] <= din; // синхронная запись
dout <= mem[addr]; // СИНХРОННОЕ чтение (через регистр) -> BRAM
end
endmoduleОбъявление reg [WIDTH-1:0] mem [0:DEPTH-1] — это двумерный массив: память из DEPTH слов по WIDTH бит. Запись и чтение идут по такту. Именно синхронное чтение (dout — регистр) подсказывает синтезатору использовать BRAM, а не россыпь триггеров.
Как работает под капотом
Промоделируем работу памяти: запишем несколько значений по адресам, потом прочитаем. Заметьте задержку чтения на такт — это плата за синхронность:
# Эмуляция синхронной памяти: чтение появляется на СЛЕДУЮЩЕМ такте
mem = [0] * 8
operations = [("W", 2, 42), ("W", 5, 99), ("R", 2, None), ("R", 5, None)]
dout = 0
print("такт | оп | addr | данные | dout (с задержкой)")
print("-----+----+------+--------+------------------")
for t, (op, addr, val) in enumerate(operations):
prev_dout = dout # то, что было прочитано на прошлом такте
if op == "W":
mem[addr] = val
info = f"запись {val}"
else:
info = "чтение"
dout = mem[addr] # читаем в регистр (появится в выводе позже)
print(f" {t} | {op} | {addr} | {info:9} | {prev_dout}")Вывод:
такт | оп | addr | данные | dout (с задержкой) -----+----+------+--------+------------------ 0 | W | 2 | запись 42 | 0 1 | W | 5 | запись 99 | 42 2 | R | 2 | чтение | 99 3 | R | 5 | чтение | 42
Видно, что прочитанное значение появляется на выходе с задержкой в такт (синхронное чтение). По адресу 2 мы записали 42 и на такте 3 прочитали именно его. Эта задержка — норма для BRAM, её закладывают в конвейер.
Частые ошибки
- Описывать асинхронное чтение. Если читать память комбинационно (
assign dout = mem[addr];), синтезатор не выведет BRAM, а построит дорогую распределённую память на LUT. - Хранить большой массив в регистрах. Тысячи слов на триггерах исчерпают ресурсы; используйте BRAM.
- Забыть про задержку чтения. Данные приходят на следующий такт; логику-потребитель надо синхронизировать с этой задержкой.
Итог
- Block RAM — встроенные блоки памяти на килобиты для буферов и таблиц.
- Память описывают двумерным массивом
reg; синтезатор выводит BRAM при синхронном чтении. - Чтение из BRAM приходит с задержкой в один такт.
- Асинхронное чтение мешает выводу BRAM и тратит LUT.