Skip to content
~/home/alelouis

Controlling servomotors in Rust with Micro:bit V2

Servomotors are cool devices that can be easily controlled any PWM capable microcontrollers. Today we'll try to control an SG90 (fig. 1) microservo using a Micro:bit and Rust!

Figure $1$: Servomotor SG90

How to talk to servos

Servomotors are controlled by using PWM (Pulse Width Modulation) signals. Think about a square signal but with unequal up and down time. The ratio between up and down time is called the duty cycle. A duty cycle can vary between $0$ (always $0$ signal) and 1 (always $1$ signal). A perfect square wave has a duty cycle of $0.5$.

Because a PWM signal is a real voltage signal, we need something to generate it. One way to do it is to use a microcontroller.

The Big Brain

I'll use a Micro:bit board in its V2 version. This cheap hardware has a microcontroller (called the nRF52833) based on Cortex-M4 ARM architecture, and other components such as a speaker, buttons or leds. Really, we'll only use the MCU (microcontroller unit) capabilities for now.

Figure $2$: The micro:bit:V2, in its cute yellow sleeve.

See the bottom pins ? This is where all GPIOs are exposed as well as voltage references such as 3V and GND.

I use a this connector breakbout board in order to access the various pins more easily.

Figure $3$: Breakout board.

The last thing, but not the least, is to program this thing. While the easy and most used pathway would be to use C++ libraries here, I decided to learn bits of embedded programming in Rust.

Programming the yellow thingy

There is a great resource out there for beginners which is the Discovery book. Its author goes from the setup instructions to various peripheral programming and is a really nice introduction to embedded rust, can't recommend it enough if you want to dive into this kind of things.

Unfortunately, the PWM peripheral is not covered in this book. So we'll have to use the specific abstractions of the nfr52833 rust crate! Really, this is where the fun begins.

There are a lot of HAL (Hardware Abstraction Layer) crates developed by the rust community for many microcontrollers (built on top of micro-architecture crates and peripheral access crates). The micro:bit also has the highest level of abstraction crate available, which is a board crate.

Ideally, one should only use board or HAL crates for any classical use. The great thing about embedded-HAL is that the API defines common traits that are later implemented in device specific HAL crates (including the one for our chip). This means that your code work on others microcontrollers if they have the same capabilities!

PWM, come at me

There are (no) few resources on this topic (enabling the PWM peripheral for micro:bit with Rust), because it's still relatively new and niche. Our best choice here is to get into the code and read comments. Below, I will describe step by step the code used to control our servo motor.

#![no_main]
#![no_std]

Hey, we are in embedded territory here, forget your println.

use rtt_target::{rprintln, rtt_init_print};
use panic_rtt_target as _;

Here we import the RTT (Real Time Transfer) I/O protocol functions that will allow use to print (actually rprint) variables living on the device via USB.

use microbit::hal::{pac::Peripherals, gpio, pwm, gpio::Level};
use microbit::hal::prelude::*;

Then, each lib from the HAL that we may use is imported, including pwm.

#[entry]
fn main() -> ! {
    rtt_init_print!();

Here we set the attribute #[entry] for our main() function in order to tell the MCU that this function is our entrypoint for the program execution.

Then, the RTT communication is initialized.

// Get ownership of peripherals
let board = Peripherals::take().expect("Couldn't initialize board.");

First, we get the ownership of all the HAL peripherals. We will use it to access pins and PWM peripherals.

// Configuring output pin
let gpio = gpio::p0::Parts::new(board.P0);
let pwm_pin = gpio.p0_02.into_push_pull_output(Level::Low).degrade();

Then, we declare the GPIO pin we want to use for the PWM signal. I will use the pin $0$ here, which, according to this reference is connected to the GPIO P0.02 on the nRF52833. I then make the pin a push_pull output and degrade() into a generic Pin struct expected by later code.

let pwm_motor = pwm::Pwm::new(board.PWM0);
pwm_motor.set_output_pin(pwm::Channel::C0, pwm_pin);

This is where the magic happens. We define our variable pwm_motor by creating a new Pwm struct from the board PWM peripheral board.PWM0. Then, we set the output pin created earlier.

pwm_motor.set_prescaler(pwm::Prescaler::Div32);
pwm_motor.set_max_duty(10_000_u16);

The prescaler part and max duty cycle here is worth going through: when I was searching how to set a given period to my signal, I read this code inside hal's pwm.rs.

pub fn period(&self) -> Hertz {
    let max_duty = self.max_duty() as u32;
    let freq = match self.prescaler() {
        Prescaler::Div1 => 16_000_000u32 / max_duty,
        Prescaler::Div2 => 8_000_000u32 / max_duty,
        Prescaler::Div4 => 4_000_000u32 / max_duty,
        Prescaler::Div8 => 2_000_000u32 / max_duty,
        Prescaler::Div16 => 1_000_000u32 / max_duty,
        Prescaler::Div32 => 500_000u32 / max_duty,
        Prescaler::Div64 => 250_000u32 / max_duty,
        Prescaler::Div128 => 125_000u32 / max_duty,
    };
    match self.counter_mode() {
        CounterMode::Up => freq.hz(),
        CounterMode::UpAndDown => (freq / 2).hz(),
    }
}

Following this code, I made sure to set my prescaler division and max_duty right. According to the SG90 datasheet, the expected frequency is $50 \text{Hz}$. I saw that 500_000u32 was an available prescaler value and I just needed to set max_duty to 10_000_u16 in order to get a frequency of $50 \text{Hz}$.

pwm_motor.loop_inf();

Then I make the pwm loop for ever.

let mut duty = 650_u16;
pwm_motor.set_duty_off(pwm::Channel::C0, duty);

At last, I send the duty variable to anything between 200 and 1200 to span the whole range of acceptable duty cycle accepted by my servo motor (the values are close to the 1ms and 2ms displayed on the datasheet, but I found those lasts to better match the whole angular span of my motor).

Do not forget the empty loop loop{} at the end in order to force the program to run indefinitely.

Flashing the MCU

Once setup, the compilation of the program (pwm.rs) and its flashing on the device is straight forward:

cargo embed --features v2 --target thumbv7em-none-eabihf --bin pwm

If no error are raised, your program is probably running right now!

Is it moving now?

Before connecting anything to the actual motor, I checked with my oscilloscope if the generated signal was indeed what I thought it was. Connecting the output pin 0 to my probe, I measured the duty cycle and frequency and validated my program.

Figure $4$: Oscilloscope capture.

I read a frequency value around $49.7 \text{Hz}$ and a duty cycle of roughly $6.5$%, which matches with my input duty_cycle variable of 650_u16 (650_u16 / 10_000_u16).

The value 650_u16 is not coming from nowhere, I figured it was the noon position of my motor. Let's try to move it all the way to one side by setting the duty_cycle to 200_u16.

Figure $5$: Turning right.

And all the way to the left ? duty_cycle to 1200_u16.

Figure $6$: Turning left.

Perfect, it's working as expected! I hope you enjoyed this small walk-through as much as I did when experimenting. I published the whole repository if you would like to try at home, feel free to fork it and make it your own.

See you later!

Get back to top