首页 / 资讯中心 / 文章详情

STM32F103 RS485传感器采集与Modbus RTU调试实战

STM32F103 RS485传感器采集与Modbus RTU调试实战 ★ FEATURED ARTICLE
简介这是一份围绕STM32F103微控制器与RS485通信展开的入门实践文档面向刚接触工业总线通信的嵌入式学习者和电子类学生用于解决半双工485收发切换、串口驱动编写与传感器数据读取等常见入门问题。压缩包共1个docx文件约1.22MB以图文与代码混合的文档形式组织便于边看原理边对照例程。文档从RS485总线抗干扰、远距离、多节点特性讲起说明如何借助UART2与DE使能引脚完成收发模式切换并给出RS485_Init、RS485_Send_Data、RS485_Receive_Data等函数的实现思路与波特率配置方法还梳理了头文件中接收缓冲区、长度变量与模式控制宏的定义方式。主函数部分以土壤温湿度传感器为例展示发送读取指令、延时等待、解析响应并在TFTLCD上显示数值的完整流程同时涉及延时函数、中断优先级分组与数据换算等细节。目前已有2629人学习适合作为RS485通信原理与STM32串口编程的对照参考。1. 从一块土壤探头说起这份 STM32 RS485 文档能省下你多少时间STM32 RS485 这个组合第一次上手几乎没人一次接对。我最早拿到的是一块土壤温湿度探头四根线两根供电、两根 A/B探头不认串口助手只认指令。接上 F103 开发板跑起来后串口助手一片安静示波器上只有自己发出去的波形回来的线上什么也没有——最后发现只是方向控制脚没翻过来。这份 STM32F103 RS485 传感器文档把整条链路摊开了USB 转 485 收发芯片上那根方向脚怎么切USART2 怎么初始化01 03 00 06 00 02 C4 38这条读寄存器指令每个字节什么意思回来的 9 个字节怎么截取温湿度怎么从四个字节里凑出来显示到 TFTLCD 上。刚拿到 F103 板子和 485 探头、想一次把链路打通的人能靠它省下大量排查时间已经能跑但搞不清为什么要delay_ms(10)的人也能从里面看到自己代码里那句延时到底在等什么。2. 硬件与协议底座差分总线、收发器方向脚和 USART2 的三角关系485 这一层如果没搞明白软件调得再细也白搭。摄像头、串口屏、传感器这类外设接在同一个 485 总线上谁说话、谁说完了、下一句什么时候轮到全靠硬件和时序配合。这一章先把硬件和协议底座理顺写驱动的时候才不至于写一句、改一句。2.1 A/B 两根线撑起 1200 米差分传输到底强在哪RS485 用的是差分信号逻辑“1”和“0”靠 A、B 两根线之间的电压差来判断而不是靠对地的绝对电压。接收端只看两根线之间的差值外界耦合到一对双绞线上的干扰往往是共模的两线同时抬高或同时压低差值不变信号就还在。这是它能拉到上千米、能在车间里活下来的根本原因——不是协议多高级是对干扰天然免疫。接线的时候有几条硬规矩我头几次全踩过手拉手串联拓扑不要从总线上分叉出去接成星形。星形接法会让分支末端形成反射点距离一长波形上就会出现台阶和振铃。总线两端各放一只 120Ω 终端电阻位置在物理最两端不在中间节点。屏蔽双绞线优先屏蔽层单端接地别两头都接否则会形成环流。A/B 的命名各家不统一有的把 A 标成正、有的把 A 标成负。别信丝印先看手册标注或者拿万用表量空闲态压差确认一遍。在长线或者节点多的场景里总线空闲时可能会浮在一个不确定的电平上常见做法是在 A 上拉到 3.3V、B 下拉到地各加一只 1kΩ4.7kΩ 的偏置电阻让空闲状态稳定在逻辑 1。这一招在干扰大的现场很管用能消掉一类“白天正常、晚上乱码”的玄学问题。2.2 DE/RE 一个引脚定生死收发切换的时序要点F103 片上只有 UART没有 485。板上一定有一颗收发器芯片它有两个控制脚DE发送使能和 RE接收使能低有效。多数模块把这两个脚并用一根 GPIO 就够GPIO 输出高 → DE1、RE1 → 芯片进入发送模式GPIO 输出低 → DE0、RE0 → 芯片回到接收模式。半双工的意思就在这同一时刻只能发或者只能收两者互斥。总线上几十个设备谁把发送使能拉高了谁就在说话其他所有设备必须处于接收态否则两条线互相打架谁的数据都传不出去。时序上最容易翻车的是“什么时候把 GPIO 拉低”。很多人发完最后一个字节就立刻拉低结果最后一个字节的低位还没从移位寄存器里出去被硬生生截断了。正确姿势是等 USART 的TCTransmission Complete标志位置位而不是TXETransmit Data Register Empty。TXE 只说明数据寄存器空了、可以塞下一个字节但移位寄存器里还有东西在往外吐TC 才表示真的发完了。文档里RS485_Send_Data用的正是USART_FLAG_TC这一点没写错/* 发送完成后必须等 TC不能等 TXE */ while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); RS485_RX_CNT 0; // 发完清接收计数避免把旧数据当新回包 RS485_TX_EN 0; // 切回接收模式最后那句RS485_RX_CNT 0是很有用的一手。发送期间如果接收缓冲区里还残留着上一轮的字节不清零的话接收函数会把它们拼到这次的回包前面长度和内容全错表现出来就是“偶尔能读出数、偶尔读出一堆乱码”。2.3 F103ZET6 引脚分配PA2、PA3 和那个对不上的方向脚STM32F103ZET6 用的是 USART2 挂 485USART1 留给串口打印和 LCD 调试。引脚分配是固定的信号引脚GPIO 模式说明USART2_TXPA2复用推挽输出 AF_PP50MHz连收发器 DIUSART2_RXPA3浮空输入 IN_FLOATING连收发器 RO方向控制 DE/RE文档头文件定义 PD7推挽输出 Out_PP高发送低接收电源3.3V / GND—收发器与单片机共地这里有份资料里一个必须指出的对不上的地方rs485.h里宏定义写的是#define RS485_TX_EN PDout(7) // 485 模式控制也就是方向脚放在PD7但RS485_Init里实际配置的是GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; // 注释写 CS-485 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOG, GPIO_InitStructure); // 配的是 PG3也就是把PG3配成了推挽输出。宏和初始化对不上编译不报错跑起来就是“我明明拉高了芯片却没进发送态”。落地之前必须先对着板子原理图确认方向脚到底接在哪个脚上然后二选一统一要么把宏改成PGout(3)要么把GPIO_Init里的端口和 Pin 改成 GPIOG 之外的 GPIOG…准确说是改成与宏一致的 PORTD 和GPIO_Pin_7。除此之外GPIO_Mode_IN_FLOATING上面那行注释写的是“模拟输入”这是笔误浮空输入和模拟输入在 F103 里是两个不同的模式工程笔记里最好顺手改掉不然新人照抄会一头雾水。还有一点PA3 配成浮空输入是为了让收发器的 RO 直接驱动别在这里开内部上拉。如果板上 RO 带上拉或者有外部上拉浮空就够开了反而在空闲态多吃一点电流如果现场干扰大导致空闲误触发优先在硬件上加偏置电阻而不是在软件里把 PA3 改成上拉来凑合。3. 驱动层逐段拆从 RS485_Init 到 RS485_Receive_Data 的完整链路驱动这一层是整份资料最厚的地方也是最值得逐行看的地方。它分成四块初始化、中断接收、发送、带空闲判定的接收。下面按调用顺序拆顺便把原文里几个容易照抄错的地方标出来。3.1 RS485_Init四步配置和两处容易照抄错的细节这个函数的骨架是标准的 F103 标准库流程开时钟 → 配 GPIO → 配 USART → 配 NVIC。顺序不能乱尤其是 NVIC 要在 USART 使能中断之前配好否则中断可能在优先级还没设定时就抢进来一次。void RS485_Init(u32 bound) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* 1. 开时钟GPIOA 给 TX/RXGPIOG 给方向脚USART2 挂在 APB1 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG | RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); /* 2. TX PA2复用推挽必须是 AF_PP 而不是 Out_PP */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 3. RX PA3浮空输入由收发器 RO 直接带动 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); /* 4. USART29600 8N1无流控收发都开 */ USART_InitStructure.USART_BaudRate bound; 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(USART2, USART_InitStructure); USART_ClearFlag(USART2, USART_FLAG_TC); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); // 开接收非空中断 USART_Cmd(USART2, ENABLE); /* 5. NVIC抢占 3、子优先级 2 */ NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 3; NVIC_InitStructure.NVIC_IRQChannelSubPriority 2; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); RS485_TX_EN 0; // 上电默认收别一开机就占着总线 }参数上只需要盯住一个bound。传感器手册上写的 9600 就是 9600写 115200 是连不上的这一层没有自适应。数据格式 8 位、1 停止位、无校验是绝大多数 485 传感器的默认值如果手册上写的是 8E1偶校验USART_Parity要改成USART_Parity_EvenUSART_WordLength同步改成USART_WordLength_9b——这一点常被忽略因为多数人抄代码时压根不看手册的格式那一栏。两处容易照抄错的细节一是优先级分组在main里设的是NVIC_PriorityGroup_22 位抢占 2 位响应那抢占优先级最大只能是 3、子优先级最大也只能是 3写成 4 以上会被截断成 0表现是“优先级设了但排序怪怪的”二是方向脚初始化必须先于USART_Cmd否则从串口使能到方向脚配好之间有一个短暂窗口那段时间引脚状态不确定。3.2 中断服务函数接收缓冲该在哪累加文档给的中断函数是这样的void USART2_IRQHandler(void) { u8 res; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { res USART_ReceiveData(USART2); // 读 DR 自动清 RXNE RS485_TX_EN 1; // 立刻切成发送 delay_ms(1); USART_SendData(USART2, res); // 把收到的字节原样回出去 while (USART_GetFlagStatus(USART2, USART_FLAG_TC) ! 1); delay_ms(2); RS485_TX_EN 0; } USART_ClearFlag(USART2, USART_FLAG_TC); }这段代码的本质是一个“回显测试”收到什么就原样吐回去用来快速验证硬件通路通不通接 USB 转 485 在电脑上敲字符能原样看到就说明 A/B、方向脚、波特率全对。作为自检手段它很好用。但它不能和主循环的采集逻辑同时存在。原因有三个第一中断里做了delay_ms(1)和delay_ms(2)。中断服务函数里做毫秒级阻塞直接把主循环的时序打乱传感器回 9 个字节可能只花 10ms 左右中断每次进去阻塞 3ms很容易在字节之间错过接收窗口。第二回显会把总线上其他设备的回包当成要转发的数据再发一遍多节点场景下直接制造总线冲突。第三也是最关键的这段代码里根本没有往RS485_RX_BUF里存数据、也没有累加RS485_RX_CNT。而RS485_Receive_Data要靠这两个全局量判断有没有收到、收到多长。也就是说如果只跑这段中断接收长度永远是 0主循环里的if(len)分支永远进不去。实际采集用的中断应该长这样——只搬运、不做业务void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { u8 res USART_ReceiveData(USART2); // 读 DR硬件自动清 RXNE if (RS485_RX_CNT sizeof(RS485_RX_BUF)) { RS485_RX_BUF[RS485_RX_CNT] res; // 存进缓冲并累加长度 } // 不在这里发数据、不在这里延时 } }缓冲区大小 64 字节对 8 字节请求 9 字节响应绰绰有余但如果后面改成一次读 10 个寄存器响应 25 字节或者做多段拼接就要往上抬到 128 甚至 256并且加上溢出保护——上面那个if判断就是干这个的不加的话一旦总线噪声把计数推过头就会踩到相邻变量。3.3 发一帧与收一帧TC 标志位和 10ms 空闲判定发送函数的逻辑很短但每一句都有用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; // 交还总线 }参数buf是报文首地址len是字节数按 Modbus 读寄存器的请求固定是 8。这里要注意循环里的等待放在发送之前语义是“等上一个字节彻底发完再塞下一个”跟“塞完再等”效果一样但能保证第一次进循环时不会在 TC 未置位的情况下硬写 DR。接收函数用的是一个很朴素的空闲判定void RS485_Receive_Data(u8 *buf, u8 *len) { u8 rxlen RS485_RX_CNT; u8 i 0; *len 0; delay_ms(10); // 等 10ms期间没有新字节就认为收完 if (rxlen RS485_RX_CNT rxlen) // 两次读数一致且非零 → 接收结束 { for (i 0; i rxlen; i) buf[i] RS485_RX_BUF[i]; *len RS485_RX_CNT; RS485_RX_CNT 0; // 清计数准备下一帧 } }逻辑说明先记下进入时的长度等 10ms 再看一次如果两次相同说明这 10ms 内没有新字节进来判定一帧结束。这是在没有硬件空闲中断、也没有定时器的情况下最省事的做法。9600 波特率下一个字节约 1.04ms10 位 / 9600一帧 9 字节大概 9.4ms10ms 的空闲窗口刚好卡在“帧内字节间隔远小于 10ms、帧间间隔大于 10ms”的边界上属于能用但不算稳。参数上要留意*len是输出参数进来先清 0只有真正判定收完才写实际长度。主循环里if(len)就是靠这个 0 值来判断“有没有新数据”。代价也很明显这个函数阻塞 10ms。一次采集循环里发一次、收一次加上delay_ms(300)和delay_ms(3000)整轮三四秒单点采集完全够用但如果要挂 10 个节点、每 200ms 轮询一次这个阻塞就会拖垮整个节奏得换成定时器 空闲中断或者 DMA 帧超时判定的写法。3.4 报文解析01 03 00 06 00 02 C4 38 每个字节是什么主循环里那条指令是整份资料的核心拆开看u8 TR_Read[8] {0x01, 0x03, 0x00, 0x06, 0x00, 0x02, 0xC4, 0x38};字节位置值含义[0]0x01从机地址总线上区分设备用[1]0x03功能码读保持寄存器[2..3]0x00 0x06起始寄存器地址 0x0006[4..5]0x00 0x02读 2 个寄存器共 4 字节数据[6..7]0xC4 0x38CRC16 校验低字节在前这是标准 Modbus RTU 帧没有帧头帧尾靠 3.5 个字符时间的静默来分帧。回包格式固定为地址 功能码 字节数 数据 CRC读 2 个寄存器时字节数是 4整帧 9 个字节。主循环里那句if (len 9) len 9; WD (rs485buf[3] * 256 rs485buf[4]) / 100; SD (rs485buf[5] * 256 rs485buf[6]) / 10;rs485buf[0]是地址回显、[1]是功能码回显、[2]是字节数 04真正的数据从[3]开始。第一个寄存器高字节在[3]、低字节在[4]拼成 16 位后除以 100第二个寄存器在[5]、[6]拼完除以 10。这里是整份资料里最需要对着传感器手册确认的地方哪个寄存器是温度、哪个是湿度、缩放系数是 10 还是 100不同厂家的定义完全不一样。代码里变量命名是WD温和SD湿但取的是第 1、第 2 个寄存器缩放分别除 100 和除 10。我见过同一型号换批次后系数互换的情况也见过干脆是两个寄存器都是除以 10 的。落地时不要照抄命名先拿手册的量程表反推一遍正常室温下读出来的原始值是多少除以系数后是否落在合理区间两分钟就能验完。长度限制那句if(len 9) len 9是个防护防止噪声导致长度异常时越界读rs485buf。但注意它只截上限不校验下限如果len是 5rs485buf[6]读的其实是上一次的残留数据显示结果会莫名其妙。严格做法是要求len 9才解析其余一律丢弃重试——这一点在第 6 章的改造里会补上。4. 把工程跑起来文件清单、初始化顺序与上机自检三步法代码能看懂和工程能跑起来是两回事。这一章按“文件在哪、谁先初始化、怎么验证”把复现路径走一遍照这个顺序做第一次上手不大会卡住。4.1 工程骨架与文件分工工程是标准库风格没有上 RTOS命名也很直白。典型文件构成如下文件作用是否可裁剪main.c主循环拼指令、收数据、刷 LCD否rs485.c / rs485.h485 初始化、收发、缓冲区否usart.c / usart.hUSART1 初始化 printf 重定向可改用途lcd.c / lcd.hTFTLCD 驱动显示用可换 OLED/串口delay.c / delay.hSysTick 延时毫秒/微秒否sys.c / sys.h位带操作宏PDout(7)这类否key.c / key.h按键主循环没用到可删sys.h里的位带宏是这套代码能写RS485_TX_EN 1这种神语句的原因它把某个 GPIO 的输出位映射成内存里一个 bit 的别名赋值就是写寄存器。好处是快且好读坏处是换芯片型号或者换端口时宏定义必须同步改这也是上一章那个 PD7/G3 对不上的根源。LCD 部分其实是可选件显示只是把结果可视化调试阶段完全可以把LCD_ShowxNum换成printf走 USART1 打到串口助手改起来只动两行。uart_init(9600)是给 USART1 用的跟 485 的 USART2 互不干扰两路串口独立配置共用 9600 只会影响打印速度不影响通信。4.2 初始化顺序时钟、延时、USART1、LCD、USART2main的初始化段落顺序不能随意调换尤其是延时要最先int main(void) { u8 i, len 0; delay_init(); // SysTick最先后面全靠它 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2 位抢占 2 位响应 uart_init(9600); // USART1调试打印用 LCD_Init(); // TFTLCD 初始化 RS485_Init(9600); // USART2 方向脚最后 POINT_COLOR BLUE; delay_ms(300); // 等总线稳定 while (1) { RS485_Send_Data(TR_Read, 8); delay_ms(300); // 给传感器留处理时间 RS485_Receive_Data(rs485buf, len); if (len) { if (len 9) len 9; WD (rs485buf[3] * 256 rs485buf[4]) / 100; SD (rs485buf[5] * 256 rs485buf[6]) / 10; LCD_ShowxNum(30, 190, WD, 4, 24, 0); LCD_ShowxNum(30, 230, SD, 4, 24, 0); } delay_ms(3000); } }delay_init()必须在所有需要延时的地方之前包括LCD_Init内部往往就带延时NVIC_PriorityGroupConfig要在任何 NVIC 初始化之前一旦分组设晚了后面配的优先级数值全按旧分组解释中断顺序就跟预期不符。RS485_Init放最后是因为它会把方向脚拉低如果 LCD 初始化过程中板上有共用引脚先配 485 有可能被 LCD 的 GPIO 配置覆盖掉。发送后那delay_ms(300)是给传感器留的响应时间不是给的通信时间——9600 下 9 个字节只要 10ms 左右。有些低功耗传感器上电后要几百毫秒才响应300ms 是一个偏保守但稳妥的值。轮询间隔 3000ms 对土壤温湿度来说完全够土温变化本来就慢采样太密没有意义还费电。4.3 上机自检先用电脑手动发一帧血泪经验先别急着往板子上接用 USB 转 485 模块在电脑上手动发一帧确认传感器是活的。这一步能砍掉一半的排查工作量。接好 A/B 和电源用 pyserial 发原始字节import serial, struct # 与板子完全相同的请求帧地址01 功能码03 起始0x0006 数量2 CRCC4 38 req bytes([0x01, 0x03, 0x00, 0x06, 0x00, 0x02, 0xC4, 0x38]) ser serial.Serial(COM3, baudrate9600, bytesize8, parityN, stopbits1, timeout1) ser.write(req) resp ser.read(9) # 响应固定 9 字节 print(raw :, resp.hex( )) if len(resp) 9 and resp[0] 0x01 and resp[1] 0x03: reg1 (resp[3] 8) | resp[4] reg2 (resp[5] 8) | resp[6] print(reg1%d reg2%d % (reg1, reg2)) else: print(无响应或帧结构不对先查 A/B 接反、地址、波特率) ser.close()这段脚本的关键是三件事一是波特率和帧格式必须和板子完全一致8N1、9600二是timeout1保证没响应时不会一直挂着三是把原始字节打出来而不是只打解析值因为排查时最需要看的是“到底一个字节都没回还是回了但内容不对”前者是硬件问题后者是协议问题两条完全不同的排查路线。电脑上能稳定读出数再把板子接上去。板子跑起来后如果读不到问题就只在 MCU 侧方向脚、初始化顺序、缓冲区、中断使能范围一下子收窄了。5. 避坑与排查五条现场踩出来的经验这一章按“现象 → 原因 → 解决”写都是这套代码实际跑起来时会遇到的。每一条我都至少撞过一次写下来省得重复交学费。5.1 现象程序在发回线上永远是空的原因三类。一是方向脚宏定义和实际初始化对不上RS485_TX_EN操作的是 PD7但芯片实际接的是 PG3写了等于没写收发器一直待在接收或发送单边状态二是 A/B 接反收发器收到的是反相信号一个字节都识别不出来三是板子的收发器 RE 是低有效但硬件上拉成高导致永远收不了。解决先用万用表量方向脚在发送瞬间有没有翻转这是最快的判断再拿示波器或者 USB 转 485 直接挂总线看有没有波形出去A/B 对调试一次两分钟的事不要凭丝印较劲。确认是宏的问题就把PDout(7)和初始化里的端口统一别只改一处。5.2 现象改了地址或者寄存器之后传感器彻底不理人原因CRC 没有跟着重算。很多人手改报文时只改前面的数据字节把最后两个字节原样保留接收端一算 CRC 不匹配按协议直接丢弃连错误响应都不回。485 传感器在这种情况下的表现是“死寂”非常容易被误判成硬件坏了。解决任何一个字节变了都要重算 CRC16低字节在前。别手算写个函数在发之前现算现填第 6 章给了实现。另外注意 CRC 的初始值 0xFFFF、多项式 0xA001跟有些资料里写的 0x1021 不是一套用错了照样全部不通。5.3 现象len经常是 0偶尔又有数据但内容是乱的原因中断里做了回显和delay_ms。文档里那段中断函数给的是回显测试代码如果直接拿来跑采集中断每次进去阻塞 3ms9 字节的响应可能被打断缓冲区里的字节数不对RS485_Receive_Data里两次读RS485_RX_CNT不一致判定成“还没收完”直接返回长度 0。另一种情况是中断里压根没往RS485_RX_BUF存数据长度永远是 0。解决中断只做搬运把res存进缓冲、RS485_RX_CNT自增别做任何发送和延时。回显测试代码单独留一个宏开关验证完就关掉。5.4 现象数据能收到但温度显示 655、湿度显示 0或者两个值反了原因三种。一是起始寄存器地址不对0x0006读到的是校准寄存器而不是测量值二是缩放系数不对代码里一个除 100 一个除 10但实际型号可能都是除 10三是高低字节顺序理解错有的传感器手册写的是小端实际发的是大端。解决把原始值打印出来对照手册的量程反推。室温 25℃ 左右如果原始值读出 2500系数就是 100读出 250系数就是 10。两个寄存器到底哪个是温度用手捂一下探头看哪个值变五秒钟就能确定比翻手册快。5.5 现象单节点正常一挂上第二个设备就时好时坏原因典型的总线问题。可能是两端没接 120Ω 终端电阻导致反射可能是各设备没有共地导致共模电压超范围也可能是两个设备地址配成了同一个回包在总线上直接打架。还有一种容易被忽略的是星形接线从主干上拉出两米线去接第二个设备这段分支就是反射源。解决先查地址逐个单独接上去确认地址不同再查共地所有设备的 GND 必须连在一起只接 A/B 不接地是最常见的错最后按手拉手改线两端补终端电阻。现场干扰大的话补上偏置电阻很多“白天好晚上坏”的问题就此消失。6. 二次开发从单点读温湿度到多节点轮询网关把这块板子当玩具是一回事做成能挂十几个探头、能上报的数据采集节点是另一回事。改造的核心只有两点把写死的报文变成可配置的把不可靠的一次收发变成带校验和重试的调用。6.1 把指令拼装抽成通用 Modbus 读寄存器函数原来的TR_Read是写死的 8 字节常量加一个探头就要复制一份数组、手算一次 CRC。抽成函数之后地址、起始寄存器、数量全变成参数/* Modbus CRC16初值 0xFFFF多项式 0xA0010x8005 反序 */ static u16 crc16_modbus(const u8 *buf, u16 len) { u16 crc 0xFFFF; u8 i; while (len--) { crc ^ *buf; for (i 0; i 8; i) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }这段是查表法的位运算版速度对 9600 波特率来说绰绰有余没必要上 256 字节的表。注意返回值是 16 位整数写进报文时要拆成低字节在前req[6] crc 0xFF; req[7] crc 8;顺序反了对方一律不认。6.2 加 CRC 校验与超时重试别让一次丢包拖垮整个循环原代码收到数据就解析长度不对也硬解这是最容易出脏数据的地方。改造成“校验通过才算成功、失败重试三次”/* 返回 1 成功0 失败out 至少 5num*2 字节 */ u8 Soil_Read(u8 addr, u16 reg, u16 num, u8 *out, u8 *outlen) { u8 req[8], retry, len 0; u16 crc; req[0] addr; req[1] 0x03; req[2] reg 8; req[3] reg 0xFF; req[4] num 8; req[5] num 0xFF; crc crc16_modbus(req, 6); req[6] crc 0xFF; // CRC 低字节在前 req[7] crc 8; for (retry 0; retry 3; retry) { RS485_Send_Data(req, 8); RS485_Receive_Data(out, len); /* 长度必须正好 52*num且 CRC 对得上地址和功能码回显也对 */ if (len 5 num * 2 out[0] addr out[1] 0x03 crc16_modbus(out, len - 2) (out[len - 2] | (out[len - 1] 8))) { *outlen len; return 1; } delay_ms(50); // 退让一下总线再重试 } *outlen 0; return 0; }几个参数值得说明num是寄存器数量响应长度一定是5 2*num用等式判断比len 9严格得多retry三次是我的经验值再多意义不大总线本身有问题时重试只会拖慢轮询重试之间的delay_ms(50)是给总线上其他节点让路不加的话连续重试有可能一直撞在别人发送的窗口上。CRC 比对那行把两个字节拼回 16 位再和函数返回值比注意拼的方向是低字节在前写反了永远不会通过。6.3 多节点轮询地址表驱动别写死循环挂多个探头时把地址和寄存器偏移做成一张表主循环按表遍历typedef struct { u8 addr; // 从机地址1~247 u16 reg; // 起始寄存器 u16 num; // 寄存器数量 u8 scale; // 缩放10 或 100 } NodeCfg; static const NodeCfg nodes[] { {0x01, 0x0006, 2, 10}, {0x02, 0x0006, 2, 10}, {0x03, 0x0006, 2, 10}, }; #define NODE_CNT (sizeof(nodes) / sizeof(nodes[0]))遍历时对每个节点调用Soil_Read成功就刷屏、失败就标红或累计错误次数连续失败超过阈值的节点做降级处理——比如打印一条告警然后跳过不要让一个掉线的探头卡住整轮轮询。轮询间隔按NODE_CNT × (帧时间 处理时间)估9600 下一个节点大约 30ms 往返10 个节点 300ms 左右主循环里再留 12 秒余量比较稳。另一个实用改造是把RS485_Receive_Data里的delay_ms(10)换成定时器超时判定让接收不再阻塞。做法不难进函数时启动一个 10ms 定时器中断里每收到一个字节就重置计时超时中断里置“帧结束”标志主循环轮询这个标志。改完之后整个循环的节奏就不再被固定的 10ms 卡死想跑快跑慢都行。最后说个我自己的习惯。这套代码我前后改过三版第一版是照着抄能跑就行第二版加了 CRC 和重试第三版才把阻塞收发改成非阻塞。后来每次接新探头我不再是先改代码再烧录而是先在电脑上用一段脚本把地址、寄存器、缩放系数全部摸清楚把确认过的参数填进nodes表再一次性烧进去。从那以后我每次接新设备都强制走一遍“电脑先验证、参数再落表”的流程省下的调试时间比多写几个函数划算得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站