☰
STM32端口复用与重映射实战:AFIO时钟配置与避坑指南
2026/10/13 16:05:07 网站建设 项目流程

1. 从一个让人抓狂的调试现场说起

刚接触STM32那会儿,我遇到过一个至今印象深刻的场景:明明代码里把某个引脚配置成了普通GPIO输出,LED灯死活不亮,示波器一测,引脚上什么波形都没有。翻遍参考手册、查了时钟使能、确认了寄存器配置,折腾了大半天,最后才发现——这个引脚默认被复用成了别的外设功能,我压根没做重映射,它当然不听GPIO的话。

这个坑,几乎每个STM32初学者都会踩一次。端口复用和重映射这两个概念,看起来只是参考手册里几页表格的事,但真正动手写代码的时候,它们会以各种意想不到的方式跳出来卡住你。更麻烦的是,很多教程只告诉你"要开启复用功能时钟""要配置重映射寄存器",却不解释为什么,导致换个芯片型号或者换个引脚就完全懵了。

这篇内容就是把我自己在端口复用和重映射上踩过的坑、总结的方法、以及实际项目中验证过的配置思路完整梳理一遍。不管你是刚点亮第一颗LED的新手,还是已经能跑通串口和SPI但说不清楚底层配置逻辑的进阶玩家,应该都能从中找到对自己有用的东西。核心关键词就三个:端口复用、重映射、AFIO时钟——这三个词贯穿全文,理解了它们之间的关系,STM32的引脚配置基本就不会再出大问题了。

2. 端口复用到底在复用什么:从芯片引脚的现实约束讲起

2.1 一颗芯片几百个引脚,外设却有几十个

STM32的引脚数量是有限的。以常见的LQFP64封装为例,总共就64个引脚,去掉电源、地、复位、晶振这些必须占用的,真正能拿来做功能引脚的也就50个左右。但STM32内部集成的外设有多少呢?USART至少3个、SPI至少2个、I2C至少2个、还有TIM、ADC、DAC、CAN、USB、SDIO等等,每个外设少则占用2个引脚,多则占用十几个。

算一笔账就明白了:如果每个外设都独占引脚,50个引脚根本不够分。所以芯片设计者采用了一个非常聪明的方案——多个外设共享同一组引脚。这就是端口复用的本质:一个物理引脚,在不同时刻可以扮演不同的角色,具体扮演什么角色,由你的代码配置决定。

打个比方,这就像一间多功能教室,上午用来上数学课,下午用来开班会,晚上还能当自习室。教室还是那间教室,但功能可以切换。STM32的引脚也是这样,PA9这个引脚,你可以让它做普通GPIO输出、也可以让它做USART1的TX、还能让它做TIM1的CH2,具体做哪个,取决于你打开了哪个外设的时钟、以及怎么配置复用寄存器。

2.2 复用功能的默认状态:为什么你的GPIO配置不生效

这里有一个非常关键的细节,很多教程一笔带过,但它恰恰是初学者最容易翻车的地方:STM32的引脚在上电复位后,默认状态并不都是普通GPIO。

以STM32F103系列为例,大部分引脚复位后确实默认是浮空输入模式,可以当普通GPIO用。但有一部分引脚,复位后默认就处于复用功能状态。比如PA13和PA14,复位后默认就是SWD调试接口,你要是想把它们当普通GPIO用,得先关闭调试功能或者做重映射。再比如PB3和PB4,复位后默认是JTAG的JTDO和NJTRST,想当普通GPIO用同样需要额外配置。

我当初就是在这个地方栽了跟头。用PB3驱动一个继电器,代码写得没问题,GPIO初始化也调用了,但继电器就是不动作。后来查手册才发现,PB3默认被JTAG占用了,必须开启AFIO时钟、关闭JTAG功能、再做重映射,才能把它释放出来当普通GPIO用。

提示:拿到一个新引脚准备用的时候,先翻参考手册的引脚定义表,确认它的默认复用功能是什么。如果默认功能不是普通GPIO,那你就需要额外的配置步骤。

2.3 复用功能的配置逻辑:时钟、模式、寄存器三步走

配置一个引脚作为某个外设的复用功能,核心步骤其实就三步,但每一步都有容易忽略的细节。

第一步:开启对应外设的时钟。这个大家都知道,用USART1就要开USART1的时钟,用SPI1就要开SPI1的时钟。但很多人会忘记,复用功能还涉及AFIO时钟。AFIO(Alternate Function I/O)是专门管理引脚复用和重映射的模块,只要你涉及复用功能配置或者重映射操作,就必须开启AFIO时钟。在STM32F1系列中,AFIO时钟挂在APB2总线上,通过RCC_APB2ENR寄存器的第0位使能。

第二步:配置GPIO的模式。复用功能模式下,GPIO的模式配置和普通GPIO有所不同。普通输出用推挽输出或开漏输出,但复用功能下要选择"复用推挽输出"或"复用开漏输出"。输入方向则通常配置为浮空输入或上拉输入,具体取决于外设的要求。比如USART的TX引脚要配置为复用推挽输出,RX引脚配置为浮空输入或上拉输入。

第三步:配置外设本身的寄存器。这一步是外设自己的事,比如USART的波特率、数据位、停止位等。但要注意,外设的引脚映射关系是固定的,你不能随便把USART1的TX分配到任意一个引脚上,只能用它支持的那几个引脚。具体支持哪些引脚,查参考手册的复用功能映射表。

这三步的顺序也有讲究。我习惯先开时钟,再配GPIO,最后配外设。因为如果时钟没开,后面配了也白配;GPIO没配好,外设配置再正确信号也出不来。

3. 重映射:当默认引脚被占用时的Plan B

3.1 重映射解决的是什么问题

端口复用解决了"一个引脚多个功能"的问题,但还有一个现实问题没解决:如果某个外设的默认引脚已经被其他功能占用了怎么办?

举个实际例子。假设你设计了一块板子,USART1的默认TX引脚是PA9,RX引脚是PA10。但你的PCB布局中,PA9和PA10正好被用来接了两个按键,而PB6和PB7空着。这时候你怎么办?重新画板子当然可以,但如果有办法把USART1的TX和RX从PA9/PA10挪到PB6/PB7上,那问题就迎刃而解了。

这就是重映射的价值:把外设的默认引脚映射到另一组备用引脚上。STM32的很多外设都支持重映射,比如USART1、USART2、USART3、TIM1、TIM2、TIM3、SPI1、I2C1、CAN等。但要注意,不是所有外设都支持重映射,也不是所有引脚都能随便映射,具体支持情况必须查参考手册的"复用功能重映射"章节。

3.2 重映射的三种类型:部分重映射与完全重映射

重映射并不是简单的"把A引脚换成B引脚",它有不同的类型,理解这些类型对正确配置至关重要。

以USART1为例,它支持两种重映射方式:不重映射时TX=PA9、RX=PA10;重映射时TX=PB6、RX=PB7。这是比较简单的二选一。

但有些外设的重映射更复杂,比如TIM2。TIM2有四个通道,不重映射时CH1到CH4分别在PA0、PA1、PA2、PA3;部分重映射时CH1和CH2在PA15和PB3,CH3和CH4仍在PA2和PA3;完全重映射时CH1到CH4分别在PA15、PB3、PB10、PB11。你看,部分重映射只挪了一部分引脚,完全重映射才把所有引脚都挪走。

再比如TIM3,部分重映射时CH1和CH2在PB4和PB5,CH3和CH4仍在PB0和PB1;完全重映射时CH1到CH4分别在PC6、PC7、PC8、PC9。这种部分和完全的区别,在配置寄存器时对应不同的位组合,配错了就会出现"有的通道有输出、有的通道没输出"的诡异现象。

3.3 重映射的配置流程与寄存器操作

重映射的配置核心在AFIO模块的寄存器上。STM32F1系列主要涉及两个寄存器:AFIO_MAPR(复用功能重映射和调试I/O配置寄存器)和AFIO_MAPR2(部分型号才有,用于扩展重映射功能)。

以USART1重映射为例,配置步骤如下:

  1. 开启USART1时钟:RCC_APB2ENR |= RCC_APB2ENR_USART1EN;
  2. 开启AFIO时钟:RCC_APB2ENR |= RCC_APB2ENR_AFIOEN;
  3. 开启GPIOB时钟:RCC_APB2ENR |= RCC_APB2ENR_IOPBEN;
  4. 配置PB6为复用推挽输出,PB7为浮空输入或上拉输入
  5. 设置重映射寄存器:AFIO_MAPR |= AFIO_MAPR_USART1_REMAP;
  6. 配置USART1的波特率、数据位等参数

用标准外设库的写法就是:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);

用HAL库的话,重映射的配置通常在HAL_UART_MspInit回调函数中完成,通过__HAL_AFIO_REMAP_USART1_ENABLE()宏来设置。

注意:重映射配置必须在开启AFIO时钟之后进行,否则写寄存器无效。这个顺序问题我见过太多人搞错,包括我自己早期也犯过。

4. 那些年我在复用和重映射上踩过的坑

4.1 忘记开AFIO时钟:最隐蔽的"配置无效"

AFIO时钟这个问题,隐蔽性极强。因为你的代码逻辑看起来完全正确,GPIO配置了、外设配置了、重映射也调用了,但就是没效果。而且它不会报错,编译通过、下载运行,一切看起来都正常,就是功能不对。

我印象最深的一次是调SPI1。SPI1默认引脚是PA5(SCK)、PA6(MISO)、PA7(MOSI),我用的就是默认引脚,按理说不需要重映射。但代码跑起来SCK上就是没有时钟输出。查了半天,最后发现虽然不需要重映射,但SPI1的复用功能配置本身就需要AFIO时钟参与。在STM32F1中,只要涉及复用功能,AFIO时钟就必须开启,哪怕你不做重映射。

这个坑的教训是:不要以为不做重映射就不用管AFIO时钟。只要你的引脚配置成了复用模式,AFIO时钟就老老实实开着,不差这一点功耗。

4.2 重映射后忘记重新配置GPIO

另一个常见错误是:做了重映射,但GPIO还是按原来的引脚配置的。比如把USART1重映射到了PB6/PB7,但GPIO初始化代码里配置的还是PA9/PA10。结果就是PA9/PA10上什么信号都没有,PB6/PB7上也没有,因为PB6/PB7根本没被配置成复用模式。

正确的做法是:先确定最终使用的引脚,再配置GPIO,最后设置重映射寄存器。顺序不能乱,引脚不能错。我现在的习惯是在代码里写注释,明确标注"重映射后TX=PB6, RX=PB7",这样后面检查的时候一眼就能看出问题。

4.3 部分重映射和完全重映射搞混

前面提到过TIM2和TIM3的部分/完全重映射区别。我自己在调TIM3的PWM输出时就犯过这个错:想要四个通道全部输出PWM,配置了部分重映射,结果CH1和CH2有输出,CH3和CH4死活没波形。查了手册才发现,部分重映射只挪了CH1和CH2,CH3和CH4还在原来的引脚上。改成完全重映射后,四个通道全部正常。

这个问题的根源在于没有仔细看参考手册的重映射表格。我的建议是:配置重映射之前,先把参考手册里对应外设的重映射表格截图放在旁边,配置一个通道就对照一个通道,确认引脚对应关系无误。

4.4 调试接口被重映射后无法下载程序

这是一个比较危险的操作。如果你把SWD或JTAG接口的引脚重映射成了普通GPIO,又没有预留其他下载方式,那恭喜你,芯片可能就"锁"了,下次下载程序只能通过BOOT模式或者擦除整片的方式恢复。

我有个朋友就干过这事:把PA13和PA14重映射成了普通GPIO去驱动两个LED,结果SWD接口失效,ST-Link连不上芯片。最后是通过把BOOT0拉高、进入系统存储器启动模式,用串口擦除整片才救回来。

所以,如果你确实需要把调试引脚当普通GPIO用,一定要确保有备用的下载方式,或者在代码里加一个延时,上电后几秒内不执行重映射操作,留出下载窗口。

5. 复用与重映射的实战配置清单

5.1 配置前的检查清单

每次配置复用功能或重映射之前,我都会过一遍这个清单,能避免90%以上的低级错误:

检查项确认内容常见错误
引脚默认功能查参考手册引脚定义表忽略默认复用功能,直接当GPIO用
外设时钟对应外设的RCC使能位忘记开时钟或开错总线
AFIO时钟RCC_APB2ENR的AFIOEN位不做重映射就不开,导致复用无效
GPIO模式复用推挽/复用开漏/浮空输入配成普通输出模式
重映射类型部分重映射还是完全重映射搞混导致部分引脚无输出
最终引脚重映射后的实际引脚编号GPIO配置和重映射不匹配
调试接口是否影响SWD/JTAG重映射后无法下载程序

5.2 一个完整的重映射配置示例

以STM32F103为例,把USART1重映射到PB6/PB7,完整代码如下:

void USART1_Remap_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 开启时钟:USART1 + GPIOB + AFIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // 2. 配置PB6为复用推挽输出(TX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 3. 配置PB7为浮空输入(RX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); // 4. 执行重映射 GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); // 5. 配置USART1参数 USART_InitStructure.USART_BaudRate = baudrate; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); // 6. 使能USART1 USART_Cmd(USART1, ENABLE); }

这段代码里,每一步的顺序都是有讲究的。时钟必须最先开,GPIO配置必须在重映射之前完成,外设配置放在最后。如果你把重映射放在GPIO配置之前,虽然大多数情况下也能工作,但逻辑上不够严谨,某些型号可能会有时序问题。

5.3 HAL库下的重映射配置差异

如果你用的是HAL库,配置方式会有所不同。HAL库把重映射的宏定义放在了stm32f1xx_hal_gpio_ex.h中,通过__HAL_AFIO_REMAP_USART1_ENABLE()这样的宏来操作。

void HAL_UART_MspInit(UART_HandleTypeDef *huart) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (huart->Instance == USART1) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_AFIO_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __HAL_AFIO_REMAP_USART1_ENABLE(); } }

HAL库的好处是宏定义清晰,不容易写错寄存器位。但要注意,HAL库的AFIO时钟使能宏是__HAL_RCC_AFIO_CLK_ENABLE(),别和__HAL_RCC_GPIOB_CLK_ENABLE()搞混了。

6. 进阶话题:复用功能与引脚冲突的排查思路

6.1 当两个外设抢同一个引脚时

实际项目中,引脚冲突是常有的事。比如你用了SPI1的PA5/PA6/PA7,同时又想用TIM2的CH1在PA0上输出PWM,这没问题。但如果你想用TIM2的CH1在PA5上输出PWM,那就和SPI1的SCK冲突了。

排查这类问题的思路是:列出所有外设的引脚需求,画一张引脚占用表,然后对照参考手册的复用功能映射表,看是否有冲突。如果有冲突,优先考虑重映射其中一个外设,或者换用其他外设。

我一般会在项目初期就做这个工作,用Excel画一张表,左边是引脚编号,右边是对应的外设功能,把所有要用到的引脚都列进去。这样一眼就能看出哪些引脚被重复占用了。

6.2 复用功能配置后外设不工作的排查链路

当你配置完复用功能,外设却不工作时,可以按以下顺序排查:

  1. 确认时钟:外设时钟开了吗?AFIO时钟开了吗?GPIO时钟开了吗?
  2. 确认引脚:GPIO配置的引脚和实际使用的引脚一致吗?重映射后引脚变了吗?
  3. 确认模式:GPIO模式配置正确吗?复用推挽还是复用开漏?输入是浮空还是上拉?
  4. 确认重映射:重映射寄存器写对了吗?部分重映射还是完全重映射?
  5. 确认外设:外设本身的配置正确吗?使能了吗?
  6. 确认硬件:引脚外部电路正确吗?有没有上拉电阻、限流电阻?

这个排查链路是我自己总结的,基本上按照这个顺序走一遍,问题都能定位到。最怕的是跳步排查,比如一上来就怀疑外设配置,结果折腾半天发现是AFIO时钟没开。

6.3 重映射对PCB布局的影响

重映射不仅仅是软件的事,它对硬件设计也有影响。如果你在PCB设计阶段就知道某个外设需要重映射,那布线的时候就可以直接按重映射后的引脚来走线,避免后期飞线。

但反过来,如果你在软件调试阶段才发现需要重映射,而PCB已经打样了,那就只能飞线或者改板。所以我的经验是:硬件设计阶段就把所有外设的引脚分配确定下来,尽量使用默认引脚,如果必须重映射,提前在原理图上标注清楚。

另外,重映射后的引脚可能分布在不同的GPIO端口上,比如从PA口挪到PB口,这会影响到PCB的层间走线和信号完整性。高速信号(如SPI时钟、USB差分线)重映射后要特别注意走线长度和阻抗匹配。

7. 我个人的几条实战心得

折腾STM32这么多年,端口复用和重映射这块,我总结了几个自己一直在用的习惯,分享出来供参考。

第一个习惯:每个工程建一个引脚分配表。不管项目大小,我都会在工程目录下放一个pinmap.md或者pinmap.xlsx,记录每个引脚的功能、复用情况、重映射状态。调试的时候对着表查,比翻代码快得多。特别是项目交接的时候,这张表能救命。

第二个习惯:复用功能配置代码集中管理。我不喜欢把GPIO配置散落在各个外设的初始化函数里,而是统一放在一个bsp_pinmux.c文件中,所有引脚的复用和重映射配置都集中在这里。这样一旦出现引脚冲突,只需要改一个文件,不用满工程找。

第三个习惯:重映射操作加注释说明原因。比如// USART1重映射到PB6/PB7,因为PA9/PA10被按键占用。这样后面回头看代码的时候,能立刻明白为什么这么做,而不是对着一行GPIO_PinRemapConfig发呆。

第四个习惯:调试引脚永远保留。不管项目多缺引脚,SWD的PA13和PA14我从来不碰。如果实在需要,就改用SWD模式(只占两个引脚),把JTAG的PB3、PB4、PA15释放出来。这样既保留了下载调试能力,又多出了三个可用引脚。

第五个习惯:新芯片先跑通默认配置再折腾重映射。拿到一颗没用过的STM32型号,我会先用默认引脚把外设跑通,确认芯片和代码都没问题,再去尝试重映射。这样如果重映射后出问题,就能确定是重映射配置的问题,而不是芯片本身或基础代码的问题。

这些习惯看起来都是小事,但每一个都是踩过坑之后才养成的。端口复用和重映射本身并不复杂,复杂的是实际项目中各种约束条件下的取舍和排查。把基础原理吃透,把配置流程固化,把排查思路理清,剩下的就是熟练度的问题了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询