Type safe manipulation

The last register we were working with, ODR, had this in its documentation:

Bits 16:31 Reserved, must be kept at reset value

We are not supposed to write to those bits of the register or Bad Stuff May Happen.

There’s also the fact the registers have different read/write permissions. Some of them are writeonly, others can be read and wrote to and there must be others that are read only.

Finally, directly working with hexadecimal addresses is error prone. You already saw that trying toaccess an invalid memory address causes an exception which disrupts the execution of our program.

Wouldn’t it be nice if we had an API to manipulate registers in a “safe” manner? Ideally, the APIshould encode these three points I’ve mentioned: No messing around with the actual addresses, shouldrespect read/write permissions and should prevent modification of the reserved parts of a register.

Well, we do! aux7::init() actually returns a value that provides a type safe API to manipulate theregisters of the GPIOE peripheral.

As you may remember: a group of registers associated to a peripheral is called register block, andit’s located in a contiguous region of memory. In this type safe API each register block is modeledas a struct where each of its fields represents a register. Each register field is a differentnewtype over e.g. u32 that exposes a combination of the following methods: read, write ormodify according to its read/write permissions. Finally, these methods don’t take primitive valueslike u32, instead they take yet another newtype that can be constructed using the builder patternand that prevent the modification of the reserved parts of the register.

The best way to get familiar with this API is to port our running example to it.

  1. #![no_main]
  2. #![no_std]
  3. #[allow(unused_imports)]
  4. use aux7::{entry, iprint, iprintln};
  5. #[entry]
  6. fn main() -> ! {
  7. let gpioe = aux7::init().1;
  8. // Turn on the North LED
  9. gpioe.bsrr.write(|w| w.bs9().set_bit());
  10. // Turn on the East LED
  11. gpioe.bsrr.write(|w| w.bs11().set_bit());
  12. // Turn off the North LED
  13. gpioe.bsrr.write(|w| w.br9().set_bit());
  14. // Turn off the East LED
  15. gpioe.bsrr.write(|w| w.br11().set_bit());
  16. loop {}
  17. }

First thing you notice: There are no magic addresses involved. Instead we use a more human friendlyway, for example gpioe.bsrr, to refer to the BSRR register in the GPIOE register block.

Then we have this write method that takes a closure. If the identity closure (|w| w) is used,this method will set the register to its default (reset) value, the value it had right after themicrocontroller was powered on / reset. That value is 0x0 for the BSRR register. Since we wantto write a non-zero value to the register, we use builder methods like bs9 and br9 to set someof the bits of the default value.

Let’s run this program! There’s some interesting stuff we can do while debugging the program.

gpioe is a reference to the GPIOE register block. print gpioe will return the base address ofthe register block.

  1. $ cargo run
  2. Breakpoint 3, main () at src/07-registers/src/main.rs:9
  3. 9 let gpioe = aux7::init().1;
  4. (gdb) next
  5. 12 gpioe.bsrr.write(|w| w.bs9().set_bit());
  6. (gdb) print gpioe
  7. $1 = (stm32f30x::gpioc::RegisterBlock *) 0x48001000

But if we instead print *gpioe, we’ll get a full view of the register block: the value of eachof its registers will be printed.

  1. (gdb) print *gpioe
  2. $2 = stm32f30x::gpioc::RegisterBlock {
  3. moder: stm32f30x::gpioc::MODER {
  4. register: vcell::VolatileCell<u32> {
  5. value: core::cell::UnsafeCell<u32> {
  6. value: 0x55550000
  7. }
  8. }
  9. },
  10. otyper: stm32f30x::gpioc::OTYPER {
  11. register: vcell::VolatileCell<u32> {
  12. value: core::cell::UnsafeCell<u32> {
  13. value: 0x0
  14. }
  15. }
  16. },
  17. ospeedr: stm32f30x::gpioc::OSPEEDR {
  18. register: vcell::VolatileCell<u32> {
  19. value: core::cell::UnsafeCell<u32> {
  20. value: 0x0
  21. }
  22. }
  23. },
  24. pupdr: stm32f30x::gpioc::PUPDR {
  25. register: vcell::VolatileCell<u32> {
  26. value: core::cell::UnsafeCell<u32> {
  27. value: 0x0
  28. }
  29. }
  30. },
  31. idr: stm32f30x::gpioc::IDR {
  32. register: vcell::VolatileCell<u32> {
  33. value: core::cell::UnsafeCell<u32> {
  34. value: 0xcc
  35. }
  36. }
  37. },
  38. odr: stm32f30x::gpioc::ODR {
  39. register: vcell::VolatileCell<u32> {
  40. value: core::cell::UnsafeCell<u32> {
  41. value: 0x0
  42. }
  43. }
  44. },
  45. bsrr: stm32f30x::gpioc::BSRR {
  46. register: vcell::VolatileCell<u32> {
  47. value: core::cell::UnsafeCell<u32> {
  48. value: 0x0
  49. }
  50. }
  51. },
  52. lckr: stm32f30x::gpioc::LCKR {
  53. register: vcell::VolatileCell<u32> {
  54. value: core::cell::UnsafeCell<u32> {
  55. value: 0x0
  56. }
  57. }
  58. },
  59. afrl: stm32f30x::gpioc::AFRL {
  60. register: vcell::VolatileCell<u32> {
  61. value: core::cell::UnsafeCell<u32> {
  62. value: 0x0
  63. }
  64. }
  65. },
  66. afrh: stm32f30x::gpioc::AFRH {
  67. register: vcell::VolatileCell<u32> {
  68. value: core::cell::UnsafeCell<u32> {
  69. value: 0x0
  70. }
  71. }
  72. },
  73. brr: stm32f30x::gpioc::BRR {
  74. register: vcell::VolatileCell<u32> {
  75. value: core::cell::UnsafeCell<u32> {
  76. value: 0x0
  77. }
  78. }
  79. }
  80. }

All these newtypes and closures sound like they’d generate large, bloated programs but, if youactually compile the program in release mode with LTO enabled, you’ll see that it produces exactlythe same instructions that the “unsafe” version that used write_volatile and hexadecimal addressesdid!

  1. $ cargo objdump --bin registers --release -- -d -no-show-raw-insn -print-imm-hex
  2. registers: file format ELF32-arm-little
  3. Disassembly of section .text:
  4. main:
  5. 8000188: bl #0x22
  6. 800018c: movw r0, #0x1018
  7. 8000190: mov.w r1, #0x200
  8. 8000194: movt r0, #0x4800
  9. 8000198: str r1, [r0]
  10. 800019a: mov.w r1, #0x800
  11. 800019e: str r1, [r0]
  12. 80001a0: mov.w r1, #0x2000000
  13. 80001a4: str r1, [r0]
  14. 80001a6: mov.w r1, #0x8000000
  15. 80001aa: str r1, [r0]
  16. 80001ac: b #-0x4 <main+0x24>

The best part of all this is that I didn’t have to write a single line of code to implement theGPIOE API. All was automatically generated from a System View Description (SVD) file using thesvd2rust tool. This SVD file is actually an XML file that microcontroller vendors provide and thatcontains the register maps of their microcontrollers. The file contains the layout of registerblocks, the base addresses, the read/write permissions of each register, the layout of theregisters, whether a register has reserved bits and lots of other useful information.