1. 从一次调试翻车说起:为什么端口复用和重映射值得单独拎出来讲
刚接触STM32那会儿,我遇到过一个至今印象深刻的坑。当时在做一个多传感器采集的小板子,原理图上明明把超声波模块的Trig引脚连到了PA9,代码里也老老实实写了GPIO_SetBits(GPIOA, GPIO_Pin_9),结果示波器一挂上去,引脚纹丝不动,倒是串口那边莫名其妙开始往外吐乱码。折腾了大半天,翻数据手册才发现——PA9默认是USART1_TX的复用功能,我既没把它配成普通推挽输出,也没关掉串口外设的时钟,两个外设抢一个引脚,最后谁也没干好活。
这件事让我意识到,端口复用(Alternate Function)和重映射(Remap)这两个概念,看着像是GPIO章节里不起眼的一小节,实际上是STM32开发中绕不开的基本功。你只要用到串口、SPI、I2C、定时器PWM、CAN这些外设,就一定会和它打交道。很多初学者卡在“代码没错但引脚没反应”“外设初始化了但收不到数据”这类问题上,十有八九就是复用和重映射没搞明白。
这篇内容我打算把STM32的端口复用和重映射从头到尾捋一遍。会讲清楚它到底是什么、为什么STM32要这么设计、AF寄存器怎么配、重映射在F1和F4系列上有什么本质区别、实操中怎么一步步把引脚切到想要的外设上,以及我自己踩过的那些坑。不管你是刚点亮LED的新手,还是已经能跑通串口但说不清底层原理的进阶玩家,应该都能从里面找到有用的东西。核心关键词就三个:端口复用、重映射、AF寄存器配置,围绕它们展开。
2. 端口复用和重映射到底在解决什么问题
2.1 一个引脚凭什么能干好几件事
STM32的引脚数量是有限的。以常见的LQFP64封装为例,去掉电源、地、复位、BOOT这些,真正能拿来用的IO大概就50个出头。但芯片内部集成的外设有多少呢?随便数数:3个USART、2个SPI、2个I2C、1个CAN、多个定时器、ADC、DAC、SDIO、FSMC……每个外设都需要引脚。如果每个外设都独占一组引脚,那芯片得做成几百个脚才够用。
所以STM32的设计思路是:一个物理引脚,通过内部的多路选择器,可以连接到不同的外设信号线上。同一时刻只能选一个,但你可以根据需要切换。这就是端口复用的本质——引脚是硬件资源,复用是让这个资源在不同外设之间共享的机制。
打个比方,一个引脚就像一间办公室,GPIO是“房东自己用”,复用功能是“租给USART当TX”“租给TIM当PWM输出”“租给SPI当SCK”。同一时间只能有一个租客,但你可以换租客。默认状态下,大部分引脚归GPIO管,你要用外设功能,就得办个“交接手续”,这个手续就是配置AF寄存器。
2.2 复用和重映射不是一回事
很多人把这两个概念混着说,其实它们解决的是不同层面的问题。
端口复用解决的是“这个引脚能不能当外设引脚用”的问题。比如PA9,它可以是GPIO,也可以是USART1_TX,还可以是TIM1_CH2。你通过配置AF寄存器(F1系列叫AFIO,F4系列叫GPIOx_AFRL/AFRH),告诉芯片“我现在要PA9走USART1_TX的信号”。
重映射解决的是“这个外设的信号能不能换个引脚出来”的问题。还是PA9和USART1_TX,在F1系列里,USART1的TX默认就在PA9,但如果你PA9被占了,可以通过重映射把USART1_TX挪到PB6上去。重映射改变的是外设信号和物理引脚之间的对应关系。
一句话总结:复用是“引脚选功能”,重映射是“功能换引脚”。F1系列有独立的重映射机制,F4及以后的系列基本用AF矩阵统一管理,重映射的概念被弱化了,但底层逻辑是一样的。
2.3 为什么F1和F4的配置方式差这么多
这是我在实际项目里感受最深的一点。F1系列(比如STM32F103)用的是AFIO外设+重映射寄存器的方案。你要用复用功能,得先开AFIO时钟,然后配置GPIO的模式(比如复用推挽输出),再通过AFIO_MAPR寄存器决定要不要重映射。这套机制比较“手工”,寄存器分散,容易漏配。
F4系列(比如STM32F407)换成了AF矩阵方案。每个GPIO引脚有AFRL和AFRH两个寄存器,每个引脚占4个bit,直接写AF编号(0~15)就能选中对应的外设功能。比如PA9要当USART1_TX,就写GPIO_AF7_USART1。不需要单独开AFIO时钟,也不需要重映射寄存器,所有复用关系都在数据手册的“Alternate function mapping”表里查。
这个差异直接导致了两套代码写法完全不同。你在网上搜到的F1教程,放到F4上跑不通,很多时候就是卡在这里。下面我会分别讲两套配置流程,你对照自己的芯片系列看。
3. 核心细节拆解:AF寄存器、复用模式和数据手册怎么查
3.1 数据手册里的复用表才是唯一权威
我见过太多人凭记忆或者凭别人的代码配引脚,结果引脚功能不对。配复用功能之前,第一件事永远是翻数据手册的Alternate function mapping表。这张表会告诉你每个引脚支持哪些复用功能,以及对应的AF编号是多少。
以STM32F407的PA9为例,数据手册里会列出:
| 引脚 | AF0 | AF1 | AF2 | AF3 | AF4 | AF5 | AF6 | AF7 | ... |
|---|---|---|---|---|---|---|---|---|---|
| PA9 | SYS | TIM1_CH2 | TIM15_BKIN | - | I2C3_SMBA | SPI2_SCK | - | USART1_TX | ... |
你要用USART1_TX,就选AF7。要当TIM1_CH2用,就选AF1。这个编号是硬件固定的,写错了引脚就没信号。F1系列没有AF编号的概念,而是通过“默认复用”和“重映射”两种状态来区分,查表方式不同,但同样以数据手册为准。
提示:数据手册(Datasheet)和参考手册(Reference Manual)要分清。引脚复用表在数据手册里,寄存器详细说明在参考手册里。两个都要备着。
3.2 GPIO模式必须和复用功能匹配
配好了AF编号,GPIO的工作模式也得设对。这里有个容易忽略的点:复用功能下,GPIO的输出模式要设成复用推挽或复用开漏,而不是普通的推挽/开漏。
为什么?因为普通输出模式下,GPIO的输出寄存器直接控制引脚电平;复用模式下,引脚的控制权交给了外设,GPIO的输出寄存器不再起作用。如果你把USART1_TX的引脚设成普通推挽输出,那串口外设根本控制不了这个引脚,发出去的数据自然出不来。
输入方向也一样。比如USART1_RX,要设成复用输入模式(F1里叫浮空输入或上拉输入,F4里叫复用模式+输入)。具体设成上拉还是浮空,要看外设要求和外部电路。串口RX一般设上拉或浮空都行,I2C的SDA/SCL则必须设成复用开漏,因为I2C总线需要线与逻辑。
3.3 时钟使能顺序不能乱
这是另一个高频翻车点。STM32的外设时钟和GPIO时钟是分开的,用哪个外设,就得开哪个外设的时钟,同时开对应GPIO端口的时钟。
以F4系列用USART1为例,需要开的时钟有:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE)—— GPIOA时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE)—— USART1时钟
F1系列还要多一个:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)—— AFIO时钟,只有F1需要
顺序上,我习惯先开GPIO时钟,再开外设时钟,最后配引脚。虽然大多数情况下顺序不影响结果,但先开时钟再操作寄存器是更稳妥的习惯,避免在时钟没开的时候写寄存器无效。
4. 实操过程:F1和F4两套配置流程完整走一遍
4.1 F1系列端口复用配置步骤
以STM32F103把PA9配成USART1_TX为例,完整流程如下。
第一步,开时钟。GPIOA、USART1、AFIO三个时钟都要开:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1 | RCC_APB2Periph_AFIO, ENABLE);第二步,配置GPIO模式。TX是输出,设成复用推挽,速度按需选:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);第三步,如果要重映射,配置AFIO_MAPR。比如把USART1重映射到PB6/PB7:
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);注意,重映射之后,引脚就变成PB6和PB7了,对应的GPIO初始化也要改成GPIOB的Pin6和Pin7。这一步很多人会忘,结果代码里还在配PA9,自然没反应。
第四步,配置USART1本身(波特率、字长、停止位等),然后使能。
F1的重映射有个细节:部分重映射和完全重映射是分开的。比如USART1只有完全重映射(TX→PB6,RX→PB7),而USART3有部分重映射(TX→PC10,RX→PC11)和完全重映射(TX→PD8,RX→PD9)两种。用哪个,查参考手册的AFIO_MAPR寄存器说明。
4.2 F4系列端口复用配置步骤
同样以USART1_TX为例,F407的PA9配置流程。
第一步,开时钟。只需要GPIOA和USART1,不需要AFIO:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);第二步,配置GPIO模式为复用模式:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);第三步,关键一步,设置AF编号:
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);这个GPIO_AF_USART1在标准库头文件里定义好了,值就是7。你也可以直接写7,但用宏更清晰。
第四步,配置USART1并使能。
F4没有独立的重映射寄存器,因为AF矩阵已经涵盖了所有可能的引脚映射。同一个外设信号可能出现在多个引脚上(比如USART1_TX可以在PA9、PB6、PB7等),你选哪个引脚,就配哪个引脚的AF编号,本质上就是“重映射”的灵活版。
4.3 用寄存器直接操作的方式
如果你用寄存器开发或者想搞清楚底层,F4的AF配置就是操作AFRL和AFRH。PA9属于高8位,用AFRH:
GPIOA->AFR[1] &= ~(0xF << ((9 - 8) * 4)); // 清掉PA9的AF位 GPIOA->AFR[1] |= (7 << ((9 - 8) * 4)); // 设为AF7每个引脚占4个bit,AFRH管Pin8~Pin15,AFRL管Pin0~Pin7。这个计算方式要记清楚,写错了就会配到别的引脚上去。
F1的寄存器方式则是操作GPIOx_CRL/CRH设置模式,再操作AFIO_MAPR设置重映射。CNF位和MODE位的组合决定复用模式,具体位定义查参考手册。
5. 常见问题与排查技巧实录
5.1 引脚没输出,怎么一步步定位
这是最高频的问题。我总结了一套排查顺序,基本能覆盖90%的情况。
| 排查项 | 检查内容 | 常见错误 |
|---|---|---|
| 时钟 | GPIO和外设时钟是否都开 | 只开了GPIO,忘了外设时钟 |
| 模式 | GPIO模式是否为复用模式 | 设成了普通推挽/开漏 |
| AF编号 | AF值是否和数据手册一致 | 记错编号或写错引脚 |
| 重映射 | F1是否配了AFIO_MAPR | 配了重映射但引脚没改 |
| 引脚冲突 | 是否有其他外设占用同引脚 | 两个外设抢一个脚 |
| 硬件 | 引脚是否被外部电路拉死 | 上拉/下拉电阻冲突 |
我遇到过一次,F1的USART1重映射到PB6/PB7之后,串口还是没数据。查了半天发现PB6同时接了I2C1的SCL,虽然I2C没初始化,但硬件上有个上拉电阻,加上重映射后两个外设的信号在引脚上打架。后来把I2C的引脚换掉才解决。所以引脚冲突不一定是软件问题,硬件上共用引脚也要注意。
5.2 复用开漏和复用推挽怎么选
这个问题我被问过很多次。简单说:
- 复用推挽:用于大多数输出场景,比如USART_TX、SPI_SCK、PWM输出。推挽能主动输出高和低,驱动能力强。
- 复用开漏:用于需要线与逻辑的总线,比如I2C的SDA和SCL。开漏只能主动拉低,高电平靠外部上拉电阻。多个设备挂同一条总线时,任何一个拉低总线就是低,不会出现推挽那种“一个输出高一个输出低导致短路”的问题。
选错了会怎样?I2C用推挽,多个设备同时通信时可能烧引脚;USART_TX用开漏,没有外部上拉的话高电平出不来,数据全是乱的。
5.3 重映射之后原来的引脚还能用吗
能,但要看情况。F1的重映射是“把外设信号从默认引脚挪到重映射引脚”,默认引脚在重映射使能后就释放了,可以当普通GPIO用。但要注意,如果默认引脚上还接着其他外设,或者外部电路有特殊连接,释放后可能影响其他功能。
F4没有这个问题,因为AF矩阵是每个引脚独立配置的。你把PA9配成USART1_TX,PB6不配AF就是普通GPIO,互不影响。
5.4 调试时的一个实用技巧
如果你不确定引脚到底有没有配成功,可以在初始化之后加一段测试代码:把引脚临时切成普通推挽输出,翻转几次,用示波器或LED看有没有反应。如果有反应,说明GPIO本身没问题,问题在复用配置或外设上;如果没反应,说明GPIO模式或时钟有问题。这个二分法能快速缩小排查范围。
另一个技巧是查寄存器。在调试器里直接看GPIOx_AFRL/AFRH、GPIOx_MODER、RCC的时钟使能寄存器,比看代码更直接。代码可能因为宏定义或者库函数版本问题和你以为的不一样,寄存器不会骗人。
6. 几个容易忽略的进阶细节
6.1 复用功能下的上下拉配置
F4的GPIO_PuPd可以设上拉、下拉或浮空。复用输出模式下,上下拉一般不影响输出,但复用输入模式下就很重要。比如USART_RX,如果外部没有上拉,建议设内部上拉,避免引脚悬空时收到乱码。I2C的SDA/SCL则必须外部上拉,内部上拉太弱,带不动总线。
F1没有独立的上下拉配置,输入模式下通过CNF位选浮空、上拉或下拉。复用输入一般选浮空或上拉,具体看外设要求。
6.2 重映射和中断的关联
F1的AFIO还管外部中断的引脚映射。比如EXTI9_5中断,默认对应PA5~PA9,但通过AFIO_EXTICR寄存器可以把它映射到PB5~PB9或PC5~PC9。这个和端口复用是两套机制,但都在AFIO里配置。如果你用了外部中断又改了引脚,记得检查EXTICR。
6.3 低功耗模式下的引脚状态
进入低功耗模式前,复用引脚的状置会影响功耗。比如一个配成复用推挽输出的引脚,如果外设已经关闭但引脚还保持输出状态,可能会有漏电流。稳妥的做法是进入低功耗前把不用的复用引脚重新配成模拟输入或浮空输入,降低功耗。这个在电池供电的项目里尤其重要。
7. 我个人的实操体会
端口复用和重映射这块,我最大的体会是:别背代码,背流程。具体寄存器值、AF编号这些,数据手册里都有,查一下就行。但配置的流程和逻辑顺序,必须刻在脑子里:开时钟→配模式→设AF/重映射→配外设→使能。这个顺序乱了,或者漏了哪一步,就会出现各种“代码看着没错但就是不工作”的问题。
另外,F1和F4的差异一定要分清。我见过有人拿着F103的代码往F407上移植,AFIO那套东西在F4上根本不存在,编译都过不了。反过来,F4的AF配置方式放到F1上也不行。所以拿到一个新芯片,先确认系列,再找对应的配置方法。
最后分享一个小习惯:每次配完复用引脚,我都会在代码注释里写清楚“这个引脚配成了什么功能、AF编号是多少、对应数据手册哪一页”。过几个月回头看代码,或者别人接手你的项目,能省很多时间。这个习惯看起来不起眼,但实际项目里真的能救命。