前言

关于STM32的教程,大部分一上来就让我们用HAL库或者标准外设库,调用几个函数就搞定了。

但这样的话,其实不知道底层发生了什么。所以我最近跟着B站尚硅谷老师重新开始学习原始的编程方式——直接操作寄存器,这样才能真正理解MCU的工作原理。

这一篇,我准备点亮开发板上的LED——但我不打算直接给你"完美代码",而是想分享一下我写代码时的优化过程。

从最原始、最粗暴的写法,一步步演进到规范、易读的版本。

这个过程,我觉得比最终结果更有价值。


我的开发板情况

我用的是STM32F103ZET6开发板,上面有两个LED:

  • LED1:接在 PB5(低电平点亮)
  • LED2:接在 PE5(低电平点亮)

"低电平点亮"的意思是:

  • PB5输出低电平(0V) → LED亮
  • PB5输出高电平(3.3V) → LED灭

要点亮这两个LED,需要完成三步:

  1. 开启GPIOB和GPIOE的时钟
  2. 配置PB5和PE5为推挽输出
  3. 控制PB5和PE5输出低电平

下面,我就按我实际写代码的过程,一步步来。


第一步:最原始的方式

最开始,我是这么写的——直接用指针操作寄存器地址:

c

int main(void)
{
    // 两个LED灯分别连在PB5和PE5上
    
    // 1.时钟配置:开启GPIOB和GPIOE的时钟
    *(uint32_t *)(0x40021000 + 0x18) = 0x48;
    
    // 2.GPIO工作模式配置
    *(uint32_t *)(0X40010C00) = 0x300000;
    *(uint32_t *)(0x40011800) = 0x300000;
    
    // 3.PB5输出低电平
    *(uint32_t *)(0X40010C00 + 0x0C) = 0xFFDF;
    *(uint32_t *)(0x40011800 + 0x0C) = 0xFFDF;
    
    // 用一个死循环保持状态
    while(1)
    {
    }
}

这种写法,优点是最原始、最直观,能帮你理解寄存器编程的本质。

但缺点也很明显:

  • 0x40021000 + 0x18 这种地址,过几天我自己都忘了是什么
  • 地址算错一位,程序就崩了
  • 如果要改配置,得重新算十六进制值

所以,我决定优化一下。


第二步:用stm32f10x.h

我注意到,官方提供的 stm32f10x.h 头文件,已经帮我定义好了所有寄存器和基地址。

于是,我的代码变成了这样:

c

#include "stm32f10x.h"

int main(void)
{
    // 两个LED灯分别连在PB5和PE5上
    
    // 1.时钟配置:开启GPIOB和GPIOE的时钟
    RCC->APB2ENR = 0x48;
    
    // 2.GPIO工作模式配置
    GPIOB->CRL = 0x300000;
    GPIOE->CRL = 0x300000;
    
    // 3.PB5输出低电平
    GPIOB->ODR = 0xFFDF;
    GPIOE->ODR = 0xFFDF;
    
    // 用一个死循环保持状态
    while(1)
    {
    }
}

这一版,代码可读性提高了——RCC->APB2ENR 比 *(uint32_t *)(0x40021000 + 0x18) 直观多了。

但是,我很快发现一个问题……


第三步:用位操作(我踩的坑)

我发现,RCC->APB2ENR = 0x48 这种直接赋值的方式,其实有问题。

假设我之前已经开启了GPIOA的时钟(第2位 = 1),执行 RCC->APB2ENR = 0x48,会把第2位清零,导致GPIOA时钟被关闭。

正确的做法,应该是用位操作,只修改需要的位,不影响其他位:

c

#include "stm32f10x.h"

int main(void)
{
    // 两个LED灯分别连在PB5和PE5上
    
    // 1.时钟配置:用位操作开启GPIOB和GPIOE的时钟
    RCC->APB2ENR |= (1 << 3);  // 开启GPIOB时钟(第3位设为1)
    RCC->APB2ENR |= (1 << 6);  // 开启GPIOE时钟(第6位设为1)
    
    // 2.GPIO工作模式配置:配置PB5为推挽输出、50MHz
    GPIOB->CRL &= ~(0x3 << 20);  // 先把第20~23位清零
    GPIOB->CRL |=  (0x3 << 20);  // 再设置为推挽输出、50MHz
    
    GPIOE->CRL &= ~(0x3 << 20);  // 先把第20~23位清零
    GPIOE->CRL |=  (0x3 << 20);  // 再设置为推挽输出、50MHz
    
    // 3.PB5输出低电平(点亮LED)
    GPIOB->ODR &= ~(1 << 5);  // PB5输出低电平(第5位清零)
    GPIOE->ODR &= ~(1 << 5);  // PE5输出低电平(第5位清零)
    
    // 用一个死循环保持状态
    while(1)
    {
    }
}

这一版,用 |= 和 &= 进行位操作,只修改特定位,不影响其他位,安全多了。

但是,(1 << 3)(0x3 << 20) 这些,还是需要查手册才能知道是哪一位。

所以,我又优化了一版。


第四步:用stm32f10x.h中现成的宏定义(最终版)

我在看 stm32f10x.h 时,发现里面已经定义了很多宏,比如:

c

// RCC时钟使能位定义
#define RCC_APB2ENR_IOPBEN    ((uint32_t)0x00000008)  // GPIOB时钟使能
#define RCC_APB2ENR_IOPEEN    ((uint32_t)0x00000040)  // GPIOE时钟使能

// GPIO_CRL寄存器位定义
#define GPIO_CRL_CNF5         ((uint32_t)0x00C00000)  // CNF5位掩码
#define GPIO_CRL_MODE5        ((uint32_t)0x00030000)  // MODE5位掩码

// GPIO_ODR寄存器位定义
#define GPIO_ODR_ODR5         ((uint16_t)0x0020)  // ODR5位掩码

于是,我的代码变成了这样:

c

#include "stm32f10x.h"

int main(void)
{
    // 两个LED灯分别连在PB5和PE5上
    
    // 1.时钟配置:开启GPIOB和GPIOE的时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;  // 开启GPIOB时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;  // 开启GPIOE时钟
    
    // 2.GPIO工作模式配置:配置PB5和PE5为推挽输出、50MHz
    GPIOB->CRL &= ~GPIO_CRL_CNF5;  // 清除PB5的CNF位
    GPIOB->CRL |= GPIO_CRL_MODE5;  // 设置PB5为推挽输出、50MHz
    
    GPIOE->CRL &= ~GPIO_CRL_CNF5;  // 清除PE5的CNF位
    GPIOE->CRL |= GPIO_CRL_MODE5;  // 设置PE5为推挽输出、50MHz
    
    // 3.PB5输出低电平(点亮LED)
    GPIOB->ODR &= ~GPIO_ODR_ODR5;  // PB5输出低电平
    GPIOE->ODR &= ~GPIO_ODR_ODR5;  // PE5输出低电平
    
    // 用一个死循环保持状态
    while(1)
    {
    }
}

这一版,代码几乎不需要注释,因为宏名字就是最好的注释

比如 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;,一看就知道是在开启GPIOB的时钟。


我的优化过程总结

回顾一下,我的代码是这样一步步优化的:

版本 写法 我的感受
第一步 直接操作地址*(uint32_t *)(0x40021000 + 0x18) = 0x48; 最原始,但能理解本质
第二步 用stm32f10x.h(直接赋值)RCC->APB2ENR = 0x48; 可读性提高,但有坑
第三步 用位操作RCC->APB2ENR |= (1 << 3); 安全了,但还要查手册
第四步 用stm32f10x.h中现成的宏定义RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;GPIOB->CRL &= ~GPIO_CRL_CNF5;GPIOB->CRL |= GPIO_CRL_MODE5; 代码自解释,可读性极高

我的建议是:

  • 学习阶段:从第一步开始,一步步理解每个优化的意义
  • 实际项目:直接用第四步,用stm32f10x.h中现成的宏定义

补充:如何查看stm32f10x.h中有哪些宏定义

有朋友可能会问:我怎么知道stm32f10x.h中有哪些宏定义?

我一般用这三种方法:

方法1:用Keil的代码补全功能

  • 输入 RCC->,会自动提示所有寄存器名
  • 输入 RCC_APB2ENR_,会自动提示所有相关的位定义宏

方法2:直接查看stm32f10x.h文件

  • 在Keil中,右键点击 #include "stm32f10x.h"
  • 选择 "Open Document 'stm32f10x.h'"
  • 然后搜索 RCC_APB2ENR 或 GPIO_CRL,就能找到所有相关的宏定义

方法3:查参考手册

  • 打开STM32参考手册(RM0008)
  • 找到对应寄存器的描述,对照着看stm32f10x.h中的宏定义

完整代码(最终版)

我把最终版的完整代码整理一下,你可以直接复制到Keil中编译。

c

#include "stm32f10x.h"

int main(void)
{
    // 两个LED灯分别连在PB5和PE5上
    
    // 1.时钟配置:开启GPIOB和GPIOE的时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;  // 开启GPIOB时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;  // 开启GPIOE时钟
    
    // 2.GPIO工作模式配置:配置PB5和PE5为推挽输出、50MHz
    GPIOB->CRL &= ~GPIO_CRL_CNF5;  // 清除PB5的CNF位
    GPIOB->CRL |= GPIO_CRL_MODE5;  // 设置PB5为推挽输出、50MHz
    
    GPIOE->CRL &= ~GPIO_CRL_CNF5;  // 清除PE5的CNF位
    GPIOE->CRL |= GPIO_CRL_MODE5;  // 设置PE5为推挽输出、50MHz
    
    // 3.PB5输出低电平(点亮LED)
    GPIOB->ODR &= ~GPIO_ODR_ODR5;  // PB5输出低电平
    GPIOE->ODR &= ~GPIO_ODR_ODR5;  // PE5输出低电平
    
    // 用一个死循环保持状态
    while(1)
    {
    }
}

总结

这篇文章,我没有直接给你"完美代码",而是分享了我写代码时的优化过程:

  1. 最原始的方式(直接操作地址)
  2. 用stm32f10x.h(但直接赋值,有坑)
  3. 用位操作(安全,但仍需查手册)
  4. 用stm32f10x.h中现成的宏定义(最终版,推荐)

我的感受是:

  • 寄存器编程不是"一把梭",而是可以一步步优化的
  • 每一步优化,都是为了解决前一步的问题
  • 不需要自己手写宏定义,stm32f10x.h里都有!
  • 最终版的代码,用官方头文件中的宏定义,代码自解释,可读性极高

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐