Files
Embedded-Hacking/drivers/0x05_servo_rust/README.md
T
Kevin Thomas f62db776e1 Initial commit
2026-07-06 14:32:12 -04:00

6.4 KiB

0x05 Servo Rust Driver

This repository contains a Bare-Metal Rust driver for controlling standard 50Hz hobby Servos via PWM on the RP2350 (and RP2040) microcontrollers.

It includes:

  • A thin demo (src/main.rs) that repeatedly sweeps a servo horn back and forth between 0 and 180 degrees while printing the current angle over UART.
  • A reusable library module (src/servo.rs) providing a hardware-agnostic servo_lib with helper math to map angles to precise pulse-width microsecond targets.
  • Board initialization logic (src/board.rs).

🚀 Getting Started from Scratch

If you're starting with a fresh machine, follow these exact steps to install the toolchain, build the code, and flash it to your microcontroller.

1. Install Rust

First, install rustup (the Rust toolchain installer) if you haven't already:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Note: Restart your terminal or run source $HOME/.cargo/env after this finishes.

Ensure your Rust compiler is up to date:

rustup update

2. Install the Target Architecture

This project is configured for the RP2350 (ARM Cortex-M33). We need to install the cross-compilation target for it:

rustup target add thumbv8m.main-none-eabihf

(If you were targeting the RP2040, you would use thumbv6m-none-eabi instead).

3. Install Build Tools

You will need a few extra tools to help link and format the firmware for the RP-series chips.

Install flip-link (adds zero-cost stack overflow protection):

cargo install flip-link

Install picotool (used by cargo run to flash the chip):

  • macOS: brew install picotool
  • Linux/Windows: Follow the official Raspberry Pi documentation to install picotool or build it from source.

4. Building the Code

To compile the code for the microcontroller, simply run:

cargo build

To build a highly optimized release version (smaller and faster):

cargo build --release

5. Flashing to the Microcontroller

This project is pre-configured in .cargo/config.toml to use picotool as the custom runner.

To flash the code:

  1. Hold down the BOOTSEL button on your RP2350 board.
  2. Plug it into your computer via USB (or press the RUN/RESET button while holding BOOTSEL).
  3. Run the following command:
cargo run --release

cargo will compile the code and automatically use picotool to upload the .elf file directly to your board and start executing it!

6. Testing on the Host

Because the Servo mapping logic is separated into a reusable math library without touching hardware registers, you can run the unit tests natively on your computer!

However, because this project sets a default bare-metal target (thumbv8m.main-none-eabihf) in .cargo/config.toml, running a plain cargo test will fail because the standard library doesn't exist on the microcontroller. You must explicitly tell Cargo to compile the tests for your host computer's processor architecture:

Mac (Apple Silicon):

cargo test --lib --target aarch64-apple-darwin

Linux (Intel/AMD 64-bit):

cargo test --lib --target x86_64-unknown-linux-gnu

Windows (64-bit):

cargo test --lib --target x86_64-pc-windows-msvc

🧠 Code Walkthrough

This section explains exactly how the code works, where the entry point is, and traces the flow of execution as if you were stepping through it line-by-line.

1. The Entry Point (src/main.rs)

Unlike a standard computer program, bare-metal microcontrollers do not have an operating system to call main(). Instead, we use the #[entry] macro from the HAL (Hardware Abstraction Layer) to define the very first function that runs after the chip boots up.

  • main() -> !: This is the absolute start of our code. It takes ownership of all the hardware peripherals (hal::pac::Peripherals::take().unwrap()) and immediately passes them into board::run(...). The -> ! means this function never returns (because embedded devices run in an infinite loop).

2. Board Initialization (src/board.rs)

Once execution enters board.rs, we initialize the system clocks, pins, UART (for logging), SysTick (for delay), and PWM (for servo control).

  • run(...): The master setup function. Calls the helper initialization functions below, prints an announcement over UART, and enters the servo sweep loop.
  • init_clocks(...): Wakes up the external 12 MHz crystal (XOSC) and configures the PLLs (Phase-Locked Loops) to drive the system clock at its maximum speed.
  • init_pins(...): Takes control of physical pins across IO_BANK0.
  • init_uart(...): Configures the hardware UART0 peripheral to operate at a standard 115200 baud rate with an 8N1 configuration for debug printing.
  • init_delay(...): Captures the ARM Cortex-M SYST (SysTick) peripheral to create a blocking delay timer.
  • init_servo_pwm(...): Obtains PWM Slice 3 and binds its channel_a to GPIO 6. Sets up the clock divider and TOP value to achieve a standard 50 Hz PWM frequency.
  • servo_loop(...): Loops infinitely, sweeping the servo angle from 0 to 180 degrees and back down to 0.
  • sweep_angle_up(...) / sweep_angle_down(...): Walks through angles in 10-degree increments.
  • apply_angle(...): Computes the hardware register level using servo_lib, sets the new duty cycle, formats an Angle: XXX deg string without heap allocation, transmits it, and delays 150ms to allow the physical servo motor to arrive at the target angle.

3. The Reusable Servo Math Library (src/servo.rs)

Unlike basic LEDs which simply vary duty cycle from 0-100%, Servos expect a precise 50 Hz signal (20ms period) where the "HIGH" time represents the angle: usually ~1ms for 0 degrees, ~1.5ms for 90 degrees, and ~2ms for 180 degrees.

  • calc_clk_div(...): Calculates the precise floating-point clock divider required to step down the fast system clock so the counter overflows exactly 50 times per second.
  • angle_to_pulse_us(...): Linearly maps an angle (0.0 to 180.0 degrees) to a pulse width (e.g. 1000 to 2000 microseconds).
  • pulse_us_to_level(...): Translates the desired microsecond pulse into a raw integer counter compare-value for the PWM hardware registers.
  • clamp_pulse_us(...) / clamp_degrees(...): Safety constraints to ensure we do not physically damage the servo by driving it past its mechanical limits.