Сегодня мы рассмотрим самый интересный блок в новых контроллерах от Raspberry Pi Foundation RP2040 - PIO или programmable input/output block. В теории он должен позволить создавать модули кастомный периферии с чем сегодня мы попробуем разобраться.
Для начала я предлагаю разобраться с тем что такое модуль PIO и из чего он состоит. Собственно внутри нашего контроллера доступны 2 модуля PIO каждый из которых состоит из 4х конечных автоматов (SM - State Machine), 4х 2-направленных FIFO буферов, памяти инструкций на 32 машинной инструкции, матрицы коммутации выходов (GPIO) и контроллера прерываний.
Собственно работает это так: сначала мы пишем программу в память инструкций, после чего запускаем один или несколько конечных автоматов которые начинают исполнять инструкции из памяти. Сам же конечный автомат может забирать или класть данные в буфер, читать и выставлять пины GPIO, а так-же вызывать прерывания.
Конечный автомат (SM) содержит в себе набор регистров:
- PC - Счетчик инструкций, работает точно так-же как и в любом другом процессоре.
- X, Y - Регистры общего назначения, можно хранить временные значения.
- OSR - (Output shift register) регистр в которой пишутся данные приходящие из FIFO буфера.
- ISR - (Input shift register) регистр из которого данные будут записаны в FIFO буфер.
С помощью SM мы можем реализовать 2 потока данных, один из FIFO к GPIO и другой идущий наоборот. В FIFO данные пишутся как в обычный регистр находящийся в адресном пространстве контроллера (так-же возможно использование DMA), после чего мы можем забрать эти данные в выходной сдвиговый регистр (OSR). Далее из OSR данные вытесняются на GPIO, причем мы можем управлять количеством вытесняемых данных.
Таким образом SM позволят нам предать данные из параллельной 32х битной шины контроллера в кастомный последовательный интерфейс. Так-же можно читать данные с GPIO, писать их во входной сдвиговый регистр (ISR) после чего писать в буфер и уже потом забирать из fifo с помощью ядра контроллера.
SM взаимодействует с GPIO весьма специфическим образом. Для начала необходимо выставить направление работы пинов. Для этого необходимо выбрать некоторый базовый пин относительно которого будет выставляться направление работы других пинов. Например: выставляем GPIO 20 как базовый и задаём что следующие 3 пина за ним будут OUT (теперь мы можем менять направление этих GPIO с помощью pioasm, GPIO 21 это PINDIR или PINS 1). Тоже самое можно сделать и для IN GPIO (IN GPIO mapping). Для чтения или записи, а также для изменения направления работы в ASM команде необходимо использовать номер относительно заданного ранее базового. Это все нужно из-за того что поле для указания этого адреса 3х битное, а значит мы не можем обращаться более чем к 5ти пинам относительно нашего базиса. Есть еще один способ менять состояние пинов, при помощи sideset(об этом ниже).
Все эти перемещения можно реализовать с помощью следующих 9 16-битных инструкций:
OUT
Выталкивает определенное количество бит (bit_count) из OSR в destination.
- PINS позволяет записать определенное количество бит в GPIO из OSR в соответствии с OUT мапингом выходных пинов.
- PINDIRS позволяет поменять направление работы пинов которые подключены к SM как выходные.
- EXEC позволяет выполнить инструкцию из OSR следом за инструкцией OUT.
- PC изменит значение в счетчике инструкций.
IN
Пишет определенное количество бит (bit_count) из источника source в ISR.
- PINS позволяет считать определенное количество бит из GPIO в ISR согласно IN мапингу входных пинов.
- NULL запишет нужное количество нулей в ISR.
PULL
Выталкивает содержимое выходного FIFO в OSR. Ожидает, если выходной FIFO пуст.
- IfEmpty если выставлен, то будет заблокирован пока буфер не будет заполнен до выставленного ранее придела.
- block если выставлен, то исполнение заблокируется пока в FIFO не будет данных.
- noblock если в FIFO нет данных просто запишет содержимое X в OSR, эквивалент MOV OSR, X.
- По умолчанию IfEmpty == 0 и Block == 1.
PUSH
Проталкивает содержимое ISR во входной FIFO и очищает ISR. Ожидает, если входной FIFO заполнен.
- IfFull если выставлен, то будет заблокирован пока количество смещений не дойдет до выставленного ранее придела.
- block если выставлен, то исполнение заблокируется пока в FIFO заполнен.
- По умолчанию IfFull == 0 и Block == 1.
MOV
Перемещение данных из source в destination.
- EXEC сработает аналогично OUT EXEC.
- PC работает аналогично безусловному JMP.
- STATUS вернет все 1 или все 0 в зависимости от заполненности FIFO.
- ! или ~ позволит инвертировать биты источника перед записью в приемник.
- :: позволит поменять направление бит (LE -> BE или наоборот).
JMP
Условный или безусловный переход к другой инструкции.
- PIN позволяет сделать условный переход по состоянию пина, данный пин должен быть выставлен при настройке SM.
- !OSRE проверяет больше (или ровно) ли количество сдвинутых PULL бит, чем выставленный рубеж.
- X--, Y-- позволяет сделать переход если регистр больше нуля, а так-же вычитает единицу из значения регистра.
IRQ
Вызывает прерывание (для одного из ядер или SM).
- IQR 4-7 доступны только SM, IRQ 0-3 могут быть перенаправлены на внешнии IRQ0_INTE и IRQ1_INTE.
- set, nowait - устанавливают флаг прерываний без ожидания, это дефолтное поведение.
- wait - установит флаг и заблокирует исполнение пока он не будет сброшен.
- clear - сбросит флаг.
WAIT
Блокировка программы до определенного события (изменение GPIO или прерывание).
- gpio ждет определенного состояния любого пина, по его номеру.
- pin ждет определенного состояния пина согласно мапингу входных пинов.
- iqr ждет выставления, или сброса флага прерывания внутри PIO модуля.
SET
Пишет value в destination.
- value может иметь значения от 0-31.
Так-же каждая команда позволяет выставить либо задержку либо сайд пины. Однако 2 эти фичи делать между собой область в 5 бит в каждой команды, а это значит что возможно либо выставлять все 5 пинов, но тогда задержки работать не будут, либо можем выставлять максимальную задержку в 31 (0b11111) такт, но тогда выставить значения пинов будет невозможно, или например использовать 2 пина, и тогда максимальная задержка будет 7 (0b111) тактов. Делается это следующим образом:
set x 7 side 0b10 [4]
Выставит 1 на первый side пин, и 0 на второй. Так-же исполнение будет остановлено на 4 такта после исполнения команды. т.е. в итоге данная команда займет 5 тактов, однако сайд пины будут выставлены только по окончанию первого такта.
Также PIO ASM поддерживает несколько директив:
.define ( PUBLIC ) <symbol> <value> - дефайн как в СИ.
.program <name> - название программы, необходимо для ссылок из СИ кода.
.origin <offset> - позволяет загрузить программу с некоторым офсетом в память.
.side_set <count> (opt) (pindirs) - позволяет указать количество side пинов.
.wrap_target - вместе с .wrap позволяют сделать бесконечный цикл без использования машинных инструкций. Начало бесконечного цикла.
.wrap - Конец бесконечного цикла.
После того как рассмотрена работа PIO модуля и синтаксис ASM, можно переходить к практике. Однако для начала нужно рассмотреть процесс конфигурации SM без которой просто не удастся запустить код.
Для начала
int freq = 115200 * 2;
float div = clock_get_hz(clk_sys) / freq;
// SM TX
uint offset = pio_add_program(pio, &uart_tx_program);
pio_sm_set_consecutive_pindirs(pio, sm_tx, pin_tx, 1, true);
pio_gpio_init(pio, pin_tx);
pio_sm_config c = uart_tx_program_get_default_config(offset);
sm_config_set_out_pins(&c, pin_tx, 1);
sm_config_set_sideset_pins(&c, pin_tx);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm_tx, offset, &c);
pio_sm_set_enabled(pio, sm_tx, true);