前言
上一篇分享了GPIO怎么点亮LED,以及GPIO的各种工作模式。
这一篇,往前走一步——做一个流水灯。
两个LED交替亮灭,每个亮500ms,循环跑起来。
听起来挺简单,但我在这个过程里主要做了两件事:
- 实现了一个精准的延时函数(用SysTick定时器,不是空循环)
- 把代码从单文件重构成了模块化的结构
这篇文章就把这两件事的过程分享出来。
我的开发板情况
和上一篇一样,两个LED:
- LED1:接在 PB5(低电平点亮)
- LED2:接在 PE5(低电平点亮)
目标效果:
LED1亮500ms → LED1灭 → LED2亮500ms → LED2灭 → 循环
第一版:单文件实现
先不管代码好不好看,先把功能跑起来。
所有代码写在一个 main.c 里:
#include "stm32f10x.h"
void Delay_us(uint16_t us);
void Delay_ms(uint16_t ms);
int main(void)
{
// 两个LED灯分别连在PB5和PE5上
// 1.时钟配置
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
// 2.GPIO工作模式配置
GPIOB->CRL &= ~GPIO_CRL_CNF5;
GPIOB->CRL |= GPIO_CRL_MODE5;
GPIOE->CRL &= ~GPIO_CRL_CNF5;
GPIOE->CRL |= GPIO_CRL_MODE5;
// 3.默认关灯(高电平熄灭)
GPIOB->ODR |= GPIO_ODR_ODR5;
GPIOE->ODR |= GPIO_ODR_ODR5;
// 4.循环执行流水灯
while(1)
{
// LED1
GPIOB->ODR &= ~GPIO_ODR_ODR5; // PB5低电平,LED1亮
Delay_ms(500);
GPIOB->ODR |= GPIO_ODR_ODR5; // PB5高电平,LED1灭
// LED2
GPIOE->ODR &= ~GPIO_ODR_ODR5; // PE5低电平,LED2亮
Delay_ms(500);
GPIOE->ODR |= GPIO_ODR_ODR5; // PE5高电平,LED2灭
}
}
// 延时函数(用SysTick,后面详细讲)
void Delay_us(uint16_t us)
{
SysTick->CTRL = 0;
SysTick->LOAD = (72 * us - 1);
SysTick->VAL = 0;
SysTick->CTRL = 0x05;
while((SysTick->CTRL & SysTick_CTRL_COUNTFLAG) == 0) {}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE;
}
void Delay_ms(uint16_t ms)
{
while(ms--)
{
Delay_us(1000);
}
}
功能上是对的,流水灯跑起来没问题。
但这个写法有两个明显的问题:
while(1)里面手动写了四次GPIO操作——如果LED多了,代码会越来越长- 延时和LED控制全堆在一起,看起来乱
插播:延时函数是怎么实现的
在说模块化之前,先解释一下延时函数的原理,因为这是这一版的新东西。
为什么不用空循环?
写延时,最简单的方法是这样:
void delay(int n)
{
while(n--);
}
这个方法的问题是:延时时间不精准。
因为每次循环的执行时间,和编译器优化等级有关——同样的代码,优化等级不同,延时时间可能差好几倍。更极端的情况是,编译器直接把这个"没有副作用"的循环优化掉了,延时消失了。
SysTick定时器
Cortex-M3内核里有一个硬件定时器,叫SysTick,专门用来做延时的。
它的工作原理很简单:
- 往 LOAD 寄存器装一个计数值
- 启动定时器,从 LOAD 开始向下计数
- 计数到0时,COUNTFLAG 位自动置1
- 查询 COUNTFLAG,等于1说明计数结束
只要计数值算对了,延时时间就是精准的。
计数值怎么算?
我的开发板,系统时钟是 72MHz。
也就是说,每秒钟计数 72,000,000 次,每计一次需要 1/72,000,000 秒。
要延时 1μs,需要计数 72次。
所以 Delay_us(us) 的计数值就是 72 * us。
(计数值减1是因为SysTick从 LOAD 计到0,实际是 LOAD + 1 次计数,所以要减1。)
代码解释
void Delay_us(uint16_t us)
{
SysTick->CTRL = 0; // 先清零,避免残留配置
SysTick->LOAD = (72 * us - 1); // 装载计数值
SysTick->VAL = 0; // 清空当前计数值
SysTick->CTRL = 0x05; // 启动:使用系统时钟,不产生中断,使能
while((SysTick->CTRL & SysTick_CTRL_COUNTFLAG) == 0) {} // 等待计数结束
SysTick->CTRL &= ~SysTick_CTRL_ENABLE; // 关闭定时器
}
CTRL = 0x05 对应的位:
- 第0位(ENABLE)= 1:使能定时器
- 第1位(TICKINT)= 0:计数结束不产生中断
- 第2位(CLKSOURCE)= 1:使用系统时钟(72MHz)
第二版:模块化重构
单文件版功能正常,但代码结构不好看。
我决定把它拆成三个模块:
├── main.c
├── led.c / led.h ← LED初始化和控制
└── delay.c / delay.h ← 延时函数
这样做的好处是:
main.c只负责主逻辑,一眼就能看清程序在干什么led.c和delay.c可以直接复用到下一个项目里
delay.h / delay.c
把延时函数单独放一个文件,没有改动逻辑:
// 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
// delay.c
#include "delay.h"
void Delay_us(uint16_t us)
{
SysTick->CTRL = 0;
SysTick->LOAD = (72 * us - 1);
SysTick->VAL = 0;
SysTick->CTRL = 0x05;
while((SysTick->CTRL & SysTick_CTRL_COUNTFLAG) == 0) {}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE;
}
void Delay_ms(uint16_t ms)
{
while(ms--)
{
Delay_us(1000);
}
}
void Delay_s(uint16_t s)
{
while(s--)
{
Delay_ms(1000);
}
}
led.h
LED模块的头文件,主要定义了:
- LED引脚的宏定义
- 一个结构体,把端口和引脚打包在一起
- 函数声明
// led.h
#ifndef __LED_H__
#define __LED_H__
#include "stm32f10x.h"
// 宏定义:LED引脚
#define LED1_PORT GPIOB
#define LED1_PIN GPIO_ODR_ODR5
#define LED2_PORT GPIOE
#define LED2_PIN GPIO_ODR_ODR5
// 结构体:把端口和引脚打包
typedef struct {
GPIO_TypeDef* port;
uint32_t pin;
} LED_TypeDef;
// 函数声明
void LED_Init(void);
void LED_On(GPIO_TypeDef* port, uint32_t pin);
void LED_Off(GPIO_TypeDef* port, uint32_t pin);
void LED_Toggle(GPIO_TypeDef* port, uint32_t pin);
void LED_AllOn(LED_TypeDef* leds, uint8_t num);
void LED_AllOff(LED_TypeDef* leds, uint8_t num);
#endif
led.c
LED控制的具体实现:
// led.c
#include "led.h"
void LED_Init(void)
{
// 1.时钟配置
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
// 2.GPIO工作模式配置
LED1_PORT->CRL &= ~GPIO_CRL_CNF5;
LED1_PORT->CRL |= GPIO_CRL_MODE5;
LED2_PORT->CRL &= ~GPIO_CRL_CNF5;
LED2_PORT->CRL |= GPIO_CRL_MODE5;
// 3.默认关灯
LED_Off(LED1_PORT, LED1_PIN);
LED_Off(LED2_PORT, LED2_PIN);
}
void LED_On(GPIO_TypeDef* port, uint32_t pin)
{
port->ODR &= ~pin;
}
void LED_Off(GPIO_TypeDef* port, uint32_t pin)
{
port->ODR |= pin;
}
void LED_Toggle(GPIO_TypeDef* port, uint32_t pin)
{
if((port->IDR & pin) == 0)
LED_Off(port, pin);
else
LED_On(port, pin);
}
void LED_AllOn(LED_TypeDef* leds, uint8_t num)
{
for(uint8_t i = 0; i < num; i++)
LED_On(leds[i].port, leds[i].pin);
}
void LED_AllOff(LED_TypeDef* leds, uint8_t num)
{
for(uint8_t i = 0; i < num; i++)
LED_Off(leds[i].port, leds[i].pin);
}
main.c(最终版)
把LED列表定义成一个数组,用循环跑流水灯:
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
// 把所有LED打包成数组
const LED_TypeDef leds[] = {
{LED1_PORT, LED1_PIN},
{LED2_PORT, LED2_PIN}
};
int main(void)
{
// 初始化LED
LED_Init();
while(1)
{
for(uint8_t i = 0; i < 2; i++)
{
LED_On(leds[i].port, leds[i].pin);
Delay_ms(500);
LED_Off(leds[i].port, leds[i].pin);
}
}
}
这一版的 main.c,逻辑一目了然——初始化,然后循环点亮每个LED,每个亮500ms。
和第一版相比,最明显的变化是:不管有多少个LED,主循环的代码不需要改,只改数组就行。
补充:LED_Toggle的两种写法
在写 LED_Toggle 的时候,我想了两种实现方式,这里也记录一下。
写法一:读GPIO的IDR寄存器判断状态
void LED_Toggle(GPIO_TypeDef* port, uint32_t pin)
{
if((port->IDR & pin) == 0)
LED_Off(port, pin);
else
LED_On(port, pin);
}
先读 IDR(输入数据寄存器)判断引脚当前状态,然后翻转。
写法二:异或ODR
void LED_Toggle(GPIO_TypeDef* port, uint32_t pin)
{
port->ODR ^= pin;
}
直接对 ODR(输出数据寄存器)做异或操作,一行代码。
^= 的效果:
- 对应位是0 → 变成1
- 对应位是1 → 变成0
就是翻转。
两种写法都可以,写法二更简洁。我在led.c里留了注释,两种都记录着。
总结
这一篇,主要分享了两件事:
-
SysTick延时:用硬件定时器实现精准延时,计数值 = 时钟频率(MHz)× 延时时间(μs),系统时钟72MHz,所以是
72 * us。 -
模块化重构:把LED控制和延时函数分离成独立的
.c/.h文件,main.c只保留主逻辑。这个结构在后续项目里可以直接复用。
:流水灯——从单文件到模块化&spm=1001.2101.3001.5002&articleId=162796796&d=1&t=3&u=4e7efda8f4984005963c38a0455ba670)
3177

被折叠的 条评论
为什么被折叠?



