Подключаем к ПЛИС оперативную память SDRAM

от автора

На страницах всевозможных статей было написано, что управление микросхемой памяти 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 буферов напрямую на физические пины inoutassign 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)beginif (reset) beginrLed <= 3'd7;top_state <= ST_TOP_IDLE;addr_mem <= 12'd0;cmd_write <= 1'd0; // Снять команду записиcmd_read <= 1'd0;  // Снять команду чтенияend else begincmd_write <= 1'd0;cmd_read  <= 1'd0;case (top_state)ST_TOP_IDLE: beginif (start) begintop_state <= ST_TOP_WRITE_MEM;endendST_TOP_WRITE_MEM: beginif (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;endendST_TOP_READ_MEM: beginif (SD_Ready) begincmd_read <= 1'd1; // Выдать команду чтенияtop_state <= ST_TOP_OUT_LED;endendST_TOP_OUT_LED: begin/* Ожидаю готовность от контроллера, когда есть готовность, значитсигналы на шине установились */if (SD_Ready) beginrLed <= dq_in_ioe[2:0];top_state <= ST_TOP_IDLE;endenddefault: top_state <= ST_TOP_IDLE;endcaseendendendmodule

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

`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)beginXQ <= !X;/* &CTQ - все биты счётчика равны единице, т.е. максимум, даёт единицу      |CTQ - все биты счётчика равны нулю, т.е. минимум, даёт ноль*/if (XQ & ~&CTQ) CTQ <= CTQ + 1'd1;else if (!XQ & |CTQ) CTQ <= CTQ - 1'd1;if (&CTQ) RSTrigQ <= 1'd1; // счётчик досчитал до максимум, запоминаем 1else if (~|CTQ) RSTrigQ <= 1'd0; // счётчик досчитал до минимума, запоминаем 0BQ <= RSTrigQ;//if (FY) TTrigQ <= !TTrigQ; // по фронту переключаем тригерTTrigQ <= FY;endendmodule

Кнопка.

`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) beginif (rst) beginrefresh_timer <= 10'd0;         refresh_req   <= 1'b0;      end else beginif (refresh_timer >= REFRESH_INTERVAL) beginrefresh_timer <= 10'd0;            refresh_req   <= 1'b1; // Время истекло, взводим флаг запроса         end else beginrefresh_timer <= refresh_timer + 1'b1;         end         // Сбрасываем запрос только тогда, когда FSM физически зашел// в состояние регенерации         if (state == ST_REFRESH && delay_cnt == 0) beginrefresh_req <= 1'b0;         endend   end   // ------------------------------reg [3:0]  state;   reg [13:0] delay_cnt;   reg [1:0]  init_ref_cnt; // Счетчик авторегенераций при старте// Конечный автомат управления переходамиalways @(posedge clk or posedge rst) beginif (rst) beginstate        <= ST_INIT_NOP;delay_cnt    <= 14'd0;init_ref_cnt <= 2'd0;ready        <= 1'b0;end else begincase (state)// --- Стадия инициализации ---ST_INIT_NOP: begin/* Пауза 200мкс */  if (delay_cnt >= WAIT_200US) beginstate     <= ST_INIT_PRE;delay_cnt <= 14'd0;  end else begindelay_cnt <= delay_cnt + 1'b1;  endendST_INIT_PRE: begin  if (delay_cnt >= WAIT_TRP) beginstate     <= ST_INIT_REF;delay_cnt <= 14'd0;  end else begindelay_cnt <= delay_cnt + 1'b1;  endendST_INIT_REF: begin  if (delay_cnt >= WAIT_TRFC) begindelay_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 begindelay_cnt <= delay_cnt + 1'b1;  endendST_INIT_MRS: begin  if (delay_cnt >= WAIT_TMRD) beginstate     <= ST_IDLE;delay_cnt <= 14'd0;  end else begindelay_cnt <= delay_cnt + 1'b1;  endend// --- Рабочий цикл и Арбитраж ---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;  endendST_ACTIVATE: begin  if (delay_cnt >= WAIT_TRCD - 1) begindelay_cnt <= 14'd0;state     <= cmd_write ? ST_WRITE : ST_READ;  end else begindelay_cnt <= delay_cnt + 1'b1;  endendST_WRITE: begindelay_cnt <= 14'd0; // Обнуляем для будущего цикла PRECHARGE!!!!!!!!!state <= ST_PRECHARGE;endST_READ: begindelay_cnt <= 14'd0; // Обнуляем для будущего цикла PRECHARGE!!!!!!!!!state <= ST_PRECHARGE;endST_PRECHARGE: begin  if (delay_cnt >= WAIT_TRP) begindelay_cnt <= 14'd0;state     <= ST_IDLE; // Возврат в IDLE, где сразу проверится refresh_req  end else begindelay_cnt <= delay_cnt + 1'b1;  endend// --- Новое состояние: Периодический Auto Refresh в процессе работы ---ST_REFRESH: begin  if (delay_cnt >= WAIT_TRFC) begindelay_cnt <= 14'd0;state     <= ST_IDLE; // Регенерация завершена, возвращаемся в ожидание  end else begindelay_cnt <= delay_cnt + 1'b1;  endenddefault: beginstate <= ST_INIT_NOP;delay_cnt <= 14'd0; //!!!!!!!!!!!!!!!!!!!!!!!!!!endendcaseendend// Формирование команд на шинуalways @(*) beginsdram_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)endST_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 словоendST_IDLE:       sdram_cmd_out = CMD_NOP;ST_ACTIVATE: begin sdram_cmd_out = CMD_ACTIVATE; sdram_a       = row;endST_WRITE: begin sdram_cmd_out = CMD_WRITE; sdram_a       = {4'b0000, col}; // A10=0 (без авто-пречарджа)endST_READ: begin sdram_cmd_out = CMD_READ; sdram_a       = {4'b0000, col}; // A10=0endST_PRECHARGE: begin sdram_cmd_out = CMD_PRECHARGE; sdram_a       = 12'b0000_0000_0000; // A10=0 (закрыть только текущий банк)end// Выдача физической команды авторегенерации на шину памятиST_REFRESH: begin sdram_cmd_out = CMD_REFRESH;enddefault:       sdram_cmd_out = CMD_NOP;endcaseendendmodule

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

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

ссылка на оригинал статьи https://habr.com/ru/articles/1053032/