Wish List: Dynamic load of user PIO scripts

This was fueled by a user’s request to generate a 12MHz square wave output…

At 12MHz, the only most one reliable option would be a PIO program. I’ve dug into the PIO assembly and byte code in the past … so it got me thinking.

Could the buspirate allow dynamically adjusting and loading a (restricted) PIO program to control the plank outputs?


Overly-long details

The PIO byte code is surprisingly easy to parse. However, it would need to be restricted to a subset of instructions (at least initially), or else it’d easily mess up operation of the device.


Initial version restrictions

  • Restrict GPIO access in the incoming program to between 0…7

  • Restrict features (at least initially) to exclude…

    • IRQs
    • side-set other than pins 0…7
    • write to a pin other than 0…7
    • read to a pin other than 0…7
    • etc.
  • Generally Permitted Interaction:

    • Delay / side-set - all variations OK (Must map within GPIO 0…7)
    • JMP - address must be within program bounds
    • WAIT
      • source of 0b00 – Allowed (abs. GPIO)
      • source of 0b01 – Allowed (rel. GPIO)
      • source of 0b10REJECTED (PIO IRQ, clears IRQ)
      • source of 0b11 – Allowed (JMP PIN)
    • IN - All variations permitted except reserved sources 0b100, 0b101
    • OUT
      • destination of 0b000 (PINS) - ? see 11.5.6
      • destination of 0b001 (X) - Allowed
      • destination of 0b010 (Y) - Allowed
      • destination of 0b011 (NULL) - Allowed
      • destination of 0b100 (PINDIRS) - ? see 11.5.6
      • destination of 0b101 (PC) - REJECTED
      • destination of 0b110 (ISR) - Allowed
      • destination of 0b111 (EXEC) - REJECTED
    • PUSH- All variations permitted (verify all fixed bits)
    • PULL- All variations permitted (verify all fixed bits)
    • MOV to RX- All variations permitted (verify all fixed bits)
    • MOV from RX- All variations permitted (verify all fixed bits)
    • MOV Dest/Src
      * destination of 0b000 (PINS) - ? see 11.5.6
      * destination of 0b001 (X) - Allowed
      * destination of 0b010 (Y) - Allowed
      * destination of 0b011 (PINDIRS) - REJECTED
      * destination of 0b100 (EXEC) - Allowed
      * destination of 0b101 (PC) - REJECTED
      * destination of 0b110 (ISR) - Allowed
      * destination of 0b111 (OSR) - Allowed
      * operation - All non-reserved variations OK
      * source of 0b000 (PINS) - Allowed?
      * source of 0b001 (X) - Allowed
      * source of 0b010 (Y) - Allowed
      * source of 0b011 (NULL) - Allowed
      * source of 0b100 (reserved) - REJECTED
      * source of 0b101 (Status) - Allowed
      * source of 0b110 (ISR) - Allowed
      * source of 0b111 (OSR) - Allowed
    • IRQ - REJECTED
    • SET - Destination, Data
      • destination of 0b000 (PINS) - ? see 11.5.6
      • destination of 0b001 (X) - Allowed
      • destination of 0b010 (Y) - Allowed
      • destination of 0b011 (reserved) - REJECTED
      • destination of 0b100 (PINDIRS) - ? see 11.5.6
      • destination of 0b101 (reserved) - REJECTED
      • destination of 0b110 (reserved) - REJECTED
      • destination of 0b111 (reserved) - REJECTED

WARNING! This presumes the eight plank outputs are contiguously numbered GPIO.


OUT/SET/SIDE-SET restrictions

To allow OUT, SET, and SIDE-SET, the following restrictions will ensure the PIO only affects the eight plank output pins:

  • OUTPINCTRL_OUT_COUNT will default to 8 (range 8…1), and PINCTRL_OUT_BASE will default to the GPIO for plank pin 0. (range +0…7). If PINCTRL_OUT_BASE is increased, PINCTRL_OUT_COUNT will be decreased accordingly.
  • SETPINCTRL_SET_COUNT will default to8 (range 8…1) and PINCTRL_SET_BASE will default to the GPIO for plank pin 0 (range +0…7). If PINCTRL_SET_BASE is increased, PINCTRL_SET_COUNT will be decreased accordingly.
  • SIDE-SETPINCTRL_SIDESET_COUNT will default to 0 (range 0…5 / 0…4 if optional) and PINCTRL_SIDESET_BASE will default to the GPIO for plank pin 0 (range +0…7). PINCTRL_SIDESET_COUNT + PINCTRL_SIDESET_BASE shall be enforced to fit within the GPIO used by the plank outputs.


Those hidden details would be sufficient to support a large subset of the PIO commands, certainly sufficient for simple programs such as a 12MHz clock output on some (or all) of the plank outputs…

Not in my short-term queue, but putting the details out early, as enabling a “safer” PIO for classroom experimentation might be of interest to some folks…?

What would be the limitation of using one of the PWM counter units of the MCU in comparison to PIO?

Updated to say “one” instead of “the only most” (itself a typo).

PWM module is likely better for a single output, interrupts after N cycles, or learning with a hardware focus.

PIO is likely better for simultaneous control of many outputs (32 pins), dynamic updates via OSR, or (maybe) when needing to synchronize two or more fractional clocks.

1 Like

The initial users request for square wave output sounded to me just like one output was required. That caused me to only think of one. But with just 8 IO pins the BP could also use the PWM units to output different square waves or pwm duty cycles on all 8 pins simultaneously.

But you are right, when you want more complex and synchronized outputs, then user controlled PIO would offer more options.

Open-drain output with switching the direction-pin of the 1T45 synchronously could also be an interesting option.

Loading PIO code dynamically isn’t particularly difficult. We already customize instructions on the fly for bit twiddling in 2 and 3wire modes.

For a bare bones demo, a simple PIO program can just execute instructions as they are pushed into the FIFO.

The issue I foresee is how to configure everything.


#define hw2wire_wrap_target 6
#define hw2wire_wrap 11
#define hw2wire_pio_version 0

#define hw2wire_offset_entry_point 6u

static const uint16_t hw2wire_program_instructions[] = {
    0xf027, //  0: set    x, 7            side 0
    0x6701, //  1: out    pins, 1                [7]
    0xbe42, //  2: nop                    side 1 [6]
    0x4701, //  3: in     pins, 1                [7]
    0x1741, //  4: jmp    x--, 1          side 0 [7]
    0xa0c3, //  5: mov    isr, null
            //     .wrap_target
    0x6026, //  6: out    x, 6
    0x6042, //  7: out    y, 2
    0x0020, //  8: jmp    !x, 0
    0x6060, //  9: out    null, 32
    0x60f0, // 10: out    exec, 16
    0x004a, // 11: jmp    x--, 10
            //     .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program hw2wire_program = {
    .instructions = hw2wire_program_instructions,
    .length = 12,
    .origin = -1,
    .pio_version = hw2wire_pio_version,
#if PICO_PIO_VERSION > 0
    .used_gpio_ranges = 0x0
#endif
};

static inline pio_sm_config hw2wire_program_get_default_config(uint offset) {
    pio_sm_config c = pio_get_default_sm_config();
    sm_config_set_wrap(&c, offset + hw2wire_wrap_target, offset + hw2wire_wrap);
    sm_config_set_sideset(&c, 2, true, false);
    return c;
}

This is the assembled output of the 2wire mode. The instruction are easy to deal with, but the 4 defines, pio_program struct, and get_default_config will need to be packed into a data structure as well. A moderate annoyance, but doable.

static inline void hw2wire_program_init(PIO pio, uint sm, uint offset, uint pin_sda, uint pin_scl, uint buf_sda, uint buf_scl, uint32_t freq) {
    assert(pin_scl == pin_sda + 1); //wait uses pin ordered inputs, need to be consecutive
    //assert(buf_scl == buf_sda + 1); //doesn't use wait, don't think they need to be consecutive
    pio_sm_config c = hw2wire_program_get_default_config(offset);
    // IO mapping
    sm_config_set_out_pins(&c, buf_sda, 1);
    sm_config_set_set_pins(&c, buf_sda, 1);
    sm_config_set_in_pins(&c, pin_sda);
    sm_config_set_sideset_pins(&c, buf_scl);
    sm_config_set_jmp_pin(&c, pin_sda);
    sm_config_set_out_shift(&c, false, true, 16);
    sm_config_set_in_shift(&c, false, true, 8);
    //with delays, there are 32 instructions per bit IO
    //we should maybe reduce this to have more accuracy around 1MHz
	float div = clock_get_hz(clk_sys) / (32 * 1000 * (float)freq); 
	sm_config_set_clkdiv(&c, div);
    // Try to avoid glitching the bus while connecting the IOs. Get things set
    // up so that pin is driven down when PIO asserts OE low, and pulled up
    // otherwise.
    #if !BP_HW_RP2350_E9_BUG
    // RP2350 has defective pull-downs (bug E9) that latch up
    // RP2350 boards have extra large external pull-downs to compensate
    // RP2040 has working pull-downs
    // Don't enable pin pull-downs on RP2350
    gpio_pull_down(pin_scl); //we pull down so we can output 0 when the buffer is an output without manipulating the actual scl/sda pin directions
    gpio_pull_down(pin_sda);
    #endif
    uint32_t pin_pins = (1u << pin_sda) | (1u << pin_scl);
    uint32_t buf_pins = (1u << buf_sda) | (1u << buf_scl);
    //io pins to inputs
    pio_sm_set_pindirs_with_mask(pio, sm, 0, pin_pins); //read pins to input (0, mask)    
    //buffer pins to outputs and initial states
    //always confirm the GPIO pin is an input/off before messing with the buffer
    pio_sm_set_pindirs_with_mask(pio, sm, buf_pins, buf_pins); //buf pins to output (pins, mask)    
    pio_sm_set_pins_with_mask(pio, sm, 0, buf_pins); //buf dir to 0, buffer input/HiZ on the bus
    pio_gpio_init(pio, buf_sda);
    gpio_set_outover(buf_sda, GPIO_OVERRIDE_INVERT);
    pio_gpio_init(pio, buf_scl);
    gpio_set_outover(buf_scl, GPIO_OVERRIDE_INVERT);  
    pio_sm_set_pins_with_mask(pio, sm, buf_pins, buf_pins);
    // Clear IRQ flag before starting, and make sure flag doesn't actually
    // assert a system-level interrupt (we're using it as a status flag)
    pio_set_irq0_source_enabled(pio, (enum pio_interrupt_source) ((uint) pis_interrupt0 + sm), false);
    pio_set_irq1_source_enabled(pio, (enum pio_interrupt_source) ((uint) pis_interrupt0 + sm), false);
    pio_interrupt_clear(pio, sm);
    // Configure and start SM
    pio_sm_init(pio, sm, offset + hw2wire_offset_entry_point, &c);
    pio_sm_set_enabled(pio, sm, true);
}

Then each program has a user created init function to configure the state machine and attach all the pins and stuff. This is the tricky bit I imagine. Either parse the C and all possible PIO config functions (and other stuff) on the Bus Pirate, or “pre compile” it on a PC with crapware.

If the desire is to have a boat load of PIO programs available with some minimal configuration options (pins, speed), that could be implemented via a command or a specific PIO playground mode.

Yes, you are right.

My scenario is a classroom environment.

The primary risk to mitigate are the PIO programs accidentally messing with core BP functions, such as the voltage regulation, display, etc.

Thus, the difficulty is not in generally loading PIO dynamically, but in finding a way to provide a (safer) environment to play around with PIO. In my collapsed details, I tried to enumerate a subset of permitted instructions (and corresponding configuration) that would be “safe” for use in such a classroom-style setting.

e.g., the pio could run, could read any pin, could wait on any pin, but could only modify the plank output pins’ direction / value.

I don’t expect this to be on the immediate-term plan. I do expect someone will eventually think, “For a STEM lesson, I wonder if we could use PIO…”. Followed quickly by, “… without folks accidentally destroying stuff”. When that happens, this thread might be relevant. :slight_smile:

1 Like

As for how to go from the PIO program to a parseable set of options,
that’s already solved:

 ./pioasm -o json ../../src/pirate/apa102.pio ./apa102.json

:mage:

1 Like