STM32寄存器开发练习(三):流水灯——从单文件到模块化

前言

上一篇分享了GPIO怎么点亮LED,以及GPIO的各种工作模式。

这一篇,往前走一步——做一个流水灯

两个LED交替亮灭,每个亮500ms,循环跑起来。

听起来挺简单,但我在这个过程里主要做了两件事:

  1. 实现了一个精准的延时函数(用SysTick定时器,不是空循环)
  2. 把代码从单文件重构成了模块化的结构

这篇文章就把这两件事的过程分享出来。


我的开发板情况

和上一篇一样,两个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);
    }
}

功能上是对的,流水灯跑起来没问题。

但这个写法有两个明显的问题:

  1. while(1) 里面手动写了四次GPIO操作——如果LED多了,代码会越来越长
  2. 延时和LED控制全堆在一起,看起来乱

插播:延时函数是怎么实现的

在说模块化之前,先解释一下延时函数的原理,因为这是这一版的新东西。

为什么不用空循环?

写延时,最简单的方法是这样:

void delay(int n)
{
    while(n--);
}

这个方法的问题是:延时时间不精准

因为每次循环的执行时间,和编译器优化等级有关——同样的代码,优化等级不同,延时时间可能差好几倍。更极端的情况是,编译器直接把这个"没有副作用"的循环优化掉了,延时消失了。

SysTick定时器

Cortex-M3内核里有一个硬件定时器,叫SysTick,专门用来做延时的。

它的工作原理很简单:

  1. LOAD 寄存器装一个计数值
  2. 启动定时器,从 LOAD 开始向下计数
  3. 计数到0时,COUNTFLAG 位自动置1
  4. 查询 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.cdelay.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模块的头文件,主要定义了:

  1. LED引脚的宏定义
  2. 一个结构体,把端口和引脚打包在一起
  3. 函数声明
// 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里留了注释,两种都记录着。


总结

这一篇,主要分享了两件事:

  1. SysTick延时:用硬件定时器实现精准延时,计数值 = 时钟频率(MHz)× 延时时间(μs),系统时钟72MHz,所以是 72 * us

  2. 模块化重构:把LED控制和延时函数分离成独立的 .c/.h 文件,main.c 只保留主逻辑。这个结构在后续项目里可以直接复用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值