; Tx Only I2C Output Driver ; 2026.04.04 RSP ; build: pioasm i2c_blind_write.pio i2c_blind_write.pio.h ; ; Designed for use with chips like HT16K33 that don't do clock stretching. ; It also assumes the I2C bus is perfect and nobody ever NAK's. .pio_version 0 // only requires PIO version 0 .program i2c_blind_write // "opt pindirs" makes side set (SCL) affect pindirs instead of pins // "1" allows "side " to be used in instructions, and delay is reduced to 3 bits (max 7) .side_set 1 opt pindirs ; TX Encoding: ; | 31..28 | 27..26 | 25 | 24 | 23..16 | 15..8 | 7..0 | ; | 0 | len 0-2| STOP | START | dataA | dataB | dataC | ; ; Pin mapping: ; - Side-set pin 0 is SCL ; - SET pin 0 is SDA ; - OUT pin 0 is SDA ; - PINDIR pin 0 is SDA do_start: ; I2C start condition set pindirs,1 side 1 [7] ; SCL = 1, SDA = 1 set pindirs,0 side 1 [7] ; SCL = 1, SDA = 0 set pindirs,0 side 0 [7] ; SCL = 0, SDA = 0 jmp do_byte do_stop: ; I2C stop condition set pindirs,0 side 0 [7] ; SCL = 0, SDA = 0 set pindirs,0 side 1 [7] ; SCL = 1, SDA = 0 set pindirs,1 side 1 ; SCL = 1, SDA = 1 public entry_point: .wrap_target ; wait for data ; once started, must keep the fifo full until end of I2C message (e.g. use DMA) ; SCL & SDA are high pull block ; Fifo -> OSR, 32 bits out y, 6 ; unpack length (0-2 = 1-3 data bytes, NA for stop) out x, 1 ; unpack STOP jmp x--, do_stop out x, 1 ; unpack START, leaving 24 bits in OSR jmp x--, do_start ; do 1 to 3 data bytes ; it's not START or STOP, it's just data do_byte: ; set x, 7 ; Loop 8 times bitloop: ; shifting out high bit first ; SCL is low and SDA is ready to be changed out pindirs, 1 side 0 [7] ; OSR -> pindir, 1 pin at a time, then delay 7 clocks nop side 1 [7] ; SCL rising edge, delay 7 clocks, ignore clock stretching ; slave gets data on SCL rising edge ; tx only, ignore receive ; bring SCL back down to allow SDA change nop side 1 [7] ; SCL falling edge jmp x-- bitloop side 0 [7] ; 32 PIO clocks/bit ; ACK pulse set pindirs, 1 side 0 [7] ; SDA high nop side 1 [7] ; SCL rising edge ; tx only, ignore receive nop side 0 [7] ; SCL falling edge nop side 0 [2] ; ignore ack jmp y-- do_byte .wrap % c-sdk { #include "hardware/clocks.h" #include "hardware/gpio.h" static inline void i2c_program_init(PIO pio, uint sm, uint offset, uint freq, uint pin_sda, uint pin_scl) { pio_sm_config c = i2c_blind_write_program_get_default_config(offset); // not using Rx, so make the Tx FIFO deeper // PIO_FIFO_JOIN_NONE (Default: 4 TX, 4 RX) // PIO_FIFO_JOIN_TX (8 TX, 0 RX) // PIO_FIFO_JOIN_RX (0 TX, 8 RX) sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); // IO mapping sm_config_set_out_pins(&c, pin_sda, 1); // OUT instruction will write 1 pin at a time from OSR sm_config_set_set_pins(&c, pin_sda, 1); // SET instruction will write (up to) 5 bit value to SET pins sm_config_set_sideset_pins(&c, pin_scl); // clock pin // OSR setup sm_config_set_out_shift(&c, false, false, 32 ); // shift left, no autopull // allow some testing without external pullups gpio_pull_up(pin_scl); gpio_pull_up(pin_sda); // 32 PIO clocks/I2C cycle float div = (float)clock_get_hz(clk_sys) / (32 * freq); // freq = 100,000 or 400,000 sm_config_set_clkdiv(&c, div); // attach pins to PIO pio_gpio_init(pio, pin_sda); pio_gpio_init(pio, pin_scl); // invert pin outputs gpio_set_oeover(pin_sda, GPIO_OVERRIDE_INVERT); gpio_set_oeover(pin_scl, GPIO_OVERRIDE_INVERT); // make SDA pins inputs for now pio_sm_set_consecutive_pindirs(pio, sm, pin_sda, 1, false); // make all PIO pins 0 so when 1 is written to pindir the pin is pulled down pio_sm_set_pins( pio, sm, 0 ); // all pins attached to PIO set to 0 // Configure and start SM pio_sm_init(pio, sm, offset + i2c_blind_write_offset_entry_point, &c); pio_sm_set_enabled(pio, sm, true); } %}