
说明本文重点讲解标志位窗口方案实现 ModbusTCP 分片缓存不涉及 xTaskGetTickCount 计时漂移修复聚焦业务逻辑设计、结构体定义、代码、测试方法。一、需求背景硬件ESP32作为 ModbusTCP 服务端接收 TCP 客户端ModbusPoll下发标准 ModbusTCP 报文主要 03 读保持寄存器指令。 业务链路TCP 收到报文 → 解析 ModbusTCP 请求 → 匹配缓存。如果缓存命中直接返回缓存数据不经过 RF 模块抄表提升响应速度如果缓存未命中下发 RF 抄表收到从站返回数据后存入缓存缓存规则每一条独立的 Modbus 请求单独管理生命周期。一条请求唯一标识从站地址 功能码 寄存器起始地址 寄存器长度超时规则某条缓存连续 5 分钟没有被客户端访问则缓存失效EN0只要 5 分钟窗口内被访问过缓存继续保留重新进入下一轮 5 分钟检测窗口。选型标志位窗口方案不使用每个缓存条目单独保存时间戳。 原理用一个全局的 5 分钟检测窗口窗口内用access_flag标记该缓存条目是否被访问窗口到期统一遍历判断所有缓存。二、核心原理大白话想象有一个 5 分钟的检查窗口窗口开启瞬间所有有效缓存的access_flag全部清零在这 5 分钟内只要客户端访问这条缓存就把这条缓存的access_flag 15 分钟窗口时间到遍历全部缓存槽位缓存使能EN1并且access_flag 1窗口内有访问缓存保留access_flag清零等待下一轮窗口缓存使能EN1并且access_flag 0整个 5 分钟没人访问缓存失效EN0清空缓存数据窗口重置开启新一轮 5 分钟计时。优点不需要每个缓存项存独立时间戳内存开销小逻辑简单调试直观日志打印EN、access_flag就能直接看状态每条缓存独立判断互不干扰。 局限超时是窗口粒度最大误差等于窗口周期5 分钟业务允许这种误差时非常适合。三、结构体定义分片缓存tcp_section[]就是分片缓存数组SECTION_NUM代表最大缓存条目数量最多同时缓存 N 条不同 Modbus 请求。// 缓存条目结构体 typedef struct { uint8_t EN; // 缓存总使能标志 1:有效 0:失效 uint8_t addr; // Modbus从站地址 uint8_t funcode; // 功能码03/04 uint16_t reg_addr; // 寄存器起始地址 uint16_t reg_len; // 读取寄存器长度 uint8_t buf[BUF_SIZE]; // 缓存的Modbus应答报文数据 uint16_t buflen; // 缓存数据有效长度 uint8_t access_flag; // 【核心标志位】本窗口是否被访问 1访问过 0未访问 }format_typedef; // 全局缓存数组 format_typedef tcp_section[SECTION_NUM] {0};字段说明EN缓存是否可用。EN0 时这条缓存槽位是空的可以存入新请求EN1 代表缓存有效可以应答客户端。access_flag窗口访问标记标志位只在 5 分钟窗口内生效不是计时器。四元组addr,funcode,reg_addr,reg_len作为匹配 key用来判断新来 TCP 报文是否命中已有缓存。四、全局变量与宏定义#define TAG TcpTrans #define SECTION_WINDOW_TICK 300000 //5分钟窗口计数阈值 uint32_t section_window_tick 0; //全局窗口计时器五、TCP 接收回调收到 ModbusTCP 报文匹配缓存TcpTrans_Rec_Callback是 TCP 接收回调收到客户端 ModbusTCP 报文后执行解析 ModbusTCP提取从站地址、功能码、起始寄存器、寄存器长度遍历tcp_section数组逐个对比四元组如果匹配成功命中缓存将access_flag 1直接读取缓存 buf 回复客户端如果遍历完没有匹配缓存寻找 EN0 的空槽位新建缓存EN1、access_flag1把请求送入 RF 任务去抄表。void TcpTrans_Rec_Callback(uint8_t *buf,uint16_t len,uint8_t sock) { uint8_t i,j; UTIL_BUFFRCV_W(TAG,buf,len); // 省略ModbusTCP报文解析得到addr,funcode,reg_addr,reg_len //第一步遍历缓存查找是否存在匹配的条目 for(i 0; i SECTION_NUM; i) { if(tcp_section[i].EN 1) { if( tcp_section[i].addr addr tcp_section[i].funcode funcode tcp_section[i].reg_addr reg_addr tcp_section[i].reg_len reg_len ) { //命中缓存置访问标志位 tcp_section[i].access_flag 1; UTIL_LOG_E(TAG,section[%d]命中缓存access_flag置1,i); //读取缓存数据组装TCP应答报文发送给客户端 //......发送代码省略 return; } } } //走到这里没有命中缓存寻找空闲槽位新建缓存 for(j 0; j SECTION_NUM; j) { if(tcp_section[j].EN 0) { UTIL_LOG_E(TAG,新建缓存 section[%d],j); tcp_section[j].EN 1; tcp_section[j].addr addr; tcp_section[j].funcode funcode; tcp_section[j].reg_addr reg_addr; tcp_section[j].reg_len reg_len; tcp_section[j].access_flag 1; //新建缓存本窗口标记为已访问 //把请求压入消息队列交给RF任务去抄表 osMessageQueuePut(tcp_trans.TxQueue,Tbuf,0,0); break; } } }关键点只要访问缓存access_flag 必须置 1告诉超时检测逻辑这个窗口内这条指令有人访问不能过期。六、5 分钟窗口轮询检测逻辑标志位核心代码放在网口 / 状态机轮询任务里面循环执行section_window_tick持续累加。 当计数达到阈值触发一次全缓存遍历执行标志位判断。// 放在while(1)轮询任务中 section_window_tick; if(section_window_tick SECTION_WINDOW_TICK) { section_window_tick 0; //重置窗口开启下一个5分钟 UTIL_LOG_E(TAG,5min窗口到达遍历检查section缓存); for(uint8_t i 0; i SECTION_NUM; i) { if(tcp_section[i].EN 1) { if(tcp_section[i].access_flag 0) { //整个5分钟窗口没有收到这条指令缓存失效 UTIL_LOG_E(TAG,section[%d] 5min无访问缓存失效,i); tcp_section[i].EN 0; tcp_section[i].buflen 0; memset(tcp_section[i].buf,0,sizeof(tcp_section[i].buf)); } else { //窗口内有访问缓存保留清空access_flag等待下一轮窗口 UTIL_LOG_E(TAG,section[%d]窗口内有访问保留缓存清空access_flag,i); tcp_section[i].access_flag 0; } } else { //EN0空槽位跳过 UTIL_LOG_E(TAG,section[%d]缓存槽位空闲,i); } } }逻辑拆解窗口到期进入 for 循环遍历全部分片缓存有效缓存EN1access_flag 05 分钟窗口全程没有访问 → 关闭 EN清空缓存数据access_flag 1窗口内有访问保留缓存清零 access_flag准备下一轮计时EN0空闲槽位跳过。注意access_flag只在窗口到期时统一清零收到报文只做置 1不做清零 这是标志位窗口方案最核心的要点不要写反。七、断网 / 重连场景缓存整体清空TCP 断开、WiFi 重连时所有缓存全部失效防止旧脏缓存残留for(uint8_t i 0 ; iSECTION_NUM; i) { tcp_section[i].EN 0; tcp_section[i].access_flag 0; tcp_section[i].buflen 0; memset(tcp_section[i].buf,0,sizeof(tcp_section[i].buf)); }八、ModbusPoll 测试方案验证分片缓存 标志位超时测试环境电脑 ModbusPoll 作为 TCP 客户端连接 ESP32 设备。 最少设备1 台 ESP32 ModbusPoll 软件不需要额外硬件。测试用例用例 1新建缓存命中缓存ModbusPoll 下发 03 读寄存器指令 A从站 1起始 0长度 10ESP32 无缓存触发 RF 抄表收到数据存入 section 缓存EN1access_flag1再次下发指令 A代码命中缓存access_flag 被置 1直接返回缓存数据不再触发 RF 抄表。用例 2多条分片缓存独立管理下发指令 A存入 section [0]下发指令 B从站 2起始 10长度 5存入 section [1]A、B 两条缓存互相独立持续轮询 AB 不轮询5 分钟窗口到期后B 缓存失效A 保持有效。用例 35 分钟超时失效下发指令 A建立缓存停止发送指令 A等待 5 分钟窗口到达窗口触发遍历检测 access_flag0 → EN 置 0缓存清空再次下发 A会重新新建缓存重新走 RF 抄表流程。用例 4持续轮询缓存永久保活循环下发指令 A每几十秒轮询一次每个窗口内 access_flag 都会被置 1每次窗口到期access_flag 清零缓存 EN 保持 1缓存永久有效。