The recommended method is to use a HAL (Hardware Abstraction Layer) library for your platform. This adds a lot of extra checks besides memory safety. For example if you attempt to use the wrong pin for an unsupported function, such as SPI on a pin that can not do SPI, your program simply won't compile.
While a good design goal for a abstraction layer, I have never seen any of this in real life. Maybe it's in some high-quality projects, but manufacturer solutions for example are much crappier: consider STM32 vendor libraries for example and you would be initializing pin modes using alternative function enumerations with no way of checking whether SPI is available on that pin or not. No such checks are made either compile time or run-time, so you totally can enable the SPI peripheral and configure a wrong pin to a wrong alternative function. No memory safety is added either, it's actually the opposite: while the direct register write has little surface area for the common memory handling errors, STM32 HAL (and many others!) use pointers for both internal state and configuration structs supplied to functions, and such pointers can accidentally point to any type of data at any address. Sure, you need colossally stupid and careless mistakes to mess something that simple up, but claiming there is some added memory safety obviously isn't true.
So usually for your nice claims to hold true you would need to develop your compile-time-checking, memory-safe hardware abstraction layer yourself. Which is then again open for any mistakes within the layer, plus added mistakes in extra housekeeping chores and interfaces. For a large team where many players contribute to the firmware it totally makes sense: put the guy who is most experienced and careful develop the most "unsafe" parts
and design simple interfaces to them as to minimize risk of misuse. Per-project hardware abstractions also have the advantage that they don't need to try to support everything and therefore might be able to replace complex configuration structs and state management by lot simpler function calls.
With a smaller project completely handled by one engineer, probably a solution which accesses UART->DR directly everywhere around the code is, albeit less elegant, and slower to port, probably
more memory-safe than the one which abstract it behind C functions where state and control actions are passed through pointers.
But this is of course matter of implementation. The idea is valid, and it certainly can be implemented correctly. Using a so-called "memory-safe" language if not forces but nudges you in the right direction. I'm just saying that be careful because real-world implementations have pretty poor track record when it comes to hardware abstraction on microcontrollers.