解读 92 · 电压遥测与电量计:BATTERY_FULL_V/EMPTY_V
解读对象:
duck-control/src/model.rs(241 行,电池节 107–135)
需要的前置:解读 45("没有电量计"这个前提)、解读 93(换电压时这两个常量怎么改)
model.rs 的电池节开头就交代了硬件真相:"There is no fuel gauge and no ADC"(model.rs:109)——整机没有任何电量测量电路。那电量百分比从哪来?答案:舵机自己。每个舵机会回报自己的供电电压,主机把它当作"从总线上看到的电池"。理解了这个出身,后面每个数字的怪脾气都说得通。
1. 测量路径:寄存器 144 起 3 字节
bus.rs 的慢速块用一次事务读回电压和温度:SLOW_READ_ADDR: u8 = 144(bus.rs:44)——寄存器 144 是 present_input_voltage(u16,0.1 V/LSB,bus.rs:47),146 是 present_temperature(u8),三个字节,电压与热学共用一次 sync_read(bus.rs:529)。换算先记住:读数 74 就是 7.4 V。
2. 两个常量:满 8.2、空 6.6
BATTERY_FULL_V = 8.2(model.rs:115)——注释:"Off a full charge, under load"。NP-F550 是 2S 锂电,满电开路 8.4 V,带载会塌;8.2 是"满电且在跑"的实测口径。BATTERY_EMPTY_V = 6.6(model.rs:119)——"the sag floor":低于它机器人开始费劲,远早于电池自己的保护动作。注释特意区分:这是"我们的空",不是"电芯的空"。
3. battery_percent:一条线性式和它的边界
((volts - BATTERY_EMPTY_V) / (BATTERY_FULL_V - BATTERY_EMPTY_V)).clamp(0.0, 1.0) * 100.0
f64 是 Rust 的 64 位浮点数,.clamp(0.0, 1.0) 把结果夹在区间内。算例:读数 7.4 V → (7.4−6.6)/(8.2−6.6) = 0.8/1.6 = 50%(测试断言,model.rs:191);9.5 V 超满 → 钳到 100(model.rs:199);5.0 V → 0(model.rs:200)。入口守卫(model.rs:131):非有限值或 ≤0 一律返回 0——NaN 是"对空集合求均值"的产物,0.0 是"总线没人应答",两者都该显示"未知",而不是一个吓人的百分数。
注释还记了一条教训:这个映射住在 model.rs 而不是 robotd,为的是全仓只有一个映射——原型曾把它在 CLI 写一份、应用里又推一遍,于是两块屏幕对同一块电池各说各话(model.rs:122-126)。
4. 消费端:EMA 平滑与没电前的坐下
原始电压每走一步塌零点几伏、迈步间又回弹,直接显示会乱摆。robotd 用 BATTERY_EMA_ALPHA = 0.1(main.rs:118)每秒刷新一次,时间常数约 10 秒——数字可读了,但也意味着负载塌陷到不了阈值,注释说破:"a pack that gets there is spent"(main.rs:2445-2446)。跌破 BATTERY_EMPTY_V 触发的不是急断电,而是先坐下再断电的序列(main.rs:2444-2451):能坐就缓降坐下、顺路播一声"peck",然后才切扭矩断电。
你带走的收获
- 没有专用电量计就借道遥测:让供电对象自报电压,一个寄存器就够。
- 满电/空电常量是带载口径,不是电芯化学口径——差的 0.2 V 与 1.8 V 都是工程决策。
- 线性映射先写边界:超界钳位、NaN/0 拒绝,再写公式本身。
- 显示值要平滑(EMA),而靠平滑值触发的动作要想清楚平滑带来的延迟。
- 一个物理量的换算只写一处:两处各自推导,迟早给出两个答案。