Interfacing with Hardware Peripherals in Rust

Explore methods for interacting with hardware peripherals in Rust, including reading from and writing to memory-mapped registers, using embedded HAL crates, and working with GPIO, I2C, SPI, UART interfaces.

16.4. Interfacing with Hardware Peripherals

Interfacing with hardware peripherals is a critical aspect of embedded systems programming. Rust, with its focus on safety and performance, provides robust tools and libraries to facilitate this process. In this section, we’ll explore how to interact with hardware peripherals using Rust, focusing on reading from and writing to memory-mapped registers, utilizing embedded HAL crates, and working with common interfaces like GPIO, I2C, SPI, and UART.

Understanding Memory-Mapped Registers

Memory-mapped registers are a fundamental concept in embedded systems. They allow software to interact with hardware by reading and writing to specific memory addresses. In Rust, accessing these registers requires careful handling to ensure safety and correctness.

Reading and Writing to Memory-Mapped Registers

To read from or write to a memory-mapped register, you typically need to know the specific address of the register. Rust’s type system and ownership model can help manage these operations safely.

 1const GPIO_BASE: usize = 0x4002_0000;
 2const GPIO_DATA_OFFSET: usize = 0x3FC;
 3
 4fn read_gpio_data() -> u32 {
 5    unsafe {
 6        let gpio_data_ptr = (GPIO_BASE + GPIO_DATA_OFFSET) as *const u32;
 7        *gpio_data_ptr
 8    }
 9}
10
11fn write_gpio_data(value: u32) {
12    unsafe {
13        let gpio_data_ptr = (GPIO_BASE + GPIO_DATA_OFFSET) as *mut u32;
14        *gpio_data_ptr = value;
15    }
16}

Note: The unsafe keyword is necessary because we’re performing raw pointer dereferencing, which bypasses Rust’s safety checks.

Using Embedded HAL Crates

The Embedded HAL is a set of traits that provide a standard interface for interacting with hardware peripherals. It abstracts over the specifics of different microcontrollers, allowing for more portable and reusable code.

Example: Using GPIO with Embedded HAL

1use embedded_hal::digital::v2::OutputPin;
2use stm32f1xx_hal::gpio::gpioc::PC13;
3use stm32f1xx_hal::gpio::{Output, PushPull};
4
5fn toggle_led(led: &mut PC13<Output<PushPull>>) {
6    led.set_high().unwrap();
7    // Delay for a bit
8    led.set_low().unwrap();
9}

Working with Common Interfaces

General-Purpose Input/Output (GPIO)

GPIO pins are used for digital input and output. They can be configured as either input or output pins and are often used for controlling LEDs, reading button states, etc.

 1use embedded_hal::digital::v2::{InputPin, OutputPin};
 2
 3fn read_button(button: &dyn InputPin) -> bool {
 4    button.is_high().unwrap()
 5}
 6
 7fn control_led(led: &mut dyn OutputPin, state: bool) {
 8    if state {
 9        led.set_high().unwrap();
10    } else {
11        led.set_low().unwrap();
12    }
13}

Inter-Integrated Circuit (I2C)

I2C is a multi-master, multi-slave, packet-switched, single-ended, serial communication bus. It’s commonly used for connecting low-speed peripherals to processors and microcontrollers.

 1use embedded_hal::blocking::i2c::{Write, Read};
 2
 3fn read_sensor_data<I2C, E>(i2c: &mut I2C, address: u8) -> Result<u8, E>
 4where
 5    I2C: Write<Error = E> + Read<Error = E>,
 6{
 7    let mut buffer = [0];
 8    i2c.write(address, &[0x00])?;
 9    i2c.read(address, &mut buffer)?;
10    Ok(buffer[0])
11}

Serial Peripheral Interface (SPI)

SPI is a synchronous serial communication interface used for short-distance communication, primarily in embedded systems.

1use embedded_hal::blocking::spi::Transfer;
2
3fn transfer_data<SPI, E>(spi: &mut SPI, data: &mut [u8]) -> Result<(), E>
4where
5    SPI: Transfer<u8, Error = E>,
6{
7    spi.transfer(data)?;
8    Ok(())
9}

Universal Asynchronous Receiver-Transmitter (UART)

UART is a hardware communication protocol that uses asynchronous serial communication with configurable speed.

 1use embedded_hal::serial::{Read, Write};
 2
 3fn send_data<UART, E>(uart: &mut UART, data: u8) -> Result<(), E>
 4where
 5    UART: Write<u8, Error = E>,
 6{
 7    uart.write(data)?;
 8    Ok(())
 9}
10
11fn receive_data<UART, E>(uart: &mut UART) -> Result<u8, E>
12where
13    UART: Read<u8, Error = E>,
14{
15    uart.read()
16}

Safety Considerations

When interfacing with hardware, safety is paramount. Rust’s ownership model and type system help prevent common errors such as data races and null pointer dereferences. However, when using unsafe code to access hardware registers, it’s crucial to ensure that:

  • The memory addresses are correct and valid.
  • The operations do not cause undefined behavior.
  • Access to shared resources is properly synchronized.

Tools for Generating Peripheral Access Crates

The svd2rust tool is used to generate Rust code from SVD (System View Description) files, which describe the registers and peripherals of a microcontroller. This tool automates the creation of safe, type-checked access to hardware registers.

Using svd2rust

  1. Obtain the SVD file for your microcontroller.
  2. Run svd2rust to generate the Rust code.
  3. Integrate the generated code into your project.
1svd2rust -i path/to/svd/file.svd

Visualizing Peripheral Interfacing

To better understand how these components interact, consider the following diagram illustrating the flow of data between a microcontroller and its peripherals:

    graph TD;
	    A["Microcontroller"] -->|GPIO| B["LED"];
	    A -->|I2C| C["Sensor"];
	    A -->|SPI| D["Display"];
	    A -->|UART| E["Computer"];

Diagram Description: This diagram shows a microcontroller interfacing with various peripherals using different communication protocols: GPIO for an LED, I2C for a sensor, SPI for a display, and UART for communication with a computer.

Knowledge Check

  • Question: What is the purpose of memory-mapped registers in embedded systems?
  • Exercise: Modify the GPIO example to toggle an LED at a different frequency.

Embrace the Journey

Interfacing with hardware peripherals in Rust is a rewarding endeavor that combines low-level programming with high-level safety guarantees. Remember, this is just the beginning. As you progress, you’ll gain more confidence in building robust and efficient embedded systems. Keep experimenting, stay curious, and enjoy the journey!

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026