03-GPIO输出:LED控制.md 12 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第6~7章:LED流水灯 + 配套代码02~04" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

GPIO输出:LED控制

用生活理解:LED 控制就像你手里有 3 个开关(GPIO 引脚)分别控制 3 盏灯。你手动打开灯(IO=0),等一会儿,再关上(IO=1),再打开下一盏,就形成了"流水灯"效果。模块化封装就像给每个灯配了一个遥控器(LED_On/LED_Off 函数),不用再直接去掰开关了。


LED硬件电路设计

LED电路原理图

开发板上有 4 个 LED:

  • LED1 → PA0(低电平点亮)
  • LED2 → PA1(低电平点亮)
  • LED3 → PA8(低电平点亮)
  • LED4 → 电源指示灯(上电即亮,不可控)

电路原理

VCC(3.3V) → 限流电阻(1KΩ) → LED 正极 → LED 负极 → GPIO引脚

当 GPIO = 0 (低电平) → 电流流过 LED → LED 点亮
当 GPIO = 1 (高电平) → LED 两端电压差接近 0 → LED 熄灭

限流电阻计算

  • LED 工作电流约 5~10mA
  • LED 正向压降约 1.8~2.0V
  • R = (VCC - V_LED) / I_LED = (3.3 - 2.0) / 0.01 = 130Ω(实际使用 1KΩ,延长 LED 寿命)

参考:开发板原理图 LED 部分


实验:LED流水灯

需求描述

依次点亮 LED1 → LED2 → LED3,每个灯亮 500ms,形成流水灯效果。

软件设计(寄存器版 — 直接操作)

项目路径stm32/02_led_flow_register

这个版本在 main.c 中将所有逻辑写在一起,直观但不利于复用。

文件:stm32/02_led_flow_register/User/main.c

#include "stm32f10x.h"

// 函数声明
void Delay_us(uint16_t us);
void Delay_ms(uint16_t ms);
void Delay_s(uint16_t s);

int main(void)
{
    // 1. 时钟配置:开启GPIOA时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. 配置PA0、PA1、PA8为推挽输出50MHz
    // PA0 (CRL低4位: MODE0=11, CNF0=00)
    GPIOA->CRL |= GPIO_CRL_MODE0;       // 设置MODE0[1:0]=11 → 50MHz输出
    GPIOA->CRL &= ~GPIO_CRL_CNF0;       // 清除CNF0[1:0]=00 → 通用推挽

    // PA1 (CRL的4~7位: MODE1=11, CNF1=00)
    GPIOA->CRL |= GPIO_CRL_MODE1;       // MODE1置1
    GPIOA->CRL &= ~GPIO_CRL_CNF1;       // CNF1清0

    // PA8 (CRH的低4位: MODE8=11, CNF8=00)
    GPIOA->CRH |= GPIO_CRH_MODE8;       // MODE8置1
    GPIOA->CRH &= ~GPIO_CRH_CNF8;       // CNF8清0

    // 3. 初始状态:所有灯熄灭(输出高电平)
    GPIOA->ODR |= (GPIO_ODR_ODR0 | GPIO_ODR_ODR1 | GPIO_ODR_ODR8);

    // 4. 流水灯循环
    while (1)
    {
        // 点亮 LED1 (PA0输出低电平)
        GPIOA->ODR &= ~GPIO_ODR_ODR0;   // &=~ 将ODR0位清0 → 低电平
        Delay_ms(500);                   // 保持500ms
        GPIOA->ODR |= GPIO_ODR_ODR0;    // |= 将ODR0位置1 → 高电平,熄灭

        // 点亮 LED2 (PA1输出低电平)
        GPIOA->ODR &= ~GPIO_ODR_ODR1;
        Delay_ms(500);
        GPIOA->ODR |= GPIO_ODR_ODR1;

        // 点亮 LED3 (PA8输出低电平)
        GPIOA->ODR &= ~GPIO_ODR_ODR8;
        Delay_ms(500);
        GPIOA->ODR |= GPIO_ODR_ODR8;
    }
}

// ==== 微秒级延时函数(基于SysTick实现)====
void Delay_us(uint16_t us)
{
    // SysTick->LOAD: 设置重装载值
    // 系统时钟72MHz, 每计数一次 = 1/72 us
    // 延时us微秒需要计数: 72 * us 次
    SysTick->LOAD = 72 * us;

    // SysTick->CTRL: 控制寄存器
    // 第0位 ENABLE=1 → 启动定时器
    // 第2位 CLKSOURCE=1 → 使用系统时钟(AHB=72MHz)
    // 0x05 = 二进制 0101 → ENABLE=1, CLKSOURCE=1, TICKINT=0
    SysTick->CTRL = 0x05;

    // 轮询等待 COUNTFLAG 位置1(计到0时硬件自动置1)
    // SysTick_CTRL_COUNTFLAG = (1<<16)
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG))
    {
    }

    // 关闭定时器(ENABLE位清0)
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE;  // &=~ 将ENABLE位清0
}

void Delay_ms(uint16_t ms)
{
    while (ms--)
    {
        Delay_us(1000);  // 1ms = 1000us
    }
}

void Delay_s(uint16_t s)
{
    while (s--)
    {
        Delay_ms(1000);
    }
}

问题:代码所有逻辑堆在 main.c 中,不方便复用。LED 操作、延时函数、main 逻辑混在一起。


软件设计(寄存器版 — 模块化封装)

项目路径stm32/03_led_flow_pro_register

这个版本将 LED 驱动提取为独立模块 Hardware/LED/led.c + led.h,延时单独成文件 User/delay.c + delay.h,main.c 只负责调用。

文件:stm32/03_led_flow_pro_register/Hardware/LED/led.h

#ifndef __LED_H
#define __LED_H

#include "stm32f10x.h"

// 宏定义:三个LED对应的ODR位
// LED_1 = GPIO_ODR_ODR0 = 0x0001 = 第0位,对应PA0
// LED_2 = GPIO_ODR_ODR1 = 0x0002 = 第1位,对应PA1
// LED_3 = GPIO_ODR_ODR8 = 0x0100 = 第8位,对应PA8
#define LED_1 GPIO_ODR_ODR0
#define LED_2 GPIO_ODR_ODR1
#define LED_3 GPIO_ODR_ODR8

void LED_Init(void);                      // 初始化LED(时钟+GPIO配置)
void LED_On(uint16_t led);                // 点亮指定LED
void LED_Off(uint16_t led);               // 熄灭指定LED
void LED_Toggle(uint16_t led);            // 翻转指定LED状态
void LED_OnAll(uint16_t leds[], uint8_t size);   // 同时点亮多个LED
void LED_OffAll(uint16_t leds[], uint8_t size);  // 同时熄灭多个LED

#endif

文件:stm32/03_led_flow_pro_register/Hardware/LED/led.c

#include "led.h"

// LED初始化:开启时钟 + 配置GPIO模式 + 初始状态熄灭
void LED_Init(void)
{
    // 1. 开启GPIOA时钟
    // RCC_APB2ENR_IOPAEN = 0x00000004 = 1<<2
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. 配置PA0、PA1、PA8为推挽输出50MHz
    // PA0: MODE0=11(50MHz), CNF0=00(通用推挽)
    GPIOA->CRL |= GPIO_CRL_MODE0;       // |= 将MODE0位置1
    GPIOA->CRL &= ~GPIO_CRL_CNF0;       // &=~ 将CNF0位清0

    // PA1: MODE1=11, CNF1=00
    GPIOA->CRL |= GPIO_CRL_MODE1;
    GPIOA->CRL &= ~GPIO_CRL_CNF1;

    // PA8: MODE8=11, CNF8=00
    GPIOA->CRH |= GPIO_CRH_MODE8;
    GPIOA->CRH &= ~GPIO_CRH_CNF8;

    // 3. 初始状态:所有灯熄灭
    LED_Off(LED_1);
    LED_Off(LED_2);
    LED_Off(LED_3);
}

// 点亮LED:ODR对应位清0(低电平点亮)
void LED_On(uint16_t led)
{
    GPIOA->ODR &= ~led;   // &=~ led: 将led对应的ODR位清0
}

// 熄灭LED:ODR对应位置1(高电平熄灭)
void LED_Off(uint16_t led)
{
    GPIOA->ODR |= led;    // |= led: 将led对应的ODR位置1
}

// 翻转LED状态:读取当前状态,然后取反
void LED_Toggle(uint16_t led)
{
    // 通过IDR寄存器读取当前引脚电平
    // IDR对应位=0表示引脚低电平(LED亮)
    if ((GPIOA->IDR & led) == 0)
    {
        LED_Off(led);   // 当前亮 → 熄灭
    }
    else
    {
        LED_On(led);    // 当前灭 → 点亮
    }
}

// 同时点亮多个LED
void LED_OnAll(uint16_t leds[], uint8_t size)
{
    for (uint8_t i = 0; i < size; i++)
    {
        LED_On(leds[i]);
    }
}

// 同时熄灭多个LED
void LED_OffAll(uint16_t leds[], uint8_t size)
{
    for (uint8_t i = 0; i < size; i++)
    {
        LED_Off(leds[i]);
    }
}

文件:stm32/03_led_flow_pro_register/User/delay.h

#ifndef __DELAY_H
#define __DELAY_H

#include "stm32f10x.h"

void Delay_us(uint16_t us);
void Delay_ms(uint16_t ms);
void Delay_s(uint16_t s);

#endif

文件:stm32/03_led_flow_pro_register/User/delay.c

#include "delay.h"

// SysTick定时器实现微秒级延时
void Delay_us(uint16_t us)
{
    // SysTick寄存器详解:
    // LOAD[23:0]: 重装载值(24位计数器最大值16,777,215)
    // CTRL[0]: ENABLE — 0=关闭, 1=启动
    // CTRL[1]: TICKINT — 0=不产生中断, 1=计数到0时产生中断
    // CTRL[2]: CLKSOURCE — 0=AHB/8(9MHz), 1=AHB(72MHz)
    // CTRL[16]: COUNTFLAG — 计数到0时硬件置1, 读后自动清0
    // VAL[23:0]: 当前计数值(读返回当前值,写清0)

    // 设置装载值: 72MHz下, 1us需要计数72次
    SysTick->LOAD = 72 * us;

    // CTRL = 0x05 = 二进制 0000 0101
    // 第0位ENABLE=1, 第2位CLKSOURCE=1, 不使用中断
    SysTick->CTRL = 0x05;

    // 轮询等待COUNTFLAG置1(计数值减到0)
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG))
    {
    }

    // 关闭SysTick
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE;  // ENABLE位清0
}

void Delay_ms(uint16_t ms)
{
    while (ms--)
    {
        Delay_us(1000);  // 1ms累积1000次1us
    }
}

void Delay_s(uint16_t s)
{
    while (s--)
    {
        Delay_ms(1000);
    }
}

文件:stm32/03_led_flow_pro_register/User/main.c

#include "delay.h"
#include "led.h"

int main(void)
{
    LED_Init();  // 时钟+GPIO+初始状态 一站式初始化

    uint16_t leds[] = {LED_1, LED_2, LED_3};
    uint8_t n = 3;

    while (1)
    {
        for (uint8_t i = 0; i < n; i++)
        {
            LED_On(leds[i]);     // 点亮当前LED
            Delay_ms(500);       // 保持500ms
            LED_Off(leds[i]);    // 熄灭当前LED
        }
    }
}

进步:main.c 非常干净,只表达业务逻辑。LED 底层操作被封装在 Hardware/LED/ 中,delay 独立模块化,可跨项目复用。


软件设计(HAL库版 — CubeMX生成)

项目路径stm32/04_led_flow_pro_hal

CubeMX 图形化配置 GPIOD(某些板子)或 GPIOA(我们用的板子),由工具生成初始化代码,用户在 /* USER CODE */ 区间添加业务逻辑。

文件:stm32/04_led_flow_pro_hal/Core/Src/main.c

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
#include "led.h"

int main(void)
{
    /* MCU Configuration */
    HAL_Init();                           // HAL库初始化(时基、Flash等)
    SystemClock_Config();                 // 系统时钟配置(72MHz)
    MX_GPIO_Init();                       // GPIO初始化(CubeMX生成)

    // 用户代码开始
    LED leds[] = {LED1_Pin, LED2_Pin, LED3_Pin};

    while (1)
    {
        for (uint8_t i = 0; i < 3; i++)
        {
            LED_On(leds[i]);              // 点亮
            HAL_Delay(500);               // HAL库毫秒延时
            LED_Off(leds[i]);             // 熄灭
        }
    }
}

文件:stm32/04_led_flow_pro_hal/Core/Src/led.c

#include "led.h"

// HAL_GPIO_WritePin 参数说明:
// 参数1: GPIOx — GPIO端口基地址
// 参数2: GPIO_Pin — 引脚编号掩码 (如 GPIO_PIN_0 = 0x0001)
// 参数3: PinState — GPIO_PIN_RESET=0(低电平) / GPIO_PIN_SET=1(高电平)
// LED 低电平点亮, 所以 On=RESET, Off=SET

void LED_On(LED led)
{
    HAL_GPIO_WritePin(GPIOA, led, GPIO_PIN_RESET);
}

void LED_Off(LED led)
{
    HAL_GPIO_WritePin(GPIOA, led, GPIO_PIN_SET);
}

void LED_Toggle(LED led)
{
    HAL_GPIO_TogglePin(GPIOA, led);
}

void LED_OnAll(LED leds[], uint8_t size)
{
    for (uint8_t i = 0; i < size; i++)
    {
        LED_On(leds[i]);
    }
}

void LED_OffAll(LED leds[], uint8_t size)
{
    for (uint8_t i = 0; i < size; i++)
    {
        LED_Off(leds[i]);
    }
}

Core 函数速查表

寄存器版 (基于 stm32f10x.h 宏)

操作 代码 说明
LED初始化 LED_Init() 开启时钟 + 配置推挽输出 + 初始熄灭
点亮LED LED_On(LED_1) GPIOA->ODR &= ~led_bit
熄灭LED LED_Off(LED_1) `GPIOA->ODR
翻转LED LED_Toggle(LED_1) 读取IDR判断当前状态,取反
延时 Delay_ms(500) 基于SysTick查询方式

HAL库版

HAL函数 说明
HAL_GPIO_WritePin(GPIOA, pin, state) 写引脚电平 (RESET=低, SET=高)
HAL_GPIO_TogglePin(GPIOA, pin) 翻转引脚电平
HAL_Delay(ms) 毫秒延时(基于SysTick中断)

常见问题与避坑

  1. LED 不亮 → 检查 GPIO 模式(必须推挽输出)、检查 ODR 电平(低电平才亮)
  2. LED 亮度不足 → 检查限流电阻、GPIO 输出速度(MODE 配置为 50MHz 驱动能力更强)
  3. 流水灯速度不对 → Delay_us 中 LOAD 值计算是否匹配系统时钟频率(72MHz?)
  4. HAL_Delay 不工作 → HAL 时基(SysTick)被其他中断阻塞;检查中断优先级配置
  5. 模块化文件找不到头文件 → Keil 中需在 Options → C/C++ → Include Paths 添加路径
  6. CubeMX 生成代码后 USER CODE 被覆盖 → 务必写在 /* USER CODE BEGIN *//* USER CODE END */ 之间