55 / 63 · Теория
Синхронная память и контракт одновременного чтения и записи
Различайте момент ответа с выхода регистра, read-first, write-first, no-change и отдельные требования к valid и инициализации адресов.
Уроки доступны бесплатно. Запишитесь, чтобы сохранять прогресс.
До адресов определите момент ответа
При подключении памяти задают не только ёмкость, но и задержку чтения, и поведение коллизий. Построим синхронную память на 16 адресов по 8 бит, принимающую одно чтение и одну запись на одном положительном фронте. Такт общий, адреса независимы. Ёмкость 16×8=128 бит, адресная ширина бита.
Если адрес чтения и 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. Обозначая массив до фронта , после — , для любого адреса на фронте без reset:
Даже при коллизии чтение использует , а не . Без запроса rd_data сохраняется.
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 не считается ответом. Данные показаны шестнадцатерично.
Показать данные диаграммы
| Сигнал | Диаграмма | Значения шины |
|---|---|---|
| фронт | 2345 | E0 → E1 → E2 → E3 |
| wr_en | 1.0. | |
| wr_data | 23.. | 25 → 9A |
| rd_en | 01.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 тоже не гарантирует инициализацию.