如何统计过去x分钟内执行器启用时长?ESP32 Arduino C++实现
ESP32执行器滑动窗口时长统计最优方案
你原有数组思路的核心问题是删除过期头部记录时需要移动整段数组元素,时间复杂度O(n),启停频繁时会浪费不必要的CPU资源,同时没有利用「单次开启时长固定」这个前提做优化。下面两种方案都是嵌入式场景下经过验证的高效实现,全静态内存分配无碎片,所有核心操作都是O(1)复杂度,完全适配ESP32的Arduino开发环境。
方案1:环形缓冲区实现(通用兼容型,支持固定/可变单次开启时长)
这是工业级嵌入式设备做滑动窗口统计的标准实现,不需要动态内存,不需要移动元素,只需要维护两个索引变量即可:
- 预分配固定长度的32位无符号整数数组作为时间戳缓冲区,数组长度为「窗口内允许的最大启动次数+1」,单条记录仅占4字节,哪怕允许100次启动也只占404字节RAM,几乎无内存开销。
- 维护两个索引:
head指向窗口内最早的有效启动记录,count记录当前窗口内的有效启动次数,不需要额外维护尾指针。 - 核心逻辑:
- 每次准备启动执行器前,先从头部开始清理所有超出x分钟窗口的过期记录,仅移动
head索引、递减count即可,不需要搬移数组数据。 - 清理完成后直接用
有效记录数 * 单次固定开启时长计算当前窗口内的累计开启时长,超过阈值就触发冷却,禁止启动。 - 确认可以启动、执行器实际启动后,把当前时间戳写入缓冲区的对应位置,递增
count即可。
- 每次准备启动执行器前,先从头部开始清理所有超出x分钟窗口的过期记录,仅移动
- 扩展适配:如果后续需要改成可变开启时长,只要把缓冲区元素改成
{启动时间戳, 本次开启时长}的结构体,把固定乘法改成累加有效记录的时长即可,整体逻辑完全不需要改动。
核心参考代码:
// **************** 配置项 按需修改 **************** const uint32_t STAT_WINDOW_MS = 5 * 60 * 1000; // 统计窗口:过去5分钟 const uint32_t SINGLE_RUN_MS = 10 * 1000; // 执行器单次固定开启时长10秒 const uint32_t MAX_ALLOW_TOTAL_MS = 2 * 60 * 1000;// 窗口内最大允许累计开启2分钟 const uint16_t BUF_SIZE = MAX_ALLOW_TOTAL_MS / SINGLE_RUN_MS + 1; // 缓冲区长度 // ************************************************ uint32_t tsBuf[BUF_SIZE]; uint16_t bufHead = 0; uint16_t validCount = 0; // 启动前调用,返回true代表可以启动,false代表需要冷却 bool checkActuatorAvailable() { uint32_t now = millis(); // 清理过期记录 while (validCount > 0) { uint32_t oldestTs = tsBuf[bufHead]; // 无符号数减法自动处理millis()溢出问题,无需额外判断 if (now - oldestTs > STAT_WINDOW_MS) { bufHead = (bufHead + 1) % BUF_SIZE; validCount--; } else { break; } } return (validCount * SINGLE_RUN_MS) < MAX_ALLOW_TOTAL_MS; } // 执行器成功启动后调用,记录本次启动时间 void logActuatorStart() { uint16_t writePos = (bufHead + validCount) % BUF_SIZE; tsBuf[writePos] = millis(); validCount++; }
方案2:时间片位掩码实现(极致省内存型,仅适配固定单次开启时长场景)
如果你的统计精度不需要卡到毫秒级(允许秒级到十秒级的误差,防过热场景完全够用),可以用这个方案,整个统计逻辑只占8字节内存:
- 把x分钟的统计窗口拆成等长的时间片,比如每个时间片10秒,5分钟窗口就对应30个时间片,用一个64位无符号整数的每一位对应一个时间片,bit为1代表该时间片内执行器处于开启状态。
- 每到一个时间片周期,把这个64位整数整体左移1位,最低位补0,如果当前执行器处于开启状态,就把最低位设为1。
- 需要统计累计时长时,直接用内置函数
__builtin_popcountll(bitMask)统计整数里1的个数,乘以时间片长度就是累计开启时长,这个函数是CPU单指令实现的,运算速度极快。
实现注意事项
- 不要用动态容器(比如类似std::vector的动态数组)存储记录,嵌入式场景长期运行下动态内存申请释放很容易产生碎片,导致内存溢出崩溃,上述两种方案全是静态预分配内存,上电后内存占用固定,长期运行稳定性拉满。
- 时间计算统一用
millis()返回的uint32_t类型,直接做减法计算时间差,不需要额外写逻辑处理millis()约49天的溢出问题,C语言无符号整数的模运算特性会自动处理这种场景的时间差计算。 - 冷却逻辑可以直接复用统计结果:当累计时长超标后,持续调用检查函数,直到累计时长降到安全阈值以下再允许执行器启动即可,不需要额外存储冷却相关的时间变量。
内容的提问来源于stack exchange,提问作者Daan Theoden
相关产品推荐
相关产品推荐

