简介:面向STM32初学者与嵌入式入门者的RS485通信实践文档,围绕STM32F103微控制器的UART接口展开,讲解半双工总线中DE数据使能线的收发切换、波特率配置与GPIO控制思路。内容以土壤温湿度传感器为实际对象,给出RS485_Init、RS485_Send_Data、RS485_Receive_Data等函数的完整代码,并说明主函数中发送读取指令、延时等待、接收响应、解析数据并在TFTLCD屏显示温湿度的流程,同时涉及中断优先级设置、接收缓冲区与宏定义等工程细节。压缩包共1个docx文件,约1.22MB,为图文与代码混合的笔记式整理,便于边看边对照移植。已有2629人学习,适合想打通STM32串口外设与工业现场总线连接、积累驱动编写与排错经验的读者参考。
1. 一块 F103 加一根两芯线:这套采集链路到底解决什么问题
手头一块 STM32F103ZET6 开发板、一个四线制土壤温湿度探头,探头引出 VCC/GND 和 A/B 两根差分线,中间夹一颗 485 收发器——这套组合在大棚环境采集、机柜温湿度监测里出镜率极高,也是大多数人第一次把 stm32 rs485 的 485通讯方式原理真正跑通的场景。它不涉及点灯和串口打印这种玩具级实验,而是把「USART 外设 + 半双工方向控制 + 一帧查询报文」三件事焊成一条完整链路:MCU 发 8 个字节问探头要数据,探头回 9 个字节,MCU 拆出温湿度显示到 TFTLCD 上。文档里给出的main.c、rs485.c、rs485.h就是这条链路的最小可跑版本,硬件上只需要三根信号线加电源。它适合手里已经有 F103 板子和 485 传感器、卡在「发得出去收不回来」或者「收到了但算出来的数不对」这两类人,也适合第一次接触 Modbus-RTU 帧格式、想找个能对照的参考实现的人。
2. 从 USART2 到 A/B 差分线:硬件层先把电气关系捋顺
先讲清楚一个前提:STM32 的 USART 外设本身只会说 TTL 电平的单端信号,它不知道什么叫 RS485。真正把 0/1 变成 A/B 两根线上的压差,是外面那颗收发器干的活。这一步理不顺,后面代码写得再漂亮也是空转。
2.1 收发器做了什么:TTL 电平与差分总线的翻译官
收发器内部就两个方向:DI 引脚收 MCU 的 TX 信号,转成 A/B 之间的差分电压推上总线;总线上的差分电压经过比较器还原成 0/1,从 RO 引脚送回 MCU 的 RX。RS485 规定 A-B 压差大于 +200mV 判为逻辑 1,小于 -200mV 判为逻辑 0,中间是死区。这个压差判据就是它抗干扰能力的来源——总线上叠加的共模噪声会同时抬升 A 和 B,差值不变,判决结果就不变。标准器件的共模输入范围通常在 -7V 到 +12V 之间,所以两端设备地电位差个几伏还能正常通信,这是 RS232 做不到的。
代价是半双工。同一时刻总线上只能有一个设备在推电流,其余设备必须把自己的输出级置成高阻,否则两个设备同时驱动 A/B 就是硬碰硬的短路。收发器上为此有两个控制脚:DE(Driver Enable,高有效)和 /RE(Receiver Enable,低有效)。绝大多数电路把这两个脚短接在一起,用一个 GPIO 控制,高电平=发送、低电平=接收。这套设计在代码里的体现就是那个RS485_TX_EN宏,它不是一个普通的状态变量,而是直接决定总线归属权的开关。
提示:选型上,3.3V 供电的收发器要和 3.3V 的 F103 直连;如果用的是 5V 器件,RO 输出给 MCU 的 RX 是高电平 5V,虽然 F103 多数引脚标称 5V 容忍,但 DE/RE 的控制电平要单独确认阈值,别想当然。
2.2 引脚与接线:PA2/PA3/PG3 三个脚位不能错
F103ZET6 的 USART2 固定挂在 PA2(TX)和 PA3(RX)上,这是复用功能,不是随便重映射的。文档里的初始化代码把 PA2 配成复用推挽、PA3 配成浮空输入,方向控制脚用的是 GPIOG 的 Pin3。把这张表抄下来贴在工作台前面,接线时逐个对:
| 信号 | STM32 引脚 | GPIO 模式 | 接到收发器 | 备注 |
|---|---|---|---|---|
| USART2_TX | PA2 | 复用推挽 AF_PP,50MHz | DI | 时钟挂在 APB2 |
| USART2_RX | PA3 | 浮空输入 IN_FLOATING | RO | 建议外部上拉 10k |
| 方向控制 | PG3 | 推挽输出 Out_PP,50MHz | DE 与 /RE 短接 | 高电平发送 |
| 电源 | 3.3V 或 5V | — | VCC | 按探头铭牌来 |
| 地 | GND | — | GND | 必须与探头共地 |
时钟使能这里有个容易忽略的点:GPIOA 和 GPIOG 都在 APB2 总线上,USART2 挂在 APB1 上,两条总线的时钟要分别开。代码里RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG|RCC_APB2Periph_GPIOA, ENABLE)和RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)是两句独立的调用,漏掉任何一句的表现都是「编译通过、运行时引脚毫无反应」,排查时先回头数这两行。
现场接线还有三条硬规矩:A 接 A、B 接 B,接反了近距离可能还能通、拉长必挂;两端设备必须共地,只接 A/B 不接地,共模电压漂到超出范围就时通时断;总线长度超过三十米或者波特率上到 115200,末端要并 120Ω 终端电阻。这几条在实验室桌面上全都体现不出来,一上现场就集中爆发。
2.3 方向切换时序:等 TC 标志而不是 TXE
这是整套代码里最容易写错、也最难通过现象定位的地方。发送函数里那个while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)等的不是「数据写进寄存器」,而是「最后一个字节的最后一位已经从移位寄存器移出去了」。
区别在哪:TXE(发送数据寄存器空)置位时,数据只是从 DR 搬到了移位寄存器,线上还在一个 bit 一个 bit 地吐。这时候如果把 DE 拉低,收发器立刻退出驱动状态,最后那一个字节就被拦腰截断。9600 波特率下一个字节约 1.04ms,截断的后果是传感器收到一帧 CRC 错误的报文,直接丢弃不回——现象就是「偶尔能通、大部分时候没反应」,非常像接触不良,实际是时序问题。
反过来,接收切换也有讲究。发完最后一个字节、TC 置位后立刻拉低 DE,收发器进入接收态需要一点建立时间,同时总线上最后一个停止位可能还没完全结束,所以代码里在拉低之前垫了delay_ms(2)。这个 2ms 是经验值,9600 下够用,波特率上去了可以缩短,但别省成 0。
注意:方向脚上电默认必须是低电平(接收态)。如果复位瞬间 GPIO 处于浮空或者高电平,收发器会短暂抢占总线,多节点系统里这一下就可能把别人正在发的帧打坏。初始化函数末尾那句
RS485_TX_EN=0不是可有可无的收尾。
3. 驱动代码逐函数拆:初始化、发送、接收三个动作的边界
硬件理顺之后,代码其实就三个动作。但每个动作都有自己的边界条件,写成「能跑」和写成「稳定跑」差的就是这些边界。
3.1 RS485_Init 里的四件事
这个函数表面上是初始化串口,实际干了四件互相独立的事,任何一件漏了都会以不同形式报错。
第一件是时钟,前一小节已经说过。第二件是 GPIO 模式配置,注意 PA3 的注释写的是「模拟输入」,但代码用的是GPIO_Mode_IN_FLOATING,以代码为准——注释是复制粘贴留下来的历史包袱,别被带偏。第三件是 USART2 的参数:波特率 9600、8 位字长、1 位停止位、无校验、无硬件流控、收发双开。第四件是 NVIC 配置,优先级分组在 main 里设成了NVIC_PriorityGroup_2(2 位抢占 + 2 位响应),USART2 中断抢占优先级 3、子优先级 2。
参数表列一下,改的时候对着改:
| 参数 | 取值 | 依据 |
|---|---|---|
| 波特率 | 9600 | 传感器出厂默认,改了探头也得跟着改 |
| 字长 | 8 位 | Modbus-RTU 固定 8N1 或 8E1 |
| 停止位 | 1 位 | 同上 |
| 校验 | 无 | 若探头是 8E1,这里要改成USART_Parity_Even |
| 流控 | 无 | 485 靠 DE 控方向,不走 RTS/CTS |
| 中断 | RXNE 使能 | 每收到一个字节进一次中断 |
EN_USART2_RX这个宏是中断接收的总开关,定义为 1 才会在USART_ITConfig里开 RXNE 中断。调试阶段如果只想验证发送是否正常,把它改成 0 可以让接收缓冲区一直为空,排除接收侧的干扰。
3.2 发送函数:循环里那个 while 到底在等什么
RS485_Send_Data的逻辑是:拉高 DE 进发送态,逐字节写 DR,每写一个之前等一次 TC,全部写完后最后再等一次 TC,清接收计数,拉低 DE 回接收态。
void RS485_Send_Data(u8 *buf, u8 len) { u8 t; RS485_TX_EN = 1; /* 抢占总线,切入发送态 */ for (t = 0; t < len; t++) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); USART_SendData(USART2, buf[t]); /* 写 DR 触发发送 */ } while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); /* 等最后一个字节彻底走完 */ RS485_RX_CNT = 0; /* 清掉发送期间可能收到的杂散字节 */ RS485_TX_EN = 0; /* 释放总线,回到接收态 */ }循环内那个while在第一次进入时等的是「上一次发送完成」,因为在初始化里已经USART_ClearFlag(USART2, USART_FLAG_TC)清过标志,第一次会立刻通过。真正起作用的是循环外那个while,它保证 DE 拉低之前移位寄存器已经空。RS485_RX_CNT = 0这一句很多人会漏,后果是发送期间收发器可能因为总线上的反射收到一两个杂散字节,接收计数被污染,下一次判帧就会多算长度。
参数只有一个len,注意它是u8,最大 255。请求帧固定 8 字节,不会溢出,但如果哪天要发长报文,这个类型得改成u16。
3.3 接收:中断攒字节 + 10ms 收敛判帧尾
接收分两半。中断里只做一件事:把 DR 里的字节搬进RS485_RX_BUF,计数加一,越界就回绕或者丢弃。
void USART2_IRQHandler(void) { u8 res; if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { res = USART_ReceiveData(USART2); /* 读 DR 同时清 RXNE */ if (RS485_RX_CNT < 64) /* 缓冲区 64 字节,防越界 */ { RS485_RX_BUF[RS485_RX_CNT++] = res; } } USART_ClearFlag(USART2, USART_FLAG_TC); }这里必须强调:中断里不要做任何发送动作,也不要用delay_ms。中断服务函数执行时间越长,主循环被抢占得越久,而且如果延时依赖的 SysTick 优先级低于 USART2,会在中断里死等,直接卡死。原文那份参考实现里在中断里做了「收到什么回发什么」的回显,还夹了两次毫秒级延时,这是导致总线自激和偶发卡死的高发点,实际项目里必须删掉。
接收的另一半在主循环里,靠「两次采样长度不变」来判帧结束:
void RS485_Receive_Data(u8 *buf, u8 *len) { u8 rxlen = RS485_RX_CNT; u8 i = 0; *len = 0; delay_ms(10); /* 帧内字符间隔远小于 10ms,等它收敛 */ if (rxlen && rxlen == RS485_RX_CNT) /* 两次长度一致,说明这一帧发完了 */ { for (i = 0; i < rxlen; i++) buf[i] = RS485_RX_BUF[i]; *len = RS485_RX_CNT; RS485_RX_CNT = 0; /* 清计数,准备下一帧 */ } }10ms 这个值的依据是 Modbus-RTU 的帧间隔定义:帧与帧之间至少静默 3.5 个字符时间。9600 波特率下 1 个字符是 11 位、约 1.15ms,3.5 字符约 4ms,取 10ms 留了一倍余量。代价是这个函数是阻塞的,主循环调用它就会停 10ms,如果还要跑按键扫描或者屏幕刷新,得改成状态机版本,最后一章会给出写法。
提示:
rxlen是u8,RS485_RX_CNT也是u8,两者在中断和主循环之间共享,读写顺序有讲究。先读长度再延时再比对,如果写成先延时后读,第一次读到的就已经是被覆盖后的值,判帧永远失败。
3.4 主循环的节奏:300ms 采样与 3s 刷新
把三个函数串起来的主循环,节奏是这样的:发 8 字节请求 → 等 300ms → 收数据 → 有数据就解析显示 → 等 3000ms 进入下一轮。
while (1) { RS485_Send_Data(TR_Read, 8); /* 发查询指令 */ delay_ms(300); /* 给探头留出响应和传输时间 */ RS485_Receive_Data(rs485buf, &len); if (len) { if (len > 9) len = 9; /* 帧长上限保护 */ /* 解析与显示,见下一章 */ } delay_ms(3000); /* 采样周期 3s,土壤量变化慢,够用 */ }300ms 的等待不是拍脑袋:9600 下 9 字节响应帧的传输时间约 9.4ms,加上探头内部 ADC 采样和固件处理,常见规格在 50~200ms 之间。给 300ms 是留余量。3s 的周期取决于被测对象的变化速率,土壤温湿度这种慢变量 3s 绰绰有余,如果换成振动或者流量,得压到几十毫秒,那时候阻塞式写法就撑不住了。
4. 报文层:Modbus-RTU 请求帧拆到字节,温湿度怎么算出来
代码跑通只说明电气和时序没问题,数据对不对全看报文层。这一层出错的表现往往不是「收不到」,而是「收到了但数值明显不合理」,比收不到更难查。
4.1 八个字节的请求帧逐个字段对照
TR_Read[8] = {0x01, 0x03, 0x00, 0x06, 0x00, 0x02, 0xC4, 0x38}这一行是整份代码里信息密度最高的地方,一个字节都不能随便改。
| 偏移 | 值 | 含义 | 改动影响 |
|---|---|---|---|
| 0 | 0x01 | 从站地址 | 多节点时必须唯一 |
| 1 | 0x03 | 功能码:读保持寄存器 | 换成 0x04 读输入寄存器,看探头手册 |
| 2 | 0x00 | 起始寄存器高字节 | 起始地址 0x0006 |
| 3 | 0x06 | 起始寄存器低字节 | 即第 7 个寄存器 |
| 4 | 0x00 | 寄存器数量高字节 | 读 2 个寄存器 |
| 5 | 0x02 | 寄存器数量低字节 | 即 4 个字节数据 |
| 6 | 0xC4 | CRC 低字节 | 前 6 字节算出 |
| 7 | 0x38 | CRC 高字节 | Modbus 规定低字节在前 |
功能码 0x03 是「读保持寄存器」,这是 Modbus 里最常用的读命令。寄存器地址 0x0006 和数量 0x0002 这两个值不是通用的,不同厂家的探头把温湿度放在不同寄存器上,有的在 0x0000,有的在 0x0012。拿到新探头第一件事是翻手册确认这两个字段,改完必须重算 CRC,手工改完不重算 CRC 是最常见的「发出去没反应」原因。
4.2 响应帧结构与 CRC 自校验
正常响应是 9 个字节,结构跟请求帧对应:地址 + 功能码 + 字节数 + 4 字节数据 + 2 字节 CRC。异常响应只有 5 个字节,功能码的最高位置 1(比如 0x83),后面跟一个异常码,常见的是 0x02(非法数据地址)和 0x03(非法数据值),这两个码基本等于在告诉你「寄存器地址写错了」。
CRC 校验必须做。总线上的干扰、方向切换时被截断的半个字节、多节点抢占总线造成的碰撞,全都会产生「长度看着对、内容已经烂了」的帧。不做校验直接拿去算,屏幕上就会时不时跳出一个离谱的温湿度值。
/* Modbus-RTU CRC16,多项式 0xA001 反向,初值 0xFFFF */ u16 CRC16_Modbus(u8 *buf, u8 len) { u16 crc = 0xFFFF; u8 i, j; for (i = 0; i < len; i++) { crc ^= buf[i]; for (j = 0; j < 8; j++) { if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; /* 低字节在前,高字节在后 */ }这个函数算出来的crc低字节要跟帧里的倒数第二个字节比、高字节跟最后一个字节比。用请求帧验证一下:前 6 字节算出来是 0x38C4,所以帧尾是0xC4, 0x38,跟常量数组对得上,说明算法实现正确。以后改任何字段,都可以拿这个函数现算现填。
4.3 湿度除以 10、温度除以 100 的缩放来源
原始数据是 4 个字节,按高字节在前拼成两个 16 位整数:
u16 raw_h = (u16)(rs485buf[3] << 8) | rs485buf[4]; /* 湿度原始值 */ u16 raw_t = (u16)(rs485buf[5] << 8) | rs485buf[6]; /* 温度原始值 */为什么要缩放:Modbus 寄存器只能存整数,传感器固件为了保留小数位,会把真实值乘一个系数再存进去。土壤湿度常见的是乘 10(单位 0.1%RH),温度常见的是乘 10 或者乘 100(单位 0.1℃ 或 0.01℃)。原文代码里WD=(rs485buf[3]*256+rs485buf[4])/100和SD=(rs485buf[5]*256+rs485buf[6])/10用的是 100 和 10 两个不同系数,这说明这颗探头的湿度分辨率是 0.01、温度是 0.1。
这里有个必须点出来的坑:WD和SD在原文里声明成u8。u8的取值范围是 0~255,湿度 0~100 勉强塞得下(但小数位全丢了),温度如果乘了 10、范围到 -400~800,赋值给u8会直接截断回绕,零下温度显示成正数、高温显示成个位数,全乱。正确做法是用s16存原始值、float或者放大十倍的s16做显示:
s16 raw_t = (s16)((rs485buf[5] << 8) | rs485buf[6]); /* 有符号,兼容零下 */ float temp_c = raw_t / 10.0f; /* 按手册系数改 */温度字段是不是有符号也要看手册。有的厂家用补码表示负数,有的用最高位做符号位加偏移量,还有的把零下映射到 0x0000 以上的正数区间。这三种编码方式算出来的结果完全不同,拿一个新探头先用冰水混合物(0℃)和室温各测一次,对照原始十六进制值反推系数,比看手册快。
4.4 把解析封装成带校验的函数
把上面几步收拢成一个函数,返回值表示解析是否成功,主循环只负责显示:
/* 返回 0 成功,-1 帧长不对,-2 CRC 错,-3 异常响应 */ int Parse_SoilFrame(u8 *frame, u8 len, float *humi, float *temp) { u16 crc_calc, crc_recv; if (len != 9) return -1; /* 正常响应固定 9 字节 */ if (frame[1] & 0x80) return -3; /* 功能码最高位=异常响应 */ crc_calc = CRC16_Modbus(frame, 7); /* 前 7 字节参与校验 */ crc_recv = (u16)frame[7] | ((u16)frame[8] << 8); /* 低字节在前 */ if (crc_calc != crc_recv) return -2; *humi = ((s16)((frame[3] << 8) | frame[4])) / 100.0f; *temp = ((s16)((frame[5] << 8) | frame[6])) / 10.0f; return 0; }三个参数:frame是接收缓冲区,len是实际收到的字节数,后两个是输出。返回值设计成负数是常见做法,主循环里可以据此决定是重发、报警还是显示上一次的有效值。crc_recv的拼接顺序容易写反,记住 Modbus 是低字节在前,帧里第 8 个字节是低字节。
提示:
(s16)((frame[3] << 8) | frame[4])这里必须显式转s16再赋值,如果先赋给u16再转,负数的位模式在两步之间不会变,但中间变量类型写错会让编译器插入不同的符号扩展,结果差 65536。这种 bug 只在零下温度出现,夏天测不出来。
5. 避坑与排查:五个现场翻车记录
这一章列的每一条都是实打实踩过的,共同特点是「代码看起来没问题,但就是不工作」。按现象、原因、解决三段式记下来,下次遇到能省半天。
5.1 方向脚宏定义和初始化端口对不上
现象:发送函数返回了,串口助手在总线上也测到波形,但传感器毫无反应;偶尔又能通一次。
原因:头文件里#define RS485_TX_EN PDout(7),初始化函数里配的却是 GPIOG 的 Pin3。宏操作的端口和实际接线的端口不是同一个,DE 永远拉不高,收发器一直处于接收态,MCU 的 TX 信号根本没上总线。示波器探 MCU 的 PA2 有波形,探 A/B 什么都没有,就是这个问题。
解决:宏定义、GPIO 初始化、原理图三处对齐同一个端口和引脚。改完在发送函数里加一个临时的翻转测试——拉高 DE 后直接读回GPIO_ReadOutputDataBit,确认电平真的变了再往下走。
5.2 中断里做回显,总线自己跟自己打架
现象:单节点测试时好时坏,接上第二个节点后彻底不通;偶尔程序卡死不响应。
原因:参考实现的中断服务函数里做了「收到一个字节就切发送态把它发回去」的处理,还夹了delay_ms(1)和delay_ms(2)。收到的每一个字节都会触发一次回发,两个节点同时这么干就是互相碰撞;中断里调用毫秒延时,如果 SysTick 优先级低于 USART2,延时永远等不到,直接死锁。
解决:中断里只做「搬字节 + 计数」,不发送、不延时、不解析。发送动作全部放到主循环,由应用层决定什么时候发、发什么。中断服务函数的执行时间控制在几微秒量级。
5.3 u8 变量存温度,数值被截断
现象:湿度显示基本正常,温度偶尔显示成个位数或者负数,冬天尤其明显。
原因:WD和SD声明成u8,而温度原始值缩放后可能超过 255 或者为负。u8赋值时的截断是按位取的,不是饱和,256 变成 0、-1 变成 255,显示出来的数完全对不上。
解决:原始值用s16,显示值用float或者放大十倍的s16。改完之后把LCD_ShowxNum换成能显示小数点和负号的LCD_ShowString,先格式化成字符串再打,别用只支持无符号整数的显示函数硬凑。
5.4 A/B 接反、没共地、缺终端电阻
现象:桌面上三十厘米的跳线通信正常,接到现场二十米线缆上就时通时断,早上能通中午不通。
原因:三个硬件问题叠在一起。A/B 接反在短线、低速率下因为反射和容性耦合可能还能勉强判对;不共地导致两端共模电压随负载变化漂移,超出收发器的 -7~+12V 范围就误判;长线末端不接 120Ω,信号反射叠加在边沿上,接收端采到的电平忽高忽低。
解决:先短接法确认 A/B 极性(不确定就交换一次试,通了就是接反);补一根地线,两端设备的地直接连;末端并 120Ω 电阻,中间节点不接。三件事做完还不行,再考虑降波特率到 4800 试一次,降速能通说明是信号完整性问题而不是协议问题。
5.5 长度截断直接显示,不做 CRC 校验
现象:大部分时候数值正常,每隔几分钟跳出一个明显不合理的值,比如湿度 400%、温度 120℃。
原因:主循环里if(len > 9) len = 9之后直接取rs485buf[3..6]算温湿度,既没验证len == 9,也没校验 CRC。干扰造成的半帧、两帧粘连、方向切换截断的残帧,长度可能恰好凑够 9,内容却是错的。
解决:加长度判断和 CRC 校验,校验失败就丢弃这一帧、沿用上一次的有效值,并累计一个错误计数。错误计数持续增长说明硬件有问题,偶尔一次说明是噪声,两种情况的处理策略不一样。
6. 从单点到多点:轮询状态机与现场验证手法
单节点跑通之后,真正的考验是多节点轮询。十个探头挂在同一条总线上,每个地址轮流问一遍,一轮下来几百毫秒,主循环里再塞阻塞延时就会互相拖死。这时候要把第 3 章那套阻塞收发改成状态机。
typedef enum { ST_IDLE, ST_WAIT_ACK, ST_NEXT } rs_state_t; static rs_state_t st = ST_IDLE; static u8 node = 1; /* 当前轮询地址,1~10 */ static u32 t_send = 0; void RS485_Poll_Task(void) { switch (st) { case ST_IDLE: TR_Read[0] = node; /* 换从站地址 */ /* 地址变了必须重算 CRC,否则从站直接丢弃 */ { u16 c = CRC16_Modbus(TR_Read, 6); TR_Read[6] = c & 0xFF; TR_Read[7] = (c >> 8) & 0xFF; } RS485_Send_Data(TR_Read, 8); t_send = GetTickCount(); /* 记录发送时刻 */ st = ST_WAIT_ACK; break; case ST_WAIT_ACK: if (RS485_RX_CNT) /* 收到任何字节就算有响应 */ { /* 交给解析函数,成功则更新该节点的缓存 */ st = ST_NEXT; } else if (GetTickCount() - t_send > 200) /* 200ms 超时,判定掉线 */ { Mark_Node_Offline(node); /* 标记离线,界面置灰 */ st = ST_NEXT; } break; case ST_NEXT: node = (node >= 10) ? 1 : node + 1; /* 轮转到下一个地址 */ st = ST_IDLE; break; } }这段的关键在于「地址变了要重算 CRC」和「超时要能跳过去」。前者漏了的表现是第一个节点通、后面全不通;后者漏了的表现是拔掉一个探头整条总线卡死。GetTickCount用 SysTick 的毫秒计数实现,两行代码的事,比delay_ms阻塞强太多。
现场验证我一般按这三步走,顺序不能反:
| 步骤 | 动作 | 通过判据 |
|---|---|---|
| 1 | 断开探头,只留 MCU 和收发器,示波器看 A/B | 发送时能看到差分波形,幅度 >1.5V |
| 2 | 接单个探头,串口助手抓原始十六进制 | 收到 9 字节,CRC 校验通过 |
| 3 | 接全部节点,跑轮询十分钟 | 无超时累积,各节点数据连续 |
第一步能把 MCU 侧和总线侧的问题切开,第二步确认协议层,第三步才是系统级验证。跳过第一步直接上全系统,出问题时根本分不清是代码、接线还是干扰。
最后说一个我自己的习惯:每换一款新传感器,我都会先用串口助手手动发一次请求帧,把响应的十六进制原样抄在纸上,标出哪两个字节是湿度、哪两个是温度、系数是多少,然后再动代码。这个动作花五分钟,能省掉后面反复改系数、怀疑类型、重烧固件的几个小时。那年冬天一个探头温度一直显示 6548℃ 折腾到半夜,最后发现是符号位没处理,从那以后每次上电第一件事就是拿冰袋和体温测两个点验证缩放系数,强制走一遍才敢接下一级。希望帮到你。
本文还有配套的精品资源,点击获取