Skip to main content

STM32 GPIO Basics

Introduction

General Purpose Input/Output (GPIO) pins are the fundamental interface between an STM32 microcontroller and the outside world. They allow your microcontroller to interact with external components like LEDs, buttons, sensors, and other devices. Understanding how to configure and use GPIO pins is essential for any embedded systems project using STM32 microcontrollers.

In this tutorial, we'll explore the basics of STM32 GPIO, including:

  • GPIO pin structure and characteristics
  • Configuring pins as inputs or outputs
  • Reading input states
  • Controlling output states
  • Common GPIO configurations and applications

STM32 GPIO Architecture

STM32 microcontrollers organize GPIO pins into ports, typically labeled as GPIOA, GPIOB, GPIOC, and so on. Each port can have up to 16 pins (0-15). For example, "PA5" refers to Port A, Pin 5.

GPIO Pin Structure

Each GPIO pin on an STM32 microcontroller has a complex internal structure that makes it highly versatile:

Key components of a GPIO pin include:

  • Mode selection: Input, output, analog, or alternate function
  • Output type: Push-pull or open-drain
  • Speed configuration: Low, medium, high, or very high speed
  • Pull-up/Pull-down resistors: Internal resistors that can be enabled
  • Alternate function selection: For connecting to internal peripherals

GPIO Initialization

Before using GPIO pins, you need to enable the clock for the corresponding GPIO port and configure the pin's properties.

Using STM32CubeIDE and HAL Libraries

The HAL (Hardware Abstraction Layer) library provides functions to simplify GPIO configuration:

c
// Enable clock for GPIOA
__HAL_RCC_GPIOA_CLK_ENABLE();

// Configure GPIO pin
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5; // Pin 5
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // Push-pull output
GPIO_InitStruct.Pull = GPIO_NOPULL; // No pull-up or pull-down
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // Low speed
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // Initialize GPIOA with these settings

Direct Register Access

For those who prefer direct register access or need more optimization:

c
// Enable clock for GPIOA
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// Configure PA5 as output (bits 10-11 in MODER register for pin 5)
GPIOA->MODER &= ~(0x3 << (5 * 2)); // Clear bits
GPIOA->MODER |= (0x1 << (5 * 2)); // Set as output

// Configure as push-pull (bit 5 in OTYPER register)
GPIOA->OTYPER &= ~(0x1 << 5);

// Set speed to low (bits 10-11 in OSPEEDR register)
GPIOA->OSPEEDR &= ~(0x3 << (5 * 2));

// Disable pull-up/pull-down (bits 10-11 in PUPDR register)
GPIOA->PUPDR &= ~(0x3 << (5 * 2));

GPIO as Output

Configuring a GPIO pin as an output allows you to control external devices like LEDs, relays, motors (through drivers), or communicate with other digital components.

Controlling Output State

With HAL functions:

c
// Set PA5 high (turn on LED)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

// Set PA5 low (turn off LED)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);

// Toggle PA5 (change state from high to low or low to high)
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);

With direct register access:

c
// Set PA5 high using BSRR (Bit Set Reset Register)
GPIOA->BSRR = GPIO_PIN_5;

// Set PA5 low using BSRR
GPIOA->BSRR = (GPIO_PIN_5 << 16);

// Toggle PA5
GPIOA->ODR ^= GPIO_PIN_5;

LED Blinking Example

Here's a complete example that blinks an LED connected to PA5 (this is the built-in LED on many STM32 development boards like the Nucleo):

c
#include "main.h"

int main(void)
{
/* MCU Configuration */
HAL_Init();
SystemClock_Config();

/* Initialize GPIO */
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/* Main program loop */
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Toggle LED
HAL_Delay(500); // Wait 500ms
}
}

GPIO as Input

Input pins allow your microcontroller to read external signals, such as button presses, sensor states, or communication from other devices.

Reading Input State

With HAL functions:

c
// Read state of pin PA0
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);

if (state == GPIO_PIN_SET)
{
// Pin is high (3.3V)
// Do something
}
else
{
// Pin is low (0V)
// Do something else
}

With direct register access:

c
// Read state of pin PA0
if ((GPIOA->IDR & GPIO_PIN_0) != 0)
{
// Pin is high
// Do something
}
else
{
// Pin is low
// Do something else
}

Button Input Example

Here's an example that reads a button connected to PA0 and toggles an LED on PA5 when the button is pressed:

c
#include "main.h"

int main(void)
{
/* MCU Configuration */
HAL_Init();
SystemClock_Config();

/* Initialize GPIO */
__HAL_RCC_GPIOA_CLK_ENABLE();

// Configure PA5 as output for LED
GPIO_InitTypeDef GPIO_LED = {0};
GPIO_LED.Pin = GPIO_PIN_5;
GPIO_LED.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_LED.Pull = GPIO_NOPULL;
GPIO_LED.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_LED);

// Configure PA0 as input for button with pull-up
GPIO_InitTypeDef GPIO_Button = {0};
GPIO_Button.Pin = GPIO_PIN_0;
GPIO_Button.Mode = GPIO_MODE_INPUT;
GPIO_Button.Pull = GPIO_PULLUP; // Enable pull-up resistor
HAL_GPIO_Init(GPIOA, &GPIO_Button);

// Variables for button debouncing
GPIO_PinState lastState = GPIO_PIN_SET; // Pulled up when not pressed
uint32_t lastDebounceTime = 0;
uint32_t debounceDelay = 50; // 50ms debounce period

/* Main program loop */
while (1)
{
// Read button state (will be GPIO_PIN_RESET when pressed with pull-up)
GPIO_PinState currentState = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);

// If state changed, reset debounce timer
if (currentState != lastState) {
lastDebounceTime = HAL_GetTick();
}

// If state has been stable for debounceDelay
if ((HAL_GetTick() - lastDebounceTime) > debounceDelay) {
// If button is pressed (LOW with pull-up)
if (currentState == GPIO_PIN_RESET) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Toggle LED

// Wait for button release to avoid multiple toggles
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// Do nothing while waiting
}
}
}

lastState = currentState;
}
}

Common GPIO Configurations

Input with Pull-up

c
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // Internal pull-up resistor enabled
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

With a pull-up, the pin reads as HIGH when nothing is connected, and LOW when connected to ground (e.g., when a button is pressed).

Input with Pull-down

c
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN; // Internal pull-down resistor enabled
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

With a pull-down, the pin reads as LOW when nothing is connected, and HIGH when connected to 3.3V (e.g., when a button is pressed).

Open-Drain Output

c
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // Open-drain output
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

An open-drain output can pull the pin to ground but cannot drive it high. It's useful for shared buses (like I2C) or level shifting.

Interrupt Input

c
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; // Interrupt on rising edge
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// Enable the EXTI interrupt
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

Then implement the interrupt handler:

c
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// Your code to handle the interrupt
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // For example, toggle an LED
}
}

Real-World Applications

Traffic Light Controller

c
#include "main.h"

// Define pins for traffic lights
#define RED_PIN GPIO_PIN_0
#define YELLOW_PIN GPIO_PIN_1
#define GREEN_PIN GPIO_PIN_2

int main(void)
{
/* MCU Configuration */
HAL_Init();
SystemClock_Config();

/* Initialize GPIO */
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = RED_PIN | YELLOW_PIN | GREEN_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/* Main program loop */
while (1)
{
// Green light
HAL_GPIO_WritePin(GPIOA, RED_PIN | YELLOW_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GREEN_PIN, GPIO_PIN_SET);
HAL_Delay(5000); // 5 seconds

// Yellow light
HAL_GPIO_WritePin(GPIOA, RED_PIN | GREEN_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, YELLOW_PIN, GPIO_PIN_SET);
HAL_Delay(2000); // 2 seconds

// Red light
HAL_GPIO_WritePin(GPIOA, YELLOW_PIN | GREEN_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, RED_PIN, GPIO_PIN_SET);
HAL_Delay(5000); // 5 seconds
}
}

Keypad Interface

c
#include "main.h"

// Define row and column pins
#define ROW1_PIN GPIO_PIN_0
#define ROW2_PIN GPIO_PIN_1
#define ROW3_PIN GPIO_PIN_2
#define ROW4_PIN GPIO_PIN_3
#define COL1_PIN GPIO_PIN_4
#define COL2_PIN GPIO_PIN_5
#define COL3_PIN GPIO_PIN_6

#define ALL_ROWS (ROW1_PIN | ROW2_PIN | ROW3_PIN | ROW4_PIN)
#define ALL_COLS (COL1_PIN | COL2_PIN | COL3_PIN)

// Keypad layout
const char keymap[4][3] = {
{'1', '2', '3'},
{'4', '5', '6'},
{'7', '8', '9'},
{'*', '0', '#'}
};

char scanKeypad(void)
{
// Check each row
for (int row = 0; row < 4; row++)
{
// Set current row LOW, others HIGH
HAL_GPIO_WritePin(GPIOA, ALL_ROWS, GPIO_PIN_SET);

uint16_t rowPin;
switch (row) {
case 0: rowPin = ROW1_PIN; break;
case 1: rowPin = ROW2_PIN; break;
case 2: rowPin = ROW3_PIN; break;
case 3: rowPin = ROW4_PIN; break;
}

HAL_GPIO_WritePin(GPIOA, rowPin, GPIO_PIN_RESET);

// Check all columns
for (int col = 0; col < 3; col++)
{
uint16_t colPin;
switch (col) {
case 0: colPin = COL1_PIN; break;
case 1: colPin = COL2_PIN; break;
case 2: colPin = COL3_PIN; break;
}

// If column is LOW, button is pressed
if (HAL_GPIO_ReadPin(GPIOA, colPin) == GPIO_PIN_RESET)
{
HAL_Delay(20); // Debounce
if (HAL_GPIO_ReadPin(GPIOA, colPin) == GPIO_PIN_RESET)
{
// Wait for release
while (HAL_GPIO_ReadPin(GPIOA, colPin) == GPIO_PIN_RESET) {}
return keymap[row][col];
}
}
}
}

return 0; // No key pressed
}

int main(void)
{
/* MCU Configuration */
HAL_Init();
SystemClock_Config();

/* Initialize GPIO */
__HAL_RCC_GPIOA_CLK_ENABLE();

// Configure rows as outputs
GPIO_InitTypeDef GPIO_RowStruct = {0};
GPIO_RowStruct.Pin = ALL_ROWS;
GPIO_RowStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_RowStruct.Pull = GPIO_NOPULL;
GPIO_RowStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_RowStruct);

// Configure columns as inputs with pull-up
GPIO_InitTypeDef GPIO_ColStruct = {0};
GPIO_ColStruct.Pin = ALL_COLS;
GPIO_ColStruct.Mode = GPIO_MODE_INPUT;
GPIO_ColStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_ColStruct);

char lastKey = 0;

/* Main program loop */
while (1)
{
char key = scanKeypad();
if (key != 0 && key != lastKey)
{
lastKey = key;
// Do something with the key
// For example, send it via UART
}
else if (key == 0)
{
lastKey = 0;
}

HAL_Delay(10); // Brief delay
}
}

Best Practices and Tips

  1. Always Enable the Clock: Always enable the GPIO port clock before configuring the pins.

  2. Handle Unused Pins: Configure unused pins as analog inputs to reduce power consumption:

    c
    GPIO_InitStruct.Pin = UNUSED_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOx, &GPIO_InitStruct);
  3. Avoid Floating Inputs: Always use pull-up or pull-down resistors for input pins to avoid unstable readings.

  4. Debounce Mechanical Inputs: Always implement debouncing for mechanical switches and buttons.

  5. Consider Current Limitations: STM32 GPIO pins can typically source/sink around 8-20mA depending on the specific microcontroller. Use external drivers for higher current loads.

  6. Protect I/O Pins: Add current-limiting resistors and consider voltage protection for pins connected to external circuits.

  7. Use GPIO Speed Appropriately: Set the GPIO speed based on your application needs. Higher speeds consume more power and create more noise but allow faster state changes.

Common Pitfalls

  1. Forgetting to Enable Clock: If your GPIO isn't working, check if you've enabled the clock for the GPIO port.

  2. Pin Configuration Conflicts: Make sure pins aren't being used by other peripherals or alternate functions.

  3. Current Overload: Connecting LEDs directly to GPIO pins without current-limiting resistors can damage the microcontroller.

  4. Voltage Level Mismatches: STM32 operates at 3.3V, but some external devices operate at 5V or other voltages. Ensure proper level shifting.

Summary

STM32 GPIO pins are versatile interfaces that allow your microcontroller to interact with the external world. Key points to remember:

  • GPIO pins can be configured as inputs or outputs with various modes and options
  • Always enable the GPIO port clock before configuration
  • Use pull-up or pull-down resistors for input pins to avoid floating inputs
  • Implement debouncing for mechanical inputs like buttons
  • Consider current limitations and protect your I/O pins
  • Choose appropriate GPIO speed settings for your application

By mastering GPIO control, you've laid a solid foundation for more advanced STM32 peripherals and applications.

Exercises

  1. Create a binary counter using 8 LEDs connected to GPIOA pins 0-7 that increments every second.

  2. Implement a simple reaction time game using a button and an LED. The LED turns on randomly, and the player must press the button as quickly as possible.

  3. Build a password lock system using a keypad and LEDs to indicate success or failure.

  4. Design a traffic light controller with pedestrian crossing button.

  5. Create a simple digital input/output example that reads the state of 4 DIP switches and displays their states on 4 LEDs.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)