拓冰建站拓冰建站
首页 / 资讯中心 / 正文

GD32F103C8T6标准库RS485通讯实战:方向切换与TC标志避坑指南

简介基于GD32F103C8T6单片机的RS485通讯标准库工程代码面向嵌入式开发者和入门学习者解决GD32平台上485总线通信的底层配置与数据收发问题。压缩包共73个文件、约326KB以31个h头文件与27个c源文件为主配合uvprojx/uvoptx等MDK工程配置、启动文件及调试文件构成可直接编译运行的完整标准库例程。代码基于GD32F10x标准外设库包含系统时钟、串口、中断与滴答定时器等关键模块注释清晰、结构规整便于按需裁剪并快速迁移到其他GD32项目中内置实验说明文档可辅助理解硬件连接与调试流程。资源已有268人学习适合需要借助标准库快速实现RS485组网通信或入门GD32开发的读者使用。 手头项目需要把一套原来的STM32F103C8T6驱动板换成GD32F103C8T6板载的串口通讯计划走RS485总线。习惯用标准库的我最初的想法很简单把工程里的芯片型号改一下代码平移过来不就完事了吗结果接上485网络一测试上位机根本收不到从站数据从站这边连接收中断都进不去。查了两天才发现问题不在GD32难用而在我对485通讯里“方向切换”和“发送完成标志”这两个细节理解不够透。这篇文章就围绕GD32F103C8T6标准库实现485通讯把硬件电路设计、标准库初始化、收发逻辑、实测中遇到的坑一步步讲清楚。最后还会给出一套可以直接跑的代码框架并延伸到Modbus RTU的对接思路。如果你正在做GD32或STM32的485通讯项目这篇内容应该能帮你省下不少排查时间。1. GD32F103C8T6与标准库能平移什么必须改什么1.1 和STM32F103C8T6的兼容边界GD32F103C8T6这颗芯片引脚和STM32F103C8T6是基本对应的内核同样是Cortex-M3官方标称主频可以到108MHz按STM32的习惯跑72MHz完全没问题。Flash是64KBSRAM是20KB这些基础参数和F103C8几乎一致。所以很多外设寄存器的地址、位定义都能对上直接把STM32标准库工程改芯片型号确实能编译过。但有两处差异一定要心里有数。第一Flash Page大小不一样STM32F103C8是1KB一页GD32F103C8是2KB一页。如果只做普通程序下载这个问题不体现凡是涉及到IAP升级、内部Flash擦写擦除扇区和页的代码都得按GD32的规格重写。第二串口的命名方式不同。STM32标准库里PA9/PA10对应的串口叫USART1GD32新版固件库里同样一组引脚通常叫USART0中断号也对应变成USART0_IRQn。这个坑不提前说明照抄例程很容易报错。1.2 用标准库的前提工程文件和时钟宏很多GD32F103的例程直接沿用STM32标准外设库那套API比如GPIO_InitTypeDef、USART_InitTypeDef这套结构体初始化方式。我推荐你也用这套因为网上能找到的485、Modbus例程大量都是这种风格迁移成本最低。如果你的工程用的是新版GD32库函数名变成了gpio_init、usart_init这种风格也没关系逻辑完全一样按头文件里的接口替换即可。新建工程时有个容易忽略的步骤必须给Keil装GD32F10x的Device Pack否则MDK不认识这颗芯片。然后要把标准外设库里的gpio、usart、rcc、misc这几个源文件添加进工程。最后重点检查system_gd32f10x.c里的外部晶振频率宏常见板子有两种设计8MHz晶振和25MHz晶振宏定义写错了后面所有串口波特率都会偏。2. 485通讯硬件设计方向脚、终端电阻和偏置电阻2.1 RS485总线上的A/B差分与空闲电平RS485是半双工总线靠A、B两根线之间的电压差来传数据。A比B高逻辑上表示1A比B低表示0。之所以用差分方式而不是单端对地是为了抗共模干扰工业现场线缆长、干扰大单端信号很容易被拉偏差分信号只要A/B两根线受到的干扰大致相同压差就不会变。这里要特别注意“空闲电平”这个概念。总线上没有设备发送时A和B之间不能悬空否则接收端输入状态不定会收到乱码甚至持续触发中断。所以工程上会给A线接上拉电阻到VCC给B线接下拉电阻到GND保证空闲时A比B高总线处于确定的高电平状态。2.2 收发芯片的典型接法485收发芯片以SP485、MAX485最为常见引脚逻辑也基本统一。RO是接收输出接MCU的RX引脚DI是发送输入接MCU的TX引脚RE是接收使能低电平有效DE是发送使能高电平有效。因为485是半双工同一时刻只能收或只能发所以RE和DE通常直接短接用一个GPIO控制引脚输出高电平芯片进入发送模式输出低电平芯片进入接收模式。这个方向控制脚就是485通讯的精髓可以类比成对讲机上的PTT按键按住说话、松开听话。很多485通讯调不通不是因为串口配置错而是方向切换的时机不对导致发送时收不到、接收时发不出。2.3 终端电阻和偏置电阻的作用总线两端要各接一个120欧终端电阻作用是匹配传输线阻抗防止信号在末端反射导致波形畸变。这里要强调只有总线物理最远端的两台设备才需要接中间设备再接就相当于给总线并了一个小电阻拉低差分阻抗反而会让信号变差。如果板上已经默认焊了120欧挂在总线中间时记得把电阻拆掉。偏置电阻可以复用A上拉B下拉的思路阻值选4.7K到10K之间都行。有些485模块上设计的是510欧加1K的搭配效果更好但静态功耗略大。调试时如果发现总线空闲时RO引脚输出不稳定优先检查这两个偏置电阻有没有焊错位置。3. GD32标准库初始化串口和方向脚的完整配置3.1 先确认系统时钟再改波特率很多人初始化串口时只关注波特率寄存器忽略了系统时钟源。GD32F103的USART波特率是从APB总线时钟分频出来的如果系统时钟不是预期值波特率必然不对。最典型的场景程序里默认8MHz外部晶振、PLL倍频9倍得到72MHz但实际板子上焊的是25MHz晶振PLL出来的频率就完全乱了现象是串口乱码或者程序跑飞。所以初始化串口之前先用调试器读一下RCC相关寄存器或者直接看SystemCoreClock这个全局变量的值。确保它是72000000再往下配波特率才有意义。3.2 初始化代码GPIO、USART、NVIC一次配齐下面这套初始化代码我用的是STM32标准库风格的APIGD32老版兼容库可以直接编译新版库如果函数名不同对照头文件替换即可。485方向控制脚我用PB0PA9和PA10给串口用。#define RS485_DE_PORT GPIOB #define RS485_DE_PIN GPIO_Pin_0 #define RS485_DE_HIGH() GPIO_SetBits(RS485_DE_PORT, RS485_DE_PIN) #define RS485_DE_LOW() GPIO_ResetBits(RS485_DE_PORT, RS485_DE_PIN) void RS485_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* PA9/PA10属于GPIOAUSART1挂在APB2上PB0属于GPIOB */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); /* 方向控制脚先配成推挽输出并且立刻拉低保证默认处于接收状态 */ GPIO_InitStructure.GPIO_Pin RS485_DE_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(RS485_DE_PORT, GPIO_InitStructure); RS485_DE_LOW(); /* PA9 复用为USART1_TX */ 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); /* PA10 复用为USART1_RX */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); 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); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }注意我把控制脚拉低放在串口使能之前这个顺序很重要。如果初始化顺序反过来上电瞬间到初始化完成这一段时间里DE可能处于高电平485芯片一直占着总线发送整个网络上的其他设备都没法通信。4. 收发逻辑实现方向切换、完成标志与帧边界4.1 发送函数为什么必须等TCvoid RS485_SendBytes(uint8_t *buf, uint16_t len) { uint16_t i; RS485_DE_HIGH(); /* 切到发送模式 */ for (i 0; i len; i) { USART_SendData(USART1, buf[i]); /* 等待TC而不是等TXE */ while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); } RS485_DE_LOW(); /* 全部发完回到接收模式 */ }这里最核心的就是等待USART_FLAG_TC。TXE标志表示数据寄存器空了但数据可能还在移位寄存器里没发完。如果看到TXE就立刻拉低DE最后一个字节的物理发送过程会被硬生生截断对方收到的帧就会缺尾巴。TC标志表示整个移位寄存器完全发送完毕。不管循环里发了几字节只要每一次都等到TC置位再进入下一轮最后拉低DE时总线上的信号一定发送完整了。4.2 接收中断与环形缓冲区485的接收端我习惯用“串口接收中断 环形缓冲区”的方式不丢字节也不容易造成中断处理函数过于臃肿。定义一组全局缓冲区#define RX_BUF_SIZE 128 volatile uint8_t rxBuf[RX_BUF_SIZE]; volatile uint16_t rxHead 0; volatile uint16_t rxTail 0;环形缓冲区的指针只在中断里写head主循环里读tail指针回绕取模即可。中断服务函数如下void USART1_IRQHandler(void) { uint8_t data; uint16_t next; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART1); next (rxHead 1) % RX_BUF_SIZE; if (next ! rxTail) { rxBuf[rxHead] data; rxHead next; } /* 缓冲区满时丢弃新数据保留未读数据 */ } }在中断里只做“存字节”这一件事不要在里面处理协议、不要直接回显、更不要在里面调用发送函数。中断处理得越短主循环的负担越小也越不容易丢字节。4.3 怎么判断一帧数据接收完毕RS485本身没有“帧”的概念它只是把字节流从A/B线传过去。应用层什么时候认为一帧收完了需要自己定义规则。最简单的做法是利用“空闲超时”定时器每隔1ms查一次如果连续几毫秒没有新的字节进来就认为上一帧结束了。volatile uint8_t frameReady 0; static uint16_t idleCnt 0; // 假设在主循环或基础定时器里每1ms调用一次 void RS485_IdleCheck(void) { if (rxHead ! lastRxHead) { lastRxHead rxHead; idleCnt 0; } else if (idleCnt 4) { idleCnt 0; frameReady 1; /* 连续4ms没有新数据判定一帧完成 */ } }这个超时时间要根据波特率调整。115200波特率下一个字符约87微秒Modbus RTU要求的帧间隔判断大概是3.5个字符时间约0.3毫秒4ms已经足够。如果波特率降到96003.5字符时间约4毫秒这里的阈值就要相应放大不然会把一帧数据硬拆成两帧。5. 实测排障那些让485通讯翻车的细节5.1 故障一上电后第一帧就丢了第一次整机联调时现象很诡异程序复位后马上发一帧数据上位机收不到但按一下复位键再发又能收到。用示波器量PB0的波形才发现上电瞬间PB0一直维持高电平直到初始化代码执行到拉低之后才变低。也就是说在程序真正配置GPIO之前485芯片一直处于发送状态持续驱动总线。这个问题的本质是GPIO在上电复位瞬间的输出状态不可控。解决办法有两个方向软件上GPIO初始化后第一时间把控制脚拉低不要等到串口都配置完才处理硬件上在DE/RE短接节点对地加一个10K下拉电阻确保芯片在上电到程序接管之前始终处于接收状态。我后来在批量板卡上都加了这颗下拉电阻复位期间总线再也没被锁死过。5.2 故障二自发自收导致数据错乱调试时为了图方便把A/B线短接让板子自发自收。结果上位机发过去一串字符回来的内容错乱而且接收中断被疯狂触发。原因是485收发芯片在工作时RO引脚会把发送的数据同步回MCU的RX引脚半双工模式下这叫“自发自收”完全是正常物理行为。如果你不希望主控收到自己发出去的数据可以考虑用带“回环抑制”逻辑的协议层处理或者在初始化时关闭回显相关的处理逻辑。实际项目里更常见的是主机发指令从机收到后回复从机自己也会在RO引脚看到主机发的指令所以协议设计上要用地址过滤从机只在地址匹配时才响应不要一收到任何数据就回帧。5.3 故障三响应帧总是缺最后一个字节这个坑我印象最深因为现象最隐蔽。从机的响应帧长度不定短帧能正常收到长帧总是缺最后一两个字节。后来用示波器抓485芯片DE引脚和A/B差分波形发现DE引脚在最后一个字节还在移位发送时就已经拉低了波形被硬生生截断。问题就出在初版发送函数里把“等待TXE”当成了“发送完成”或者是写完所有字节后只delay了10微秒就切回接收模式。10微秒在9600波特率下连一个字节的一半都发不完数据当然会丢尾巴。改成每字节等待TC之后问题立刻消失这个教训让我养成了习惯凡是半双工通讯发送完成的判定一定以TC为准。5.4 常见问题对照表现象可能原因解决办法上电后第一帧丢失DE引脚复位期间为高电平GPIO初始化后立刻拉低硬件加10K下拉数据完全不通A/B两根线接反确认A对A、B对B不要交叉长帧缺最后一个字节等TXE就拉低DE方向脚改为等待TC标志置位乱码系统时钟或晶振宏配置错误检查SystemCoreClock是否为72MHz偶尔收到错数据缺少偏置电阻或终端电阻位置不对A线上拉、B线下拉总线两端焊120欧接收中断风暴总线空闲电平不稳定检查偏置网络确保空闲时A比B高6. 落地延伸把485驱动接到Modbus RTU协议上6.1 Modbus RTU帧结构与CRC16485驱动跑通之后最常用的应用层协议就是Modbus RTU。一帧标准的Modbus RTU报文格式是从站地址1字节、功能码1字节、数据N字节、CRC16校验2字节。CRC16在数据链路层承担错误检测算错就直接丢弃整帧。CRC16的完整查表算法篇幅比较长初学者直接用逐位算法代码短且容易理解uint16_t ModbusCRC16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i, j; for (i 0; i len; i) { crc ^ buf[i]; for (j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }Modbus RTU规定CRC发送时低字节在前组帧时记得把crc的低8位放在前、高8位放在后。6.2 从站端的功能码03响应示例基于前面的接收缓冲区和frameReady标志主循环里可以这样轮询void Modbus_Poll(void) { uint8_t frame[64]; uint16_t len, crc, i; if (!frameReady) return; frameReady 0; len 0; while (rxTail ! rxHead len sizeof(frame)) { frame[len] rxBuf[rxTail]; rxTail (rxTail 1) % RX_BUF_SIZE; } if (len 8) return; /* 最短报文也要8字节 */ crc ModbusCRC16(frame, len - 2); if (crc ! (((uint16_t)frame[len - 1] 8) | frame[len - 2])) return; /* CRC不对丢弃 */ if (frame[0] ! 0x01) return; /* 地址不匹配 */ if (frame[1] 0x03) /* 读保持寄存器 */ { uint16_t start ((uint16_t)frame[2] 8) | frame[3]; uint16_t num ((uint16_t)frame[4] 8) | frame[5]; uint8_t resp[32]; uint16_t respLen 0; resp[respLen] 0x01; /* 从站地址 */ resp[respLen] 0x03; /* 功能码 */ resp[respLen] (uint8_t)(num * 2); /* 数据字节数 */ for (i 0; i num; i) { uint16_t val holdingReg[start i]; /* 从寄存器表取值 */ resp[respLen] (uint8_t)(val 8); resp[respLen] (uint8_t)(val 0xFF); } crc ModbusCRC16(resp, respLen); resp[respLen] (uint8_t)(crc 0xFF); resp[respLen] (uint8_t)(crc 8); RS485_SendBytes(resp, respLen); } }这套结构基本就是一个可用的Modbus从站雏形。要扩展功能码04、06、16都是同样的套路解析地址、解析功能码、拼装响应、计算CRC、经485接口发送。寄存器表就是一块全局数组按协议映射到位号、温度、状态这些实际变量上即可。6.3 调试建议排查485通讯问题建议按这个顺序推进先把MCU的TX和RX直接短接做串口自发自收测试确认串口本身工作正常再通过485芯片的A/B线连接两个节点测试半双工通讯最后再往上挂Modbus协议。这样一层一层隔离出现问题能快速定位到硬件还是软件。从我个人经验看485这类半双工总线硬件设计里最容易翻车的是DE方向脚时序软件里最容易翻车的是发送完成标志选择。拿示波器同时抓DE引脚和A/B差分波形几乎能看出所有时序类问题。我在几个量产项目里都是这套代码框架GD32标准库加中断接收、TC判定发送完成、Modbus RTU做应用层只要方向脚上电默认拉低总线空闲电平正确跑起来非常稳定。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门