В статье про интерфейс VGA я писал, что использовал внешнюю память SDRAM в качестве буфера кадра. Хочу поделиться его реализацией, хотя бы потому, что когда я разрабатывал этот модуль, я потратил на него много времени, потому что стандартные IP-ядра не поддерживают этот чип. И, следовательно, я хочу помочь кому-то в этом вопросе.
Отладочная плата использовалась с той же FPGA семейства Spartan6 xc6slx16. На борту также имеется 32 МБ SDRAM (MT48LC16M16A2).
Вот изображение отладочной платы:
Входные данные.
И так, из техническая спецификация на микросхеме MT48LC16M16A2 имеет структуру 4 банка по 4 млн ячеек разрядностью 16 бит (4М ячеек х 16 х 4 банка). В принципе тут все понятно.
Чтобы понять адресацию этой памяти, смотрим в даташит и видим такую таблицу, нас интересует последний столбец:

Это показывает, что у нас есть 13 бит для адресации строки, 9 бит для адресации столбца и 2 бита для адресации банка; всего 24 бита для адресации всего объема памяти. Используя простую математику, вы можете получить тот же объем памяти, что и в таблице данных: 16 * 2 ^ 24 = 268435456 бит (33554432 байта).
Если вы посмотрите на цоколевку этой микросхемы, то увидите, что там всего 13 битов вывода для адресации + 2 бита адресации банка. На рисунке видно, что сигналы A0-A12 – это адресная шина, а BA0-BA1 – шина выбора банка, и все… Где остальные 9 бит адреса?

Не лучше ли изучить вопрос механизма работы SDRAM (или лучше заучивать материал парами, как в моем случае[я, разумеется, все забыл]), получается, что адресная шина используется дважды. Первый раз используется для выбора строки, в нашем случае используются все 13 бит адреса; и второй раз используется для выбора столбца (всего 9 битов адреса), в основном ячейки внутри этой строки. Опять же: сначала выбираем строку (N тактов), затем выбираем столбец (N + x тактов) и их пересечение даст нам ячейку памяти, конечно, не забываем выбранный банк.

Также обращает на себя внимание шина данных DQ0-DQ15. Как видите, он один и используется — и для записи, и для чтения. Мне, как человеку, часто использующему примитивы BRAM в ПЛИС, такая архитектура показалась крайне неудобной. Но если посмотреть на это с другой стороны, то сразу можно понять, что микросхема – это физическое устройство, распаянное на плате, и если бы все выводы были разделены, то это был бы просто геморрой, и никому он не нужен . Также есть адресная шина, и я не знаю случаев, когда нужно одновременно запрашивать чтение и запись по одному и тому же адресу.
Алгоритм работы.
SDRAM работает на системе управления, которая определяет режим и этапы работы. Вот список команд для микросхемы MT48LC16M16A2:

Сама микросхема имеет множество различных режимов работы, например:
-
Пакеты разной длины могут быть прочитаны и записаны одновременно.
-
Автоматическая перезарядка ячеек памяти.
-
Количество участвующих банков памяти во время загрузки обновления.
-
Также есть различные варианты переключения между состояниями.
Чтобы облегчить себе жизнь, я переключился на режим доступа по одной ячейке. Я также воспользовался тем свойством, что после команды чтения/записи вы можете выполнить следующую команду чтения/записи, если новый адрес находится в разрешенной строке. В моем случае я смог без остановки написать 512 слов, затем запускается процесс обновления ячейки памяти и ПЛК контроллера переходит в ожидание команды. В итоге у меня получился следующий модуль:

Интерфейс m_* является входом для загрузки команд и данных, если команда предназначена для записи. Интерфейс s_* отображает результат чтения памяти. Данные считываются с задержкой в 3 такта.
Логика модуля проста, команды чтения или записи фиксируются до тех пор, пока адрес меняется в младших 9 битах. Кроме того, команды больше не перехватываются, если тип команды изменился (читай<=>письмо). Модуль чувствителен к сигналу m_valid, если он падает, то контроллер памяти переходит к закрытию активированной линии и обновлению нагрузки в ячейках.
Несмотря на то, что в даташите указано, что максимальная частота этого чипа составляет 133 МГц, во время моей отладки модуль работал на частоте 150 МГц. Но я не стал искушать судьбу и оставил частоту 100 МГц (мне так удобно для дальнейшего использования).
Вот код модуля:
`timescale 1ns / 1ps
module ctrl_sdram_v2
#(
parameter [2:0] CL = 'd3
)
(
input clk,
input rst,
//user interface)
input [15:0] m_data, // valid if m_we==1
input [23:0] m_addr, //2bit BANK, 13bit ROW, 9bit COLUMM
input m_we , // 0 - read, 1 - write)
input m_valid,
output m_ready,
output reg[15:0] s_data,
output reg s_valid,
input s_ready,// invalid ready. only s_valid
//SDRAM interface
output reg sd_cke,
output sd_clk,
output sd_dqml,
output sd_dqmh,
output reg sd_cas_n,
output reg sd_ras_n,
output reg sd_we_n,
output reg sd_cs_n,
output reg [14:0] sd_addr,
inout [15:0] sd_data
);
reg [3:0] state_main = 'd0;
reg [15:0] state_tri ;
reg [15:0] sd_data_o ;
wire [15:0] sd_data_i ;
reg [23:0] m_addr_set="d0;
reg flg_first_cmd = "d1;
wire new_row_addr;
reg [15:0] cnt_wait="d0;
reg [15:0] cnt_wait_buf = "d0;
reg [10:0] cnt_refresh_sdram = 'd0;
always@(posedge clk)
begin
if(rst) begin
state_main <= 'd0;
m_addr_set <= 'd0;
flg_first_cmd <= 'd1;
end else begin
case(state_main)
0: begin //wait 100 us
if(cnt_wait >= 8000) state_main<= 'd1;
else cnt_wait <= cnt_wait + 1;
end
1: begin //set NOP
if(cnt_wait >= 10000) begin
state_main<= 'd2;
cnt_wait <= 'd0;
end else cnt_wait <= cnt_wait + 1;
end
2: begin //cmd PRECHARGE ALL
if(cnt_wait >= 'd1) begin
cnt_wait <= 'd0;
state_main <= 'd3;
end else cnt_wait <= cnt_wait + 1'b1;
end
3: begin // AUTO REFRESH 0
if(cnt_wait[14:0] >= 'd6) begin
cnt_wait[14:0] <= 'd0;
if(cnt_wait[15]) begin
state_main <= 'd4;
cnt_wait[15] <= 'd0;
end else cnt_wait[15] <= 'd1;
end else cnt_wait <= cnt_wait + 1'b1;
end
4: begin //cmd LOAD MODE
if(cnt_wait >= 'd1) begin
cnt_wait <= 'd0;
state_main <= 'd5;
end else cnt_wait <= cnt_wait + 1'b1;
end
5: begin //IDLE state
if(m_valid) begin
state_main <= 'd6;
cnt_refresh_sdram <= 'd0;
end else begin
if(&cnt_refresh_sdram) begin
state_main <= 'd8;
cnt_refresh_sdram <= 'd0;
end else cnt_refresh_sdram <= cnt_refresh_sdram + 1;
end
end
6: begin // cmd ACTIVATE row
if(cnt_wait >= CL) begin
cnt_wait <= 'd0;
if(m_we) begin //cmd WRITE
state_main <= 'd7;
end else begin //cmd READ
state_main <= 'd9;
end
m_addr_set <= m_addr;
flg_first_cmd <= 'd1;
end else cnt_wait <= cnt_wait + 1'b1;
end
7: begin //WRITE
m_addr_set <= m_addr;
if(flg_first_cmd) begin
flg_first_cmd <= 'd0;
end else begin
if(new_row_addr=='d1 || m_valid == 'd0 || m_ready == 'd0) begin//goto precharge
state_main <= 'd8;
end
end
end
8: begin //cmd PRECHARGE after write
if(cnt_wait >= 'd3) begin
cnt_wait <= 'd0;
state_main <= 'd5;
end else cnt_wait <= cnt_wait + 1'b1;
end
9: begin //READ and reading data from SDRAM
m_addr_set <= m_addr;
if(flg_first_cmd) begin
flg_first_cmd <= 'd0;
end else begin
if(new_row_addr=='d1 || m_valid == 'd0 || m_ready == 'd0) begin//
state_main <= 'd10;
cnt_wait_buf <= cnt_wait;
end
end
cnt_wait <= cnt_wait + 1'b1;
end
10: begin //reading data from SDRAM
if(cnt_wait == cnt_wait_buf+CL) begin
state_main <= 'd11;
cnt_wait <= 'd0;
end else cnt_wait <= cnt_wait + 1;
end
11: begin // cmd AUTO REFRESH after read
if(cnt_wait >= 'd3) begin
cnt_wait <= 'd0;
state_main <= 'd5;
end else cnt_wait <= cnt_wait + 1'b1;
end
endcase
end
end
assign new_row_addr = (m_addr_set[23:9] != m_addr[23:9]) ? 'd1 : 'd0;
assign m_ready = (state_main == 'd7 && m_we == 'd1 && new_row_addr == 'd0) ? 'd1 :
(state_main == 'd9 && m_we == 'd0 && new_row_addr == 'd0) ? 'd1 :
'd0;
always@(posedge clk)
begin
s_data <= sd_data_i;
s_valid <= ((state_main == 'd9 || state_main == 'd10) && cnt_wait > CL) ? 'd1 : 'd0;
end
assign sd_dqml =0;
assign sd_dqmh =0;
always@(posedge clk)
begin
state_tri <= (state_main == 'd7) ? 16'd0 : 16'hFFFF;
sd_data_o <= (state_main == 'd7) ? m_data : 'd0;
sd_cke <= (state_main == 'd0) ? 'd0 : 'd1;
sd_cas_n<= (state_main == 'd1) ? 'd1 : // INIT NOP
(state_main == 'd2 && cnt_wait==0) ? 'd1 : //PRECHARGE
(state_main == 'd2 && cnt_wait>0) ? 'd1 :
(state_main == 'd3 && cnt_wait[14:0]==0) ? 'd0 : //autorefresh
(state_main == 'd3 && cnt_wait[14:0]!=0) ? 'd1 ://nop
(state_main == 'd4 && cnt_wait==0) ? 'd0 : //load mode
(state_main == 'd4 && cnt_wait!=0) ? 'd1 : //nop
(state_main == 'd5) ? 'd1 : //nop
(state_main == 'd6 && cnt_wait==0) ? 'd1 : //activate
(state_main == 'd6 && cnt_wait!=0) ? 'd1 : //nop
(state_main == 'd7 && m_valid=='d1 && m_ready=='d1 ) ? 'd0 : //WRITE
(state_main == 'd7 && (m_valid=='d0 || m_ready=='d0)) ? 'd1 : //nop
(state_main == 'd8 && cnt_wait==0) ? 'd1 : //precharge after write
(state_main == 'd8 && cnt_wait!=0) ? 'd1 : //nop
(state_main == 'd9 && m_valid=='d1 && m_ready=='d1 ) ? 'd0 : //READ
((state_main == 'd9 || state_main == 'd10) && (m_valid=='d0 || m_ready=='d0)) ? 'd1 : // nop
(state_main == 'd11 && cnt_wait==0) ? 'd1: //'d0 : //auto REFRESH(1) //precharge after read
(state_main == 'd11 && cnt_wait!=0) ? 'd1 : // nop
'd1;
sd_ras_n<= (state_main == 'd1) ? 'd1 :
(state_main == 'd2 && cnt_wait==0) ? 'd0 :
(state_main == 'd2 && cnt_wait>0) ? 'd1 :
(state_main == 'd3 && cnt_wait[14:0]==0) ? 'd0 :
(state_main == 'd3 && cnt_wait[14:0]!=0) ? 'd1 :
(state_main == 'd4 && cnt_wait==0) ? 'd0 :
(state_main == 'd4 && cnt_wait!=0) ? 'd1 :
(state_main == 'd5) ? 'd1 :
(state_main == 'd6 && cnt_wait==0) ? 'd0 :
(state_main == 'd6 && cnt_wait!=0) ? 'd1 :
(state_main == 'd7 && m_valid=='d1 && m_ready=='d1 ) ? 'd1 :
(state_main == 'd7 && (m_valid=='d0 || m_ready=='d0)) ? 'd1 :
(state_main == 'd8 && cnt_wait==0) ? 'd0 :
(state_main == 'd8 && cnt_wait!=0) ? 'd1 :
(state_main == 'd9 && m_valid=='d1 && m_ready=='d1 ) ? 'd1 :
((state_main == 'd9 || state_main == 'd10) && (m_valid=='d0 || m_ready=='d0)) ? 'd1 :
(state_main == 'd11 && cnt_wait==0) ? 'd0: //'d0 :
(state_main == 'd11 && cnt_wait!=0) ? 'd1 :
'd1;
sd_we_n <= (state_main == 'd1) ? 'd1 :
(state_main == 'd2 && cnt_wait==0) ? 'd0 :
(state_main == 'd2 && cnt_wait>0) ? 'd1 :
(state_main == 'd3 && cnt_wait[14:0]==0) ? 'd1 :
(state_main == 'd3 && cnt_wait[14:0]!=0) ? 'd1 :
(state_main == 'd4 && cnt_wait==0) ? 'd0 :
(state_main == 'd4 && cnt_wait!=0) ? 'd1 :
(state_main == 'd5) ? 'd1 :
(state_main == 'd6 && cnt_wait==0) ? 'd1 :
(state_main == 'd6 && cnt_wait!=0) ? 'd1 :
(state_main == 'd7 && m_valid=='d1 && m_ready=='d1 ) ? 'd0 :
(state_main == 'd7 && (m_valid=='d0 || m_ready=='d0)) ? 'd1 :
(state_main == 'd8 && cnt_wait==0) ? 'd0 :
(state_main == 'd8 && cnt_wait!=0) ? 'd1 :
(state_main == 'd9 && m_valid=='d1 && m_ready=='d1 ) ? 'd1 :
((state_main == 'd9 || state_main == 'd10) && (m_valid=='d0 || m_ready=='d0)) ? 'd1 :
(state_main == 'd11 && cnt_wait==0) ? 'd0 ://'d1 :
(state_main == 'd11 && cnt_wait!=0) ? 'd1 :
'd1;
sd_cs_n <= (rst == 'd1) ? 'd1 :
(state_main == 'd1) ? 'd0 :
(state_main == 'd2 && cnt_wait==0) ? 'd0 :
(state_main == 'd2 && cnt_wait>0) ? 'd0 :
(state_main == 'd3 && cnt_wait[14:0]==0) ? 'd0 :
(state_main == 'd3 && cnt_wait[14:0]!=0) ? 'd0 :
(state_main == 'd4 && cnt_wait==0) ? 'd0 :
(state_main == 'd4 && cnt_wait!=0) ? 'd0 :
(state_main == 'd5) ? 'd0 :
(state_main == 'd6 && cnt_wait==0) ? 'd0 :
(state_main == 'd6 && cnt_wait!=0) ? 'd0 :
(state_main == 'd7 && m_valid=='d1 && m_ready=='d1 ) ? 'd0 :
(state_main == 'd7 && (m_valid=='d0 || m_ready=='d0)) ? 'd0 :
(state_main == 'd8 && cnt_wait==0) ? 'd0 :
(state_main == 'd8 && cnt_wait!=0) ? 'd0 :
(state_main == 'd9 && m_valid=='d1 && m_ready=='d1 ) ? 'd0 :
((state_main == 'd9 || state_main == 'd10) && (m_valid=='d0 || m_ready=='d0)) ? 'd0 :
(state_main == 'd11 && cnt_wait==0) ? 'd0: //'d0 :
(state_main == 'd11 && cnt_wait!=0) ? 'd0 :
'd0;
sd_addr[14:13] <= m_addr[23:22];
sd_addr[12:0] <= (state_main == 'd2 && cnt_wait==0) ? {4'b0,1'b1,10'b0} : //[10] = 1
(state_main == 'd4 && cnt_wait==0) ? {2'b00,3'b000,1'b1,2'b00,CL[2:0],1'b0,3'b000} : //BA[1:0]==0,A[12:10]==0,WRITE_BURST_MODE = 0,OP_MODE = 'd0, CL = 2, TYPE_BURST = 0, BURST_LENGTH = 1
(state_main == 'd6 && cnt_wait==0) ? m_addr[21:9] :
(state_main == 'd7) ? {5'd0,m_addr[8:0]} :
(state_main == 'd8 && cnt_wait==0) ? {4'b0,1'b1,10'b0} : //[10] = 1
(state_main == 'd9) ? {7'd0,m_addr[8:0]} :
(state_main == 'd11 && cnt_wait==0) ? {4'b0,1'b1,10'b0} : //[10] = 1
'd0;
end
ODDR2 #(
.DDR_ALIGNMENT("NONE"), // Sets output alignment to "NONE", "C0" or "C1"
.INIT(1'b0), // Sets initial state of the Q output to 1'b0 or 1'b1
.SRTYPE("SYNC") // Specifies "SYNC" or "ASYNC" set/reset
) ODDR2_inst (
.Q (sd_clk), // 1-bit DDR output data
.C0 (clk), // 1-bit clock input
.C1 (!clk), // 1-bit clock input
.CE (!rst), // 1-bit clock enable input
.D0 (1), // 1-bit data input (associated with C0)
.D1 (0), // 1-bit data input (associated with C1)
.R (0), // 1-bit reset input
.S (0) // 1-bit set input
);
genvar i;
generate
for (i=0; i < 16; i=i+1)
begin: tri_state
OBUFT #(
.DRIVE(12), // Specify the output drive strength
.IOSTANDARD("DEFAULT"), // Specify the output I/O standard
.SLEW("SLOW") // Specify the output slew rate
) OBUFT_inst (
.O(sd_data[i]), // Buffer output (connect directly to top-level port)
.I(sd_data_o[i]), // Buffer input
.T(state_tri[i]) // 3-state enable input
);
IBUF #(
.IOSTANDARD("DEFAULT") // Specify the input I/O standard
)IBUF_inst (
.O(sd_data_i[i]), // Buffer output
.I(sd_data[i]) // Buffer input (connect directly to top-level port)
);
end
endgenerate
endmodule
Заключение. Статья получилась не исчерпывающей и пояснительной, а на Этот сайт есть русскоязычная версия техпаспорта и с комментариями автора. Я опирался на него, когда осваивал материал.
PS как я закрепил модуль VGA и контроллер SDRAM работают.

В проекте было два тактовых домена: 100 МГц и 25 МГц. Из-за того, что контроллер памяти работал на частоте 100 МГц, он теоретически мог записать в себя 3 новых кадра до того, как изображение отрисовывалось на монитор.
Автомат работает в двух состояниях, либо загружает новое изображение, либо вычитает существующее изображение для последующего рендеринга. Режим по умолчанию — это режим записи нового кадра в память, при поступлении сигнала из FIFO о том, что он почти пуст, автомат переключается на чтение памяти и вычитает необходимое количество. В данном случае сигнал почти_пусто повышается, когда в FIFO осталось 100 значений, это сделано для того, чтобы машина успела перейти в режим чтения и модуль Ctrl_SDRAM успел выполнить предыдущую команду. Машина считывает следующие 900 значений пикселей из памяти и возвращается в режим записи.
FIFO — это два такта, с глубиной около 1000 значений. На частоте 100МГц пишет в него, а модуль VGA вычитает из своей частоты 25МГц. Если оценить время, через которое нужно будет снова переключить машину на чтение, то оно таково: в память считывается 100 значений + 900 новых значений – четверть значений\ u200b, которые успевают вычесть за этот период, в итоге имеем 750 значений в FIFO. В итоге модуль VGA прочитает следующие 650 циклов значений, до того, как поднимется почти пустой флаг, переводим его в 100 МГц, получаем 2600 циклов записи в память, этого более чем достаточно. Естественно, здесь вы должны понимать, что модуль VGA не читает FIFO в областях, где изображение не отрисовывается: около 160 тактов в конце каждой строки пикселей и 7200 тактов в конце кадра на частоте 25 МГц. , всего 336 000 циклов бездействия на частоте 100 МГц.