资讯动态

ESP32 ModbusTCP分片缓存实战:环形缓冲区与状态机解析

发布时间:2026/10/27 10:53:50 来源:尧图企业网站定制
1. 项目缘起与整体设计思路1.1 为什么要在 ESP32 上折腾 ModbusTCP 分片缓存做过工业数据采集的朋友都清楚ModbusTCP 在工控现场几乎是绕不开的协议。它简单、开放、生态成熟PLC、电表、温控器、变频器基本都支持。但 ESP32 这颗芯片有个很现实的问题它的 RAM 实在不算宽裕。以常见的 ESP32-WROOM-32 为例可用堆内存通常在 200KB 到 300KB 之间如果跑 WiFi 协议栈、TCP/IP 协议栈再加上你自己的业务逻辑留给数据缓冲的空间就更紧张了。问题就出在这里。ModbusTCP 的单个 ADU应用数据单元理论上最大可以到 260 字节左右但实际项目中很多设备会返回远超这个长度的数据。比如某些电力仪表一次读取几十个保持寄存器返回的报文可能上千字节。更麻烦的是TCP 是流式协议它不保证一次 recv 就能收到一个完整的 Modbus 报文。你可能第一次收到 100 字节第二次收到 200 字节第三次才把剩下的收完。如果代码里假设“一次 recv 就是一个完整帧”那在真实网络环境下必然翻车。我最初做这个项目就是因为一个现场问题ESP32 作为 ModbusTCP 客户端去轮询一台多回路电表偶尔会解析出错误的电量数据。抓包一看发现是 TCP 分片导致的——电表返回的 800 多字节报文被拆成了三个 TCP 段而我的代码只处理了第一个段后面的数据被当成下一个响应整个时序全乱了。所以这个项目的核心目标很明确在 ESP32 上实现一套可靠的 ModbusTCP 分片缓存机制能够正确处理 TCP 流式传输带来的粘包和拆包问题保证每一个 Modbus 响应都能被完整、准确地解析。它适合所有用 ESP32 做 ModbusTCP 主站或从站的开发者尤其是那些需要读取大量寄存器、网络环境不太稳定的工业场景。1.2 整体架构环形缓冲 状态机 超时管理方案选型上我考虑过几种思路。最简单的是用一个固定大小的数组每次 recv 追加到数组末尾然后检查是否收够了一个完整帧。这种做法在数据量小的时候能用但有几个致命缺陷一是数组大小不好定定小了会溢出定大了浪费 RAM二是如果一次 recv 里包含了多个 Modbus 帧粘包处理起来很别扭三是没有超时机制万一对方发了一半就不发了程序会一直等下去。最终我采用的是环形缓冲区Ring Buffer加协议状态机的组合方案。环形缓冲区的优势在于内存固定不会动态分配导致碎片读写指针分离天然适合生产者-消费者模型配合长度判断可以灵活处理任意大小的数据流。状态机则负责从缓冲区里“切”出完整的 Modbus 帧每切出一个就交给上层处理剩下的继续留在缓冲区里等后续数据。具体来说整个数据流是这样的TCP 收到数据后先写入环形缓冲区然后状态机从缓冲区读取数据先检查功能码和字节数再根据 ModbusTCP 的 MBAP 头里的长度字段判断整个帧有多长如果缓冲区里的数据够一个完整帧就取出来解析否则就等待下一次 recv。同时每个请求发出时启动一个超时定时器如果超时还没收到完整响应就丢弃当前缓冲区里的残帧重新发起请求。这个设计的另一个好处是它天然支持多连接。ESP32 作为 ModbusTCP 服务器时可能有多个客户端同时连接每个连接都需要独立的缓冲区。环形缓冲区可以按连接分配互不干扰。1.3 内存规划与参数选择在 ESP32 上做缓存内存规划是重中之重。我实测下来一个 ModbusTCP 连接分配 2KB 的环形缓冲区是比较合理的。为什么是 2KB因为 ModbusTCP 单帧最大约 260 字节但实际项目中经常遇到一次读取多个寄存器的情况比如读 125 个保持寄存器返回数据就是 250 字节加上 MBAP 头和 PDU 头总共约 260 字节。2KB 可以缓存大约 8 个这样的帧足够应对网络抖动和粘包。如果你用的是 ESP32-S3 或者带 PSRAM 的型号可以适当放大到 4KB 甚至 8KB但没必要太大。因为 ModbusTCP 是请求-响应模式正常情况下不会出现大量数据堆积。缓冲区太大反而浪费内存影响 WiFi 和 TCP 协议栈的稳定性。还有一个关键参数是接收超时时间。ModbusTCP 规范里没有强制规定超时但实际项目中我一般设置为 500ms 到 1s。太短了容易误判尤其是在 WiFi 信号不好的时候太长了会影响轮询效率。我的做法是首次请求超时设为 1s如果超时重试一次重试超时设为 500ms两次都失败就上报错误。2. 核心细节解析与实操要点2.1 环形缓冲区的实现细节环形缓冲区的核心就两个指针写指针write index和读指针read index。写入时数据从写指针位置开始存放写指针向后移动读取时数据从读指针位置取出读指针向后移动。当指针到达缓冲区末尾时回绕到开头。判断缓冲区空的条件是读指针等于写指针判断缓冲区满的条件是写指针加一等于读指针留一个字节区分空和满。在 ESP32 上我用 C 语言实现了一个轻量级的环形缓冲区没有用任何动态内存分配。核心结构体大概是这样typedef struct { uint8_t *buffer; size_t size; volatile size_t write_idx; volatile size_t read_idx; } ring_buffer_t;写入函数ring_buffer_write会先计算当前可写空间如果空间不够就返回实际写入的字节数可能小于请求写入的字节数。读取函数ring_buffer_read类似返回实际读取的字节数。另外我还实现了ring_buffer_peek只读取数据但不移动读指针用于状态机预判帧长度。这里有个细节要注意write_idx和read_idx我加了volatile关键字。因为在 ESP32 上TCP 接收任务和 Modbus 解析任务可能运行在不同的 FreeRTOS 任务里编译器优化可能会导致指针读取不一致。加volatile可以强制每次从内存读取避免缓存问题。当然更严谨的做法是加互斥锁但在单生产者单消费者的场景下volatile加上内存屏障基本够用。另一个坑是字节对齐。ESP32 是 32 位架构如果缓冲区指针没有对齐访问效率会下降。我在分配缓冲区时用heap_caps_malloc并指定MALLOC_CAP_8BIT确保按字节访问没问题。如果你追求极致性能可以用MALLOC_CAP_32BIT并手动对齐但一般没必要。2.2 ModbusTCP 帧结构解析与长度判断要正确分片必须先搞清楚 ModbusTCP 的帧结构。一个完整的 ModbusTCP ADU 由三部分组成MBAP 头7 字节、PDU协议数据单元至少 2 字节。MBAP 头的结构是字段长度说明事务标识符2 字节用于匹配请求和响应协议标识符2 字节Modbus 固定为 0长度2 字节后续字节数单元标识符 PDU单元标识符1 字节从站地址关键就在“长度”这个字段。它表示的是从单元标识符开始到 PDU 结束的总字节数。所以一个完整帧的总长度 6MBAP 前 6 字节 长度字段的值。举个例子如果长度字段是 5那么总帧长就是 6 5 11 字节。状态机的逻辑就是先从缓冲区里 peek 至少 6 个字节读出长度字段然后计算总帧长再检查缓冲区里是否有这么多字节如果有就读取整个帧并解析如果没有就等待更多数据。这里有个容易出错的地方长度字段本身是网络字节序大端。ESP32 是小端架构所以读取的时候要做字节序转换。我一般用(data[4] 8) | data[5]这种方式手动拼接比用ntohs更直观也避免引入额外头文件。还有一个边界情况如果长度字段的值小于 2 或者大于 254说明这个帧有问题应该直接丢弃并重置状态机。ModbusTCP 规范规定 PDU 最大 253 字节加上单元标识符就是 254所以长度字段最大 254。超过这个值肯定是异常数据。2.3 状态机设计与超时处理状态机我设计了四个状态IDLE、WAIT_HEADER、WAIT_BODY、PROCESS。IDLE表示没有待处理的请求WAIT_HEADER表示正在等待 MBAP 头WAIT_BODY表示头已收到正在等待完整的 PDUPROCESS表示帧已完整准备解析。每次 TCP 收到数据写入缓冲区后就调用状态机处理函数。状态机会根据当前状态和缓冲区里的数据量决定下一步动作。如果处于WAIT_HEADER且缓冲区数据不足 6 字节就返回继续等待如果够了就读取长度字段计算总帧长切换到WAIT_BODY。如果处于WAIT_BODY且缓冲区数据够总帧长就读取整个帧切换到PROCESS然后交给解析函数。超时处理是另一个关键点。我在每个请求发出时记录一个时间戳然后在主循环里定期检查如果当前时间减去时间戳超过了设定的超时时间就强制重置状态机和缓冲区。这里要注意重置缓冲区不能简单地清空读写指针因为可能还有未处理完的数据。我的做法是把读指针追到写指针位置相当于丢弃所有未读数据然后重置状态机到IDLE。实测下来这个超时机制能有效解决“对方发了一半就断线”的问题。有一次现场调试一台电表因为固件 bug返回的报文少了一个字节导致我的状态机一直等不到完整帧。加了超时重置后程序能自动恢复继续轮询下一个请求不会卡死。注意超时时间不要设得太短。我试过设 200ms结果在 WiFi 信号波动时频繁误判超时反而降低了采集成功率。后来改成 1s稳定多了。3. 实操过程与核心环节实现3.1 环境搭建与工程配置开发环境我用的是 VS Code 加 PlatformIO这是目前 ESP32 开发比较顺手的组合。PlatformIO 的好处是库管理方便编译速度快而且支持多平台。如果你习惯 Arduino IDE也可以但 PlatformIO 在依赖管理和调试上更省心。在platformio.ini里我一般这样配置[env:esp32dev] platform espressif32 board esp32dev framework arduino monitor_speed 115200 board_build.f_cpu 240000000L build_flags -DCORE_DEBUG_LEVEL3 -DCONFIG_ARDUHAL_LOG_COLORS1这里有几个点值得说明。board_build.f_cpu设为 240MHz 是为了让 TCP 处理和 Modbus 解析更快尤其是在高轮询频率下。CORE_DEBUG_LEVEL3是开启调试日志方便排查问题正式发布时可以改成 0 减少串口输出。如果你用的是 ESP32-S3板子选择要注意。ESP32-S3 有很多型号比如esp32-s3-devkitc-1是官方开发板esp32-s3-devkitm-1是另一种。选错了会导致引脚定义不对串口没输出。我一般先用esp32-s3-devkitc-1测试确认能跑通再根据实际硬件调整。3.2 环形缓冲区代码实现下面是我实际项目中用的环形缓冲区核心代码去掉了业务逻辑只保留通用部分#include string.h #include esp_heap_caps.h typedef struct { uint8_t *buf; size_t size; volatile size_t w; volatile size_t r; } ringbuf_t; ringbuf_t* ringbuf_create(size_t size) { ringbuf_t *rb (ringbuf_t*)malloc(sizeof(ringbuf_t)); if (!rb) return NULL; rb-buf (uint8_t*)heap_caps_malloc(size, MALLOC_CAP_8BIT); if (!rb-buf) { free(rb); return NULL; } rb-size size; rb-w 0; rb-r 0; return rb; } size_t ringbuf_write(ringbuf_t *rb, const uint8_t *data, size_t len) { size_t free (rb-r rb-w) ? (rb-r - rb-w - 1) : (rb-size - rb-w rb-r - 1); if (len free) len free; for (size_t i 0; i len; i) { rb-buf[rb-w] data[i]; rb-w (rb-w 1) % rb-size; } return len; } size_t ringbuf_read(ringbuf_t *rb, uint8_t *data, size_t len) { size_t avail (rb-w rb-r) ? (rb-w - rb-r) : (rb-size - rb-r rb-w); if (len avail) len avail; for (size_t i 0; i len; i) { data[i] rb-buf[rb-r]; rb-r (rb-r 1) % rb-size; } return len; } size_t ringbuf_peek(ringbuf_t *rb, uint8_t *data, size_t len) { size_t avail (rb-w rb-r) ? (rb-w - rb-r) : (rb-size - rb-r rb-w); if (len avail) len avail; size_t r rb-r; for (size_t i 0; i len; i) { data[i] rb-buf[r]; r (r 1) % rb-size; } return len; }这段代码里ringbuf_write和ringbuf_read都做了边界检查不会溢出。ringbuf_peek用于状态机预判不移动读指针。注意free的计算方式当读指针大于写指针时可写空间是r - w - 1否则是size - w r - 1。减一是为了留一个字节区分空和满。3.3 ModbusTCP 分片解析状态机状态机的实现我封装成了一个函数每次 TCP 收到数据后调用typedef enum { MB_STATE_IDLE, MB_STATE_WAIT_HEADER, MB_STATE_WAIT_BODY, MB_STATE_PROCESS } mb_state_t; typedef struct { mb_state_t state; uint16_t expect_len; uint32_t last_activity; } mb_parser_t; void mb_parser_feed(mb_parser_t *p, ringbuf_t *rb) { uint8_t header[6]; switch (p-state) { case MB_STATE_IDLE: if (ringbuf_peek(rb, header, 6) 6) { p-expect_len ((uint16_t)header[4] 8) | header[5]; if (p-expect_len 2 || p-expect_len 254) { ringbuf_read(rb, header, 6); p-state MB_STATE_IDLE; return; } p-state MB_STATE_WAIT_BODY; } break; case MB_STATE_WAIT_BODY: if (ringbuf_peek(rb, header, 6) 6) { size_t total 6 p-expect_len; uint8_t tmp[260]; if (ringbuf_peek(rb, tmp, total) total) { ringbuf_read(rb, tmp, total); mb_process_frame(tmp, total); p-state MB_STATE_IDLE; } } break; default: p-state MB_STATE_IDLE; break; } p-last_activity xTaskGetTickCount(); }这段代码的逻辑很直接先 peek 6 字节读长度再 peek 完整帧。如果数据不够就保持当前状态等下次 TCP 数据到来再试。mb_process_frame是上层解析函数负责提取功能码、寄存器数据等。这里有个优化点tmp数组我开了 260 字节放在栈上。ESP32 的默认任务栈一般是 4KB 或 8KB260 字节完全没问题。如果你担心栈溢出可以改成静态数组或者从堆里分配但没必要。3.4 TCP 接收任务与超时检查TCP 接收我放在一个独立的 FreeRTOS 任务里优先级设为 5比 WiFi 任务低比空闲任务高。任务主体是一个循环调用recv读取数据写入环形缓冲区然后调用状态机void tcp_recv_task(void *arg) { mb_parser_t parser { .state MB_STATE_IDLE }; ringbuf_t *rb ringbuf_create(2048); uint8_t buf[512]; while (1) { int len recv(sock, buf, sizeof(buf), 0); if (len 0) { ringbuf_write(rb, buf, len); mb_parser_feed(parser, rb); } else if (len 0) { // 错误处理 vTaskDelay(pdMS_TO_TICKS(10)); } // 超时检查 if (parser.state ! MB_STATE_IDLE) { uint32_t now xTaskGetTickCount(); if ((now - parser.last_activity) pdMS_TO_TICKS(1000)) { parser.state MB_STATE_IDLE; rb-r rb-w; // 清空缓冲区 } } } }超时检查放在循环里每次 recv 后都检查一次。如果超过 1 秒没有新数据就重置状态机和缓冲区。rb-r rb-w这行是直接丢弃所有未读数据简单粗暴但有效。提示recv的缓冲区大小我设的是 512 字节。这个值不用太大因为 TCP 本身有接收窗口一次 recv 太多反而增加延迟。512 字节足够覆盖大多数 Modbus 响应。4. 常见问题与排查技巧实录4.1 粘包与拆包问题速查现象可能原因排查方法解决方案解析出错误数据粘包一次 recv 包含多个帧打印每次 recv 的字节数和内容用状态机逐帧切分不要假设一次 recv 一帧响应不完整拆包一帧被分成多次 recv抓包看 TCP 分段环形缓冲区累积够一帧再解析程序卡死对方发了一半就断线看是否一直停在 WAIT_BODY加超时重置机制数据错位长度字段解析错误检查字节序转换用 (data[4]8)缓冲区溢出写入速度大于读取速度打印缓冲区剩余空间增大缓冲区或提高解析任务优先级这个表是我在实际调试中总结的基本上覆盖了 90% 的分片相关问题。其中“粘包”和“拆包”是最常见的尤其是在 WiFi 环境下TCP 分段行为比有线网络更不可预测。4.2 内存不足与任务栈溢出ESP32 的内存问题很隐蔽有时候程序跑着跑着就重启了串口打印Guru Meditation Error一看是栈溢出。我遇到过几次都是因为局部变量太大。比如在状态机里定义了一个 260 字节的数组如果这个函数被多层调用栈很快就爆了。解决办法有两个一是把大数组改成static放到全局区二是用heap_caps_malloc从堆里分配。我一般用static因为简单而且 Modbus 解析是单线程的不会有重入问题。另外FreeRTOS 任务的栈大小要合理设置。TCP 接收任务我设的是 4096 字节解析任务设的是 3072 字节。如果你发现任务创建失败或者运行不稳定可以适当加大。用uxTaskGetStackHighWaterMark可以查看任务栈的历史最低剩余量这个函数在调试时非常有用。4.3 超时时间与重试策略的平衡超时时间设多少这个问题我调了很久。设太短WiFi 抖动时频繁超时采集成功率下降设太长轮询效率低实时性差。我的经验值是首次请求 1s重试 500ms最多重试 2 次。为什么这样设因为 ModbusTCP 的正常响应时间通常在 10ms 到 100ms 之间1s 已经是很宽松的阈值了。如果 1s 还没响应大概率是网络问题或者设备忙。重试时缩短到 500ms是因为第一次超时后网络路径可能已经稳定了不需要等那么久。重试次数不要太多。我试过重试 5 次结果一个故障设备导致整个轮询周期被拖长影响了其他设备的采集。2 次是比较好的平衡点。还有一个技巧动态超时。如果连续多次请求都在 50ms 内响应可以把超时时间降到 300ms如果连续超时就升到 1.5s。这个逻辑实现起来不复杂但能显著提升整体效率。我在一个 20 台设备的项目里用了动态超时轮询周期从 3s 降到了 1.8s。4.4 调试工具与抓包技巧调试 ModbusTCP 分片问题光看串口日志是不够的必须抓包。我常用的工具是 Wireshark配合 ESP32 的 WiFi 抓包功能。具体做法是在 ESP32 上开启 promiscuous 模式把 WiFi 数据包通过串口转发到 PC然后用 Wireshark 解析。这个方案稍微有点复杂但能看到最真实的 TCP 分段情况。如果嫌麻烦可以在代码里加日志每次 recv 后打印字节数和前几个字节的十六进制值。比如ESP_LOGI(TAG, recv %d bytes: %02X %02X %02X %02X, len, buf[0], buf[1], buf[2], buf[3]);这样能快速判断是否发生了粘包或拆包。如果一次 recv 的前几个字节是00 01 00 00说明是 MBAP 头如果中间出现了00 01但不是帧开头那可能就是粘包了。注意日志输出本身会消耗 CPU 和串口带宽高频率轮询时不要开太详细的日志否则会影响实时性。我一般只在调试阶段开正式运行关掉。4.5 多连接场景下的缓冲区管理如果你的 ESP32 同时作为 ModbusTCP 服务器接受多个客户端连接那每个连接都需要独立的环形缓冲区和状态机。我一般用一个结构体数组来管理typedef struct { int sock; ringbuf_t *rb; mb_parser_t parser; } mb_conn_t; mb_conn_t conns[MAX_CONNS];MAX_CONNS根据你的内存和需求定一般 4 到 8 个就够了。每个连接的缓冲区大小可以动态调整比如主站连接给 2KB从站连接给 1KB。这里有个坑连接断开后要记得释放缓冲区。我见过有人忘了释放跑几天后内存耗尽程序重启。解决办法是在连接关闭的回调里调用ringbuf_destroy把buf和结构体都 free 掉。另外多连接下的超时检查要遍历所有连接不能只检查当前活跃的。我一般用一个定时器任务每 100ms 遍历一次连接数组检查每个连接的超时状态。5. 性能优化与扩展思路5.1 零拷贝优化上面的实现里数据从 TCP 缓冲区到环形缓冲区是一次拷贝从环形缓冲区到解析函数又是一次拷贝。两次拷贝在低频轮询下没问题但如果你的轮询频率很高比如 100ms 一次拷贝开销就不可忽略了。零拷贝的思路是让解析函数直接操作环形缓冲区里的数据不经过中间数组。具体做法是提供一个ringbuf_get_contiguous函数返回当前读指针位置和连续可读的字节数。如果数据跨越了缓冲区末尾就分两段处理。这样能省掉一次 memcpy实测在 240MHz 下能降低约 15% 的 CPU 占用。不过零拷贝的代码复杂度会上升尤其是处理回绕的时候。如果你的项目对性能要求不是极致建议先用两次拷贝的版本稳定后再优化。5.2 与 FreeRTOS 流缓冲区的结合FreeRTOS 本身提供了 Stream Buffer也可以用来做 TCP 数据缓存。它的优势是自带任务阻塞和唤醒机制不需要手动轮询。但 Stream Buffer 的缺点是只能单生产者单消费者而且不支持 peek 操作。对于 ModbusTCP 分片场景peek 是必须的所以我没有直接用 Stream Buffer而是自己实现了环形缓冲区。如果你想让代码更“FreeRTOS 原生”可以用 Message Buffer 加自定义 peek 逻辑但我觉得没必要。自己实现的环形缓冲区更灵活也更容易调试。5.3 扩展到 ModbusRTU over TCP有些场景下设备用的是 ModbusRTU over TCP也就是把 RTU 帧封装在 TCP 里传输。这种帧没有 MBAP 头而是用 3.5 个字符时间的静默间隔来分帧。在 TCP 里静默间隔没法直接检测所以需要根据功能码和字节数来推算帧长。我的做法是先判断是标准 ModbusTCP 还是 RTU over TCP。如果是 RTU就用功能码加字节数的方式计算帧长。比如功能码 03读保持寄存器响应帧的长度是 3 2 * 寄存器数量 2CRC。这个逻辑比 MBAP 头复杂但原理是一样的都是先确定帧长再切分。5.4 边缘计算场景下的数据预处理ESP32 做 ModbusTCP 采集很多时候不只是转发数据还要做预处理。比如计算平均值、最大值、最小值或者做阈值判断。这些逻辑可以放在解析函数里每收到一帧就更新统计值。这样上报到云端的数据量能大幅减少适合用 NB-IoT 或 LoRa 回传的场景。我做过一个项目ESP32 采集 10 台电表的电量数据本地计算 15 分钟平均值然后通过 MQTT 上报。这样云端只需要存平均值数据量从每分钟 10 条降到每 15 分钟 10 条流量费省了不少。6. 实操心得与避坑清单6.1 缓冲区大小的经验值场景推荐缓冲区大小说明单连接少量寄存器1KB够用省内存单连接大量寄存器2KB应对粘包和突发数据多连接每连接1-2KB根据连接数调整总内存带 PSRAM 的型号4-8KB可以更宽松这个表是我在多个项目中总结的基本上按这个来不会出问题。如果你不确定先从 2KB 开始跑一段时间看uxTaskGetStackHighWaterMark和堆剩余量再调整。6.2 常见踩坑记录第一个坑忘了处理 TCP 连接断开。ESP32 的recv返回 0 表示对方关闭了连接返回负数表示错误。我最初只处理了负数没处理 0结果连接断开后程序还在往缓冲区写数据状态机一直等不到完整帧卡死了。后来加了判断if (len 0) { 关闭连接释放资源 }。第二个坑字节序搞反了。Modbus 是大端ESP32 是小端。我一开始用memcpy直接把长度字段拷到uint16_t变量里结果读出来是反的。后来改成手动拼接(data[4] 8) | data[5]问题解决。第三个坑超时重置时没清空缓冲区。我最初只重置了状态机没清空缓冲区结果下次请求的响应和上次的残帧混在一起解析出乱七八糟的数据。后来在超时处理里加了rb-r rb-w彻底清空。第四个坑任务优先级设太高。我把 TCP 接收任务的优先级设成了 10结果 WiFi 任务被抢占网络反而更不稳定了。后来降到 5一切正常。ESP32 的 WiFi 任务优先级一般是 23 左右你的应用任务不要超过它。6.3 最后分享一个小技巧如果你在调试分片问题时发现日志太多看不过来可以加一个“只打印异常”的开关。比如只在recv返回的字节数小于预期或者状态机超时时才打印日志。正常流程不打印这样串口输出干净异常一目了然。这个技巧帮我省了很多时间。有一次现场调试设备每隔几小时才出一次分片问题如果一直开详细日志串口缓冲区很快就满了反而看不到关键信息。改成只打印异常后问题一出现就能抓到。这个项目后续还可以这样扩展把环形缓冲区和状态机封装成一个独立的 ModbusTCP 库支持主站和从站模式提供简单的 API 给上层调用。这样下次做新项目时直接拿来用不用再从头写分片逻辑了。

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

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

免费获取报价 →
↑