STM32串口重映射总失败?别再瞎试了,这才是真正能跑通的硬核操作 🛠️
你有没有遇到过这种情况:代码写得一丝不苟,引脚也改成了PB6/PB7,可USART1就是死活没信号输出?用示波器一测——TX线上安静得像午夜的实验室,RX也收不到半个字节。而你明明记得“已经打开了重映射”,甚至还在网上抄了好几份所谓的“标准例程”……
别急,这根本不是你的问题。 绝大多数STM32开发者都曾在这个坑里摔过跤 ,而且往往卡上好几天,最后靠换板子、换IDE、甚至怀疑人生才勉强解决。
但真相是: 串口重映射压根儿没那么玄学 。它之所以“看起来”难搞,是因为很多教程只告诉你“怎么做”,却从不解释“为什么必须这样”。今天我们就来撕开这层窗户纸,把整个机制掰碎了讲清楚——让你一次搞懂,永不再错 ✅
你以为只是换个引脚?其实背后牵动的是整个系统级配置 ⚙️
先问一个问题:当你把
USART1_TX
从PA9改成PB6时,STM32内部到底发生了什么?
很多人会说:“哦,就是改了个寄存器呗。”
错!远远不止。
在STM32的世界里,每个外设(比如USART)和它的物理引脚之间,并不是直接连通的。它们中间隔着一个关键角色——
AFIO(Alternate Function I/O)控制器
。你可以把它想象成一个“交通调度中心”:默认情况下,
USART1
的数据走的是PA9这条路;但如果你要让它改道到PB6,就必须提前通知这个调度中心,否则数据根本不知道该往哪走。
更麻烦的是,这个“调度中心”本身也有个开关——
AFIO时钟
。如果这个时钟没开,哪怕你写了
AFIO->MAPR |= ...
,那也是对空气下命令,完全无效!
所以你看,一个看似简单的“重映射”,实际上涉及四个环节:
1. 打开AFIO时钟(让调度中心开始工作)
2. 修改MAPR寄存器(告诉调度中心换路线)
3. 配置新引脚为复用功能(确保PB6愿意接待USART1的数据流)
4. 初始化USART外设(正式启动通信)
任何一个环节掉链子,都会导致“配置了却没反应”的诡异现象。
为什么你的重映射总是失败?这些隐藏雷区90%的人都踩过 💣
我们来看几个真实项目中高频出现的“自杀式写法”。
❌ 雷区1:跳过AFIO时钟使能 → 操作等于没做
// 错误示范
AFIO->MAPR |= AFIO_MAPR_USART1_REMAP; // 直接写!boom!
你以为这一行就能开启重映射?Too young.
在STM32F1系列中,所有对
AFIO->MAPR
的操作都依赖于APB2总线上的AFIO模块供电。而这个供电是由时钟控制的。也就是说,
你不先打开RCC_APB2ENR中的AFIOEN位,AFIO外设压根就没电,自然不会响应任何配置
。
这就是为什么有些人发现:“我明明设置了remap,怎么还是从PA9出信号?”
答案很简单:因为你的设置压根没生效,系统仍在走默认路径。
✅ 正确姿势:
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // 第一步!必须先给AFIO上电
记住一句话: 没有AFIO时钟,就没有重映射 。这不是建议,这是铁律 🔒
❌ 雷区2:暴力赋值MAPR寄存器 → 不小心关掉了JTAG调试 😱
另一个极其危险的操作是这样的:
// 危险操作!不要模仿!
AFIO->MAPR = 0x00100000; // 只设置USART1_REMAP=1
乍一看没问题,对吧?但你有没有想过:
AFIO->MAPR
这个寄存器可不是专属于USART1的!它是多个外设共用的一个全局配置寄存器,里面还管着:
- JTAG/SWD 调试接口映射
- 定时器通道重定向
- CAN引脚选择
- …
你这一句
AFIO->MAPR = XXXX
,相当于把其他所有配置全清零了。最惨的情况是什么?——
你把自己的SWD下载口给关了!
结果就是:程序烧不进去,调试器连不上,只能通过BOOT0进ISP模式救砖……是不是听着就很痛?
✅ 安全做法永远是: 读-改-写
uint32_t temp = AFIO->MAPR;
temp &= ~AFIO_MAPR_USART1_REMAP_Msk; // 先清除原有位
temp |= AFIO_MAPR_USART1_REMAP; // 再设置新值
AFIO->MAPR = temp; // 最后整体写回
这样做既能精准修改目标字段,又能保护其他外设配置不受影响。工业级产品必须这么干!
❌ 雷区3:GPIO模式配错了 → 引脚“形同虚设”
即使你前面两步都做对了,如果第三步GPIO配置出错,照样白搭。
比如下面这段常见错误:
GPIOB->CRL |= GPIO_CRL_MODE6_1; // 设置PB6为50MHz输出
// 忘记设置CNF6!!!
问题来了:MODE只是决定速度,CNF才决定功能模式!
对于复用推挽输出(即用来当TX),你需要同时设置:
- MODE: 输出速度(如50MHz)
- CNF: 功能模式 = 复用推挽(
CNF = 10b
)
查一下参考手册就知道:
| CNF[1:0] | 含义 |
|---------|------|
| 00 | 输入模式(模拟) |
| 01 | 输入模式(浮空) |
| 10 | 输入模式(上/下拉) |
| 11 |
复用功能推挽输出
✅ |
所以正确的配置应该是:
GPIOB->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6); // 清零相关位
GPIOB->CRL |= GPIO_CRL_MODE6_1 | GPIO_CRL_CNF6_1; // MODE=50MHz, CNF=11 => 复用推挽
同理,RX引脚要设为输入模式,推荐使用“上拉输入”以防止悬空干扰:
GPIOB->CRL &= ~(GPIO_CRL_MODE7 | GPIO_CRL_CNF7);
GPIOB->CRL |= GPIO_CRL_CNF7_1; // CNF7=10 -> 上拉输入
// 注意:还需额外设置PUPD电阻
GPIOB->ODR |= GPIO_ODR_ODR7; // PB7上拉
📌 小贴士:如果你不确定当前引脚状态,可以用逻辑分析仪或万用表测一下电压。正常情况下,未通信时TX应为高电平(空闲态)。如果一直是低电平,大概率就是GPIO模式没配对。
来吧,手把手带你走一遍真正的黄金四步法 ✨
现在我们抛开所有花里胡哨的封装库,直接面对寄存器,一步一步实现 可靠、稳定、可移植 的USART1重映射。
目标:将
USART1_TX/RX
从默认的PA9/PA10迁移到PB6/PB7
芯片:STM32F103C8T6(LQFP48封装支持此重映射)
主频:72MHz,PCLK2=72MHz
波特率:115200
Step 1️⃣ 开启所有必要的时钟
顺序很重要!必须按依赖关系依次开启:
// ① 先开AFIO时钟 —— 这是重映射的前提!
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
// ② 再开GPIOB时钟 —— 因为我们要用PB6/PB7
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
// ③ 最后开USART1时钟 —— 外设初始化前才需要
// (这里暂时不开,等GPIO配完再说)
⚠️ 特别提醒:有些资料说“IOPxEN可以随便什么时候开”,理论上没错,但从工程实践角度, 建议严格按照“资源依赖顺序”来 。这样不仅逻辑清晰,还能避免某些极端情况下的初始化异常(尤其是低功耗场景)。
Step 2️⃣ 配置AFIO_MAPR进行重映射
接下来告诉芯片:“我要把USART1搬到PB6/PB7”。
// 读取当前MAPR值,保留其他配置
uint32_t mapr_temp = AFIO->MAPR;
// 清除USART1_REMAP字段(bit 2)
mapr_temp &= ~AFIO_MAPR_USART1_REMAP_Msk; // 等价于 & ~0x0000000C
// 设置为“部分重映射”模式(值为1)
mapr_temp |= (1 << 2); // 或者直接用宏:AFIO_MAPR_USART1_REMAP
// 写回寄存器
AFIO->MAPR = mapr_temp;
🔍 解释一下:
AFIO_MAPR_USART1_REMAP_Msk
是ST官方CMSIS头文件定义的掩码,对应bit 2~3。虽然USART1只有两种状态(0=PA9/10,1=PB6/7),但它占两位是为了与其他系列保持兼容。
🤔 有人问:“能不能直接用 |= ?”
不推荐!因为万一之前已经是1了,再|=一次也没问题;但如果原来是2或3呢?就可能出错。安全起见,永远先清零再赋值。
Step 3️⃣ 配置PB6和PB7为复用功能
这是最容易出错的地方,一定要严格按照数据手册要求设置。
✅ PB6 (TX):复用推挽输出,50MHz
// 配置CRL寄存器(控制Port B低8位)
// PB6 对应 CRL 的 bit[27:24]
// 先清除旧配置
GPIOB->CRL &= ~(0xF << (4 * 6)); // 清除MODE6和CNF6(共4位)
// 设置:MODE6 = 11 (50MHz), CNF6 = 11 (复用推挽输出)
GPIOB->CRL |= (0xB << (4 * 6)); // 0xB = 1011b → MODE=11, CNF=11
✅ PB7 (RX):上拉输入
// 清除旧配置
GPIOB->CRL &= ~(0xF << (4 * 7));
// 设置:MODE7 = 00 (输入), CNF7 = 10 (上拉/下拉输入)
GPIOB->CRL |= (0x8 << (4 * 7)); // 0x8 = 1000b
// 额外设置上拉电阻
GPIOB->ODR |= GPIO_ODR_ODR7; // PB7输出高,启用上拉
💡 为什么RX推荐上拉而不是浮空?
因为在实际电路中,如果对方设备未连接或处于断电状态,RX线容易受到噪声干扰。加上拉可以保证空闲时为高电平,符合UART协议规范,提升抗干扰能力。
Step 4️⃣ 初始化USART1外设
终于到了最后一步。此时硬件通道已准备就绪,我们可以安全地启动USART1了。
// 开启USART1时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
// 波特率设置:BRR = f_PCLK / baudrate
// PCLK2 = 72MHz, 115200 → 72000000 / 115200 ≈ 624.99 → 取整625
USART1->BRR = 625;
// 配置控制寄存器
USART1->CR1 = 0; // 先清零
USART1->CR1 |= USART_CR1_TE // 使能发送
| USART_CR1_RE // 使能接收
| USART_CR1_UE; // 使能USART
// 可选:等待发送完成,确保初始化稳定
while (!(USART1->SR & USART_SR_TC));
至此,USART1已在PB6/PB7成功运行!
如何验证你真的做对了?三个层次的调试方法 🔍
光写代码不够,还得会验证。以下是我在项目中常用的三级验证法:
Level 1️⃣ 寄存器级验证(最快)
打开Keil或STM32CubeIDE的 寄存器视图 ,检查以下几点:
-
RCC->APB2ENR是否包含AFIOEN,IOPBEN,USART1EN -
AFIO->MAPR的 bit2 是否为1 -
GPIOB->CRL中对应PB6/PB7的4位是否正确设置 -
USART1->CR1的UE位是否置位
只要这几项都对,基本可以排除配置错误。
Level 2️⃣ 信号级验证(最准)
拿个示波器或逻辑分析仪,探一下PB6:
- 上电瞬间是否有起始位(下降沿)?
- 波特率是否接近115200?(周期≈8.68μs)
如果没有起始位,说明USART根本没发;
如果有但乱码,可能是波特率算错了;
如果是规律方波,恭喜你,通信链路通了!
Level 3️⃣ 数据级验证(最实用)
写个小函数测试收发:
void usart1_send_byte(uint8_t ch) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = ch;
}
int main(void) {
USART1_Remap_Config();
while (1) {
usart1_send_byte('H');
usart1_send_byte('i');
usart1_send_byte('\r');
usart1_send_byte('\n');
for (volatile int i = 0; i < 1000000; i++);
}
}
接到串口助手,应该能看到连续输出
Hi
。如果能收到,说明软硬件全部打通!
实战案例:我在智能水表项目中如何靠重映射救场 🚑
去年做一个NB-IoT远传水表,主控是STM32F103C8T6,需求如下:
- 使用USART1与NB模块通信(AT指令)
- PA9/PA10原本要用于高级定时器PWM输出(驱动阀门)
- PC调试也需要串口打印日志
矛盾出现了: 同一个串口不能既接模组又接电脑 ,而PA9/PA10又被PWM刚需占用。
怎么办?
👉 解决方案: 利用重映射,把调试串口挪到PB6/PB7
这样一来:
- PA9/PA10 → TIM1_CH1/CH2 → 控制阀门开度
- PB6/PB7 → USART1_TX/RX → 接CH340转USB,供调试用
- NB模块则通过USART2(PD5/PD6)连接
完美避开引脚冲突,还不用改PCB!
更妙的是,在量产时可以直接禁用调试串口,节省功耗。开发阶段插个杜邦线就能打印日志,灵活得不行。
这正是重映射带来的 设计弹性 ——同样的硬件,通过软件配置适应不同阶段的需求。
高阶技巧:如何写出可移植、易维护的重映射代码?🧩
别再写那种“只能在这块板子上跑”的硬编码了。真正的高手会让代码具备跨平台能力。
技巧1️⃣ 封装成独立模块
// usart_remap.h
#ifndef __USART_REMAP_H
#define __USART_REMAP_H
typedef enum {
USART_REMAP_NONE,
USART_REMAP_PARTIAL,
USART_REMAP_FULL
} UsartRemapMode;
void USART1_Remap(UsartRemapMode mode);
void USART1_UnRemap(void);
#endif
// usart_remap.c
#include "usart_remap.h"
void USART1_Remap(UsartRemapMode mode) {
// 统一时钟使能
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPBEN;
uint32_t temp = AFIO->MAPR;
temp &= ~AFIO_MAPR_USART1_REMAP_Msk;
switch(mode) {
case USART_REMAP_PARTIAL:
temp |= AFIO_MAPR_USART1_REMAP; // PB6/PB7
break;
case USART_REMAP_FULL:
// 某些大容量型号支持 full remap,此处省略
break;
default:
break; // none,保持默认
}
AFIO->MAPR = temp;
}
这样以后想切换映射模式,只需调一行函数,再也不用手动算寄存器了。
技巧2️⃣ 结合编译选项适配不同芯片
#if defined(STM32F103xB)
#define HAS_USART1_PARTIAL_REMAP
#elif defined(STM32F103xE)
#define HAS_USART1_FULL_REMAP
#endif
然后在代码中做条件编译,确保不会在不支持的芯片上调用非法映射。
那些没人告诉你但超级有用的细节 💡
🔸 不是所有封装都支持重映射!
这是很多人忽略的一点。STM32F103C8T6虽然是主流型号,但它有不同封装(TSSOP20/LQFP48/QFN32等)。其中:
- LQFP48 :支持USART1部分重映射(PB6/PB7)
- TSSOP20 :引脚太少,根本不带PB6/PB7 → 无法重映射!
所以在选型时就要确认:你要改的引脚,在目标封装中是否存在。
📌 建议:使用STM32CubeMX工具查看具体封装的可用引脚,避免纸上谈兵。
🔸 重映射会影响SWD调试吗?
会!特别是当你不小心改了
AFIO_MAPR
中关于JTAG/SWD的位时。
例如:
-
SWJ_CFG[2:0]
控制着PA13~PA15的功能
- 默认是
100
:PA13=SW-DIO, PA14=SW-CLK, PA15=JTDI
- 如果你设成
111
,就会关闭所有调试接口,只能ISP救砖
所以再次强调: 修改MAPR时务必保留原始值中关于调试接口的部分 !
稳妥做法是在初始化时备份
AFIO->MAPR
,或者使用STM32CubeMX生成初始配置。
🔸 可以动态切换重映射吗?
技术上可行,但强烈不推荐!
因为:
- 切换过程中可能导致通信中断
- 若新引脚未及时配置,可能出现短路风险
- 多任务环境下容易引发竞态条件
更好的做法是: 在系统启动阶段一次性确定映射方案,之后不再更改 。
如果真有动态需求(比如双模式设备),建议通过外部跳线或Boot引脚选择,而不是运行时修改。
写在最后:底层理解才是解决问题的终极武器 🧱
你看,串口重映射这件事,本质上并不复杂。它只是一个涉及 时钟、复用、引脚、外设 四者的协同配置流程。但正因为缺少系统性的认知,很多人只能靠“试错+复制粘贴”来应付,一旦环境变化就束手无策。
而当你真正明白了:
- 为什么必须先开AFIO时钟?
- 为什么不能直接赋值MAPR?
- 为什么GPIO模式必须精确匹配?
你就不会再被“玄学问题”困扰。你会知道每一行代码背后的硬件动作,能在出错时快速定位根源,甚至能预判哪些操作会有副作用。
这才是嵌入式开发的核心竞争力。
所以下次当你看到“XX功能不工作”时,别急着换库、换IDE、换开发板。静下心来,打开参考手册第8章(AFIO)和第9章(GPIO),一行一行读过去。你会发现, 大多数所谓的“bug”,其实都是我们还没理解的“feature” 😉
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
2384

被折叠的 条评论
为什么被折叠?



