Память в 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.
Проверьте себя
1. Когда вместо регистров стоит использовать block RAM?
AДля хранения одного-двух значений
BДля больших массивов данных (буферов, таблиц) на тысячи слов
CНикогда, регистры всегда лучше
DТолько для тактового сигнала
2. Что подсказывает синтезатору использовать block RAM, а не память на LUT?
AАсинхронное чтение
BСинхронное чтение через регистр по такту
CИспользование типа wire
DБольшая тактовая частота
3. Какая особенность у синхронного чтения block RAM?
AЧтение мгновенно
BПрочитанные данные появляются на выходе с задержкой в один такт
CЧтение невозможно
DДанные теряются при чтении