Hacker News new | ask | show | jobs
by jsmith45 1400 days ago
Ouch. Making you treat hardware registers as owned (or use unsafe to access them) feels like it would be unpleasant if writing single threaded firmware, where the only preemption is the interrupt handler.

I’d much rather have them exposed as some form of atomic that is restricted to operations that are atomic on this specific hardware.

3 comments

Unsafe is exactly what they are in C. They're essentially volatile memory that can change at any time, and woe befall anyone messing around with the same register without synchronization when using an RTOS.

Good HAL crates don't have the entire register set as one struct, thank goodness. And for something like printing a message to serial in a panic handler, there are still unsafe options to yank control of registers, same as C.

What I really like about some of the HAL crates is the usage of builder patterns while configuring a peripheral. Setting up timer parameters before starting it by design is chef’s kiss wonderful.

But if you want to put the whole peripheral set in a static global, you can do that with a RefCell<Mutex<T>> or something similar. Or just yolo it and use unsafe blocks to ditch the mutex.

Atomic ops only address a small part of the problem.* Sure, your internal queue pointers will be sane, and two-stage register settings won't be interrupted. But locks are very fine-grained, and aren't going to help you keep co-ordinated in-the-large. In fact, you could argue that each mutex is a warning about muddled coordination. "Who's going to be changing this queue?" "Everybody!" "Better put a lock around it."

Yes, it's all trade-offs. You aren't going to avoid all shared responsibility, and will always need micro-coordination. But safe Rust forces you to assign ownership to every structure, at every scale. Some similar effects to opaque data structures, or actors. But unlike those techniques, it allows you to dynamically transfer responsibility.

The most obvious issue is use-after-"free", use-before-"allocate". "Free" and "allocate" don't just refer to malloc--it's any situation where you pinky-swear you aren't going to be touching something.

But more problematic is this kind of code is easy, even natural, to write in C:

- Task A is waiting for a state changes on resources Q1 and Q2. When it sees a particular set, it will modify resource Q3's state.

- Task B is firing off events to Q1 and handling error responses. It might need to stop Q1.

- Since Q1 is no longer changing state, what happens to Q3? Who's responsible for keeping stuff straight? What's "stuff"? Do we need to worry about Q2?

Rust is going to very strongly push you to explicitly designate what owns and is responsible for Q1, Q2, Q3, at all times. "B's got Q1, so A can't even look at it. I'm @#!* going to have to make A tell B what it wants and let B handle it." That was painful, but a good thing.

* Note to self: Write a blog post titled "Atomics won't save you now!" Start a band named "Useless Atomics".

Being owned and unsafe feels like the most natural fit. If multiple threads (or the interrupt handler) manipulate a register while you're already working on it could leave you in a very undefined state you have no idea about. In this case, rust makes you promise that you checked that this will be entirely safe.

You could also write wrappers, for example one that automatically turns off interrupt when you do a write operation or gives you a guard that lets you write and read the register, which turns off interrupts until the guard is dropped. Or one that has a lock or uses atomics. Plenty of options you can use here.

On ARM v7m the "correct" way to would be to raise the current thread's BASEPRI the the highest exception priority you have to lock up. The M0+ cores on the RP2040 only support v6m and require masking those interrupts, but since M0+ cores are limited to the 16 internal exceptions and at most 32 external interrupts the code sequence to temporarily mask those interrupts isn't much longer. The annoying downside is that it forces a tighter coupling on programmers. The RP2040 specifically may offer a cleaner solution if you can afford to dedicate one of the hardware locks available in the single-cycle I/O block to each conflict that requires resolution. Such a solution should even work for both ARM cores.