STM32寄存器开发练习(一):GPIO-从最原始的代码到规范写法

发布时间:2026/7/2 1:09:42

STM32寄存器开发练习(一):GPIO-从最原始的代码到规范写法 前言关于STM32的教程大部分一上来就让我们用HAL库或者标准外设库调用几个函数就搞定了。但这样的话其实不知道底层发生了什么。所以我最近跟着B站尚硅谷老师重新开始学习原始的编程方式——直接操作寄存器这样才能真正理解MCU的工作原理。这一篇我准备点亮开发板上的LED——但我不打算直接给你完美代码而是想分享一下我写代码时的优化过程。从最原始、最粗暴的写法一步步演进到规范、易读的版本。这个过程我觉得比最终结果更有价值。我的开发板情况我用的是STM32F103ZET6开发板上面有两个LEDLED1接在PB5低电平点亮LED2接在PE5低电平点亮低电平点亮的意思是PB5输出低电平0V→ LED亮PB5输出高电平3.3V→ LED灭要点亮这两个LED需要完成三步开启GPIOB和GPIOE的时钟配置PB5和PE5为推挽输出控制PB5和PE5输出低电平下面我就按我实际写代码的过程一步步来。第一步最原始的方式最开始我是这么写的——直接用指针操作寄存器地址cint 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) { } }总结这篇文章我没有直接给你完美代码而是分享了我写代码时的优化过程最原始的方式直接操作地址用stm32f10x.h但直接赋值有坑用位操作安全但仍需查手册用stm32f10x.h中现成的宏定义最终版推荐我的感受是寄存器编程不是一把梭而是可以一步步优化的每一步优化都是为了解决前一步的问题不需要自己手写宏定义stm32f10x.h里都有最终版的代码用官方头文件中的宏定义代码自解释可读性极高

相关新闻