当前位置: 首页 > news >正文

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


前言

关于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_APB2ENRGPIO_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里都有!
  • 最终版的代码,用官方头文件中的宏定义,代码自解释,可读性极高
http://www.cnnetsun.cn/news/3092857.html

相关文章:

  • 从推荐系统到大模型:算法工程师的转型实战指南
  • 机械设计公差与配合实战指南:从核心原理到图纸标注
  • 零代码设计小米穿戴表盘:Mi-Create让创意触手可及
  • 为什么说APAxpo已然成为各大品牌新品首发的核心阵地?
  • Redis Bitmap 实现北极星日淘用户签到与活跃度统计(极致省内存)
  • 2026大二寸证件照制作工具指南:手机App、免费无水印小程序操作教程
  • Topit:告别窗口切换烦恼,让你的Mac窗口永远在最前面
  • 机电安装公司有哪些?广州机电安装公司推荐!
  • IDEA大纲导航突然卡顿?,紧急排查清单:内存泄漏、插件冲突、AST缓存溢出——3分钟定位根因的5个诊断命令
  • Claude 3.5语义压缩层解析:零偏移输出与灰度信息蒸发
  • GPT-4o深度解析:技术落地与工程避坑指南
  • 三通道直流电阻测试仪的现场效率对比
  • 如何在Blender中高效创作GTA V模型:Sollumz插件实战指南
  • Playwright元素定位实战:从原理到健壮策略的完整指南
  • STM32驱动WS2812全彩LED:SPI+DMA高效实现动态光效
  • Anthropic Mythos:语义约束引擎驱动的推理阶跃
  • Navicat Mac版无限试用重置终极指南:3分钟解决14天试用限制
  • MATLAB水果蔬菜颜色识别工具:KNN分类+RGB/HSV特征提取
  • Postman接口自动化测试:从工具到框架的实战指南
  • 国内主流大厂toekn价格
  • 大模型版本命名规范与事实核查指南
  • Claude 3.7 Sonnet:面向软件开发的可调控推理模型
  • 从Selenium到Playwright:构建稳定高效的跨浏览器自动化测试实战
  • 阴阳师百鬼夜行终极自动化指南:如何用智能脚本解放你的双手
  • Spring Boot MockMvc实战:高效测试REST API的完整指南
  • 用心理学原理强化AI工程纪律:权威、承诺与社会认同的实战框架
  • Mythos门控发布:大模型推理深度与责任治理的双重跃迁
  • Anthropic Mythos:可信推理链与门控式能力发布解析
  • Claude推理中间层‘蒸发’:模型内核如何替代Router Layer
  • AI系统五大核心组件:告别大模型幻觉的工程化方案