Внешняя память
В прошлом разделе мы обсудили память, которая доступна на кристалле. Теперь перейдём к внешней памяти, а именно к DDR3, установленной на плате Tang Mega 138K Pro Dock.
RAM (Random Access Memory) — энергозависимая память с возможностью быстрого чтения и записи данных в произвольном порядке. Существует две основные технологии хранения данных: SRAM (Static RAM), с которой мы познакомились в процессе изучения B-SRAM, и DRAM (Dynamic RAM), ячейкам которой требуется постоянная регенерация (обновление заряда для предотвращения потери данных). Технология DDR является развитием динамического способа хранения данных.
DDR SDRAM (Double Data Rate Synchronous DRAM, чаще просто DDR) пришла на смену памяти SDR SDRAM (Single Data Rate). Отличие состоит в том, что в DDR передача данных осуществляется как по фронту, так и по спаду тактового сигнала, что удваивает пропускную способность. При этом управляющие команды и адреса по-прежнему передаются только по восходящему фронту.
Конфигурация DDR3
Для взаимодействия с DDR необходимо сгенерировать и настроить контроллер DDR3 Memory Interface IP: IP Core Generator → Soft IP Core → Memory Controller → DDRx SDRAM Memory Interface → DDR3 Memory Interface. Вендор предоставляет возможность генерации как полноценного контроллера (MC), так и отдельного физического интерфейса (PHY). При генерации MC также присутствует PHY, но контроллер берёт на себя управление им, предоставляя пользователю нативный или стандартный AXI4 интерфейс.
По умолчанию уже стоят оптимальные настройки, однако наиболее важные параметры следует проверить и настроить вручную:
-
AXI4 Interface: IP-ядро предоставляет 5 режимов диспетчеризации каналов AXI4 (во вкладкеAXI Interface Options). При использовании AXI4, мы рекомендуем использовать режимMode4, при котором каналы чтения и записи обладают равным приоритетом. -
CLK Configuration: IP-ядро оперирует тремя тактовыми доменами: двумя входными — низкочастотнымclk(для внутренней базовой логики IP) и высокочастотнымmemory_clk(для PHY-уровня и тактирования чипа DDR3), а также одним выходным сигналомclk_out(для пользовательской логики), частота которого задаётся черезCLK ratioотносительноmemory_clk. -
Memory Configuration: ПараметрDq Widthопределяет суммарную разрядность шины данных, аDram Width— разрядность отдельной микросхемы памяти. Так как на нашей плате установлены два чипа, образующие общую 32-битную шину (спецификация платы), параметрыDq WidthиDram Widthнеобходимо установить в значения32и16соответственно. -
Memory Address:Row AddressиColumn Addressотвечают за разрядность адресного пространства. ЗначенияRow = 15иColumn = 10определяют 1 ГБ — максимально возможное пространство на Tang Mega 138K Pro.
Остальные параметры определяются физическими характеристиками конкретных чипов памяти и не оказывают влияния на интерфейс контроллера. Авторы курса используют значения из этого примера производителя платы для следующих параметров: CAS Latency, Rtt Nom, а также тайминги во вкладке Memory Timing.
Поскольку динамические ячейки памяти требуют регулярного циклического обновления, IP-ядро поддерживает автоматический и пользовательский режимы регенерации. Мы будем использовать автоматический, интервал работы которого задаётся параметром t_REFI во вкладке Memory Timings.
При старте системы необходимо отслеживать сигнал init_calib_complete, который поднимается после успешного завершения инициализации и калибровки памяти. Для корректного запуска процедуры калибровки используется сигнал pll_lock. Чтобы сформировать данный сигнал (как и разнообразные клоки и pll_stop для экономии ресурсов), нужно воспользоваться модулем синхронизации Gowin_PLL. Для этого нужно сгенерировать IP-ядро: IP Core Generator → Hard Module → CLOCK → PLL_ADV. Далее необходимо выбрать частоту клоков (в нашем случае CLKIN = 100, CLKOUT0 = 400) и на вкладке Common включить PLL Reset, Enable Lock и Clock Enable Ports.
Помимо пользовательского интерфейса (нативный или Full AXI4 Slave) и рассмотренных клоков/сбросов, IP-ядро содержит порты физического уровня (PHY). Данные порты предназначены для прямого подключения к внешней памяти через внешние порты top-модуля и привязаны к соответствующим физическим пинам (в файле .cst).
| Подробнее про регенерацию (раздел 4.4.9), режимы AXI4 (раздел 4.5.1), порты (глава 6) и остальные параметры IP (глава 7) можно прочитать в User Guide. |
Подключение
Подключение портов можно импортировать из готовых файлов конфигурации производителя платы, либо задать самостоятельно с помощью инструмента FloorPlanner. Рассмотрим некоторые описания сигналов, а весь файл можно посмотреть в примере производителя.
IO_LOC "ddr_addr[14]" U5; (1)
IO_PORT "ddr_addr[14]" IO_TYPE=SSTL15 PULL_MODE=NONE DRIVE=12 BANK_VCCIO=1.5;
IO_LOC "ddr_clk" M2,L2; (2)
IO_PORT "ddr_clk" IO_TYPE=SSTL15D PULL_MODE=NONE DRIVE=8 BANK_VCCIO=1.5;
INS_LOC "u_ddr3/gw3_top/u_GW_DDR3_PHY_MC/u_ddr_phy_top/u_dll" DDRDLLM_BL; (3)
INS_LOC "u_ddr3/gw3_top/u_GW_DDR3_PHY_MC/u_ddr_phy_top/fclkdiv" LEFTSIDE[4]; (4)
| 1 | Порт для старшего бита адреса DDR3 ddr_addr. Конфигурация IO_TYPE=SSTL15 задаёт стандарт ввода-вывода Stub Series Terminated Logic (1.5 В), предназначенный для интерфейсов памяти DDR3. Значение DRIVE=12 устанавливает максимальный выходной ток буфера в 12 мА, а BANK_VCCIO=1.5 указывает на напряжение питания соответствующего банка (соответствует значению из стандарта). |
| 2 | Порт ddr_clk представляет собой дифференциальную тактовую пару, указываются сразу два пина M2,L2, соответствующие прямому и инвертированному выходам. Конфигурация IO_TYPE=SSTL15D задаёт дифференциальный SSTL стандарт. |
| 3 | Директива фиксирует положение DLL-примитива (u_dll) в специализированной области ПЛИС DDRDLLM_BL (Bottom-Left). |
| 4 | Директива фиксирует положение специализированного делителя частоты (fclkdiv) на левой стороне ПЛИС (LEFTSIDE[4]). |
Путь к примитивам u_dll и fclkdiv может меняться в зависимости от версии САПР. В случае появлении ошибок на этапе генерации прошивки, проверьте их актуальные пути в сгенерированном файле src/ddr3_memory_interface/ddr3_memory_interface.vo с помощью обычного текстового поиска.
|
Помимо привязки пинов необходимо корректно описать управляющие сигналы.
create_clock -name div_clk -period 10 -waveform {0 5}
[get_pins {uut_div2/CLKOUT}] (1)
create_clock -name ddr_out_clk -period 10 -waveform {0 5}
[get_pins {u_ddr3/gw3_top/u_GW_DDR3_PHY_MC/u_ddr_phy_top/fclkdiv/CLKOUT}] (2)
create_clock -name memory_clk -period 2.5 -waveform {0 1.25}
[get_nets {memory_clk}] (3)
set_clock_groups -asynchronous
-group [get_clocks {div_clk}]
-group [get_clocks {ddr_out_clk}]
-group [get_clocks {memory_clk}] (4)
| 1 | Определение базового тактового сигнала div_clk (100 МГц, период 10 нс), на котором работает основная логика проекта. В данном случае, он формируется делителем частоты uut_div2. |
| 2 | Определение тактового сигнала ddr_out_clk (100 МГц, период 10 нс) на выходе делителя fclkdiv внутри IP-ядра. Клок, на котором будет работать логика, взаимодействующая с DDR. |
| 3 | Определение высокочастотного тактового сигнала памяти memory_clk (400 МГц, период 2.5 нс). |
| 4 | Данная директива объявляет указанные тактовые домены взаимно асинхронными, запрещая тайминг-анализатору проверять временные соотношения (setup/hold) между их фронтами. Используется в случаях, когда анализатор не видит фазовой связи. В данном случае ему мешает делитель частоты uut_div2 и встроенный делитель DDR3 IP-ядра. |
Пример работы с памятью
Чтобы избавиться от необходимости управлять пятью каналами AXI4, воспользуемся AXI DMA-контроллером. C одной стороны он подключён к DDR3-контроллеру, являясь Full AXI4 master, а с другой стороны — два AXI4-Stream интерфейса: DMA выступает master-ом на канале чтения (выдаёт прочитанные из памяти данные) и slave-ом на канале записи (принимает данные от логики, чтобы записать их в память). Таким образом, вместо адресов и управляющих сигналов Full AXI4 пользовательская логика работает с двумя потоками данных и дескрипторами, которые задают, откуда/куда и сколько нужно передать.
| Работу с native-интерфейсом можно посмотреть в примере производителя. Там есть BIST-модуль (Built-In Self-Test), который применяется для проверки модуля памяти в процессе разработки. Похожим образом выглядит и обычная пользовательская логика, взаимодействующая с контроллером. Там же можно посмотреть получение результатов теста через UART и индикацию на плате. |
С Scatter-Gather DMA мы познакомились в разделе про PCIe. Сейчас же мы будем использовать AXI DMA-контроллер, который работает с одним непрерывным буфером — готовую реализацию такого контроллера можно взять, например, здесь. Дескриптор в этом случае — просто адрес начала и длина передачи: контроллер сам инкрементирует адрес и разбивает передачу на burst-ы. Единица передачи данных на AXI4-Stream — beat: одно слово данных (шириной в разрядность шины), а burst — группа из нескольких подряд идущих beat-ов, адресованных одной непрерывной командой к DDR3. Размер burst-а (число beat-ов в нём) задаётся настройками контроллера.
Например, при длине передачи 4096 байт и ширине шины 256 бит (32 байта) получится 4096 / 32 = 128 beat-ов. Если размер burst-а — 8 beat-ов, эти 128 beat-ов объединятся в 128 / 8 = 16 burst-ов: то есть контроллер выдаст DDR3 всего 16 команд (со сменой адреса).
|
| Обратите внимание, что такой DMA-контроллер, да и в целом, AXI4-Stream интерфейс удобны при непрерывном обращении к памяти. Если нужен произвольный доступ к отдельным ячейкам, то лучше использовать Full AXI4 или нативный интерфейс DDR3-контроллера. Подключение AXI DMA к DDR3-контроллеру и пользовательской логике можно посмотреть здесь. |
В качестве примера взаимодействия с DDR3-контроллером через AXI DMA рассмотрим чтение предварительно загруженного в память изображения: модуль переводит его в оттенки серого и побайтово выводит на внешний порт. Обратите внимание, что данным модулем также управляет некоторая логика, которая перед его запуском ожидает завершения калибровки памяти (init_calib_complete).
| Для упрощения примера размер burst-а равен 1 — это позволяет обойтись без FIFO между AXI4-Stream и выходным портом. При необходимости повысить пропускную способность ширину шины данных можно увеличить. |
module rgb_to_grayscale_stream #(
parameter int AXI_ADDR_WIDTH = 29,
parameter int AXI_LEN_WIDTH = 20,
parameter int AXI_DATA_WIDTH = 256
) (
input clk,
input rst_n,
input [AXI_ADDR_WIDTH-1:0] cfg_read_addr,
input [ AXI_LEN_WIDTH-1:0] cfg_len,
output logic m_axis_read_desc_valid,
input m_axis_read_desc_ready,
output [AXI_ADDR_WIDTH-1:0] m_axis_read_desc_addr,
output [ AXI_LEN_WIDTH-1:0] m_axis_read_desc_len,
output s_axis_rx_tready,
input s_axis_rx_tvalid,
input [AXI_DATA_WIDTH-1:0] s_axis_rx_tdata,
input s_axis_rx_tlast,
output logic tx_valid,
output logic [7:0] tx_data,
output logic tx_last,
input tx_ready,
input run,
output logic done
);
assign m_axis_read_desc_addr = cfg_read_addr; (1)
assign m_axis_read_desc_len = cfg_len;
localparam int COUNT_PIXELS = AXI_DATA_WIDTH / 32; (2)
localparam int WIDTH_PTR = $clog2(COUNT_PIXELS);
localparam [WIDTH_PTR-1:0] INDEX_PIXEL_LAST = COUNT_PIXELS - 1;
logic [COUNT_PIXELS*8-1:0] pixels;
genvar i;
generate
for (i = 0; i < COUNT_PIXELS; i = i + 1) begin : gen_pixels
wire [ 7:0] r = s_axis_rx_tdata[i*32+0+:8];
wire [ 7:0] g = s_axis_rx_tdata[i*32+8+:8];
wire [ 7:0] b = s_axis_rx_tdata[i*32+16+:8];
wire [15:0] grayscale = (r * 8'd77) + (g * 8'd150) + (b * 8'd29); (3)
assign pixels[i*8+:8] = grayscale[15:8];
end
endgenerate
logic [1:0] state; (4)
localparam IDLE = 2'd0;
localparam ISSUE_CMD = 2'd1;
localparam WAIT_DATA = 2'd2;
localparam DONE_STATE = 2'd3;
logic busy;
logic [WIDTH_PTR-1:0] pixel_ptr;
logic tx_last_reg;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
m_axis_read_desc_valid <= 1'b0;
done <= 1'b0;
busy <= 1'b0;
tx_valid <= 1'b0;
tx_last <= 1'b0;
end else begin
if (m_axis_read_desc_valid && m_axis_read_desc_ready) begin
m_axis_read_desc_valid <= 1'b0;
end
case (state)
IDLE: begin
done <= 1'b0;
if (run) begin
m_axis_read_desc_valid <= 1'b1;
state <= ISSUE_CMD;
end
end
ISSUE_CMD: begin
if (!m_axis_read_desc_valid) begin
state <= WAIT_DATA;
end
end
WAIT_DATA: begin
if (s_axis_rx_tvalid && s_axis_rx_tready) begin (5)
busy <= 1'b1;
pixel_ptr <= '0;
tx_valid <= 1'b0;
tx_last <= 1'b0;
tx_last_reg <= s_axis_rx_tlast;
end else if (tx_ready && busy) begin (6)
tx_data <= pixels[int'(pixel_ptr)*8+:8];
tx_valid <= 1'b1;
pixel_ptr <= pixel_ptr + 1;
if (pixel_ptr == INDEX_PIXEL_LAST) begin (7)
busy <= 1'b0;
if (tx_last_reg) begin
tx_last <= 1'b1;
done <= 1'b1;
state <= DONE_STATE;
end
end
end
end
DONE_STATE: begin
tx_valid <= 1'b0;
if (!run) begin
done <= 1'b0;
state <= IDLE;
end
end
default: state <= IDLE;
endcase
end
end
assign s_axis_rx_tready = (state == WAIT_DATA) && !busy && (!tx_valid || tx_ready); (8)
endmodule
| 1 | Адрес и длина дескриптора чтения устанавливается один раз внешней управляющей логикой. По команде run поднимается флаг валидности дескриптора и держится, пока DMA-контроллер не подтвердит приём. На этом работа с адресами и дескрипторами закончена. |
| 2 | Один пиксель занимает 32 бита — по 8 бит на каждый из компонентов, в порядке R, G, B и неиспользуемый старший байт (alpha или padding). При ширине шины данных AXI_DATA_WIDTH = 256 бит в один beat умещается COUNT_PIXELS = 256 / 32 = 8 пикселей. WIDTH_PTR — разрядность указателя, которого достаточно, чтобы перебрать все 8 пикселей внутри beat-а ($clog2(8) = 3 бита). |
| 3 | Комбинаторный перевод в оттенки серого по формуле Y = 0.299*R + 0.587*G + 0.114*B, приближённой как (77*R + 150*G + 29*B) / 256 (сумма весов 77 + 150 + 29 = 256, поэтому деление — отбрасывание младших 8 бит результата grayscale[15:8]). |
| 4 | Логика оформлена в виде FSM с четырьмя состояниями: IDLE — ожидание запуска, ISSUE_CMD — выдача дескриптора чтения, WAIT_DATA — приём beat-ов и выдача пикселей наружу, DONE_STATE — передача завершена, ожидание снятия run. |
| 5 | Получение beat данных. Флаг busy показывает, что модуль выдает наружу пиксели уже принятого beat-а. Выдача начнется на следующий такт. |
| 6 | Условие передачи пикселя наружу с проверкой готовности получателя. |
| 7 | Обработка последнего пикселя beat-а: busy сбрасывается в этом же такте, то есть уже со следующего такта модуль готов принять новый beat (s_axis_rx_tready снова может подняться, а данные могут прийти через такт). При этом последний пиксель будет отдан на следующем такте. |
| 8 | Готовность получить данные при условии, что модуль не занят раздачей текущего beat-а, автомат в состоянии ожидания данных и предыдущий выходной байт уже принят либо не был выставлен. |
| Обязательно порешайте упражнения. |