Fokajian

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

灰尘补偿算法笔记

一、算法概述

1.1 解决的核心问题

设备长期运行后,传感器积灰导致 AD 采样基线(零刻度)漂移,若仍用出厂基准值(PsAD)会导致浓度测量不准(误报 / 漏报),算法通过动态统计 + 自校准抵消基线漂移。

1.2 核心思路

  1. 数据统计:实时记录采样值与初始基准值的偏差,按 “10 天滚动窗口” 统计偏差出现频次;
  2. 校准触发:每 2 分钟(测试)/1 天(实际)触发周期检查,累计 10 天数据后启动校准;
  3. 动态校零:找到出现频次最高的偏差(最频偏差),将设备 “零刻度索引” 调整到该位置;
  4. 阈值同步:更新报警阈值适配新零刻度,补偿值持久化到 EEPROM(掉电不丢失)。

二、核心配置与数据结构

2.1 宏定义

宏名 取值 / 说明 原对应值
INDEX_SCOPE 21U _Scope
ENABLE_DOWN_OFFSET 1U(启用)/0U(禁用) _EnableDown
STATISTIC_DAY_COUNT 10U _10DAY
CYCLE_TRIGGER_THRESHOLD 12U(测试,2 分钟) 12
CYCLE_TRIGGER_THRESHOLD_REAL 98181U(实际,1 天) 98181
AD_VALUE_COUNT_MAX 0xFFFEU(计数上限,防溢出) 0xfffe
DUST_VALUE_EEPROM_ADDR 0x00U(补偿值存储地址) DustValueAddr
EEPROM_WRITE_ERR_FLAG 1U(EEPROM 写入错误标记) -

2.2 数据结构

typedef struct
{
    unsigned int  sec_counter;        // 秒计数器(触发周期逻辑)
    unsigned char day_index;          // 当前统计天数索引(0~9)
    unsigned char calibration_flag;   // 校准触发标记(1=需要校准)
    unsigned char base_index;         // 动态零值基准索引(核心补偿参数)
    signed char   offset_value;       // 校准计算出的偏移量(补偿量)
    unsigned int  smoke_ad_count[STATISTIC_DAY_COUNT][INDEX_SCOPE]; // 偏差统计数组
} DustData_T;

// 全局灰尘数据(可按需调整作用域)
static DustData_T g_dust_data = {0};

// 全局阈值
static signed int g_dust_compensate_value = 0;  // 总补偿值
static unsigned int g_normal_threshold = 2000;  // 平时阈值
static unsigned int g_alarm_threshold = 3000;   // 报警阈值
static unsigned int g_early_alarm_threshold = 2800; // 提前报警阈值

// 出厂基准阈值(常量,不随校准变化)
static const unsigned int BASE_NORMAL_THRESHOLD = 2000;
static const unsigned int BASE_ALARM_THRESHOLD = 3000;
static const unsigned int BASE_EARLY_ALARM_THRESHOLD = 2800;

三、算法核心流程

四、关键函数解析

4.1 函数总览

函数名 核心作用
DustDataClear 清空指定天数的偏差统计计数
AdjustBaseIndex 调整基准索引,清零旧索引区间计数(防历史数据干扰)
FindCalibrationOffset 查找最频索引,计算校准偏移量
UpdateAdValueCount 封装计数累加逻辑,防止溢出
CalculateOffsetIndex 计算采样值对应的偏移索引(核心映射逻辑)
UpdateAlarmThresholds 叠加补偿值,更新所有报警阈值
SaveAndVerifyDustValue 补偿值写入 EEPROM 并校验,失败触发故障处理
Dust_Calculate 核心主函数,整合周期判断、校准、实时统计逻辑

4.2 工具函数完整实现

4.2.1 DustDataClear - 清空指定天数统计数据

/**
 * @brief  清空指定天数的AD统计计数
 * @param  day: 要清空的天数索引(0~STATISTIC_DAY_COUNT-1)
 * @retval 无
 * @note   遍历指定天数的所有索引,将计数重置为0,为新统计周期准备
 */
static void DustDataClear(unsigned char day)
{
    unsigned char index = 0;
    // 遍历所有索引,清零计数
    do
    {
        g_dust_data.smoke_ad_count[day][index] = 0U;
        index++;
    } while (index < INDEX_SCOPE);
}

4.2.2 AdjustBaseIndex - 调整基准索引并清理旧数据

/**
 * @brief  调整基准索引并清零旧索引区间的计数
 * @param  old_base_index: 调整前的基准索引
 * @param  shift_num: 偏移量(可正可负,正=正向偏移,负=反向偏移)
 * @retval 调整后的新基准索引
 * @note   1. +INDEX_SCOPE避免负数取模异常,保证索引在0~INDEX_SCOPE-1;
 *         2. 清零旧索引到新索引区间的计数,避免历史数据干扰校准后统计
 */
static unsigned char AdjustBaseIndex(unsigned char old_base_index, signed char shift_num)
{
    unsigned char new_base_index = 0;
    unsigned char day = 0;
    unsigned char index = 0;

    // 计算新基准索引:+INDEX_SCOPE避免负数取模异常,保证结果在0~INDEX_SCOPE-1
    new_base_index = (old_base_index + INDEX_SCOPE + shift_num) % INDEX_SCOPE;

    // 清零旧索引到新索引之间的计数(避免历史数据干扰)
    if (shift_num > 0)
    {
        // 正向偏移:旧索引→新索引,正序清零
        for (day = 0; day < STATISTIC_DAY_COUNT; day++)
        {
            for (index = old_base_index; index != new_base_index; index++)
            {
                if (index >= INDEX_SCOPE)
                {
                    index = 0U;
                }
                g_dust_data.smoke_ad_count[day][index] = 0U;
            }
        }
    }
    else if (shift_num < 0)
    {
        // 反向偏移:旧索引→新索引,倒序清零
        for (day = 0; day < STATISTIC_DAY_COUNT; day++)
        {
            for (index = old_base_index; index != new_base_index; index--)
            {
                if (index >= INDEX_SCOPE)  // 无符号数减到0后再减会溢出,判断溢出
                {
                    index = INDEX_SCOPE - 1U;
                }
                g_dust_data.smoke_ad_count[day][index] = 0U;
            }
        }
    }
    // 偏移量为0时无需清零

    return new_base_index;
}

4.2.3 FindCalibrationOffset - 查找校准偏移量

/**
 * @brief  查找统计周期内计数最多的索引,计算偏移量(核心校准逻辑)
 * @param  current_base_index: 当前基准索引
 * @retval 计算出的补偿偏移量(可正可负)
 * @note   1. 最频索引=计数最多的索引,代表灰尘导致的稳定基线偏移;
 *         2. 启用向下偏移时,以(INDEX_SCOPE-1)/2为中点计算偏移,否则直接计算索引差
 */
static signed char FindCalibrationOffset(unsigned char current_base_index)
{
    signed char offset = 0;
    unsigned int max_count = 0U;
    unsigned char max_index = current_base_index;
    unsigned char day = 0;
    unsigned char index = 0;

    // 遍历10天的所有索引,找到计数最多的索引(最频索引)
    for (day = 0; day < STATISTIC_DAY_COUNT; day++)
    {
        for (index = 0; index < INDEX_SCOPE; index++)
        {
            if (g_dust_data.smoke_ad_count[day][index] > max_count)
            {
                max_count = g_dust_data.smoke_ad_count[day][index];
                max_index = index;
            }
        }
    }

    // 计算最频索引与当前基准索引的偏移量
    if (ENABLE_DOWN_OFFSET)
    {
        // 启用向下偏移时,以(INDEX_SCOPE-1)/2为中点计算偏移
        unsigned char mid_index = (INDEX_SCOPE - 1U) >> 1;
        offset = (max_index - mid_index) - (current_base_index - mid_index);
    }
    else
    {
        // 禁用向下偏移时,直接计算索引差
        offset = max_index - current_base_index;
    }

    return offset;
}

4.2.4 UpdateAdValueCount - 更新统计计数(防溢出)

/**
 * @brief  更新AD值统计计数(封装重复逻辑,防止溢出)
 * @param  day: 天数索引(0~STATISTIC_DAY_COUNT-1)
 * @param  index: 要累加的偏差索引(0~INDEX_SCOPE-1)
 * @retval 无
 * @note   计数上限为AD_VALUE_COUNT_MAX,避免溢出导致统计失真
 */
static void UpdateAdValueCount(unsigned char day, unsigned char index)
{
    // 计数累加,超过最大值则限制为最大值(防止溢出)
    if (g_dust_data.smoke_ad_count[day][index] < AD_VALUE_COUNT_MAX)
    {
        g_dust_data.smoke_ad_count[day][index]++;
    }
    // 已达最大值则不操作
}

4.2.5 CalculateOffsetIndex - 计算采样值偏移索引

/**
 * @brief  计算采样值与基准值的偏移索引
 * @param  smoke_ad: 实时AD采样值
 * @param  base_ad: 初始基准AD值(出厂零刻度)
 * @retval 计算后的偏移索引(0~INDEX_SCOPE-1)
 * @note   1. 差值>>4(÷16)过滤采样噪声,保留粗粒度偏差;
 *         2. 启用向下偏移时,正向偏差映射到中点右侧,反向映射到左侧;
 *         3. 禁用向下偏移时,反向偏差直接归基准索引
 */
static unsigned char CalculateOffsetIndex(unsigned int smoke_ad, unsigned int base_ad)
{
    unsigned int diff = 0U;
    unsigned char offset_index = 0U;
    unsigned char mid_index = (INDEX_SCOPE - 1U) >> 1;  // 索引中点(10)

    if (smoke_ad > base_ad)
    {
        // 正向偏差:采样值 > 基准值
        diff = smoke_ad - base_ad;
        diff = diff >> 4;  // 差值÷16,过滤噪声,保留粗粒度偏差

        // 限制偏移量上限
        if (ENABLE_DOWN_OFFSET)
        {
            if (diff >= mid_index)
            {
                diff = mid_index;
            }
            // 映射到中点右侧索引(正向偏差区)
            offset_index = (unsigned char)diff + mid_index;
        }
        else
        {
            if (diff >= (INDEX_SCOPE - 1U))
            {
                diff = INDEX_SCOPE - 1U;
            }
            offset_index = (unsigned char)diff;
        }
    }
    else
    {
        // 反向偏差:采样值 ≤ 基准值
        if (ENABLE_DOWN_OFFSET)
        {
            diff = base_ad - smoke_ad;
            diff = diff >> 4;  // 差值÷16

            // 限制偏移量上限
            if (diff >= mid_index)
            {
                diff = mid_index;
            }
            // 映射到中点左侧索引(反向偏差区)
            offset_index = mid_index - (unsigned char)diff;
        }
        else
        {
            // 禁用向下偏移时,反向偏差直接归基准索引
            offset_index = g_dust_data.base_index;
        }
    }

    // 计算最终索引:基准索引+偏移量,取模保证不越界
    offset_index = (offset_index + g_dust_data.base_index) % INDEX_SCOPE;

    return offset_index;
}

4.2.6 UpdateAlarmThresholds - 更新报警阈值

/**
 * @brief  更新报警阈值(叠加补偿值)
 * @param  无
 * @retval 无
 * @note   1. 偏移量×16还原原始AD偏差(反向抵消÷16操作);
 *         2. 所有阈值叠加总补偿值,适配新的零刻度基线
 */
static void UpdateAlarmThresholds(void)
{
    // 补偿值还原为原始AD差值(偏移量×16,反向抵消÷16操作)
    g_dust_compensate_value += (signed int)(g_dust_data.offset_value * 16);

    // 同步更新所有阈值(叠加补偿值)
    g_normal_threshold = BASE_NORMAL_THRESHOLD + g_dust_compensate_value;
    g_alarm_threshold = BASE_ALARM_THRESHOLD + g_dust_compensate_value;
    g_early_alarm_threshold = BASE_EARLY_ALARM_THRESHOLD + g_dust_compensate_value;
}

4.2.7 SaveAndVerifyDustValue - 补偿值持久化并校验

/**
 * @brief  保存补偿值到EEPROM并校验
 * @retval 0=成功,1=失败
 * @note   1. 需确保eep_input/eep_get/Fault_handling函数已实现;
 *         2. 校验失败触发故障处理,保证补偿值可靠性
 */
static unsigned char SaveAndVerifyDustValue(void)
{
    unsigned char err_flag = 0U;
    signed int saved_value = 0;

    // 写入补偿值到EEPROM(需替换为实际EEPROM写入函数)
    eep_input(DUST_VALUE_EEPROM_ADDR, g_dust_compensate_value);
    // 读取验证
    saved_value = eep_get(DUST_VALUE_EEPROM_ADDR);

    // 校验写入是否成功
    if (saved_value != g_dust_compensate_value)
    {
        err_flag = EEPROM_WRITE_ERR_FLAG;
        Fault_handling();  // 执行故障处理(需保证该函数已定义)
    }

    return err_flag;
}

4.3 核心计算主函数 - Dust_Calculate

/**
 * @brief  灰尘补偿核心计算函数(主入口)
 * @param  smoke_ad: 实时AD采样值
 * @param  base_ad: 初始基准AD值(出厂零刻度)
 * @retval 无
 * @note   整合“时间周期判断→校准逻辑→实时统计”全流程,是算法的核心入口
 */
void Dust_Calculate(unsigned int smoke_ad, unsigned int base_ad)
{
    unsigned char offset_index = 0U;

    /*************************** 阶段1:时间周期判断 ***************************/
    if (g_dust_data.sec_counter >= CYCLE_TRIGGER_THRESHOLD)
    {
        // 重置秒计数器
        g_dust_data.sec_counter = 0U;

        // 切换天数索引(循环0~STATISTIC_DAY_COUNT-1)
        g_dust_data.day_index++;
        if (g_dust_data.day_index >= STATISTIC_DAY_COUNT)
        {
            g_dust_data.day_index = 0U;
            // 10天周期到,触发校准标记
            g_dust_data.calibration_flag = 1U;
        }

        /*************************** 阶段2:校准逻辑 ***************************/
        if (g_dust_data.calibration_flag == 1U)
        {
            // 1. 查找最频偏差对应的补偿偏移量
            g_dust_data.offset_value = FindCalibrationOffset(g_dust_data.base_index);

            // 2. 清空当前天数的统计数据,为新周期准备
            DustDataClear(g_dust_data.day_index);

            // 3. 偏移量非0时执行补偿
            if (g_dust_data.offset_value != 0)
            {
                // 3.1 调整动态基准索引(核心补偿动作)
                g_dust_data.base_index = AdjustBaseIndex(g_dust_data.base_index, g_dust_data.offset_value);

                // 3.2 更新报警阈值(叠加补偿值)
                UpdateAlarmThresholds();

                // 3.3 持久化补偿值到EEPROM并校验
                (void)SaveAndVerifyDustValue();
            }
        }
    }
    // 秒计数器累加(需保证该函数被周期性调用,比如1秒1次)
    g_dust_data.sec_counter++;

    /*************************** 阶段3:实时AD偏差统计 ***************************/
    // 计算偏移索引
    offset_index = CalculateOffsetIndex(smoke_ad, base_ad);
    // 更新对应索引的计数
    UpdateAdValueCount(g_dust_data.day_index, offset_index);
}

五、优化亮点总结

优化方向 具体措施
命名语义化 Smokeadsmoke_ad_ScopeINDEX_SCOPE,见名知意
拆分大函数 Dust_Calculate拆分为 7 个工具函数,降低逻辑复杂度
消除魔法数 硬编码值(如0xfffe12)定义为宏,便于维护
逻辑分层 按 “时间周期→校准→实时统计” 分层,流程清晰
鲁棒性提升 无符号数溢出防护、计数先判断后累加、负数取模异常规避
注释增强 函数添加功能 / 参数注释,关键逻辑添加 “为什么这么做” 的说明

六、扩展建议

  1. 全局变量封装:将g_dust_data封装为单例结构体,避免全局变量污染;
  2. 抗干扰增强FindCalibrationOffset添加 “最小有效计数”(如计数 < 10 则不校准);
  3. 调试扩展:增加日志打印接口(如校准偏移量、阈值更新值),便于问题定位;
  4. 参数校验:添加输入参数合法性检查(如day_index超出范围时容错);
  5. 补偿回退:增加补偿值上限,超出合理范围则回退到 EEPROM 默认值。

七、关键知识点备注

7.1 负数取模异常规避

// 优化后写法:+INDEX_SCOPE保证被除数为正,取模结果0~INDEX_SCOPE-1
new_base_index = (old_base_index + INDEX_SCOPE + shift_num) % INDEX_SCOPE;
// 反例:直接计算可能得到负数,无符号转换后越界
new_base_index = (old_base_index + shift_num) % INDEX_SCOPE; // 错误

7.2 最频索引的意义

  • 最频索引 = 统计周期内计数最多的索引,代表 “灰尘导致的稳定基线偏移”;
  • 选最频值而非平均值:过滤偶然噪声(如单次大粉尘),匹配灰尘漂移 “慢变化、稳定” 的特性。
posted on 2025-12-26 23:08  Fokajian  阅读(3)  评论(0)    收藏  举报