На страницах всевозможных статей было написано, что управление микросхемой памяти SDRAM это очень сложно. Отчасти это верно, есть масса тонкостей. Процесс освоения новичкам осложнён отсутствием примеров на русском языке. Вашему вниманию предлагается небольшой пример, как можно подключить оперативную память к ПЛИС. Эти заметки для новичков, таких как и я. А следовательно не торопитесь, проверяйте всё что будете использовать. Особенно реализацию платы, если она у вас самодельная. Опытным пользователям можно не читать (разве что из спортивного интереса). Не буду увлекаться теорией, кому нужно читайте литературу или хотя бы спросите искусственный интеллект (он неплохо может расписать что к чему).

В общих чертах, необходимо провести инициализацию микросхемы (см. документацию на микросхему) и в дальнейшем подавать команды (это сигналы CS, RAS, CAS, WE). Повторюсь, почитайте литературу. Для тех кто совсем не в теме краткое резюме по работе с SDRAM.

Каждые 64 мс необходимо выполнить перезаряд строки памяти. Какой строки, решает сама микросхема, главное команду ей дать (плюс-минус не сильно позже указанного интервала). Так как команды выполняются значительно быстрее этого срока, то не страшно если пришло время регенерации, а выполняется команда. Можно подождать.

Дальше для чтения записи, выполняем активацию строки (память организованна в виде строк и столбцов), на шине адрес строки и команда активации. Далее команда (например) чтения столбца, на шине адрес столбца и команда чтения. Далее чтение данных и в завершении команда перезаряда (после доступа данные нужно перезаписать см. литературу). Естественно между командами выдерживаются заданные паузы (см. документацию).

В составе макета имеем четыре переключателя, четыре светодиода, две кнопки (сброс и старт) и микросхема памяти 8МБ HY57V641620FTP-H (4-ре банка по мегабайту 16-ти битных слов). Алгоритм работы макета следующий. По нажатию кнопки читаем состояние переключателей (только трёх т.к. на плате кнопки параллельны с переключателем), это будет адрес ячейки памяти и данные. Выполняем запись данных по этому адресу (используем только три младших разряда). Выполняем чтение по этому адресу и выводим результат на светодиоды. Всё довольно просто.

Светодиоды.
Светодиоды.
Переключатели.
Переключатели.
Микросхема SDRAM.
Микросхема SDRAM.

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

Поясняю, мы выдаём команду по фронту синхроимпульса и предполагаем, что сигналы на входе микросхемы установятся мгновенно. Но это не так, что бы быть уверенным, что к моменту фронта синхросигнала уже все сигналы установились предлагается следующий трюк. Тактировать микросхему памяти синхросигналом смещённым по фазе на 90 градусов (это общий случай). Этот сдвиг подбирается экспериментально (по крайней мере в любительских схемах). У меня стабильно работает в диапазоне сдвига фаз от 25 до 35 градусов (использую 30).

Во вторых на ПЛИС Altera есть рекомендация компилятору размещать выходные элементы прямо возле выходной ножки. В настройках выводов нужно указать “Fast Input Register On” и “Fast Output Enable Register On”.

В третьих все выводы на микросхему памяти в режим 3.3-V LVTTL. Не забываем все неиспользуемые ножки ПЛИС перевести в третье состояние (по умолчанию так и есть, но проверить не помешает). Далее код на verilog. Несколько сумбурный и много строчный, я говорил, что я сам новичок.

`timescale 1ns / 1ps
/* !!! тактирование для SDRAM (сдвиг +30 град.) ЗАРАБОТАЛО!!! */
/*
    По нажатию на кнопку KEY4 фиксируем состояние переключателей CKEY[1..3],
	   выдаём команду записи по адресу {9'd0, CKEY3, CKEY2, CKEY1},
	   записываем данные {13'd0, CKEY3, CKEY2, CKEY1}.
	 После этого читаем содержимое памяти по тому же адресу и
	   выводим младшие три бита на светодиоды.
    Микросхема на плате HY57V641620FTP-H
*/
module TestSDRAM(
		(* chip_pin = "76"  *) output SD_A00, // Адрессная шина
		(* chip_pin = "77"  *) output SD_A01,
		(* chip_pin = "80"  *) output SD_A02,
		(* chip_pin = "83"  *) output SD_A03,
		(* chip_pin = "68"  *) output SD_A04,
		(* chip_pin = "67"  *) output SD_A05,
		(* chip_pin = "66"  *) output SD_A06,
		(* chip_pin = "65"  *) output SD_A07,
		(* chip_pin = "64"  *) output SD_A08,
		(* chip_pin = "60"  *) output SD_A09,
		(* chip_pin = "75"  *) output SD_A10,
		(* chip_pin = "59"  *) output SD_A11,
		
		(* chip_pin = "73"  *) output SD_BS0, // Выбор банка
		(* chip_pin = "74"  *) output SD_BS1,
		
		(* chip_pin = "42"  *) output SD_LDQM, // Маскирование младшего байта
		(* chip_pin = "55"  *) output SD_UDQM, // Маскирование старишего байта
		
		(* chip_pin = "87"  *) output LED1,
		(* chip_pin = "86"  *) output LED2,
		(* chip_pin = "85"  *) output LED3,
		(* chip_pin = "84"  *) output LED4,
		
		(* chip_pin = "58"  *) output SD_CKE,  // Разрешение тактирования
		
		/* Тактовый сигнал сдвинутый по фазе на 90 грд. для компенсации задержек
		из-за длинны дорожек на плпте. */
		(* chip_pin = "43"  *) output SD_CLK,  

		(* chip_pin = "72"  *) output SD_CS,   // Выбор чипа
		(* chip_pin = "71"  *) output SD_RAS,  // Строб строки
		(* chip_pin = "70"  *) output SD_CAS,  // Строб колонки
		(* chip_pin = "69"  *) output SD_WE,   // Разрешение записи
		
		(* chip_pin = "23"  *) input clk,   // тактирование 50МГц
		(* chip_pin = "25"  *) input rst_n, // сброс

		(* chip_pin = "88"  *) input CKEY1, // Задаём число для записи в ОЗУ 16-bit
		(* chip_pin = "89"  *) input CKEY2, // {13'd0, CKEY3, CKEY2, CKEY1}
		(* chip_pin = "90"  *) input CKEY3,

		/* нажатие - команда записи в ОЗУ,
		после записи чтение и вывод на светодиоды */
		(* chip_pin = "91"  *) input KEY4_n,

		(* chip_pin = "28" *) inout SD_D00, // Шина данных
		(* chip_pin = "30" *) inout SD_D01,
		(* chip_pin = "31" *) inout SD_D02,
		(* chip_pin = "32" *) inout SD_D03,
		(* chip_pin = "33" *) inout SD_D04,
		(* chip_pin = "34" *) inout SD_D05,
		(* chip_pin = "38" *) inout SD_D06,
		(* chip_pin = "39" *) inout SD_D07,
		(* chip_pin = "54" *) inout SD_D08,
		(* chip_pin = "53" *) inout SD_D09,
		(* chip_pin = "52" *) inout SD_D10,
		(* chip_pin = "51" *) inout SD_D11,
		(* chip_pin = "50" *) inout SD_D12,
		(* chip_pin = "49" *) inout SD_D13,
		(* chip_pin = "46" *) inout SD_D14,
		(* chip_pin = "44" *) inout SD_D15
		);
	
	// Состояния конечного автомата top
   localparam ST_TOP_IDLE        = 2'd0,
              ST_TOP_WRITE_MEM   = 2'd1,
			  ST_TOP_READ_MEM    = 2'd2,
			  ST_TOP_OUT_LED     = 2'd3;
	//-----------------------------------

	wire clk_sys;    	// 50 MHz (0 deg)
    wire clk_sdram;  	// 50 MHz (30 deg)
	wire pll_locked; 	// 0 - PLL не стабильно, 1 - стабильно
	
	wire Button_Reset;// Кнопка сброс
	wire reset;			// Это общий сброс. Кроме кнопки, влияет состояние PLL.
	wire start;			// Кнопка запуска процесса записи/чтения
	wire SD_Ready;		// Готовность контроллера принимать команды
	
	wire [11:0] addr_from_cntrl;
	reg cmd_write, cmd_read;

	reg [1:0] top_state;
	reg [2:0] rLed;

	/* Это входные данные для контроллера SDRAM,
		  т.е. здесь указывается адрес по которому
		  хотим читать либо писать. А контроллер,
		  на основании этой информации сформирует сигнал на внешние ноги. */
	reg [11:0] addr_mem;

	/* Здесь хранится результат чтения переключателей,
		исходная информация для шины данных (запись в память) */
	reg [15:0] data_to_ram;

	reg  [15:0] dq_out_ioe;
	reg  [15:0] dq_oe_ioe;
	reg  [15:0] dq_in_ioe;
	// ------------------------------------------------------------------------

	// Синхронная передача сигналов на периферию (внутри IOE)
	always @(posedge clk_sys) begin
	  dq_out_ioe  <= data_to_ram;
	  dq_oe_ioe   <= {16{cmd_write}};
	  // Фиксация входных данных из памяти
	  	dq_in_ioe[0]  <= SD_D00;  dq_in_ioe[1]  <= SD_D01;
		dq_in_ioe[2]  <= SD_D02;  dq_in_ioe[3]  <= SD_D03;
		dq_in_ioe[4]  <= SD_D04;  dq_in_ioe[5]  <= SD_D05;
		dq_in_ioe[6]  <= SD_D06;  dq_in_ioe[7]  <= SD_D07;
		dq_in_ioe[8]  <= SD_D08;  dq_in_ioe[9]  <= SD_D09;
		dq_in_ioe[10] <= SD_D10;  dq_in_ioe[11] <= SD_D11;
		dq_in_ioe[12] <= SD_D12;  dq_in_ioe[13] <= SD_D13;
		dq_in_ioe[14] <= SD_D14;  dq_in_ioe[15] <= SD_D15;
	end

	// Побитовое назначение Tri-state буферов напрямую на физические пины inout
	assign SD_D00 = dq_oe_ioe[0]  ? dq_out_ioe[0]  : 1'bZ;
	assign SD_D01 = dq_oe_ioe[1]  ? dq_out_ioe[1]  : 1'bZ;
	assign SD_D02 = dq_oe_ioe[2]  ? dq_out_ioe[2]  : 1'bZ;
	assign SD_D03 = dq_oe_ioe[3]  ? dq_out_ioe[3]  : 1'bZ;
	assign SD_D04 = dq_oe_ioe[4]  ? dq_out_ioe[4]  : 1'bZ;
	assign SD_D05 = dq_oe_ioe[5]  ? dq_out_ioe[5]  : 1'bZ;
	assign SD_D06 = dq_oe_ioe[6]  ? dq_out_ioe[6]  : 1'bZ;
	assign SD_D07 = dq_oe_ioe[7]  ? dq_out_ioe[7]  : 1'bZ;
	assign SD_D08 = dq_oe_ioe[8]  ? dq_out_ioe[8]  : 1'bZ;
	assign SD_D09 = dq_oe_ioe[9]  ? dq_out_ioe[9]  : 1'bZ;
	assign SD_D10 = dq_oe_ioe[10] ? dq_out_ioe[10] : 1'bZ;
	assign SD_D11 = dq_oe_ioe[11] ? dq_out_ioe[11] : 1'bZ;
	assign SD_D12 = dq_oe_ioe[12] ? dq_out_ioe[12] : 1'bZ;
	assign SD_D13 = dq_oe_ioe[13] ? dq_out_ioe[13] : 1'bZ;
	assign SD_D14 = dq_oe_ioe[14] ? dq_out_ioe[14] : 1'bZ;
	assign SD_D15 = dq_oe_ioe[15] ? dq_out_ioe[15] : 1'bZ;

	/* Вывод содержимого addr на выходные ножки
		  содержимое addr меняет контроллер SDRAM	*/
	assign {SD_A11, SD_A10, SD_A09, SD_A08,
			  SD_A07, SD_A06, SD_A05, SD_A04,
			  SD_A03, SD_A02, SD_A01, SD_A00} = addr_from_cntrl;

	/*-------------------------------------------------------------------------
	   Из одного входного тактового сигнала формируется два:
			-первый копия входного,
			-второй смещённый по фазе на -90 градусов. Это необходимо
			для корректной работы микросхемы памяти на высокой частоте.
			Так как не всегда все дорожки на плате одинаковые и короткие,
			что приводит к задержкам и несогласованной с контроллером работе. */
	PLL_SDRAM_OFF PLL1(.inclk0(clk),	// Входной тактовый сигнал
						.c0(clk_sys),	// тактирование для всех, кроме SDRAM
						.c1(clk_sdram),// тактирование для SDRAM (сдвиг +30 град.)
						.locked(pll_locked));	// 0- сигнал не стабилен
	//-------------------------------------------------------------------------

	/* Контроллер реализован в виде автомата. На основании входных данных
			формирует сигналы управления, с учётом необходимых задержек. */
	Controller_SDRAM Controller_50MHz(
		.sdram_cmd_out({SD_CS, SD_RAS, SD_CAS, SD_WE}),
		.sdram_ba({SD_BS1, SD_BS0}),		// Номер банка
		.sdram_a(addr_from_cntrl),			// Адрес на шину
		.ready(SD_Ready),					// 1- готов к приёму команд
		.cmd_read(cmd_read),
		.cmd_write(cmd_write),
		.bank(2'd0),
		.row(12'd0),
		.col(addr_mem[7:0]),	// Адрес от "меня", куда писать/читать
		.clk(clk_sys),
		.rst(reset)
	);	

	// Выводим сдвинутый тактовый сигнал наружу на микросхему
	// через специализированный DDIO примитив Intel/Altera.
	// Обычный assign вызовет джиттер и фазовый сдвиг, ломающий Fast I/O.
	altddio_out #(
		.width(1)
	) sdram_clk_ddio_buf (
		.datain_h(1'b1),
		.datain_l(1'b0),
		.outclock(clk_sdram),
		.dataout(SD_CLK)
	);
	assign SD_LDQM = 1'd0; // Не используем маскирование
	assign SD_UDQM = 1'd0; // Не используем маскирование
	assign SD_CKE  = 1'd1; // Всё время тактирование разрешено

	Button BT_R(.TTrigQ(Button_Reset), 	// 1 - сброс
					.X(rst_n),			// Кнопка сброс, 0 - сброс
					.C(clk_sys));
					
	Button BT_K(.TTrigQ(start),	// Чтение переключателей, з/ч памяти, вывод
					.X(KEY4_n),		// Кнопка Старт
					.C(clk_sys));

	assign reset = Button_Reset | ~pll_locked;

	assign LED1 = reset ? 1'd1 : rLed[0];
	assign LED2 = reset ? 1'd1 : rLed[1];
	assign LED3 = reset ? 1'd1 : rLed[2];
	assign LED4 = 1'd1;

	always @(posedge clk_sys or posedge reset)
	begin
		if (reset) begin
			rLed <= 3'd7;
			top_state <= ST_TOP_IDLE;
			addr_mem <= 12'd0;
			cmd_write <= 1'd0; // Снять команду записи
			cmd_read <= 1'd0;  // Снять команду чтения
		end else begin

			cmd_write <= 1'd0;
			cmd_read  <= 1'd0;

			case (top_state)
				ST_TOP_IDLE: begin
					if (start) begin
						top_state <= ST_TOP_WRITE_MEM;
					end
				end
					
				ST_TOP_WRITE_MEM: begin
					if (SD_Ready) begin
						/* Читаем состояние переключателя - это адрес ячейки
							и данные одновременно */
						addr_mem <= {9'd0, CKEY3, CKEY2, CKEY1};
						data_to_ram <= {13'd0, CKEY3, CKEY2, CKEY1};
						cmd_write <= 1'd1; // Выдать команду записи
						top_state <= ST_TOP_READ_MEM;
					end
				end

				ST_TOP_READ_MEM: begin
					if (SD_Ready) begin
						cmd_read <= 1'd1; // Выдать команду чтения
						top_state <= ST_TOP_OUT_LED;
					end
				end

				ST_TOP_OUT_LED: begin
				/* Ожидаю готовность от контроллера, когда есть готовность, значит
					сигналы на шине установились */
					if (SD_Ready) begin
						rLed <= dq_in_ioe[2:0];
						top_state <= ST_TOP_IDLE;
					end
				end

				default: top_state <= ST_TOP_IDLE;
			endcase
		end
	end
endmodule

Основной модуль.

`timescale 1ns / 1ps
/* Код для кнопок */
module Button(output reg TTrigQ,
              input X,
				  input C);

	initial TTrigQ <= 1'd1;
	
	reg [18:0]CTQ; // счётчик подавления дребезга контактов
	reg XQ, RSTrigQ, BQ;
	
	wire FY = !RSTrigQ & BQ; // схема выделения фронта
	
	always @(posedge C)
	begin
		XQ <= !X;
		/* &CTQ - все биты счётчика равны единице, т.е. максимум, даёт единицу
	      |CTQ - все биты счётчика равны нулю, т.е. минимум, даёт ноль	*/
		if (XQ & ~&CTQ) CTQ <= CTQ + 1'd1;
			else if (!XQ & |CTQ) CTQ <= CTQ - 1'd1;
		
		if (&CTQ) RSTrigQ <= 1'd1; // счётчик досчитал до максимум, запоминаем 1
			else if (~|CTQ) RSTrigQ <= 1'd0; // счётчик досчитал до минимума, запоминаем 0
		
		BQ <= RSTrigQ;
		//if (FY) TTrigQ <= !TTrigQ; // по фронту переключаем тригер
		TTrigQ <= FY;
	end

endmodule

Кнопка.

`timescale 1ns / 1ps
/* Сигналы маскирования (SD_LDQM, SD_UDQM) не используем */

/* Принцип работы логики регенерации
   Счетчик интервала: Внутренний таймер непрерывно отсчитывает 780 тактов.
	
	Как только интервал истекает, выставляется скрытый флаг
		запроса refresh_req = 1.
   
	Арбитраж: Этот флаг имеет наивысший приоритет. Когда автомат находится
		в состоянии ST_IDLE, он проверяет этот флаг раньше, чем запросы
		от пользователя (cmd_read/cmd_write).
   
	Сброс флага: Перейдя в состояние регенерации ST_REFRESH,
		автомат сбрасывает этот флаг, выполняет команду CMD_REFRESH,
		выдерживает паузу tRFC (4 такта) и возвращается в ST_IDLE.

   Предотвращение коллизий: Метод проверки флага refresh_req строго
		в состоянии ST_IDLE абсолютно безопасен. Контроллер никогда не прервет
		активную операцию чтения или записи посередине. Он корректно закончит
		транзакцию пользователя, выполнит команду PRECHARGE (закроет строку),
		вернется в ST_IDLE и только затем уйдет на регенерацию.

   Запас по времени: На частоте 50 МГц полный цикл чтения или записи
		с закрытием строки занимает около 6-7 тактов. Даже если запрос
		refresh_req придет в самом начале чтения, регенерация задержится
		всего на ~140 нс, что ничтожно мало по сравнению с критическим
		окном удержания данных.
   */
module Controller_SDRAM(
	//-------------------------------------------------------------------------
	// Интерфейсы к IOE верхнего уровня
	output reg  [3:0]  sdram_cmd_out, // Команда наружу {CS, RAS, CAS, WE}
	output reg  [1:0]  sdram_ba,		 // Номер банка
	output reg  [11:0] sdram_a,		 // Адрес
	//-------------------------------------------------------------------------
	
	//-------------------------------------------------------------------------
	// Интерфейс пользователя (ПЛИС)
	output reg         ready,    // Готовность принять команду
	
	input  wire        cmd_read, // Команда чтения
	input  wire        cmd_write,// Команда записи
	input  wire [1:0]  bank,	  // Номер банка
	input  wire [11:0] row,		  // Номер строки
	input  wire [7:0]  col,		  // Номер столбца
	//-------------------------------------------------------------------------
	
	input  wire        clk,        // 50 МГц (период 20 нс)
	input  wire        rst         // Сброс
);

	// Состояния конечного автомата
   localparam ST_INIT_NOP  = 4'd0,
              ST_INIT_PRE  = 4'd1,
              ST_INIT_REF  = 4'd2,
              ST_INIT_MRS  = 4'd3,
              ST_IDLE      = 4'd4,
              ST_ACTIVATE  = 4'd5,
              ST_WRITE     = 4'd6,
              ST_READ      = 4'd7,
              ST_PRECHARGE = 4'd8,
              ST_REFRESH   = 4'd9; // Новое рабочее состояние регенерации
				  
	// Команды SDRAM {CS, RAS, CAS, WE}
   localparam CMD_NOP      = 4'b0111,
              CMD_PRECHARGE= 4'b0010,
              CMD_REFRESH  = 4'b0001,
              CMD_LOAD_MODE= 4'b0000,
              CMD_ACTIVATE = 4'b0011,
              CMD_READ     = 4'b0101,
              CMD_WRITE    = 4'b0100;

   // Параметры задержек для 50 МГц
   localparam WAIT_200US = 14'd10000; // 200mks = 10000 * (1 / (50 * 10^6))
	/* Time of Row Precharge - завершения команды PRECHARGE
	(закрытие текущей строки в банке памяти).
	Контроллер обязан "замереть" в состоянии WAIT_TRP на количество тактов,
	равное или превышающее паспортное значение
	(t_RP = 15нс) для данной частоты.	*/
   localparam WAIT_TRP   = 14'd2;
	
   localparam WAIT_TRFC  = 14'd4; // Регенерация 60нс
   localparam WAIT_TMRD  = 14'd2; // Установка ModeRegister 2 такта
   localparam WAIT_TRCD  = 14'd2; // Активация чтение запись 15нс
	
	// --- ЛОГИКА REFRESH COUNTER ---
   // 15.6 мкс / 20 нс = 780 тактов. Счет ведем от 0 до 779.
   localparam REFRESH_INTERVAL = 10'd779;
	
	reg [9:0] refresh_timer; // Счетчик интервала 15.6 мкс
   reg       refresh_req;   // Триггер-запрос на регенерацию
	
	always @(posedge clk or posedge rst) begin
		if (rst) begin
			refresh_timer <= 10'd0;
         refresh_req   <= 1'b0;
      end else begin
			if (refresh_timer >= REFRESH_INTERVAL) begin
				refresh_timer <= 10'd0;
            refresh_req   <= 1'b1; // Время истекло, взводим флаг запроса
         end else begin
				refresh_timer <= refresh_timer + 1'b1;
         end

         // Сбрасываем запрос только тогда, когда FSM физически зашел
			// в состояние регенерации
         if (state == ST_REFRESH && delay_cnt == 0) begin
				refresh_req <= 1'b0;
         end
		end
   end
   // ------------------------------
	
	reg [3:0]  state;
   reg [13:0] delay_cnt;
   reg [1:0]  init_ref_cnt; // Счетчик авторегенераций при старте

	// Конечный автомат управления переходами
	always @(posedge clk or posedge rst) begin
		if (rst) begin
			state        <= ST_INIT_NOP;
			delay_cnt    <= 14'd0;
			init_ref_cnt <= 2'd0;
			ready        <= 1'b0;
		end else begin
			case (state)
				// --- Стадия инициализации ---
				ST_INIT_NOP: begin
				/* Пауза 200мкс */
				  if (delay_cnt >= WAIT_200US) begin
						state     <= ST_INIT_PRE;
						delay_cnt <= 14'd0;
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				ST_INIT_PRE: begin
				  if (delay_cnt >= WAIT_TRP) begin
						state     <= ST_INIT_REF;
						delay_cnt <= 14'd0;
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				ST_INIT_REF: begin
				  if (delay_cnt >= WAIT_TRFC) begin
						delay_cnt <= 14'd0;
						if (init_ref_cnt >= 2'd2) begin
							 state <= ST_INIT_MRS;
						end else begin
							 init_ref_cnt <= init_ref_cnt + 1'b1;
						end
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				ST_INIT_MRS: begin
				  if (delay_cnt >= WAIT_TMRD) begin
						state     <= ST_IDLE;
						delay_cnt <= 14'd0;
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				// --- Рабочий цикл и Арбитраж ---
				ST_IDLE: begin
				  ready <= 1'b1;
				  delay_cnt <= 14'd0; // Явно держим счетчик в 0, пока отдыхаем!!!!!!!!!!!!!!!
				
				  if (refresh_req) begin
						// Приоритет №1: принудительная регенерация памяти
						state <= ST_REFRESH;
						ready <= 1'b0;
				  end else if (cmd_read || cmd_write) begin
						// Приоритет №2: команды пользователя, если нет запроса регенерации
						state <= ST_ACTIVATE;
						ready <= 1'b0;
				  end
				end
				
				ST_ACTIVATE: begin
				  if (delay_cnt >= WAIT_TRCD - 1) begin
						delay_cnt <= 14'd0;
						state     <= cmd_write ? ST_WRITE : ST_READ;
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				ST_WRITE: begin
					delay_cnt <= 14'd0; // Обнуляем для будущего цикла PRECHARGE!!!!!!!!!
					state <= ST_PRECHARGE;
				end
				
				ST_READ: begin
					delay_cnt <= 14'd0; // Обнуляем для будущего цикла PRECHARGE!!!!!!!!!
					state <= ST_PRECHARGE;
				end
				
				ST_PRECHARGE: begin
				  if (delay_cnt >= WAIT_TRP) begin
						delay_cnt <= 14'd0;
						state     <= ST_IDLE; // Возврат в IDLE, где сразу проверится refresh_req
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				// --- Новое состояние: Периодический Auto Refresh в процессе работы ---
				ST_REFRESH: begin
				  if (delay_cnt >= WAIT_TRFC) begin
						delay_cnt <= 14'd0;
						state     <= ST_IDLE; // Регенерация завершена, возвращаемся в ожидание
				  end else begin
						delay_cnt <= delay_cnt + 1'b1;
				  end
				end
				
				default: begin
					state <= ST_INIT_NOP;
					delay_cnt <= 14'd0; //!!!!!!!!!!!!!!!!!!!!!!!!!!
				end
			endcase
		end
	end
	
	// Формирование команд на шину
	always @(*) begin
		sdram_ba  = bank;
		sdram_a   = 12'd0;
		
		case (state)
			ST_INIT_NOP:   sdram_cmd_out = CMD_NOP;
			ST_INIT_PRE: begin
				 sdram_cmd_out = CMD_PRECHARGE;
				 sdram_a       = 12'b0100_0000_0000; // A10=1 (Precharge All)
			end
			ST_INIT_REF:   sdram_cmd_out = CMD_REFRESH;
			ST_INIT_MRS: begin
				 sdram_cmd_out = CMD_LOAD_MODE;
				 sdram_a       = 12'b0000_0010_0000; // CL=2, BL=1 чтение/запись пакетом, длиной 1 слово
			end
			
			ST_IDLE:       sdram_cmd_out = CMD_NOP;
			
			ST_ACTIVATE: begin
				 sdram_cmd_out = CMD_ACTIVATE;
				 sdram_a       = row;
			end
			ST_WRITE: begin
				 sdram_cmd_out = CMD_WRITE;
				 sdram_a       = {4'b0000, col}; // A10=0 (без авто-пречарджа)
			end
			ST_READ: begin
				 sdram_cmd_out = CMD_READ;
				 sdram_a       = {4'b0000, col}; // A10=0
			end
			ST_PRECHARGE: begin
				 sdram_cmd_out = CMD_PRECHARGE;
				 sdram_a       = 12'b0000_0000_0000; // A10=0 (закрыть только текущий банк)
			end
			
			// Выдача физической команды авторегенерации на шину памяти
			ST_REFRESH: begin
				 sdram_cmd_out = CMD_REFRESH;
			end
			
			default:       sdram_cmd_out = CMD_NOP;
		endcase
	end
endmodule

Контроллер SDRAM.

Кому нужен был рабочий пример, наслаждайтесь.

PS: Упустил один важный нюанс, в коде он отмечен. Как нужно выводить наружу сдвинутый по фазе тактовый сигнал для SDRAM. Альтеровская "функция" altddio_out.

Комментарии (6)


  1. ZillahGiovanni
    29.06.2026 02:48

    Любопытно, а на чем это можно повторить?
    Судя по тексту: какая-то Альтера, но их несколько.
    Что за модуль SDRAM, просто микруха?
    Лепится проводочками?
    Может это делалось на какой то девборде?
    А на какой?

    В целом наверное интересная статься, если найдутся ответы на все эти вопросы...


    1. JackKatch Автор
      29.06.2026 02:48

      У меня вот эта плата https://habr.com/ru/articles/749298/ Cyclon 4. Главная цель статьи, что бы все кто пожелает могли это повторить где угодно. SDRAM видно на картинке, HY57V641620FTP-H.


  1. GooseWing
    29.06.2026 02:48

    А есть советы как подступиться к изучению ПЛИС с нуля? Последний раз щупал альтеру на лабах в универе (в симуляторе на временных диаграммах да прошивка готового фильтра фнч, фвч) и по сути можно начинать сначала. Гайды, статьи хорошие? Литературу не сильно люблю, но тоже пойдет.


    1. JackKatch Автор
      29.06.2026 02:48

      Я смотрел уроки на Ютуб-канале ПЛИСоводство. Думаю там основная база есть.


    1. rot97
      29.06.2026 02:48

      Рекомендую книгу А. Б. Романов, Ю. А. Панчул Цифровой синтез. Практический курс.


  1. yamifa_1234
    29.06.2026 02:48

    Нужно указать источники которые вы использовали, чтобы другие смогли найти дополнительную информацию при изучении.

    Ещё интересно о режиме работы. У вас одна команда за раз выполняется или есть возможность писать несколько слов последовательно?