灰尘补偿算法笔记
一、算法概述
1.1 解决的核心问题
设备长期运行后,传感器积灰导致 AD 采样基线(零刻度)漂移,若仍用出厂基准值(PsAD)会导致浓度测量不准(误报 / 漏报),算法通过动态统计 + 自校准抵消基线漂移。
1.2 核心思路
- 数据统计:实时记录采样值与初始基准值的偏差,按 “10 天滚动窗口” 统计偏差出现频次;
- 校准触发:每 2 分钟(测试)/1 天(实际)触发周期检查,累计 10 天数据后启动校准;
- 动态校零:找到出现频次最高的偏差(最频偏差),将设备 “零刻度索引” 调整到该位置;
- 阈值同步:更新报警阈值适配新零刻度,补偿值持久化到 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);
}
五、优化亮点总结
| 优化方向 |
具体措施 |
| 命名语义化 |
Smokead→smoke_ad、_Scope→INDEX_SCOPE,见名知意 |
| 拆分大函数 |
原Dust_Calculate拆分为 7 个工具函数,降低逻辑复杂度 |
| 消除魔法数 |
硬编码值(如0xfffe、12)定义为宏,便于维护 |
| 逻辑分层 |
按 “时间周期→校准→实时统计” 分层,流程清晰 |
| 鲁棒性提升 |
无符号数溢出防护、计数先判断后累加、负数取模异常规避 |
| 注释增强 |
函数添加功能 / 参数注释,关键逻辑添加 “为什么这么做” 的说明 |
六、扩展建议
- 全局变量封装:将
g_dust_data封装为单例结构体,避免全局变量污染;
- 抗干扰增强:
FindCalibrationOffset添加 “最小有效计数”(如计数 < 10 则不校准);
- 调试扩展:增加日志打印接口(如校准偏移量、阈值更新值),便于问题定位;
- 参数校验:添加输入参数合法性检查(如
day_index超出范围时容错);
- 补偿回退:增加补偿值上限,超出合理范围则回退到 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 最频索引的意义
- 最频索引 = 统计周期内计数最多的索引,代表 “灰尘导致的稳定基线偏移”;
- 选最频值而非平均值:过滤偶然噪声(如单次大粉尘),匹配灰尘漂移 “慢变化、稳定” 的特性。