玩过STM32的人应该都有这种经历:最小系统板拿到手,正想从PA0开始挨个点灯,结果发现引脚旁边印着一堆复用功能,看着就头大。STM32F103C8T6这颗经典的Cortex-M3芯片,48个引脚里藏着37个可以作为GPIO使用的管脚,而几乎每个GPIO都带着三四个“第二身份”。想真正用好它,第一步就是把管脚功能和复用机制彻底搞清楚。这篇文章我就从引脚布局说起,结合我实际踩过的坑,聊聊怎么从原理图、芯片手册和CubeMX三个角度快速掌握F103C8T6的管脚分配。无论你是刚入门的玩家、做课程设计的学生,还是正在调试USART、I2C、PWM、编码器这类外设的开发者,这篇文章应该都能给你省下不少翻手册的时间。
1. 引脚全景:先搞懂48只脚里哪些能“自由发挥”
1.1 电源、复位、启动脚:接错直接翻车
F103C8T6是LQFP48封装,引脚看着多,但有一类引脚不是用来传数据的——电源和启动脚。先把这些认清楚,比背一百个GPIO名字都重要。
VBAT是备份电源脚,给RTC和备份寄存器供电。如果不做低功耗RTC功能,直接把它接到3.3V就可以,很多最小系统板就是这么干的。VDDA和VSSA是模拟电源,这个脚特别容易被忽略,但它直接影响ADC采样质量。C8T6这个封装里没有单独引出VREF+,参考电压是内部和VDDA绑定的,所以VDDA的滤波比想象中更关键——别只丢一颗100nF,最好再并一颗10uF,而且电容要尽量靠近引脚放。
NRST是复位脚,芯片内部有上拉,但外接一个100nF电容到地能有效抗干扰,很多设计还会再加一颗10k上拉到3.3V,提升稳定性。BOOT0和PB2共同决定启动模式,BOOT0为低时从Flash启动,这是正常模式;BOOT0为高、PB2(也就是BOOT1)为低时进入系统存储器,也就是串口烧录用的ISP模式。很多翻车案例都是BOOT0悬空导致芯片随机进入异常模式,所以板子上BOOT0必须接一颗10k下拉。同样,PB2这个脚在复位瞬间会被当成BOOT1采样,如果悬空,就有可能触发SRAM启动,导致程序没跑起来——这也是不少人“代码没问题但就是跑不动”的原因之一。
还有一对隐藏的“多功能脚”——PD0和PD1。它们是外部高速晶振的输入输出脚,绝大多数板子都用它们接8MHz HSE晶振。只有当你的项目确定不打算使用外部晶振时,才可以把它们当成普通GPIO来规划。如果你用内部HSI跑系统,又想把PD0/PD1拿出来当GPIO,就要问自己:后续要不要上USB、要不要做高精度串口波特率?如果答案是要,那还是老老实实把这两个脚留给晶振吧。
1.2 FT标识:5V容忍不是所有脚都有
F103C8T6是3.3V芯片,但很多外设是5V逻辑,比如传统舵机、LCD1602、5V供电的传感器模块。这时候就有个概念必须分清:芯片的引脚能不能直接输入5V电平。
官方数据手册的引脚定义表里有一个FT列,标了FT的引脚才支持5V容忍,意思是这个引脚可以直接输入5V高电平而不会损伤芯片。注意,这说的是输入容忍,不是说它能输出5V。所有GPIO输出高电平都只有3.3V,想给5V设备送高电平,一样需要电平转换或开漏加外部上拉。
所以,“能不能接5V”这件事,不要凭感觉,也不要看开发板丝印。最简单的做法就是把数据手册Table 4找出来,对着FT那一栏逐个确认。很多人第一次自己画板子,直接把5V单片机的传感器输出接到某个没标FT的引脚上,短时间没事,时间长了引脚内部保护二极管就会出问题,表现为那个脚永远读不到正确电平,甚至整片芯片异常发热。
顺带说一句:FT引脚可以接受5V输入,但如果输入端有上拉到5V,而这个引脚又被配置成推挽输出,一旦输出低电平,就等于5V直接通过外部上拉灌进芯片,电流路径不安全。所以FT引脚接5V上拉时,输出模式最好用开漏,让外部上拉决定高电平电压。
2. 复用机制深度拆解:AFIO到底在切换什么
2.1 GPIO的输入输出模式与“复用”的关系
很多人第一次接触“复用”这个词,以为就是把一个引脚当两种功能轮流用,其实不是。STM32的复用是指:引脚不再由GPIO的ODR寄存器直接控制,而是把控制权交给某个外设,比如USART的发送电路、定时器的PWM输出、SPI的时钟信号。
F103的GPIO配置里有几种模式,日常最容易搞混的是“推挽输出”和“复用推挽输出”的区别。普通推挽输出,是你自己写ODR寄存器,引脚就直接输出高低电平;复用推挽输出,是外设电路接管了引脚,你写ODR反而没用。同理还有复用开漏输出,I2C的SCL和SDA就常用这个模式配合外部上拉电阻。
这里有个容易踩的坑:USART的TX脚要配成复用推挽输出,但RX脚不能复用推挽,得配成浮空输入或上拉输入。有人把RX也设成复用推挽,结果串口只能发不能收,查半天才发现引脚模式就错了。复用输入和外设输入的关系类似,外设只是“听”引脚上的电平,引脚本身还是要按输入模式配置。
另一个关键点是AFIO时钟。F103里要使用重映射功能,或者使用外部中断的引脚映射,必须先使能AFIO时钟。标准和HAL库的坑不太一样,标准库有个经典的遗漏:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);这行没写,后面的GPIO_PinRemapConfig基本都是无效的。HAL库在MX_GPIO_Init里通常会自动处理,但如果你手动初始化,也别忘了开这个时钟。
2.2 C8T6常用外设的引脚占用与重映射速查
直接上干货,下面这张表涵盖C8T6实际可用的主流外设映射,画板前对着它做初步规划非常方便。但我要强调一句:所有引脚映射最终必须以官方数据手册Table 4为准,我的表只是帮你快速筛选,不能替代手册。
| 外设 | 默认引脚 | 是否可重映射到其他脚 | 实际可用情况 |
|---|---|---|---|
| USART1 | PA9_TX / PA10_RX | 可重映射到PB6/PB7 | 很常用,重映射后能释放PA9/PA10 |
| USART2 | PA2_TX / PA3_RX | 无 | 固定引脚,注意和ADC、TIM2冲突 |
| USART3 | PB10_TX / PB11_RX | 部分/完全重映射到PC10/PC11、PD8/PD9 | C8T6封装没有这些脚,实际只能用默认位置 |
| I2C1 | PB6_SCL / PB7_SDA | 部分型号支持重映射 | 多数板子直接用默认脚,注意和USART1重映射冲突 |
| I2C2 | PB10_SCL / PB11_SDA | 无 | 和USART3完全撞车,二选一 |
| SPI1 | PA5_SCK / PA6_MISO / PA7_MOSI | 可重映射到PA15/PB3/PB4 | 重映射前提是先把JTAG关闭,且PB3/PB4默认被调试占用 |
| TIM1_CH1/2/3/4 | PA8/PA9/PA10/PA11 | 完全重映射到PE9~PE12 | C8T6没有PE引脚,实际等于不能换位置 |
| TIM2_CH1/2/3/4 | PA0/PA1/PA2/PA3 | 有部分和完全重映射 | 重映射脚会跑到PA15/PB3等,需关JTAG |
| TIM3_CH1/2/3/4 | PA6/PA7/PB0/PB1 | 有重映射选项 | 部分重映射会到PB4/PB5,也占JTAG位 |
| TIM4_CH1/2/3/4 | PB6/PB7/PB8/PB9 | 无 | 和I2C1/CAN1有大面积重叠 |
| CAN1_RX/TX | PB8/PB9 | 可重映射到PA11/PA12 | 重映射后和USB D-/D+冲突,极其容易撞车 |
| USB_DP/DM | PA11/PA12 | 无 | USB占死,且和CAN1重映射冲突 |
| ADC1/2外部通道 | PA0~PA7、PB0、PB1 | 无 | C8T6只引出10个外部ADC通道,PC0~PC5这个封装没有 |
| MCO时钟输出 | PA8 | 无 | 可输出内部或外部时钟,优先给其它芯片用 |
这张表看下来你会发现,C8T6不是缺外设,而是引脚资源紧到“用了这个就得放弃那个”的程度。比如PB10/PB11同时是USART3和I2C2的默认位置,你不可能让两个外设同时默认占用;PA11/PA12同时是USB和CAN1重映射的位置,用了USB就别想用CAN。
2.3 冲突处理思路:USART1和TIM1同时用怎么办
TIM1是高级定时器,通道位置很多都和通信外设重叠,最典型的就是TIM1_CH2在PA9、TIM1_CH3在PA10,而这两个脚正好是USART1的TX和RX默认位置。新手调PWM时喜欢把四个通道全用上,结果发现串口没法初始化,或者串口通了但PWM通道少一半。
解决思路有两种。第一种是给USART1做重映射,把它怼到PB6/PB7,把PA9/PA10让给TIM1的CH2和CH3。代价是PB6/PB7同时也是I2C1的默认位置,如果你还要用硬件I2C,就得更仔细地错开。
第二种思路更常见:干脆不用TIM1的这几个通道,改用TIM2或TIM3。TIM2的通道分布在PA0~PA3,TIM3分布在PA6/PA7/PB0/PB1,很多板子上这些脚是独立的,不一定和串口冲突。要PWM就优先挑定时器通道,再看这些通道有没有占用UART或I2C,这样规划起来更省心。
我现在画板子的习惯是:先列外设清单,再列每个外设的可选引脚,最后做一张占用矩阵,把不能共存的组合一眼标出来。这比焊好板子再跳线救火高效得多。
3. 最小系统板的管脚规划逻辑:从原理图反推
3.1 一块常见C8T6最小板上各脚去哪了
市面上最常见的C8T6最小系统板,其实管脚分配是有规律的,看懂一块,其他板子原理图基本都能猜个七七八八。
电源部分:VBAT接3.3V,VDDA走一颗10uF加100nF到地,VDD每个引脚旁都有一颗100nF,这是常规操作。8MHz晶振接在PD0/PD1,旁边是两颗22pF负载电容,入门设计里还常并联一颗1M反馈电阻,确保晶振起振稳定。如果要跑RTC,PC14/PC15会再接一颗32.768kHz晶振;不用RTC时这两个脚可以当GPIO,但速度受限,不能当高速输出用。
启动部分:BOOT0有一个跳线或拨码开关,默认下拉到地。PB2在多数板上没有引出到跳线,但设计上会放一个下拉电阻,保证复位时BOOT1只能采到低电平。
功能部分:PA9/PA10引到串口接口,给板载USB转串口芯片用;PA11/PA12做成USB接口的D-/D+;PA13/PA14接SWD调试口。LED几乎总是在PC13上,原因是这个脚离电源比较远,而且芯片设计上PC13、PC14、PC15本身驱动能力偏弱,直接驱动一个LED加上限流电阻是极限了,别拿它去推继电器或者大功率器件。
USB接口和串口共用一个芯片时,很多板子会把USB的DP/DM放得离PA11/PA12很近,但如果没有做静电保护和限流电阻,热插拔几次就可能把引脚打坏。所以自己设计时,USB差分线上至少加一颗22R串联电阻,有条件再加ESD保护器件。
3.2 画板前做一张引脚占用表
我自己画板子前一定会先在表格里过一遍引脚,而不是直接在原理图里随手拉网络。表格不复杂,列几个字段就行:功能模块、使用引脚、GPIO复用类型、是否需要上拉下拉、备注。
比如一个例子:按键扫描用PA0~PA3,配成上拉输入;OLED用I2C1,PB6/PB7配成复用开漏;RGB灯用PA8的TIM1_CH1,配成复用推挽;调试串口用UART1的PA9/PA10;SWD用PA13/PA14预留。
这样列完,有没有冲突一目了然。尤其是PA8一旦被MCO或TIM1_CH1占用,这个引脚就不能再做普通按键输入了;PA9/PA10一旦被串口占用,TIM1_CH2/CH3的位置就得用别的定时器补上。
我见过最典型的问题:有人想在蓝牙模块、OLED、按键、编码器全部塞进C8T6,结果列完表发现GPIO根本不够用。这时候正确的做法是提前砍功能,或者把I2C改成GPIO模拟,把矩阵键盘改成轮询,而不是把引脚重复使用硬塞。芯片上的复用只是选择“由哪个外设来控制这个脚”,不是让你一个脚同时干两件事的。
4. 实操配置:标准库、HAL库与CubeMX的管脚设置
4.1 标准库模式的“三板斧”
F103圈子里标准库仍然有大量老项目在用,配置管脚复用无非三板斧:开GPIO时钟、开AFIO时钟、配置GPIO模式,需要重映射的再补一步GPIO_PinRemapConfig。
举个例子,把USART1重映射到PB6/PB7,然后把PA9/PA10让给TIM1_CH2/CH3:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_TIM2, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; 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);这里有个细节:重映射某外设之前,要确保新位置的引脚没被其它外设占用。比如我把USART1重映射到PB6/PB7,如果我还想用I2C1,就冲突了。还有,GPIO_Mode_IN_FLOATING只适用于输入功能脚,比如USART1的RX,别套到TX上。
如果要用SPI1的重映射到PA15/PB3/PB4,逻辑就不一样了,因为这组引脚默认属于JTAG调试接口。必须先执行:
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);把JTAG关闭,保留SWD,才能把PA15、PB3、PB4释放出来给复用外设。注意,只关JTAG不关SWD,这样PA13/PA14还能继续调试;如果你连SWD也关了,板子就彻底失去在线调试能力,只能靠串口ISP救回来。
4.2 CubeMX里怎么看复用、怎么避免耽误事
CubeMX的优势在于,你点一下引脚,它就把这个引脚所有可选复用项列出来了,并且会自动生成HAL库初始化代码,不用手动去翻重映射寄存器。比如你点一下PA9,弹出的列表里会有USART1_TX、TIM1_CH2、CAN_TX重映射等选项,选好之后它会自动在代码里帮你配置AFIO。
但CubeMX也有迷惑性。很多人生成工程后,只看外设配置,不看生成的引脚代码,结果在板子上发现引脚不通。建议生成完代码,第一时间打开main.c里的MX_GPIO_Init和对应外设的MspInit回调函数,看一眼GPIO到底被初始化成了什么模式。比如HAL_UART_MspInit里会看到:
GPIO_InitStruct.Alternate = GPIO_AF1_USART1; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);如果你手动改了引脚,CubeMX的Pinout view会同步变化,但还是那句话,引脚表最终以芯片手册为准。C8T6这种48脚封装,CubeMX里高亮的可用引脚非常多,但像PC13这种低驱动能力的脚,CubeMX不会给你任何提示,只能自己在设计时记着限流。
还有一个常见的坑:CubeMX里如果把PA13、PA14设成普通GPIO输出,它会自动把调试口功能关掉。生成代码时你会看到类似“不保持调试接口”的配置,烧完程序就再也连不上ST-Link了。碰到这种情况不要慌,把BOOT0拉高,进入系统存储器模式,用串口ISP把Flash全片擦除,再回到BOOT0拉低,重新烧录就能恢复。所以我的建议是:除非引脚实在不够用,否则永远给SWD留两个脚。
5. 把复用在项目里用顺:PA8的PWM+DMA驱动WS2812
5.1 为什么这个场景会选PA8
热词里有个很具体的用例:用PA8脚,通过PWM+DMA驱动一颗WS2812。这个场景能很好地把管脚复用、定时器PWM、DMA三种东西串起来讲。
WS2812是单线通信,需要按精确时序输出高低电平。直接用GPIO翻转加延时,系统一旦被中断打扰,波形就走样,颜色会变得不可控。更稳的办法是让定时器产生PWM波形,同时用DMA把一串预设好的占空比值搬运到定时器的比较寄存器里,形成连续的色码波形。
为什么选PA8?因为PA8默认就是TIM1_CH1的输出脚。TIM1是高级定时器,挂在APB2总线上,在72MHz主频下可以获得更高分辨率的PWM。同时PA8不占UART、I2C这些常用的调试通信口,把串口留给了PA9/PA10,SWD留给了PA13/PA14,这对平时调试很友好。
配置的思路是:TIM1输出PWM,通道映射到PA8,GPIO配成复用推挽输出。假如你的板子上PA8引脚又被MCO功能用到,比如想从PA8引出系统时钟给外部芯片,那就冲突了,二选一。这个细节在CubeMX里不会自动提示,要自己心里有数。
5.2 时序参数计算
WS2812的数据格式是按GRB顺序,每颗灯24bit,高位在前。bit的时序大概如下:
| 信号 | 高电平时间 | 低电平时间 |
|---|---|---|
| 逻辑0 | 0.35~0.45us | 0.8~1.0us |
| 逻辑1 | 0.65~0.95us | 0.2~0.45us |
| RESET | 低电平 >50us | — |
在72MHz主频下,TIM1的时钟也是72MHz,一个计数周期是1/72MHz约等于13.9ns。把自动重装载值ARR设成89,也就是90个计数周期,得到一个bit周期约1.25us,完全落在WS2812可接受的范围内。
逻辑1的高电平时间取57个计数,约0.79us;逻辑0的高电平时间取28个计数,约0.39us。这里给出一个最简单的核心配置:
#define ARR_VAL 89 #define HIGH_CODE 57 #define LOW_CODE 28 uint16_t pwmBuf[24]; // 存放一帧24bit的比较值 void ws2812_init(void) { // TIM1_CH1 -> PA8 配置成复用推挽 // PWM模式1,ARR = ARR_VAL,初始CCR = 0 } void ws2812_send_frame(uint8_t r, uint8_t g, uint8_t b) { for(int i = 0; i < 8; i++) { pwmBuf[i] = (g & (0x80 >> i)) ? HIGH_CODE : LOW_CODE; pwmBuf[i + 8] = (r & (0x80 >> i)) ? HIGH_CODE : LOW_CODE; pwmBuf[i + 16]= (b & (0x80 >> i)) ? HIGH_CODE : LOW_CODE; } // 把pwmBuf通过DMA搬运到TIM1->CCR1,连续产生24个PWM周期 // 发送完后拉低一段时间,产生RESET信号 }如果发现颜色和预期不符,先查GRB顺序和bit移位方向。很多人第一次写WS2812驱动,RGB顺序搞反,或者高位低位写反,灯亮起来就是五颜六色的乱码。另外,DMA搬运的数据宽度、TIM1的DMA请求通道,不同HAL库版本写法差异很大,但核心思想都是把比较寄存器CCR的值按顺序刷新。
这个方案的坑在于TIM1作为高级定时器,默认有主输出使能的要求,需要额外配置TIM_CtrlPWMOutputs或对应HAL接口,否则PWM不输出波形。新手最容易在这里卡住,表现为配了PWM但PA8一直低电平,量半天引脚才发现高级定时器还有个主输出开关没开。
5.3 延伸到编码器、按键矩阵、ADC
学会了PA8的复用,再看别的外设就触类旁通了。
做旋转编码器,可以用TIM3的CH1/CH2,也就是PA6/PA7,直接配置成编码器模式,硬件计数,不用自己轮询IO。但要注意PA6/PA7同时也是SPI1的MISO/MOSI默认脚,如果项目里还要用SPI读SD卡,就得错开。
做矩阵按键,一般用一组GPIO配置成上拉输入,另一组配置成推挽输出扫描。这个不涉及复用,但选脚时要避开那些被UART、I2C占了的引脚,否则按键扫描会干扰通信。比如PA15、PB3、PB4要先释放JTAG才能当GPIO用,很多人没做这一步,按键怎么扫都没反应。
做ADC采样,PA0~PA7加PB0、PB1就是C8T6能用的全部外部模拟通道。选ADC引脚时要留意PA0可能同时被WKUP功能占用,PA2/PA3同时是USART2的TX/RX,用了ADC就不能用串口2,二者得选一个。我的习惯是:给ADC单独留一组连续的引脚,比如PA0~PA3,然后其它功能全部避开这组脚,避免调模拟量的时候还要跟数字外设抢位置。
6. 管脚复用高频翻车现场与排查路径
6.1 症状对照速查表
这里列几个我实际遇到过的翻车场景,以及对应的排查思路。
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 串口只能发不能收 | RX脚被配成了复用推挽输出 | 把RX改成浮空输入或上拉输入 |
| 连不上ST-Link/J-Link | 把PA13/PA14配置成了普通GPIO | BOOT0拉高进ISP,串口全片擦除恢复 |
| PWM输出一直高或一直低 | 高级定时器没开主输出,或引脚模式不对 | 检查TIM_CtrlPWMOutputs,确认模式是复用推挽Output |
| I2C通信一卡就死 | SCL/SDA配置成推挽,或者没加外部上拉 | 配成复用开漏输出,外部上拉4.7k到3.3V |
| 按键按下没反应 | 用到PA15/PB3/PB4,但没有释放JTAG | 调用GPIO_PinRemapConfig关闭JTAG,保留SWD |
| WS2812颜色乱码 | GRB顺序写错、位序反了、DMA发送长度不对 | 逐项核对颜色字节、移位方向、缓冲长度 |
| BOOT0悬空,程序莫名不跑 | 启动脚电平不定 | 板子上加10k下拉到地 |
| 两个外设同时用同一组引脚 | 没有提前做引脚占用表 | 列外设和引脚矩阵,冲突二选一或重映射 |
6.2 我的排查工具与习惯
在实际联调的时候,我会先准备一个逻辑分析仪或示波器,哪怕是最便宜的几块钱逻辑分析仪,也比空想强得多。比如怀疑串口没数据,先用逻辑分析仪夹在TX脚上看波形;怀疑PWM没输出,直接量PA8的电平跳变。波形都没出来,就别急着查初始化代码,先确认引脚到底有没有被正确复用出去。
代码层面,我喜欢在初始化完成之后,先做一个临界GPIO测试:把一个没用到的引脚配置成普通推挽输出,500ms翻转一次,接上LED。这个灯只要能闪,就说明GPIO时钟、引脚模式、主循环都活着。之后再去查UART、I2C、PWM,问题范围一下就缩小了。
在板子设计层面,我现在的习惯很简单:第一,把数据手册的Table 4打印出来,贴在工作台正前方,所有复用配置以它为准,不凭记忆画图;第二,画原理图前先做引脚占用表,和CubeMX的Pinout View逐脚核对一遍再动手;第三,SWD口和至少一个串口永远预留,不管多缺引脚都不省。
踩过几次坑之后你会发现,F103C8T6的管脚复用其实是同一套逻辑规则:先查手册确认FT和复用映射,再画占用表避开冲突,最后在最接近硬件的位置用逻辑分析仪验证波形。只要把这条链路走顺,这颗芯片能陪你写完相当多的课程设计和实际项目。