5
votes
What programming/technical projects have you been working on?
This is a recurring post to discuss programming or other technical projects that we've been working on. Tell us about one of your recent projects, either at work or personal projects. What's interesting about it? Are you having trouble with anything?
The Two Trees of Valinor, silver Telperion and golden Laurelin, provided light to the Undying Lands in a regular 12-hour light cycle, switching between silver light and golden light.
A few months ago, I wrote a very small library that maps real-world time onto this cycle, returning the brightnesses of the trees at any given time as a pair of
0.0 <= n <= 1.0
scaling floats, according to rather simple curves. I also included a rudimentary color struct so that it could turn those scalars into RGB. Last month, I obtained an LED light strip, and set to work writing a driver to run it off the GPIO on my Raspberry Pi.It has been...a journey.
WS2812B dictates that a data signal has a period in the vicinity of 1.25μs, with pulses as short as 0.4μs. My first attempt was to simply toggle the GPIO that quickly in software, but I soon found out that, using Rust, I could only generate a signal with a period of around 10μs...So I went all-in and learned a bit of ARM assembly to build and link in via
build.rs
.First, I wrote my waiting spin-loop; then I ran it with a value of 2000000000, five times, and measured the runtime with
time
. Finding that each loop averaged ~3ns (at 1GHz, 3 cycles per loop, surprisingly nice), I set that as a constant to divide the delay values to find the iteration counts. This worked...sort of. Raspbian was rudely swapping me out and making my actual delays utterly unpredictable; the colors were usually generally correct, but shifted bits were making them the wrong brightness or making them bleed into each other. Breaking more new territory, I even compiled a custom kernel for the first time, in order to get realtime scheduling, but even that was not enough to keep that 3ns consistent.My third attempt was to modify my assembly spin-loop to read the output level of GPIO18, which I would set running with the PWM hardware as a bootleg clock, and for which the output value is conveniently just a few registers down in the same memory block I already had mapped. I have a variant of the loop that watches for changes in the level, and a variant that watches for only the rising edge; and by painstaking and tedious experimentation, I zeroed in on a set of values that keep the signal remarkably close to stable, even with being swapped out. This is currently my best-working version, and it is almost perfect, so long as I only run the update once every few seconds. Its stability is even affected by how much debug info I print before and after each update.
For the past few days, I have been trying to comprehend the process of using DMA to inject the data directly into the hardware PWM output, which, if it works, should be perfect. However, pretty much all of the example code is in C, and I do not know C very well, which is something of an obstacle.
I just wanted some holy trees, man.
UPDATE: I have perfected the driver. The DMA+PWM approach was a dead end, so I finally surrendered and used the SPI hack that seems to be the go-to method. The library was written for a different version of the WS28XX protocol, so the timing values were just barely wrong, but fortunately, it includes a function that assumes the data you give it is already encoded for SPI; all I had to do was write a new encoder using my own timing constants, and now I can essentially use that library as an easy preconfigured gate into the SPI device.
A quick stress test with all sleeps removed showed that my full pipeline can output upwards of 2400 strip refreshes per second, with no flickering. I now possess the light of a Silmaril.
I've been working on a multiplayer board game website, https://birdga.me, where you can just share a link and easily have a networked tabletop game with physics. I've basically finished the MVP and I have the code on Github. The frontend is written with Three.JS/ES6, and the backend is written in Rust + Warp.
It's been a really great learning experience, because I didn't really know much about netcode + websockets. But now that I've spent a lot of time working on it there's some design decisions that I might have to rethink: