GPIO Control Tutorial

This tutorial teaches you advanced GPIO control techniques using the Nexus HAL. You’ll learn how to read inputs, control outputs, handle interrupts, and implement debouncing.

Learning Objectives

By the end of this tutorial, you will:

  • Configure GPIO pins as inputs and outputs

  • Read button states with proper debouncing

  • Use GPIO interrupts for event-driven programming

  • Control multiple LEDs with patterns

  • Understand GPIO electrical characteristics

Prerequisites

  • Completed Your First Nexus Application tutorial

  • STM32F4 Discovery board or compatible hardware

  • Basic understanding of digital I/O concepts

Hardware Setup

For this tutorial, you’ll use:

Outputs (LEDs on STM32F4 Discovery):

  • PD12: Green LED

  • PD13: Orange LED

  • PD14: Red LED

  • PD15: Blue LED

Input (User Button):

  • PA0: User button (active high)

No additional wiring is required for the STM32F4 Discovery board.

Part 1: Reading GPIO Inputs

Let’s start by reading the user button state.

GPIO Input Workflow

The following diagram shows the workflow for reading GPIO inputs and controlling outputs:

        flowchart TD
    START([Start]) --> INIT_HAL[Initialize HAL]
    INIT_HAL --> INIT_BTN[Configure Button GPIO as Input]
    INIT_BTN --> INIT_LED[Configure LED GPIO as Output]
    INIT_LED --> LOOP{Main Loop}

    LOOP --> READ[Read Button State]
    READ --> CHECK{Button Pressed?}

    CHECK -->|Yes| LED_ON[Turn LED On]
    CHECK -->|No| LED_OFF[Turn LED Off]

    LED_ON --> DELAY[Delay/Debounce]
    LED_OFF --> DELAY
    DELAY --> LOOP

    style START fill:#e1f5ff
    style INIT_HAL fill:#fff4e1
    style INIT_BTN fill:#ffe1f5
    style INIT_LED fill:#ffe1f5
    style READ fill:#e1ffe1
    style LED_ON fill:#ffe1e1
    style LED_OFF fill:#ffe1e1
    

Basic Button Reading

#include "hal/hal.h"

/* Button pin */
#define BTN_PORT    HAL_GPIO_PORT_A
#define BTN_PIN     0

/* LED pin */
#define LED_PORT    HAL_GPIO_PORT_D
#define LED_PIN     12

/**
 * \brief           Initialize button GPIO
 */
static hal_status_t button_init(void) {
    hal_gpio_config_t config = {
        .direction = HAL_GPIO_DIR_INPUT,
        .pull = HAL_GPIO_PULL_DOWN,  /* Pull-down for active-high button */
        .output_mode = HAL_GPIO_OUTPUT_PP,  /* Not used for input */
        .speed = HAL_GPIO_SPEED_LOW,
        .init_level = HAL_GPIO_LEVEL_LOW
    };

    return hal_gpio_init(BTN_PORT, BTN_PIN, &config);
}

/**
 * \brief           Initialize LED GPIO
 */
static hal_status_t led_init(void) {
    hal_gpio_config_t config = {
        .direction = HAL_GPIO_DIR_OUTPUT,
        .pull = HAL_GPIO_PULL_NONE,
        .output_mode = HAL_GPIO_OUTPUT_PP,
        .speed = HAL_GPIO_SPEED_LOW,
        .init_level = HAL_GPIO_LEVEL_LOW
    };

    return hal_gpio_init(LED_PORT, LED_PIN, &config);
}

int main(void) {
    hal_init();
    button_init();
    led_init();

    while (1) {
        /* Read button state */
        hal_gpio_level_t button_state = hal_gpio_read(BTN_PORT, BTN_PIN);

        /* Control LED based on button */
        if (button_state == HAL_GPIO_LEVEL_HIGH) {
            hal_gpio_write(LED_PORT, LED_PIN, HAL_GPIO_LEVEL_HIGH);
        } else {
            hal_gpio_write(LED_PORT, LED_PIN, HAL_GPIO_LEVEL_LOW);
        }
    }

    return 0;
}

Key Points:

  • Use HAL_GPIO_DIR_INPUT for input pins

  • Configure pull resistors based on button type (pull-down for active-high, pull-up for active-low)

  • hal_gpio_read() returns HAL_GPIO_LEVEL_HIGH or HAL_GPIO_LEVEL_LOW

Button Debouncing

Mechanical buttons can “bounce” when pressed, causing multiple transitions. Here’s how to debounce:

#define DEBOUNCE_DELAY_MS  50

/**
 * \brief           Read button with debouncing
 * \return          true if button is pressed (debounced)
 */
static bool button_read_debounced(void) {
    static uint32_t last_change_time = 0;
    static bool last_stable_state = false;

    /* Read current state */
    bool current_state = (hal_gpio_read(BTN_PORT, BTN_PIN) == HAL_GPIO_LEVEL_HIGH);

    /* Check if state changed */
    if (current_state != last_stable_state) {
        uint32_t now = hal_get_tick();

        /* Check if enough time has passed since last change */
        if ((now - last_change_time) >= DEBOUNCE_DELAY_MS) {
            last_stable_state = current_state;
            last_change_time = now;
        }
    }

    return last_stable_state;
}

/* Usage in main loop */
while (1) {
    if (button_read_debounced()) {
        hal_gpio_write(LED_PORT, LED_PIN, HAL_GPIO_LEVEL_HIGH);
    } else {
        hal_gpio_write(LED_PORT, LED_PIN, HAL_GPIO_LEVEL_LOW);
    }

    hal_delay_ms(10);  /* Small delay to reduce CPU usage */
}

Edge Detection

Detect button press and release events:

/**
 * \brief           Detect button edges
 * \param[out]      pressed: Set to true on press event
 * \param[out]      released: Set to true on release event
 */
static void button_detect_edges(bool* pressed, bool* released) {
    static bool last_state = false;
    bool current_state = button_read_debounced();

    *pressed = false;
    *released = false;

    if (current_state && !last_state) {
        *pressed = true;  /* Rising edge */
    } else if (!current_state && last_state) {
        *released = true;  /* Falling edge */
    }

    last_state = current_state;
}

/* Usage */
while (1) {
    bool pressed, released;
    button_detect_edges(&pressed, &released);

    if (pressed) {
        /* Button was just pressed */
        hal_gpio_toggle(LED_PORT, LED_PIN);
    }

    hal_delay_ms(10);
}

Part 2: Advanced Output Control

LED Patterns

Create complex LED patterns:

/* LED array for easy access */
typedef struct {
    hal_gpio_port_t port;
    hal_gpio_pin_t pin;
} led_t;

static const led_t leds[] = {
    {HAL_GPIO_PORT_D, 12},  /* Green */
    {HAL_GPIO_PORT_D, 13},  /* Orange */
    {HAL_GPIO_PORT_D, 14},  /* Red */
    {HAL_GPIO_PORT_D, 15}   /* Blue */
};

#define NUM_LEDS  (sizeof(leds) / sizeof(leds[0]))

/**
 * \brief           Initialize all LEDs
 */
static hal_status_t leds_init(void) {
    hal_gpio_config_t config = {
        .direction = HAL_GPIO_DIR_OUTPUT,
        .pull = HAL_GPIO_PULL_NONE,
        .output_mode = HAL_GPIO_OUTPUT_PP,
        .speed = HAL_GPIO_SPEED_LOW,
        .init_level = HAL_GPIO_LEVEL_LOW
    };

    for (size_t i = 0; i < NUM_LEDS; i++) {
        if (hal_gpio_init(leds[i].port, leds[i].pin, &config) != HAL_OK) {
            return HAL_ERR_FAIL;
        }
    }

    return HAL_OK;
}

/**
 * \brief           Set LED state
 */
static void led_set(size_t index, bool on) {
    if (index < NUM_LEDS) {
        hal_gpio_write(leds[index].port, leds[index].pin,
                      on ? HAL_GPIO_LEVEL_HIGH : HAL_GPIO_LEVEL_LOW);
    }
}

/**
 * \brief           Turn off all LEDs
 */
static void leds_all_off(void) {
    for (size_t i = 0; i < NUM_LEDS; i++) {
        led_set(i, false);
    }
}

/**
 * \brief           Knight Rider pattern
 */
static void pattern_knight_rider(void) {
    /* Forward */
    for (size_t i = 0; i < NUM_LEDS; i++) {
        led_set(i, true);
        hal_delay_ms(100);
        led_set(i, false);
    }

    /* Backward */
    for (int i = NUM_LEDS - 1; i >= 0; i--) {
        led_set(i, true);
        hal_delay_ms(100);
        led_set(i, false);
    }
}

/**
 * \brief           Binary counter pattern
 */
static void pattern_binary_counter(void) {
    for (uint8_t count = 0; count < 16; count++) {
        for (size_t i = 0; i < NUM_LEDS; i++) {
            led_set(i, (count & (1 << i)) != 0);
        }
        hal_delay_ms(500);
    }
}

PWM-like LED Dimming

Create software PWM for LED dimming:

/**
 * \brief           Set LED brightness (0-100%)
 * \param[in]       index: LED index
 * \param[in]       brightness: Brightness percentage (0-100)
 */
static void led_set_brightness(size_t index, uint8_t brightness) {
    if (index >= NUM_LEDS || brightness > 100) {
        return;
    }

    /* Software PWM: on_time / period = brightness / 100 */
    uint32_t period_us = 1000;  /* 1ms period = 1kHz */
    uint32_t on_time_us = (period_us * brightness) / 100;
    uint32_t off_time_us = period_us - on_time_us;

    if (on_time_us > 0) {
        hal_gpio_write(leds[index].port, leds[index].pin, HAL_GPIO_LEVEL_HIGH);
        /* Note: hal_delay_us() would be needed for microsecond delays */
        /* For now, use millisecond approximation */
        if (on_time_us >= 1000) {
            hal_delay_ms(on_time_us / 1000);
        }
    }

    if (off_time_us > 0) {
        hal_gpio_write(leds[index].port, leds[index].pin, HAL_GPIO_LEVEL_LOW);
        if (off_time_us >= 1000) {
            hal_delay_ms(off_time_us / 1000);
        }
    }
}

/**
 * \brief           Fade LED in and out
 */
static void pattern_fade(size_t led_index) {
    /* Fade in */
    for (uint8_t brightness = 0; brightness <= 100; brightness += 5) {
        for (int i = 0; i < 10; i++) {  /* Repeat for smooth appearance */
            led_set_brightness(led_index, brightness);
        }
    }

    /* Fade out */
    for (uint8_t brightness = 100; brightness > 0; brightness -= 5) {
        for (int i = 0; i < 10; i++) {
            led_set_brightness(led_index, brightness);
        }
    }
}

Part 3: GPIO Interrupts

GPIO interrupts allow event-driven programming without polling.

Note

GPIO interrupt support depends on the platform. Check your platform guide for availability.

Configuring GPIO Interrupts

#include "hal/hal.h"

static volatile bool button_pressed = false;

/**
 * \brief           GPIO interrupt callback
 * \param[in]       port: GPIO port
 * \param[in]       pin: GPIO pin
 */
static void gpio_interrupt_callback(hal_gpio_port_t port, hal_gpio_pin_t pin) {
    if (port == BTN_PORT && pin == BTN_PIN) {
        button_pressed = true;
    }
}

/**
 * \brief           Initialize button with interrupt
 */
static hal_status_t button_init_interrupt(void) {
    hal_gpio_config_t config = {
        .direction = HAL_GPIO_DIR_INPUT,
        .pull = HAL_GPIO_PULL_DOWN,
        .output_mode = HAL_GPIO_OUTPUT_PP,
        .speed = HAL_GPIO_SPEED_LOW,
        .init_level = HAL_GPIO_LEVEL_LOW
    };

    /* Initialize GPIO */
    if (hal_gpio_init(BTN_PORT, BTN_PIN, &config) != HAL_OK) {
        return HAL_ERR_FAIL;
    }

    /* Configure interrupt on rising edge */
    hal_gpio_irq_config_t irq_config = {
        .trigger = HAL_GPIO_IRQ_TRIGGER_RISING,
        .callback = gpio_interrupt_callback
    };

    return hal_gpio_irq_init(BTN_PORT, BTN_PIN, &irq_config);
}

int main(void) {
    hal_init();
    button_init_interrupt();
    leds_init();

    /* Enable GPIO interrupts */
    hal_gpio_irq_enable(BTN_PORT, BTN_PIN);

    while (1) {
        /* Check flag set by interrupt */
        if (button_pressed) {
            button_pressed = false;  /* Clear flag */

            /* Toggle LED */
            hal_gpio_toggle(LED_PORT, LED_PIN);
        }

        /* CPU can sleep here to save power */
        hal_delay_ms(10);
    }

    return 0;
}

Interrupt Triggers:

  • HAL_GPIO_IRQ_TRIGGER_RISING: Trigger on low-to-high transition

  • HAL_GPIO_IRQ_TRIGGER_FALLING: Trigger on high-to-low transition

  • HAL_GPIO_IRQ_TRIGGER_BOTH: Trigger on any edge

Important Notes:

  • Keep interrupt handlers short and fast

  • Use volatile for variables shared between interrupt and main code

  • Avoid calling blocking functions in interrupt handlers

  • Consider using a flag to signal the main loop

Part 4: Complete Example

Here’s a complete example combining all concepts:

/**
 * \file            gpio_demo.c
 * \brief           GPIO Control Demo
 */

#include "hal/hal.h"
#include <stdbool.h>

/*-----------------------------------------------------------------------*/
/* Configuration                                                         */
/*-----------------------------------------------------------------------*/

#define BTN_PORT    HAL_GPIO_PORT_A
#define BTN_PIN     0

#define DEBOUNCE_DELAY_MS  50

typedef struct {
    hal_gpio_port_t port;
    hal_gpio_pin_t pin;
} led_t;

static const led_t leds[] = {
    {HAL_GPIO_PORT_D, 12},  /* Green */
    {HAL_GPIO_PORT_D, 13},  /* Orange */
    {HAL_GPIO_PORT_D, 14},  /* Red */
    {HAL_GPIO_PORT_D, 15}   /* Blue */
};

#define NUM_LEDS  (sizeof(leds) / sizeof(leds[0]))

/*-----------------------------------------------------------------------*/
/* State Machine                                                         */
/*-----------------------------------------------------------------------*/

typedef enum {
    MODE_OFF,
    MODE_SINGLE_BLINK,
    MODE_KNIGHT_RIDER,
    MODE_BINARY_COUNTER,
    MODE_MAX
} led_mode_t;

static led_mode_t current_mode = MODE_OFF;

/*-----------------------------------------------------------------------*/
/* Helper Functions                                                      */
/*-----------------------------------------------------------------------*/

static hal_status_t leds_init(void) {
    hal_gpio_config_t config = {
        .direction = HAL_GPIO_DIR_OUTPUT,
        .pull = HAL_GPIO_PULL_NONE,
        .output_mode = HAL_GPIO_OUTPUT_PP,
        .speed = HAL_GPIO_SPEED_LOW,
        .init_level = HAL_GPIO_LEVEL_LOW
    };

    for (size_t i = 0; i < NUM_LEDS; i++) {
        if (hal_gpio_init(leds[i].port, leds[i].pin, &config) != HAL_OK) {
            return HAL_ERR_FAIL;
        }
    }
    return HAL_OK;
}

static hal_status_t button_init(void) {
    hal_gpio_config_t config = {
        .direction = HAL_GPIO_DIR_INPUT,
        .pull = HAL_GPIO_PULL_DOWN,
        .output_mode = HAL_GPIO_OUTPUT_PP,
        .speed = HAL_GPIO_SPEED_LOW,
        .init_level = HAL_GPIO_LEVEL_LOW
    };
    return hal_gpio_init(BTN_PORT, BTN_PIN, &config);
}

static void led_set(size_t index, bool on) {
    if (index < NUM_LEDS) {
        hal_gpio_write(leds[index].port, leds[index].pin,
                      on ? HAL_GPIO_LEVEL_HIGH : HAL_GPIO_LEVEL_LOW);
    }
}

static void leds_all_off(void) {
    for (size_t i = 0; i < NUM_LEDS; i++) {
        led_set(i, false);
    }
}

static bool button_read_debounced(void) {
    static uint32_t last_change_time = 0;
    static bool last_stable_state = false;

    bool current_state = (hal_gpio_read(BTN_PORT, BTN_PIN) == HAL_GPIO_LEVEL_HIGH);

    if (current_state != last_stable_state) {
        uint32_t now = hal_get_tick();
        if ((now - last_change_time) >= DEBOUNCE_DELAY_MS) {
            last_stable_state = current_state;
            last_change_time = now;
        }
    }

    return last_stable_state;
}

static bool button_pressed_event(void) {
    static bool last_state = false;
    bool current_state = button_read_debounced();
    bool pressed = current_state && !last_state;
    last_state = current_state;
    return pressed;
}

/*-----------------------------------------------------------------------*/
/* LED Patterns                                                          */
/*-----------------------------------------------------------------------*/

static void mode_single_blink(void) {
    led_set(0, true);
    hal_delay_ms(500);
    led_set(0, false);
    hal_delay_ms(500);
}

static void mode_knight_rider(void) {
    for (size_t i = 0; i < NUM_LEDS; i++) {
        led_set(i, true);
        hal_delay_ms(100);
        led_set(i, false);
    }
    for (int i = NUM_LEDS - 1; i >= 0; i--) {
        led_set(i, true);
        hal_delay_ms(100);
        led_set(i, false);
    }
}

static void mode_binary_counter(void) {
    static uint8_t count = 0;
    for (size_t i = 0; i < NUM_LEDS; i++) {
        led_set(i, (count & (1 << i)) != 0);
    }
    count++;
    hal_delay_ms(500);
}

/*-----------------------------------------------------------------------*/
/* Main Function                                                         */
/*-----------------------------------------------------------------------*/

int main(void) {
    /* Initialize */
    hal_init();
    leds_init();
    button_init();

    /* Main loop */
    while (1) {
        /* Check for button press to change mode */
        if (button_pressed_event()) {
            current_mode = (current_mode + 1) % MODE_MAX;
            leds_all_off();
        }

        /* Execute current mode */
        switch (current_mode) {
            case MODE_OFF:
                leds_all_off();
                hal_delay_ms(100);
                break;

            case MODE_SINGLE_BLINK:
                mode_single_blink();
                break;

            case MODE_KNIGHT_RIDER:
                mode_knight_rider();
                break;

            case MODE_BINARY_COUNTER:
                mode_binary_counter();
                break;

            default:
                current_mode = MODE_OFF;
                break;
        }
    }

    return 0;
}

How It Works:

  1. Press the button to cycle through modes

  2. Mode 0: All LEDs off

  3. Mode 1: Single LED blinks

  4. Mode 2: Knight Rider pattern

  5. Mode 3: Binary counter (0-15)

Best Practices

  1. Always Initialize: Call hal_init() before using GPIO functions

  2. Check Return Values: Always check return values from HAL functions

  3. Use Appropriate Pull Resistors: Configure pull-up/down based on your circuit

  4. Debounce Inputs: Always debounce mechanical switches

  5. Keep Interrupts Short: Minimize work in interrupt handlers

  6. Use Appropriate Speed: Don’t use high-speed GPIO for slow signals (wastes power)

  7. Document Pin Assignments: Clearly document which pins are used for what

Common Pitfalls

Forgetting Pull Resistors:

Floating inputs can cause erratic behavior. Always configure pull resistors for inputs.

No Debouncing:

Mechanical switches bounce. Always implement debouncing for reliable operation.

Blocking in Interrupts:

Never call hal_delay_ms() or other blocking functions in interrupt handlers.

Wrong Trigger Type:

Choose the correct interrupt trigger (rising, falling, or both) for your application.

Next Steps