I currently follow the awesome rust-raspberrypi-OS tutorial to learn how to do some bare-metal programming on my Raspberry Pi 3.
The code for the examples are taken from this tutorial
It uses the macros register_bitfield!
and register_structs!
, similar to the bitflags! macro, that I saw in many places in embedded Rust codes.
However I wasn't able to read it easily and grasp what it was doing, and felt like digging deeper.
That's when @devonmorris proposed to use cargo-expand
to look the code after the macro is resolved.
This post will explain what registers are, what are their use in bare-metal programming, and how these macros allows for smooth Rust code when working with them.
Throughout this post, we'll use the datasheet for the CPU of the Raspberry Pi 3 as an example, but this principle applies to any CPU, just grab its datasheet and read it up ! When working with embedded systems, or low level programming (in the Linux kernel for example), we make extensive use of memory addresses to store things. It may be an area of the RAM to store a variable, or a way to communicate with the GPU (using a mailbox), or even a peripheral thanks to the Memory-Mapped I/O (MMIO) embedded inside the CPU. This way, a program running bare-metal on a Raspberry Pi for example could lit the onboard LED just by writing the right value at the right memory address. How do we find where to do this ? What you have here, is a list of registers, their memory address, their size, and the register to write to if we want to set a GPIO to High or Low (GPSETn and GPCLEARn). To set a GPIO to High (as we see in page 95), we set a single bit of the 32-bit value stored at this memory address (what is called a register). The register Let's treat it this way, as a As we can see on the datasheet, to set the GPIO number 5 of the CPU, you need to write As you can see on the binary value, to set the pin So to set the pin If a register holds the value For this, we use a For example: To check its value, we can shift it to the right Usually, the manufacturer of CPUs meant to be programmed in bare-metal will provide some If working on a common board, it may even exist a When we deal with memory addresses directly, we are basically re-creating the We can of course do everything from scratch in Rust, get the memory addresses and such, but the crate tock_registers allows us to get a much better API using macros ! This macro defines a list of registers that holds a In this register This sets up the memory addresses, fields and values, but not the struct interfaces yet, we need to use another macro for this: For now, it seams a bit mysterious, but we'll expand that macro later and understand how it works. What it does, is that it creates a struct Now, this allows that kind of code: But what does it do exactly ? How does it work ? I'll use To start, I'll inspect the macro First, it generates a module that will hold all the defined structs, enums and constants. Inside, a In itself, the struct doesn't do much, but uses the type system to later set the generic types to a type related to This allows the code to use the form Then, a constant The arguments passed to its creation function is the To get the basic mask, we perform the computation: The macro then generates a new sub-module As we gave the possible values for this field inside the macro, it generates 2 possible interfaces: In addition, it will generate the This interface allows to write On the macro This generates a The unused blocks are replaced with an array of the expected size (computed as And that's pretty much it ! But how do we get a clean Rust type from the raw memory address ? To map the type The pointer type we use here is a Dereferencing the pointer However, there is a case where the user could mess up the inputs in the macro, and it wouldn'tbe detected, up until we map the type In order to detect this at compile time, the macro generates a These tests will check the memory alignment of the offsets given, check for any overlap and make sure the whole address space is covered. I will not go into details of the tests, but it's basically a nested In the past, I already attempted to write Rust on embedded systems: All of them were fun to do, and I'm getting more and more used to embedded Rust, it was hacky when I first started playing with it, it now feels much more mature and I'm happy about this ! Big big thanks to @andre-richter for his tutorial on Rust bare metal programming on Raspberry Pi Thank you for reading this post ! As usual, feel free to correct me, contact me on Mastodon or email, or browse this blog !Introduction
If you already worked with embedded systems, you can probably skip this section.
If you don't skip it, please check it for mistakes I may have written :-)
Just get the datasheet, and look up the peripheral you want to interact with ! (for example, the GPIO section is on page 90)Note that on the Raspberry pi board, the GPIO numbers may not be the same as the CPU. We are inside the CPU, not on the board. To check what board device is connected to what CPU GPIO, you can look inside the device-tree file and look up your board version, the "pin@N" on the left refers to the CPU pin
GPSET0
can be viewed as a "field of bits", where to access some of the bits of this field, you have to check a portion of the binary data of the whole value.struct
(or class
if you're Pythonic), containing fields we can get or set.Set the value of a register
[..20 more zeros..]_0000_0001_0000
(or 0x20
, or 32
) to the memory address of GPSET0
which is (on page 90) 0x7E20_001C
.5
, we have a 1
and 4
zeros after, it's like shifting 1 to the left 4 times, this operation is generally noted 1 << 4
in programming languages.N
, we write 1 << (N - 1)
to the address of the register GPSETn
(as stated in the datasheet, if N < 32
we use GPSET0
, if 32 < N < 54
we use GPSET1
)Note that this way we can set multiple pins at the same time, using an AND operation (usually noted
|
), we can set multiple bits like so: (1 << 4) | (1 << 12) | (1 << 8)
Get the value of a register
[...]_1101_1001
, how do we check if the 5th and 6th bit are set ?mask
, a value that will only leave the bits we are interested in if we apply it on the value of our register with an AND (&
).1101_1001 & 0110_000 (mask) = 0100_000
The mask allowed us to only leave the 2 bits we were interested in.(0b0100_0000 >> 4) = 0b0000_0010
, giving us the bits we are interested in, 0b10
.Note that for a single bit, we can just check that the value is > 0 to see if the bit is set,
0b0100_0000 > 0 = true
, 0b0000_0000 > 0 = false
.In the real world
hal
(Hardware Abstraction Layer), a library containing all the memory addresses for the registers.bsp
(Board Support Package), that contains a lot of useful tools, functions and constants to play with the hardware.hal
Rusting it up
register_bitfields!
u32
(32 bits), containing a single register GPFSEL1
.In this example, this register allows to select whether we want a pin to be treated as an Input or Output (or a special function).
The FSEL15 field of this register refers to the configuration for the pin number 15GPFSEL1
of 32 bits, the value of 3 bits
starting at the offset 15
is named FSEL15
, and can hold 3 different values:000
, configuration as an Input001
, configuration as an Output100
, configuration as its Alternate Function 0, which in this case is the UART RXregister_structs!
RegisterBlock
that holds a list of registers, some of them are not even defined (_unused
), and at the offset 0x04
we find our previously defined register GPFSEL1
that we configure with the ReadWrite
permissions.let registers : RegisterBlock = // We'll see how it's initialized later ...
registers.GPFSEL1.write // We erase all the register, and set FSEL15 to Input
registers.GPFSEL1.modify // We keep the changes made before, and set FSEL14 to Output
registers.GPFSEL1.write // We erase all the data in the register
The
RegisterBlock
refers to all the registers that we want to put together in our struct.
In our case we can define one made for interfacing with the GPIO.Inspecting the macro
cargo-expand
here, and will simplify the code so we can have a clear view on itRegister bitfields
register_bitfield
we used beforeregister_bitfields!
Register
struct is defined, which will be useful for generic type definition on the Field
type, which will be our interface.GPFSEL1
only.GPFSEL1::Register
FSEL15
is created, of type Field
, which is an interface for the whole register value.mask
and the offset
used.
We defined FSEL15
to be a 3 bit wide field, so the mask will be [...]_0000_0111
, as it has an offset of 15, the mask will be shifted to the left of 15
rows in order to get the final mask.A = 1 << 2
, which gives A = [...]_0000_0100
B = (1 << 2) - 1
, which gives B = [...]_0000_0011
mask = A + B
, resulting to mask = [...]_0000_0111
FSEL15
which will use the FieldValue
type to define the interfaces to this field.Value
, which can be translated to FieldValue
to be applied, and can be used as u32
directlyFieldValue
SET
constant that holds the FieldValue
linked to the max value (all bits set to 1
), and a CLEAR
constant for all bits set to 0
.GPFSEL1::FSEL15::Value::Input
, or directly GPFSEL1::FSEL15::Input
.Register structs
register_structs!
, we used the previously defined fields and modules like so:register_structs!
struct RegisterBlock
, containing an attribute for each data we defined for it.end - start
), and our GPFSEL1
register has the type ReadWrite
over the generated GPFSEL1::Register
struct.
Note that
repr(C)
here allows to respect the order, size and alignment of the fields inside this struct.
The binary representation of the struct's fields will then match the register addresses and fields that we define.
See more on the official documentation of repr(C)
All we have to do now is create our register to the expected memory address, and use it !
Thanks to #[repr(C)]
, we can directly map the RegisterBlock
structure into a memory address, the fields will be provisionned by their binary value stored at this address directly.RegisterBlock
struct to the address 0x7E200_0000
, we need to:RegisterBlock
type*const _
, it means that the pointer will not move in memory (const
), the _ means that the type will be determined using the inference system, here the return type.*(ptr)
is an unsafe operation, and as we type the var as RegisterBlock
, we get the expected type, containing the expected values stored at the expected address !let start_addr = 0x7E200000;
let regblock : RegisterBlock = unsafe ;
regblock.GPFSEL1.write;
The memory address
0x7E20_0000
(found on page 90) we passed corresponds to the start of all the registers related to the GPIO. From this base address, an offset of 0x04
will link to the memory address 0x7E20_0004
, which leads to the register GPFSEL1
RegisterBlock
to the start address.const
which will be evaluated at compile time, and any panic!
in it will trigger a failed build.const
definition with panics inside.const _: = ;
Conclusion
Take care <3Thanks to
Romain KELIFA
for pointing out a typo, and an unclear passage of the article.