news 2026/9/6 3:23:07

嵌入式Linux下Modbus RTU主站开发与RS485串口配置实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux下Modbus RTU主站开发与RS485串口配置实战

做嵌入式Linux开发,迟早会接到这样的需求:现场一堆RS485接口的传感器,温湿度、压力、液位之类,要接到板子上,用Modbus RTU协议把它们的数据读回来。我刚做这个项目时,以为只是打开串口读几个字节那么简单,结果在串口配置、CRC校验、RS485方向切换上踩了一圈坑,才把数据稳定采集下来。这篇文章就是把这个过程完整盘一遍:从Linux串口配置,到Modbus RTU报文拆解,再到主站读写程序实现和现场联调排错,适合所有准备在嵌入式Linux上接Modbus从站设备的开发者。项目本身不复杂,但细节极多,任何一个环节不对,读回来的都是乱码或者干脆没响应。

1. 项目背景与整体方案

1.1 实际场景:一块板子要读一堆传感器

这类项目的典型场景是这样的:设备端是一块嵌入式Linux开发板,可能是i.MX6ULL、全志或者是瑞芯微的方案,系统跑着Linux,业务逻辑包括数据采集、本地存储、上云上报。传感器端则是现场部署的各种变送器,比如RS485接口的温湿度传感器、压力变送器、光照度传感器,厂家不同,协议不同,但绝大多数都支持Modbus RTU。

需求往往很直白:主控板作为Modbus主站,定时去读取每个从站传感器的寄存器值,把数据解析成物理量,再交给业务层。难点在于,嵌入式Linux环境下没有现成的工控组态软件,你需要自己写串口程序,自己构造Modbus报文,自己处理各种异常情况。看似简单,实际做起来才发现坑都在细节里。

1.2 为什么工业现场如此依赖Modbus RTU

其实有很多总线协议可以选,比如CAN、EtherCAT、Profibus,甚至直接上MQTT走网络,为什么还是要用Modbus RTU?原因是这套组合在成本、成熟度和普及度上几乎是无可替代的。

Modbus RTU走的是RS485物理层,两根线(A、B)半双工差分传输,抗干扰能力强,最远能到1200米左右,挂32个从站设备(加中继可以更多)。传感器本身只要有RS485接口,基本都带Modbus RTU协议支持,而且价格便宜,采购周期短。嵌入式Linux板卡自带UART,加一个MAX3485或者SP3485电平转换芯片,就能低成本接进RS485网络,不需要额外买网关。

对比一下:CAN总线虽然抗干扰更强,但传感器端支持CAN的很少,而且协议层还得自己定义;Modbus TCP走以太网,布线成本高,现场很多传感器并不带网口。所以在中小规模的数据采集场景里,嵌入式Linux + RS485 + Modbus RTU是最稳妥、最省钱的方案。

1.3 环境准备:硬件和软件都要心里有数

硬件方面,我这次用的是一块瑞芯微方案的Linux板卡,板载三路UART,其中一路被引出到RS485芯片,接口是端子排。如果你手头的板子没有板载RS485,也可以用USB转RS485的模块,比如FTDI芯片方案的,Linux下识别为/dev/ttyUSB0,效果一样。

软件方面,跨平台调试我用的是Modbus Poll(主站模拟)和Modbus Slave(从站模拟)这两个工具,写代码之前先用它们验证传感器是否正常。嵌入式Linux侧则自己写C程序直接操作串口设备节点,不依赖任何第三方库。当然也有libmodbus这样的现成库可以选,但手写一遍能让你真正理解协议细节,后面用库时也更清楚自己到底在调什么,所以我更推荐先裸写一遍再谈封装。

2. Linux串口配置:把UART调到能用的状态

2.1 串口设备节点与硬件确认

嵌入式Linux下,串口设备节点的命名规则和平台相关。IMX平台通常是/dev/ttymxc0/dev/ttymxc1,瑞芯微平台是/dev/ttyS0/dev/ttyS1,USB转串口则是/dev/ttyUSB0或者/dev/ttyACM0。拿到板子第一步,就是确认接的串口对应哪个节点。

最笨也最可靠的方法是看dmesg启动日志:dmesg | grep tty,或者直接ls -l /dev/tty*。如果是USB转串口设备,插拔前后对比一下设备列表,就能锁定节点。还有一种情况是板卡厂商在设备树里给串口配置了别名,比如aliases里把某个串口标成serial0,那它就可能额外出现一个/dev/serial0的软链接,实际指向的还是底层UART节点。

需要注意权限问题。大部分嵌入式Linux发行版为了省事,/dev/ttyS*的权限是root所有,普通用户无法打开,跑程序时要么用root,要么给设备节点加udev规则。调试阶段我建议直接chmod 666 /dev/ttyS0,先用起来,部署时再规范处理。

2.2 termios核心参数:波特率、数据位、停止位、校验位

Linux下串口配置的标准接口是termios,这套东西历史悠久,字段含义又多,新手很容易绕晕。但如果我们只是做Modbus RTU,需要关心的参数其实就那几个:波特率、数据位、停止位、校验位、原始模式、超时时间。

Modbus RTU最常见的帧格式是8数据位、1停止位、无校验(8N1),波特率常见有9600、19200、38400、115200。传感器出厂默认一般是9600或19200,具体看手册,不确定就用Modbus Poll逐个试。

配置代码的核心部分长这样:

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <termios.h> #include <string.h> #include <errno.h> int uart_open(const char *dev, int baud) { int fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd < 0) { perror("open"); return -1; } struct termios opt; memset(&opt, 0, sizeof(opt)); if (tcgetattr(fd, &opt) < 0) { perror("tcgetattr"); close(fd); return -1; } /* 设置为原始模式,避免tty层对数据的任何加工 */ cfmakeraw(&opt); speed_t speed; switch (baud) { case 9600: speed = B9600; break; case 19200: speed = B19200; break; case 38400: speed = B38400; break; case 115200: speed = B115200; break; default: speed = B9600; } cfsetispeed(&opt, speed); cfsetospeed(&opt, speed); /* 8N1: 8数据位、无校验、1停止位 */ opt.c_cflag &= ~PARENB; opt.c_cflag &= ~CSTOPB; opt.c_cflag &= ~CSIZE; opt.c_cflag |= CS8; opt.c_cflag |= CLOCAL | CREAD; /* 读超时设置,后面细说 */ opt.c_cc[VMIN] = 0; opt.c_cc[VTIME] = 10; tcsetattr(fd, TCSANOW, &opt); tcflush(fd, TCIOFLUSH); return fd; }

这段代码里最关键的是cfmakeraw,它把termios里所有行规程(line discipline)相关的东西全部关掉:关闭ECHO、关闭ICANON、关闭ISIG,不做回车换行转换,不做流控。Modbus RTU是纯二进制协议,任何一字节的数据被tty层修改都会直接导致报文错乱。如果你不用cfmakeraw,就要手动设置opt.c_lflag &= ~(ICANON | ECHO | ISIG),还得处理c_iflagc_oflag,容易漏。所以我的建议是:直接用cfmakeraw,简单可靠。

2.3 VMIN与VTIME:读超时是怎么工作的

串口读数据的阻塞行为由c_cc[VMIN]c_cc[VTIME]控制,这两个参数非常关键,尤其是在Modbus这种“发一帧、等一帧”的交互模型中。

VMIN表示读到多少个字节才返回,VTIME表示等待超时,单位是0.1秒。两者组合有几种典型情况:

  • VMIN=0, VTIME=0:完全非阻塞,没有数据立即返回0。
  • VMIN=1, VTIME=0:阻塞直到读到1个字节。
  • VMIN=0, VTIME=10:最多等1秒,超时返回0。这是最常用的组合,常用于Modbus主站读取响应。
  • VMIN=1, VTIME=10:读到1字节后启动计时器,后续每次有数据就刷新计时,间隔超过1秒才返回。

Modbus RTU模式下,从站响应一帧通常几毫秒到几十毫秒就发完了,主站发送请求后不能无限等,也不能读一字节就返回,否则会把一帧响应拆成多次读取,帧完整性无法保证。建议的做法是VMIN=1, VTIME=20,这样读到第一个字节后开始等,后续数据到达会刷新计时,帧间间隔超过2秒才会返回,整个响应帧基本都能被一次read完整读出。

不过要注意,VTIME在实际实现中是个近似值,不同内核版本和驱动实现会有偏差,所以正式代码里最好还是加上帧完整性判断:读到数据后用selectpoll继续等待一小段时间,确认没有新字节到达才算一帧结束。

2.4 RS485方向切换的两种做法

RS485是半双工总线,只能有一方在发数据。发送时要让发送使能(DE/RE)引脚拉高,发送完毕要拉低,否则会一直占着总线,影响其他设备通信。这个问题在嵌入式Linux上有两种解法。

第一种是硬件自动方向切换。现在很多RS485芯片都带自动方向功能,比如MAX13487,它会根据DI脚的电平自动控制收发方向,软件层面完全不用管。这是最省心的方案,我强烈推荐。如果板卡上用的是这种芯片,串口配置完全不用处理方向问题。

第二种是用IO口控制方向。芯片是MAX485这种需要手动控制DE引脚的,那就要在设备树里把某个GPIO配置为RS485方向脚,驱动里有对这个的支持,比如linux,rs485-enabled-at-boot-time这类属性。代码层面也可以用ioctl配合TIOCSRS485结构体配置内核RS485模式,内核会帮你自动切换方向:

#include <linux/serial.h> struct serial_rs485 rs485conf; memset(&rs485conf, 0, sizeof(rs485conf)); rs485conf.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND; rs485conf.delay_rts_before_send = 1; rs485conf.delay_rts_after_send = 1; ioctl(fd, TIOCSRS485, &rs485conf);

配置成功之后,写串口时内核会自行控制RTS引脚翻转,用户程序不用关心方向。但有个坑:不是所有串口驱动都支持TIOCSRS485,特别是USB转串口芯片,很多不支持。遇到这种情况,就只能用GPIO手动拉方向了,在write之前将GPIO置高,写完之后置低。代码上虽然麻烦点,但可控性最强,现场调试RS485通信异常时,用这种方案也最容易定位问题。

3. Modbus RTU协议拆解:帧结构、功能码与CRC

3.1 RTU报文到底长什么样

Modbus RTU的报文格式非常规整,一帧数据由四部分组成:从站地址、功能码、数据、CRC校验。从站地址1字节,功能码1字节,数据区长度可变,CRC是2字节校验码。帧与帧之间有至少3.5个字符时间的间隔,用于从站判断一帧的起始和结束。

一个典型的读取请求帧长这样:

地址功能码起始寄存器高位起始寄存器低位寄存器数量高位寄存器数量低位CRC低位CRC高位
0x010x030x000x010x000x020x950xCB

含义是:向地址为1的从站,发送功能码0x03(读保持寄存器),从寄存器地址0x0001开始,连续读2个寄存器。CRC是前面6个字节算出来的校验值。

如果正常,从站会返回类似这样的帧:

地址功能码字节数数据1高数据1低数据2高数据2低CRC低CRC高
0x010x030x040x000x640x000x0A0x??0x??

表示读回了两个寄存器,值分别是0x0064和0x000A。如果出错,从站返回的帧是:地址、功能码最高位置1、异常码、CRC,比如01 83 02 CRC,其中02表示非法数据地址。

3.2 常用功能码及应用场景

Modbus RTU功能码很多,但嵌入式采集场景真正高频用到的就这几个:

功能码名称用途
0x01读线圈读DO开关量,比如继电器状态
0x02读离散输入读DI开关量,比如按钮、限位开关
0x03读保持寄存器读可读写的寄存器,占绝大多数
0x04读输入寄存器读只读寄存器,很多传感器测量值在这里
0x06写单个寄存器设置参数,如修改从站地址
0x10写多个寄存器批量设置参数

实际经验里,温湿度传感器、压力变送器这类仪表,测量值大多放在输入寄存器(0x04)或者保持寄存器(0x03)里。先用Modbus Slave配合设备手册确认数据在哪个区域,再决定用哪个功能码。

3.3 CRC16-Modbus计算原理与代码实现

CRC是Modbus RTU里最容易写错的部分。它使用的是一种特定的CRC16算法,多项式是0xA001(即标准CRC-16/MODBUS,多项式0x8005反序后得到0xA001)。计算的初始值是0xFFFF,每个字节先和当前CRC异或,然后右移8次,每次判断最低位是否为1,是1就和0xA001异或,最后得到的CRC需要低字节在前发送。

直接贴代码:

static uint16_t modbus_crc(uint8_t *data, int len) { uint16_t crc = 0xFFFF; for (int i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; }

这个函数返回的CRC是主机字节序。发送时先发低字节再发高字节,比如计算结果是0xCB95,发送顺序是0x95, 0xCB。很多新手在这里出错,因为平时习惯了大端序,直接按memcpy发到串口,结果算出来的CRC对着Modbus Poll一比对总是不对,原因就是字节序反了。

CRC校验必须放在接收和发送两侧一起检查。发送侧算出来填到帧尾,接收侧重新算一遍,判断结果是否为0。我这里提供一个经验:接收端把整帧(包括收到的CRC)一起交给CRC函数,如果结果等于0,说明帧完整没有错误。这个判断方法不用单独拆帧尾的CRC字节,代码会简洁很多。

3.4 拿真实报文一步一步解析

假设现场有个RS485温湿度传感器,从站地址是0x01,波特率9600,8N1。根据手册,温度值存在保持寄存器0x0001,湿度值存在0x0002,都是16位有符号整数,实际值是寄存器值除以10。

要读温度和湿度两个寄存器,构造请求帧:

  • 地址:0x01
  • 功能码:0x03
  • 起始寄存器地址:0x0001
  • 寄存器数量:0x0002
  • 前6字节:01 03 00 01 00 02
  • CRC:用上面的函数计算,结果假设是95 CB

完整发送帧是:01 03 00 01 00 02 95 CB

从站可能返回:01 03 04 02 26 01 3A 2F 7E,逐个解析:

  • 01:从站地址
  • 03:功能码,和请求一致,表示正常响应
  • 04:数据字节数,2个寄存器共4字节
  • 02 26:寄存器1的值,即温度0x0226 = 550,除以10就是55.0度
  • 01 3A:寄存器2的值,即湿度0x013A = 314,除以10就是31.4%RH
  • 2F 7E:CRC

整个流程清晰地证明了Modbus RTU本质上就是“拼帧、发帧、收帧、拆帧”的循环,并没有任何神秘之处。真正的难点反而在串口层和工程化层。串口没配好,帧乱;CRC没做好,数据错;超时没处理好,程序卡死。每一项都值得单独踩一遍坑。

4. 主站读写程序实现:从零手写一个Modbus主站

4.1 整体架构:线程、队列与超时

嵌入式Linux上的Modbus主站程序,我建议不要全部塞在一个大循环里。轮询多个传感器时,如果某个从站没响应,read会阻塞,后面的设备也跟着等,整个采集周期被拉得很长。

合理的架构是两层:一个独立的采集线程专门负责串口读写和协议帧处理,一个业务层通过共享缓冲区获取最新数据。采集线程内部的逻辑用状态机来实现最清晰:

  1. 空闲:从待采集列表里取下一个从站设备。
  2. 发送:构造请求帧,写入串口。
  3. 等待响应:设置超时(比如500毫秒),阻塞读串口。
  4. 解析帧:校验地址、功能码、CRC,提取数据。
  5. 记录状态:成功则更新数据缓存,失败则记录错误次数。
  6. 回到空闲,采集下一个设备。

这种状态机和超时控制结合的方式,天然就避免了单个设备故障拖垮整个采集链路的问题。某个设备连续几次超时后,可以标记为离线,同时把它在轮询队列里的优先级降低,避免它一直占用总线。

4.2 读取寄存器的完整代码实现

下面是我实际项目里用的一段核心代码,去掉了平台相关的初始化细节,保留了完整的读寄存器逻辑:

#include <stdint.h> #include <unistd.h> #include <fcntl.h> #define FRAME_TIMEOUT_MS 500 static uint16_t modbus_crc(uint8_t *data, int len) { uint16_t crc = 0xFFFF; for (int i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; } int modbus_read_registers(int fd, uint8_t slave_addr, uint16_t start_reg, uint16_t reg_count, uint8_t func, uint16_t *values, int timeout_ms) { uint8_t tx_buf[8]; uint8_t rx_buf[256]; int ret; if (func != 0x03 && func != 0x04) return -1; tx_buf[0] = slave_addr; tx_buf[1] = func; tx_buf[2] = start_reg >> 8; tx_buf[3] = start_reg & 0xFF; tx_buf[4] = reg_count >> 8; tx_buf[5] = reg_count & 0xFF; uint16_t crc = modbus_crc(tx_buf, 6); tx_buf[6] = crc & 0xFF; tx_buf[7] = crc >> 8; ret = write(fd, tx_buf, 8); if (ret != 8) return -1; /* 这里用select做超时控制,确保读不到响应时能及时返回 */ fd_set rfds; struct timeval tv; FD_ZERO(&rfds); FD_SET(fd, &rfds); tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; ret = select(fd + 1, &rfds, NULL, NULL, &tv); if (ret <= 0) return -2; /* 超时或select被中断 */ ret = read(fd, rx_buf, sizeof(rx_buf)); if (ret < 5) /* 最短合法响应帧至少5字节 */ return -3; /* 校验从站地址是否匹配 */ if (rx_buf[0] != slave_addr) return -4; /* 异常码响应判断 */ if (rx_buf[1] == (func | 0x80)) return -5; /* 功能码不匹配 */ if (rx_buf[1] != func) return -6; int byte_count = rx_buf[2]; if (byte_count != reg_count * 2) return -7; /* CRC校验 */ uint16_t calc_crc = modbus_crc(rx_buf, ret - 2); uint16_t recv_crc = rx_buf[ret - 2] | (rx_buf[ret - 1] << 8); if (calc_crc != recv_crc) return -8; /* 解析数据,Modbus寄存器大端序 */ for (int i = 0; i < reg_count; i++) { values[i] = (rx_buf[3 + i * 2] << 8) | rx_buf[4 + i * 2]; } return reg_count; }

这个函数踩坑比较多的地方在于:单次select只读一次。如果从站的响应帧因为串口驱动缓存的原因被拆成了两段,第一次read可能只读到一部分。实际项目里我是在select返回后,再用一个小循环继续读,直到帧间超过一定时间没有新数据到达,再把所有字节拼起来解析。代码会变得复杂一些,我建议先跑通这个基础版本,再根据实际抓包结果决定要不要加帧重组逻辑。

4.3 多设备轮询与调度策略

当总线上挂了多个从站,轮询策略就需要考虑了。最简单的做法是维护一个设备链表,每个设备节点包含从站地址、寄存器起始地址、寄存器数量、上次采集时间、采集间隔、失败次数等信息。主循环每次只采集“到时间该采”的设备,采集完就处理下一个。

不同设备的采集频率可能不一样。比如温湿度传感器1秒采一次就够了,但一个高速的振动传感器可能需要100毫秒采一次。把采集间隔做成设备属性,轮询时检查时间戳,就能自然实现差异化调度,不需要复杂算法。

有个重要的经验:如果有设备连续失败,不要用很短的间隔去反复重试。我通常会做一个退避策略,失败一次后,把该设备的重试间隔从1秒逐渐拉长到30秒,直到它恢复通信,再把间隔降回来。这样可以避免故障设备不断占用总线,拖慢其他正常设备的采集周期。

总线容量也要心里有数。9600波特率下,一帧请求加响应大概需要10到20毫秒,挂10个设备,一轮全采大概需要200毫秒左右,对于大多数传感器采集需求完全够用。但如果要采集的设备非常多,或者要求毫秒级响应,就需要升级到115200波特率,或者拆成多条串口总线分担负载。

4.4 数据转换与业务对接

寄存器值读出来后,不能直接用。传感器返回的一般是原始整数,需要根据设备手册转换成物理量。常见转换方式有:

  • 直接除以10、100:很多温湿度、压力传感器这么干,比如寄存器值550表示55.0度。
  • IEEE 754浮点数:某些高端传感器用两个寄存器拼一个float,按大端序把4字节拼起来,直接按float指针解析。
  • 查表线性变换:精度要求高的场景,传感器手册会给一个y = kx + b的校准公式。

浮点数转换有一个经典陷阱:设备返回的数据是ABCD这样的4字节,有些设备是大端序(AB CD),有些是小端序(CD AB,寄存器内部反序),甚至有的设备是字序反(CD AB但每个字节又反)。我有一个项目遇到过,同一个厂家的不同批次传感器,浮点字节序居然不完全一样,最后只能加一个配置项让现场随意切换字节序。所以解析浮点数时,一定先用传感器厂家提供的上位机软件和设备手册核对原始字节流,不要想当然。

5. 联调实测与高频问题排查

5.1 实测记录:从乱码到稳定采集

我调这块板子时,第一次上电测试,程序报CRC错误,数据根本读不到。用逻辑分析仪抓波形,发现从站确实有响应,但响应帧里多了几个字节。查了一圈,问题出在VMINVTIME配置上,我用了VMIN=0, VTIME=10,结果read返回时只读到了响应帧的一部分,CRC当然对不上。改成VMIN=1, VTIME=20之后,问题立刻消失。

第二次遇到的是RS485方向切换问题。板卡用的是MAX485,需要GPIO方向控制,但设备树里没配置好,导致发送完成后方向引脚一直保持发送状态,从站发来的数据被自己的发送电路短路,接收端全是乱码。这个问题排查花了不少时间,后来用示波器同时看差分波形和GPIO电平才发现,GPIO在write返回后没有及时拉低。解决办法是write完之后立刻操作GPIO,不能让调度器有时间插入其他任务。如果用TIOCSRS485的自动方向功能,就不会有这个问题。

第三次是有两路传感器,分别接在不同UART上,但只有一路能正常读取。查下来是第二路UART在设备树里默认被禁用(status = "disabled"),需要在内核设备树里显式使能。这属于硬件资源启用的基础问题,但特别容易忽略,建议拿到新板子先检查设备树里所有要用到的串口节点状态。

5.2 常见问题速查表

现象可能原因解决方法
完全没有响应接线A/B反了交换RS485的A/B线
完全没有响应从站地址错误先用Modbus Slave或设备上位机确认地址
完全没有响应波特率、校验位不匹配逐个尝试常用波特率:9600、19200、38400、115200
读回来乱码tty层没配成raw模式cfmakeraw(&opt),关闭行规程转换
读回来乱码RS485方向没切换检查GPIO/TIOCSRS485配置
CRC总错字节序反了CRC低字节先发,高字节后发
CRC总错响应帧被拆分调整VMIN/VTIME,或做帧重组
单设备正常,多设备挂掉从站地址冲突给每个从站设唯一地址,避免用0作为从站地址
偶尔丢数据总线无终端电阻总线两端加120欧姆终端电阻
传感器无响应但Modbus Poll正常程序里没flush缓冲区发送前调用tcflush(fd, TCIOFLUSH)清掉残留数据

5.3 避坑经验:这些细节值得反复确认

第一个,tcflush在发送前必须做。串口缓冲区里可能残留上一次的响应帧或者杂讯,如果不清理,这些垃圾字节会被当成当前请求的响应,导致帧错位。我写过一版程序,write之后直接read,结果读回来的是上一次的报文,排查了好久才发现是缓冲区残留问题。发送前用tcflush(fd, TCIOFLUSH)清一次,发送后再tcdrain(fd)确保数据全部从缓冲区写出,这样收发时序才干净。

第二个,关于Modbus RTU的帧间隔。标准规定帧内字节间隔不能超过1.5个字符时间,帧与帧之间至少要3.5个字符时间。在Linux下,这个时序主要靠前一个write返回后立即发下一帧来控制,程序层面很容易满足。但如果你的程序在两次写之间做了大量计算、打印日志、或者usleep了一段不小的延时,碰上有严格时序要求的从站,就会导致通信失败。所以写日志时要克制,尤其是不能在收发关键路径上打印大段调试信息,printf的开销足以破坏帧间隔。

第三个,数据处理要用缓冲区环形队列,不要让业务层直接阻塞在串口上。嵌入式Linux虽然不像裸机那样对实时性要求苛刻,但如果业务线程在等待串口数据时占用锁,会让整个系统体验很差。实际项目里我习惯把Modbus采集线程和数据使用方彻底解耦,采集线程只管把结果写入共享内存,业务线程读到的是最近一次成功的快照,两边加一个简单的互斥锁保护即可,简单实用。

最后再分享一个小技巧。现场调试时,先不要急着写程序,而是用PC机上的Modbus Poll软件直连传感器,确认地址、寄存器、波特率这些基本信息全对,再去碰嵌入式Linux侧的代码。这样可以快速区分问题是出在传感器配置上,还是出在代码逻辑上。我见过太多同事在代码里反复调CRC、调超时,结果最后发现传感器默认地址不是手册上写的“1”,而是“247”,属于出厂设置被改过的特殊型号。先把硬件和协议验证清楚,再写代码,能省掉大部分无谓的排错时间。

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

Obsidian 2026 插件推荐:同步、AI 与版本控制一体化工作流

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

作者头像 李华
网站建设 2026/9/6 3:21:48

车载CAN通信与UDS诊断协议开发:从底层硬件到上层应用全解析

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

作者头像 李华
网站建设 2026/9/6 3:15:56

2026靠谱白转黑维生素品牌推荐

选购白转黑类维生素产品时首先要明确三个核心避坑红线&#xff0c;第一是宣称7天快速白发转黑的产品绝对不要选&#xff0c;营养素补充剂遵循人体正常代谢规律&#xff0c;不可能实现超常规的激进功效&#xff1b;第二是没有国家营养素补充剂备案号的产品要直接排除&#xff0c…

作者头像 李华
网站建设 2026/9/6 3:15:18

GEO 落地五步法:让品牌成为 AI 的“标准答案“

GEO 落地五步法&#xff1a;让品牌成为 AI 的"标准答案"接触过的企业里&#xff0c;最常见的误区是把 GEO 理解成"换个地方发软文"。于是内容照旧、渠道照旧&#xff0c;只是标题里多了几个问句&#xff0c;三个月后一查&#xff0c;AI 里还是查无此品牌。…

作者头像 李华
网站建设 2026/9/6 3:13:53

FPGA跑通100G UDP协议栈:移植适配与上板测试全记录

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

作者头像 李华
网站建设 2026/9/6 3:08:37

【免费数据】2019-2022年中国10米分辨率大豆分布栅格数据

大豆是我国核心粮油经济作物&#xff0c;国内自产供给缺口较大、对外进口依存度高&#xff0c;精确获取多年全国性大豆种植区空间分布信息对保障粮食安全和制定贸易政策具有重要意义。今日分享的是2019-2022年中国10米分辨率大豆分布栅格数据! 该数据集简称ChinaSoybean10&…

作者头像 李华