4. GPIO复用功能与重映射:AFR寄存器配置、复用功能选择与引脚冲突解决

各位做嵌入式开发的朋友,今天我们来聊聊GPIO复用功能与重映射。说实话,这个知识点在项目里踩坑率极高。我见过不少工程师,明明代码逻辑没问题,板子就是跑不起来,最后发现是引脚功能冲突了。

嗯,咱们直接进入正题。

4.1 AFR寄存器配置——复用功能的“开关”

每个GPIO引脚,除了做普通的输入输出,还能干别的活。比如PA9,你可以让它输出高低电平,也可以让它变成USART1的TX发送数据。这个切换,靠的就是AFR寄存器。

AFR全称是Alternate Function Register,也就是复用功能寄存器。在STM32中,每个端口有16个引脚,对应两个AFR寄存器:AFRL(低8位)和AFRH(高8位)。

每个引脚占用4位,用来选择15种复用功能中的一种。说白了,就是给引脚分配一个“第二职业”。

核心要点:

  • AFRL控制Pin0~Pin7,AFRH控制Pin8~Pin15
  • 每个引脚4位,取值0~15,对应AF0~AF15
  • 复位后默认是AF0,也就是系统功能

配置代码其实很简单,我习惯用HAL库或者直接操作寄存器。直接操作寄存器的方式更直观:

// 将PA9配置为AF7(USART1_TX)
GPIOA->AFR[1] |= (7 << 4);  // AFRH,Pin9对应第4位开始

这里要注意,AFR[1]对应的是AFRH,AFR[0]对应AFRL。我刚开始学的时候,经常搞混下标,后来干脆记成:下标0管低8位,下标1管高8位。

4.2 复用功能选择——USART、SPI、I2C、TIM怎么配

不同的外设,对应的AF编号不一样。这个得查数据手册,别靠猜。

我整理了一张常用外设的AF映射表,以STM32F103为例:

外设功能 AF编号 常用引脚
USART1_TX AF7 PA9, PB6
USART1_RX AF7 PA10, PB7
SPI1_SCK AF5 PA5, PB3
SPI1_MOSI AF5 PA7, PB5
I2C1_SCL AF4 PB6, PB8
I2C1_SDA AF4 PB7, PB9
TIM2_CH1 AF1 PA0, PA15

配置的时候,先查表找到AF编号,然后设置对应的寄存器。举个例子,配置PA9为USART1_TX:

// 1. 开启GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

// 2. 配置PA9为复用推挽输出
GPIOA->CRH &= ~(0xF << 4);  // 清除配置
GPIOA->CRH |= (0xB << 4);   // 复用推挽输出,50MHz

// 3. 选择AF7功能
GPIOA->AFR[1] |= (7 << 4);

我个人习惯,配置完一定要检查一下寄存器值。用调试器读一下AFR,确认写进去了。有一次我忘了开启时钟,折腾了半天才发现。

4.3 重映射功能配置——引脚不够用时的救命稻草

什么叫重映射?说白了,就是同一个外设功能,可以换到别的引脚上。比如USART1,默认在PA9/PA10,但你可以把它重映射到PB6/PB7。

为什么要重映射?因为引脚冲突了。你想想看,一个项目里要用USART、SPI、I2C、好几个定时器,引脚就那么几个,不打架才怪。

重映射的配置,主要靠AFIO寄存器。以STM32F103为例,重映射控制寄存器是AFIO->MAPR。

我的经验:重映射之前,一定要先开启AFIO时钟。很多人忘了这一步,结果配置无效。我曾经因为这个,浪费了整整一个下午。

代码示例:将USART1重映射到PB6/PB7

// 1. 开启AFIO时钟
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;

// 2. 配置重映射
AFIO->MAPR |= AFIO_MAPR_USART1_REMAP;

// 3. 配置PB6/PB7为复用功能
GPIOB->CRL &= ~(0xFF << 24);  // 清除PB6/PB7配置
GPIOB->CRL |= (0xBB << 24);   // 复用推挽输出

// 4. 选择AF7
GPIOB->AFR[0] |= (7 << 24);   // PB6
GPIOB->AFR[0] |= (7 << 28);   // PB7

这里要注意,不是所有功能都能重映射。有些功能只有一组引脚,没得选。所以选型的时候,就得提前规划好引脚分配。

4.4 调试接口(SWD/JTAG)的GPIO复用——小心把调试口搞没了

这个坑,我估计很多人都踩过。SWD接口的SWDIO和SWCLK,默认在PA13和PA14上。JTAG接口占用更多引脚:PA15、PB3、PB4。

如果你把这些引脚配置成普通GPIO,调试器就连不上了。嗯,后果就是——程序烧不进去,板子变砖。

警告:千万不要轻易把SWD引脚配置成普通GPIO!除非你确定不再需要调试功能,或者有备用调试接口。

如果你确实需要释放这些引脚,可以这样做:

// 禁用JTAG,保留SWD
AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_1;  // 0x02,仅SWD

// 完全禁用调试接口(慎用!)
// AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_2;  // 0x04,全禁用

配置之后,PA15、PB3、PB4就可以当普通GPIO用了。但PA13和PA14还是SWD功能,调试器还能连。

我曾经犯过一个错误:为了省引脚,把SWD全禁用了。结果程序跑飞了,想调试都调不了,只能重新烧bootloader。从那以后,我再也不敢全禁用了。

4.5 实际项目中的引脚冲突解决——一个真实案例

去年我做了一个项目,主控是STM32F103C8T6,要用到:

  • USART1(调试打印)
  • USART2(与传感器通信)
  • SPI1(驱动LCD)
  • I2C1(读取温度传感器)
  • TIM2_CH1(PWM输出)

引脚一分配,发现冲突了。USART1默认在PA9/PA10,SPI1的SCK在PA5,MOSI在PA7,这些都不冲突。但I2C1的SCL在PB6,而USART1重映射后也在PB6。

怎么办?我的解决方案是:

  1. USART1保持默认引脚PA9/PA10,不动
  2. I2C1重映射到PB8/PB9(I2C1支持重映射)
  3. TIM2_CH1用PA0,不冲突

最终引脚分配表:

功能 引脚 备注
USART1_TX PA9 默认
USART1_RX PA10 默认
USART2_TX PA2 默认
USART2_RX PA3 默认
SPI1_SCK PA5 默认
SPI1_MOSI PA7 默认
I2C1_SCL PB8 重映射
I2C1_SDA PB9 重映射
TIM2_CH1 PA0 默认

这个案例告诉我们,做项目之前,先画一张引脚分配表。把所有要用到的功能列出来,看看有没有冲突。有冲突就考虑重映射,或者换功能。

避坑指南:

  • 先查数据手册的引脚定义表,确认每个功能有哪些可选引脚
  • 优先使用默认引脚,减少配置复杂度
  • 重映射时,记得开启AFIO时钟
  • 调试接口引脚,非必要不改动
  • 画PCB之前,用Excel或表格工具做引脚分配

好了,关于GPIO复用功能与重映射,就讲到这里。说白了,就是学会怎么给引脚分配“第二职业”,以及当引脚不够用时怎么“换岗”。这些技能,在实际项目中几乎天天用。希望各位能少踩坑,一次点亮。