时钟合闸了,地址的门道也摸清了,今天手里的活儿就剩 配置引脚(写四位模式)和 翻转引脚(写输出位)两样了。两样都做完,裸寄存器点灯的全程也就打通了。往后再看任何外设库的 GPIO 代码,眼底都是地址和位。

配置寄存器:每个引脚四位
十六条脚的配置,寄存器却只留了一对:CRL 管 pin0-7、CRH 管 pin8-15,配置寄存器:每个引脚四位:低两位 MODE[1:0]、高两位 CNF[1:0]。PC13 在 CRH 管的高半段里,拿一道算术把它的偏移求出来:(13 - 8) × 4 = 20,所以它的四位就是 CRH 的 bit23-20。设备头文件里的宏就是这么写的(stm32f103xb.h L1576-L1632):
#define GPIO_CRH_MODE13_Pos (20U)
#define GPIO_CRH_MODE13_Msk (0x3UL << GPIO_CRH_MODE13_Pos) // 0x00300000
#define GPIO_CRH_CNF13_Pos (22U)
#define GPIO_CRH_CNF13_Msk (0x3UL << GPIO_CRH_CNF13_Pos) // 0x00C00000
四位能凑出的组合一共有十六种,把常用的八种列成表(完整表在 RM0008 第 9 章,RM0008 就是 ST 的 STM32F1 参考手册,下面每一行都是照着手册翻译的):
| MODE |
CNF |
意思 |
典型用途 |
| 00 |
00 |
模拟输入 |
ADC 采样 |
| 00 |
01 |
浮空输入 |
复位默认,外部信号电平稳定时 |
| 00 |
10 |
上拉/下拉输入 |
按键(F1 里上拉还是下拉由 ODR 决定) |
| 01 |
— |
10MHz 输出 |
中速驱动 |
| 10 |
00 |
2MHz 推挽输出 |
咱们点灯的选择 |
| 10 |
01 |
2MHz 开漏输出 |
I2C 这类总线 |
| 11 |
00 |
50MHz 推挽输出 |
高速驱动 |
| 1x |
1x |
复用输出 |
片上外设接管引脚时 |
要让 PC13 当一个普通 2MHz 推挽输出 脚:CNF=00, MODE=10,四位合起来的 0010,落到位上就成了 0x2 << 20 = 0x00200000。对着上一篇 Renode 读回的 CRH = 0x00200000,逐位对上,结果一个位都不差。
复位之后的默认值也值得看上一眼。手册里写的 CRL/CRH 复位值是 0x44444444,拆开之后每个脚分到的都是 0x4,也就是 CNF=01, MODE=00 的浮空输入。所以上电之后所有脚默认都是输入,等您来配。
Renode 的模型又和手册岔了一道:它给的复位值是 0x00000000,在模拟器里读到的复位值就是 0。上一篇的门控偏差是"模型少算了约束",这一回岔的是"模型没按手册设默认值",原则不变:逻辑看的是模型,默认值与时序看的是手册。
这么一来,配置这两行在干什么,已经完全看得懂了:
GPIOC->CRH &= ~(0xFu << 20); // 清掉 PC13 的四位旧配置
GPIOC->CRH |= (0x2u << 20); // 写入新配置: 0010 = 2MHz 推挽输出
在上一篇的反汇编里已经给这两行验过身:bic.w 清位、orr.w 置位,前后两次读改写都落进了 0x40011004。
翻车实录:写错半段的代价
例程的第一版,笔者亲手翻过一次车,当初还留下了一个注释。多亏了 git,把它又翻了出来:
// 2) PC13 模式:pin ≥ 8 的配置段在 CRH(每脚 4 位:pin13 → 偏移
// (13-8)*4=20,CNF 占 bit23:22,MODE 占 bit21:20)。第一版把这段写进
// CRL(配成了 pin5),PC13 保持复位默认的浮空输入,BSRR/BRR 写了
// 也不驱动引脚 —— Renode 里 ODR 读数纹丝不动,当场抓包。
错在哪?笔者把配置写进了 CRL,也就是管 pin0-7 的低半段。0x2 << 20 写进了 CRL,配到的是 (20-0)/4 = 5 号脚:PC5 莫名其妙成了 2MHz 推挽输出,PC13 的四位却原封不动,还是复位默认的浮空输入。
把翻车现场在 Renode 里复现一遍:时钟合闸、CRH 不配、直接写 BSRR:
--- CRH before (model reset):
0x00000000
--- write 0x00002000 to BSRR, then ODR:
0x00000000 ← ODR 纹丝不动
这毛病阴在哪?其实编译零警告、运行零报错。BSRR 的写入"成功"返回,程序照样跑它的循环,而引脚纹丝不动。真翻车的时候,排障的第一反应就是把 CRH/ODR 读上一眼——配置要是没落地,后面写 BSRR 的每一步都在空转。至于"为什么浮空输入模式下输出寄存器不驱动引脚"?引脚内部那两片 MOS 管和四位配置的电路关系,正是下一篇的正题,这里先不展开。
翻转的三条路
配置写好了,PC13 已经成了输出脚。现在要它高低电平翻转,手册给了三条路。
最顺手的路子是直接改 ODR:GPIOC->ODR ^= 0x2000,或者用 |= 置一位、&= ~ 清一位。干活的正是之前见过的读改写三件套:ldr 读回、orr/bic/eor 修改、str 写回。第一条路的问题在于:读和写是两次分开的访问,中间隔着一个窗口。
窗口里能出什么事?把最坏的顺序摆一遍:主循环对 CRH 的读改写刚做完"读"和"改","写"的那一步还没轮到,中断进来了,ISR 也动了同一个寄存器。等中断返回的时候,主循环接着走它没走完的"写",把手里的旧值加上自己的修改写回 CRH,ISR 刚写进去的东西就这么被无声覆盖了。单线程裸机里没人跟您抢,这个窗口也就无害了。不过一旦中断和主循环共享外设,它就是实打实的竞争条件。今天把这个隐患记下,真碰上了再解决,那得等按钮中断那一站。
第二条路的主角是 BSRR 和 BRR:手册专门设了这对寄存器,让一次写就能改好一个位。BSRR 刚在翻车实录里见过,配置没落地时怎么写都不驱动引脚的就是它。这回配置是对的,直接看指令:
08000178: 6125 str r5, [r4, #16] ; BSRR ← 0x2000, 置位 PC13
08000182: 6165 str r5, [r4, #20] ; BRR ← 0x2000, 复位 PC13
数一数,一条 str 就把活干完了。
原子性从哪来?在 BSRR 的语义里:写 1 生效、写 0 无视。这个 32 位寄存器被分成了两半来用。bit0-15 的低半字是置位区:写哪一位,哪个脚就被顶上了高电平。bit16-31 的高半字是复位区:写哪一位,哪个脚就被拉低了。整个动作里没有"读回旧值"的一步:硬件按位独立处理这次写入,每一位只看写了什么。上一条路留下的那个窗口,在这里压根就不存在了。
BRR 干的事更简单:把 BSRR 的复位区单独抽出来,做成一个专管拉低的寄存器,用起来更直白。那万一一次写里同一位的置位和复位都写了 1 呢?赢的是置位区,这是 RM0008 原文写明的。
例程 01_register_led 走的就是第二条路。灭灯靠的是对 BSRR 写 0x2000(1<<13),把 BS13 写成 1,PC13 就被顶高了。点灯的时候对 BRR 写 0x2000,把 BR13 写成 1,PC13 就被拉低了。这里有个初看别扭的细节:Blue Pill 的 LED 接在 PC13 上,偏偏低电平才点亮,所以拉低是亮、顶高是灭,"复位"反而对应点灯。刚上手的时候容易记反,写两回就顺了。
最后一条路是位带:给一个位配上专属的地址,Cortex-M3 把这个机制做进了硬件。外设区的头一兆(0x40000000-0x400FFFFF,GPIOC 的 0x40011000 在内),还有 SRAM 的头一兆。这两段里的每一个位,都在别名区里有一个自己的 32 位地址。往别名地址写值的时候,硬件只看写值的 bit[0]:写 1 原位置 1、写 0 原位清零。这是硬件保证的,单次访问天然就是原子的。地址的算术,现场算一遍:
别名地址 = 0x42000000 + (字节偏移 × 32) + (位序号 × 4)
GPIOC_ODR 的 bit13:
0x42000000 + (0x4001100C - 0x40000000) × 32 + 13 × 4
= 0x42000000 + 0x1100C × 0x20 + 0x34
= 0x42000000 + 0x220180 + 0x34
= 0x422201B4
口说无凭,看 Renode 的实测(时钟合闸后):
--- bit-band write 0x1 to 0x422201B4, then ODR:
0x00002000 ← bit13 应声而起
--- bit-band write 0x0 to 0x422201B4, then ODR:
0x00000000 ← 应声而落
位带的好处是,一个位就是一个专属地址,一条指令就写完了,连 BSRR 那步"算掩码"都省了,读单个位也一样是原子的。代价是那个魔数地址可读性稀烂,所以库代码里少见裸写,用的时候通常拿宏包一层。把三条路摆一起:
| 路 |
指令数 |
原子性 |
读回来 |
适用 |
| ODR 读改写 |
3 |
无窗口保障 |
顺路能读 |
单线程、不在乎干扰 |
| BSRR/BRR |
1 |
单写原子 |
不行 |
中断在场时的正选 |
| 位带别名 |
1 |
读写都原子 |
单位可读 |
高频单位操作、ISR 标志 |
走到这一步,裸寄存器点灯的活儿就全干完了。不过有几个问题一路用着一直没解释:CNF=00 凭什么叫"推挽"?浮空输入到底"浮"在哪?四位配置拧过去,引脚里面的电路发生了什么?下一篇下到电路层把这几个问题讲清。
您来动手
- 您算一遍 PC2 的四位:它在 CRL 还是 CRH?四位是哪几位?2MHz 推挽输出时这两条配置代码怎么写?
- 您在 Renode 里合闸时钟后,手工把 CRH 配成
0x00300000(MODE=11,50MHz),再跑固件,观察翻转行为有没有可见差别,想想为什么。
- 用位带公式手算 IDR(
0x40011008)bit0 的别名地址,到 Renode 里读它,和直接读 IDR 的结果比一比。
- 最后,把
register_led 的 main 里 BSRR 那行改成 GPIOC->ODR ^= 0x2000,反汇编对比一下,前后 main 的指令数差几条。
欸欸!自查一下再走
- PC13 的四位为什么在 CRH 的 bit23-20?您拿
(13-8)×4 现场推一遍。
- 复位默认
0x44444444,拆开数一数,每个脚的默认配置是什么?这种默认为什么对上电安全很重要?
- BSRR 的原子性,靠的是"单条指令",还是"写 1 生效、写 0 无视"的语义?两者缺一行不行?
- 位带地址
0x422201B4 里的 0x34 是怎么来的?代回上面的公式算一算。
参考文献
- STMicroelectronics. RM0008 Reference Manual — STM32F101/102/103/105/107, 2021. https://www.st.com/resource/en/reference_manual/rm0008-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-armbased-32bit-mcus-stmicroelectronics.pdf (9.1 GPIO functional description; 9.2.1-9.2.2 CRL/CRH; 9.2.5-9.2.6 BSRR/BRR)
- Arm Ltd. Arm Cortex-M3 Processor Technical Reference Manual (DDI 0337), 2021. https://developer.arm.com/documentation/ddi0337 (4.2 Bit-banding)
- STMicroelectronics. stm32f103xb.h — CMSIS Device Header, 2016. https://github.com/STMicroelectronics/cmsis_device_f1/blob/master/Include/stm32f103xb.h (L1576-L1632 GPIO_CRH_MODE/CNF definitions)