news 2026/9/11 8:34:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux下Modbus RTU传感器采集全攻略

做嵌入式Linux开发这几年,跟工业现场的设备打交道几乎是家常便饭。不管是传感器、变送器,还是PLC、电表,Modbus RTU始终是绕不开的一个协议。前阵子我接手一个项目,要在ARM Linux主板上通过RS485总线下挂十几个温湿度、气压传感器,把数据读回来做本地展示和上云。整个开发过程踩了不少坑,也沉淀了一些经验,今天就把这整套东西梳理一遍,从串口配置、RTU报文构造,到传感器数据解析和常见问题的排查,一次性讲清楚。

这篇文章适合正在做嵌入式Linux采集程序、准备用Modbus RTU对接传感器但还没完全理清思路的开发者。你不需要是Modbus专家,只要会基本的C语言和Linux操作,就能顺着文章把一套可用的主站采集代码搭起来。

1. 项目起步:为什么在嵌入式Linux上选了Modbus RTU

1.1 需求场景与设备拓扑

这次项目的硬件环境很典型:主控板是一块ARM Cortex-A7的嵌入式Linux开发板,运行着精简的buildroot系统,板子上有UART串口,外接了一路RS485转换芯片,然后通过屏蔽双绞线把总线上的传感器串起来。传感器来自不同厂家,内部寄存器地址定义各不相同,但都支持标准的Modbus RTU协议。

嵌入式Linux在工业采集场景里之所以常见,是因为它比单片机有更丰富的软件生态。你可以在上面跑多线程、用文件系统做日志、对接云平台,甚至嵌一个轻量级的Web服务用来远程查看数据。但如果只是做Modbus这类现场总线通信,它的内核依然会落到“打开串口、读写字节流、解析报文”这三件事上。搞懂这三个环节,无论是接Modbus还是将来接其他串口协议,都会一通百通。

1.2 为什么不是Modbus TCP,而是RTU

Modbus家族里最常用的两个变种是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,使用cfsetispeedcfsetospeed设置
  • 数据位:通常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到247,0通常作为广播地址。功能码告诉从站要做什么,比如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主站读写流程,不能只是“发送请求、等待响应”,还要考虑超时和重试。

标准的轮询流程是:

  1. 构造请求帧,计算CRC,写入发送缓冲区
  2. 清空串口接收缓冲区,确保里面没有残留的旧数据
  3. 拉高485方向,发送整帧数据
  4. 延时等待发送完成,拉低485方向
  5. 用select或poll等待接收数据,设定超时时间
  6. 校验接收帧的地址、功能码、数据长度和CRC
  7. 校验不通过则重试,超过重试次数就上报错误

用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波特率下给50ms,115200波特率下给20ms。如果不确定,固定设100ms也不会对传感器采集造成太大负担,重试3次后放弃即可。

4. 传感器数据读取实战:从寄存器到浮点数

4.1 案例设备与寄存器表

这次项目里用得最多的一款传感器是RS485工业温湿度变送器。它内部有输入寄存器,地址和内容如下:

寄存器地址内容数据类型说明
0x0001温度值16位有符号整数实际温度 = 原始值 / 10
0x0002湿度值16位无符号整数实际湿度 = 原始值 / 10
0x0003温度值带符号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,异常帧功能码就是0x83,data域为异常码。

异常码含义排查方向
0x01非法功能码设备可能不支持该功能
0x02非法数据地址寄存器地址超出范围
0x03非法数据值寄存器数量参数错误
0x04从站设备故障传感器可能内部异常

处理异常帧时,不能把0x83当成正常功能码后续处理,应该先判断功能码的最高位。如果发现异常,记录一下异常码,方便之后对照手册查故障。

另外,RS485总线上数据碰撞、供电不稳、线路干扰都可能导致响应帧CRC错误。我的做法是:校验失败时丢弃这一帧,连续3次失败才报对应从站离线,避免把瞬时干扰误判成设备故障。每个从站可以维护一个连续失败计数,恢复成功后清零。这样做的好处是上层业务不会因为一次瞬间干扰就误报整个传感器链路故障。

5. 调试、踩坑与问题排查实录

5.1 用Modbus Poll之类的工具辅助调试

在没有业务程序介入的情况下,先用PC上的调试工具确认传感器本身工作正常,是省时省力的做法。Windows下常用的Modbus Poll/Modbus Scan工具,可以手动填从站地址、功能码、寄存器起始地址和数量,直接看到返回的寄存器值。

我的调试顺序一般是这样的:

  1. 先用USB转RS485把传感器接到PC
  2. 打开Modbus Poll,设置串口参数、从站地址、功能码
  3. 读寄存器,确认数据能正常返回
  4. 如果读不到,换Modbus Scan扫描整个地址段,看看传感器实际地址是不是跟标称的不一致
  5. 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传感器采集程序。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 8:31:13

GEO/AEO时代重写Schema Markup:WordPress与Shopify实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 8:29:32

大文件断点续传上传插件实战:切片、哈希与Web Worker

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 8:26:32

如何彻底卸载数字人工具HeyGem.ai:三层清理法一次讲清

如何彻底卸载数字人工具HeyGem.ai&#xff1a;三层清理法一次讲清 【免费下载链接】Duix-Avatar &#x1f680; Truly open-source AI avatar(digital human) toolkit for offline video generation and digital human cloning. 项目地址: https://gitcode.com/GitHub_Trendi…

作者头像 李华