Module 4: Blinking LED

At a Glance

Prerequisites

Blinking LED

Now, let's modify the getting_started project to toggle an LED once a second. First, we'll use our Hardware Abstraction Library (HAL). As a refresher, the key purpose of a HAL is to abstract platform details into a common, portable API to allow developers to write high-level application code independently of the underlying hardware.

At this point, you should be familiar with the basics of our build system and flashed your first project. You should also be aware of our Git Workflow and Coding Standards.

# Move to the firmware folder (if you weren't already there)
cd ~/shared/firmware
# Checkout the wip_getting_started branch (just in case)
git checkout wip_getting_started
# Check what branch we're on and if we have any changes
# This should say something along the lines of "nothing to commit, working directory clean"
git status

Controlling an LED - HAL

Let's update getting_started for basic General Purpose Input/Output (GPIO) pin control. We'll assume that our LED is an active-low (turns on when output is 0) and connected to PC9 (port C, pin 9).

main.c
// Turns the LED on and sits forever
#include <stdbool.h>
#include "gpio.h"
#include "log.h"

int main(void) {
  LOG_DEBUG("Hello World!\n");

  // Init GPIO module
  gpio_init();

  // Set up PC9 as an output, default to output 0 (GND)
  GpioAddress led = {
    .port = GPIO_PORT_B,  //
    .pin = 5,             //
  };
  GpioSettings gpio_settings = {
    .direction = GPIO_DIR_OUT,  //
    .state = GPIO_STATE_HIGH,    //
  };
  gpio_init_pin(&led, &gpio_settings);

  // Add infinite loop so we don't exit
  while (true) {
  }

  return 0;
}


# Just make we're following the style guide :)
make format
make lint


# Build and flash to the Controller Board
make program PROJECT=getting_started

Great! You should see a solid LED on the Controller Board. This is pretty simple.

Blinking an LED - HAL

Let's add a lot more code! Now, we'll use our soft timers to toggle the LED once a second.

main.c
// Blink an LED
#include <stdbool.h>
#include "gpio.h"
#include "interrupt.h"
#include "log.h"
#include "soft_timer.h"
#include "wait.h"

#define BLINK_LED_TIMEOUT_SECS 1

// Timeout callback
static void prv_blink_timeout(SoftTimerId timer_id, void *context) {
  GpioAddress *led = context;
  gpio_toggle_state(led);

  LOG_DEBUG("Toggling LED\n");

  // Schedule another timer - this creates a periodic timer
  soft_timer_start_seconds(BLINK_LED_TIMEOUT_SECS, prv_blink_timeout, led, NULL);
}

int main(void) {
  LOG_DEBUG("Hello World!\n");

  // Initialize our modules
  gpio_init();
  interrupt_init();
  soft_timer_init();

  // Set up the pin
  GpioAddress led = {
    .port = GPIO_PORT_B,  //
    .pin = 5,             //
  };
  GpioSettings gpio_settings = {
    .direction = GPIO_DIR_OUT,  //
    .state = GPIO_STATE_HIGH,    //
  };
  gpio_init_pin(&led, &gpio_settings);

  // Begin a timer
  soft_timer_start_seconds(BLINK_LED_TIMEOUT_SECS, prv_blink_timeout, &led, NULL);

  // Infinite loop
  while (true) {
    // Wait for interrupts
    wait();
  }

  return 0;
}
# Just make we're following the style guide :)
make format
make lint


# Build and flash to the Discovery board
make program PROJECT=getting_started PROBE=stlink-v2

Woah, there's a lot going on here! Do you understand how everything works?

Code Breakdown - HAL

First, it really helps to have the firmware folder open in your editor so you can reference our source code.

If you look at the headers we've included and their corresponding source files, things will make a lot more sense. We highly recommend looking at each module in-depth.

  • stdbool: Part of the C Standard Library. Defines booltrue and false.
  • gpio: Used to initialize and read/write GPIO. The read/write functions in this module are digital (i.e. true/false).
  • interrupt: Used to initialize interrupts across supported platforms. In this case, we need it for our soft timers.
  • log: Defines useful log macros that are retargeted to UART and stdout on STM32 and x86 respectively.
  • soft_timer: Software-based timers backed by a single hardware timer. Used to create arbitrary timeouts without consuming all of our hardware timers.
  • wait: Attempts to put the CPU to sleep until an interrupt. No-op on x86.

The control flow is relatively simple.

  1. Initialize all our modules
  2. Initialize the GPIO pin attached to the LED (output)
  3. Begin a timer with a callback to toggle the LED
  4. Sit in an infinite loop, waiting for the soft timer to timeout
  5. On timeout, toggle the LED and start a new soft timer to toggle the LED

A few things to note:

  • Most of our digital inputs and outputs are active-low. This means that GND (0V) is true/on and Vcc (3.3V) is false/off.
  • Did you notice the //'s and trailing commas in led and gpio_settings? To find out why, look at our Coding Standards. (Hint: It's related to clang-format)
  • On a similar note: prv_ as prefix for static functions, snake_case for variables, etc.

Try playing with the code - adjust the timeout, replace wait(), change the LED, add another LED at a different interval, etc.

Cleanup and Commit - HAL

You should know the drill:

# Format and lint!
make format
make lint


# Add all of our changes and commit
# Note that adding main.c would also work.
git add .
git commit -m "WIP: Added blinking LED"

In all honesty, we should've committed our code when we first got a solid LED. The jump from a solid LED to blinking LED is definitely worth a commit.

Blinking LED - Registers

Just for fun, let's take a look at what something like this might look like when accessing registers directly. For this, the STM32F0 reference manual is indespensible. It will tell you everything you'll ever need to know about working with the STM32F0.

This time, we'll dedicate a hardware timer entirely to toggling the LED and forego the ability to run on x86. We'll also hardcode everything.

main.c
#include "stm32f0xx.h"

static void prv_init_gpio(void) {
  // Enable GPIO clock - RM0091 6.4.6
  RCC->AHBENR |= RCC_AHBENR_GPIOCEN;

  // Set up GPIO as output - RM0091 8.3.10
  // Be sure to understand where these values are from and how they're calculated!
  // Understanding registers and how to read a datasheet is critical for debugging.

  // Set state first so we don't have a period where we're unsure of the output value
  // Use atomic bit set/reset register to reset the pin's output - RM0091 8.4.7
  // Prefer using BSRR over ODR - ODR requires read-modify-write
  GPIOC->BSRR = GPIO_BSRR_BR_9;

  // Mode: Output (0b01) - RM0091 8.4.1
  GPIOC->MODER &= GPIO_MODER_MODER9;
  GPIOC->MODER |= GPIO_MODER_MODER9_0;

  // Type: Push-pull (0b0) - RM0091 8.4.2
  GPIOC->OTYPER &= GPIO_OTYPER_OT_9;

  // Speed: Low (0b00) - RM0091 8.4.3
  GPIOC->OSPEEDR &= GPIO_OSPEEDER_OSPEEDR9;

  // Pull-up/pull-down: None (0b00) - RM0091 8.4.4
  GPIOC->PUPDR &= GPIO_PUPDR_PUPDR9;
}

static void prv_init_timer(void) {
  // Enable TIM2 clock - RM0091 6.4.5
  // Look at the clock tree to determine what clock peripherals are attached to (Fig 10, 11)
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

  // CR1: keep disabled (0b0), no clock division (0b0), up counter (0b0) - RM0091 18.4.2
  // See also 18.3.2
  TIM2->CR1 = 0;
  // Clock prescaler: Divider = PSC + 1 - RM0091 18.4.11
  // Assume TIM2 is connected to a 48MHz clock, divide by 48 to get a 1MHz clock
  TIM2->PSC = 48 - 1;
  // Auto reload: 0 to 999999 = 1000000 ticks - RM0091 18.4.12
  TIM2->ARR = 1000000 - 1;
  // Counter: reset to 0 - RM0091 18.4.10
  TIM2->CNT = 0;
  // Force an update event immediately - RM0091 18.4.6
  TIM2->EGR = TIM_EGR_UG;

  // Clear interrupt flag: Update (0b0) - RM0091 18.4.5
  // We clear the flag first so it isn't triggered by accident when we enable the interrupt source
  TIM2->SR &= ~TIM_SR_UIF;

  // Enable SYSCFG clock
  RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

  // Set NVIC priority to highest
  NVIC_EnableIRQ(TIM2_IRQn);
  NVIC_SetPriority(TIM2_IRQn, 0);

  // Enable TIM2
  TIM2->CR1 |= TIM_CR1_CEN;

  // Enable interrupt source: Update enabled (0b1) - RM0091 18.4.4
  // We enable it after enabling the timer clock to make sure we don't get an update event
  // from the forced update
  TIM2->DIER |= TIM_DIER_UIE;
}

int main(void) {
  prv_init_gpio();
  prv_init_timer();

  while (1) {
    __WFI();
  }

  return 0;
}

void TIM2_IRQHandler(void) {
  // Toggle ODR value - Could also store a static variable or use BSRR
  GPIOC->ODR ^= GPIO_ODR_9;
  // Clear interrupt flags
  TIM2->SR = 0;
}

Try to understand how this works! It's always useful to understand roughly how our HAL works. It also gives you an appreciation for how easy our HAL makes using these peripherals!

If you're working with other ICs, you'll likely need to read datasheets to understand how to communicate with them and what the chips are capable of. The STM32F0 reference manual is just a very long, very complex example for a very powerful chip. Personally, I find this part to be the most fulfilling and the most useful, especially when it comes to figuring out why things aren't working the way they should.

I'd recommend playing around with this example and comparing it to our HAL implementations. For example, try making it easy to change the LED or adjust the timing! Maybe add another LED or play with the timer peripheral modes.