PulseTrain
One bitbanger to rule them all...
What do these things have in common?
The Silicon Labs workhorse EFM8 microcontroller, used in SPIDriver and I2CDriver
The WS2812B RGB LED, better known as the Adafruit NeoPixel
The CH32V003 RISC-V microcontroller, famously priced at about 10¢
DHT11 / DHT22 / AM2302 temperature-humidity sensors
Dallas / Maxim 1-Wire devices, like DS18B20
They’re all configured with a 1- or 2-wire protocol that requires timed pulses. Because pulse shape and line turnaround are timing-critical, they require custom low-level code on a microcontroller to implement the protocol.
Here’s an example I was looking at recently. It implements the debug protocol for the CH32V003, in AVR assembly. It’s technical, fiddly stuff. I’ve written my share of this kind of thing over the years.
But all these protocols aren’t so different, really. They all specify pulse high and low times, within a certain margin. For instance here’s Adafruit’s documentation on the NeoPixel 1-wire protocol.
Imagine you could write some CircuitPython that did this:
import pulsetrain
pt = pulsetrain.PulseTrain(board.GP0, 2_500_000) # each tick is 0.4 us
code0 = "HLL"
code1 = "HHL"
pt.drive("H") # line high by default
pt.drive(code0 * 24 + "H") # set neopixel to black, 24 zeroes
pt.drive(code1 * 24 + "H") # set neopixel to white, 24 onesYou wouldn’t need to count cycles; pulseTrain takes care of the bit timing. Each call to pt.drive() guarantees a precise pulse train. The control string is made of single characters for line operations, and integers for pauses.
Lline lowHline highztristate busiread inputn delay n ticks
Some examples, rendered with the excellent wavedrom. A single pulse
A repeating 7-tick square wave with a 4:3 duty cycle
Two single cycle high pulses separated by 8 low ticks.
So much for output. Input uses the “i” code, which samples one bit from the line. The library packs these bits into bytes which can be read out with pt.read(). So a protocol that sends a pulse then expects a transmitted bit (like the microcontroller 1-wire debug schemes), we have:
H L z iWhich outputs a high/low pulse, then tristates the line and reads a bit.
Currently the implementation runs on RP2040 CircuitPython. It only supports a single I/O line right now, but it seems not too difficult to get it running with 2 or more lines. It doesn’t have any external dependencies. It’s in https://github.com/jamesbowman/rp2040-circuitpython-pulsetrain. There are a couple of examples there. example-neopixel.py drives the neopixel strip in the photo above. example-ch32v.py is a simple but complete programmer for the CH32V003 microcontroller.
The implementation uses RP2040’s PIO block, and was, ah, interesting. PIO is hard, and I’m feeling that PulseTrain is an easier way to get results. The library is a work-in-progress right now, but I’ll be adding some more features soon, including multi-signal support.
Thanks for reading.









Or, in MicroPython, you could use the existing machine.bitstream:
https://docs.micropython.org/en/latest/library/machine.html#machine.bitstream
The Neopixel implementation is built on top of bitstream.
How does this
pt = pulsetrain.PulseTrain(board.GP0, 2_500_000) # each tick is 0.4 us
get from 2_500_000 to 0.4 usecs?
I must be missing something obvious.