Tutor Speccy
Tutor Speccy
Tutor Speccy
www.zx.pk.ru
Ewgeny7
В этом мануале я попытаюсь рассказать о принципах построения столь дорогих
сердцам многих ретро-компьютеров на ПЛИС на примере простейшего ZX-Spectrum 48.
Описанный здесь вариант построения микропроцессорной системы на ПЛИС далеко не
единственный, но возможно наиболее простой для понимания. Для полноты примера к
этому талмуду прилагается готовый проект.
Итак, поехали.
1. Генераторы и счетчики.
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.std_logic_unsigned.all;
use IEEE.numeric_std.ALL;
entity speccy is
port (
-- CLOCK
CLK_50MHz : in std_logic;
-- VGA
VGA_R0 : out std_logic;
VGA_R1 : out std_logic;
VGA_R2 : out std_logic;
VGA_G0 : out std_logic;
VGA_G1 : out std_logic;
VGA_G2 : out std_logic;
VGA_B0 : out std_logic;
VGA_B2 : out std_logic;
VGA_B1 : out std_logic;
VGA_VSYNC : out std_logic;
VGA_HSYNC : out std_logic;
-- SRAM
SRAM_A : out std_logic_vector(18 downto 0);
SRAM_D : inout std_logic_vector(7 downto 0);
SRAM_nOE : out std_logic;
SRAM_nWE : out std_logic;
-- PS2
PS2_KBCLK : inout std_logic;
PS2_KBDAT : inout std_logic
);
end speccy;
component altpll0 is
port(
inclk0 : IN STD_LOGIC := '0';
c0 : OUT STD_LOGIC
);
end component;
PLL: altpll0
port map(
inclk0 => CLK_50MHz,
c0 => clock
);
Как видим, тактовая частота с внешнего генератора теперь поступает на вход модуля
PLL, а с его выхода c0 мы снимаем готовый пиксельклок clock частотой уже 14Мгц.
Здесь же счетчик тактируется сигналом hcnt. Точнее, когда hcnt достигает значения
328 то ближайший фронт clock увеличит значение vcnt на единицу.
Для удобства при дальнейших манипуляциях младший бит (0) счетчика vcnt
игнорируется, поскольку он тупо перебирает пары одинаковых строк. Поэтому в
дальнейшем что-то значащими разрядами для нас будут старшие (9..1) биты.
Пятиразрядный счетчик flash при тактировании старшим разрядом vcnt дает нам
импульсы частотой: 50Гц/32=1,56Гц. Что вполне удобоваримо для мигания курсора.
Сигнал int служит «стартером» для запуска импульса. Параметры 239 (строка
фрейма) и 316 (пиксель от начала строки) определяют положение начала импульса
на поле фрейма.
Создаем сигналы:
process(clock, hcnt)
begin
if (clock'event and clock = '1') then
if hcnt = 328 then hsync <= '0';
elsif hcnt = 381 then hsync <= '1';
end if;
end if;
end process;
Здесь всё просто, сигнал screen ограничен «квадратом» 0..191 по вертикали и 0..255
по горизонтали. Забегая вперед скажу, что этот сигнал является лишь
предварительным, реально управлять выводом видеоданных будет
«модифицированный» screen1. Но это будет попозже.
component lpm_rom0 is
PORT
(
address : IN STD_LOGIC_VECTOR (13 DOWNTO 0);
clock : IN STD_LOGIC ;
q : OUT STD_LOGIC_VECTOR (7 DOWNTO 0)
);
end component;
ROM: lpm_rom0
port map(
address => cpu_a_bus(13 downto 0),
clock => clock,
q => rom_do
);
rom_sel <= '1' when (cpu_a_bus(15 downto 14) = "00") else '0';
Едем далее. ОЗУ. Для работы ОЗУ нам понадобится сразу несколько сигналов.
Один – сигнал, информирующий что наступил цикл чтения видеоданных самим
видеогенератором:
Этот сигнал активируется каждый экранный байт по два раза – чтение пикселов
изображения, и чтение цветовых атрибутов. Как расположить эти моменты чтения
«внутри» байта собственно значения не имеет, возьмем от балды. Поскольку три
младших бита счетчика hcnt как раз и определяют биты внутри экранного байта, то
привязку сделаем именно к этому счетчику:
vid_sel <= '1' when (hcnt(2 downto 1) = "10" and clock = '0') else '0';
Здесь видно, что я выбрал значения этих битов 4 и 5. Когда hcnt(2 downto 0) будет
равно 4 (b'100) – мы прочитаем байт изображения (точек), когда 5 (b’101) – байт
атрибутов цвета.
Также обязательно сделаем привязку к состоянию clock = '0'. Это необходимо для
реализации «окна прозрачного доступа» к ОЗУ. Принцип работы этого окна я опишу
позже.
SRAM_D <= cpu_do_bus when (vid_sel = '0' and cpu_mreq_n = '0' and
cpu_wr_n = '0') else "ZZZZZZZZ";
Т.е. данные будут напрвляться на ОЗУ только в случае если цикл чтения данных
видеогенератором не активен, и процессор затребовал запись (cpu_wr_n = '0')
данных в ОЗУ (cpu_mreq_n = ‘0’). Напомню, что сигналы управления у процессора
Z80 – инверсные, активный «0».
SRAM_nOE <= '0' when (vid_sel = '1') or (cpu_mreq_n = '0' and cpu_rd_n = '0')
else '1';
Чтение данных с ОЗУ активируется или когда активен цикл чтения видеогенератором,
или когда процессор затребовал чтение из памяти. Всё просто и логично.
SRAM_nWE <= '0' when (vid_sel = '0' and cpu_mreq_n = '0' and cpu_wr_n =
'0') else '1';
С записью также нет сложностей, видим, что сигнал записи активен когда не активен
видеогенератор и процессор дал команду записи в память.
cpu_di_bus <= rom_do when (rom_sel = '1' and cpu_mreq_n = '0') else
SRAM_D when (rom_sel = '0' and cpu_mreq_n = '0') else
"11111111";
Здесь мы создали сразу два сигнала. Первый сигнал – это сигнал выборки порта
h'FE из общего адресного пространства. Второй – непосредственно сам порт h'FE.
Реально разрядность у него конечно меньше восьми бит, но компилятор сам уберет
все лишние разряды при синтезе. Третий сигнал – процессорный, «запрос операций с
внешними устройствами». Как всегда, инверсный. Пишем логику работы:
port_fe_sel <= '1' when (cpu_a_bus(7 downto 0) = x"FE" and cpu_iorq_n = '0')
else '0';
port_fe <= cpu_do_bus when (port_fe_sel = '1' and (cpu_wr_n'event and
cpu_wr_n = '0'));
Хлебнув пивка, начинаем разбирать что же тут сильно вумный дядько написал такое.
Начнем с внешнего мультиплексора, сделанного на конструкции if..then..else..end if.
Здесь переключение делается сигналом выборки цикла видеогенератора vid_sel.
Если цикл неактивен, то по else на шину адреса ОЗУ поступают три ноля в старшие
биты и шина адреса с процессора. Шина процессора у нас шестнадцатибитная, а
разрядность ОЗУ – 19 бит. Поэтому нулями мы просто отсекаем неиспользуемую
память. В этом случае процессор спокойно себе общается с ОЗУ и не парится.
Если же видеоцикл активен, то в действие включается второй мультиплексор,
реализованный разнообразия ради на конструкции case…when…end case.
В свою очередь, в этом блоке рулит уже сигнал hcnt(0). Напрягаем свой склероз, и
вспоминаем что я там говорил про сигнал vid_sel. Что он активен, когда три младшие
бита счетчика пикселей hcnt равны по значению цифрам 4 или 5. Вот этот «довесок»
в виде hcnt(0) как раз и определяет конкретно, какая цифра сейчас в этих битах.
Если разряд равен '0' (а значение счетчика – 4), то на шину адреса выплёвывается
сложная конструкция из запутанных битов счетчиков hcnt и vcnt. Этим на шину
выставляется адрес в ОЗУ, где хранится байт изображения.
Если значение равно '1', то на шине – тоже запутка, но уже другая. И теперь
считывается байт атрибутов цвета.
Вообще-то, этот блок руления адресами характерен для создания любого
видеогенератора. Похожие блоки я использовал в «Орионе-2010», «u10_Spetz».
Разница в основном только в сигналах счетчиков.
Давайте разберемся в этих сигналах. Накопав в сети всякоразные доки и
скопипастив нужное, смотрим сюда:
3. Подключаем процессор
Процессор Z80 для ПЛИС реализован в версии на языке VHDL (T80) и на Verilog
(TV80). Воспользуемся первым. Для этой реализации спектрума я выбрал проект Т80 в
творческой обработке syd'a, как самый безглючный.
Копируем в папку проекта speccy папку с софтядром Т80. Подключаем
необходимые модули. Их шесть:
T80s.vhd
T80.vhd
T80_ALU.vhd
T80_MCode.vhd
T80_Pack.vhd
T80_Reg.vhd
component T80s is
generic (
Mode : integer := 0;
T2Write : integer := 1;
IOWait : integer := 1 );
port (
RESET_n : in std_logic;
CLK_n : in std_logic;
WAIT_n : in std_logic;
INT_n : in std_logic;
NMI_n : in std_logic;
BUSRQ_n : in std_logic;
M1_n : out std_logic;
MREQ_n : out std_logic;
IORQ_n : out std_logic;
RD_n : out std_logic;
WR_n : out std_logic;
RFSH_n : out std_logic;
HALT_n : out std_logic;
BUSAK_n : out std_logic;
A : out std_logic_vector(15 downto 0);
DI : in std_logic_vector(7 downto 0);
DO : out std_logic_vector(7 downto 0);
RestorePC_n : in std_logic );
end component;
Z80:T80s
port map (
RESET_n => '1',
CLK_n => clk_cpu,
WAIT_n => '1',
INT_n => cpu_int_n,
NMI_n => '1',
BUSRQ_n => '1',
M1_n => open,
MREQ_n => cpu_mreq_n,
IORQ_n => cpu_iorq_n,
RD_n => cpu_rd_n,
WR_n => cpu_wr_n,
RFSH_n => open,
HALT_n => open,
BUSAK_n => open,
A => cpu_a_bus,
DI => cpu_di_bus,
DO => cpu_do_bus,
RestorePC_n => '1'
);
Все подключенные сигналы нам уже знакомы. Выход (точнее, вход) процессора
RESET_n пока оставляем просто подключенным к ‘1’. Туда же подтягиваем входы
WAIT_n, NMI_n, BUSRQ_n. Поскольку торможения процессора у нас не
предусмотрено, немаскируемое прерывание не применяется, захвата шин внешними
устройствами не предвидится – эти сигналы нам не нужны. Обойдемся без них,
повесив соответствующие входы на единицу. В таком виде процессор будет готов к
работе и стартует сразу после запуска проекта.
Больше относительно подключения процессора мне добавить и нечего. Пускай
это будет самая короткая глава в этом мануале .
И перейдем к следующей, достаточно запутанной главе о реализации
видеовыхода.
Это как раз то, о чем я говорил выше. Видеоданные переписаны в рабочие регистры
изображения, сигнал screen1 дает добро на вывод информации. Неустанно работает
сдвиговый регистр, передающий сигналу vid_dot с каждым тактом по очередному
биту регистра изображения vid_b_reg:
VGA_R2 <= r;
VGA_G2 <= g;
VGA_B2 <= b;
VGA_R1 <= rb;
VGA_G1 <= gb;
VGA_B1 <= bb;
VGA_R0 <= 'Z';
VGA_G0 <= 'Z';
VGA_B0 <= 'Z';
component zxkbd is
port(
clk :in std_logic;
reset :in std_logic;
res_k :out std_logic;
ps2_clk :in std_logic;
ps2_data :in std_logic;
zx_kb_scan :in std_logic_vector(7 downto 0);
zx_kb_out :out std_logic_vector(4 downto 0);
k_joy :out std_logic_vector(4 downto 0);
f :out std_logic_vector(12 downto 1);
num_joy :out std_logic
);
end component;
zxkey: zxkbd
port map(
clk => clock,
reset => '0',
res_k => res_n,
ps2_clk => PS2_KBCLK,
ps2_data => PS2_KBDAT,
zx_kb_scan => kb_a_bus,
zx_kb_out => kb_do_bus,
k_joy => open,
f => open,
num_joy => open
);
Как видим, добавляются новые сигналы. Ну, PS2_KBCLK и PS2_KBDAT у нас уже
зарегистрированы как пины. Их объявлять не нужно.
Сигналы (шины) kb_a_bus и kb_do_bus предназначены для получения модулем
старших адресных линий процессора для сканирования матрицы, и выдачи
пятибитового кода состояния кнопок по запрошенным линиям матрицы.
Выход res_n наконец-то добавит в проект официальный сигнал сброса .
Объявляем сигналы:
cpu_di_bus <= rom_do when (rom_sel = '1' and cpu_mreq_n = '0') else
SRAM_D when (rom_sel = '0' and cpu_mreq_n = '0') else
"111" & kb_do_bus when (port_fe_sel = '1') else
"11111111";
Как видите, ничего хитрого, просто добавил строчку чтения данных из порта FE,
когда процессор к ним обращается.
За сим заканчиваю этот мануал. Надеюсь, он пойдет вам на пользу, или как
минимум – доставит удовольствие от прочтения.
www.zx.pk.ru
2011г.