--- 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电路原理图](assets/led_电路原理图.png) 开发板上有 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`** ```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`** ```c #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`** ```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`** ```c #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`** ```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`** ```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`** ```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`** ```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_bit` | | 翻转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 */` 之间