Close to the metal doesn’t mean “I can create an operation that I could do in assembly, and it compiles to that assembly”. It doesn’t necessarily mean “fast”, either. If that were the case all compiled and optimised native languages would count. We aren’t talking about integer addition here - we’re talking about interaction with hardware.
The general issue with safe rust in these circumstances is exactly what makes it safe in the first place - it wants to be firmly in the driving seat. Want to access memory? You can only do so if it is being “managed” by rust (an abuse of terms, really, as that management is done at compile time). But with device drivers, there’s a pretty good chance you’ll want to access memory at a certain fixed address, managed outside of your program, and there’s a pretty good chance that it is volatile.
Safe rust has already slammed on the brakes, exited the driver’s seat and is running in the rain just to get away from the thought of such things. Unsafe rust could easily manage it, otherwise embedded rust wouldn’t exist.
For MMIO loads and stores you want intrinsics which do an MMIO load / store, which is exactly what Rust provides. The fact this is an "address" in "memory" couldn't matter less, it's not really memory, that's just for the convenience of the CPU so that we don't need two extra instructions (and I believe ironically on some embedded CPUs we do have special instructions and we do need to use them for IO to get the desired behaviour, so Rust ends up making that case easier...)
What C does here instead is a type system hack named "volatile" types. Let's label the type so that we know it's not really memory, but then we'll allow all the same operations as for regular types even though that doesn't make much sense. Ideally programmers avoid using any of the resulting nonsense operations, in practice they mostly seem to just hope it magically works, after all the C looks like it should work...
The general issue with safe rust in these circumstances is exactly what makes it safe in the first place - it wants to be firmly in the driving seat. Want to access memory? You can only do so if it is being “managed” by rust (an abuse of terms, really, as that management is done at compile time). But with device drivers, there’s a pretty good chance you’ll want to access memory at a certain fixed address, managed outside of your program, and there’s a pretty good chance that it is volatile.
Safe rust has already slammed on the brakes, exited the driver’s seat and is running in the rain just to get away from the thought of such things. Unsafe rust could easily manage it, otherwise embedded rust wouldn’t exist.