STM32 For Beginners [3]: LEDs and inputs


In the previous part we got our STM to print stuff out via UART and figured out how view this printout on our PC. Using this, we did the typical “Hello world” example of programming. Well, while a printout is indeed the common first step for setting up software development enviornments, there’s another, more simple “Hello world” that’s common in the embedded world: flashing a LED. We’ll be doing that now.

GPIO - General Purpose Input-Output

On an MCU, one of the easiest ways to interact with the world (and other devices) is via General Purpose Input-Output pins. A GPIO pin can be set as an input or as an output. For the input, the line will translate the voltage that’s being applied to it into a true or false (1 or 0) value that we can read and respond to with our code. For the output, we can write a true or false value from our code onto the line, and in response to that, the MCU will either put a voltage onto that line or leave it connected to ground.

For the STM32 series of MCUs, the digital logic voltage is 3.3 volts. This, in principle, that a true value will correspond to a voltage of 3.3 volts, and a false value will correspond to a value of 0 volts. In reality, there’s a bit more to consider. First, some pins on the STM32 controllers capable of handling up to 5 volts of input. These pins are marked in the respective controller’s datasheet as “FT” - “Five volt Tolerant”. The other exception is that there’s some tolerances built into differentiating between true and false: we could say that a true value is a voltage between 2.3 and 3.3 volts. Again, exact specifications can be found in the datasheet.

Writing into GPIO Output

We previously configured PB3 as our digital output. Now’s the time to use it. Specifically, we named the pin as “OUT_LED”. With this, STM32CubeMX has generated the following defines:

  • OUT_LED_GPIO_Port - this is an alias for PORTB of the MCU,
  • OUT_LED_Pin - this an alias for pin number.

On most MCUs, pins are divided into different ports. Each port has a specific amount of pins, and to address a specific pin, you must know both the pin’s port and the pin’s number in that port to control it. For PB3, the port is PORTB, and the pin itself is numbered as 3 in that port. These defines give us a more meaningful alias via which to access these specifiers.

For writing into the pin, ST’s HAL provides us with the following functions:

  • HAL_GPIO_WritePin(GPIOx, GPIO_Pin, PinState) - for specifically turning the line high or low,
  • HAL_GPIO_TogglePin(GPIOx, GPIO_Pin) - for toggling the pin from its previous state.

In both cases, GPIOx is the argument for the port, and GPIO_Pin is the argument for the pin number. PinState is either GPIO_PIN_SET for writing a 1, or GPIO_PIN_RESET for writing a 0 onto the line.

So, if we wanted to flash the LED, we would write this into our main():

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_GPIO_WritePin(OUT_LED_GPIO_Port, OUT_LED_Pin, GPIO_PIN_SET); // LED will turn on.
    HAL_Delay(1000); // Keep the LED on for a second.

    HAL_GPIO_WritePin(OUT_LED_GPIO_Port, OUT_LED_Pin, GPIO_PIN_RESET); // LED will turn off.
    HAL_Delay(1000); // Keep the LED off for a second.
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Build and run the program, and you will see LD3 on the Nucleo board (assuming STM32F303k8 Nucleo) start flashing.

Obviously, with the knowledge that we also have HAL_GPIO_TogglePin, this code could be written a bit shorter as well:

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_GPIO_TogglePin(OUT_LED_GPIO_Port, OUT_LED_Pin); // LED will be toggled from its previous state.
    HAL_Delay(1000); // Maintain state for a second.
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Congratulations, you’ve flashed a LED! You’ve now done your second “Hello world” exercise! Now let’s make the LED actually respond to some external input.

Reading GPIO Inputs (and buttons!)

Let’s say that now we want to drive our LED based on the state of another digital input. We designated PB0 as a GPIO Input, IN_BTN, so let’s use it!

To read a GPIO, we have a single function: HAL_GPIO_ReadPin(GPIOx, GPIO_Pin). It will return a GPIO_PinState value, which is the same type we used as an input before. So it’s either a value of GPIO_PIN_SET for when the line is high, or GPIO_PIN_RESET for when the piin is set low. We’ll review this a bit more indepth shortly.

First, we must also figure out how to manipulate the pin. If you don’t have a push button or something else on hand, then all you need is a jumper wire: connect one end of the jumper wire to PB0 (marked D3 on the board itself) and the other end either to a GND or 3V3 pin.

One thought exercise to consider here is: “What happens when we leave the wire disconnected?” The answer is: your pin will float. It may take a high value or a low value, due to inductance. This is why we configured the pin with a pull-down in the beginning: the pull-down resistor will assert a low state on the pin if it’s not connected to a stronger source. This will negate the floating.

So, we can bring the line high or low by connecting the wire to 3V3 or GND. Now, onto reading it. To read it, we must call the function and save the result into a variable, for example. And then, make our logic respond to this value: for example, when the pin is at a state of GPIO_PIN_SET, we turn the LED on, else, we turn it off.

An example code for this is:

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    GPIO_PinState button_state = HAL_GPIO_ReadPin(IN_BTN_GPIO_Port, IN_BTN_Pin); // declare a new variable and read the pin state into it.
    if (button_state == GPIO_PIN_SET) // Check if the button is being pressed.
    {
      // Write the LED on.
      HAL_GPIO_WritePin(OUT_LED_GPIO_Port, OUT_LED_Pin, GPIO_PIN_SET);
    }
    else
    {
      // Write the LED off.
      HAL_GPIO_WritePin(OUT_LED_GPIO_Port, OUT_LED_Pin, GPIO_PIN_RESET);
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

You will notice that we defined a variable here as well. We made it of type GPIO_PinState, as that is what HAL_GPIO_ReadPin returns. And then we can respond to this using the if statements. Obviously you can also write some printf’s in or around those statements to see some more of the action.

For example, if we wanted to also print out the state of the pin:

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    GPIO_PinState button_state = HAL_GPIO_ReadPin(IN_BTN_GPIO_Port, IN_BTN_Pin); // declare a new variable and read the pin state into it.

    printf("The button state is: %d\n\r", button_state);

    if (button_state == GPIO_PIN_SET) // Check if the button is being pressed.
    {
      // Write the LED on.
      HAL_GPIO_WritePin(OUT_LED_GPIO_Port, OUT_LED_Pin, GPIO_PIN_SET);
    }
    else
    {
      // Write the LED off.
      HAL_GPIO_WritePin(OUT_LED_GPIO_Port, OUT_LED_Pin, GPIO_PIN_RESET);
    }

    HAL_Delay(250); // Delay to stop printout flooding.
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

This will work by printing the button state out as a 1 or 0. Note the necessity for a delay, this will make the logic slower as well, obviously.

But, that’s it for this lesson. Next time we’ll review motors and PWM.