But what if you want to share a GPIO pin because it has two functions depending on the mode of operation? Think about using a pin to configure and FPGA which after configuration gets a different function.
The way embedded-hal is structured is to use typestate programming to ensure that there is always a maximum of one owner of each peripheral / register. The language itself then guarantees that each peripheral/register can only be (mutably) held by one execution flow at the same time, through the borrow checker. It doesn't directly speak to how you then use these singletons in your own code. If you need to 'share' pins among tasks there are a few ways you can accomplish it. You can use a barrier mechanism like a Mutex<RefCell>, though this should generally only be necessary if you need them in interrupt context. You can continue in the spirit of standard Rust programming and the borrow checker, for example in a main loop, each sub-task can borrow the peripherals from main, and implicitly return them when it is done. This pattern is very common in standard Rust as well, as you also need to contend with the borrow checker for your shared state on desktop too (though you can make use of 'heavy' std library solutions like Arc there).
Or, as others have mentioned, you can use a lightweight framework to help with handling shared state, such as rtic, which for most embedded work satisfies the common requirements - but we are stacking more on top of Rust itself to get there.
Mutex and ownership seem to perform the same function.
Mutexes are runtime guards. Locking them is blocking and they consume resources and can lead to deadlocks. Ownership is guaranteed by static analysis at compile-time, so it's free at runtime. So yeah, you could build a Rust embedded project that encapsulates all the peripherals in Mutex guards and it'd accomplish roughly the same thing, but it would be quite inefficient. Really in any program, embedded or not, if you're using mutexes when you don't have concurrency, you're doing it wrong. Without the borrow checker you wouldn't really want to force access through Mutexes, due to their cost and risks, but if you don't force it, it means unsafe access is possible. Borrow checker guarantees zero-cost safety for the normal case, and you can still use a mutex (or other concurrent primitives like atomic operations) where you need concurrent access to state.
How do you define "race condition" here? Such details matter. It is impossible to "simultaneously" write a pin in a single-core microcontroller from two places. Writing a pin is always a correct and allowed operation. The only question is, is it functionally correct?
This is not generally true, at least with the abstraction we are using here. Some microcontrollers do have atomic access at the pin level, but many others do not and require read-modify-write semantics that can be interrupted. It is also not always clear/guaranteed that even if it is a single instruction to write to GPIO, that it is in fact an *atomic* instruction. Without a guarantee, you can guess but can't prove who wins if an interrupt fires in the middle of a GPIO write and performs a conflicting write; most modern cores are pipelined at least if not superscalar, and peripherals are connected via a bus that may need arbitration etc. etc..
So, you write this in Rust. The interrupt gets invoked, your code tries to take ownership of the pin, but it cannot because a task at one of the lower IRQLs had the ownership of the pin when the critical interrupt was invoked. Boom!
Extra care is definitely required when you need preemptive access to peripherals. This is a special case though, and generally the exact sort of thing you *want* to fail (preferably at compile time) and require extra consideration, because even in C this design is fraught with concurrency pitfalls. I think you could accomplish this with critical sections to guard access. In Rust though this idea of a higher priority access to the peripheral 'working by default' just isn't a thing, and you can't bypass the borrow checker. So you wouldn't be able to compile the implementation you describe in the first place. It is safe by default because it just won't let you assume that it works. You'd need to explicitly create some mechanism to share access to this peripheral between the two threads, and that means taking care of concurrency. This does require a different way of thinking about this kind of situation, but it forces you to be aware of it and consider the details, which I don't think is a bad thing.
What I'm after is resource locking (by process). Not resource sharing!
There is not much that borrow checker can prove about concurrent tasks, so this is a lot to ask of it. In general, for runtime locking you'll need to use runtime primitives like Mutexes to guard access to multiple references to the same resource. It might be an option to pass ownership when the task is first spawned, but this depends on how your task concurrency is working, I don't *think* this is possible in rtic but I haven't looked into it.