Altifigence Academy

55 / 63 · Теория

Синхронная память и контракт одновременного чтения и записи

Различайте момент ответа с выхода регистра, read-first, write-first, no-change и отдельные требования к valid и инициализации адресов.

До адресов определите момент ответа

При подключении памяти задают не только ёмкость, но и задержку чтения, и поведение коллизий. Построим синхронную память на 16 адресов по 8 бит, принимающую одно чтение и одну запись на одном положительном фронте. Такт общий, адреса независимы. Ёмкость 16×8=128 бит, адресная ширина log⁡216=4\log_2 16=4 бита.

Если адрес чтения и rd_en устойчивы перед фронтом, после него rd_data и rd_valid появляются на выходах регистров. Это задержка одной ступени чтения с регистрацией выхода. Другой always_ff на том же фронте прочитает прежний выход, поэтому потребляющий регистрРегистр хранит многобитное состояние. Синхронные регистры в этом курсе записывают определённый вход на фронте тактового сигнала. Подробнее получает ответ на следующем положительном фронте. Отдельно отмечайте на диаграмме запрос и наблюдение ответа. Новые запросы принимаются каждый такт, так что одна ступень задержки не означает пропускную способностьПропускная способность — число операций, принимаемых или завершаемых за единицу времени. Конвейер может повысить её, не уменьшая задержку отдельной операции. Подробнее 1/2.

Чтение и запись одного адреса

Пусть перед фронтом M[3]=8'h25, одновременно читается адрес 3 и записывается 8'h9A. Read-first отвечает 25 и обновляет память до 9A. Write-first отвечает новым 9A. No-change сохраняет выходной регистр, и его значение не обязательно является прежним содержимым адреса 3. Это три разных нормальных контракта. Они также различаются в документации AMD о режимах синхронизации RAM.

Пример использует read-first. Обозначая массив до фронта M−M^{-}, после — M+M^{+}, для любого адреса aa на фронте без reset:

M+[a]={wr_data,wr_en=1∧a=wr_addrM−[a],otherwiseM^{+}[a]=\begin{cases} \mathrm{wr\_data}, & \mathrm{wr\_en}=1\land a=\mathrm{wr\_addr}\\ M^{-}[a], & \text{otherwise} \end{cases}
rd_data+={M−[rd_addr],rd_en=1rd_data−,rd_en=0rd_valid+=rd_en\begin{aligned} \mathrm{rd\_data}^{+}&=\begin{cases} M^{-}[\mathrm{rd\_addr}], & \mathrm{rd\_en}=1\\ \mathrm{rd\_data}^{-}, & \mathrm{rd\_en}=0 \end{cases}\\ \mathrm{rd\_valid}^{+}&=\mathrm{rd\_en} \end{aligned}

Даже при коллизии чтение использует M−M^{-}, а не M+M^{+}. Без запроса rd_data сохраняется.

SystemVerilog
module sync_mem16x8 (
  input  logic       clk, rst,
  input  logic       wr_en, rd_en,
  input  logic [3:0] wr_addr, rd_addr,
  input  logic [7:0] wr_data,
  output logic [7:0] rd_data,
  output logic       rd_valid
);
  timeunit 1ns;
  timeprecision 1ps;
  logic [7:0] mem [0:15];

  always_ff @(posedge clk) begin
    if (rst) begin
      rd_valid <= 1'b0;
    end else begin
      rd_valid <= rd_en;
      if (wr_en)
        mem[wr_addr] <= wr_data;
      if (rd_en)
        rd_data <= mem[rd_addr];
    end
  end
endmodule

Правые части обоих присваиваний используют прежние значения, затем применяются неблокирующие обновленияНеблокирующее присваивание обозначается <= в последовательностном RTL. Оно вычисляет правую часть, затем планирует обновление, позволяя регистрам на одном фронте рассчитываться по предыдущему состоянию. Подробнее. Поэтому размещение присваивания записи перед присваиванием чтения не заставляет чтение возвращать новое значение. Для write-first можно явно заменить выражение чтения на (wr_en && wr_addr==rd_addr) ? wr_data : mem[rd_addr]. Этот обход сохраняет выходной регистр чтения.

Valid и инициализация — разные обязательства

Rst — синхронный active-high. При rst=1 на положительном фронте оба запроса игнорируются, обнуляется только rd_valid. Массив и rd_data не заполняются нулями. Содержимое ни разу не записанного после включения адреса контракт не определяет: в симуляторе возможно X, в оборудовании тоже нельзя предполагать конкретное число.

Rd_valid=1 означает «цикл ответа на запрос чтения», а не «адрес инициализирован и значение известно». Пользователь должен читать только ранее записанные адреса либо вести отдельную таблицу инициализации. Даже read-first на фронте первой записи возвращает неопределённое прежнее содержимое. Также нужен valid guard, запрещающий использование rd_data при valid=0. Условие инициализированного адреса и valid guard не заменяют друг друга.

Reset не очищает память, поэтому записанные значения сохраняются по RTL. Если система после reset считает прежнее содержимое недействительным, нужно соответственно управлять внешней таблицей инициализации. Удаление чувствительных данных — отдельная задача, не равная управляющему сбросу.

Каждый столбец — запись наблюдения одного положительного фронтаПоложительный фронт — момент перехода такта из 0 в 1. Его следует отличать от уровня, означающего весь интервал CLK=1. Подробнее. Входы показаны непосредственно перед фронтом, состояния со словом «после» в имени — сразу после обновления. Выравнивание столбцов обозначает порядок выборок, а не физическую задержку распространенияЗадержка распространения — время от изменения входа до установления правильного выхода. Логическая эквивалентность выражений и их временные характеристики — разные вопросы. Подробнее.
Все адреса чтения и записи — 3. E0 впервые пишет 25, E1 перезаписывает 9A и одновременно читает. Прочерк data — неопределённое значение; шина при valid=0 не считается ответом. Данные показаны шестнадцатерично.

Прочитайте прежнее значение при коллизии read-first
Показать данные диаграммы
Данные диаграммы: каждый символ — один интервал; точка сохраняет предыдущее состояние; p — один тактовый цикл.
СигналДиаграммаЗначения шины
фронт2345E0 → E1 → E2 → E3
wr_en1.0.
wr_data23..25 → 9A
rd_en01.0
M[3] после23..25 → 9A
rd_data после234.- → 25 → 9A
rd_valid после01.0

Проследите четыре фронта вручную

На E0 запишите 25 по адресу 3 при rd_en=0. На E1 запишите 9A и прочитайте тот же адрес: после E1 valid=1, data=25. На E2 прочитайте адрес 3 без записи: data=9A. На E3 при rd_en=0 valid=0, data остаётся 9A. Если считать E3 новым ответом лишь из-за 9A на шине, данные будут обработаны дважды.

При разных адресах чтение и запись независимы. Например, чтение 7 при записи 3 возвращает прежнее содержимое 7. Отображение RTL на RAM конкретной FPGA и обработку коллизий этим ресурсом проверяют по отчёту синтеза и документации компонента. Определить семантику RTL и проверить реализацию на конкретном устройстве — отдельные этапы.

Попробуйте сами

В M[6] уже записано 8'h10. На E0 читают адрес 6 и пишут 8'h20, на E1 снова читают без записи, на E2 rd_en=0. Найдите data/valid после каждого фронта для read-first. Что изменится при write-first? Определено ли значение первой read-first-коллизии по ранее не записанному адресу?

Прочитать объяснение

E0: data=10, valid=1, массив обновляется до 20. E1: data=20, valid=1. E2: data хранит 20, valid=0, поэтому ответ не учитывается. При write-first меняется только data на E0: будет 20. Read-first одновременно с первой записью возвращает прежнее неинициализированное содержимое, поэтому заданного числового ответа нет. Valid=1 тоже не гарантирует инициализацию.

Выбор действует в этом браузере. Его можно изменить внизу страницы.