Submitted by admin on Tue, 09/19/2023 - 15:34

1. Introduction

This tutorial is dedicated to the implementation of few BSP functions. In the world of embedded systems, BSP stands for “Board Support Package”. In simple words, it is a collection of functions to address on-board components such as I/Os, displays, sensors, communication interfaces… A collection of functions that simplifies the software interface with a given peripheral is also called a library, or a driver. Nucleo boards are rather poor in terms of accessories. Basically, you have a user LED, a user button, and a serial interface through the ST-Link dongle. LEDs and Buttons are directly interfaced with MCU pins in output or input modes. These are configured by means of GPIO (General Purpose Input Output) peripherals.

2. Starting project and first commit

Start VSCode and open my_project project. At this step, my_project is a rather clean starting project you've created by cloning blink project project. my_project project will be used all along subsequent tutorials.

We will start by some housework to get a clean starting point. In the main() function, remove the LED blinking code. In the SystemClock_Config(), we don't need the MCO on PA8 anymore as it was there only to monitor clock frequency. When done, your main.rs may look like this (without taking into account system_core_clock_update which should stay unchanged at the bottom of the main.rs file):

#![no_std]
#![no_main]

use panic_halt as _;
use stm32f0 as _;

use cortex_m_rt::entry;
use stm32f0::stm32f0x2::Peripherals;

#[entry]
fn main() -> ! {
    // Get peripherals
    let peripherals = Peripherals::take().unwrap();

    // Configure System Clock
    let fclk = system_clock_config(&peripherals).unwrap();

    loop {}
}

/*
 *  Clock configuration for the Nucleo STM32F072RB board
 *  HSE input Bypass Mode    -> 8MHz
 *  SYSCLK, AHB, APB1    -> 48MHz
 *
 *  Laurent Latorre - 05/08/2017
 *  Translated to Rust by Paul Delestrac - 15/04/2022
 */
#[derive(Debug)]
enum ClockConfigError {
    HSEError,
    PLLError,
    SWError,
}

fn system_clock_config(peripherals: &Peripherals) -> Result<u32, ClockConfigError> {
    let (mut hse_status, mut pll_status, mut sw_status): (bool, bool, bool);
    let mut timeout: i32;

    let rcc = &peripherals.RCC;
    let flash = &peripherals.FLASH;

    // Start HSE in Bypass Mode
    rcc.cr.modify(|_, w| w.hsebyp().bypassed().hseon().on());

    // Wait until HSE is ready
    timeout = 1000;
    loop {
        hse_status = rcc.cr.read().hserdy().is_ready();
        timeout -= 1;
        if (hse_status) || (timeout <= 0) {
            break;
        };
    }

    if timeout == 0 {
        return Err(ClockConfigError::HSEError); // HSE Error
    };

    // Select HSE as PLL input source
    rcc.cfgr.modify(|_, w| w.pllsrc().hse_div_prediv());

    // Set PLL Prediv to /1
    rcc.cfgr2.modify(|_, w| w.prediv().div1());

    // Set PLL MUL to x6
    rcc.cfgr.modify(|_, w| w.pllmul().mul6());

    // Enable the main PLL
    rcc.cr.modify(|_, w| w.pllon().on());

    // Wait until PLL is ready
    timeout = 1000;
    loop {
        pll_status = rcc.cr.read().pllrdy().is_ready();
        timeout -= 1;
        if (pll_status) || (timeout <= 0) {
            break;
        };
    }

    if timeout == 0 {
        return Err(ClockConfigError::PLLError); // PLL Error
    };

    // Set AHB and APB1 prescaler to /1
    rcc.cfgr.modify(|_, w| w.hpre().div1().ppre().div1());

    // Enable FLASH Prefetch Buffer and set Flash Latency (required for high speed)
    flash
        .acr
        .modify(|_, w| w.prftbe().enabled().latency().ws1()); //

    // Select the main PLL as system clock source
    rcc.cfgr.modify(|_, w| w.sw().pll());

    // Wait until PLL becomes main switch input
    timeout = 1000;
    loop {
        sw_status = rcc.cfgr.read().sws().is_pll();
        timeout -= 1;
        if (sw_status) || (timeout <= 0) {
            break;
        }
    }

    if timeout == 0 {
        return Err(ClockConfigError::SWError); // SW Error
    }

    // Return core clock frequency value
    return Ok(system_core_clock_update(&peripherals));
}

Save the code (Ctrl + S) and start a debug session to make sure there is no error or warning. Verify fclk value...

3. LED driver

A look at the board schematics tells us that the green user LED is connected to the pin PA5 of the MCU. Given that LED cathode goes to ground, the LED is "ON" when PA5 is at its high logic voltage.

 

Let us take a moment to think about what functions we would like to have in order to play with the LED:

  • A function that turn the LED on
  • A function that turn the LED off
  • A function that toggle the LED state

Note that it would have been possible to use a single function that sets the LED state using an argument (ON/OFF/Toggle). That's a matter of taste...

We know that a MCU pin needs to be configured to serve as an output. If we include the pin configuration into the previous functions, it will be done every time we want to change the LED state. This is a waste of both CPU time and power consumption as configuration does not change once it is done. Therefore, let us write a separate function to address pin configuration, that will be called only once at the beginning of the main code.

Create a new source file bsp.rs under bsp/ folder.

Open, edit bsp.rs and write the functions.

In the following, the function belongs to the BSP, concerns the LED, and its purpose is the initialization of the MCU pin. Let’s name the function bsp_led_init():

// * bsp.rs
//  *
//  *  Created on: 5 août 2017
//  *      Author: Laurent
//  *  Translated to Rust on: 15 avril 2022
//  *      Author: Paul

use stm32f0::stm32f0x2::Peripherals;

/*
 * bsp_led_init()
 * Initialize LED pin (PA5) as a High-Speed Push-Pull output
 * Set LED initial state to OFF
 */
pub fn bsp_led_init(peripherals: &Peripherals) {
    let rcc = &peripherals.RCC;
    let gpioa = &peripherals.GPIOA;

    // Enable GPIOA clock
    rcc.ahbenr.modify(|_, w| w.iopaen().enabled());

    // Configure PA5 as output
    gpioa.moder.modify(|_, w| w.moder5().output());

    // Configure PA5 as Push-Pull output
    gpioa.otyper.modify(|_, w| w.ot5().push_pull());

    // Configure PA5 as High-Speed Output
    gpioa.ospeedr.modify(|_, w| w.ospeedr5().high_speed());

    // Disable PA5 Pull-up/Pull-down
    gpioa.pupdr.modify(|_, w| w.pupdr5().floating());

    // Set Initial State OFF
    gpioa.bsrr.write(|w| w.br5().reset());
}

You must refer to the reference manual for a complete description of RCC AHBENR register, and GPIO MODER, OTYPER, OSPEEDR, PUPDR, BSRR registers.

Next comes the three controlling functions:

/*
 * bsp_led_on()
 * Turn ON LED on PA5
 */

pub fn bsp_led_on(peripherals: &Peripherals) {
    let gpioa = &peripherals.GPIOA;
    gpioa.bsrr.write(|w| w.bs5().set());
}

/*
 * bsp_led_off()
 * Turn OFF LED on PA5
 */

pub fn bsp_led_off(peripherals: &Peripherals) {
    let gpioa = &peripherals.GPIOA;
    gpioa.bsrr.write(|w| w.br5().reset());
}

/*
 * bsp_led_toggle()
 * Toggle LED on PA5
 */

pub fn bsp_led_toggle(peripherals: &Peripherals) {
    let gpioa = &peripherals.GPIOA;
    gpioa.odr.modify(|r, w| w.odr5().bit(!r.odr5().bit()));
}

That’s it. You’ve written a LED driver. Not too hard?

Test the LED functions in the main code. The example below must be used with the debugger stepping mode, otherwise the state change is too fast to be seen (unless you probe PA5 with an oscilloscope).

mod bsp;

#[entry]
fn main() -> ! {
    // Get peripherals
    let peripherals = Peripherals::take().unwrap();

    // Configure System Clock
    let fclk = system_clock_config(&peripherals).unwrap();

    // Initialize LED pin
    bsp::bsp_led_init(&peripherals);

    // Turn LED On
    bsp::bsp_led_on(&peripherals);

    // Turn LED Off
    bsp::bsp_led_off(&peripherals);

    loop {
        // Toggle LED state
        bsp::bsp_led_toggle(&peripherals);
    }
}

4. Push-button driver

Again, we start with the board schematics. The user button is a switch that connects PC13 pin to ground when pushed down. Otherwise, PC13 pin is held at high logic level by means of the pull-up resistor R30.

In order to interface the push-button, we will write two functions:

  • A function to initialize PC13 as an input pin
  • A function that return the state of the button (0 for released, 1 for pushed)

You can add those functions in bsp.rs, below LED functions.

/*
 * bsp_pb_init()
 * Initialize Push-Button pin (PC13) as input without Pull-up/Pull-down
 */

pub fn bsp_pb_init(peripherals: &Peripherals) {
    let rcc = &peripherals.RCC;
    let gpioc = &peripherals.GPIOC;

    // Enable GPIOC clock
    rcc.ahbenr.modify(|_, w| w.iopcen().enabled());

    // Configure PC13 as input
    gpioc.moder.modify(|_, w| w.moder13().input());

    // Disable PC13 Pull-up/Pull-down
    gpioc.pupdr.modify(|_, w| w.pupdr13().floating());
}

/*
 * bsp_pb_get_state()
 * Returns the state of the button (0=released, 1=pressed)
 */

pub fn bsp_pb_get_state(peripherals: &Peripherals) -> bool {
    let gpioc = &peripherals.GPIOC;

    return gpioc.idr.read().idr13().is_low();
}

Then, test your new functions in main.rs:

mod bsp;

#[entry]
fn main() -> ! {
    // Get peripherals
    let peripherals = Peripherals::take().unwrap();

    // Configure System Clock
    let _fclk = system_clock_config(&peripherals).unwrap();

    // Initialize LED pin
    bsp::bsp_led_init(&peripherals);

    // Initialize User-Button pin
    bsp::bsp_pb_init(&peripherals);

    // Turn LED On
    bsp::bsp_led_on(&peripherals);

    // Turn LED Off
    bsp::bsp_led_off(&peripherals);

    loop {
        // Turn LED On if User-Button is pushed down
        if bsp::bsp_pb_get_state(&peripherals) {
            bsp::bsp_led_on(&peripherals);
        }
        // Otherwise turn LED Off
        else {
            bsp::bsp_led_off(&peripherals);
        }
    }
}

The LED is ON only when you press the user-button.

5. Summary

In this tutorial, we have written simple drivers for both LED and user-button. Associated functions manipulate GPIOs with both input and output configurations.