找回密码
立即注册
搜索
发回帖 发新帖

6568

积分

0

好友

831

主题
发表于 6 小时前 | 查看: 8| 回复: 0

时钟合闸了,地址的门道也摸清了,今天手里的活儿就剩 配置引脚(写四位模式)和 翻转引脚(写输出位)两样了。两样都做完,裸寄存器点灯的全程也就打通了。往后再看任何外设库的 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 凭什么叫"推挽"?浮空输入到底"浮"在哪?四位配置拧过去,引脚里面的电路发生了什么?下一篇下到电路层把这几个问题讲清。

您来动手

  1. 您算一遍 PC2 的四位:它在 CRL 还是 CRH?四位是哪几位?2MHz 推挽输出时这两条配置代码怎么写?
  2. 您在 Renode 里合闸时钟后,手工把 CRH 配成 0x00300000(MODE=11,50MHz),再跑固件,观察翻转行为有没有可见差别,想想为什么。
  3. 用位带公式手算 IDR(0x40011008)bit0 的别名地址,到 Renode 里读它,和直接读 IDR 的结果比一比。
  4. 最后,把 register_led 的 main 里 BSRR 那行改成 GPIOC->ODR ^= 0x2000,反汇编对比一下,前后 main 的指令数差几条。

欸欸!自查一下再走

  • PC13 的四位为什么在 CRH 的 bit23-20?您拿 (13-8)×4 现场推一遍。
  • 复位默认 0x44444444,拆开数一数,每个脚的默认配置是什么?这种默认为什么对上电安全很重要?
  • BSRR 的原子性,靠的是"单条指令",还是"写 1 生效、写 0 无视"的语义?两者缺一行不行?
  • 位带地址 0x422201B4 里的 0x34 是怎么来的?代回上面的公式算一算。

参考文献

  1. 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)
  2. Arm Ltd. Arm Cortex-M3 Processor Technical Reference Manual (DDI 0337), 2021. https://developer.arm.com/documentation/ddi0337 (4.2 Bit-banding)
  3. 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)



上一篇:PostgreSQL 19 延期发布:AI 代码审查太强,人类修 Bug 速度跟不上了
下一篇:Fotor Agent 实测:自然语言直出 MG 动画,这个国产视频 Agent 真能打
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-22 08:10 , Processed in 0.071154 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表