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

Вводные данные
И так, из даташита на микросхему MT48LC16M16A2, она имеет структуру из 4 банок по 4 млн ячеек с разрядностью 16 бит (4M ячеек x 16 x 4 банка). В принципе, тут все понятно.
Для понимания адресации этой памяти смотрим в даташит и видим такую таблицу, нас интересует последний столбик:

Из него видно, что у нас есть 13 бит — для адресации по строкам, 9 бит — для адресации по столбцам и 2 бита — для адресации по банкам; всего 24 бита для адресации по всему объёму памяти. Используя нехитрую математику, можно получить те самые объёмы памяти, которые обещают в даташите: 16*2^24 = 268435456 бит (33554432 байт).
Если заглянуть в распиновку этой микросхемы, то увидим, что пинов под адресацию всего 13 бит + 2 бита адресации банков. На рисунке видно, что сигналы А0-А12 это шина адреса и ВА0-ВА1 это шина выбора банка, ну и как бы все… Где еще 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
Заключение
Статья не получилась всеобъемлющей и всеобъясняющей, но на этом сайте есть русский вариант даташита и с комментариями от автора. Я опирался на нее, когда осваивал материал.
P.S. как я состыковал работу VGA модуля и SDRAM контроллера.

В проекте имелось два клоковых домена, на 100 МГц и на 25 МГц. За счет того что контроллер памяти работал на частоте 100 МГц, он теоретически мог записать в себя 3 новых кадра прежде чем 1 кадр отрисуется на мониторе.
Автомат работает в двух состояниях, либо он загружает новый кадр либо он вычитывает имеющийся кадр для дальнейшей отрисовки. Режим по умолчанию это режим записи нового кадра в память, когда приходит сигнал от ФИФО о том что оно почти пустое, автомат переключается на чтение из памяти и вычитывает необходимый объем. В данном случае сигнал almost_empty поднимается когда в ФИФО осталось 100 значений, это сделано для того чтобы автомат успел переключиться на режим чтения и модуль Ctrl_SDRAM успел закончить предыдущую команду. Автомат вычитывает из памяти следующие 900 значений пикселей и снова переключается на режим записи.
ФИФО является двухклоковым, глубиной порядка 1000 значений. На частоте 100 МГц происходит запись в него, а модуль VGA вычитывает на своей частоте в 25 МГц. Если прикинуть время через которое вновь потребуется переключение автомата на чтение то она следующая: 100 значений + 900 новых значений вчитывается из памяти - четверть значений которые успеют вычитаться за этот период, в конечном счете имеем 750 значений в ФИФО. В итоге модуль VGА будет вычитывать следующие значения 650 тактов, до того как поднимется флаг almost_empty, переведя это на 100МГц получим 2600 тактов записи в память, этого более чем достаточно. Естественно тут нужно понимать что VGA модуль не читает из ФИФО в тех областях где не отрисовывается картинка: порядка 160 тактов в конце каждой строки пикселей и 7200 тактов в конце фрейма на частоте 25 МГц, а это в общей сложности 336000 тактов простоя на частоте 100МГц.
