💾 Archived View for tozip.chickenkiller.com › 2022-07-08-lockless.gmi captured on 2023-06-16 at 16:12:39. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2022-07-16)

-=-=-=-=-=-=-

A lockless buffering technique for Microcontrollers

Created 2022-07-08

I had a little sniff around Rust the other day. What interested me is RTIC, a real-time interrupt-driven concurrency framework for Arm Cortex-M microcontrollers. I'm not a Rust programmer, but it sounded pretty cool.

I had previously written a buffering DAC in C. The idea is that a Raspberry Pi is a master that produces audio and sends data via SPI to an STM32 MCU (microcontroller). The MCU buffers the data, transmitting it at a fixed rate. It sends a block/unblock signal to the Pi when it is ready to receive data.

Why? Well, the Raspbian isn't a real-time OS. Timing is not reliable, not by a longshot. The kernel interrupts processes as it feels fit. Sound will be terrible. I know that there are audio libraries available, but they are kinda complicated. So I had this idea that the Pi could produce sound as fast as it wanted and send it to my MCU. So long as it blocked when it was told to, there shouldn't be any problems.

I did a quick review of my project to see how I coped with concurrency issues. I implemented a circular buffer. There are a few variables that must be kept consistent. If there is a pre-emptive interrupt, then data might twist into an inconsistent state. And nobody wants that.

My program consisted of two interrupts: a SPI interrupt that produced data for the buffer from the Pi, and a timer interrupt that consumed data from the buffer and set the DAC. Now here's the "clever" part: the interrupts had the same priority, so neither could pre-empt the other and produce corrupted state.

Is setting the interrupts to the same priority a good idea (which is the default anyway)? Well, I think it's an OK design decision. Which one ought I to give priority to anyway? They are both time-critical, and both interrupts had damn-well better do their processing fast. Or at least "fast enough". One cannot of course ramp up throughput indefinitely without something buckling under the strain. A sample rate of 44.1kHz seemed to work fine. I don't think one needs more than that realistically.

I had recently learned of the Arm instructions LDREX and STREX, which are ways of atomically updating memory in two separate steps. From what I understand you can read a register value, update it, then store it. An interrupt may have invalidated the register, but STREX knows this, and you can create a simple loop. I further understand that this is likely to be the fastest way of updating memory, as you don't need to have mutexes, semaphores or worry about memory barriers.

Although I didn't need it for my DAC project, it is worth bearing in mind. I'm not sure how well the instructions work with C. It probably will not mitigate problems that need to adjust several values. Maybe you could hybridise the variables into a 32-bit word in many instances.

DACs and noise

For my DAC project, I used an STM32L432KC, which has an onboard DAC. It produces lovely output. I wish more microcontrollers had DACs. I tried using external DACs, and even PWM, but I think I found that they produced a certain amount of noise. You tended to notice it when the sound output was quiet, or produced a sine wave. It was unfuriating. Once you notice it you can't help not noticing it. If you did something like play a regular song then you didn't notice any problems. Presumably the rapidly-changing volume masked the noise. It is entirely possible that my program had some kind of bug.

Using the L432's DAC, though, I was more than happy. I seemed to have problems with I2C, though. Even using STM's IDE, which was unpexected. A hardware problem, perhaps? I resorted to using bit-banging in a couple of instances, so at least I had a work-around.

Rust ain't easy

I'll probably continue my investigations with Rust, as I really like the look of their RTIC library. Nothing in Rust seems particularly easy. Even trying to get other people's programs to compile was a headache. I think that Rust programmers soak up a lot of implicit knowledge that newbies don't know about. I'm not talking about the intricacies of the borrow checker, I'm talking about just setting things up in the first place. I think putting Rust in the Linux kernel would be a huge mistake. I really don't understand Torvald's attitude on this. It may work at first, but I predict it will become a nightmare. But then I'm not a genius like Linus.

Just my 2¢.