STM32寄存器开发练习(三):流水灯——从单文件到模块化 前言上一篇分享了GPIO怎么点亮LED以及GPIO的各种工作模式。这一篇往前走一步——做一个流水灯。两个LED交替亮灭每个亮500ms循环跑起来。听起来挺简单但我在这个过程里主要做了两件事实现了一个精准的延时函数用SysTick定时器不是空循环把代码从单文件重构成了模块化的结构这篇文章就把这两件事的过程分享出来。我的开发板情况和上一篇一样两个LEDLED1接在PB5低电平点亮LED2接在PE5低电平点亮目标效果LED1亮500ms → LED1灭 → LED2亮500ms → LED2灭 → 循环第一版单文件实现先不管代码好不好看先把功能跑起来。所有代码写在一个main.c里#includestm32f10x.hvoidDelay_us(uint16_tus);voidDelay_ms(uint16_tms);intmain(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){// LED1GPIOB-ODR~GPIO_ODR_ODR5;// PB5低电平LED1亮Delay_ms(500);GPIOB-ODR|GPIO_ODR_ODR5;// PB5高电平LED1灭// LED2GPIOE-ODR~GPIO_ODR_ODR5;// PE5低电平LED2亮Delay_ms(500);GPIOE-ODR|GPIO_ODR_ODR5;// PE5高电平LED2灭}}// 延时函数用SysTick后面详细讲voidDelay_us(uint16_tus){SysTick-CTRL0;SysTick-LOAD(72*us-1);SysTick-VAL0;SysTick-CTRL0x05;while((SysTick-CTRLSysTick_CTRL_COUNTFLAG)0){}SysTick-CTRL~SysTick_CTRL_ENABLE;}voidDelay_ms(uint16_tms){while(ms--){Delay_us(1000);}}功能上是对的流水灯跑起来没问题。但这个写法有两个明显的问题while(1)里面手动写了四次GPIO操作——如果LED多了代码会越来越长延时和LED控制全堆在一起看起来乱插播延时函数是怎么实现的在说模块化之前先解释一下延时函数的原理因为这是这一版的新东西。为什么不用空循环写延时最简单的方法是这样voiddelay(intn){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。代码解释voidDelay_us(uint16_tus){SysTick-CTRL0;// 先清零避免残留配置SysTick-LOAD(72*us-1);// 装载计数值SysTick-VAL0;// 清空当前计数值SysTick-CTRL0x05;// 启动使用系统时钟不产生中断使能while((SysTick-CTRLSysTick_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__#includestm32f10x.hvoidDelay_us(uint16_tus);voidDelay_ms(uint16_tms);voidDelay_s(uint16_ts);#endif// delay.c#includedelay.hvoidDelay_us(uint16_tus){SysTick-CTRL0;SysTick-LOAD(72*us-1);SysTick-VAL0;SysTick-CTRL0x05;while((SysTick-CTRLSysTick_CTRL_COUNTFLAG)0){}SysTick-CTRL~SysTick_CTRL_ENABLE;}voidDelay_ms(uint16_tms){while(ms--){Delay_us(1000);}}voidDelay_s(uint16_ts){while(s--){Delay_ms(1000);}}led.hLED模块的头文件主要定义了LED引脚的宏定义一个结构体把端口和引脚打包在一起函数声明// led.h#ifndef__LED_H__#define__LED_H__#includestm32f10x.h// 宏定义LED引脚#defineLED1_PORTGPIOB#defineLED1_PINGPIO_ODR_ODR5#defineLED2_PORTGPIOE#defineLED2_PINGPIO_ODR_ODR5// 结构体把端口和引脚打包typedefstruct{GPIO_TypeDef*port;uint32_tpin;}LED_TypeDef;// 函数声明voidLED_Init(void);voidLED_On(GPIO_TypeDef*port,uint32_tpin);voidLED_Off(GPIO_TypeDef*port,uint32_tpin);voidLED_Toggle(GPIO_TypeDef*port,uint32_tpin);voidLED_AllOn(LED_TypeDef*leds,uint8_tnum);voidLED_AllOff(LED_TypeDef*leds,uint8_tnum);#endifled.cLED控制的具体实现// led.c#includeled.hvoidLED_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);}voidLED_On(GPIO_TypeDef*port,uint32_tpin){port-ODR~pin;}voidLED_Off(GPIO_TypeDef*port,uint32_tpin){port-ODR|pin;}voidLED_Toggle(GPIO_TypeDef*port,uint32_tpin){if((port-IDRpin)0)LED_Off(port,pin);elseLED_On(port,pin);}voidLED_AllOn(LED_TypeDef*leds,uint8_tnum){for(uint8_ti0;inum;i)LED_On(leds[i].port,leds[i].pin);}voidLED_AllOff(LED_TypeDef*leds,uint8_tnum){for(uint8_ti0;inum;i)LED_Off(leds[i].port,leds[i].pin);}main.c最终版把LED列表定义成一个数组用循环跑流水灯#includestm32f10x.h#includeled.h#includedelay.h// 把所有LED打包成数组constLED_TypeDef leds[]{{LED1_PORT,LED1_PIN},{LED2_PORT,LED2_PIN}};intmain(void){// 初始化LEDLED_Init();while(1){for(uint8_ti0;i2;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寄存器判断状态voidLED_Toggle(GPIO_TypeDef*port,uint32_tpin){if((port-IDRpin)0)LED_Off(port,pin);elseLED_On(port,pin);}先读IDR输入数据寄存器判断引脚当前状态然后翻转。写法二异或ODRvoidLED_Toggle(GPIO_TypeDef*port,uint32_tpin){port-ODR^pin;}直接对ODR输出数据寄存器做异或操作一行代码。^的效果对应位是0 → 变成1对应位是1 → 变成0就是翻转。两种写法都可以写法二更简洁。我在led.c里留了注释两种都记录着。总结这一篇主要分享了两件事SysTick延时用硬件定时器实现精准延时计数值 时钟频率MHz× 延时时间μs系统时钟72MHz所以是72 * us。模块化重构把LED控制和延时函数分离成独立的.c/.h文件main.c只保留主逻辑。这个结构在后续项目里可以直接复用。