第6章:电池与低功耗

一个"能读一整晚"的墨水屏阅读器,电量到底怎么算?电压、百分比、充电、低电量、关机——这条链路每一步都由谁负责?

⚖️

本章导师:包青天

核心方法论:铁面无私,以数据为准绳,不放过任何一个数字

「断案讲究'铁证如山':电压就是铁证,百分比是推理出来的结论。有人说电量 98% 了、有人说还差一点,本官只信一条——'只在数据真正变化时才去刷新画面',不许在毫伏的抖动上做文章,更不许把一丁点电量报多。这条链路里的每一个数字(50 毫伏阈值、3 次滤波、10 秒周期、2% 低电临界)都不是随手写的,它们共同决定这台设备能不能'铁面无私'地告诉你真实电量、并把自己的功耗压到最低。这一章,我们逐项过堂。」

6.1 电池管理:ADC 电压采样与电量表

先解释三个术语。ADC(模数转换器,Analog-to-Digital Converter)是一类把"连续变化的模拟电压"转换成"离散数字数值"的硬件模块——MCU 只能读数字,而电池电压是模拟量,所以必须靠它"翻译"。电压采样就是用 ADC 周期性去读电池当前的电压值,这是我们获取电量信息的唯一直接手段。电量表(battery calculator)则是一段软件:它拿到电压采样,查表、插值、滤波,最终输出一个 0–100 的百分比。为什么要这么绕?因为电池没有一个寄存器能直接告诉你"还剩百分之几"——剩下的只有电压,百分比全靠软件从电压推出来。

看真实实现 ADCBattery.cpp:构造函数里用 rt_device_find("bat1") 找到电池 ADC 设备,通道号是 7,然后 rt_adc_enable 打开采样。get_voltage()rt_adc_read 读回原始数值,除以 10 转成毫伏(1mV 精度);get_percentage() 把电压(毫伏 ×10 还原)交给电量表 battery_calculator_get_percent 换出百分比。这里的关键是:电压到百分比的换算不是一条直线公式,而是一张查表 + 插值——放电和充电各有一张曲线表(sf32-oed-epd_base/battery_table.c),比如放电表 100% ≈ 41950mV,一路降到 0% ≈ 35007mV;充电表 100% ≈ 42180mV,0% ≈ 36990mV。为什么两张表不一样?因为充电时的电池电压比放电时偏高(同一电量,充电中电压更高),用同一条曲线会在充电时误报偏高电量,所以分表。

曲线表中间只有离散的 101 个点(每 1% 一个电压),落在两点之间的电压怎么算?SDK 的 battery_percent_from_curve_table线性插值:找到上下两个相邻的已知点,按电压比例把百分比"夹"出来。光有插值还不够——电池电压会在负载、温度影响下抖动,直接查表会让百分比来回跳。于是电量表再加了两级滤波:一级滤波按充/放电状态分别处理(电压在 50mV 阈值内的小波动直接接受,超过阈值的突变要连续累计 3 次才采纳,防止把采样毛刺当真变化);二级滤波是 90/10 的加权平均(上次电压占 90%、本次占 10%),让百分比平滑过渡。最后还有一道"方向校验":充电时百分比不许下跌、放电时不许上涨,且每次最多 ±1,从软件层面杜绝数字"说谎"。

数据放电曲线表充电曲线表
100% 对应电压41950 mV42180 mV
0% 对应电压35007 mV36990 mV
趋势随放电单调下降随充电整体偏高
用途不充电时的百分比查询充电中的百分比查询
/* src/boards/battery/ADCBattery.cpp:构造函数——找设备、开采样、喂曲线表 */
ADCBattery::ADCBattery(rt_mq_t ui_queue)
{
    this->ui_queue = ui_queue;
    s_adc_dev = rt_device_find("bat1");   // 找到电池 ADC 设备
    channel = 7;                        // 采样通道
    low_power = 0;                    // 低功耗状态:0=正常
    if (s_adc_dev) {
        rt_adc_enable((rt_adc_device_t)s_adc_dev, channel);
    }

    // 初始化电量表:喂给它充电 / 放电两张曲线表 + 滤波参数
    static const battery_calculator_config_t config = {
        .charging_table              = charging_curve_table,
        .charging_table_size         = charging_curve_table_size,
        .discharging_table           = discharge_curve_table,
        .discharging_table_size      = discharge_curve_table_size,
        .charge_filter_threshold     = 50,  // 充电时电压波动滤波阈值(mV)
        .discharge_filter_threshold  = 50,  // 放电时电压波动滤波阈值(mV)
        .filter_count                = 3,   // 滤波计数阈值
        .secondary_filter_enabled    = true, // 启用二级滤波
        .secondary_filter_weight_pre = 90,  // 上次电压权重
        .secondary_filter_weight_cur = 10   // 当前电压权重
    };
    battery_calculator_init(&battery_calc, &config);
}
/* ADCBattery.cpp:电压采样 → 百分比 → 充电状态 */
float ADCBattery::get_voltage()
{
    if (!s_adc_dev) return 0;
    rt_uint32_t value = rt_adc_read((rt_adc_device_t)s_adc_dev, channel);
    float voltage = value / 10.0f;  // 原始采样值 → 毫伏(mV)
    return voltage;
}

int ADCBattery::get_percentage()
{
    float voltage = get_voltage();
    uint8_t percentage = battery_calculator_get_percent(&battery_calc,
                                                        (uint32_t)(voltage * 10));
    return percentage;
}

bool ADCBattery::is_charging()
{
    int pin_val = rt_pin_read(CHG_STATUS);  // 读充电状态引脚
    return pin_val == 0;                   // 低电平有效:读到 0 = 正在充电
}
/* middleware/battery/battery_calculator.c:查表 + 线性插值(节选) */
return p_cur->percent
       + ((p_pre->percent - p_cur->percent)
          * (voltage - p_cur->voltage)
          / (p_pre->voltage - p_cur->voltage));   // 两点之间按比例夹出百分比
包青天提示

记一句话:电压是证据,百分比是推理。推理要让人信服,就得"查表 + 插值 + 两级滤波 + 方向校验"四步全做——缺了任何一步,屏幕上那个数字就可能抖动、虚高或说谎。等你以后做自己的电池管理,这套"先滤波、再查表、最后限速"的顺序可以直接复用。

6.2 充电检测:充电状态、满电图标与刷屏控制

怎么知道在充电?充电状态由硬件引脚给出:CHG_STATUS(GPIO 26)是充电芯片输出的状态脚,低电平有效——读到 0 表示正在充电,读到 1 表示未充电。软件里 is_charging() 就是一行 rt_pin_read(CHG_STATUS) == 0。别小看这个引脚,它是后面整个低功耗状态机迁移的重要输入之一。

充电链路有一个"满电收尾"的细节:充电完成后百分比会停在 98%–100% 之间波动,如果每次波动都重画状态栏,墨水屏会被反复刷新。看主循环的 MSG_UPDATE_CHARGE_STATUS 处理:百分比 ≥ 98 且还没清除过图标时,调 clear_charge_icon 抹掉充电闪电图标、置 charge_full = true,之后就不再重复处理;一旦低于 98%,才把 charge_full 复位并重画状态栏。这样"满电"这件事只处理一次,而不是每轮都刷。

更彻底的优化在电池刷新环节:主循环末尾专门有一段——只有 cur_percent != last_battery_percentcur_charging != last_battery_charging 时,才调用 draw_status_bar + request_flush 重画,否则什么都不做。为什么墨水屏尤其要避免无谓刷新?因为电子墨水屏的"刷新"不是像 LCD 那样瞬间换一帧,而是一次物理上的粒子翻转,慢、耗电、还会闪屏——一次无关紧要的状态栏刷新,代价可能比 LCD 高一个数量级。"只在真正变化时刷屏"是墨水屏设备的黄金法则,本方案在电量这一路贯彻得最彻底。状态栏的呈现也值得一看:draw_charge_status 在充电时用两个 fill_triangle 拼一个闪电图标,draw_battery_level 用一个 40×20 的矩形按百分比填充来表示电量。

触发条件动作目的
percentage >= 98charge_full == falseclear_charge_icon + request_flush,置 charge_full = true满电只处理一次,避免反复刷
percentage < 98charge_full = false,重画状态栏离开满电,恢复充电/放电显示
cur_percent != last_battery_percentcur_charging != last_battery_chargingdraw_status_bar + request_flush仅在电量/充电状态真正变化时刷屏
/* src/main.cpp:充电状态消息——满电收尾 */
if (ui_action == MSG_UPDATE_CHARGE_STATUS)
{
    int percentage = battery->get_percentage();
    if (percentage >= 98 && charge_full == false)
    {
        clear_charge_icon(renderer);   // 抹掉充电闪电图标
        request_flush();
        charge_full = true;       // 记录:已显示过满电,不再重复
    }
    else if (percentage < 98)
    {
        charge_full = false;        // 离开满电状态
        draw_status_bar(renderer, battery);
        request_flush();
    }
    continue;
}
/* src/main.cpp:主循环末尾——仅在电量/充电状态变化时才刷新 */
if (battery) {
    int cur_percent = battery->get_percentage();
    bool cur_charging = battery->is_charging();
    if (cur_percent != last_battery_percent ||
        cur_charging != last_battery_charging) {
        draw_status_bar(renderer, battery);   // 画电量 + 充电图标
        request_flush();                       // 真正刷一次屏
        last_battery_percent = cur_percent;
        last_battery_charging = cur_charging;
    }
}
/* src/main.cpp:状态栏里的充电图标(节选) */
void draw_charge_status(Renderer *renderer, Battery *battery)
{
    if (battery->is_charging())
    {
        draw_lightning(renderer, xpos + icon_size / 2,
                       ypos + icon_size / 2, icon_size);
    }
}
包青天提示

记住"只在变化时刷新"这条铁律:墨水屏刷新贵,凡是画状态栏、画电量这类高频但低频变化的内容,都必须先比较再决定要不要刷。这条原则在第 4 章学波形、在第 12 章学书库重绘时都会反复出现——它是墨水屏功耗优化的第一课。

6.3 低功耗状态机:WELCOME / LOW_POWER / CHARGING / SHUTDOWN

状态机是一个描述"系统处于哪个状态、什么事件导致状态跳转"的模型——它把纷杂的事件流整理成一张"谁在什么条件下到哪去"的路线图。全项目的 UI 状态机在 AppUIState 里有 12 个状态(src/type.h),本章只聚焦电量相关的一条子链:WELCOME_PAGE(欢迎页)→ LOW_POWER_PAGE(低电量页)→ CHARGING_PAGE(充电页)→ SHUTDOWN_PAGE(关机页)。它们的迁移由一个低功耗标志 low_power_state 和一个定时消息驱动,规则是"铁面无私"的。

驱动源是一个 10 秒周期的 RT-Thread 软定时器RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER)。这里有个值得学习的工程细节:定时器回调里不做任何重活(不读 ADC、不算电量、不打日志),只往消息队列发一条 MSG_BATTERY_CHECK——真正的 ADC 读取、电量计算、状态迁移全部回到主循环上下文执行。为什么?定时器回调运行在定时器线程上下文,在里面做耗时的采样和计算,会卡住整个定时器调度;"回调只发消息、主线做重活"是嵌入式里标准的线程安全范式。

主循环收到 MSG_BATTERY_CHECK 后,用三个条件把"电量 × 充电"组合映射成状态迁移:百分比 < 2 且没在充电且还没进低功耗 → 置 low_power_state=1、发 MSG_DRAW_LOW_POWER_PAGE(进低电量页,屏幕显示电池见底提示);正在充电但还停留在低功耗态 → 发 MSG_DRAW_CHARGE_PAGE(插上电,切到充电页);百分比回升到 ≥ 2 且仍在低功耗态 → 置 low_power_state=0、发 MSG_DRAW_WELCOME_PAGE(电量够用了,回欢迎页)。一旦 low_power_state == 1handleUserInteraction 开头就 return——所有按键和触控都被吞掉,防止用户在低电量下误操作把最后一点电耗光。

最后一道保险是硬关机。主循环的整体条件是"距上次交互不足 60 × 1000 × 60 × TIMEOUT_SHUTDOWN_TIME 毫秒",也就是连续 5 小时没有任何交互就退出循环,依次执行 renderer->dehydrate()(保存阅读进度)、board->stop_filesystem()(安全卸载文件系统)、draw_shutdown_page()(画关机页,屏幕提示"请长按 Key1 开机")、board->prepare_to_sleep()(进入深睡眠)。关机页配的位图叫 shutdown_map,和欢迎页 welcome_map、低电量页 low_power_map、充电页 chargeing_map 是同一批素材。

状态进入条件表现退出条件
WELCOME_PAGE开机 / 电量回升 ≥ 2%显示欢迎页,等待交互电量 < 2% 且未充电
LOW_POWER_PAGEpercentage < 2 且未充电低电量提示,交互被吞插入充电器 / 电量回升
CHARGING_PAGE低功耗态下检测到充电显示充电页电量 ≥ 2% 后回欢迎页
SHUTDOWN_PAGE5 小时无交互(硬关机)"请长按 Key1 开机",进入深睡眠长按 Key1 重新开机
/* src/main.cpp:MSG_BATTERY_CHECK——三个条件驱动低功耗状态迁移 */
if (ui_action == MSG_BATTERY_CHECK)
{
    float voltage = battery->get_voltage();
    uint8_t percentage = battery->get_percentage();
    bool is_charging = battery->is_charging();

    ADCBattery* adc_battery = static_cast<ADCBattery*>(battery);
    if (percentage < 2 && !is_charging && adc_battery->get_low_power_state() != 1) {
        adc_battery->set_low_power_state(1);      // 电量见底 → 进入低功耗
        UIAction msg = MSG_DRAW_LOW_POWER_PAGE;     // 画低电量页
        rt_mq_send(ui_queue, &msg, sizeof(UIAction));
    } else if (is_charging && adc_battery->get_low_power_state() == 1) {
        UIAction msg = MSG_DRAW_CHARGE_PAGE;        // 插上电 → 充电页
        rt_mq_send(ui_queue, &msg, sizeof(UIAction));
    } else if (percentage >= 2 && adc_battery->get_low_power_state() == 1) {
        adc_battery->set_low_power_state(0);      // 电量回升 → 退出低功耗
        UIAction msg = MSG_DRAW_WELCOME_PAGE;       // 回欢迎页
        rt_mq_send(ui_queue, &msg, sizeof(UIAction));
    }
}
/* ADCBattery.cpp:10 秒软定时器——回调只发消息,重活留到主循环 */
void ADCBattery::start_battery_monitor()
{
    battery_check_timer = rt_timer_create("battery_check",
                                          battery_check_callback,
                                          this,
                                          rt_tick_from_millisecond(10000), // 10 秒周期
                                          RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER);
    if (battery_check_timer != RT_NULL) rt_timer_start(battery_check_timer);
}

void ADCBattery::battery_check_callback(void* parameter)
{
    // Timer 回调只发消息:ADC 读值 / 计算都在主循环做
    ADCBattery* battery = static_cast<ADCBattery*>(parameter);
    if (battery && battery->ui_queue) {
        UIAction msg = MSG_BATTERY_CHECK;
        rt_mq_send(battery->ui_queue, &msg, sizeof(UIAction));
    }
}

/* main.cpp:低功耗态下吞掉所有交互 */
void handleUserInteraction(Renderer *renderer, UIAction ui_action, bool needs_redraw)
{
    if (battery && battery->get_low_power_state() == 1) {
        return;   // 低电量:一切操作无效
    }
    // ... 正常交互处理
}
/* src/main.cpp:5 小时硬关机流程 */
#define TIMEOUT_SHUTDOWN_TIME 5 // 默认关机超时(小时);0 表示不关机

// 主循环:距上次交互 < 60*1000*60*5 毫秒(即 5 小时)才继续运行
while ((rt_tick_get_millisecond() - last_user_interaction
         < 60 * 1000 * 60 * TIMEOUT_SHUTDOWN_TIME))
{
    // ... 处理消息、刷屏
}

// 退出循环:执行关机四步曲
renderer->dehydrate();        // 1. 保存阅读进度
board->stop_filesystem();     // 2. 安全卸载文件系统
draw_shutdown_page();         // 3. 画关机页:"请长按 Key1 开机"
board->prepare_to_sleep();    // 4. 进入深睡眠
注意

这里有一个"同一常量、两处含义"的坑,正是第 1 章警告框埋的伏笔:TIMEOUT_SHUTDOWN_TIME = 5while 循环里被当成小时60×1000×60×5 毫秒 = 5 小时关机);但它又被当成参数传给 screen_init(TIMEOUT_SHUTDOWN_TIME),而 screen_init 的实现把参数当作分钟使用——于是欢迎页默认超时变成 5 分钟。更隐蔽的是 epub_screen.h 把形参写成 default_timeout_hoursepub_screen.cpp 却按 default_timeout_minutes 实现,头文件与实现连名字都对不上。这类"名字说小时、代码算分钟"的漂移,正是移植项目里最值得警惕的隐患。

6.4 超时策略:5 / 10 / 30 分钟 / 1 小时 / 不关机

前面 6.3 的硬关机是"最后一道保险",而真正经常被用户调的是超时策略——设备空闲多久后自动回到欢迎页。注意这个"回欢迎页"和"关机"是两码事:回到欢迎页是为了省电(欢迎页是纯静态图,几乎不耗电,且停在欢迎页时屏幕、触摸都可保持最低功耗),而关机是彻底断电。设置页里超时选项来自 epub_screen.cppkTimeoutOptions[]{5, 10, 30, 60, 0},单位分钟,0 表示不关机,默认 30 分钟。用户每按一次调整,adjust_timeout 就在数组里用取模循环切换——到了"不关机"再按就绕回 5 分钟。

主循环怎么用这个值?每轮开头有一个条件判断:距上次交互 ≥ 60×1000×screen_get_timeout_shutdown_minutes() 毫秒、且当前不在低功耗/欢迎/充电页时,重画欢迎页。注意末尾那句 && screen_get_timeout_shutdown_minutes()——当策略是"不关机"(0)时,整个条件为假,设备永远不会自动回欢迎页。结合 6.3 你就会发现两个"5"的并置:欢迎页默认超时 5 分钟来自 screen_init(TIMEOUT_SHUTDOWN_TIME),硬关机 5 小时来自 while 循环里的同一个宏。它们共用同一个宏、单位却不同,这就是上面警告框说的"两处含义"。

还有个值得较真的细节:"不关机"(0)只是让设备不再自动回欢迎页,并不会关掉 5 小时硬关机——因为硬关机循环用的是写死的 TIMEOUT_SHUTDOWN_TIME,与设置里的超时策略无关。也就是说,即便选了"不关机",若连续 5 小时完全无交互,设备依然会断电。这是"设置文案与底层行为不完全一致"的又一例证。站在包青天的立场:行为以代码为准,README 和菜单文案只能当参考。

选项意义欢迎页自动回退5 小时硬关机
5 / 10 / 30 / 60 分钟空闲多久回欢迎页按所选分钟数触发仍为兜底(与选项无关)
0(不关机)不回欢迎页不触发仍存在(5 小时兜底)
/* src/epub_screen.cpp:超时选项与循环切换 */
static const int kTimeoutOptions[] = {5, 10, 30, 60, 0}; // 单位:分钟,0为不关机
static const int kTimeoutOptionsCount =
    sizeof(kTimeoutOptions) / sizeof(kTimeoutOptions[0]);
static int timeout_shutdown_minutes = 30; // 默认30分钟

// 在选项间循环切换(取模绕回)
static void adjust_timeout(bool increase)
{
    if (increase) {
        timeout_idx = (timeout_idx + 1) % kTimeoutOptionsCount;
    } else {
        timeout_idx = (timeout_idx - 1 + kTimeoutOptionsCount) % kTimeoutOptionsCount;
    }
    timeout_shutdown_minutes = kTimeoutOptions[timeout_idx];
}

int screen_get_timeout_shutdown_minutes() { return timeout_shutdown_minutes; }
/* src/main.cpp:每轮主循环——超时后自动回欢迎页(带多重守卫) */
if (rt_tick_get_millisecond() - last_user_interaction
        >= 60 * 1000 * screen_get_timeout_shutdown_minutes()
    && battery && battery->get_low_power_state() != 1     // 不在低功耗
    && ui_state != WELCOME_PAGE && ui_state != CHARGING_PAGE  // 不在欢迎/充电页
    && ui_state != LOW_POWER_PAGE                              // 不在低电量页
    && screen_get_timeout_shutdown_minutes())                  // 0 = 不关机,跳过
{
    draw_welcome_page(battery);
}
包青天提示

把 6.4 和 6.3 连起来看,就是一个完整的三级省电策略:① 超时回欢迎页(分钟级,可配置);② 低电量只保留关机页(2% 临界,由 10 秒定时器巡逻);③ 5 小时无交互硬关机(小时级兜底)。三级各管一段,既不会因为一次超时就把书丢掉,也保证哪怕用户忘了关机,电量也不会被放干。

章末练习

练习 1:为什么百分比不能"直接读" 入门

电池为什么没有寄存器能直接给出百分比?本项目的"电压 → 百分比"要经历哪几步(结合 6.1 的查表、插值、滤波)?充电和放电为什么各用一张曲线表?

提示

电池只输出模拟电压;同一电量下,充电中的电压比放电时偏高,所以分表。步骤可从"ADC 读电压 → 除 10 转 mV → 一级滤波 → 二级滤波 → 查表插值 → 方向校验"梳理。

参考答案

电池是电化学器件,只能对外表现为模拟电压,没有任何数字接口告诉 MCU"剩余电量"。所以软件必须:ADC 读电压(rt_adc_read,除 10 转毫伏)→ 一级状态滤波(50mV 阈值、累计 3 次才采纳)→ 二级加权滤波(上次 90% / 本次 10%)→ 按充/放电状态查对应曲线表 → 线性插值得到百分比 → 方向校验(充电不许跌、放电不许涨,单次最多 ±1)。充电中电池电压整体偏高,若共用放电表会误报虚高电量,因此两张表分开。

练习 2:两级滤波各防什么 进阶

分别说明电量表一级滤波(_battery_voltage_filter)和二级滤波(_battery_secondary_filter)解决什么问题。50mV 阈值、filter_count = 3、90/10 加权各是什么含义?为什么充电状态下"电压上涨"与"电压下跌"的处理不完全对称?

提示

一级滤波对抗"采样毛刺/突变",先判断是充电还是放电;二级滤波对抗"残留抖动",做平滑。充电时电压应上升,突然下跌多为噪声;放电时电压应下跌,突然上涨也多为噪声——二者处理对称性取决于各自"合理方向"。

参考答案

一级滤波是"状态相关"的:充电状态优先信任电压上涨、放电状态优先信任电压下跌;凡是在"合理方向"内的波动且幅度小于 50mV 阈值就立即采纳,反之(突变)要连续累计 filter_count=3 次才更新,把单次采样毛刺过滤掉。二级滤波对滤波后的电压再做 90/10 加权平均(上次占 90%),让百分比平滑过渡、抑制残留抖动。充电/放电处理的不对称来自物理方向:充电中电压合理走势是上升,上升偏差小可信、下跌多疑;放电中反之——所以两分支的信任方向正好相反。

练习 3:低功耗状态迁移推断 进阶

结合 6.3 的迁移规则,推断以下场景当前应处于哪个 UI 状态、接下来会怎么走:(1) 电量 1%、未充电、当前在欢迎页;(2) 低功耗态下插入充电器;(3) 电量回升到 3%、仍在充电、当前停在充电页。再说明:为什么 handleUserInteraction 在低功耗态要直接 return

提示

三条规则:百分比 < 2 且未充电 → 低功耗;低功耗 + 充电 → 充电页;百分比 ≥ 2 且低功耗 → 回欢迎页。交互被吞是为了保护最后一点电量。

参考答案

(1) 满足"百分比 < 2 且未充电且低功耗标志≠1",下一次 MSG_BATTERY_CHECK 会置 low_power_state=1 并画 LOW_POWER_PAGE。(2) 满足"正在充电且低功耗标志==1",发 MSG_DRAW_CHARGE_PAGE,切到 CHARGING_PAGE。(3) 百分比 ≥ 2 但低功耗标志已被 (2) 清成 0,三条都不命中,继续停在充电页直到下次迁移。(4) handleUserInteractionlow_power_state==1 时直接 return,是为了让任何按键/触控都无效——低电量下用户操作(开书、翻页、刷新屏幕)都是最耗电的行为,必须全部拦截,把电量留给"长按 Key1 开机"之外的最后一格。

练习 4:常量两用与刷屏节制 挑战

(1) TIMEOUT_SHUTDOWN_TIME = 5 在两处被使用,含义各是什么?epub_screen.hepub_screen.cppscreen_init 形参的名字为什么不一致?这暴露了什么风险?(2) 为什么"不关机"(0)选项不能让设备彻底不关机?(3) 结合 6.2,说明"仅在电量/充电状态真正变化时才刷屏"为什么对墨水屏设备尤其重要。

提示

(1) while 循环按小时算(5 小时关机),screen_init 按分钟算(欢迎页 5 分钟超时)。(2) 硬关机循环写死用宏,不看设置值。(3) 墨水屏刷新是一次物理粒子翻转,慢、耗电、闪屏。

参考答案

(1) 在 while ((rt_tick_get_millisecond() - last_user_interaction < 60*1000*60*TIMEOUT_SHUTDOWN_TIME)) 里它是"5 小时硬关机";在 screen_init(TIMEOUT_SHUTDOWN_TIME) 里它被当作分钟,初始化出"5 分钟欢迎页超时"。宏注释说"小时",screen_init 却按分钟实现,且 epub_screen.h 形参名 default_timeout_hoursepub_screen.cppdefault_timeout_minutes 对不上——"命名即文档"在此失效,暴露的是"同一语义被两个单位引用"的配置漂移风险,也是教学里最该警惕的一类坑。

(2) 主循环的退出条件写死 TIMEOUT_SHUTDOWN_TIME(5 小时),从不读取设置里的超时策略;"不关机"(0)只是让回欢迎页的条件为假。所以选"不关机"后,5 小时无交互依然会硬关机——菜单文案与底层行为存在偏差。

(3) LCD 刷一帧瞬间完成、耗电极低;墨水屏刷新是把带电粒子上翻下翻的物理过程,耗时、费电且伴随残影/闪屏。电量百分比本来变化就很慢(每 10 秒才查一次),若每次都重画状态栏,等于把宝贵的刷新次数浪费在没有实际变化的内容上——所以必须"先比较、再刷新"。