背景
随着智能设备对电池续航、充电管理和温控的需求不断增加,电池管理系统(BMS)中的 I2C 通信成为了一个核心技术。然而,I2C 作为一种基于总线的通信协议,随着设备数量的增加,容易出现总线竞争、超时、阻塞等问题,尤其在充电管理、电流/电压监控等高频场景中,I2C 总线的竞争会导致性能瓶颈和系统卡顿,最终影响用户体验和系统稳定性。
本文将分析一种通过 属性缓存 和 自适应刷新机制 来优化 I2C 访问的设计方案,结合噪声门限、窗口振幅等自适应算法来提升系统的鲁棒性和实时性。
设计的目标
减少 I2C 访问冲突
I2C 作为共享总线,当多个设备同时请求访问时,会导致总线的争用和锁竞争,尤其是在需要频繁访问的属性(如电流、电压、温度等)上,I2C 请求可能会阻塞其他关键操作,甚至导致超时和死锁。
降低 I2C 请求频率
频繁的 I2C 请求不仅增加了总线的负担,还导致了大量不必要的通信。在传统的方案中,很多属性在短时间内不会发生变化,但仍会频繁请求访问,造成资源浪费。
上层的所有数据请求,均变成读取缓存中的数据,但也仍然兼容实时读取的接口(强实时性)
保持系统稳定性和实时性
在电池管理系统中,某些属性(如电压、电流、温度等)对系统的稳定性至关重要。为确保系统在任何时刻都能及时响应变化,必须保证数据的一致性和实时性。通过自适应刷新机制,可以根据属性的重要性和变化频率动态调整刷新频率,避免频繁更新导致的性能下降。
方案原理
属性缓存机制
传统的 I2C 访问模式中,每次读取设备的状态时,都会直接发起 I2C 请求并读取数据,这可能导致频繁的总线竞争和性能下降。为了解决这个问题,我们引入了 属性缓存 机制:
缓存层:将每个 I2C 属性的值保存在本地缓存中,并且缓存值有一个有效期(TTL)。只有当缓存过期时,才会重新发起 I2C 请求。通过减少重复的 I2C 请求,显著降低了总线的负担。
缓存失效和刷新机制:当属性的缓存过期或收到请求刷新时,会通过后台任务(如 workqueue)异步刷新缓存,而不是在主线程中阻塞等待 I2C 请求完成。
这种机制的优点是能够减少 I2C 请求频率,并确保关键属性能在需要时及时刷新,同时避免了 I2C 总线的竞争和冲突。
自适应刷新机制
为了提高系统的实时性和鲁棒性,我们设计了一套自适应刷新机制,具体原理如下:
按时间敏感度分类
根据属性的变化频率和对系统的影响程度,将所有的 I2C 属性分为 FAST、MED、SLOW 和 STATIC 四类:
FAST 类:快速变化的属性,如电流、电压、状态等,需要较高的刷新频率,TTL 范围较小。
MED 类:中等变化频率的属性,如温度、SOC 等,TTL 范围较大。
SLOW 类:变化较慢的属性,如充电周期、电池健康等,TTL 范围更大。
STATIC 类:基本不变化的属性,如设备型号、设计容量等,TTL 非常大,甚至可以忽略刷新。
TTL的动态调整
TTL(Time-To-Live)是属性缓存的有效期,决定了缓存多久后过期。TTL 的值不仅与属性的时间敏感度相关,还会根据 变化频率 和 稳定性 动态调整:
显著变化:当属性的变化大于某个设定的阈值时,TTL 会缩短,强制进行快速刷新。
稳定状态:当属性变化较小时,TTL 会延长,减少不必要的刷新。
噪声抑制:通过 噪声门限 和 窗口振幅,避免因为测量噪声或微小波动导致过度频繁的刷新。
噪声门限与窗口振幅
噪声门限(Noise Threshold)和窗口振幅(Window Amplitude)是自适应刷新机制中的关键算法。它们的作用是过滤掉不重要的噪声变化,只对 显著变化 做出反应。
噪声门限:通过计算属性值的变化幅度,设定一个最小变化阈值,只有当变化大于该阈值时才认为是有效变化。例如,电流传感器可能会因为测量误差或 ADC 抖动导致微小的波动,但这些波动不足以影响系统决策,因此需要设置噪声门限来过滤掉这些微小的变化。
窗口振幅:通过滑动窗口计算一段时间内的 最大变化范围,如果窗口内的变化超过设定的阈值,则认为有显著变化。这能够有效捕捉到 慢变,避免系统错过重要的变化。
并发控制与总线熔断(bus-recovery)
在多属性并发刷新时,可能会导致 I2C 总线资源的竞争。为了避免这种情况,我们采用了 并发控制 和 总线熔断 的机制:
并发控制:通过使用
spinlock或其他锁机制,确保同一时刻只有一个任务访问同一个属性的缓存或 I2C 总线,避免并发冲突。总线熔断:当 I2C 总线出现过多错误或延迟时,系统会进行熔断处理,暂停所有的 I2C 访问,直到总线恢复正常。这样可以避免错误的连锁反应,保持系统稳定。
关键算法
自适应刷新算法
自适应刷新算法的核心目标是根据 属性的变化频率和敏感性 动态调整缓存的有效期(TTL)。TTL 决定了缓存何时过期,需要重新获取数据。算法的思路是,属性变化越频繁,其 TTL 越短;变化较少的属性可以延长 TTL,减少不必要的刷新请求。
基本概念
TTL (Time-To-Live):属性缓存的有效期。TTL 较短时,属性会更频繁地刷新,TTL 较长时,属性的刷新频率较低。
显著变化:当属性的变化超过了一个设定的阈值时,认为发生了显著变化,TTL 应缩短。
稳定状态:如果属性的值保持不变,TTL 可以逐渐增大,减少刷新频率。
自适应算法的核心逻辑
显著变化检测:
当属性的值发生显著变化时(例如,电流或电压的变化超过一定的阈值),TTL 会缩短,强制进行更频繁的刷新。
显著变化的判定通常通过比较当前值与前一个值之间的差异(
delta)来实现。
稳定状态检测:
当属性值稳定并且变化幅度较小(低于设定的噪声门限和窗口振幅),TTL 会逐渐增大,减少刷新频率。
自适应更新:
基于 噪声门限(noise threshold) 和 窗口振幅(range threshold),调整 TTL。噪声门限用于避免小幅度的波动影响刷新频率,而窗口振幅则用于检测慢变趋势。
算法公式
显著变化检测
其中:
Δ=∣current−previous∣是两次采样的差值
\epsilon是噪声门限或动态变化门限,通常通过噪声估计(EWMA)来计算
TTL 更新
这样,TTL 在 显著变化时会被缩短(0.5倍系数),在 稳定时逐渐延长(1.5倍系数),但仍然在设置的min_ttl ~ max_ttl内。系数可以按需调整
比如:
当发现温度在一段时间t内变化很小,那下一次的缓存刷新时间就变成1.5t,
然后再1.5t时间内如果发现仍然变化很小,则再下一次的缓存刷新时间就变成了1.5t*1.5,
然后再1.5t*1.5时间内,发现缓存刷新时间很显著,则下一次缓存刷新时间变为 1.5t*1.5 / 2
当然所有的刷新时间都必须满足在min_ttl和max_ttl之内!
自适应刷新伪代码
// 获取当前的显著变化(delta)和噪声估计值
delta = abs(current_value - previous_value);
noise = calculate_noise_estimate();
// 计算当前的门限(阈值),根据噪声门限和窗口振幅动态调整
eps = max(abs_eps_floor, k_noise * noise);
range = calculate_window_range();
// 判断是否为显著变化
if (delta > eps || range > range_eps_floor) {
// 显著变化:加速刷新,缩短 TTL
ttl = max(min_ttl, ttl / 2); // 缩短 TTL
stable_count = 0; // 重置稳定计数
} else {
// 稳定状态:延长 TTL,减少刷新频率
stable_count++;
if (stable_count >= 3) { // 3次稳定才延长TTL
ttl = min(max_ttl, ttl + (ttl / 2)); // 延长 TTL
}
}
// 更新缓存
update_cache(ttl, current_value);噪声门限算法
噪声门限算法的目标是避免将小的测量误差、噪声或量化误差当作有效变化来进行处理。通过设定一个阈值(eps),当属性值变化小于该阈值时,认为其变化不足以触发刷新的操作。
噪声门限的作用
过滤噪声:由于 ADC、传感器或者电路噪声,电流/电压等信号可能会产生小幅度波动,这些波动不代表实际的系统状态变化,通过噪声门限来避免这些微小的变化触发刷新。
减少不必要的刷新:如果每次微小变化都触发 I2C 请求,会导致 I2C 总线的拥塞。噪声门限能够有效减少这种情况。
噪声门限算法原理
噪声门限的基本思想是,通过计算变化值
delta(即当前值与前一个值的差异),与设定的门限值eps进行比较。当
delta > eps时,认为发生了显著变化,需要刷新缓存。当
delta <= eps时,认为变化太小,可以忽略,不进行刷新。
算法公式
噪声估计(EWMA)
这里Δ是当前值与前一个值的差异,noise是当前的噪声估计。
1/8的平滑系数可以改动,是一个暂时合理的数字,具体还是需要在实际中调整
过大的平滑系数会使得噪声估计对短期波动反应过于敏感,导致噪声门限过低,有可能会触发误判
过小的平滑系数会使得噪声估计对短期波动反应无反应,导致慢变的真实反映被平滑掉,系统无法相应这样的变化
门限计算
其中:
\epsilon是计算出的门限,用于判断是否触发刷新。
\text{abs\_eps\_floor}是设置的最低门限,防止门限过小导致不必要的触发。
EWMA(指数加权移动平均):用于计算当前的噪声估计,避免瞬时波动对系统的影响。通过 k_noise 参数来控制噪声敏感度,k_noise 越大,系统对噪声的敏感度越低。
变化显著判定:
只有当 变化幅度 delta 大于门限 $$\epsilon$$时,才认为是 显著变化。
噪声门限算法伪代码
// 更新噪声估计(采用EWMA)
noise = (current_delta - noise) / 8 + noise;
// 计算当前的噪声门限
eps = max(abs_eps_floor, k_noise * noise);
// 检测变化是否显著
if (abs(current_value - previous_value) > eps) {
// 发生显著变化
update_cache(current_value);
}窗口振幅算法
窗口振幅算法的作用是检测属性的 累计变化。即使单步的变化 delta 小于噪声门限,如果多次小的变化累计到一定程度,也应该触发刷新。
窗口振幅的作用
检测慢变变化:例如,充电电流可能逐步增加,而每次变化都很小。虽然这些变化小于噪声门限,但它们的累计效果可能对系统有影响。通过窗口振幅,我们可以检测到这些慢变的趋势。
避免TTL过度延长:在某些情况下,尽管变化幅度很小,但如果变化累积起来,它可能会导致系统状态发生显著变化。窗口振幅能够确保系统能够及时响应这些变化。
窗口振幅算法原理
在每次属性值变化时,都会将当前值与历史值存入一个滑动窗口。
通过计算窗口内的最大值和最小值,得到窗口振幅(
range = max - min)。如果窗口振幅超过设定的阈值,则认为发生了显著变化,并触发刷新。
算法公式
窗口内值更新
每次有新值时,都将该值插入滑动窗口,并更新窗口位置。
窗口振幅计算
计算滑动窗口内的 最大值与最小值的差,作为变化的幅度。
显著变化判定
如果 窗口振幅 超过了设定的阈值,认为发生了显著变化,触发刷新。
窗口振幅算法伪代码
// 更新滑动窗口
push_to_window(current_value);
// 计算窗口振幅
range = window_max - window_min;
// 如果振幅超过阈值,认为是显著变化
if (range > range_eps_floor) {
// 发生显著变化
update_cache(current_value);
}
引擎适配
引擎驱动
charger_property_engine.ko已在本地编译成功

https://gerrit.odm.mioffice.cn/c/kernel/msm-5.15/+/1100794
注册引擎
以下代码均为伪代码
引擎初始化
/// 定义需要加入到引擎的属性
enum charger_property_props {
CHG_PROP_STATUS = 0;
CHG_PROP_CURRENT;
CHG_PROP_VOLTAGE;
CHG_PROP_TEMP;
//...
};// 将引擎接口插入到charger device里
struct charger_device {
struct device *device;
//...
struct prop_engine *pe;
//...
};
static struct prop_engine *g_charger_pe = NULL; /// 全局静态变量,供稳定性用trace32 debug// 在charger_probe中
static int charger_probe(struct i2c_client *client)
{
struct charger_device *chg;
/* ... 原有代码 ... */
/* 创建Property Engine */
chg->pe = pe_create("charger_property_engine", &client->dev, chg,
&charger_pe_ops, charger_prop_descs,
ARRAY_SIZE(charger_prop_descs));
g_charger_pe = chg->pe;
/* 设置日志级别(可选) */
pe_set_log_level(chg->pe, PE_LOG_DEBUG); // 最高级别,记录所有日志
/* 初始化debugfs(可选) */
#ifdef CONFIG_DEBUG_FS
pe_debugfs_init(chg->pe);
#endif
return 0;
}属性描述符配置
所有参数值仅供参考,需要按照实际情况调整
static const struct pe_prop_desc charger_prop_descs[] = {
/* 状态寄存器 - 快速变化 */
[CHG_PROP_STATUS] = {
.name = "status",
.reg = CHG_REG_STATUS,
.mask = 0x0F, /* 低4位 */
.shift = 0,
.scale = 1,
.offset = 0,
.cls = PE_FAST,
.min_ttl_ms = 100, /* 最小100ms刷新 */
.max_ttl_ms = 1000, /* 最大1s刷新 */
.k_noise = 2,
.abs_eps_floor = 1, /* 变化1个单位就认为显著 */
.range_eps_floor = 3,
.win_size = 0, /* 状态位不需要窗口 */
.safe_default = 0, /* 默认状态:未知 */
.writable = false,
.min_req_gap_ms = 50, /* 最小请求间隔50ms */
},
/* 电压 - 中等变化,0.01V/LSB */
[CHG_PROP_VOLTAGE] = {
.name = "voltage",
.reg = CHG_REG_VOLTAGE,
.mask = 0xFF,
.shift = 0,
.scale = 10, /* 10mV/LSB */
.offset = 0,
.cls = PE_MED,
.min_ttl_ms = 200,
.max_ttl_ms = 5000,
.k_noise = 3,
.abs_eps_floor = 20, /* 20mV变化 */
.range_eps_floor = 50,
.win_size = 8, /* 8个样本窗口 */
.safe_default = 3700, /* 3.7V默认 */
.writable = false,
.min_req_gap_ms = 100,
},
/* 电流 - 快速变化,0.05A/LSB */
[CHG_PROP_CURRENT] = {
.name = "current",
.reg = CHG_REG_CURRENT,
.mask = 0xFF,
.shift = 0,
.scale = 50, /* 50mA/LSB */
.offset = 0,
.cls = PE_FAST,
.min_ttl_ms = 100,
.max_ttl_ms = 2000,
.k_noise = 2,
.abs_eps_floor = 50, /* 50mA变化 */
.range_eps_floor = 100,
.win_size = 8,
.safe_default = 0,
.writable = false,
.min_req_gap_ms = 50,
},
/* 温度 - 慢速变化,0.5°C/LSB */
[CHG_PROP_TEMP] = {
.name = "temperature",
.reg = CHG_REG_TEMP,
.mask = 0xFF,
.shift = 0,
.scale = 5, /* 0.5°C/LSB */
.offset = -400, /* 偏移-40°C */
.cls = PE_SLOW,
.min_ttl_ms = 1000,
.max_ttl_ms = 30000,
.k_noise = 4,
.abs_eps_floor = 5, /* 0.5°C变化 */
.range_eps_floor = 10,
.win_size = 16,
.safe_default = 250, /* 25°C默认 */
.writable = false,
.min_req_gap_ms = 500,
},
/* 故障状态 - 快速变化 */
[CHG_PROP_FAULT_STATUS] = {
.name = "fault_status",
.reg = CHG_REG_FAULT,
.mask = 0x1F,
.shift = 0,
.scale = 1,
.offset = 0,
.cls = PE_FAST,
.min_ttl_ms = 100,
.max_ttl_ms = 1000,
.k_noise = 1,
.abs_eps_floor = 0, /* 任何变化都重要 */
.range_eps_floor = 0,
.win_size = 0,
.safe_default = 0, /* 默认无故障 */
.writable = false,
.min_req_gap_ms = 50,
},
/* 充电使能 - 静态,可写 */
[CHG_PROP_CHARGE_ENABLE] = {
.name = "charge_enable",
.reg = CHG_REG_CONTROL,
.mask = 0x01,
.shift = 0,
.scale = 1,
.offset = 0,
.cls = PE_STATIC,
.min_ttl_ms = 1000,
.max_ttl_ms = 60000,
.k_noise = 0,
.abs_eps_floor = 0,
.range_eps_floor = 0,
.win_size = 0,
.safe_default = 0,
.writable = true,
.reg_wr = CHG_REG_CONTROL,
.mask_wr = 0x01,
.shift_wr = 0,
.min_req_gap_ms = 100,
},
/* 终止电流 - 静态,可写 */
[CHG_PROP_TERMINATION_CURRENT] = {
.name = "termination_current",
.reg = CHG_REG_TERM_CURRENT,
.mask = 0x0F,
.shift = 0,
.scale = 100, /* 100mA/LSB */
.offset = 100, /* 基础100mA */
.cls = PE_STATIC,
.min_ttl_ms = 1000,
.max_ttl_ms = 60000,
.k_noise = 0,
.abs_eps_floor = 0,
.range_eps_floor = 0,
.win_size = 0,
.safe_default = 500, /* 默认500mA */
.writable = true,
.reg_wr = CHG_REG_TERM_CURRENT,
.mask_wr = 0x0F,
.shift_wr = 0,
.min_req_gap_ms = 100,
},
};属性解析:
name: 纯描述字符串
reg: 寄存器地址
mask/shift : field = (raw & mask) >> shift
比如 STATUS寄存器的低四位
.mask = 0x0F, .shift = 0
scale/offset: 把field转化成对外物理单位的线性映射
如何设置取决于读出来的值,以及想要展示出来的值,这个是一种线性映射关系
Value = field * scale +offset
注意: 单位必须与缓存/上层所希望看到的单位一致(mv,mA,℃)
电压:
scale=10表示 10mV/LSB,输出单位是 mV(3700 表示 3.7V)电流:
scale=50表示 50mA/LSB,输出单位是 mA温度:
scale=5, offset=-400
Cls : PE_FAST / PE_MED / PE_SLOW / PE_STATIC
决定属性的时间敏感度和调度优先级:
FAST:电流/电压/在线/状态(UI、策略、插拔窗口需要快)
MED:温度、SOC(中等)
SLOW:cycle_count、charge_full(很慢)
STATIC:model_name、配置项(几乎不变)
.min_ttl_ms/.max_ttl_msTTL 是“缓存有效期”。
now-ts <= ttl就直接返回缓存。自适应算法会在
[min_ttl, max_ttl]之间调节:变化显著 → TTL 变小(更快刷新)
稳定 → TTL 变大(更少 I2C 读)
k_noise
动态门限中噪声倍数:
eps = max(abs_eps_floor, k_noise * noiseEWMA)越大越“钝”(更不容易判显著变化)。
abs_eps_floor
最低绝对门限(底噪下限),防止 noiseEWMA 很小时 eps 太小。白话就是:只要当变化超过abs_eps_floor设定的值才是有效的,否则都当作噪声忽略
单位和 value 一致(电流用 mA、电压用 mV、温度用 0.1°C)。
比如:
电压:abs_eps_floor=20 → 20mV 内当噪声
电流:abs_eps_floor=50 → 50mA 内当噪声
status:abs_eps_floor=1 → 低4位状态变 1 就显著
fault_status:写 abs_eps_floor=0 “任何变化都重要”
win_size
滑动窗口长度(8/16 常见)
采样周期×win_size ≈ 观察时间窗
200ms×8=1.6s
200ms×16=3.2s
range_eps_floor
窗口振幅阈值下限(range=max-min)
单位同 value
可以这么理解:
比如电压:range_ops_floor = 50 -> 50mV的窗口峰值算累计变化
电流:range_ops_floor = 100 -> 100mA的累计变化
Temp: win_size=16, range_ops_floor=10,例子里温度的单位是0.1℃,所有10就是1摄氏度,1摄氏度的累计变化
safe_default
这是一种默认态
min_req_gap_ms:
这是同一个属性被连续request_refresh的最小间隔,主要目的就是为了防止高频繁触发refresh
建议:FAST:50ms / MED: 100ms
也支持单属性单独定制比如 TEMP: 500ms
Writable:
是否支持写 目前仅预留
reg_wr/mask_wr/shift_wr
写哪个寄存器,写哪个域, (目前仅预留)
reg/mask/shift/scale/offset : 决定最终的寄存器值,需要根据实际调整参数以获得最佳的数据
cls/min/max_ttl : 决定刷新频率的范围
abs_eps_floor/knoise : 决定多大范围的变化才算显著变化(抑制噪声),如果不需要抑制噪声则knoise设置为0,win_size/range_ops_floor : 决定“慢变累计是否触发显著”(防止TTL过度拉长)
safe_default : 决定没有值时返回什么(为了稳定性)
min_req_gap_ms : 决定 “请求刷新的间隔”,防止刷新风暴
writeable/reg_wr/... : 决定写操作,暂时仅预留接口
封装读写函数
/* Property Engine操作函数 */
static int charger_pe_read_reg(void *ctx, u8 reg, u8 *val)
{
struct charger_device *chg = ctx;
int ret;
/* 使用mutex保护I2C访问 */
mutex_lock(&chg->lock);
ret = i2c_smbus_read_byte_data(chg->client, reg);
mutex_unlock(&chg->lock);
if (ret < 0) {
dev_err(chg->dev, "Failed to read reg 0x%02x: %d\n", reg, ret);
return ret;
}
*val = (u8)ret;
return 0;
}
static int charger_pe_write_reg(void *ctx, u8 reg, u8 val)
{
struct charger_device *chg = ctx;
int ret;
/* 使用mutex保护I2C访问 */
mutex_lock(&chg->lock);
ret = i2c_smbus_write_byte_data(chg->client, reg, val);
mutex_unlock(&chg->lock);
if (ret < 0) {
dev_err(chg->dev, "Failed to write reg 0x%02x: %d\n", reg, ret);
return ret;
}
return 0;
}
static const struct pe_ops charger_pe_ops = {
.read_reg = charger_pe_read_reg,
.write_reg = charger_pe_write_reg,
};获取缓存
自适应获取缓存
目标:将目前驱动中所有的读写i2c的寄存器操作的接口都改为pe_get_cached函数
参考伪代码:
static int charger_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
struct charger_device *chg = power_supply_get_drvdata(psy);
s32 cached_val;
int ret;
switch (psp) {
case POWER_SUPPLY_PROP_STATUS:
ret = pe_get_cached(chg->pe, CHG_PROP_STATUS, &cached_val);
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
ret = pe_get_cached(chg->pe, CHG_PROP_VOLTAGE, &cached_val);
break;
//...
}特殊事件获取缓存
目的:一些特殊情况,需要立即刷新缓存,通过pe_on_event通知引擎进行处理
/* 系统唤醒时触发事件 */
static int charger_resume(struct device *dev)
{
struct charger_device *chg = dev_get_drvdata(dev);
if (chg && chg->pe) {
pe_on_event(chg->pe);
}
return 0;
}
/* USB插拔事件 */
static void charger_usb_event(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct charger_device *chg = container_of(nb, struct charger_device,
usb_notifier);
if (chg && chg->pe) {
pe_on_event(chg->pe);
}
}引擎注销
需要注销初始化的pe
static int charger_remove(struct i2c_client *client)
{
struct charger_device *chg = i2c_get_clientdata(client);
if (chg->pe) {
pe_destroy(chg->pe);
g_charger_pe = NULL;
chg->pe = NULL;
}
if (chg->psy)
power_supply_unregister(chg->psy);
mutex_destroy(&chg->lock);
return 0;
}debug
全局变量g_charger_pe
这个变量是charger property engine引擎的实例的静态变量,当系统出现异常后,用trace32/crash 可以直接获取这个静态变量检查核心配置
debugfs
/sys/kernel/debug/charger_property_engine/log: 输出此模块的日志
echo "clear" > log ,可以清除日志缓存
/sys/kernel/debug/charger_property_engine/log_level: 调整模块日志等级
echo "ERROR" > log_level
echo "3" > log_level