资讯动态

嵌入式Linux下Modbus RTU传感器采集全攻略

发布时间:2026/9/11 8:34:02 来源:尧图企业网站定制
做嵌入式Linux开发这几年跟工业现场的设备打交道几乎是家常便饭。不管是传感器、变送器还是PLC、电表Modbus RTU始终是绕不开的一个协议。前阵子我接手一个项目要在ARM Linux主板上通过RS485总线下挂十几个温湿度、气压传感器把数据读回来做本地展示和上云。整个开发过程踩了不少坑也沉淀了一些经验今天就把这整套东西梳理一遍从串口配置、RTU报文构造到传感器数据解析和常见问题的排查一次性讲清楚。这篇文章适合正在做嵌入式Linux采集程序、准备用Modbus RTU对接传感器但还没完全理清思路的开发者。你不需要是Modbus专家只要会基本的C语言和Linux操作就能顺着文章把一套可用的主站采集代码搭起来。1. 项目起步为什么在嵌入式Linux上选了Modbus RTU1.1 需求场景与设备拓扑这次项目的硬件环境很典型主控板是一块ARM Cortex-A7的嵌入式Linux开发板运行着精简的buildroot系统板子上有UART串口外接了一路RS485转换芯片然后通过屏蔽双绞线把总线上的传感器串起来。传感器来自不同厂家内部寄存器地址定义各不相同但都支持标准的Modbus RTU协议。嵌入式Linux在工业采集场景里之所以常见是因为它比单片机有更丰富的软件生态。你可以在上面跑多线程、用文件系统做日志、对接云平台甚至嵌一个轻量级的Web服务用来远程查看数据。但如果只是做Modbus这类现场总线通信它的内核依然会落到“打开串口、读写字节流、解析报文”这三件事上。搞懂这三个环节无论是接Modbus还是将来接其他串口协议都会一通百通。1.2 为什么不是Modbus TCP而是RTUModbus家族里最常用的两个变种是RTU和TCP。TCP走以太网RTU走串行链路。很多刚开始接触的人会纠结选哪个我的建议很简单看物理链路能提供什么。如果设备已经在网线旁边或者你用4G/网口做远程采集那Modbus TCP确实更方便——它直接基于TCP 502端口不需要考虑串口参数报文里也没有CRC校验因为底层TCP已经保证了可靠性。但工业现场大量的低成本传感器和变送器默认只有RS485接口它们只支持RTU。RS485总线可以一条线上挂32个甚至更多设备传输距离在1200米左右这种多点、远距离、抗干扰能力强的特性决定了它依然是传感器接入的主力方式。这次的传感器清一色是RS485接口固件只实现了Modbus RTU从站协议所以方案没有悬念Linux主机做Modbus主站通过UART转RS485下挂多个从站。1.3 整体系统架构从软件角度看整个采集程序可以拆成下面几个层级层次职责关键技术点应用层业务逻辑、数据上报、显示定时轮询、线程调度Modbus协议层构造请求帧、解析响应帧、CRC校验功能码、寄存器地址、字节序串口层打开设备、配置波特率/数据位等、收发数据termios、select/poll超时物理层UART信号转RS485差分信号485方向控制、A/B接线这四层越往下越接近硬件。很多时候程序“莫名其妙收不到数据”问题并不在Modbus协议本身而是串口参数配错了或者485方向切换时机不对。所以串口这层值得花最多时间夯实。2. 串口底子要打牢Linux下的串口配置2.1 termios是绕不开的基础Linux下操作串口本质上就是操作一个设备文件比如/dev/ttymxc1、/dev/ttyS0。所有参数配置都是通过termios结构体和一组函数完成的。常用的几个配置项波特率常见9600、19200、115200使用cfsetispeed和cfsetospeed设置数据位通常8位对应CS8停止位通常1位1.5位和2位在Modbus RTU里极少见校验位无校验(None)、偶校验(Even)、奇校验(Odd)Modbus RTU默认无校验流控一般关闭硬件流控CRTSCTS、软件流控IXON/IXOFF需要特别注意一点在Linux中波特率是输入和输出分开设置的虽然工业上几乎都是收发一致但如果你只设置了输入波特率忘了输出也可能出现“只能收不能发”或者“只能发不能收”的怪现象。2.2 串口初始化完整代码从我实际项目里摘出来的串口初始化函数已经精简成可以直接套用的版本#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include termios.h #include errno.h int uart_open(const char *dev, int baudrate) { int fd open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open serial port failed); return -1; } struct termios opt; memset(opt, 0, sizeof(opt)); /* 获取当前串口配置 */ tcgetattr(fd, opt); /* 设置输入输出波特率 */ speed_t speed; switch (baudrate) { case 9600: speed B9600; break; case 19200: speed B19200; break; case 38400: speed B38400; break; case 115200: speed B115200; break; default: speed B9600; break; } cfsetispeed(opt, speed); cfsetospeed(opt, speed); /* 8数据位无校验1停止位 */ opt.c_cflag ~PARENB; /* 无校验 */ opt.c_cflag ~CSTOPB; /* 1位停止位 */ opt.c_cflag ~CSIZE; opt.c_cflag | CS8; /* 8位数据 */ /* 关闭流控 */ opt.c_cflag ~CRTSCTS; opt.c_iflag ~(IXON | IXOFF | IXANY); /* 本地连接使能接收 */ opt.c_cflag | (CLOCAL | CREAD); /* 设置为原始模式避免特殊字符处理 */ opt.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); opt.c_oflag ~OPOST; /* 配置生效 */ tcsetattr(fd, TCSANOW, opt); /* 清空缓冲区 */ tcflush(fd, TCIOFLUSH); return fd; }这段代码看起来简单但有几个细节需要注意。O_NDELAY表示打开串口时不等待DCD信号避免有些开发板串口在没有接设备时打开失败。CLOCAL保证程序不会因为远端掉线而挂起CREAD使能接收。关闭ICANON这一点特别重要。如果忘掉串口会进入行模式读数据要等到换行符才返回而Modbus RTU报文是二进制帧里面可能根本没有换行符收数据就会永远等不到头。我见过很多新手在这个地方卡住排查半天发现是终端模式导致的。2.3 RS485方向控制一根线上的收与发RS485是半双工通信同一时刻只能发送或接收。所以从“发送模式”切换到“接收模式”需要控制DE/RE引脚。常见的控制方式有两种。第一种是硬件自动流控有些USB转485模块内部自动处理方向但很多嵌入式板载RS485电路需要用RTS引脚来控制方向这时需要在termios里开启RTS流控让驱动在发送时自动拉高RTS。不过这种方式在部分平台上有延迟容易出现发完立刻切接收时导致最后一个字节被自己吃掉。第二种是用普通GPIO直接控制方向这也是我在项目里采用的方式道理很简单发送前把GPIO拉高发完再拉低。代码如下void rs485_set_dir(int fd, int gpio_fd, int tx_enable) { if (gpio_fd 0) return; if (tx_enable) { write(gpio_fd, 1, 1); } else { write(gpio_fd, 0, 1); } }注意发送完数据后不能立刻切回接收。串口发送是字节移位的过程write函数返回只代表数据拷贝到内核缓冲区不代表已经全部从TX引脚发出去了。通常需要在写完数据后延时1到3毫秒再切方向。如果时序太紧最后几个字节会丢失或者收到自己的回显造成干扰。这里补充一个计算参考9600波特率下传输1字节大约1.04ms如果发8字节最好等10ms再切接收115200波特率下可以适当地缩短但保险起见我一般统一用2ms打底再根据实际示波器测量结果微调。2.4 串口参数速查表参数Modbus RTU常用值配置要点波特率9600/115200主机从机必须一致数据位8不能配成7位否则无法传输二进制数据停止位1常见1位部分设备要求2位校验位None/Even站号扫描时要确认从站实际校验方式缓冲区无需额外设置用tcflush清空残留数据配置串口时最容易遗漏的是每个串口设备出厂默认参数不一样。生产环境里遇到过从站设备默认9600,8,N,1但另一批设备默认19200,8,E,1的情况。所以写代码前一定先看设备手册或者用一个串口调试工具先探一下。3. Modbus RTU协议拆解与主站实现3.1 报文格式每个字节的用途Modbus RTU的报文格式非常紧凑一条完整的请求帧长这样从站地址功能码数据域CRC16校验1字节1字节N字节2字节低字节在前从站地址取值范围1到2470通常作为广播地址。功能码告诉从站要做什么比如03是读保持寄存器04是读输入寄存器。数据域根据功能码不同而变化读操作一般是“起始寄存器地址 寄存器数量”。CRC16是对除了CRC本身之外的所有字节计算出来的校验值低字节在前高字节在后。从站返回的响应帧格式也类似但数据域最前面多了一个“字节数”字段后面紧跟寄存器数据。比如读2个寄存器从站返回的帧一般是地址、功能码、字节数(04)、数据高字节、数据低字节、数据高字节、数据低字节、CRC低、CRC高。3.2 常用功能码与使用场景Modbus功能码很多但传感器采集场景下用到的主要是以下几个功能码命令含义适用场景0x01读线圈读取开关量输出0x02读离散输入读取开关量输入0x03读保持寄存器读取可读写寄存器传感器校准值常在此0x04读输入寄存器读取只读寄存器温湿度、压力等实时值常在此0x06写单个寄存器写校准参数、修改从站地址0x10写多个寄存器批量设置参数我的经验是先看传感器手册里的寄存器表。很多温湿度变送器把测量值放在输入寄存器里用04功能码读最合适但有些设备把出厂地址和波特率参数放在保持寄存器里想改参数就得用03和06。3.3 CRC16计算原理与实现CRC校验是Modbus RTU区别于Modbus ASCII的重要一点它的作用是确保数据在RS485线缆上传输时没有被干扰。计算基于多项式0xA001本质是对数据帧按位进行异或和移位。工程上很少按位去算都是查表法速度快。下面是我一直用的查表实现static const unsigned char crc_hi_table[] { 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, /* ... 完整表可参考标准Modbus CRC查表码 ... */ }; static const unsigned char crc_lo_table[] { 0x00, 0xC0, 0x01, 0xC1, 0x02, 0xC2, 0x03, 0xC3, /* ... 完整表可参考标准Modbus CRC查表码 ... */ }; unsigned short modbus_crc16(unsigned char *buf, int len) { unsigned char crc_hi 0xFF; unsigned char crc_lo 0xFF; unsigned int i; for (i 0; i len; i) { unsigned int idx crc_hi ^ buf[i]; crc_hi crc_lo ^ crc_hi_table[idx]; crc_lo crc_lo_table[idx]; } return (crc_hi 8) | crc_lo; }如果你不想写完整查表也可以用按位计算版本性能在传感器采集场景下完全够用。重要的是理解CRC在报文里是低字节在前比如计算出来0x1234那么先发送0x34再发送0x12。我排查过很多“从站无响应”的问题最后发现是主机端CRC字节序发反了。先写CRC低字节再写高字节这条规则一定要记住。3.4 主站请求/响应流程封装一个健壮的Modbus RTU主站读写流程不能只是“发送请求、等待响应”还要考虑超时和重试。标准的轮询流程是构造请求帧计算CRC写入发送缓冲区清空串口接收缓冲区确保里面没有残留的旧数据拉高485方向发送整帧数据延时等待发送完成拉低485方向用select或poll等待接收数据设定超时时间校验接收帧的地址、功能码、数据长度和CRC校验不通过则重试超过重试次数就上报错误用select的好处是可以精确控制等待时间避免read函数无限期阻塞。下面是一个从站读请求的超时接收框架int wait_for_response(int fd, unsigned char *buf, int max_len, int timeout_ms) { fd_set fds; struct timeval tv; int len 0; FD_ZERO(fds); FD_SET(fd, fds); tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; int ret select(fd 1, fds, NULL, NULL, tv); if (ret 0) return -1; len read(fd, buf, max_len); return len; }超时时间怎么定Modbus RTU的标准规定从站收到请求后需要在3.5个字符时间内开始响应否则就是超时。实际工程中不敢卡这么死建议根据波特率动态计算9600波特率下给50ms115200波特率下给20ms。如果不确定固定设100ms也不会对传感器采集造成太大负担重试3次后放弃即可。4. 传感器数据读取实战从寄存器到浮点数4.1 案例设备与寄存器表这次项目里用得最多的一款传感器是RS485工业温湿度变送器。它内部有输入寄存器地址和内容如下寄存器地址内容数据类型说明0x0001温度值16位有符号整数实际温度 原始值 / 100x0002湿度值16位无符号整数实际湿度 原始值 / 100x0003温度值带符号32位浮点数IEEE754格式高字节在前0x0005湿度值32位浮点数IEEE754格式高字节在前这种寄存器表很常见一类设备提供16位整数除以10的格式另一类直接提供32位浮点数。如果你拿到的是后者就意味着要处理IEEE754的4字节转换。很多新手在这一步直接拿memcpy去转用错了字节序读出来就是天文数字。4.2 读寄存器请求帧构造实例假设从站地址是0x01用功能码0x03读取保持寄存器起始地址0x0001读2个寄存器。请求帧构造如下unsigned char request[8]; request[0] 0x01; /* 从站地址 */ request[1] 0x03; /* 功能码 */ request[2] 0x00; /* 起始寄存器地址高字节 */ request[3] 0x01; /* 起始寄存器地址低字节 */ request[4] 0x00; /* 寄存器数量高字节 */ request[5] 0x02; /* 寄存器数量低字节 */ unsigned short crc modbus_crc16(request, 6); request[6] crc 0xFF; /* CRC低字节 */ request[7] (crc 8) 0xFF; /* CRC高字节 */然后把8个字节通过串口发出去。如果一切正常从站会返回如下形式的响应0x01 0x03 0x04 0x00 0x1E 0x00 0x2B 0x?? 0x??其中0x04表示后面有4个字节数据即2个寄存器。0x001E换算成十进制是30因为温度分辨率是0.1所以实际温度是3.0度。0x002B换算成43实际湿度就是4.3%RH。这个例子说明数据到底怎么解释完全取决于设备手册协议只保证寄存器值能正确传回来。4.3 IEEE754浮点数转换别再搞错字节序另一款传感器直接输出IEEE754浮点数4个字节在寄存器里是连续存放的。比如返回的4个字节是0x41 0x45 0x70 0xA4代表的就是一个32位浮点数。转换的关键在于字节序。Modbus RTU规定寄存器高位在前即大端模式也就是第一个寄存器的高字节是数据的最高字节。C语言在小端平台上直接memcpy会把高低字节倒过来所以手动拼接是最稳妥的做法float ieee754_to_float(unsigned char *buf) { unsigned int bits 0; float result; bits | (unsigned int)buf[0] 24; bits | (unsigned int)buf[1] 16; bits | (unsigned int)buf[2] 8; bits | (unsigned int)buf[3]; memcpy(result, bits, sizeof(result)); return result; }如果你确定平台是大端直接memcpy也没问题但为了代码的可移植性手动拼接更保险。拼接完成后也可以按IEEE754标准把bits拆成符号位、指数位、尾数位来验证计算是否正确不过大部分时候直接memcpy就够了。还有一些传感器输出的是32位无符号整数把两段16位数据拼成一个int即可。这里要小心有符号扩展问题温度可能为负直接用short类型会比较安全。4.4 异常响应与数据健壮性处理Modbus RTU从站如果收到无法执行的请求会返回异常帧功能码最高位置1比如请求功能码0x03异常帧功能码就是0x83data域为异常码。异常码含义排查方向0x01非法功能码设备可能不支持该功能0x02非法数据地址寄存器地址超出范围0x03非法数据值寄存器数量参数错误0x04从站设备故障传感器可能内部异常处理异常帧时不能把0x83当成正常功能码后续处理应该先判断功能码的最高位。如果发现异常记录一下异常码方便之后对照手册查故障。另外RS485总线上数据碰撞、供电不稳、线路干扰都可能导致响应帧CRC错误。我的做法是校验失败时丢弃这一帧连续3次失败才报对应从站离线避免把瞬时干扰误判成设备故障。每个从站可以维护一个连续失败计数恢复成功后清零。这样做的好处是上层业务不会因为一次瞬间干扰就误报整个传感器链路故障。5. 调试、踩坑与问题排查实录5.1 用Modbus Poll之类的工具辅助调试在没有业务程序介入的情况下先用PC上的调试工具确认传感器本身工作正常是省时省力的做法。Windows下常用的Modbus Poll/Modbus Scan工具可以手动填从站地址、功能码、寄存器起始地址和数量直接看到返回的寄存器值。我的调试顺序一般是这样的先用USB转RS485把传感器接到PC打开Modbus Poll设置串口参数、从站地址、功能码读寄存器确认数据能正常返回如果读不到换Modbus Scan扫描整个地址段看看传感器实际地址是不是跟标称的不一致PC端确认没问题后再交叉编译板端程序这一步能帮你把“传感器问题”和“Linux程序问题”快速切分开。很多次我以为传感器坏了结果用Modbus Poll一扫才发现是出厂地址不是1而是12。板端没有图形界面的时候可以用一个简单的十六进制dump工具打印发送和接收的原始字节方便对比协议帧。我给采集程序加了一个debug开关打开后每个请求和响应都以hex形式输出到日志文件排查问题非常高效。5.2 串口打不开、乱码的排查方向串口相关的故障大多逃不过下面这几种情况设备节点不存在。检查/dev下有没有对应节点确认设备树里UART是否被占用权限不够。普通用户访问不了串口需要加入dialout组或者用root运行波特率不一致。主机和从站配置不同会出现“能收到但全是乱码”校验位配置错误。可能表现为CRC校验一直失败信号线接反。RS485的A和B接反会导致完全无响应处理思路是先用回环测试排查硬件把开发板串口的TXD和RXD短接用程序发一串数据看能不能原样收回来。如果可以说明串口硬件通路没问题如果不可以说明驱动、设备树或引脚复用有问题。遇到乱码优先用示波器看波形没有示波器就多试几组波特率。之前遇到一批传感器实际波特率是9600但设备标签写的是19200用调试工具一连接才发现参数错位。这种坑很难从代码层面看出来只能靠工具去探。5.3 典型485故障单独测试正常连上总线就异常这个故障现象很经典传感器单独接USB转485的时候主站怎么读写都正常但把传感器挂到开发板RS485总线上程序就时不时超时或者读到CRC错误的数据。我排查这类问题时发现最容易被忽略的原因有三个。第一个是共地问题。RS485虽然是差分信号但不同设备的参考地如果不一致共模电压超过芯片承受范围通信就会不稳定。解决方案是把所有设备的工作电源地连在一起。第二个是终端电阻问题。RS485总线两端需要并联120欧姆终端电阻如果线缆较长或挂载设备较多没有终端电阻会导致信号反射波形失真。但注意不要每个设备都加120欧那样会拉低差分信号反而通信更差。第三个是设备地址冲突。总线上有两个从站地址相同会导致它们同时响应主机收到拼接在一起的乱帧。用Modbus Scan扫一遍总线就能发现地址冲突。项目里遇到最隐蔽的情况是某款传感器的485芯片内部偏置太弱总线空闲时A/B电平不稳定导致主站误认为有数据进来。解决办法是给主站端外加一块偏置电阻网络或者换一款带自动偏置的485收发芯片。5.4 轮询调度与线程模型别让采集程序卡死Modbus RTU是半双工主从协议主站是唯一发起通信的一方。最简单可靠的调度方式是单线程循环在一个while(1)里按顺序轮询所有从站每一轮完成后休眠一小段时间。但如果采集程序还承担了网络上报、UI刷新等任务单线程就会互相拖累。我的建议是采用生产者消费者模型一个采集线程负责串口收发和Modbus协议处理采集线程把解析好的数据放入共享缓冲区加互斥锁保护网络上报线程定时读取缓冲区数据并发到服务器上层的网络抖动不会影响串口采集串口等待超时也不会阻塞网络上报。这种模型在Linux下很常见实现起来也不复杂。需要注意Modbus协议的请求和响应是严格的先后关系不能让多个线程同时往串口里写数据。如果需要支持多个任务同时读取传感器应该在Modbus层做统一的“请求队列”而不是各自直接操作串口。否则两个线程同时发请求从站会收到拼帧响应自然对不上。线程里还有个常见坑是使用日志打印过多导致调度延迟。实际项目里我建议在实时性要求高的轮询循环里少用printf把日志写到内存缓冲由另一个线程批量落盘。调试完毕后再降低日志级别。6. 从能跑到好用工程化经验补充6.1 参数配置化别把地址写死在代码里一台嵌入式设备可能根据现场需要挂不同数量的传感器从站地址也可能需要改。如果全部写死在代码里每改一次都要重新编译现场维护会很痛苦。我习惯用JSON或简单的ini文件保存设备配置程序启动时读取。比如记录每个从站的名称、地址、功能码、寄存器起始地址、读取长度、数据解析类型整数/浮点/带符号然后根据配置动态构造请求帧。这样现场改一个从站地址只需要改配置文件不用改代码。{ devices: [ { name: temperature_sensor_1, slave_addr: 1, function: 4, start_reg: 0, reg_count: 2, data_type: int16_div10 } ] }这个设计在前期的配置成本略高但从长期维护角度看非常值得。尤其是传感器型号多、寄存器表五花八门的时候一套配置驱动的Modbus采集框架能省下大量重复代码。6.2 数据质量与异常告警传感器数据读回来了不代表就能直接用于业务。项目里我加了三个基本的数据质量检查范围检查温度在-40到80度、湿度在0到100%RH超出就判为异常变化率检查相邻两次采样值跳变超过阈值比如1秒内湿度跳变30%大概率是噪声或设备异常连续性检查连续N次读不到或CRC错误判定设备离线异常数据不要删除而是打上标志位传给上层让上层决定是报警还是忽略。这样即使某个传感器瞬时故障也不会影响整个系统的数据完整性。还有一个容易被忽略的点Modbus从站的寄存器数据不一定实时更新。某些传感器内部采样周期是1到2秒你即使每100ms读一次拿到的还是旧值。读得太频繁反而浪费总线带宽。设计轮询周期时一定要参考设备的采样周期通常1秒轮询一次就够了。6.3 后续扩展从RTU到TCP网关如果项目后续要求把数据转发到局域网或云端可以考虑在Linux主机上实现一个Modbus RTU转Modbus TCP的网关。核心思路是监听502端口解析TCP主站发来的Modbus请求把它转换成RTU帧下发给串口从站再把响应转成TCP包回传。这个扩展需要处理并发连接但Modbus TCP本身对同一从站的并发访问要加锁否则串口侧会乱。基于上面已有的RTU主站框架扩展并不是难事。我在这个项目上最大的体会是Modbus RTU本身不算复杂难的是把它放到真实的工业环境里跑得稳、跑得久。串口参数、485方向控制、CRC字节序、浮点转换、超时重试每一个看起来不起眼的细节都可能决定整个采集链路是否可靠。希望这篇文章能帮你少走几步弯路如果按照上面的步骤搭建你也能在嵌入式Linux上快速完成一套Modbus RTU传感器采集程序。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价