Изучаем САПР Altera Quartus II / Intel Quartus Prime, язык описания аппаратуры Verilog HDL и кое-что про VHDL. Программируем FPGA Altera / Intel Cyclone III, Cyclone IV, MAX II, MAX10. Экспериментируем с платами разработчика FPGA серии Марсоход, Марсоход2, Марсоход3. А еще разбираемся с FPGA Gowin.
В предыдущей статье я рассказывал, что такое амплитудная модуляция и проделал программные эксперименты по переносу спектра аудиосигнала на несущую частоту. Сейчас я хочу продемонстрировать проект в плате Марсоход2 - простой АМ передатчик из ПЛИС.
Если вы помните, когда-то я сделал из платы Марсоход2 простой радиопередатчик с частотной модуляцией. Сейчас я хочу сделать радиопередатчик с амплитудной модуляцией радиосигнала. Амплитудную модуляцию по идее будет проще сделать, но и тут нужно будет хорошенько подумать и поломать голову.
Для начала, нужно разобраться, что же такое амплитудная модуляция сигнала. Эта статья как раз про это.
Вчера пришел на работу, думал заниматься текущими проектами и тут читаю, что оказывается 20-го марта солнечное затмение. Ну как можно работать в таких условиях, все бросил, решил попробовать посмотреть затмение через свой телескоп.
Да-да, знаю, это затмение уже всем надоело - все во всех соц. сетях про него уже сто раз написали. Ну и я напишу.
По давней академической традиции начало календарного года у большинства студентов совпадает с промежуточной аттестацией, которая является своеобразным итогом нескольких предыдущих месяцев напряжённой учёбы. Мне тоже пришлось на время забыть о Марсоходе и погрузиться в экзаменационную рутину, но, слава роботам, сессия закончилась и я могу вернуться к более приятным вещам.
Прежде всего, я решил подготовить небольшое обновление простого контроллера SDRAM. Основным недостатком существующей версии этого контроллера является отсутствие поддержки автоматической регенерации ячеек памяти – это незаметно, если регулярно обращаться ко всему объёму используемой оперативной памяти, однако, если требуется отложить данные "в долгий ящик", можно столкнуться с проблемой их постепенной порчи.
Производитель микросхемы памяти предусмотрел два способа регенерации, из которых задействовать можно только AUTO REFRESH (таковы особенности схемотехники платы Марсоход2). Micron рекомендует подавать 4096 команд AUTO REFRESH в течении 64 миллисекунд, причём каждая команда регенерирует какую-то одну строку соответственно внутреннему счётчику. Для применения команды необходимо, чтобы все активные строки и банки были закрыты, а после команды следует выждать некоторое время (66 наносекунд для микросхемы, установленной на Марсоход2).
Вот мой обновленный контроллер:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity sdram_controller is
generic (
--memory frequency in MHz
sdram_frequency : integer := 50
);
port (
--ready to operation
ready : out std_logic := '0';
--clk
clk : in std_logic;
--read
rd_req : in std_logic;
rd_adr : in std_logic_vector(21 downto 0);
rd_data : out std_logic_vector(15 downto 0);
rd_valid : out std_logic;
--write
wr_req : in std_logic;
wr_adr : in std_logic_vector(21 downto 0);
wr_data : in std_logic_vector(15 downto 0);
--SDRAM interface
sdram_wren_n : out std_logic := '1';
sdram_cas_n : out std_logic := '1';
sdram_ras_n : out std_logic := '1';
sdram_a : out std_logic_vector(11 downto 0);
sdram_ba : out std_logic_vector(1 downto 0);
sdram_dqm : out std_logic_vector(1 downto 0);
sdram_dq : inout std_logic_vector(15 downto 0);
sdram_clk_n : out std_logic
);
end entity;
architecture rtl of sdram_controller is
signal adr : std_logic_vector(21 downto 0) := (others => '0'); --selected address
signal adr_reg : std_logic_vector(21 downto 0) := (others => '0'); --selected address register
signal state : std_logic_vector(2 downto 0) := "100"; --state machine register
signal sdram_cmd : std_logic_vector(2 downto 0) := (others => '1'); --command register
signal wr_data1 : std_logic_vector(15 downto 0) := (others => '0'); --write data pipe stage1
signal wr_data2 : std_logic_vector(15 downto 0) := (others => '0'); --write data pipe stage2
signal rd_pipe_valid : std_logic_vector(3 downto 0) := (others => '0');
signal rd_now : std_logic := '0';
signal rd_selected : std_logic := '0';
signal wr_selected : std_logic := '0';
signal rd_cycle : std_logic := '0';
signal same_row_and_bank : std_logic := '0';
signal sdram_dq_oe : std_logic := '0'; --output enable
signal init_cnt : integer := 0; --initialization counter value
signal refresh_cnt : integer := 0; --refresh counter
--sdram commands
constant cmd_loadmode : std_logic_vector(2 downto 0) := "000";
constant cmd_refresh : std_logic_vector(2 downto 0) := "001";
constant cmd_precharge : std_logic_vector(2 downto 0) := "010";
constant cmd_active : std_logic_vector(2 downto 0) := "011";
constant cmd_write : std_logic_vector(2 downto 0) := "100";
constant cmd_read : std_logic_vector(2 downto 0) := "101";
constant cmd_nop : std_logic_vector(2 downto 0) := "111";
--Timing parameters
constant tRAS : integer := ((sdram_frequency * 44)/1000)+1; --ACTIVE-to-PRECHARGE command
constant tRC : integer := ((sdram_frequency * 66)/1000)+1; --ACTIVE-to-ACTIVE command period
constant tRCD : integer := ((sdram_frequency * 20)/1000)+1; --ACTIVE-to-READ or WRITE delay
constant tRFC : integer := ((sdram_frequency * 66)/1000)+1; --AUTO REFRESH command period
constant tRP : integer := ((sdram_frequency * 20)/1000)+1; --PRECHARGE command period
constant tRRD : integer := ((sdram_frequency * 15)/1000)+1; --ACTIVE bank a to ACTIVE bank b command
constant tWR : integer := ((sdram_frequency * 15)/1000)+1; --WRITE recovery time
constant tINIT : integer := (sdram_frequency * 10)+1; --minimal initialization time
constant tREF : integer := ((sdram_frequency * 1560)/1000)+1; --REFRESH period (for row)
begin
state_machine:process(clk)
begin
if rising_edge(clk) then
case state is
when "000" => if ((rd_req = '1') or (wr_req = '1')) then --if read or write request
sdram_cmd <= cmd_active; --then activate sdram
sdram_ba <= adr(21 downto 20); --and open this bank
sdram_a <= adr(19 downto 8); --and this row
sdram_dqm <= "11";
state <= "001"; --go to "read or write" state after all
else
sdram_cmd <= cmd_nop; --if no requests then no operation needed
sdram_ba <= (others => '0');
sdram_a <= (others => '0');
sdram_dqm <= "11";
state <= "000";
end if;
when "001" => if (rd_selected = '1') then
sdram_cmd <= cmd_read; --run read if read needed
else
sdram_cmd <= cmd_write; --...or write
end if;
sdram_ba <= adr_reg(21 downto 20);
sdram_a(9 downto 0) <= "00" & adr_reg(7 downto 0);
sdram_a(10) <= '0';
sdram_dqm <= "00";
--if row address do not change, repeat prev operation
if ((rd_selected = '1' and rd_req = '1' and same_row_and_bank = '1') or (rd_selected = '0' and wr_req = '1' and same_row_and_bank = '1')) then
state <= "001";
else --else open new bank and row
state <= "010";
end if;
when "010" => sdram_cmd <= cmd_precharge; --closing row
sdram_ba <= (others => '0');
sdram_a <= (10 => '1', others => '0');
sdram_dqm <= "11";
state <= "011";
when "011" => sdram_cmd <= cmd_nop;
sdram_ba <= (others => '0');
sdram_a <= (others => '0');
sdram_dqm <= "11";
if (refresh_cnt = tREF+1) then
state <= "101";
else
state <= "000";
end if;
when "100" => if (init_cnt = tINIT+1) then --initialization
sdram_cmd <= cmd_precharge;
sdram_a(10) <= '1';
elsif (init_cnt = tINIT+tRP+1 or init_cnt = tINIT+tRP+tRFC+1) then
sdram_cmd <= cmd_refresh;
elsif (init_cnt = tINIT+tRP+2*tRFC+1) then
sdram_cmd <= cmd_loadmode;
sdram_a(9 downto 0) <= "0000100111";
elsif (init_cnt = tINIT+tRP+2*tRFC+3+1) then
state <= "000";
else
sdram_cmd <= cmd_nop;
end if;
when "101" => sdram_cmd <= cmd_refresh;
if ((refresh_cnt > 0) and refresh_cnt < tRFC) then
sdram_cmd <= cmd_nop;
elsif (refresh_cnt = tRFC) then
state <= "000";
end if;
when others => null;
end case;
end if;
end process state_machine;
read_priority:process(rd_req, wr_req) --read requests have priority
begin
rd_now <= rd_req;
end process read_priority;
process(clk)
begin
if rising_edge(clk) then
if (state = "000") then
rd_selected <= rd_now;
end if;
end if;
end process;
address_select:process(clk, rd_cycle)
begin
if rising_edge(clk) then
adr_reg <= adr;
end if;
end process address_select;
output_enable:process(clk)
begin
if rising_edge(clk) then
if (state = "001") then
sdram_dq_oe <= wr_selected;
else
sdram_dq_oe <= '0';
end if;
end if;
end process output_enable;
process(clk)
begin
if rising_edge(clk) then
wr_data1 <= wr_data;
wr_data2 <= wr_data1;
end if;
end process;
read_valid:process(clk)
begin
if rising_edge(clk) then
if (state = "001" and rd_selected = '1') then
rd_pipe_valid <= rd_pipe_valid(2 downto 0) & '1';
else
rd_pipe_valid <= rd_pipe_valid(2 downto 0) & '0';
end if;
end if;
end process read_valid;
read_data:process(clk)
begin
if rising_edge(clk) then
rd_data <= sdram_dq;
end if;
end process read_data;
init_counter:process(clk)
begin
if rising_edge(clk) then
if (init_cnt < (tINIT+2*tRFC+tRP+3+1)) then
init_cnt <= init_cnt+1;
else
null;
end if;
end if;
end process init_counter;
refresh_counter:process(clk)
begin
if rising_edge(clk) then
if (refresh_cnt < tREF+1) then
refresh_cnt <= refresh_cnt+1;
else
if (state = "101") then
refresh_cnt <= 0;
end if;
end if;
end if;
end process refresh_counter;
ready <= '1' when ((state = "000") or (state = "001")) else '0';
adr <= rd_adr when (rd_cycle = '1') else wr_adr; --address select
same_row_and_bank <= '1' when (adr(21 downto 8) = adr_reg(21 downto 8)) else '0';
rd_cycle <= rd_now when (state = "000") else rd_selected;
wr_selected <= not rd_selected;
rd_valid <= rd_pipe_valid(3);
--command set
sdram_ras_n <= sdram_cmd(2);
sdram_cas_n <= sdram_cmd(1);
sdram_wren_n <= sdram_cmd(0);
--write
sdram_dq <= wr_data2 when sdram_dq_oe = '1' else (others => 'Z');
--sdram_clock
sdram_clk_n <= not clk;
end rtl;
Изменения в контроллере нельзя назвать масштабными. Прежде всего, появился счётчик, который отмеряет периоды между командами AUTO REFRESH. Надо заметить, что для перехода автомата в состояние "регенерации", на котором базируется работа контроллера, должно выполниться два условия: счётчик должен накопить определённое значение, и должна быть применена операция закрытия активной строки/банка. Автомат будет находиться в состоянии "регенерации" столько, сколько необходимо для соблюдения рекомендуемых производителем временных интервалов.
Алгоритм работы с контроллером тоже немного изменился – теперь следует больше внимания уделять выходу ready. На нём формируется высокое логическое состояние, когда контроллер готов воспринимать сигналы со входов (запросы на чтение и запись, данные, адреса), и низкое, когда производятся операции инициализации, закрытия строк и регенерации. На мой взгляд, это должно сделать взаимодействие с контроллером более удобным.
Наконец, я добавил настраиваемый параметр sdram_frequency – на его основе высчитываются основные задержки микросхемы в тактах синхронизирующего сигнала. Этот параметр следует указывать в МГц, причём я бы не рекомендовал выходить за пределы 100 МГц – на такой частоте стабильная работа контроллера и памяти уже не гарантируется.
Честно говоря, у меня пока нет возможности верифицировать работу контроллера на 100%, поэтому я надеюсь на обратную связь – о найденных ошибках и прочих "неопрятностях" можно писать прямо в комментариях к этой статье или мне на электронную почту.
PS: эта статья - продолжение моей другой работы Простейший SDRAM-контроллер на VHDL
Подробнее...