В статье про 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МГц.
Комментарии (10)
antonsosnitzkij
24.05.2023 21:08На плате вижу Spartan-6, а на скриншоте кажется Vivado? Вроде ведь нет поддержки семейств старше 7 в Vivado, или я что-то путаю?
yamifa_1234 Автор
24.05.2023 21:08Все верно, Спартан6 в вивадо не поддерживается. Я ее использовал чтобы красивую схему получить)
ovn83
24.05.2023 21:08На spartan 7 через MIG вопросы по работе с sdram решаються гораздо проще
yamifa_1234 Автор
24.05.2023 21:08а там не ддр распаивают случайно? просто мне кажется SRAМ вещь уже старенькая)
ovn83
24.05.2023 21:08SDRAM -это синхронная динамическая память с произвольным доступом. DDR тоже SDRAM. На Spartan-7 работал с DDR2, на Kintex-7 с DDR3
Pyhesty
не занудства ради, но спрошу: отличие, преимущества вашего модуля перед модулем" FTSDCTRL - 32/64-bit PC133 SDRAM Controller with EDAC" представленном в GPL библиотеке GRLIB IP CORE?
вы пробовали запускать другие модули и сравнивать результаты?
ps: а так, спасибо за статью) каждый уважаемый радиотехник должен написать контроллер памяти) я правда стал лениться и исрользую открытые библиотеки ядер, где это возможно, чего и вам рекомендую)
yamifa_1234 Автор
Не знал я про это айпи ядро) Насчет преимуществ: вряд ли в моей реализации есть особый список преимуществ, хотя бы потому что я выбрал самый простой режим работы. Возможно при последовательной записи/чтении производительность будет более менее одинаковой, но в остальных случаях будет безбожно проигрывать) Например у меня не реализован побайтовый валид, у меня не предусмотрен переход от чтения к записи без перезарядки, нужно загружать каждое значение вместе с адресом.
Посмотрел код из библиотеки GRLIB IP CORE (mt48lc16m16a2.vhd)
С ходу я так и не понял как его подцепить в свой проект. И вот тут вижу преимущества моего кода в том что он вполне прозрачен)
Pyhesty
В общем случае это касается всех модулей (не только этого пакета), что нужно разобраться с шиной AHB (ну и заодно с APB), так как модули взаимодействуют через эту шину. Использование уже готовых IP решений через шины AHB/AXI/APB - откроет для вас массу преимуществ =)
повторюсь это не занудства ради, а для и остальных читателей данной статьи, кто планирует дальше в ПЛИС.