tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第6~7章:LED流水灯 + 配套代码02~04" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:LED 控制就像你手里有 3 个开关(GPIO 引脚)分别控制 3 盏灯。你手动打开灯(IO=0),等一会儿,再关上(IO=1),再打开下一盏,就形成了"流水灯"效果。模块化封装就像给每个灯配了一个遥控器(LED_On/LED_Off 函数),不用再直接去掰开关了。
开发板上有 4 个 LED:
电路原理:
VCC(3.3V) → 限流电阻(1KΩ) → LED 正极 → LED 负极 → GPIO引脚
当 GPIO = 0 (低电平) → 电流流过 LED → LED 点亮
当 GPIO = 1 (高电平) → LED 两端电压差接近 0 → LED 熄灭
限流电阻计算:
R = (VCC - V_LED) / I_LED = (3.3 - 2.0) / 0.01 = 130Ω(实际使用 1KΩ,延长 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 独立模块化,可跨项目复用。
项目路径: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]);
}
}
| 操作 | 代码 | 说明 |
|---|---|---|
| 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_GPIO_WritePin(GPIOA, pin, state) |
写引脚电平 (RESET=低, SET=高) |
HAL_GPIO_TogglePin(GPIOA, pin) |
翻转引脚电平 |
HAL_Delay(ms) |
毫秒延时(基于SysTick中断) |
/* USER CODE BEGIN */ 和 /* USER CODE END */ 之间