☰
ESP32 ModbusTCP 分片缓存
2026/10/5 6:12:41 网站建设 项目流程

说明:本文重点讲解标志位窗口方案实现 ModbusTCP 分片缓存,不涉及 xTaskGetTickCount 计时漂移修复,聚焦业务逻辑设计、结构体定义、代码、测试方法。

一、需求背景

硬件:ESP32,作为 ModbusTCP 服务端,接收 TCP 客户端(ModbusPoll)下发标准 ModbusTCP 报文(主要 03 读保持寄存器指令)。 业务链路:TCP 收到报文 → 解析 ModbusTCP 请求 → 匹配缓存。

  • 如果缓存命中:直接返回缓存数据,不经过 RF 模块抄表,提升响应速度;
  • 如果缓存未命中:下发 RF 抄表,收到从站返回数据后存入缓存;
  • 缓存规则:每一条独立的 Modbus 请求单独管理生命周期。

一条请求唯一标识:从站地址 + 功能码 + 寄存器起始地址 + 寄存器长度

  • 超时规则:某条缓存,连续 5 分钟没有被客户端访问,则缓存失效(EN=0);
  • 只要 5 分钟窗口内被访问过,缓存继续保留,重新进入下一轮 5 分钟检测窗口。

选型:标志位窗口方案,不使用每个缓存条目单独保存时间戳。 原理:用一个全局的 5 分钟检测窗口,窗口内用access_flag标记该缓存条目是否被访问;窗口到期统一遍历判断所有缓存。

二、核心原理(大白话)

想象有一个 5 分钟的检查窗口:

  1. 窗口开启瞬间,所有有效缓存的access_flag全部清零;
  2. 在这 5 分钟内,只要客户端访问这条缓存,就把这条缓存的access_flag = 1;
  3. 5 分钟窗口时间到,遍历全部缓存槽位:
    • 缓存使能EN=1,并且access_flag ==1:窗口内有访问,缓存保留,access_flag清零,等待下一轮窗口;
    • 缓存使能EN=1,并且access_flag ==0:整个 5 分钟没人访问,缓存失效,EN=0,清空缓存数据;
  4. 窗口重置,开启新一轮 5 分钟计时。

优点:

  1. 不需要每个缓存项存独立时间戳,内存开销小;
  2. 逻辑简单,调试直观,日志打印EN、access_flag就能直接看状态;
  3. 每条缓存独立判断,互不干扰。 局限:超时是窗口粒度,最大误差等于窗口周期(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:缓存是否可用。EN=0 时,这条缓存槽位是空的,可以存入新请求;EN=1 代表缓存有效,可以应答客户端。
  • 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 报文后执行:

  1. 解析 ModbusTCP,提取从站地址、功能码、起始寄存器、寄存器长度;
  2. 遍历tcp_section数组,逐个对比四元组;
  3. 如果匹配成功(命中缓存):将access_flag =1,直接读取缓存 buf 回复客户端;
  4. 如果遍历完没有匹配缓存:寻找 EN=0 的空槽位,新建缓存,EN=1、access_flag=1,把请求送入 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 { //EN=0,空槽位,跳过 UTIL_LOG_E(TAG,"section[%d]缓存槽位空闲",i); } } }

逻辑拆解

  1. 窗口到期,进入 for 循环遍历全部分片缓存;
  2. 有效缓存EN=1:
    • access_flag == 0:5 分钟窗口全程没有访问 → 关闭 EN,清空缓存数据;
    • access_flag == 1:窗口内有访问,保留缓存,清零 access_flag,准备下一轮计时;
  3. EN=0:空闲槽位,跳过。

注意:access_flag只在窗口到期时统一清零,收到报文只做置 1,不做清零! 这是标志位窗口方案最核心的要点,不要写反。

七、断网 / 重连场景:缓存整体清空

TCP 断开、WiFi 重连时,所有缓存全部失效,防止旧脏缓存残留:

for(uint8_t i = 0 ; i<SECTION_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:新建缓存,命中缓存
  1. ModbusPoll 下发 03 读寄存器指令 A(从站 1,起始 0,长度 10);
  2. ESP32 无缓存,触发 RF 抄表,收到数据存入 section 缓存,EN=1,access_flag=1;
  3. 再次下发指令 A,代码命中缓存,access_flag 被置 1,直接返回缓存数据,不再触发 RF 抄表。
用例 2:多条分片缓存独立管理
  1. 下发指令 A,存入 section [0];
  2. 下发指令 B(从站 2,起始 10,长度 5),存入 section [1];
  • A、B 两条缓存互相独立;持续轮询 A,B 不轮询;5 分钟窗口到期后,B 缓存失效,A 保持有效。
用例 3:5 分钟超时失效
  1. 下发指令 A,建立缓存;
  2. 停止发送指令 A,等待 5 分钟窗口到达;
  3. 窗口触发遍历,检测 access_flag=0 → EN 置 0,缓存清空;
  4. 再次下发 A,会重新新建缓存,重新走 RF 抄表流程。
用例 4:持续轮询,缓存永久保活
  1. 循环下发指令 A,每几十秒轮询一次;
  2. 每个窗口内 access_flag 都会被置 1;
  3. 每次窗口到期,access_flag 清零,缓存 EN 保持 1,缓存永久有效。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询