(with too many hexadecimals)
HEADS UP Most of the links in this page are PDFs!
So far we have only used the processor inside our microcontroller. The processor can only do math and logic, and, on its own, it can't interact with the external world: it can't drive a LED or a motor, read a sensor or communicate with other devices.
To make our programs more useful (and fun!) we must learn to use peripherals. A peripheral is an extra piece of electronics that's built, alongside the processor, in the same microcontroller package. Peripherals give the processor the extra functionality it needs to interact with the external world.
Awesome! What can I do with these peripherals?
All sort of things! There are several different types of peripherals, each one provides a different functionality. Microcontrollers manufacturers call them by different names even if they provide the same functionality though. Here are some of the most common ones (using STM32 naming convention):
-
GPIO
. General Purpose Input/Output. Can be used to turn on/off external devices (e.g. a motor, a lamp, etc.) or to read the state of a "switch" (e.g. a two-state (ON/OFF) switch, a keyboard, etc.). -
ADC
. Analog-to-Digital Converter. Can be used to "read" analog sensors (e.g. a thermometer, a light (intensity) sensor, etc.) or signals (e.g. voltage level of a battery, electric current, etc.). -
TIM
. Timers. Can be used to perform periodic tasks (e.g. every 100 ms), measure lengths of time (e.g. for how long was this button pressed?) or generate periodic signals with variable duty cycle (AKA Pulse Width Modulation (PWM)). PWM is mainly used to control how much power is supplied to an electric machine like a motor which, in turn, lets you indirectly control other parameters like speed and torque.
We'll explore these and several other peripherals in more detail in a later chapter.
So, how do I use these peripherals?
Thanks to some magic called memory mapped I/O, to the processor, peripherals appear as memory regions (!). This means that, for example, by writing to some special memory address you can use the digital I/O peripheral to turn on/off a LED. Another example: By reading from some special memory address you can use the Analog to Digital Converter peripheral to "read" an analog sensor like a thermometer and get the current environment temperature as a digital/discrete value.
A chunk of memory associated to a single peripheral is known as a "register block". As other types of memory, these regions are usually accessed in word sized chunks (32-bit on ARM). Each of these word sized chunks is referred to as a (hardware) register; though registers can also be half-word or byte sized. Each of these registers has a human-friendly name and an address associated to it.
A concrete example: The STM32F303VCT6 microcontroller has a peripheral known as
Reset and Clock Control (RCC). The register block associated with this
peripheral starts at address 0x4002_10000
. This register block is comprised of
several registers as seen on its register map. One of registers associated
with this peripheral is the "AHB peripheral clock enable register"
(AKA APB2ENR
) which lives at address 0x4002_1014
. This particular register
can be used to power on/off other peripherals.
To get familiar with the use of peripherals, we'll write the microcontroller version of the "hello world" program: Blinking an LED.
This is an overview of what our program will do:
- Power on the digital output peripheral.
To save energy, most of the peripherals in a microcontroller boot in a powered off state. We have to explicitly "power on" the peripherals we want to use.
- Put the pin that's connected to the LED in output mode.
A pin a metal contact that a microcontroller exposes and that can be electrically connected to another device. A pin can either be (configured) to be in input mode or in output mode, but it must be in output mode to be able to drive (i.e. supply current to) an external device. Most pins start in input mode right after the micro boots to avoid spuriously driving external devices.
- Set the pin high or low to turn on the LED.
Low means outputting zero volts (0V) on the pin whereas high means outputting a non-zero voltage, usually the power supply voltage (3.3V on most Cortex-M micros), on the pin. Depending on how the LED is wired to the pin, setting the pin low/high should turn it off/on or the other way around.
After we've confirmed that we can turn the LED on/off, we'll modify the program to toggle the state of the LED pin every few seconds.
Now we must fill in the device-specific details to realize our plan. All the needed information will come from the microcontroller reference manual (here's mine) and the dev board user manual (here's mine).
First, we must pick a LED on the dev board to work with. Your dev board very likely has at least one "user LED" that's connected to one of the microcontroller's pin (check its user manual). Don't confuse an "user LED" with the "power LED". The latter is an indicator of whether the board is powered on or off and can't be controlled by the microcontroller.
TODO What do I do if my dev board doesn't have an "user LED"?
The STM32F3DISCOVERY has eight user LEDs. For this example, I'll be using the red one that's connected to the pin PE9. Because micros have many I/O pins, these pins are usually grouped in ports. A port is a collection of 8, 16, or some other number of pins. Ports are usually identified with letters: A, B, etc. and the pins in it are usually identified with numbers: 0, 1, etc. Therefore, you can think of the the pin PE9 as the 10th (because numbering starts at 0) pin in the port E.
Micros have a dedicated peripheral that's in charge of "clocking" other peripherals. Clocking in this context means powering on/off a peripheral. A peripheral that doesn't receive a clock signal is basically powered off -- it can't be used and it doesn't (actively) consume energy.
On STM32 micros this peripheral is called RCC. The family of *ENR
registers
in this peripheral control the clocking of other peripherals. In my case, I'm
interested in the AHBENR register which contains a IOPEEN
bit that controls
the clocking of the E port.
In my case, I need to put the pin PE9
in output mode. Some register in the
GPIOE peripheral should let me do that. After looking through the
documentation, I found that the MODER
register does that. In particular, the
MODER register contains the bitfield MODER9
which control the "mode" (input
or output) of the pin PE9
. I'll use the following setting:
MODER9 = 0b01
Puts the pin in general purpose push-pull output mode.
Again the register that I want must be in the GPIOE
peripheral. In this case,
it's the BSRR
register. It can individually set or reset a pin. Here,
reset means putting the pin low and set means driving the pin high.
Here's a detailed specification of the program:
-
Turn on the GPIOC peripheral: Set the
IOPEEN
bit in theRCC->AHBENR
register to1
. -
Put the PE9 pin in output mode: Set the
MODER9
bitfield in theGPIOE->MODER
register to0b01
. -
Set the PE9 pin high: Set the
BS9
bit in theGPIOE->BSRR
register to1
. -
Set the PE9 pin low: Set the
BR9
bit in theGPIOE->BSRR
register to1
.
And here's the code. I'm omitting the exception
and lang_items
modules which
haven't changed since our previous program.
#[export_name = "_reset"]
pub extern "C" fn main() -> ! {
power_on_gpioe();
put_pe9_in_output_mode();
set_pe9_high();
set_pe9_low();
loop {}
}
fn power_on_gpioe() {
/// Start address of the RCC register block
const RCC: u32 = 0x4002_1000;
/// Offset address of the AHBENR register
const RCC_AHBENR: u32 = 0x14;
/// IOPCEN bit mask
const RCC_AHBENR_IOPEEN: u32 = 1 << 21;
unsafe {
// Pointer to the AHBENR register
let ahbenr = (RCC + RCC_AHBENR) as *mut u32;
// IOPECN = 1
*ahbenr |= RCC_AHBENR_IOPEEN;
}
}
/// Start address of the GPIOC register block
const GPIOE: u32 = 0x4800_1000;
/// Offset address of the BSRR register
const GPIOE_BSRR: u32 = 0x18;
fn put_pe9_in_output_mode() {
/// Offset address of the CRH register
const GPIOE_MODER: u32 = 0x0;
unsafe {
// Pointer to the MODER register
let moder = (GPIOE + GPIOE_MODER) as *mut u32;
// MODER9 = 0b01
*moder = (*moder & !(0b11 << 18)) | (0b01 << 18)
}
}
fn set_pe9_high() {
unsafe {
// Pointer to the BSRR register
let bsrr = (GPIOE + GPIOE_BSRR) as *mut u32;
// BS9 = 1
*bsrr = 1 << 9;
}
}
fn set_pe9_low() {
unsafe {
// Pointer to the BSRR register
let bsrr = (GPIOE + GPIOE_BSRR) as *mut u32;
// BR9 = 1
*bsrr = 1 << (16 + 9);
}
}
Quite unsightly, right? So many magic values. In a later chapter, we'll refactor this code to get rid of the magic values, the pointer arithmetic and the raw pointers. But this code will make do for now!
Time to test our code! Don't feel discouraged if your program crashes or doesn't work on the first try! I certainly get most of my embedded programs wrong when I'm just starting to write drivers and have to deal with all these magic values and/or have to jump back and forth between the microcontroller reference manual and my library/program.
OK, here's how I'd debug this program:
-
Starting from the program entry point,
_step
, repeatedlystep
over the program until you hit the the "set the pin high" statement, in my case this is the*bsrr = 1 << 8
. If you didn't hit an exception, congratulations! Head to step 3, otherwise go to step 2. -
If you hit an exception, you should now know which statement triggered it. Reset your microcontroller with
monitor reset halt
, then step all the way until your reach the faulty statement but don't execute it!. At this point, inspect the address of the register that will be modified by the faulty statement. Is the address right/valid? If not, fix it then go to step 1. -
You should now be about to execute the instruction that sets the LED pin high. Step from here all the way to the endless
loop
. This should toggle the state of the LED at least once. If it doesn't, then quite a few things could have gone wrong ... See below:
-
Wrong register address as seen in step 2.
-
GPIO
has not been powered on or configured properly. You'll have to "examine" ((gdb) x $ADDRESS
) all the related registers. If you didn't power on theGPIO
peripheral, you'll see that trying to write to that peripheral registers has no effect. -
You are driving the wrong pin, i.e. one that's not connected to an LED. Confirm this against your dev board user manual.
Now that we know that we can toggle the state of the LED. Making the LED blink
is relatively easy. We need to add a delay
function and then move the LED
toggling code inside a loop:
#[export_name = "_reset"]
pub extern "C" fn main() -> ! {
power_on_gpioe();
put_pe9_in_output_mode();
let ticks = 100_000;
loop {
set_pe9_high();
delay(ticks);
set_pe9_low();
delay(ticks);
}
}
fn delay(n: u32) {
for _ in 0..n {}
}
I have no way of telling you what value of n
will give you e.g. a delay of 1
second because that depends on the built-in internal clock of your
microcontroller (mine is 8 MHz) and the actual instructions that delay
compiles to in debug mode. However, using a value between 10_000
and 100_000
for ticks
should make the LED blink at a visible rate.
To test, simply flash the program and let it run from the debugger:
(gdb) continue
You should now see the LED blink at some rate. To make the LED blink faster make
the value of ticks
smaller. To do this, first manually break the program by
pressing Crtl-C
at gdb
's prompt, then use the following commands:
# break somewhere inside the loop
(gdb) break main.rs:13
(gdb) continue
Breakpoint 1, app::main () at (..)/src/main.rs:13
13 set_pe9_high()
# make ticks smaller
(gdb) set ticks = 10000
# clear breakpoint
(gdb) clear main.rs:13
(gdb) continue
The LED should now blink at a faster rate. You can repeat the experiment but
setting ticks
to a larger value.