ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

嵌入式Linux下Modbus RTU串口通信与传感器数据读取实战

嵌入式Linux下Modbus RTU串口通信与传感器数据读取实战 干过嵌入式Linux的老哥应该都有这种体会硬件平台换了一茬又一茬从AM335x到i.MX6ULL再到RK3568但和传感器打交道最绕不开的协议始终是Modbus RTU。为啥因为工业现场的老设备、老传感器几乎都是这个出身温湿度变送器、压力变送器、电量采集模块十有八九支持Modbus RTU over RS485。这篇文章我就把完整的“串口配置 RTU读写传感器数据”流程拆开揉碎从termios底层配置说到CRC校验和浮点转换最后附上我在实际项目中踩过的坑和排查方法。不管是刚接触嵌入式Linux的新手还是想系统梳理Modbus RTU主站开发的工程师这篇都能给你一套能直接落地的方案。1. 整体设计与技术选型1.1 为什么在嵌入式Linux上做Modbus RTU先聊一个很多人纠结的问题既然MCU裸机开发也可以跑Modbus RTU为什么要在嵌入式Linux上做一句话回答因为Linux平台的管理能力、网络能力和扩展能力是裸机没法比的。如果项目里只有一路RS485接一个温度传感器那STM32裸机完全够用但一旦设备需要同时处理多路传感器、需要断线重连、需要数据上云、需要远程升级固件Linux平台的进程管理、线程模型、socket编程接口和文件系统优势就非常明显了。而且嵌入式Linux上做Modbus RTU开发本质上就是操作串口文件。Linux把一切外设都抽象成了文件串口是/dev/ttyS*或/dev/ttyUSB*用标准的open()、read()、write()配合ioctl()和termios结构体就能完成全部收发工作。这种“文件即设备”的设计让开发思路特别清晰。我做的那个项目里用了四路RS485接多路传感器外加一路以太网口上送数据如果是裸机方案光处理Modbus RTU的时序和网络协议栈就得写掉大半的Flash而在Linux下不过是几个线程和socket的事。1.2 技术方案整体架构整个方案的架构可以拆成四层层级作用关键点硬件层RS485转串口USB转485或CPU原生UART485芯片方向控制、电气隔离驱动层Linux内核串口驱动配置波特率、数据位、停止位协议层Modbus RTU协议栈帧封装、CRC16校验、功能码解析应用层传感器数据读写、业务逻辑线程模型、超时管理、数据解析应用层建议单独封装一个模块把Modbus RTU的主站逻辑和具体传感器业务解耦。比如我习惯建一个modbus_rtu_master库上层只关心“读几号从站的哪个寄存器”底层自动完成组帧、发帧、收帧、校验、超时重试。1.3 串口和RS485选型嵌入式Linux平台常用的串口方案有两种原生UART 外接RS485收发器。优点是稳定可靠缺点是方向切换需要控制DE/RE引脚。Linux下通常用TIOCMBIS和TIOCMBIC操作RTS引脚来控制方向或者驱动层面直接支持自动方向切换有些内核配置开了CONFIG_SERIAL_RS485。需要注意的是并非所有板子的内核默认支持RS485模式很多时候得手动控制RTS。USB转RS485模块。比如CH340T转出来的串口再接MAX485优点是即插即用、调试方便缺点是延迟和稳定性不如原生UART高波特率下偶尔会丢数据。我在这里的建议是正式项目不推荐走USB转串口尽量使用处理器原生UART调试阶段用USB转485没问题但量产必须换掉。2. 串口配置实操与核心细节2.1 termios结构体配置串口参数串口配置在Linux下就是填充和设置struct termios。这是整个项目的基石配置不对后面全部白搭。下面这段代码是我常用的串口初始化函数包括了波特率、数据位、停止位、校验位、原始模式、非阻塞和缓冲区刷新#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include termios.h #include string.h #include errno.h int uart_init(const char *dev, int baud, int data_bits, int stop_bits, char parity) { int fd open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd 0) { perror(open serial); return -1; } struct termios opt; memset(opt, 0, sizeof(opt)); /* 获取当前串口配置不能直接清零后设置 */ if (tcgetattr(fd, opt) ! 0) { perror(tcgetattr); close(fd); return -1; } /* 设置为原始模式禁止流控 */ cfmakeraw(opt); opt.c_cflag | CLOCAL | CREAD; /* 忽略调制解调器控制线使能接收 */ opt.c_cflag ~CRTSCTS; /* 禁止硬件流控 */ opt.c_iflag ~(IXON | IXOFF | IXANY); /* 禁止软件流控 */ /* 设置波特率 */ cfsetispeed(opt, baud); cfsetospeed(opt, baud); /* 设置数据位 */ opt.c_cflag ~CSIZE; switch (data_bits) { case 5: opt.c_cflag | CS5; break; case 6: opt.c_cflag | CS6; break; case 7: opt.c_cflag | CS7; break; case 8: opt.c_cflag | CS8; break; default: opt.c_cflag | CS8; break; } /* 设置校验位 */ switch (parity) { case N: opt.c_cflag ~PARENB; /* 无校验 */ opt.c_iflag ~INPCK; break; case E: opt.c_cflag | PARENB; /* 偶校验 */ opt.c_cflag ~PARODD; opt.c_iflag | INPCK; break; case O: opt.c_cflag | PARENB; /* 奇校验 */ opt.c_cflag | PARODD; opt.c_iflag | INPCK; break; } /* 设置停止位 */ if (stop_bits 2) opt.c_cflag | CSTOPB; else opt.c_cflag ~CSTOPB; /* 最少读取字符数0表示非阻塞模式直接返回 */ opt.c_cc[VMIN] 0; opt.c_cc[VTIME] 0; /* 清空缓冲区并应用配置 */ tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, opt) ! 0) { perror(tcsetattr); close(fd); return -1; } return fd; }2.2 关键配置项深入解读先解释几个容易踩坑的选项cfmakeraw()这一行很多人会漏掉。它实际上是设置termios的“原始模式”关掉了ICANON规范模式、ECHO回显、ISIG信号产生、IXON输出流控等一堆行规模式。Modbus RTU是二进制协议里面的字节可能是0x11、0x13这些控制字符如果不关掉这些选项内核会把这些字节当成终端控制字符截断或丢弃直接导致数据错乱。CLOCAL | CREAD几乎是必须的。CLOCAL忽略modem控制线比如串口接的模块没有DCD信号时不加这个read()会直接阻塞等待“载波检测”CREAD使能接收器。少了CLOCAL在部分板子上会出现打开串口后无法发送的问题。O_NOCTTY打开串口时如果当前进程没有控制终端不加这个标志串口会成为进程的控制终端。这会导致收到特殊字节比如CtrlC对应的0x03时向进程发SIGINT信号。RS485总线上出现0x03字节的概率非常大一旦触发这个信号程序直接死掉。VMIN和VTIME这个两个成员我设置为0/0配合O_NONBLOCK实现“读串口立即返回”。如果你用的是阻塞模式VMIN0, VTIME1意思是读操作最多等待100ms。这个在Modbus RTU的超时控制上非常好用后面章节会说。2.3 波特率参数的处理cfsetispeed和cfsetospeed接收的是B9600、B115200这样的宏定义值而不是整数。很多项目里需要动态传波特率这时候建议做映射表speed_t baudrate_to_b115200(int baud) { switch (baud) { case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; default: return B9600; } }注意不同架构的Linux内核波特率宏定义的范围不一样如果用到2M、4M的高波特率得查内核头文件确认。嵌入式Linux下4800bps反而比115200bps常见因为很多Modbus RTU传感器默认波特率就是4800或9600。3. Modbus RTU协议核心细节3.1 RTU报文帧格式Modbus RTU的帧格式非常简洁每帧最多256字节字段地址码功能码数据区CRC16校验长度1字节1字节N字节2字节低字节在前请求帧和响应帧的前两个字段相同。比如我要读从站地址为1的传感器的保持寄存器起始地址0x0000读1个寄存器发送的原始字节就是01 03 00 00 00 01 84 0A其中01是从站地址03是功能码读保持寄存器00 00是起始寄存器00 01是寄存器数量84 0A是CRC16校验值。3.2 CRC16-MODBUS算法实现CRC16校验是Modbus RTU绕不开的坎。它的多项式是0x8005初始值是0xFFFF结果异或0x0000数据流按小端序输出。这是MODBUS标准专用算法和常见的CRC16-CCITT多项式0x1021根本不是一回事千万别用错。下面是查表法的经典实现static const uint16_t crc16_table[256] { 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0281, 0xC240, /* ... 完整查表项略可在线生成 ... */ }; uint16_t modbus_crc16(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc (crc 8) ^ crc16_table[(crc ^ data[i]) 0xFF]; } return crc; }如果项目不大直接用按位计算也不慢uint16_t modbus_crc16_bitwise(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t 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低字节再发高字节。比如CRC算出来是0x0A84发送顺序就是84 0A。很多新手在这块儿搞反导致从站一直不应答。3.3 常用功能码与寄存器类型Modbus RTU的功能码就那么几个做传感器读取最常用的就这几个功能码名称用途寄存器类型0x01读线圈读开关量输出线圈0x02读离散输入读开关量输入离散输入0x03读保持寄存器读模拟量输出/参数可写保持寄存器0x04读输入寄存器读模拟量输入只读输入寄存器0x05写单个线圈控制开关线圈0x06写单个寄存器写参数保持寄存器0x0F写多个线圈批量控制线圈0x10写多个寄存器批量写参数保持寄存器读传感器数据时优先用0x04读输入寄存器因为输入寄存器是只读的专为传感器测量值设计。但也有传感器厂家把数据放在保持寄存器里这个一定要查数据手册我遇到过不少传感器用户手册上写的是保持寄存器结果用0x03读到的却是全0白白折腾了半天。3.4 Modbus RTU的帧间隔要求这个点很多新手根本不知道Modbus RTU规定帧与帧之间至少要有3.5个字符时间的静默间隔字符间隔不得大于1.5个字符时间超过1.5个字符时间则认为一帧结束。这意味着接收时必须做“帧超时”判断不能只靠read完整字节数来判断帧结束。比如9600波特率下1个字符时间约是1/9600秒*10位≈1.04ms3.5个字符时间就是3.65ms1.5个字符时间就是1.56ms。不过在实际的Linux系统中纯靠定时器去卡这个时间很复杂我一般用双策略一是读取预期字节数根据功能码能算出来正确响应帧长二是设置整体响应超时时间比如从机响应超时100ms或200ms可配置。4. 传感器数据读写完整实现4.1 主站请求帧构建与发送先实现一个最核心的函数发送一个读保持寄存器功能码0x03的请求。核心逻辑是用send()写串口写完立即调用tcdrain(fd)等待串口数据全部发送完成。int modbus_read_registers(int fd, uint8_t slave_id, uint16_t start_reg, uint16_t reg_count, uint16_t *regs) { uint8_t tx_buf[8]; tx_buf[0] slave_id; tx_buf[1] 0x03; /* 功能码 */ tx_buf[2] (start_reg 8) 0xFF; /* 起始地址高字节 */ tx_buf[3] start_reg 0xFF; /* 起始地址低字节 */ tx_buf[4] (reg_count 8) 0xFF; /* 寄存器数量高字节 */ tx_buf[5] reg_count 0xFF; /* 寄存器数量低字节 */ uint16_t crc modbus_crc16(tx_buf, 6); tx_buf[6] crc 0xFF; tx_buf[7] (crc 8) 0xFF; int ret write(fd, tx_buf, 8); if (ret ! 8) { perror(write serial); return -1; } tcdrain(fd); /* 等待发送完成后继续 */ return 0; }tcdrain这行容易忽略。因为串口驱动有发送缓冲区write()成功返回只代表数据进了内核缓冲区不代表真正从TX引脚发出去了。如果紧接着就去读串口从机可能还没收到请求。加tcdrain之后会阻塞到发送FIFO为空确保数据完全出线。4.2 响应帧接收与超时处理接收响应帧是Modbus RTU主站里最容易出错的地方。响应帧长度取决于请求了多少个寄存器比如读N个保持寄存器响应帧长度就是3 2 * N字节。所以响应预期的字节数是完全可以预先算出来的。下面是一个带超时控制的接收函数核心是selectint modbus_read_response(int fd, uint8_t *rx_buf, int expect_len, int timeout_ms) { fd_set fds; struct timeval tv; int total 0; while (total expect_len) { 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) { perror(select); return -1; } else if (ret 0) { printf(timeout: rx %d bytes, expect %d bytes\n, total, expect_len); return -1; } int n read(fd, rx_buf total, expect_len - total); if (n 0) { if (errno EAGAIN) continue; perror(read serial); return -1; } else if (n 0) { continue; /* 没有数据可读 */ } total n; } return total; }这个函数的巧妙点在于先用select设置超时等待超时后判断已经收到的字节数。如果收到了部分帧就超时了说明帧不完整必须把串口缓冲区清掉丢弃残帧否则会污染下一次读取。还有一个容易忽略的点select超时之后要继续循环。因为在O_NONBLOCK模式下read()可能返回EAGAIN表示暂时没数据这时候要重新select等待不能直接当错误处理。4.3 完整读多个寄存器的流程封装把上面几个函数串起来就是一个完整的Modbus RTU读寄存器流程int modbus_read_holding_registers(int fd, uint8_t slave_id, uint16_t start_reg, uint16_t count, uint16_t *regs) { uint8_t rx_buf[256]; uint8_t slave, func; uint8_t byte_count; uint16_t crc_rx, crc_calc; /* 发送请求帧 */ if (modbus_send_read_request(fd, slave_id, 0x03, start_reg, count) 0) return -1; /* 期望响应长度 从站地址1 功能码1 字节数1 2*N CRC2 */ int expect_len 5 2 * count; int ret modbus_read_response(fd, rx_buf, expect_len, 200); if (ret ! expect_len) { tcflush(fd, TCIFLUSH); /* 清空残帧 */ return -1; } slave rx_buf[0]; func rx_buf[1]; byte_count rx_buf[2]; if (slave ! slave_id) { printf(slave addr mismatch: %02x ! %02x\n, slave, slave_id); return -1; } if (func (0x03 | 0x80)) { printf(exception code: 0x%02x\n, rx_buf[2]); return -1; } /* 校验CRC */ crc_rx rx_buf[expect_len - 2] | (rx_buf[expect_len - 1] 8); crc_calc modbus_crc16(rx_buf, expect_len - 2); if (crc_rx ! crc_calc) { printf(CRC error: rx 0x%04x calc 0x%04x\n, crc_rx, crc_calc); return -1; } /* 把16位寄存器值拷贝出来 */ for (int i 0; i count; i) { regs[i] (rx_buf[3 i * 2] 8) | rx_buf[4 i * 2]; } return 0; }这个流程里最关键的是异常码判断。Modbus从站如果返回异常功能码会最高位置1比如请求功能码0x03返回异常时功能码是0x83。异常码0x01表示非法功能码0x02表示非法地址0x03表示非法数据。如果不判断异常码你可能把一个只有5字节的异常响应硬生生按正常帧长度解析导致缓冲区错乱。4.4 浮点数转换4字节如何变成传感器真实值这是做传感器数据读取时问得最多的一个问题。很多传感器比如温湿度变送器、压力变送器用2个寄存器保存一个32位浮点数即IEEE 754单精度格式。先看怎么把4字节转成float#include stdint.h #include string.h float bytes_to_float(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) { uint32_t bits ((uint32_t)b0 24) | ((uint32_t)b1 16) | ((uint32_t)b2 8) | (uint32_t)b3; float val; memcpy(val, bits, 4); return val; }难点在字节顺序。不同传感器厂家对寄存器内字节的排列约定不一样常见四种序含义举例两个寄存器值转换为浮点数ABCD高字在前大端0x420C 0x000035.0CDAB低字在前0x0000 0x420C35.0BADC低字在前的高低位颠倒0x420C 0x0000对应不同结果DCBA小端顺序......我强烈建议的做法是在程序里写一个“字节序探测”函数读4个字节后用四种顺序各转换一次打印出来对比再根据传感器说明书选择正确的顺序。比如常见的温湿度变送器返回的4字节00 00 10 41是ABCD序10 41 00 00是CDAB序。选错了35.0度可能变成8.06e-27这种离谱数字。实际项目里我一般这样封装float modbus_f32_from_regs(uint16_t reg_high, uint16_t reg_low, int order) { uint8_t b[4]; switch (order) { case ORDER_ABCD: b[0] reg_high 8; b[1] reg_high 0xFF; b[2] reg_low 8; b[3] reg_low 0xFF; break; case ORDER_CDAB: b[2] reg_high 8; b[3] reg_high 0xFF; b[0] reg_low 8; b[1] reg_low 0xFF; break; default: break; } return bytes_to_float(b[0], b[1], b[2], b[3]); }4.5 多路传感器轮询与线程模型项目里有多个从站传感器时主站必须轮询。轮询模型用单线程就够了核心是维护一个从站表定时循环发送请求并等待响应。不过有一点得特别注意RS485是半双工总线同一时刻总线上只能有一个设备在发送。所以轮询必须串行绝对不能并发发请求。我见过有人在多线程里同时向不同从站发请求结果总线冲突所有数据全乱套。线程模型我推荐这么设计主线程负责业务逻辑和数据上报一个独立的Modbus轮询线程负责定时读写传感器轮询线程内部按从站ID顺序执行端口读完后才能发起下一个请求每路传感器请求之间设置间隔比如10~50ms给总线留出余量轮询线程的伪代码如下void *modbus_poll_thread(void *arg) { while (running) { for (int i 0; i slave_count; i) { int ret modbus_read_holding_registers(fd, slaves[i].addr, slaves[i].start_reg, slaves[i].reg_count, slaves[i].regs); if (ret 0) { slaves[i].online 1; slaves[i].data_valid 1; } else { slaves[i].online 0; } usleep(50000); /* 50ms */ } usleep(50000); } return NULL; }这种轮询方式简单可靠唯一的代价是单圈轮询时间等于“请求响应时间×从站数”如果100个从站就会很慢。真遇到这种规模就得考虑分总线和多串口并行这里不展开。5. 调试工具与常见问题排查5.1 调试工具推荐从mbpoll到逻辑分析仪嵌入式Linux下调试Modbus RTU我用过的最顺手的工具是mbpoll。它是个命令行工具直接在开发板或PC上装用法很简单mbpoll -a 1 -t 3 -r 0 -c 2 -b 9600 -d 8 -p none /dev/ttyS0意思是从站地址1功能码0x03读保持寄存器从寄存器0开始读2个寄存器波特率96008数据位无校验串口设备/dev/ttyS0。mbpoll的价值在于它能独立验证从站的响应。当你的程序读不到数据时先用mbpoll看看是不是传感器或者线路本身就有问题这样能快速缩小问题范围是硬件链路问题还是程序逻辑问题。另外推荐一个思路如果手头有逻辑分析仪或者示波器可以直接抓RS485总线上A/B线之间的波形能看到每一帧的字节间隔和帧间隔。Modbus RTU的时序问题比如帧间隔过短用示波器一看就明白远比盲查代码高效。5.2 从站无应答的排查思路这是Modbus RTU开发里遇到最多的现象发送请求后完全收不到响应或者select一直超时。排查顺序我建议从硬件到软件查硬件接线。RS485的A通常接D-和B通常接D别接反屏蔽层单端接地。别小看这个我至少有一半的“无应答”问题是A/B接反了。查从站地址和参数。用官方软件或工具先把从站摸清楚看实际设置是多少。很多传感器出厂默认从站号不是1而是247或者255。查串口设备节点和权限。ls -l /dev/ttyS0看设备是否存在运行用户是否有权限。嵌入式Linux里经常遇到/dev下根本没有设备节点得自己mknod或者设备节点有了但权限是root程序跑在普通用户下打不开。查请求帧是否正确。用crc16工具或者自己打印发送的字节确认CRC和寄存器地址没有算错。最简单的试验ABC三个环节分别验证——从站能不能被mbpoll读到程序发出去的字节和mbpoll发出去的是否一致。查方向控制。如果是非自动方向控制的RS485电路RTS或DE引脚必须由程序拉高/拉低。如果没做方向切换RS485芯片默认处于接收态数据根本发不出去但发送者本身可能察觉不到因为回环测试可能正常。5.3 CRC校验失败的常见原因CRC校验失败最常见的是这两个原因第一读取的帧长度不对。如果期望接收帧长度算错了比如传感器实际返回了9个字节但程序只读了8个字节最后一个CRC高字节被漏掉CRC算出来必定错误。我建议把收到的原始字节全部打印出来对照Modbus协议算一遍字节数确认功能码和字节计数字段的含义。第二收发共线但发送没有完全结束。RS485是半双工如果发送完立刻切到接收模式、并且读取时还带着发送残留数据会导致读到自己的回波或者半截数据。解决办法是tcdrain之后延时或者等到帧静默时间再打开接收。还有一点串口驱动和传感器之间如果还挂了一级RS485中继器或者HubCRC错误特别容易出现。这不是协议问题是硬件信号质量问题优先检查终端匹配电阻一般在总线两端各挂一个120Ω电阻。5.4 串口数据出现乱码或字节丢失数据乱码多半是波特率不匹配或者主从两端的数据位/校验位设置不一致。比如从站是8E18数据位偶校验1停止位主站却设成了8N1那读出来的数据必定偶发错位。字节丢失则需要检查接收缓冲区和内核FIFO。嵌入式Linux的串口驱动默认有16字节FIFO和软件缓冲区如果应用层读得不够快数据会溢出丢包。可以适当调大内核的tty缓冲区大小或者在应用层用更大的read缓冲区一次把数据全部读走。其他几个容易忽略的点串口的RTS/CTS流控是否被硬件默认拉高。如果板载485芯片的RTS接了RS485方向引脚而程序没有正确控制双向通信会非常混乱。stty命令快速查看当前串口配置stty -F /dev/ttyS0 -a如果发现ispeed和ospeed不一致说明配置没生效。Linux下串口有/dev/ttyS0和/dev/ttyS1编号不同内核配置下设备节点可能对不上。用dmesg | grep tty快速定位硬件对应关系。6. 实操心得与几个提升可靠性的细节最后聊几个我在实际项目里积累的、不算难但很提升稳定性的细节。波特率匹配要两端都确认。别只信传感器的标签像某些老传感器标签上印着9600实际出厂时被改成19200了。用mbpoll停在一个端口上测三次用确定能通讯的波特率再接程序。串口打开后一定要延时再发第一帧。有些串口驱动或RS485收发器在上电后需要几十毫秒稳定时间打开串口后立刻发请求第一帧容易丢失。稳妥的做法是open成功之后usleep(100000)再开始轮询。超时时间要根据波特率调整。9600波特率下读1个寄存器大约需要20多ms而115200波特率下只需要几ms。统一设置200ms超时在9600下问题不大但在高速轮询的场景下会让系统响应变慢。我的做法是请求前根据波特率估算出一个基础时延再加上一个固定余量作为本次请求的超时时间。写寄存器操作前先读一次原值。特别是写保持寄存器控制参数的时候如果只发写指令不校验读回一旦写入失败很多传感器的参数被改变后不会自动恢复得手动复位重置这种事故在工业现场非常被动。多从站项目建议给每个从站加“离线计数”。连续3次请求失败就标记离线恢复后自动重新上线。很多人在轮询线程里只保留了一个online标志位导致单次丢帧就误报离线误判率特别高。还有一个细节是信号处理。挂到后台运行的嵌入式程序会收到SIGHUP、SIGTERM如果没有在这些信号处理函数里关闭串口fd下次重新启动时再打开同一个串口可能会因为之前的进程没释放而引起设备占用。我通常会在signal(SIGTERM, handler)里执行close(fd)和tcflush。这套方案我从AM3358一直用到IMX6ULL从裸串口的温湿度采集做到四路RS485采集卡核心逻辑基本没有大动过。Modbus RTU这个协议本身不复杂真正决定稳定性的是串口配置是否严谨、超时处理是否完善、CRC校验是否到位以及现场的硬件接线是否可靠。按上面这套流程走下来遇到问题的大方向基本都能被快速定位。
返回列表