做嵌入式Linux开发的人应该都有过这种体验硬件平台换得比手机还勤串口设备节点名字却各有各的脾气。前阵子做工业网关ARM板卡上跑Linux需要通过RS485总线轮询挂载的温湿度传感器通信协议是标准的Modbus RTU。硬件接线半小时搞定软件却断断续续磨了两天多。回头复盘真正卡住我的不是Modbus协议本身有多难而是串口配置的细枝末节、RS485方向切换的时序、CRC校验的字节顺序这些“地基问题”一个接一个冒出来。所以这篇文章决定把完整的开发链路重新走一遍从串口配置、RTU报文拆解、读写实现到现场排错经验一次性讲透希望能帮你少走几个我已经踩过的坑。无论你是刚接触嵌入式Linux的新手还是写了好几年MCU程序想往Linux上迁移的老手只要涉及串口类和Modbus类设备通信这篇文章都适用。我会尽量少讲空泛的理论多给能直接抄的代码和步骤同时把每个关键选择背后的原因说清楚。1. 串口通信的“地基”设备节点、termios与读写超时的正确姿势1.1 设备节点怎么认ttyS0、ttyUSB0、ttymxc0的区别嵌入式Linux下串口设备节点不是一个固定名字不同SoC和扩展方式差异很大。PC风格的x86板卡一般叫ttyS0、ttyS1NXP i.MX系列叫ttymxc0树莓派和部分全志平台叫ttyAMA0USB转串口芯片CH340、CP2102、FT232则统一挂在ttyUSB0或ttyACM0下面。这块有一个非常容易踩的坑你以为插上USB转串口就是ttyUSB0但如果系统里有多个USB转串口设备设备节点会根据内核枚举顺序动态分配重启之后可能变成ttyUSB1甚至ttyUSB2。所以正式项目里强烈建议通过udev规则绑定设备节点比如按芯片序列号或者USB端口路径固定成/dev/ttySensor之类的自定义名字。另外操作权限也常被忽略。很多板卡默认普通用户没有/dev/tty*的读写权限调试时要么用sudo要么把自己加到dialout组。否则代码里一切正常就是open失败很容易让人怀疑人生。# 查看当前已识别的串口节点 ls -l /dev/tty* # 查看串口设备详细信息 udevadm info --name/dev/ttyUSB0 --attribute-walk1.2 termios五大参数配置波特率、数据位、校验位、停止位、原始模式在Linux上配置串口本质上就是填充和修改termios结构体。新手最容易犯的错误是只设波特率不设置raw模式结果读上来的数据被内核的线路规程处理过换行符被转换、特殊字符被解释Modbus这种二进制帧根本没法用。先看一份可以直接用的配置函数#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 baud) { int fd open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open serial); return -1; } struct termios opt; memset(opt, 0, sizeof(opt)); tcgetattr(fd, opt); // 核心一步切换到原始模式 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; break; } cfsetispeed(opt, speed); cfsetospeed(opt, speed); // 数据位Modbus RTU固定用8位 opt.c_cflag ~CSIZE; opt.c_cflag | CS8; // 校验位无校验是最常见的Modbus配置 opt.c_cflag ~PARENB; // 禁止奇偶校验 opt.c_cflag ~PARODD; opt.c_iflag ~INPCK; // 不使能输入奇偶校验 // 停止位1位 opt.c_cflag ~CSTOPB; // 本地连接模式、使能接收避免端口占用或悬空时挂起 opt.c_cflag | (CLOCAL | CREAD); // 关闭软件流控 opt.c_iflag ~(IXON | IXOFF | IXANY); // 关闭硬件流控RS485半双工根本用不上RTS/CTS流控 opt.c_cflag ~CRTSCTS; tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, opt) ! 0) { perror(tcsetattr); close(fd); return -1; } return fd; }有人会问cfmakeraw到底做了什么它一次性把ICANON、ECHO、ISIG、IEXTEN等都关掉并把输入输出的换行转换、信号字符处理全部禁用。Modbus RTU是纯二进制协议0x0A、0x0D这类字节在文本模式下可能被转换或拦截所以原始模式是必须的没有讨价还价的余地。1.3 VMIN与VTIME串口读操作不阻塞或乱返回的根源配置完波特率和数据位很多人直接read(fd, buf, 256)发现read要么立刻返回0要么卡住不动。这个问题的根源就是VMIN和VTIME没有理解到位。VMIN表示read返回前最少需要读取的字节数VTIME表示等待数据到达的超时时间单位是0.1秒如果VMIN1、VTIME0read会一直阻塞直到收到1个字节适合等待完整的请求帧、边收边处理如果VMIN0、VTIME10read会等待1秒时间内有数据就返回已读到的字节数超时则返回0。对于Modbus RTU这种不定长帧我推荐把它设成VMIN0、VTIME5或10再加上自己的帧超时逻辑而不是完全依赖内核参数。实际调试中我遇到过一个问题从站响应比较慢VTIME设短了导致响应帧被截断read只返回了前半段数据后面的字节还留在内核缓冲区里。解决办法有两个思路一个是调大VTIME另一个是既然Modbus响应帧第一个字节是地址、第二个字节是功能码、第三个字节是数据字节数我们可以根据头3个字节动态算出剩余需要读的长度再用精确的超时去读剩余部分。这个在后面的响应解析部分还会细讲。2. Modbus RTU报文拆解寄存器模型、帧结构与CRC16计算原理2.1 四种寄存器对象从线圈到保持寄存器在写任何Modbus代码之前先得把“寄存器”这个概念搞明白。Modbus协议定义了四类数据对象很多传感器厂家手册里会说“温度保存在输入寄存器40001地址”你不能直接拿40001这个数字去拼帧得先搞清楚是哪类寄存器。对象类型位/字只读/读写功能码典型用途线圈Coil1位读写01读、05写单、15写多开关量输出离散输入Discrete Input1位只读02读开关量输入输入寄存器Input Register16位只读04读传感器采集值保持寄存器Holding Register16位读写03读、06写单、16写多参数配置、校准值温湿度传感器绝大部分把实时测量值放在输入寄存器里用04功能码读也有一部分比较老的传感器用03读保持寄存器。所以拿到设备手册第一件事是确认寄存器类型是什么、地址从几号开始、每个测量值是几个寄存器。这里有个历史坑很多设备手册用PLC的地址表示法比如“温度寄存器地址40001”这个“4”开头代表保持寄存器真正的协议地址是0000输入寄存器用“3”开头协议地址也是0000开始的偏移。如果用40001去拼帧实际地址偏差可能直接导致设备返回异常码02非法数据地址。正确做法是把4或3前缀去掉剩下的数值换算成十六进制。2.2 RTU报文结构地址、功能码、数据、CRC的排列规则RTU模式下一条完整报文由四部分组成。我以读取保持寄存器为例画一个直观的拆分请求帧主机→从站字段长度值示例说明从站地址1字节0x01要访问的从站编号功能码1字节0x03读保持寄存器起始地址高字节1字节0x00起始寄存器地址高位起始地址低字节1字节0x00起始寄存器地址低位寄存器数量高字节1字节0x00数量高位寄存器数量低字节1字节0x02数量低位读2个寄存器CRC低字节1字节0xC4前6字节CRC结果低8位CRC高字节1字节0x0B前6字节CRC结果高8位注意多字节数据在Modbus RTU里统一是高字节在前大端这是协议明确规定也是和许多人的习惯思维不同的地方——寄存器地址、寄存器数量、寄存器数据值统统都是高位在前。如果你写代码时用memcpy直接填充uint16_t变量的地址在x86这类小端架构上就会把字节序搞反。响应帧从站→主机字段长度值示例说明从站地址1字节0x01自己的地址功能码1字节0x03原样返回请求功能码字节数1字节0x04后面数据的总字节数2个寄存器4字节寄存器数据N字节0x00 0xAA 0x00 0xBB每个寄存器2字节高字节在前CRC低字节1字节...前面所有字节的CRC如果从站收到请求但无法执行比如寄存器地址越界它会返回异常响应功能码最高位置1比如03变成83后面跟一个异常码。这个后面细讲。2.3 CRC16-Modbus的循环冗余计算查表法与逐位法CRC16-Modbus是整个RTU协议里最容易被写错的部分但它实际上是一个很简单的东西。它的算法本质是CRC初值0xFFFF每次取一个字节和CRC的低8位异或然后右移8次遇到最低位是1就异或多项式0xA001。下面是两种常用实现。先看逐位法逻辑清晰适合理解和调试uint16_t modbus_crc16(uint8_t *buf, int len) { uint16_t crc 0xFFFF; for (int i 0; i len; i) { crc ^ buf[i]; for (int j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }如果用在每帧几百字节的场合逐位法性能也完全够用。但Modbus设备可能每秒轮询几十个从站每个从站都要算一次CRC这时候用查表法更省CPUstatic const uint8_t crc_hi_table[] { 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, // ... 完整256项 };查表法代码网上很多我不重复贴完整表。实际工程里我更推荐用查表法因为嵌入式Linux哪怕性能不强轮询频率高了以后逐位法的开销还是能感知到的。CRC最容易翻车的点有三个第一是字节序。CRC计算结果的低字节要排在帧的前面高字节排在后面。也就是CRC16结果0x0BC4在帧里要写0xC4、0x0B。写反了从站会直接丢弃。第二是计算范围。CRC只覆盖从站地址、功能码和数据部分不包括CRC本身。很多人顺手把整个缓冲区都算进去了永远配对不上。第三是响应帧也要校验CRC。不少人只关心发送帧CRC对不对忽略了接收帧。从站如果配置错误或通信链路受到干扰返回的CRC也可能错需要校验后再使用数据。我习惯的做法是uint16_t recv_crc buf[len - 2] | (buf[len - 1] 8); uint16_t calc_crc modbus_crc16(buf, len - 2); if (recv_crc ! calc_crc) { // 丢弃该帧 }3. 用C语言完整实现03功能码读取从请求构造到响应解析3.1 构造读取请求帧地址、寄存器、数量与CRC拼装假设从站地址是0x01要读取起始地址0x0000的2个保持寄存器也就是获取4字节的测量数据。完整的发送代码可以这样封装int modbus_read_registers(int fd, uint8_t slave, uint16_t start_addr, uint16_t count) { uint8_t req[8]; req[0] slave; req[1] 0x03; // 功能码 req[2] (start_addr 8) 0xFF; // 地址高字节 req[3] start_addr 0xFF; // 地址低字节 req[4] (count 8) 0xFF; // 数量高字节 req[5] count 0xFF; // 数量低字节 uint16_t crc modbus_crc16(req, 6); req[6] crc 0xFF; // CRC低字节在前 req[7] (crc 8) 0xFF; // RS485半双工发送前需要置发送方向 uart_set_rs485_dir(fd, 1); int ret write(fd, req, 8); // 确保所有字节都已经从驱动发出而不是还在用户态/内核缓冲区 tcdrain(fd); // 发送完成后切回接收方向 uart_set_rs485_dir(fd, 0); if (ret ! 8) { return -1; } return 0; }这里为什么一定要tcdrain加上RS485半双工场景在一块说。write返回8只能说明数据放进了内核缓冲区不代表物理线路上已经发完。如果在数据还没发完时就切换RS485方向最后一两个字节会被截断。tcdrain会阻塞等待发送完成之后切方向才安全。3.2 解析响应帧正常应答、异常应答与超时处理发送完请求主站要等待从站响应。Modbus RTU是严格的请求-响应模型同一时间总线上只能有一个主机在发起请求从站之间不会主动通信。接收解析我建议这样设计int modbus_read_response(int fd, uint8_t slave, uint8_t *data, int max_len, int timeout_ms) { uint8_t buf[256]; int len 0; int ret; // 设置轮询非阻塞读取VMTIME作为兜底 struct timeval tv; fd_set fds; FD_ZERO(fds); FD_SET(fd, fds); tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; // 用一个循环把可能分片到达的响应拼起来 while (1) { ret select(fd 1, fds, NULL, NULL, tv); if (ret 0) { return -1; // 超时 } int n read(fd, buf[len], sizeof(buf) - len); if (n 0) { len n; // 帧长至少5字节地址功能码字节数数据CRC2 if (len 5 len 3 buf[2] 2) { break; } } } // 校验从站地址 if (buf[0] ! slave) { return -2; // 地址不匹配可能是总线冲突 } // 判断异常响应 if (buf[1] 0x80) { return -0x100 buf[2]; // 返回负数异常码 } // 校验CRC uint16_t recv_crc buf[len - 2] | (buf[len - 1] 8); uint16_t calc_crc modbus_crc16(buf, len - 2); if (recv_crc ! calc_crc) { return -3; // CRC错误 } // 复制正常响应数据 int data_len buf[2]; if (data_len max_len) { data_len max_len; } memcpy(data, buf[3], data_len); return data_len; }这段代码的核心逻辑是通过响应帧第三字节里的数据长度先判断这一帧是否收完整了避免只读到半个帧就去解析。如果一帧里既有03功能码响应又混入了其他字节说明总线时序有问题严格来说还要做帧边界对齐但现场环境里先把握好“完整接收地址校验CRC校验”这三道防线已经能挡掉绝大多数问题。3.3 数据解析的字节序陷阱16位整数与IEEE754浮点从站返回的裸数据是字节流比如温度可能占2个寄存器4个字节。最常见的解析错误是把4字节当成float直接memory copy——在小端CPU上必须先把字节按大端组装成uint32_t再转float。float regs_to_float(uint16_t reg_hi, uint16_t reg_lo) { uint32_t tmp ((uint32_t)reg_hi 16) | reg_lo; float f; memcpy(f, tmp, sizeof(f)); return f; }举例说明从站返回4字节0x42 0x20 0x00 0x00高字寄存器是0x4220低字寄存器是0x0000。按上面的组合方式得到浮点数40.0。如果搞反了先组合低字得到的是一个极小或异常的数值温度变成几百上千一查一个准。还有一个更隐蔽的坑不同厂商对32位浮点在两个寄存器里的排列顺序定义不同。有的“AB CD”是“高字在前”有的却是“低字在前”还有一些挂在保持寄存器里的32位整数甚至会把高低16位互换。所以拿到传感器手册一定要看它对寄存器数据段的说明不要只依据协议文档就下结论。我处理过的传感器里约七成是高字在前但确实有三成反着来。要补充的是很多气象站、环境监测传感器会返回类似0x00 0x00 0x41 0x7A这样的IEEE754单精度格式实际上还有个“Word Swap”问题某些设备为了兼容PLC习惯会把两个寄存器的顺序颠倒。排查办法很简单打印出收到的4个字节用浮点转换工具算一下再对照设备当前显示值立刻就能判断是哪种排列。4. RS485方向切换与多从站轮询现场不稳定问题的根源与对策4.1 RS485半双工收发切换RTS控制DE/RE的时序RS485是半双工总线同一时刻只能有一个方向的数据在传输。这就要求主机的收发器方向控制引脚DE/RE正确切换。很多USB转RS485模块自动切换但板载RS485芯片不一定带自动方向功能这时候就需要用GPIO或串口的RTS来控制。用软件控制RTS的经典做法是ioctlint uart_set_rs485_dir(int fd, int tx_enable) { int rts_flag TIOCM_RTS; if (tx_enable) { ioctl(fd, TIOCMBIS, rts_flag); // RTS拉高使能发送 } else { ioctl(fd, TIOCMBIC, rts_flag); // RTS拉低恢复接收 } return 0; }关键点在于RTS的硬件极性在不同板卡上不一定相同有的RS485收发器是DE接RTS高电平发送有的则反过来。如果发现发出的帧对端完全收不到先拿起万用表量一下A/B引脚或者用示波器看波形也可能是方向极性反了。Linux内核其实提供了更优雅的解决方案通过ioctl设置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);如果你的板卡内核和驱动支持TIOCSRS485强烈建议用它省去应用层切向量的各种麻烦。但要注意老内核或某些USB转串口芯片驱动对这个ioctl支持不完整测试不生效时还是得回到RTS ioctl方案。4.2 轮询策略设计帧间隔、超时阈值与重试机制一条RS485总线上可以挂多个从站主机需要周期性地一个一个轮询。这里有一个Modbus RTU的底层约束帧与帧之间必须保持至少3.5个字符时间的静默间隔否则从站会认为前一帧还没结束把两个帧混在一起解析。这个间隔怎么算以9600波特率、8位数据、无校验、1位停止位为例一个字符11位3.5个字符时间就是3.5×11/9600约等于4毫秒。所以轮询完一个从站建议至少延时4毫秒再发下一帧。115200波特率时间更短但也得保留至少1毫秒余量。实际工程里我一般固定至少usleep(5000)省心。轮询策略还可以考虑自适应超时如果某个从站连续多次无响应不要死等先把它标记为离线继续轮询下一个从站等到下一轮再试。否则一个离线从站会让整条总线的轮询周期拖慢好几倍。重试次数建议不超过2次重试机制最多做一轮避免总线风暴。typedef struct { uint8_t slave_addr; uint16_t start_reg; uint16_t reg_count; float value; int valid; } sensor_node_t; sensor_node_t sensors[] { {1, 0, 2, 0.0f, 0}, {2, 0, 2, 0.0f, 0}, {3, 0, 2, 0.0f, 0}, }; void poll_all_sensors(int fd) { for (int i 0; i sizeof(sensors) / sizeof(sensors[0]); i) { int ret modbus_read_registers(fd, sensors[i].slave_addr, sensors[i].start_reg, sensors[i].reg_count); if (ret 0) { uint8_t data[4]; int n modbus_read_response(fd, sensors[i].slave_addr, data, sizeof(data), 200); if (n 4) { uint16_t hi (data[0] 8) | data[1]; uint16_t lo (data[2] 8) | data[3]; sensors[i].value regs_to_float(hi, lo); sensors[i].valid 1; } else { sensors[i].valid 0; } } else { sensors[i].valid 0; } // 帧间隔保证满足3.5字符时间 usleep(5000); } }4.3 现场不稳定排查终端电阻、共地、地址冲突与波特率轮询代码写好了不代表现场就稳定。我遇到过最典型的现场故障是单独测试每个从站都正常把所有从站挂到总线上之后要么通信时好时坏要么偶尔报CRC错误。先排查物理层终端电阻RS485总线的两端需要各接一个120欧姆电阻。总线长度超过几十米或者节点数多时没有终端电阻会出现反射导致波形畸变和CRC错误。但节点数少、布线短的情况下不接也没事这不是必须的看现场距离。共地RS485的A/B线是差分信号但各个从站设备之间最好共地否则当从站供电来自不同电源时地电位差可能超出收发器共模范围直接通信失败。线缆极性A/B反接是常见低级错误很多USB转485模块上的A/B丝印和实际端子定义不一致对调一下就能恢复正常。再排查协议层从站地址重复总线上有两个从站都设置成地址1主机读地址1时两个从站同时抢总线响应帧直接乱掉表现为CRC频繁错误或地址对不上。波特率不一致某个从站被误设为19200其他都是9600通信表现是请求发出后完全无响应而不是乱码。功能码不支持有些简易传感器只支持04功能码你用03去读它会返回异常帧需要在代码里增加对异常码的记录和日志方便快速定位。现场排查时我一般先缩小范围只挂一个从站能否正常通信然后逐步增加节点。如果单从站正常、多从站异常优先怀疑地址冲突和总线负载如果所有从站都时好时坏优先怀疑物理层和接线。这个排查顺序能省很多时间。5. 调试与验证从串口抓包到modbus poll的实战心得5.1 Linux下串口调试三板斧stty、hexdump、python写协议代码之前建议先用系统自带工具验证串口通路。这里有一套非常实用的组合拳。先设置串口参数并检查# 设置串口为9600 8N1 stty -F /dev/ttyUSB0 9600 cs8 -parenb -cstopb raw # 查看当前参数是否生效 stty -F /dev/ttyUSB0 -a如果要直接看串口收到的原始字节用hexdump比cat更直观# 以十六进制打印串口收到的所有字节 hexdump -C /dev/ttyUSB0不过最好先把串口设成raw模式再抓取。如果不设内核可能会对输入做处理。如果需要构造和发送任意字节用Python的pyserial最方便尤其是临时验证CRC或手动发测试帧import serial import time ser serial.Serial(/dev/ttyUSB0, 9600, timeout1) # 构造读取请求从站地址1、功能码03、起始地址0、数量2 req bytes.fromhex(01 03 00 00 00 02 C4 0B) ser.write(req) time.sleep(0.1) resp ser.read(64) print(resp.hex( ))这套组合拳能在不写任何C代码的情况下验证串口参数是否配对、从站是否在线、CRC是否匹配。如果你手动发的字节序列正确从站返回了正常数据那么问题一定在你自己写的代码里如果没有返回问题在物理层或从站配置。5.2 用modbus poll做协议对标测试快速定位偏差modbus poll是Windows上一个非常经典的Modbus主站调试工具虽然很多Linux工程师不愿装Windows虚拟机但调试一些复杂的传感器时它确实好用。它也经常被问到密钥、注册码之类的东西实际上试用版足够做最简单的单从站测试不要去找那些乱七八糟的破解版本。modbus poll的使用逻辑很简单选择串口COM号、设置波特率/校验位/数据位/停止位再设置从站地址、功能码、起始地址和寄存器数量点连接后它会周期性地轮询并显示每个寄存器的数值。如果你写的C代码读出来的数据和modbus poll读出来的对不上说明协议层有偏差大概率是寄存器地址、功能码或者字节序的问题。反过来如果你手头有modbus poll但缺少真实的从站可以用modbus slave模拟一个从站这样不用接任何硬件在纯软件环境里就能把主站程序调通。我经常在程序联调前先跑一遍“虚拟主站虚拟从站”的自测确认协议栈本身没问题再上硬件。5.3 自己写协议栈时的分层排错思路最后说说自己写协议栈时遇到问题怎么一步步缩小范围。我建议把整个通信过程分成四层每一层单独验证第一层物理链路。示波器或万用表量A/B差分电压确认发送请求时总线有波形变化。没有波形问题在RS485方向控制或DTE配置。第二层收发字节。用串口调试助手或hexdump抓原始字节确认主机发出去的帧和从站返回的帧在字节层面对不对。CRC对不对在这一层就能看出来。第三层协议语义。字节都对得上但功能码或寄存器地址不对从站返回异常码。把异常码转成文字比如02代表非法数据地址说明起始地址和数量超出了从站固件定义的范围。第四层应用解析。协议层数据正常但最终数值不对那就是寄存器排列顺序、浮点格式、量纲换算这些应用层细节的问题。这套分层方法看起来朴素但真的能救命。很多时候我们一上来就怀疑是自己代码有问题翻过来覆过去查CRC计算、查缓冲区最后发现原来是把04和03搞错了或者把地址偏移算错了位。分层排错能让你快速锁定问题在哪一层不会在错误的方向上浪费数小时。另外代码里一定要加调试日志至少要能打印每一帧的收发字节。我自己的习惯是封装一层debug_hex(TX, req, 8)和debug_hex(RX, buf, len)上线前可以通过环境变量关掉排查问题时打开。实际开发中还有一个经验不要把超时设得太短。Modbus从站的响应时间一般在几十毫秒到几百毫秒不等有些继电器类设备可能响应较慢。我第一次写轮询时很激进超时只设了100毫秒结果总线上有几台设备经常“超时”后来看抓包才发现它们响应要200毫秒左右。把超时统一放宽到500毫秒后一切正常。轮询周期长了点但可靠性优先。到现在回想一下这个项目让我印象最深刻的不是代码难写而是很多问题被我们下意识归咎于“协议复杂”实际上真相只是串口参数没配好、方向切换快了半拍、字节序反了。Modbus RTU本身是一个极其简单、成熟的协议只要你把基础打牢后面不管换什么传感器、什么从站都能很快上手。如果你正准备在自己的嵌入式Linux板卡上接Modbus设备希望这篇文章能帮你少走一些弯路把时间花在真正有价值的数据处理和业务逻辑上。
