This project provides a Rust RAL (register access layer) for all STM32 microcontrollers.
The underlying data is generated via the patched SVD files in stm32-rs.
Documentation · Repository · Supported Devices · Example Project
stm32ral is an experiment into a lightweight register access layer. It provides access to every register, and provides constants which define the fields and possible field values in those registers. In that sense it is a lot like C device header files. However, it also provides a couple of simple macros that permit very easy register access, with very simple generated code which is efficient even without optimisations enabled.
The main aims are simplicity, compactness, and completeness. You get a module structure that contains a struct for each peripheral comprising just its registers in order, and you get a lot of constants for field widths, positions, and possible values. There's not much else, so it takes little disk and builds very quickly. It covers all registers of all STM32 devices, including core Cortex-M peripherals, and aims to include full enumerated values for each field as soon as possible.
Please consider trying it out and contributing or leaving feedback!
use stm32ral::{read_reg, write_reg, modify_reg, reset_reg};
use stm32ral::{rcc, gpio};
// For safe access we have to first `take()` the peripheral instance.
// This only returns Some(Instance) if that instance is not already
// taken; otherwise it returns None. This ensures that no other code can be
// simultaneously accessing the peripheral, which could lead to a race
// condition. There's `release()` to return it. See below for unsafe use.
let gpioa = gpio::GPIOA::take().unwrap();
let rcc = rcc::RCC::take().unwrap();
// Field-level read/modify/write, with either named values or just literals.
// Most of your code will look like this.
modify_reg!(rcc, rcc, AHB1ENR, GPIOAEN: Enabled);
modify_reg!(gpio, gpioa, MODER, MODER1: Input, MODER2: Output, MODER3: Input);
while read_reg!(gpio, gpioa, IDR, IDR3 == High) {
let pa1 = read_reg!(gpio, gpioa, IDR, IDR1);
modify_reg!(gpio, gpioa, ODR, ODR2: pa1);
}
// You can also reset whole registers or specific fields
reset_reg!(gpio, gpioa, GPIOA, MODER, MODER13, MODER14, MODER15);
reset_reg!(gpio, gpioa, GPIOA, MODER);
// Whole-register read/modify/write.
// Rarely used but nice to have the option.
let port = read_reg!(gpio, gpioa, IDR);
write_reg!(gpio, gpioa, ODR, 0x12345678);
modify_reg!(gpio, gpioa, MODER, |r| r | (0b10 << 4));
// Or forego the macros and just use the constants yourself.
// The macros above just expand to these forms for you, bringing
// the relevant constants into scope. Nothing else is going on.
let pa1 = (gpioa.IDR.read() & gpio::IDR::IDR1::mask) >> gpio::IDR::IDR1::offset;
gpioa.ODR.write(gpio::ODR::ODR2::RW::Output << gpio::ODR::ODR2::offset);
// Once you're done with a peripheral, you can release it so it is available
// to `take()` again. You can't use `gpioa` after this line.
gpio::GPIOA::release(gpioa);
// For unsafe access, you don't need to first call `take()`, just use `GPIOA`:
unsafe { modify_reg!(gpio, GPIOA, MODER, MODER1: Output) };
// With the `nosync` feature set, this is the only way to access registers.
See the example project for a more complete example that should build out of the box.
- Small and lightweight (~30MB total file size, ~2MB compressed)
- Simple (just 4 macros and a lot of constants)
- Quick to compile (~2s build time)
- Covers all STM32 devices in one crate
- Supports
cortex-m-rt
via thert
feature, including interrupts - Supports
cortex-m-rtic
via thertic
feature, exposing adevice
with all peripherals taken - Doesn't get in your way
- A bit like what you're used to from C header files
- Still experimental, might have breaking changes to API design
- Won't keep you warm burning CPU time
- A bit like what you're used to from C header files
- svd2rust is the obvious choice for generating Cortex-M device crates from SVD files, and provided inspiration for this project
- stm32-rs provides
svd2rust
crates for all STM32 devices supported by this crate, and uses the same underlying patched SVD files - TockOS has a nice looking
API for register access using their
svd2regs
tool. - Bobbin also has some good looking ideas for register
access (see
bobbin-dsl
).
In your Cargo.toml
:
[dependencies.stm32ral]
version = "0.4.0"
features = ["stm32f405"]
Replace stm32f405
with the required chip name. See
Supported Devices for the full list.
Then, in your code:
#[macro_use]
extern crate stm32ral;
let gpioa = stm32ral::gpio::GPIOA::take().unwrap();
modify_reg!(stm32ral::gpio, gpioa, MODER, MODER1: Input, MODER2: Output, MODER3: Input);
inline-asm
: enablesinline-asm
on thecortex_m
dependency. Recommended if you're using a nightly compiler that supports it.rt
: enablesdevice
on thecortex_m_rt
dependency, and provides the relevant interrupt linker scripts. Recommended for most users, but you can leave it off if you want to handle interrupts yourself.doc
: makes all devices visible in the output without using any of them at the top level. Ideal for generating documentation. Not useful for actually building code.nosync
: disables all synchronised access (take()
/release()
functions). The only way to access registers is with the directunsafe
access, such aswrite_reg!(stm32ral::gpio, GPIOA, MODER, MODER1: Input)
. Removes all associated synchronisation overhead, but of course the user must ensure they do not cause race conditions. "C" mode. Especially useful if enabled by a HAL crate which will perform its own synchronisation but can still permit unsafe direct access to peripherals by users (which is why this is a 'negative' feature).rtfm
: adds aPeripherals
struct to each device module which contains aInstance
for each device peripheral, and has asteal()
method to unsafely create it while taking all the peripherals; this feature adds compatibility for using stm32ral as a cortex-m-rtfm device crate. Ifnosync
is also enabled, thePeripherals
struct will be empty and have an emptysteal()
method, retaining compatibility (but only direct unsafe access to peripherals is possible).- CPU features like
armv7em
: brings in peripherals from the CPU core itself, the relevant one is automatically included by the device features. - Device features: one per supported device, for example,
stm32f405
. You should enable precisely one of these.
At the top level of stm32ral
, there is a module for each supported family
of devices, such as stm32ral::stm32f4
. Inside each family are modules for
each supported device, such as stm32ral::stm32f4::stm32f405
. When you
specify a device feature, everything inside that module is re-exported
at the top level, so that for example stm32ral::stm32f4::stm32f405::gpio
is also accessible as stm32ral::gpio
. This means for many devices you can
simply change which feature you build stm32ral
with, and not have to
change any of the code that uses it (since the paths will remain the same).
Inside each device module there is a module for each peripheral, such as
stm32f405::gpio
. Inside each peripheral module is a module for each register,
such as stm32f405::gpio::MODER
, and inside each register module is a module
for each field, such as stm32f405::gpio::MODER::MODER15
.
Inside each field is a mask
and offset
constant which define the bitmask
for that field and its bit offset within the register. Each field also
contains an R
, W
, and RW
module, which contain values which may be read,
written, or read+written from this module (mapping to enumeratedValues
from the SVD).
An example so far:
// Equivalent to stm32ral crate root with `--features stm32f405`
pub mod stm32f4 {
pub mod stm32f405 {
pub mod gpio {
pub mod MODER {
pub mod MODER15 {
pub const offset: u32 = 30;
pub const mask: u32 = 0b11 << offset;
pub mod R {}
pub mod W {}
pub mod RW {
pub const Input: u32 = 0b00;
pub const Output: u32 = 0b01;
pub const Alternate: u32 = 0b10;
pub const Analog: u32 = 0b11;
}
}
}
}
}
}
pub use stm32f4::stm32f405::*;
Next there is the RegisterBlock
, a struct which contains the registers
for all instances of this peripheral. Each register is one of RWRegister
,
RORegister
, WORegister
, or the Unsafe*
variants thereof. These provide
.read()
and .write(value)
methods.
// Inside a peripheral module such as `stm32ral::stm32f4::stm32f405::gpio`
pub struct RegisterBlock {
pub MODER: RWRegister<u32>,
pub OTYPER: RWRegister<u32>,
// ...
}
Then there is the ResetValues
struct, which has an integer field for each
register in the RegisterBlock
. Each instance of the peripheral will include
one ResetValues
appropriately initialised, so you can:
// In reality, you'd use reset_reg!(gpio, gpioa, GPIOA, MODER);
gpioa.MODER.write(stm32ral::gpio::GPIOA::reset.MODER);
There is an Instance
struct which represents a value you can own and move
around and give out references to, which Derefs to a RegisterBlock
to actually access the registers. There's only one Instance
for each
peripheral instance; you can get it using take()
and return it for someone
else using release()
(see below).
// Inside a peripheral module such as `stm32ral::stm32f4::stm32f405::gpio`
pub struct Instance {
addr: u32,
}
impl Deref for Instance {
type Target = RegisterBlock;
fn deref(&self) -> &RegisterBlock { ... }
}
Finally there is a module for each instance of the peripheral, containing
its ResetValues
, its one Instance
, and a take()
function to obtain it.
take()
returns an Option<Instance>
which will be Some
if the instance
was available and None
if not. This ensures you have exclusive acess
to the peripheral and cannot encounter data races with other safe code.
You can call release()
to return the instance for others to take()
.
// Inside a peripheral module such as `stm32ral::stm32f4::stm32f405::gpio`
pub mod GPIOA {
pub const reset: ResetValues = ResetValues { ... };
const INSTANCE: Instance = Instance { ... };
pub fn take() -> Option<Instance> { ... };
pub fn release(Instance) { ... };
}
pub mod GPIOB { ... }
pub mod GPIOC { ... }
// and so on
These instances are what permit access to the relevant registers:
// In reality, you'd use write_reg!(gpio, gpioa, MODER, 0x1234)
// and read_reg!(gpio, gpioa, MODER)
let gpioa = gpio::GPIOA::take().unwrap();
gpioa.MODER.write(0x1234);
let _ = gpioa.MODER.read();
For convenience in unsafe code, there is also a raw pointer directly to each
RegisterBlock
:
pub const GPIOA: *const RegisterBlock = ...;
This permits direct use in macros without requiring you to first call take()
(see below for macros).
Note that with the nosync
feature enabled, the Instance
and
take()
/release()
methods are not generated; the only access is via the
raw pointer described above.
As an implementation detail, many structs are actually refactored to live
in the family level, with the original definitions replaced by pub use
statements, to reduce duplication and bloat in the crate source files.
The same is also true of duplicated values in the R
, W
, and RW
modules.
To simplify using all the constants and registers, four macros are provided. For full details please check out the documentation.
In the definitions below:
peripheral
is a path to a peripheral module, for examplestm32ral::gpio
,instance
is any expression that dereferences toRegisterBlock
: anInstance
,&Instance
,&RegisterBlock
, or*const RegisterBlock
,INSTANCE
is the path to an instance module, for examplestm32ral::gpio::GPIOA
, but anything inside theperipheral
module will be in scope, so you can simply specifyGPIOA
,REGISTER
is an ident and the name of any register in the peripheral, for exampleMODER
, which must exist as a field in theRegisterBlock
,value
can be a literal value or any named values from the register module.
- Directly writes
value
toinstance.REGISTER
.
// Set PA3 high (and all other GPIOA pins low).
write_reg!(stm32ral::gpio, gpioa, ODR, 1<<3);
- Writes
value
s toFIELD
s and all other fields to 0 (for one or moreFIELD
s) - You can use any
FIELD
which is a submodule ofREGISTER
. - You can specify any arbitrary
value
, or you can also use any constant value inside theW
orRW
modules of theFIELD
module.
// Set PA3 to Output, PA4 to Analog, PA5 to 0b01 (also Output), everything
// else gets set to 0 (Input).
// (In reality, be careful, as this operation will change the state of the
// JTAG/SWD pins PA13-15, possibly breaking debugger access.
// Use modify_reg!() instead.)
write_reg!(stm32ral::gpio, gpioa, MODER, MODER3: Output, MODER4: Analog, MODER5: 0b01);
- Reads and returns the current value of
instance.REGISTER
// Get the value of the whole register IDR
let val = read_reg!(stm32ral::gpio, gpioa, IDR);
- Reads and returns the current values of
FIELD1
,FIELD2
, ... insideinstance.REGISTER
// Get the value of IDR2 (masked and shifted down to the LSbits)
let idr2 = read_reg!(stm32ral::gpio, gpioa, IDR, IDR2);
// Get the value of IDR2 and IDR3
let (idr2, idr3) = read_reg!(stm32ral::gpio, gpioa, IDR, IDR2, IDR3);
- Reads the current value of
FIELD
and returns the value ofFIELD EXPRESSION
EXPRESSION
can be any token tree that makes sense in context, but is typically something like== value
,!= value
- As with
write_reg!()
, all the values fromR
andRW
modules ofFIELD
are brought into scope, so you can useMODER5 == Output
for example
// Busy wait while PA2 is high
while read_reg!(stm32ral::gpio, gpioa, IDR, IDR2 == High) {}
- Reads
instance.REGISTER
asr
, then writesfn(r)
to it - Any lambda or function taking the register's type is acceptable
// Set PA3 high without affecting any other bits
// (in reality, use the BSRR register for this).
modify_reg!(stm32ral::gpio, gpioa, ODR, |reg| reg | (1<<3));
- Updates only the specified
FIELD
s to the newVALUE
s, without changing any other fields - Reads
instance.REGISTER
, masks out the bits corresponding to the specifiedFIELD
s, sets those bits to the specifiedVALUE
s, and writes back the result - As with
write_reg!()
, all the values from theW
andRW
modules ofFIELD
are brought into scope
// Set PA3 to Output and PA4 to Analog, but without affecting any other pins.
modify_reg!(stm32ral::gpio, gpioa, MODER, MODER3: Output, MODER4: Analog);
- Writes the reset value to
instance.REGISTER
- Note you have to specify both an
instance
(anything that derefs toRegisterBlock
) andINSTANCE
(the name of the instance module inside the peripheral module, e.g.peripheral::INSTANCE::reset
must exist).
// Reset GPIOA back to reset state, with JTAG/SWD pins on PA13, PA14, PA15.
reset_reg!(stm32ral::gpio, gpioa, GPIOA, MODER);
- Writes the reset value to the specified
FIELD
s without changing the other fields - Reads
instance.REGISTER
, masks off the specifiedFIELD
s, sets those bits to their reset values, and writes back the result - Note you have to specify both an
instance
(anything that derefs toRegisterBlock
) andINSTANCE
(the name of the instance module inside the peripheral module, e.g.peripheral::INSTANCE::reset
must exist).
// Reset PA13, PA14, PA15 to their reset state.
reset_reg!(stm32ral::gpio, gpioa, GPIOA, MODER, MODER13, MODER14, MODER15);
For convenience, when using the macros in an unsafe
context,
you do not need to first take()
the instance and can instead
specify it directly:
// Unsafely and directly access GPIOE.
unsafe { write_reg!(stm32ral::gpio, GPIOE, 0x01010101) };
// The macro is effectively doing this:
unsafe { (*stm32ral::gpio::GPIOE).MODER.write( 0x01010101 ) };
This works because each instance also exists as a *const RegisterBlock
in the peripheral module, which the macros bring into scope and dereference.
Use the rt
feature to bring in cortex-m-rt
support, providing a
suitable device.x
linker script and interrupt definitions.
You can then specify your own interrupt handler:
#[interrupt]
fn TIM2() {
write_reg!(stm32ral::tim2, TIM2, SR, UIF: 0);
}
If you're using cortex-m
, the Interrupt
enum is compatible (it implements
Nr
):
peripherals.NVIC.enable(stm32ral::Interrupt::TIM2);
First, a safety preface. This crate considers safety strictly in the Rust
sense of avoiding undefined behaviour,
and not in any more general sense related to embedded hardware. We use safety
to avoid data races but not to avoid shorting out your hardware: that's on you.
Given the low-level nature of this crate, it is expected it will often (though
not always!) be used in an unsafe
context, and is designed to facilitate this
as much as possible. Higher level crates such as HALs seem a better place
to facilitate safe abstractions.
There are two major safety concerns with a register access crate.
First is the possibility that peripherals will perform actions on unrelated
memory, for example a DMA peripheral or a cache control register. Such
registers are marked as unsafe and reading or writing them will always
require an unsafe
block or function. Under the hood, they use
the UnsafeXXRegister
types instead of the usual XXRegister
. Since such
registers could potentially cause undefined behaviour, the user must make sure
when accessing them to provide their own safety guarantees.
Most registers will not be unsafe and can be directly accessed in safe code. The macros provided for field access ensure values are masked for the field width, but otherwise nothing prevents safe code writing arbitrary values to registers not specifically marked unsafe. This is considered a usability trade-off; while some illegal values in some device registers will surely cause unexpected behaviour; so will many legal values (Rust cannot prevent you setting an output low which is hardwired to the supply rail, for example). Aside from a few specific registers, writing those values should not cause undefined behaviour in Rust itself, so our tradeoff is to try and prevent UB while not trying to use the safety system to enforce that all register fields may only be written with legal values.
The second safety issue is around synchronised access to peripherals, which are effectively global shared memory. The safety concern is around data races: if you are reading and writing from a peripheral but halfway through an interrupt routine wants to access the same peripheral, you will race it, leading to undefined behaviour.
The solution provided here is similar to svd2rust
, though more granular:
every peripheral instance has a take() -> Option<Instance>
function
which returns Some(Instance)
if the instance is not currently taken, and
None
if it is. You can therefore use this safe function in your code to
obtain an Instance, and pass it (or a reference to it) on to any other
functions that require it, while ensuring no other threads (or interrupt
routines) can access the peripheral in safe code. When you're done using it,
you can call release(instance)
to make it available to take()
again.
However, you will often need to use peripherals in other contexts where it is
awkward or impossible to safely pass the Instance
around.
This crate provides a *const RegisterBlock
which can be unsafely dereferenced
for this purpose, and can be given directly in the macros in an unsafe context.
When using these unsafe features, you must ensure no data races will happen
yourself (for instance, because an interrupt will only fire after you are done
initialising the peripheral and don't access it thereafter, or because you use
your own mutex to ensure exclusive access, etc).
The nosync
feature removes all the synchronisation methods described above
and leaves only unsafe access, reducing overhead and permitting some higher
level crate to provide its own safe access guarantees without having to take
every peripheral at runtime.
Contributions are very welcome!
To add new named values for registers or fix errors in the registers, please instead update the SVD files in stm32-rs, which will then be used in this crate.
Changes to this crate are primarily concerned with how the RAL is generated from the SVD files.
You only need to do this if you are planning on modifying stm32ral.py
or
otherwise changing how stm32ral
is put together; it's not required just to
use it in another Rust project.
First set up the stm32-rs submodule:
$ git submodule update --init
Now you should simply be able to run make, which will automatically run
make patch
inside the stm32-rs submodule to produce up-to-date patched SVDs.
$ make
Be sure to update the submodule (git submodule update
) if it's been changed
upstream to make sure you're using the latest available SVD patches.
There are a few unresolved issues which require some further thought, but shouldn't present major backwards compatibility issues:
The work to add all possible enumerated values and fix incorrect SVD files is always ongoing at stm32-rs.
Aliased registers are not as well handled as they could be. This is a classic problem for Rust register accesses as Rust does not yet have anonymous unions, which would be the usual solution in C.
At the moment, stm32ral merges aliased registers, attempting to pick a suitable
merged name, and combining all the fields together. This is ergonomic for
many aliased registers (e.g., CCMR
in timers), but not for others
(such as OTG_HS_DIEPINT5
and OTG_HS_DIEPTSIZ7
, yuck).
Once there are anonymous unions there might be a better solution.
Most peripherals combine well, for instance GPIOA
through GPIOK
are all
instances of gpio::RegisterBlock
. The same applies to most other peripherals
like USART
, SPI
, I2C
, and so on.
Timers are not well merged because their hierarchy is complicated: we can't
just have a single TIM
since there are so many different register blocks,
but the solution at the moment (no merging) is not optimal either.
Ideally we might identify the various categories, such as:
- Advanced
- Basic
- General purpose (type 1)
- General purpose (type 2)
- General purpose (32 bit)
- Low power
- High resolution
We could then try to group those together.
A few other peripherals do not merge well yet either, especially
on STM32F373 and STM32F3x8 where some GPIO peripherals do not have the
LCKR
registers, annoyingly. The best solution might be to just pretend
it does have it.
- Update version number in stm32ral.py
- Update CHANGELOG.md
- cd stm32-rs/svd; rm *.svd *.svd.patched; ./extract.sh; cd ../..;
- make
- git add -u .
- git commit -am "vX.X.X"
- git push origin master
- git tag -a "vX.X.X" -m "vX.X.X"
- git push origin vX.X.X
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.