Не так давно я спрашивал о механизме опроса PCI-устройств. После я устроился на работу, доделал тестовое задание, а спрашивал я именно о нем, и благополучно забыл о нем. Но недавно выдали новый проект и пришлось все вспомнить, заодно и решил написать сюда.
Транзакций на шине PCI достаточно много, в данном топике будет описаны только следующие:
- Конфигурационные транзакции
- Транзакции ввода/вывода
- Транзакции обращения к памяти
При осуществлении транзакций возможно 2 варианта:
- Когда ведущим устройством является южный мост
- Когда ведущим устройством является устройство, подключенное к шине PCI
Как уже сложилось, при рассмотрении шины PCI, ведущее устройство я буду называть мастером(Master), ведомое — таргетом (Target).
В данной статье рассматриваются только транзакции, когда мастером является южный мост, так как транзакции, когда мастером является устройство, подключенное к шине PCI заслуживает отдельной статьи.
И так, для работы с шиной, нам понадобятся следующие сигналы:
entity main is
Port (
clk : in std_logic;
AD : inout std_logic_vector(31 downto 0);
IDSEL : in std_logic;
CBE : in std_logic_vector(3 downto 0);
FRAME : in std_logic;
IRDY : in std_logic;
TRDY : inout std_logic;
STOP : inout std_logic;
PAR : inout std_logic;
RST : in std_logic;
DEVSEL : inout std_logic
);
end main;
clk (Clock) — обеспечивает синхронизацию всех транзакций на PCI, а также является входным для каждого PCI — устройства.
AD (Address and Data) — мультиплексирования шина адреса и данных.
IDSEL (Initialization Device Select) — выбор устройства инициализации, используется для выбора кристалла при транзакциях чтения конфигурации и записи.
CBE (Bus Command and Byte Enables) — команды шины и разрешение байта.
FRAME (Frame) — сигнал выдаётся мастером в начале транзакции и определяет её длительность.
IRDY (Initiator Ready) — сигнал готовности мастера. Он свидетельствует о готовности мастера завершить текущую фазу данных.
TRDY (Target Ready) — сигнал готовности таргета, свидетельствующий о готовности таргета завершить текущую фазу данных.
STOP (Stop) — этот сигнал выдаётся таргетом, если он хочет остановить текущую транзакцию.
PAR (Parity) — контроль четности по линиям AD и CBE.
RST(Reset) — cигнал сброса. Является асинхронным.
DEVSEL (Device Select) — сигнал выбора устройства.
Перед началом работы с любым устройством его нужно инициализировать. Поэтому рассмотрим особенности выполнения конфигурационных транзакций.
Конфигурационные транзакции. Общие сведения.
Порт CONFIG_ADDRESS имеет размер двойное слово и доступен только как единое целое. Обращения меньшего размера по принадлежащим ему адресам передаются на шину PCI как обычные транзакции ввода-вывода. Этот порт доступен для чтения и записи и имеет следующий формат:
Когда необходимо выполнить конфигурационную транзакцию, в этот порт записывается адрес регистра конфигурационного пространства PCI, состоящий из номеров шины (разряды 23–16), устройства (15–11), функции (10–8) и собственно регистра (7–2). Биты 1 и 0 должны всегда содержать нули, а старший бит должен содержать единицу, разрешая тем самым выполнение конфигурационной транзакции. Разряды 30–24 зарезервированы и должны содержать нули.
Собственно генерация конфигурационной транзакции происходит при чтении или записи порта CONFIG_DATA, когда в CONFIG_ADDRESS был записан адрес с установленным старшим битом и номером шины, соответствующим шине, подключенной к мосту Host–PCI, или любой шине PCI, лежащей ниже этой шины и соединённой с ней через один или несколько мостов PCI–PCI (допустимый диапазон номеров шин задаётся мосту Host–PCI в процессе его настройки). Доступ к порту CONFIG_DATA должен иметь размер, равный размеру считываемого или записываемого конфигурационного регистра, адрес которого находится в CONFIG_ADDRESS.
Если номер шины, заданный в CONFIG_ADDRESS, совпадает с номером шины, подключённой непосредственно к мосту Host–PCI, генерируется конфигурационная транзакция с адресом типа 0, причём номер устройства, находящийхся в разрядах 15–11 порта CONFIG_ADDRESS, используется для выдачи одного из сигналов IDSEL, которые и служат для выбора конкретного устройства. Кроме того, декодированный номер устройства (один единичный и остальные нулевые биты) в фазе адреса конфигурационной транзакции передаётся в разрядах 31–11 адреса.
Если адрес в CONFIG_ADDRESS указывает не ту шину, которая непосредственно подключена к мосту Host–PCI, последний генерирует конфигурационную транзакцию с адресом типа 1. Она будет обработана мостом PCI–PCI, который опознает содержащийся в адресе номер шины. Этот мост либо выполнит конфигурационную транзакцию с адресом типа 0 (если адресуемое устройство подключено к шине, прямо подсоединённой к этому мосту), либо сгенерирует транзакцию с адресом типа 1, обеспечив тем самым её прохождение через следующий мост. Длина этой цепочки теоретически ограничена только разрядностью поля, отведённого под номер шины (8 бит).
Если при выполнении транзакции выяснится, что адресуемого конфигурационного регистра не существует (указан номер несуществующей шины, устройства, функции или регистра), то операция записи не возымеет никаких действий, а операция чтения вернёт процессору значение, содержащее единицы в каждом разряде
Формат адреса для транзакции типа 1.
Формат адреса для транзакции типа 0.
Формат регистра конфигурации:
Минимальный набор регистров:
- Vendor ID — поле идентифицирует изготовителя устройства. Запрещено использовать значение 0xFFFF.
- Device ID — поле идентифицирует конкретный вид устройства.
- Revision ID — дополнение к идентификатору устройства. Может быть равно нулю.
- Header Type — Для многофункциональных устройств. Если 7ой бит равен 0, то устройство является однофункцальным, иначе — многофункциональное.
- Class Code — доступен только для чтения. Используется для идентификации общего функционального назначения устройства. Старший байт (адрес 0Bh) определяет базовый класс, средний — подкласс, младший — программный интерфейс (если он стандартизован).
- Subsystem ID, Subsystem Vendor ID — задаются производителем. Только для чтения. Хранят идентификаторы, позволяющие точно идентифицировать карты и устройства (в системе могут быть установлены
несколько карт с совпадающими идентификаторами устройства и производителя (Device ID и Vendor ID). - BAR0 — BAR5 — описывают области памяти и портов ввода-вывода.
Для областей памяти и портов описания различаются:
- Бит 0 = 0 — признак памяти. Размером не более 2 Гбайт
- Бит 0 = 1 — признак области портов. Размером до 256 байт.
Размер областей вычисляется следующим образом. В BAR записывается 0xFFFFFFFF. Далее, из BAR считывается значение, и вычитается из 0xFFFFFFFF. Результат и есть размер области. Единица в младшем бите не учитывается.
Общий алгоритм выполнения транзакций
Мастер выставляет на шине AD адрес устройства, на шине CBE выполняемую команду, устанавливает сигнал FRAME в 0 и сигнал IRDY в 0. Далее, мастер ждет от таргета — выставления им сигналов TRDY и DEVSEL. Так же, таргет выставляет на шину AD запрашиваемые данные. Данные считаются валидными, когда FRAME, IRDY и DEVSEL равны уровню логического нуля.
Реализация
Для обращения к выводам ПЛИС потребуются специальные компоненты: буферы ввода/вывода.
Так, для шины AD подключение будет выглядеть следующим образом:
signal AD_I: std_logic_vector (AD'range);
signal AD_O: std_logic_vector (AD'range);
signal AD_T: std_logic;
AD_BUF:
for iCount in AD'low to AD'high generate
begin
IOBUF_AD : IOBUF
generic map
(
DRIVE => 12,
IOSTANDARD => "PCI33_3",
SLEW => "SLOW")
port map (
O => AD_I(iCount),
IO => AD(iCount),
I => AD_O(iCount),
T => AD_T
);
end generate;
Где,
- O — выход буфера.
- IO — вход/выход буфера, непосредственно подключается к выводу ПЛИС.
- I — выход буфера.
- T — управление входом, уровень единицы — вход, уровень нуля — выход.
Для остальных сигналов аналогично, не буду приводить, что бы не загромождать статью.
Как я уже писал выше, при начале транзации, когда на шине AD выставлен адрес, всегда сигнал FRAME равен нулю. Ниже приведен код, который формирует сигнал AdrPhASE, во время действия которого нужно защелкнуть шину адреса и шину команд для последующей работы.
signal AdrPhASE: std_logic;
signal FRAME_D: std_logic;
signal Addres: std_logic_vector(AD_I'range);
signal Command: std_logic_vector(CBE'range);
signal bCfgTr: boolean;
process (clk_I, RST_I) begin
if (RST_I = '1') then
if (rising_edge(clk_I)) then
FRAME_D <= FRAME_I after cTCQ;
end if;
else FRAME_D <= '1' after cTCQ;
end if;
end process;
AdrPhASE <= not FRAME_I and FRAME_D;
process (clk_I, RST_I) begin
if (RST_I = '0') then
Address <= (others => '0') after cTCQ;
Command <= (others => '0') after cTCQ;
bCfgTr <= false after cTCQ;
elsif (rising_edge(clk_I)) then
if (AdrPhASE = '1') then
Address <= AD_I after cTCQ;
Command <= CBE_I after cTCQ;
bCfgTr <= (IDSEL_I = '1') after cTCQ;
end if;
end if;
end process;
Далее, работу всего устройства можно описать с помощью автомата.
type TSM_PCI_T is (sIDLE, sDECODE, sCFG_READ, sCFG_WRITE, sIO_READ, sIO_WRITE, sMEM_READ, sMEM_WRITE);
signal smPCI_T: TSM_PCI_T;
process(clk_I, RST_I) begin
if (RST_I = '0') then
smPCI_T <= sIDLE after cTCQ;
elsif (rising_edge(clk_I)) then
case (smPCI_T) is
when sIDLE => if (AdrPhASE = '1') then smPCI_T <= sDECODE after cTCQ; end if;
when sDECODE => if (bCfgTr and Address(10 downto 8) = b"000" and Command(3 downto 1) = b"101") then
if (Command(0) = '0') then smPCI_T <= sCFG_READ after cTCQ;
else smPCI_T <= sCFG_WRITE after cTCQ; end if;
elsif (Command(3 downto 1)= b"001") and (Addres(31 downto 8) = BAR0(31 downto 8))then
if (Command(0) = '0') then smPCI_T <= sIO_READ after cTCQ;
else smPCI_T <= sIO_WRITE after cTCQ; end if;
elsif (Command(3 downto 1) = b"011") and (Addres(31 downto 16) = BAR1(31 downto 16)) then
if (Command(0) = '0') then smPCI_T <= sMEM_READ after cTCQ;
else smPCI_T <= sMEM_WRITE after cTCQ; end if;
else smPCI_T <= sIDLE after cTCQ;
end if;
when sCFG_READ => if (IRDY_I = '0') then smPCI_T <= sIDLE after cTCQ; end if;
when sCFG_WRITE => if (IRDY_I = '0') then smPCI_T <= sIDLE after cTCQ; end if;
when sIO_WRITE => if (IRDY_I = '0') then smPCI_T <= sIDLE after cTCQ; end if;
when sIO_READ => if (IRDY_I = '0') then smPCI_T <= sIDLE after cTCQ; end if;
when sMEM_READ => if (IRDY_I = '0') then smPCI_T <= sIDLE after cTCQ; end if;
when sMEM_WRITE => if (IRDY_I = '0') then smPCI_T <= sIDLE after cTCQ; end if;
when others => smPCI_T <= sIDLE after cTCQ;
end case;
end if;
end process;
Для понимания выше написанного, приведу возможные команды, передаваемые по шине CBE.
- 0010 I/O Read
- 0011 I/O Write
- 0110 Memory Read
- 0111 Memory Write
- 1010 Configuration Read
- 1011 Configuration Write
Каждой команде соответствует свое состояние автомата. Переход в него зависит от текущего состояния шины CBE и шины AD для транзакций обращения к памяти и портам ввода-вывода. Выход в начальное состояние осуществляется по приходу сигнала IRDY от мастера.
Чтение конфигурации
Как было описано выше, для обработки устройством используются транзакции типа 0. Так как устройство однофункциональное, то номер функции — 000, который проверятся в управляющем автомате. В зависимости от номера регистра (биты 7..0 шины AD) на шину AD выдается нужный регистра, согласно рисунку выше.
signal CfgRData: std_logic_vector(31 downto 0):=x"00000000";
signal CommandReg: std_logic_vector(15 downto 0) := x"0000";
signal StatusReg: std_logic_vector(15 downto 0) := x"0200";
signal LatencyTimer: std_logic_vector(7 downto 0) := x"00";
signal CacheLineSize: std_logic_vector(7 downto 0) := x"00";
signal BAR0: std_logic_vector(31 downto 0) := x"00000001";
signal BAR1: std_logic_vector(31 downto 0) := x"00000000";
signal InterruptLine: std_logic_vector(7 downto 0);
process (clk_I) begin
if (rising_edge(clk_I)) then
case (Address(7 downto 0)) is
when x"00" => CfgRData <= x"00017788" ; --Device ID and Vendor ID
when x"04" => CfgRData <= StatusReg & CommandReg; --Status Register, Command Register
when x"08" => CfgRData <= x"10000001"; -- Class Code and Revision ID
when x"0C" => CfgRData <= x"0000" & LatencyTimer & CacheLineSize; -- BIST, Header Type(bit 7 = 0, single, bits 6-0 = 0, type0), Latency Timer(for masters), Cache Line Size (bit 2 in 1)
when x"10" => CfgRData <= BAR0; -- Base Adress 0 (Register IO address decoder)
when x"14" => CfgRData <= BAR1; -- Base Adress 1
when x"28" => CfgRData <= x"00000000"; -- CarfdBus CIS Pointer
when x"2C" => CfgRData <= x"00017788"; -- Subsystem ID, Subsystem Vendor ID
when x"30" => CfgRData <= x"00000000"; -- Expanxion Rom Base Address
when x"34" => CfgRData <= x"00000000"; -- Reserved, Capabilitis Pointer
when x"38" => CfgRData <= x"00000000"; -- Reserved
when x"3C" => CfgRData <= x"004001" & InterruptLine; -- Max_Lat(only bus master), Min_Gnt, Interrupt Pin, Interrupt Line
when others => CfgRData <= (others => '0');
end case;
end if;
end process;
Так выгядит чтение конфигурации в симуляторе:
Полное изображение
Запись конфигурации
На шине AD выставляет адрес регистра для записи, а в следующем такте выставляются данные, которые нужно записать. В BAR0 биты 7..0 являются read-only, в BAR1 биты 15..0 являются read-only. Поэтому адресов ввода/вывода 256, адресов памяти 4 294 967 296.
process(clk_I, RST_I) begin
if(RST_I = '0')then
CommandReg <= x"0000" after cTCQ;
StatusReg <= x"0200" after cTCQ;
LatencyTimer <= x"00" after cTCQ;
CacheLineSize <= x"00" after cTCQ;
BAR0 <= x"00000001" after cTCQ;
BAR1 <= x"00000000" after cTCQ;
elsif(rising_edge(clk_I)) then
if (smPCI_T = sCFG_WRITE) then
case(Address(7 downto 0)) is
when x"04" => if (CBE_I(1) = '0') then CommandReg(15 downto 8) <= AD_I(15 downto 8) after cTCQ; end if;
if (CBE_I(0) = '0') then CommandReg(7 downto 0) <= AD_I(7 downto 0) after cTCQ; end if;
when x"0C" => if (CBE_I(1) = '0') then LatencyTimer <= AD_I(15 downto 8) after cTCQ; end if;
if (CBE_I(0) = '0') then CacheLineSize <= AD_I(7 downto 0) after cTCQ; end if;
when x"10" => if (CBE_I(3) = '0') then BAR0(31 downto 24) <= AD_I(31 downto 24) after cTCQ; end if;
if (CBE_I(2) = '0') then BAR0(23 downto 16) <= AD_I(23 downto 16) after cTCQ; end if;
if (CBE_I(1) = '0') then BAR0(15 downto 8) <= AD_I(15 downto 8) after cTCQ; end if;
when x"14" => if (CBE_I(3) = '0') then BAR1(31 downto 24) <= AD_I(31 downto 24) after cTCQ; end if;
if (CBE_I(2) = '0') then BAR1(23 downto 16) <= AD_I(23 downto 16) after cTCQ; end if;
when x"3C" => if (CBE_I(0) = '0') then InterruptLine <= AD_I(7 downto 0) after cTCQ; end if;
when others => null;
end case;
end if;
end if;
end process;
Запись в порт
На шине AD выставляется номер регистра для записи, в следующем такте выставляются данные, которые нужно записать.
Приведем пример только для записи одного регистра, остальные записываются аналогично.
signal IOReg0: std_logic_vector (31 downto 0);
process(clk_I, RST_I) begin
if(RST_I = '0') then
IOReg0 <= x"00000000" after cTCQ;
elsif (rising_edge(clk_I)) then
if (smPCI_T = sIO_WRITE and Address(7 downto 0) = x"00") then
if (CBE_I(0) = '0') then IOReg0( 7 downto 0) <= AD_I( 7 downto 0) after cTCQ; end if;
if (CBE_I(1) = '0') then IOReg0(15 downto 8) <= AD_I(15 downto 8) after cTCQ; end if;
if (CBE_I(2) = '0') then IOReg0(23 downto 16) <= AD_I(23 downto 16) after cTCQ; end if;
if (CBE_I(3) = '0') then IOReg0(31 downto 24) <= AD_I(31 downto 24) after cTCQ; end if;
end if;
end if;
end process;
Чтение порта
На шине AD выставляется номер регистра, который нужно прочитать. Затем устройство выдает на шину AD запрашиваемые данные.
signal IORDate: std_logic_vector (31 downto 0);
process (clk_I, RST_I) begin
if (RST_I = '0') then
IORDate <= x"00000000";
elsif (rising_edge(clk_I)) then
case (Address(7 downto 0)) is
when x"00" => IORDate <= IOReg0 after cTCQ;
when x"04" => IORDate <= IOReg1 after cTCQ;
when x"08" => IORDate <= IOReg2 after cTCQ;
when x"0C" => IORDate <= IOReg3 after cTCQ;
when x"10" => IORDate <= IOReg4 after cTCQ;
when x"14" => IORDate <= IOReg5 after cTCQ;
when x"18" => IORDate <= IOReg6 after cTCQ;
when x"1C" => IORDate <= IOReg7 after cTCQ;
when x"20" => IORDate <= IOReg8 after cTCQ;
when x"24" => IORDate <= IOReg9 after cTCQ;
when others => IORDate <= (others => '0');
end case;
end if;
end process;
Так выглядит запись и чтение порта ввода-вывода:
Полное изображение
Запись и чтение памяти
На шине AD выставляется адрес, по которому нужно записать данные, а в следующем такте сами данные. При чтении на шину AD выставляет адрес для чтения, затем на шину AD таргет выставляет сами данные.
Данные пишуется в RAM в порт А, читаются из порта B.
signal RamWrEn: std_logic;
signal RamOutputDate: std_logic_vector (31 downto 0);
signal RamInputDate: std_logic_vector (31 downto 0);
signal RamRst: std_logic := '0';
RAMB16_S36_S36_inst : RAMB16_S36_S36
port map (
DOA => open, -- Port A 32-bit Data Output
DOB => RamOutputDate, -- Port B 32-bit Data Output
DOPA => open, -- Port A 4-bit Parity Output
DOPB => open, -- Port B 4-bit Parity Output
ADDRA => Address(8 downto 0), -- Port A 9-bit Address Input
ADDRB => Address(8 downto 0), -- Port B 9-bit Address Input
CLKA => clk_I, -- Port A Clock
CLKB => clk_I, -- Port B Clock
DIA => RamInputDate, -- Port A 32-bit Data Input
DIB => x"00000000", -- Port B 32-bit Data Input
DIPA => x"0", -- Port A 4-bit parity Input
DIPB => x"0", -- Port-B 4-bit parity Input
ENA => '1', -- Port A RAM Enable Input
ENB => '1', -- PortB RAM Enable Input
SSRA => '0', -- Port A Synchronous Set/Reset Input
SSRB => '0', -- Port B Synchronous Set/Reset Input
WEA => RamWrEn, -- Port A Write Enable Input
WEB => '0' -- Port B Write Enable Input
);
process(clk_I) begin
if (rising_edge(clk_I)) then
if (RST_I = '1') then
RamRst <= '0';
else
RamRst <= '1';
end if;
end if;
end process;
process(clk_I, RST_I) begin
if(RST_I = '0') then
RamInputDate <= (others => '0') after cTCQ;
RamWrEn <= '0' after cTCQ;
elsif (rising_edge(clk_I)) then
if (smPCI_T = sMEM_WRITE) then
if (CBE_I(0) = '0') then RamInputDate(7 downto 0) <= AD_I( 7 downto 0) after cTCQ; end if;
if (CBE_I(1) = '0') then RamInputDate(15 downto 8) <= AD_I(15 downto 8) after cTCQ; end if;
if (CBE_I(2) = '0') then RamInputDate(23 downto 16) <= AD_I(23 downto 16) after cTCQ; end if;
if (CBE_I(3) = '0') then RamInputDate(31 downto 24) <= AD_I(31 downto 24) after cTCQ; end if;
RamWrEn <= '1' after cTCQ;
else
RamWrEn <= '0' after cTCQ;
end if;
end if;
end process;
Так выглядит запись и чтение памяти в симуляторе:
Полное изображение
Данные на шину AD выводятся следующим образом. В зависимости от состояния автомата, к выходному буферу подключается соответствующий регистр.
process (clk_I, RST_I) begin
if (RST_I = '0') then
AD_O <= (others => '0') after cTCQ;
elsif (rising_edge(clk_I)) then
if (smPCI_T = sCFG_READ) then
AD_O <= CfgRData after cTCQ;
elsif (smPCI_T = sIO_READ) then
AD_O <= IORDate after cTCQ;
elsif (smPCI_T = sMEM_READ) then
AD_O <= RamOutputDate after cTCQ;
end if;
end if;
end process;
Сигнал разрешения выдачи данных на шину AD формируется следующим образом:
process (clk_I, RST_I) begin
if (RST_I = '0') then
AD_T <= '1' after cTCQ;
elsif (rising_edge(clk_I)) then
AD_T <= not b2l(smPCI_T = sCFG_READ or smPCI_T = sIO_READ or smPCI_T = sMEM_READ) after cTCQ;
end if;
end process;
Заключение
В заключение хочу сказать, что выполнение транзакций на шине PCI не так сложно как кажется. Разработанная прошивка была залита в ПЛИС. Плата с ПЛИС вставлена в PCI слот и был включен компьютер. Система нашла плату и запросила драйвера на нее.
Работает! :)
Сам проект rghost.ru/46686218. Открывать xilinx ise 14.2.
Автор: Fenja