GD32引脚重映射实战:USART1从部分映射到完全映射的完整配置指南
做GD32开发尤其是画过PCB的人几乎都逃不过引脚重映射Remap这一关。前阵子我帮朋友调一块GD32F103的驱动板板子画到一半才发现USART1的默认引脚PA2、PA3被一个模拟量采集芯片占用了串口要怎么接当时脑子里第一反应就是查重映射表把串口挪到备用引脚上。GD32的重映射功能和STM32一脉相承但细节上又有不少差异尤其是“部分映射”和“完全映射”这两档配置方式完全不同踩坑的姿势也完全不同。这篇就结合GD32标准外设库以USART1为例把从部分映射到完全映射的完整配置思路和实操代码拆开讲清楚也给从STM32转过来的朋友提个醒GD32的串口命名习惯、固件库接口都跟STM32对不上号直接套用容易翻车。1. 重映射机制到底是怎么回事1.1 一个必踩的引脚冲突现场芯片的引脚资源是固定的但外设数量又多厂商不可能给每个外设单独拉一组独占引脚所以同一个引脚往往承载着多个外设的复用功能。默认情况下芯片出厂时每个外设的信号会绑定在一组默认引脚上比如USART1默认走PA2发送、PA3接收。但实际项目里这组默认引脚很可能正好被别的功能占了。我见过最典型的场景有两种一种是画PCB时没仔细核对原理图板子投出去之后才发现某个引脚被重复分配了另一种是想要板子尺寸更小、走线更顺希望把某个外设的引脚放到芯片另一侧去。这时候重映射就是唯一的救命稻草——它允许你通过配置寄存器把外设的信号从默认引脚切换到备选引脚不需要改动PCB走线也不用换芯片。很多新手第一反应是“直接把GPIO模式改成复用功能不就行了”这是把GPIO复用和重映射搞混了。GPIO复用解决的是“这个引脚是普通IO还是外设信号”重映射解决的是“外设信号到底连到哪一组引脚”。两者是不同维度的配置必须同时做对。1.2 重映射的本质一个信号路由开关要理解重映射可以把它想象成一个老式电话交换机的跳线面板。外设比如USART1的发送信号就是一路电话线它默认插在PA2这个“插座”上重映射就是把电话线拔下来插到PD5或者PD8这些备用插座上。而控制这个“跳线面板”的就是芯片里的AFIOAlternate Function I/O模块。GD32F103的AFIO模块里有一个专门的寄存器来控制各路信号的走向固件库中对应的接口是gpio_pin_remap_config()。每次切换引脚本质上就是改写这个寄存器的某些位。这个寄存器里的很多位是组合字段比如某个外设的重映射占2个bit四种取值对应四种不同的引脚组合这就是“部分映射”和“完全映射”的底层来源。这里有个关键的坑AFIO模块本身也有独立的时钟使用任何重映射功能之前必须先使能AFIO时钟rcu_periph_clock_enable(RCU_AF)否则写入寄存器的配置完全不生效。很多人的重映射代码写了一大堆引脚却纹丝不动十有八九就是栽在这个地方。1.3 部分映射和完全映射差的不只是引脚数量简单来说部分映射只改变外设的一部分引脚位置完全映射则是把外设的全部信号引脚都挪到另一组。以GD32F103的USART1为例串口默认映射部分映射完全映射USART1PA2(TX)、PA3(RX)PD5(TX)、PD6(RX)PD8(TX)、PD9(RX)USART2PB10(TX)、PB11(RX)PC10(TX)、PC11(RX)PD8(TX)、PD9(RX)你看USART1从默认切到部分映射就把发送引脚从PA2换到PD5从部分映射再切到完全映射引脚又跑到PD8和PD9上去了。而USART2的完全映射目标也是PD8和PD9这意味着这两个串口如果都想用完全映射就会在引脚上打架——这种同组引脚的冲突在重映射设计里非常常见查表的时候必须看全。还有一个容易误会的点不是所有外设都同时支持部分映射和完全映射。比如USART0对应STM32的USART1就只有一档重映射从PA9/PA10换到PB6/PB7没有更进一步的完全映射。如果你拿USART0去找“完全映射”的宏定义多半会找不到这不是库有bug而是芯片本身就没设计这一档。2. 动手前先看懂映射表2.1 GD32F103串口映射速查表我把常用的串口映射整理成了一张表方便对照查阅。这张表是基于GD32F103系列的参考手册具体型号有差异的话一定以你手上的数据手册为准。外设默认 TX/RX部分映射 TX/RX完全映射 TX/RXUSART0PA9 / PA10PB6 / PB7无USART1PA2 / PA3PD5 / PD6PD8 / PD9USART2PB10 / PB11PC10 / PC11PD8 / PD9还有其他外设的常用重映射外设默认引脚重映射引脚说明SPI0PA5(SCK) PA6(MISO) PA7(MOSI)PB3(SCK) PB4(MISO) PB5(MOSI)PB3/PB4是JTAG引脚需先释放I2C0PB6(SCL) PB7(SDA)PB8(SCL) PB9(SDA)注意与USART0重映射冲突CAN0PA11(RX) PA12(TX)PB8(RX) PB9(TX)注意与I2C0重映射冲突TIMER0_CH0PA8部分映射PA0、完全映射PC6PWM输出切换常用2.2 为什么说“查表”比“记结论”重要很多人喜欢把重映射的引脚组合背下来比如“USART1完全映射是PD8/PD9”觉得够用了。但一旦换芯片型号比如从GD32F103换到GD32F303映射表很可能就变了甚至同一个系列里不同封装的引脚可用性也不一样。背结论是最容易翻车的做法。我自己的习惯是每个项目开工前先把会用到的外设对应的重映射表截个图放到项目文档里。画原理图的时候一边分配引脚一边对照表格看目标引脚有没有被其他外设占用有没有跟调试引脚冲突有没有跟相邻引脚的复用功能互相干扰。这个习惯帮我挡住了好几次在画板阶段就会踩的雷。另外要注意标准外设库里的gpio_pin_remap_config()函数参数不同库版本可能名字不一样。有的版本叫GPIO_USART1_REMAP有的叫GPIO_USART1_PARTIAL_REMAP本质都是指同一个bit位。完全映射通常叫GPIO_USART1_FULL_REMAP。写代码时先查一下当前库的头文件里到底定义了哪些宏不要凭记忆硬写。2.3 映射引脚冲突问题不得不防查表的时候重点要检查三类冲突第一类是同组引脚冲突。比如USART1完全映射和USART2完全映射都指向PD8/PD9这两个串口不能同时用完全映射只能二选一。设计时如果两个串口都要用就得一个用部分映射一个用完全映射或者一个保持默认映射。第二类是与调试引脚冲突。GD32F103的JTAG/SWD调试功能占用了PA15、PB3、PB4等引脚。如果重映射把外设信号引到这些引脚上调试器可能就无法工作了需要在配置GPIO之前先释放JTAG功能。后面我会专门讲这个操作。第三类是与其他外设的默认复用冲突。比如I2C0重映射到PB8/PB9但CAN0重映射也指向PB8/PB9如果两个功能同时使用就要有一个让路。3. 从默认到部分映射USART1实战3.1 环境准备与工程基础为了保证下面的代码能直接跑起来先说下环境我使用的是GD32F103系列芯片开发环境用的是Keil MDK配合GigaDevice官方提供的标准外设库Firmware Library。在Keil里用GD32需要先装对应的Device Pack或者直接使用GigaDevice官方的GD32 Embedded Builder那个IDE对新手更友好一些模板工程和驱动的整合度也更高。新建工程之后外设库的文件夹结构一般是Firmware核心库、Peripheral外设驱动、User用户代码这几块。你需要确认工程里已经包含了gd32f10x_gpio.c、gd32f10x_rcu.c、gd32f10x_usart.c这几个源文件以及对应的头文件路径。3.2 串口重映射配置的三个必做动作配置部分映射的核心就那么几步但每一步都不能省使能AFIO时钟RCU_AF这是重映射功能生效的前提。调用gpio_pin_remap_config()设置部分映射。初始化目标引脚的GPIO模式并配置串口本身。其中第2步和第3步的顺序有讲究我习惯先做重映射再配置GPIO。虽然有些情况下顺序反过来也能跑但先设置好信号路由再初始化引脚逻辑上更清晰排查问题时也更容易定位。3.3 完整代码示例USART1部分映射到PD5/PD6下面这段代码是我实际项目中简化出来的功能是把USART1从默认的PA2/PA3切换到部分映射引脚PD5/PD6#include gd32f10x.h #include gd32f10x_gpio.h #include gd32f10x_rcu.h #include gd32f10x_usart.h void usart1_partial_remap_init(void) { /* 1. 使能相关时钟 */ rcu_periph_clock_enable(RCU_AF); // AFIO时钟必须开 rcu_periph_clock_enable(RCU_USART1); // 串口时钟 rcu_periph_clock_enable(RCU_GPIOD); // PD端口时钟 /* 2. 配置USART1部分重映射PD5/PD6 */ gpio_pin_remap_config(GPIO_USART1_REMAP, ENABLE); /* 3. 配置GPIO引脚模式 */ gpio_init(GPIOD, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5); // TX gpio_init(GPIOD, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6); // RX /* 4. 配置串口参数 */ usart_baudrate_set(USART1, 115200); usart_parity_config(USART1, USART_PM_NONE); usart_word_length_set(USART1, USART_WL_8BIT); usart_stop_bit_set(USART1, USART_STB_1BIT); usart_transmit_config(USART1, USART_TRANSMIT_ENABLE); usart_receive_config(USART1, USART_RECEIVE_ENABLE); usart_enable(USART1); }逐行解释一下关键点rcu_periph_clock_enable(RCU_AF)对应之前说的AFIO时钟使能没有这一步下面的重映射配置就是白写。gpio_pin_remap_config()的第一个参数GD32库里USART1部分映射的宏名有可能是GPIO_USART1_REMAP也有可能是GPIO_USART1_PARTIAL_REMAP取决于库版本你可以在gd32f10x_gpio.h里搜“USART1”确认。GPIO模式配置那块要特别注意引脚方向。TX是发送端必须配置为复用推挽GPIO_MODE_AF_PP这样才能由串口外设驱动引脚RX是接收端配置为浮空输入GPIO_MODE_IN_FLOATING即可。如果TX配错了模式引脚输出电平不稳定对方收到的数据就是乱码。3.4 发送数据的验证代码配置好之后怎么确认真的通了最简单的方式是写一个字符串发送函数然后用USB转TTL模块直接看输出void usart1_send_string(char *str) { while (*str ! \0) { while (RESET usart_flag_get(USART1, USART_FLAG_TBE)); usart_data_transmit(USART1, *str); } } int main(void) { usart1_partial_remap_init(); while (1) { usart1_send_string(PD5/PD6 remap OK\r\n); delay_1ms(1000); } }把USB转TTL模块的RX接到PD5TX接到PD6共地打开串口助手看到PD5/PD6 remap OK字符串就表示部分映射配置成功。这里有个小细节串口助手的波特率要和代码里设置的一致我习惯用115200避免在不必要的地方引入变量。3.5 一个容易被忽略的问题接收引脚要不要上拉浮空输入在接收串口数据时如果对方芯片没有驱动信号引脚电平是悬空的容易受到外界干扰。很多开发板在设计时会给RX引脚加上拉电阻但最小系统板上不一定有。如果出现“不插线的时候串口疯狂进中断”这种问题多半是RX引脚悬空导致电平抖动。解决办法有两种硬件上加一个10k左右的上拉电阻或者在代码里把RX引脚配置为GPIO_MODE_IPU上拉输入。我一般优先用代码解决省事但要注意上拉会不会改变正常通信时的电平逻辑对于标准UART来说上拉不会影响可以放心用。4. 再从部分映射推到完全映射4.1 完全映射到底改了什么部分映射已经让串口在PD5/PD6上工作了为什么还需要完全映射这往往是牵一发而动全身的连锁反应PD5/PD6这两个引脚可能同时是另一个外设的复用功能引脚。比如在某些板子上PD5/PD6被接到了LCD的数据线上这时候就需要把串口再挪到完全映射的PD8/PD9上。从部分映射切换到完全映射不只是改一个宏那么简单。USART1完全映射的宏是GPIO_USART1_FULL_REMAP对应的引脚是PD8(TX)、PD9(RX)。如果固件库的gpio_pin_remap_config实现是直接改写整个AFIO重映射寄存器的值那么当你执行GPIO_USART1_FULL_REMAP, ENABLE时之前的部分映射位会被自动清掉不会叠加。但如果你用的库实现是“读-改-写”逻辑就可能出现部分映射和完全映射同时被置位的情况这时行为就是不确定的。所以稳妥的做法是先显式关闭部分映射再开启完全映射不要直接把参数从一个宏换成另一个宏就完事。4.2 配置时的两个坑第一个坑是旧配置残留。部分映射位和完全映射位在寄存器里是两个不同的bit位。直接调用完全映射部分映射位可能仍然为1两个配置打架串口就是不出数据。清理方式gpio_pin_remap_config(GPIO_USART1_REMAP, DISABLE); // 先关部分映射 gpio_pin_remap_config(GPIO_USART1_FULL_REMAP, ENABLE); // 再开完全映射第二个坑是GPIO时钟和引脚选择。完全映射用的引脚是PD8/PD9如果之前只使能了GPIOD时钟那还好PD8/PD9也在GPIOD上不用额外开时钟。但如果某个外设的默认引脚和完全映射引脚分属不同端口就得注意把新端口时钟也打开。这里最容易漏的是换了映射引脚之后GPIO初始化里还是旧引脚的宏比如GPIO_PIN_5没改成GPIO_PIN_8。4.3 完整配置代码完全映射到PD8/PD9void usart1_full_remap_init(void) { /* 1. 时钟 */ rcu_periph_clock_enable(RCU_AF); rcu_periph_clock_enable(RCU_USART1); rcu_periph_clock_enable(RCU_GPIOD); /* 2. 先关部分映射再开完全映射 */ gpio_pin_remap_config(GPIO_USART1_REMAP, DISABLE); gpio_pin_remap_config(GPIO_USART1_FULL_REMAP, ENABLE); /* 3. GPIO初始化 */ gpio_init(GPIOD, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8); // TX gpio_init(GPIOD, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_9); // RX /* 4. 串口参数 */ usart_baudrate_set(USART1, 115200); usart_parity_config(USART1, USART_PM_NONE); usart_word_length_set(USART1, USART_WL_8BIT); usart_stop_bit_set(USART1, USART_STB_1BIT); usart_transmit_config(USART1, USART_TRANSMIT_ENABLE); usart_receive_config(USART1, USART_RECEIVE_ENABLE); usart_enable(USART1); }注意如果你之前跑过部分映射的代码现在下载这个完全映射版本看到串口助手没输出第一反应不应该是“代码错了”而是先检查模块接线USB转TTL的RX要接到PD8TX要接到PD9而不是接在PD5/PD6上。这种低级问题我调试时犯过不止一次。4.4 验证方式自发自收没有第二块板子也没有USB转TTL模块时可以把PD8和PD9直接短接然后代码里先发一个字节再接收uint16_t data; usart_data_transmit(USART1, A); while (RESET usart_flag_get(USART1, USART_FLAG_RBNE)); data usart_data_receive(USART1); if (data A) { usart1_send_string(loopback ok\r\n); }如果短接之后收到了自己发出去的字节说明PD8/PD9的收发通路没问题重映射配置基本正确。这个方法同样可以用来验证部分映射的PD5/PD6。5. 重映射进阶定时器、CAN、SWD引脚释放5.1 定时器PWM输出切换通道串口不是唯一需要重映射的外设定时器的PWM输出在实际项目里用得也很多。GD32F103的TIMER0有4个通道默认引脚在PA8到PA11上重映射之后可以搬到PA0到PA3或者PC6到PC9。以TIMER0_CH0输出PWM为例默认引脚是PA8。如果PA8被别的功能占了我通常会切到部分映射的PA0rcu_periph_clock_enable(RCU_AF); gpio_pin_remap_config(GPIO_TIMER0_PARTIAL_REMAP, ENABLE); rcu_periph_clock_enable(RCU_GPIOA); gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0);这里有个特别容易忽略的坑PA0同时也是TIMER2_CH0的默认引脚。如果TIMER2也开了两个定时器的信号都往PA0上送GPIO复用关系就乱了。所以做定时器重映射之前一定要把整个芯片上使用了相同引脚的外设列出来逐个核对。5.2 CAN总线引脚重映射CAN总线在工业控制项目里很常见。GD32F103的CAN0默认引脚是PA11(RX)、PA12(TX)重映射后切到PB8(RX)、PB9(TX)。rcu_periph_clock_enable(RCU_AF); rcu_periph_clock_enable(RCU_GPIOB); gpio_pin_remap_config(GPIO_CAN0_REMAP, ENABLE); gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); // TX gpio_init(GPIOB, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_8); // RX注意CAN的RX引脚我习惯配置成上拉输入因为CAN总线本身是差分信号RX引脚如果浮空在总线空闲时容易受到干扰。这个经验同样可以推广到其他外设的接收引脚上。另外CAN0重映射到PB8/PB9后会和I2C0重映射到PB8/PB9产生冲突两者不能同时使用。所以在用到一个重映射功能时最好把整张映射表过一遍。5.3 释放SWD/JTAG引脚做普通GPIO这个主题在STM32圈子里是老生常谈到了GD32上问题依然存在。芯片复位后PA13/PA14/PA15、PB3/PB4默认是SWD和JTAG调试引脚。其中PA13/PA14是SWD的SWDIO/SWCLKPA15是JTDIPB3是JTDOPB4是NJTRST。如果重映射功能想让某个外设占用的PB3/PB4变成普通IO需要先释放JTAG功能同时保留SWDgpio_pin_remap_config(GPIO_SWD_JTAG_REMAP, ENABLE);这个宏的含义是关闭JTAG-DP保留SW-DP。执行之后PA15、PB3、PB4都变成普通GPIO但PA13/PA14仍然可以接SWD调试器。对于大多数项目来说SWD两线调试已经够用JTAG用不太到所以这个配置很划算。如果你连SWD也不想保留想把PA13/PA14也用来做GPIO可以配置GPIO_SWD_REMAP。但我要强烈建议你谨慎使用这个选项因为一旦释放了SWD引脚调试器就再也连不上芯片了程序里如果没留串口ISP烧录入口就只能用串口ISP方式擦除整个Flash才能恢复。这种恢复操作很费时间而且要额外的硬件支持。在配置GPIO_SWD_JTAG_REMAP之后如果外设信号本身就映射到了PA15/PB3/PB4GPIO初始化代码里直接配置这些引脚即可不需要再做额外的“复用使能”操作。因为重映射寄存器已经把信号路由过去了GPIO的模式配置才是最后一步。6. 常见问题与排查技巧实录6.1 症状速查表把这几年调试GD32重映射过程中遇到的典型死法整理成了下面的表先对照自查能省很多时间症状可能原因排查方法解决方式引脚没有任何电平变化AFIO时钟未使能检查RCU_AF是否开启加上rcu_periph_clock_enable(RCU_AF)引脚有电平但数据乱码TX引脚GPIO模式错误确认GPIO_MODE_AF_PP把TX改成复用推挽切换映射后还是旧引脚输出部分映射/完全映射残留冲突检查寄存器状态先DISABLE旧映射再ENABLE新映射重映射后调试器连不上SWD引脚被释放或占用确认GPIO_SWD_REMAP未开启保留GPIO_SWD_JTAG_REMAP级别能发不能收RX引脚模式错误或接线错误检查RX配置和接线配置为浮空/上拉输入核对TX/RX交叉串口间歇性乱码波特率误差大或时钟源不对检查系统时钟配置调整系统时钟或降低波特率至96006.2 几个典型排查案例案例一重映射功能配置了但引脚就是没反应。排查过程是先看AFIO时钟再看重映射库函数的宏定义再看GPIO时钟最后发现是使能了GPIOD时钟但忘了调用gpio_init()引脚保持在默认的模拟输入状态。GPIO模块不像串口外设不初始化就直接用引脚根本不会输出。这种“低级”错误其实比复杂错误更常见。案例二部分映射和完全映射来回切换之后串口完全罢工。通过读取AFIO重映射寄存器的值发现部分映射位和完全映射位同时为1。库函数在某个版本里确实是“读-改-写”方式所以切换前必须手动清掉旧配置。后来我把“先关旧、再开新”写成了一个标准的配置流程每次切换重映射都严格执行。案例三重映射SPI0到PB3/PB4/PB5后程序能跑但调试器无法下载。就是因为PB3/PB4属于JTAG引脚GPIO初始化之前没有释放JTAG功能。当时处理方式是按住复位键点下载等进度条出现再松开勉强把恢复程序烧进去然后用GPIO_SWD_JTAG_REMAP重新释放。折腾了大半天后面我只要看到重映射目标引脚包含PB3/PB4第一件事就是先写释放代码。6.3 我的个人建议这几年做GD32项目我对重映射最大的体会是它能救急但更应该预防。画原理图之前就把重映射表摊开把所有用到的外设引脚列一个Excel表格标出默认引脚、部分映射、完全映射三个选项再标注哪些引脚被调试器占用、哪些与其他外设冲突。分配引脚时优先用默认映射实在不行再往重映射方向想。另外代码层面最好封装一个统一的串口初始化函数把重映射配置、GPIO模式和串口参数全部收进去参数改为“选择映射方式”。这样后续换映射引脚只需要改一个枚举值不用到处找哪里改了GPIO、哪里改了重映射。这个习惯帮我减少了很多低级错误。还有就是不要迷信“完全映射就一定更好”。完全映射往往意味着引脚在芯片的另一侧走线长、过孔多信号完整性未必比部分映射好。我见过一个项目为了追求“完全映射”的整齐布局结果PD8/PD9旁边就是大电流走线串口通信在高负载时频繁出错最后改回部分映射反而好了。选映射方式不是选配置最全的而是选引脚最合适、走线最合理的。