В статье про 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:

картинка с сайта http://microsin.net/adminstuff/hardware/sdr-sdram-mt48lc16m16a2.html

У самой же микросхемы полно различных режимов работы, например:

  • Различные длины пакетов, которые можно за раз прочитать и записать.

  • Авто перезарядка ячеек памяти.

  • Количество участвующих банков памяти во время обновления заряда.

  • Так же имеются различные варианты перехода от состояния к состоянию.

Для того чтобы упростить себе жизнь, я перешел в режим доступа к одной ячейке за одно обращение. Так же воспользовался свойством, что после команды чтения/записи можно выполнить следующую команду чтения/записи, если новый адрес в пределах активированной строки. В моем случае, я мог писать по 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 контроллера.
Структурная схема взаимодействия VGA модуля с памятью.
Структурная схема взаимодействия VGA модуля с памятью.

В проекте имелось два клоковых домена, на 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)


  1. Pyhesty
    24.05.2023 21:08

    не занудства ради, но спрошу: отличие, преимущества вашего модуля перед модулем" FTSDCTRL - 32/64-bit PC133 SDRAM Controller with EDAC" представленном в GPL библиотеке GRLIB IP CORE?

    вы пробовали запускать другие модули и сравнивать результаты?

    ps: а так, спасибо за статью) каждый уважаемый радиотехник должен написать контроллер памяти) я правда стал лениться и исрользую открытые библиотеки ядер, где это возможно, чего и вам рекомендую)


    1. yamifa_1234 Автор
      24.05.2023 21:08
      +1

      Не знал я про это айпи ядро) Насчет преимуществ: вряд ли в моей реализации есть особый список преимуществ, хотя бы потому что я выбрал самый простой режим работы. Возможно при последовательной записи/чтении производительность будет более менее одинаковой, но в остальных случаях будет безбожно проигрывать) Например у меня не реализован побайтовый валид, у меня не предусмотрен переход от чтения к записи без перезарядки, нужно загружать каждое значение вместе с адресом.

      Посмотрел код из библиотеки GRLIB IP CORE (mt48lc16m16a2.vhd)

      С ходу я так и не понял как его подцепить в свой проект. И вот тут вижу преимущества моего кода в том что он вполне прозрачен)


      1. Pyhesty
        24.05.2023 21:08
        +3

        С ходу я так и не понял как его подцепить в свой проект

        В общем случае это касается всех модулей (не только этого пакета), что нужно разобраться с шиной AHB (ну и заодно с APB), так как модули взаимодействуют через эту шину. Использование уже готовых IP решений через шины AHB/AXI/APB - откроет для вас массу преимуществ =)

        повторюсь это не занудства ради, а для и остальных читателей данной статьи, кто планирует дальше в ПЛИС.

        Подключение модуля
        Подключение модуля


  1. antonsosnitzkij
    24.05.2023 21:08

    На плате вижу Spartan-6, а на скриншоте кажется Vivado? Вроде ведь нет поддержки семейств старше 7 в Vivado, или я что-то путаю?


    1. yamifa_1234 Автор
      24.05.2023 21:08

      Все верно, Спартан6 в вивадо не поддерживается. Я ее использовал чтобы красивую схему получить)


  1. ovn83
    24.05.2023 21:08

    На spartan 7 через MIG вопросы по работе с sdram решаються гораздо проще


    1. yamifa_1234 Автор
      24.05.2023 21:08

      а там не ддр распаивают случайно? просто мне кажется SRAМ вещь уже старенькая)


      1. ovn83
        24.05.2023 21:08

        SDRAM -это синхронная динамическая память с произвольным доступом. DDR тоже SDRAM. На Spartan-7 работал с DDR2, на Kintex-7 с DDR3


        1. yamifa_1234 Автор
          24.05.2023 21:08

          об том и речь) что для DDR микросхем MIGи поддержку имеют.


          1. AquariusStar
            24.05.2023 21:08

            Готовое — хорошо, а свой велосипед — ещё лучше. )