stm32寄存器开发,根据点灯了解寄存器底层逻辑(超详细)
目录
- 前言
- 一、配置寄存器(学会看官方参考手册)
- 1.1 两个手册的区别
- 1.2 GPIO地址公式
- 1.3 怎样看外设基地址
- 1.4 怎样看偏移地址
- 1.5 详细步骤总结
- 选CRL还是CRH(配置引脚工作模式)
- 根据操作选择电平读写寄存器
- 判断逻辑
- 常见错误
- 二、使用APB2 外设时钟使能寄存器
- 2.1 什么是APB2总线
- 2.2 为什么必须开启时钟
- 2.3 RCC_APB2ENR关键bit位
- 三、练习:点亮LED灯
- 3.1 LED原理图分析
- 3.2 完整代码及分析
- led.h
- main.c
- led.c
- led.c代码解析
- 1.开启GPIOC时钟
- 2.清空PC13旧配置
- 3.配置PC13推挽输出 50MHZ
- 4.初始化默认熄灭LED
- 5.led_on() 点亮LED函数
- 6.led_off() 熄灭LED函数
- 3.3 使用BSRR/BRR寄存器
- 四、常见报错总结
- 4.1 没有开启GPIOC外设时钟
- 4.2 缺少`volatile`关键字
- 4.3 CRH寄存器没有先清空旧配置,直接`|=`赋值
- 4.5 指针强制转换括号写错
- 五、Keil Debug寄存器调试(新手必备)
- 5.1正确状态
前言
很多STM32新手直接学HAL库、标准库,只会调用函数,完全不懂底层原理,一旦程序异常、调试报错,完全无从下手,本文从零开始,以点灯为例,让你搞懂寄存器开发
- 怎么看懂官方数据手册
- 怎么自己算出寄存器地址
- 每一行寄存器代码到底在干嘛
- 常见报错 + 完整解决办法
- Keil Debug 寄存器调试实战
实战硬件:STM32F103C8T6
一、配置寄存器(学会看官方参考手册)
1.1 两个手册的区别
- 数据手册:引脚、电气参数
- 参考手册(重点):所有寄存器地址、功能(写代码只看这个)
1.2 GPIO地址公式
寄存器真实地址 = 外设基地址 + 偏移地址
1.3 怎样看外设基地址
打开《STM32F103 参考手册》,搜索存储器映像,会看到一张外设地址表。
这张表里列出所有外设的基地址:就是这个外设的起始首地址。
为了方便阅读,我举GPIO的例子,把它各个端口基地址的图片贴在这
现在我们知道了寄存器的外设基地址,那么偏移地址在哪呢?
1.4 怎样看偏移地址
搜索GPIO寄存器描述,这里包括的所有寄存器都有一个名为地址偏移(Offset)的地址,这个就是偏移地址。
下面给出总结,这里有一些专有名词,现在不懂没关系,你只需要知道如何使用,我将通过一个练习来说明
| 寄存器 | 偏移Offset | 寄存器作用 | 详细说明 |
|---|---|---|---|
| CRL | 0x00 | 0‑7号引脚模式配置寄存器 | 专门配置Px0~Px7(0到7号引脚)的工作模式。每个引脚占用4个bit:CNF(2位)设置功能模式,MODE(2位)设置输出速度。可以配置成推挽输出、开漏输出、输入、复用功能。修改某一个引脚时,要先把对应的4bit清零,避免旧配置残留。 |
| CRH | 0x04 | 8‑15号引脚模式配置寄存器 | 专门配置Px8~Px15(8到15号引脚)。内部结构、位定义和CRL完全一样。只负责高8个引脚,不要用来操作0‑7引脚。 |
| IDR | 0x08 | 输入数据寄存器 | 只读寄存器,不能写。用来读取芯片引脚外部真实电平。某bit为1代表引脚外部是高电平;bit为0代表引脚外部是低电平。做按键输入、外部信号检测时读取这个寄存器。对IDR做写操作没有任何效果。 |
| ODR | 0x0C | 输出数据寄存器 | 可读、可写。用来设置GPIO引脚输出电平。bit位置1,引脚输出高电平;bit位置0,引脚输出低电平。 |
| BSRR | 0x10 | 置位寄存器(置高) | 只写寄存器,读出的数据无意义。对某一位写1,对应GPIO引脚输出高电平;写0不起任何作用。不会影响其他引脚的状态,不需要提前清零,中断中使用很安全。 |
| BRR | 0x14 | 复位寄存器(置低) | 只写寄存器,读出的数据无意义。对某一位写1,对应GPIO引脚输出低电平;写0不起任何作用。不会影响其余引脚,工程上优先推荐BSRR+BRR操作输出。 |
1.5 详细步骤总结
选CRL还是CRH(配置引脚工作模式)
- CRL、CRH专门用来配置引脚模式(输入、输出、速度)。
- 引脚编号0 ~ 7(Px0~Px7)→ 使用CRL(偏移0x00)
- 引脚编号8 ~ 15(Px8~Px15)→ 使用CRH(偏移0x04)
示例:
- PA3、PA7:0‑7号引脚 → CRL
- PC8、PC13:8‑15号引脚 → CRH
判断规则:引脚号≥8,一律使用CRH。
根据操作选择电平读写寄存器
| 想要实现的功能 | 寄存器 | 偏移 |
|---|---|---|
| 读取外部引脚电平 | IDR | 0x08 |
| 普通方式读写输出电平 | ODR | 0x0C |
| 设置引脚输出高电平 | BSRR | 0x10 |
| 设置引脚输出低电平 | BRR | 0x14 |
判断逻辑
- 需要读取外部电压、检测按键 → 使用IDR(只读,不能写)
- 普通方式控制输出电平 → 使用ODR
- 拉高引脚 → BSRR,向对应bit写1
- 拉低引脚 → BRR,向对应bit写1
BSRR、BRR为只写寄存器,读取返回值无意义。
常见错误
- BSRR写0不能拉低引脚,拉低引脚要使用BRR写1
- 读取按键电平不要读ODR,外部真实电平看IDR
- CRL、CRH只设置模式,不能控制引脚输出高低电平
口诀:0‑7用CRL,8‑15找CRH;读按键用IDR;ODR普通输出;BSRR拉高,BRR拉低。
二、使用APB2 外设时钟使能寄存器
2.1 什么是APB2总线
STM32内部外设挂载在不同总线上:
- APB2总线:挂载GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、USART1、SPI1等高速外设
- APB1总线:挂载USART2、TIM2‑TIM4、I2C等外设,由
RCC_APB1ENR控制时钟
GPIO端口全部挂载在APB2总线上,因此操作GPIO,就要操作RCC_APB2ENR。
2.2 为什么必须开启时钟
STM32低功耗机制:上电之后所有外设时钟默认关闭。
时钟相当于外设的工作动力,没有时钟,外设内部电路不工作。
即使你给CRL、CRH、ODR寄存器写入配置,硬件也不会生效,引脚没有任何反应。
开启时钟本质:向RCC_APB2ENR对应bit写1,给对应外设提供时钟信号,外设才能正常工作。
2.3 RCC_APB2ENR关键bit位
写1开启时钟,写0关闭时钟
也就是
| bit位 | 控制外设 |
|---|---|
| bit2 | GPIOA时钟 |
| bit3 | GPIOB时钟 |
| bit4 | GPIOC时钟 |
| bit5 | GPIOD时钟 |
| bit6 | GPIOE时钟 |
三、练习:点亮LED灯
3.1 LED原理图分析
LED点亮条件:阳极电压 > 阴极电压,形成电流。
本电路D2(PC13控制LED)
- LED阳极接VCC3V3(3.3V)
- LED阴极经过510Ω限流电阻R5连接单片机PC13引脚
PC13输出低电平(0V),对应寄存器bit写0
阳极3.3V,阴极0V,存在电压差。
电流路径:VCC3V3 →LED →R5 →PC13。
有电流,LED点亮。PC13输出高电平(3.3V),对应寄存器bit写1
阳极3.3V,阴极3.3V,两端电压几乎相等,没有电流。
LED熄灭。
R5(510Ω)是限流电阻,保护LED,防止电流过大烧坏灯珠。
上方D1:阴极直接通过电阻接GND,上电就常亮,不受单片机控制。
3.2 完整代码及分析
led.h
#ifndef__LED_H#define__LED_Hvoidled_init(void);voidled_on(void);voidled_off(void);#endifmain.c
#include"led.h"intmain(void){led_init();while(1){led_on();led_off();}}led.c
#include"led.h"//PC13 板载LED:高电平灭、低电平亮voidled_init(void){//1.开启GPIOC时钟*((volatileunsignedint*)(0x40021000+0x18))|=1<<4;//2.清空PC13旧配置*((volatileunsignedint*)(0x40011000+0x04))&=~(0xf<<20);//3.配置PC13 推挽输出 50MHZ 00 11*((volatileunsignedint*)(0x40011000+0x04))|=3<<20;//4.默认熄灭*((volatileunsignedint*)(0x40011000+0x0C))|=1<<13;}//点亮LEDvoidled_on(void){*((volatileunsignedint*)(0x40011000+0x0C))&=~(1<<13);}//熄灭LEDvoidled_off(void){*((volatileunsignedint*)(0x40011000+0x0C))|=1<<13;}led.c代码解析
硬件背景:板载LED接PC13;PC13输出低电平(0)灯亮,输出高电平(1)灯灭
寄存器地址说明
- RCC_APB2ENR:
0x40021000 + 0x18时钟控制寄存器- GPIOC_CRH:
0x40011000 + 0x04PC8~PC15模式配置寄存器- GPIOC_ODR:
0x40011000 + 0x0CIO输出电平寄存器- 寄存器访问模板:
*((volatile unsigned int *)(基地址 + 偏移))
#include"led.h"#include "led.h":引入头文件- led.h里面存放
led_init()、led_on()、led_off()函数声明,告诉编译器这几个函数存在。 - 如果不包含头文件,别的文件调用这些函数会报编译警告。
//PC13 板载LED:高电平灭、低电平亮voidled_init(void){void led_init(void):LED初始化函数,程序启动必须调用一次,PC13引脚才可以正常工作。void:函数没有返回值;括号内void:代表调用函数不需要传入任何参数。
1.开启GPIOC时钟
//1.开启GPIOC时钟*((volatileunsignedint*)(0x40021000+0x18))|=1<<4;0x40021000 + 0x18:RCC_APB2ENR寄存器硬件物理地址,STM32寄存器地址固定写死。(volatile unsigned int *):把数字地址强制转为无符号int类型指针。volatile关键字:告诉编译器这是硬件寄存器,禁止编译器做优化,每次读写都直接访问硬件,去掉会出现莫名其妙bug。
- 最前面的
*:指针解引用,代表读写这个地址对应的硬件寄存器。 1 << 4:数字1二进制0001,向左移动4位,得到0001 0000,bit4等于1,其余bit全部为0。
小技巧:借助电脑的计算器:Win+R → 输入 calc → 回车
这个代表偏移
2.清空PC13旧配置
//2.清空PC13旧配置*((volatileunsignedint*)(0x40011000+0x04))&=~(0xf<<20);0x40011000 + 0x04:GPIOC_CRH寄存器地址,PC8~PC15引脚全部由CRH寄存器配置。手册:PC13占用CRH的bit20、bit21、bit22、bit23,一个GPIO引脚占用4bit配置模式。
0xf:十六进制,二进制1111,4个连续的1,正好对应PC13的4个配置位。0xf << 20:将1111整体向左移动20位,对齐bit20~bit23。~(0xf << 20):~按位取反运算符,bit20‑23变成0000,寄存器剩下所有bit全部变成1。&=:按位与赋值,等价a = a & b。运算规则:有0则0,全1才1。- 只把bit20‑bit23强制清零,寄存器其他bit完全不变。
为什么要清零:单片机上电寄存器是随机乱值,不清零旧配置,新引脚模式会出错。
3.配置PC13推挽输出 50MHZ
//3.配置PC13 推挽输出 50MHZ 00 11*((volatileunsignedint*)(0x40011000+0x04))|=3<<20;3二进制=0011。3 << 20:把二进制0011移动到bit20、bit21。
CRH每个引脚4bit格式:
CNF1 CNF0 MODE1 MODE0
PC13:bit23 bit22 bit21 bit20 =00 11
- MODE[1:0] =
11:输出模式,最大速度50MHz - CNF[1:0] =
00:通用推挽输出,引脚既可以输出0V,也可以输出3.3V。
4.初始化默认熄灭LED
//4.默认熄灭*((volatileunsignedint*)(0x40011000+0x0C))|=1<<13;}0x40011000 + 0x0C:GPIOC_ODR输出寄存器地址。ODR每1bit对应一个IO引脚输出电平。bit13对应PC13。- bit13=1:引脚输出高电平3.3V
- bit13=0:引脚输出低电平0V
1 <<13:bit13位置置1,其余bit为0。|=:把ODR寄存器bit13写1。
5.led_on() 点亮LED函数
//点亮LEDvoidled_on(void){*((volatileunsignedint*)(0x40011000+0x0C))&=~(1<<13);}- 操作ODR输出寄存器。
1 <<13:bit13=1。~(1<<13):按位取反,bit13变为0,其他所有bit全部为1。&=:只把bit13写0,其余bit保持不变。
ODR.bit13 =0 →PC13输出0V低电平,LED点亮。
6.led_off() 熄灭LED函数
//熄灭LEDvoidled_off(void){*((volatileunsignedint*)(0x40011000+0x0C))|=1<<13;}- 操作ODR输出寄存器。
1 << 13:bit13位置为1。|=:ODR寄存器bit13置1。
ODR.bit13=1 →PC13输出3.3V高电平,LED熄灭。
3.3 使用BSRR/BRR寄存器
不需要清除位,不会出错,推荐工程使用:
//亮灯*((volatileunsignedint*)(0x40011000+0x14))|=1<<13;//灭灯*((volatileunsignedint*)(0x40011000+0x10))|=1<<13;读者一定要自己写出完整代码检验一下
总结
- 开启外设时钟永远是第一步,不开时钟GPIO完全无效。
- CRH寄存器:每个引脚占4bit,设置引脚工作模式
- 清空旧值:
&= ~(0xf << n) - 设置新模式:
|= 数值 << n
- 清空旧值:
- ODR寄存器:1bit控制1个引脚输出电平
- 输出高电平(置1):
|= 1 << n - 输出低电平(置0):
&= ~(1 << n)
- 输出高电平(置1):
volatile关键字操作硬件寄存器不能省略。- 灯亮灭不只看寄存器0/1,必须看原理图;PC13电路是写0亮,写1灭。
四、常见报错总结
4.1 没有开启GPIOC外设时钟
- 现象:代码逻辑正确,LED无任何反应,寄存器写入无效。
- 原理:STM32外设上电默认关闭时钟,时钟关闭硬件模块不工作。
- 解决:
RCC_APB2ENR |= 1 << 4;开启GPIOC时钟。
4.2 缺少volatile关键字
- 现象:编译无报错,运行功能异常,寄存器读写被编译器优化。
- 原理:
volatile禁止编译器优化,强制每次读写直接访问硬件寄存器。 - 正确写法:
*((volatile unsigned int *)(addr))
4.3 CRH寄存器没有先清空旧配置,直接|=赋值
- 现象:引脚模式异常,输出错乱。
- 原因:上电寄存器是随机值,旧配置位没有清除,新旧数值叠加。
- 正确顺序:先清零
&= ~(0xf << 20),再写入新配置|= 3 << 20。
4.5 指针强制转换括号写错
指针转换括号必须完整,括号位置错误直接编译报错,地址相加也要带括号,血与泪的教训!
五、Keil Debug寄存器调试(新手必备)
- 进入Debug调试(小虫子)
- 打开外设窗口:
- Peripherals → RCC(看时钟)
- Peripherals → GPIOC(看引脚配置)
5.1正确状态
- RCC bit4 = 1(时钟开启)
- CRH bit20~23 = 0011(输出模式)
- ODR bit13可0可1(电平正常翻转)
不会点灯全部靠这三个地方排查
