Programmable IO¶
The Programmable IO (PIO) IP Core is a versatile and highly configurable module designed for generating and controlling low-speed digital interfaces. It features an internal state machine that executes a user-defined program stored in on-chip memory to drive IO signals in precise sequences. This allows the IP core to support a wide range of protocols and custom interfaces by bit-banging signals according to the programmed commands, without requiring continuous CPU intervention.
Program Memory¶
Unlike a streaming FIFO approach, the PIO stores its program in an internal memory. Software writes instructions sequentially into the program memory via the bus interface. A write pointer tracks the next free slot, and an execution pointer tracks the currently executing instruction. Execution begins when the enable flag is set, which also resets both the execution pointer and the loop counter to zero.
This separation of loading and execution means software can fill the entire program buffer before starting the state machine, removing any real-time loading pressure.
To replace a running program, disable the controller, issue a program reset (which resets the write pointer), write the new instructions, then re-enable.
State Machine¶
The state machine reads instructions from program memory and executes them in sequence. The following commands are supported:
Single-pin commands use the pin field to select the target IO pin:
Signal High (HIGH)
Sets the selected pin to a high logic level (output, driven high).
Command value: 0x0
Signal Low (LOW)
Sets the selected pin to a low logic level (output, driven low).
Command value: 0x2
Float (FLOAT)
Sets the selected pin to a high-impedance (input) state.
Command value: 0x4
Toggle (TOGGLE)
Toggles the direction of the selected pin (output ↔ input).
Command value: 0x6
Wait (WAIT)
Holds execution for a number of clock-divider ticks specified in the
datafield.Command value: 0x8
Wait for High (WAIT_FOR_HIGH)
Stalls execution until the selected pin reads a high level.
Command value: 0x9
Wait for Low (WAIT_FOR_LOW)
Stalls execution until the selected pin reads a low level.
Command value: 0xA
Read (READ)
Sets the selected pin to input mode, waits for
readDelayValueclock-divider ticks (configured globally via the read delay register) to allow the line to settle, then samples the pin and pushes the result into the read FIFO.Stalls until the read FIFO accepts the result (backpressure).
The
datafield of a READ instruction is ignored.Command value: 0xB
Multi-pin commands use the data field as a bitmask — each bit corresponds to one IO pin:
Signal High Set (HIGH_SET)
Sets all pins indicated by the
databitmask to output high.Command value: 0x1
Signal Low Set (LOW_SET)
Sets all pins indicated by the
databitmask to output low.Command value: 0x3
Float Set (FLOAT_SET)
Sets all pins indicated by the
databitmask to high-impedance (input).Command value: 0x5
Toggle Set (TOGGLE_SET)
Toggles the direction of all pins indicated by the
databitmask.Command value: 0x7
Program flow commands:
Loop (LOOP)
Controls program repetition.
data = 0: jumps back to instruction 0 indefinitely (endless loop).data = N: runs the program N times in total, then advances past the LOOP instruction and asserts theloopDoneinterrupt.If the
stopAtLoopcontrol bit is set when the LOOP instruction is reached, the state machine always advances past it regardless ofdata, allowing software to gracefully terminate an endless loop. See thestopAtLoopregister field.Command value: 0xC
Command Encoding¶
Each instruction is a 32-bit word with the following layout:
Bit 3 - 0 : command (4-bit enum, see command values above)
Bit P - 4 : pin (log2Up(io.width) bits, 0 for multi-pin commands)
Bit 31 - Q : data (dataWidth bits, bitmask for _SET commands, count for WAIT/READ/LOOP)
where P = 3 + log2Up(io.width), Q = P + 1
Usage¶
Example: Blink pin 0 forever
// 1. Stop and reset the program memory
WRITE32(PIO_ENABLE, 0);
WRITE32(PIO_STATUS, 1); // any write triggers programReset
// 2. Set clock divider
WRITE32(PIO_CLK_DIV, 99); // tick every 100 cycles
// 3. Load program
WRITE32(PIO_TX, CMD(HIGH, pin=0, data=0));
WRITE32(PIO_TX, CMD(WAIT, pin=0, data=500));
WRITE32(PIO_TX, CMD(LOW, pin=0, data=0));
WRITE32(PIO_TX, CMD(WAIT, pin=0, data=500));
WRITE32(PIO_TX, CMD(LOOP, pin=0, data=0)); // endless
// 4. Enable IRQ mask and start
WRITE32(PIO_IRQ_MASK, 0x2); // unmask loopDone (optional for endless)
WRITE32(PIO_ENABLE, 1); // rising edge resets execPtr and loopCounter
Example: Run 3 times, receive loopDone interrupt, reload
WRITE32(PIO_ENABLE, 0);
WRITE32(PIO_STATUS, 1);
WRITE32(PIO_TX, CMD(HIGH, 0, 0));
WRITE32(PIO_TX, CMD(WAIT, 0, 100));
WRITE32(PIO_TX, CMD(LOW, 0, 0));
WRITE32(PIO_TX, CMD(WAIT, 0, 100));
WRITE32(PIO_TX, CMD(LOOP, 0, 3)); // run 3 times, then loopDone
WRITE32(PIO_IRQ_MASK, 0x2);
WRITE32(PIO_ENABLE, 1);
// In the ISR when loopDone fires:
WRITE32(PIO_ENABLE, 0);
WRITE32(PIO_STATUS, 1); // reset write pointer
// ... write new program ...
WRITE32(PIO_ENABLE, 1); // re-arm execPtr via rising edge
Example: gracefully stop an endless loop and reload
// Program is running an endless LOOP — stopping with stopAtLoop:
// 1. Set stopAtLoop (bit 1). The state machine completes the current
// iteration and advances past the LOOP instruction on the next pass.
WRITE32(PIO_ENABLE, READ32(PIO_ENABLE) | 0x2);
// 2. Poll until execPtr == writePtr (state machine has idled).
while ((READ32(PIO_STATUS) & 0xFF) != ((READ32(PIO_STATUS) >> 8) & 0xFF));
// 3. Clear stopAtLoop, then reload.
WRITE32(PIO_ENABLE, READ32(PIO_ENABLE) & ~0x2);
WRITE32(PIO_ENABLE, 0); // disable
WRITE32(PIO_STATUS, 1); // reset write pointer
// ... write new program ...
WRITE32(PIO_ENABLE, 1); // re-arm and start
Applications¶
The flexibility of the Programmable IO IP Core makes it suitable for a variety of applications, including but not limited to:
Custom serial communication protocols
Precise GPIO signal sequencing without CPU polling
Interface with legacy hardware with specific timing requirements
Prototyping new digital communication standards
Configuration¶
Available bus architectures:
APB3
Wishbone
AvalonMM
By default, all buses are defined with 12 bit address and 32 bit data width.
Parameter¶
Pio.Parameter defines the IO pins of the programmable IO controller.
Name |
Type |
Description |
Default |
|---|---|---|---|
Width |
Int |
Number of IO pins. Must be greater then 0. |
PioCtrl.InitParameter defines the initialization values for certain registers.
Name |
Type |
Description |
Default |
|---|---|---|---|
clockDivider |
Int |
Initialization value of the internal clock divider |
0 |
readDelay |
Int |
Initialization value of the register which delays read actions |
0 |
Note
Parameters in InitParameter with a value of 0 are treated as disabled. This allows
selectively initializing only certain registers.
PioCtrl.MemoryMappedParameter defines the program memory and read FIFO depths.
Name |
Type |
Description |
Default |
|---|---|---|---|
commandFifoDepth |
Int |
Depth of the program memory (maximum number of instructions). |
16 |
readFifoDepth |
Int |
Depth of the read result FIFO. |
8 |
PioCtrl.PermissionParameter defines the permission rules for bus access.
Name |
Type |
Description |
Default |
|---|---|---|---|
busCanWriteClockDividerConfig |
Boolean |
Toggles bus write access to the clock divider. |
PioCtrl.Parameter configures the programmable IO controller. It uses Pio.Parameter as parameter.
Name |
Type |
Description |
Default |
|---|---|---|---|
io |
Pio.Parameter |
Class with IO parameters |
|
readBufferDepth |
Int |
Number of registers on the input path to stabilize read values. Disabled when 0. |
2 |
init |
PioCtrl.InitParameter |
Class to parametrize the initialization values. |
InitParameter.disabled |
permission |
PioCtrl.PermissionParameter |
Class to set bus access. |
PermissionParameter.granted |
memory |
PioCtrl.MemoryMappedParameter |
Class to define program memory and read FIFO depth. |
MemoryMappedParameter.default |
dataWidth |
Int |
Width of the data field in each instruction word. |
24 |
clockDividerWidth |
Int |
Width of the clock divider counter. |
20 |
readDelayWidth |
Int |
Width of the read delay counter. |
8 |
interrupt |
Boolean |
Enables the interrupt controller with loopDone and RX data sources. |
true |
error |
Boolean |
Enables the error controller with a read FIFO overflow source. |
true |
PioCtrl.Parameter has some functions with pre-defined parameters for common use-cases.
object Parameter {
def default(pins: Int = 1) =
Parameter(Pio.Parameter(pins))
def light(pins: Int = 1) =
Parameter(Pio.Parameter(pins), memory = MemoryMappedParameter.lightweight, dataWidth = 16)
}
Register Mapping¶
IP Identification:
The register map starts with an IP Identification block to provide all information about the underlying IP core to software drivers. This allows to provide backwards compatible drivers.
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x000 |
31 - 24 |
API |
0x0 |
Rx |
API version of the implemented IP Identification. |
23 - 16 |
Length |
0x8 |
Rx |
Length of the IP Identification block in Bytes. |
|
15 - 0 |
ID |
0x1 |
Rx |
IP value of this IP core. |
|
0x004 |
31 - 24 |
Major Version |
0x1 |
Rx |
Major number if this IP core. Version schema is major.minor.patch. |
23 - 16 |
Minor Version |
0x1 |
Rx |
Minor number if this IP core. Version schema is major.minor.patch. |
|
15 - 0 |
Patch Version |
0x0 |
Rx |
Patch number if this IP core. Version schema is major.minor.patch. |
Self Disclosure:
This block discloses information about the IP core to software drivers to simplify them.
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x008 |
31 - 24 |
readBufferDepth |
Rx |
Number of registers on the input path to stabilize read values. |
|
23 - 16 |
clockDividerWidth |
Rx |
Width of the clock divider counter. |
||
15 - 8 |
dataWidth |
Rx |
Width of the data field in each instruction word. |
||
7 - 0 |
IO width |
Rx |
Number of IO pins. |
||
0x00C |
15 - 8 |
readFifoDepth |
Rx |
Depth of the read result FIFO. |
|
7 - 0 |
commandFifoDepth |
Rx |
Depth of the program memory (maximum number of instructions). |
||
0x010 |
0 |
busCanWriteClockDividerConfig |
Rx |
Flag whether the clock divider register is bus-writable. |
Control:
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x014 |
1 |
stopAtLoop |
0 |
RW |
When set, the next LOOP instruction encountered advances the execution pointer past the LOOP instruction instead of jumping back, regardless of the loop count. Once the execution pointer reaches the write pointer the state machine idles. Clear this bit after the program has stopped. Used by software to gracefully terminate an endless loop. |
0 |
enable |
0 |
RW |
Enables program execution. A low-to-high transition resets the execution pointer and loop counter to zero. |
Program Memory and Read FIFO:
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x018 |
16 |
valid |
Rx |
Set when the read FIFO returned a valid result. Must be checked before consuming the payload. |
|
15 - 0 |
readResult |
Rx |
Result sampled by the last READ command, consumed from the read FIFO on each read. |
||
0x018 |
31 - 0 |
programWrite |
xW |
Writes one instruction word into program memory at the current write pointer position and advances the write pointer. |
|
0x01C |
31 - 24 |
rxOccupancy |
Rx |
Number of unread results currently in the read FIFO. |
|
15 - 8 |
writePtr |
Rx |
Current program write pointer (next free instruction slot). |
||
7 - 0 |
execPtr |
Rx |
Current execution pointer (instruction being executed). |
||
0x01C |
31 - 0 |
programReset |
xW |
Any write resets the write pointer to zero, allowing the program memory to be overwritten. |
Clock Divider and Read Delay:
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x020 |
clockDividerWidth - 0 |
clockDividerValue |
Depends on PioCtrl.InitParameter |
RW or Rx |
Value for the clock divider counter. The divider produces one tick every |
0x024 |
readDelayWidth - 0 |
readDelayValue |
Depends on PioCtrl.InitParameter |
RW |
Number of clock-divider ticks to wait after switching a pin to input mode before sampling it during a READ command. Applies globally to all READ instructions. Set to 0 to sample immediately. |
Error Controller:
Present when PioCtrl.Parameter.error = true. Follows the standard interrupt controller
pattern: writing a 1 to a pending bit clears it; the mask register enables individual
error sources.
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x028 |
0 |
readFifoOverflow pending |
0 |
RW |
Sticky flag set when a READ result is dropped because the read FIFO is full.
Write |
0x02C |
0 |
readFifoOverflow mask |
0 |
RW |
Enables the read FIFO overflow error source. |
Interrupt Controller:
Present when PioCtrl.Parameter.interrupt = true. Same pattern as the error controller.
Address |
Bit |
Field |
Default |
Permission |
Description |
|---|---|---|---|---|---|
0x030 |
1 |
loopDone pending |
0 |
RW |
Sticky flag set when a LOOP N command finishes all N iterations. Write |
0 |
rxData pending |
0 |
RW |
Sticky flag set when the read FIFO contains at least one result. Write |
|
0x034 |
1 |
loopDone mask |
0 |
RW |
Enables the loopDone interrupt source. |
0 |
rxData mask |
0 |
RW |
Enables the RX data available interrupt source. |