3、低功耗刷新策略:静态页面检测、差分刷新、区域刷新调度、帧率自适应控制

OLED 的低功耗刷新,说白了就是「能不刷就不刷,能少刷就少刷」。我刚开始做嵌入式 UI 时,总觉得屏幕刷新越快越流畅,结果电池续航直接腰斩。后来才明白——对于 OLED,每一帧的像素点亮都是在消耗能量,尤其是白色背景,功耗比黑色高好几倍。

这一章,我们来拆解四个核心策略:静态页面检测、差分刷新、区域刷新调度、帧率自适应控制。它们组合起来,能让你的 OLED 功耗降低 60% 以上,同时保持用户体验不降级。

3.1 静态页面检测:别做无用功

很多场景下,屏幕内容根本没变——比如用户在看一页文字,或者停在某个菜单。但如果你不管三七二十一,以 60fps 持续刷新,那就是在浪费电量。

静态页面检测的核心思路:比较当前帧与上一帧的帧缓冲数据,如果差异小于某个阈值,就判定为「静态」,然后停止刷新或进入极低刷新模式。

关键指标:

  • 像素变化率:变化像素数 / 总像素数。低于 0.1% 可判定为静态。
  • 检测间隔:每 10~30 帧做一次全缓冲比较,避免每帧都做增加开销。
  • 去抖机制:连续 3 次检测均为静态,才真正进入低功耗模式。防止闪烁误判。

我的经验: 我曾经在一个智能手表项目里,直接用 memcmp() 比较两个帧缓冲,发现每次比较要花 2ms。后来改成「分块哈希比较」——把屏幕分成 16x16 的块,每块算一个 CRC,只比较 CRC 值。比较时间降到了 0.1ms,而且能精确定位变化区域。

3.2 差分刷新:只发变化的部分

静态页面检测告诉你「要不要刷」,差分刷新告诉你「刷哪里」。传统做法是每次刷新都发送整帧数据,哪怕只变了一个像素。差分刷新只传输变化区域的像素数据,配合 OLED 驱动 IC 的「窗口设置命令」(如 SSD1306 的 SetColumnAddress / SetPageAddress),可以大幅减少 SPI/I2C 传输量。

差分刷新的工作流程:

  1. 维护两个帧缓冲:current_buffer 和 previous_buffer。
  2. 逐行或逐块比较,找出变化区域的最小矩形边界(dirty rectangle)。
  3. 只发送该矩形区域内的像素数据到 OLED。
  4. 更新 previous_buffer 为当前内容。
// 差分刷新核心逻辑(伪代码)
typedef struct {
    uint16_t x_min, y_min;
    uint16_t x_max, y_max;
} DirtyRect;

DirtyRect find_dirty_rect(uint8_t *curr, uint8_t *prev, 
                          uint16_t w, uint16_t h) {
    DirtyRect rect = {w, h, 0, 0};
    for (uint16_t y = 0; y < h; y++) {
        for (uint16_t x = 0; x < w; x++) {
            if (curr[y * w + x] != prev[y * w + x]) {
                if (x < rect.x_min) rect.x_min = x;
                if (x > rect.x_max) rect.x_max = x;
                if (y < rect.y_min) rect.y_min = y;
                if (y > rect.y_max) rect.y_max = y;
            }
        }
    }
    return rect;
}

void diff_refresh(uint8_t *curr, uint8_t *prev, 
                  uint16_t w, uint16_t h) {
    DirtyRect dr = find_dirty_rect(curr, prev, w, h);
    if (dr.x_max < dr.x_min || dr.y_max < dr.y_min) {
        return; // 无变化,跳过
    }
    // 设置 OLED 窗口为 dirty rect
    oled_set_window(dr.x_min, dr.y_min, dr.x_max, dr.y_max);
    // 只发送 dirty rect 内的数据
    for (uint16_t y = dr.y_min; y <= dr.y_max; y++) {
        oled_send_data(&curr[y * w + dr.x_min], 
                       dr.x_max - dr.x_min + 1);
    }
    memcpy(prev, curr, w * h); // 更新 previous buffer
}

注意: 差分刷新在「全屏滚动」场景下反而更耗电——因为整个屏幕都在变,dirty rect 几乎等于全屏,还要额外做比较运算。我建议加一个开关:当 dirty rect 面积超过屏幕 80% 时,直接回退到全帧刷新,省掉比较开销。

3.3 区域刷新调度:分而治之

差分刷新找到了变化区域,但如果有多个不相邻的区域同时变化呢?比如时钟数字在变,同时电池图标也在变。如果把它们合并成一个大的 dirty rect,中间没变化的区域也会被刷新,浪费带宽。

区域刷新调度的做法:

  • 将屏幕划分为固定大小的区域(如 8x8 像素的 tile)。
  • 每个 tile 独立维护一个 dirty 标志位。
  • 刷新时,只遍历 dirty 的 tile,分别设置窗口并发送数据。
  • 支持优先级调度:高频变化区域(如动画)优先刷新,静态区域延后。
调度策略 适用场景 功耗节省 实现复杂度
全帧刷新 视频播放、游戏 0% 低
单矩形差分 按钮点击、文字输入 30%~50% 中
多区域 tile 调度 仪表盘、多控件 UI 50%~70% 高
混合模式(动态切换) 通用场景 60%~80% 高

我建议: 区域大小选 8x8 或 16x16 像素比较合适。太小了管理开销大,太大了又失去分块意义。另外,每个 tile 可以附带一个「年龄计数器」——连续多帧不变的 tile,优先级逐渐降低,最后进入「休眠」状态,不再参与比较。

3.4 帧率自适应控制:该快则快,该慢则慢

固定帧率是最简单的做法,但也是最浪费的。帧率自适应控制根据内容动态调整刷新率:

  • 静态内容:降到 1~5 fps,甚至停止刷新(只保留显示 RAM 供电)。
  • 慢速动画:10~15 fps,人眼基本无感知。
  • 快速交互:30~60 fps,保证响应流畅。
  • 视频播放:匹配视频源帧率(24/30/60 fps)。

实现上,我习惯用一个简单的状态机:

typedef enum {
    REFRESH_STOP,    // 停止刷新
    REFRESH_SLOW,    // 5 fps
    REFRESH_NORMAL,  // 15 fps
    REFRESH_FAST,    // 30 fps
    REFRESH_TURBO    // 60 fps
} RefreshMode;

RefreshMode determine_refresh_mode(DirtyRect *rects, int count) {
    if (count == 0) return REFRESH_STOP;
    
    int total_area = 0;
    for (int i = 0; i < count; i++) {
        total_area += (rects[i].x_max - rects[i].x_min) *
                      (rects[i].y_max - rects[i].y_min);
    }
    
    float change_ratio = (float)total_area / (SCREEN_W * SCREEN_H);
    
    if (change_ratio < 0.001f) return REFRESH_STOP;
    if (change_ratio < 0.05f)  return REFRESH_SLOW;
    if (change_ratio < 0.2f)   return REFRESH_NORMAL;
    if (change_ratio < 0.5f)   return REFRESH_FAST;
    return REFRESH_TURBO;
}

避坑指南: 我曾经在一个产品里,帧率切换太激进——用户刚触摸屏幕,帧率从 5fps 跳到 60fps,结果 SPI 总线瞬间拥堵,导致触摸响应反而变慢了。后来我加了「帧率平滑过渡」:每次只跳一档,并且用定时器做渐变,比如从 5→10→15→30,每档停留 2 帧。用户体验好了很多。

3.5 整体调度架构

把这四个策略串起来,就构成了一个完整的低功耗刷新调度器。下面这张图展示了数据流和决策路径:

低功耗刷新调度架构 帧缓冲数据 静态页面检测 分块 CRC 比较 + 去抖 变化率 < 0.1% ? 是 停止刷新 / 1fps 否 差分刷新检测 区域刷新调度 Tile 划分 + 优先级队列 帧率自适应控制 状态机:STOP→SLOW→NORMAL→FAST→TURBO OLED 驱动 IC 刷新指令 每帧循环执行此流程

这个调度器每帧执行一次。静态检测做第一道过滤,差分检测定位变化区域,区域调度决定刷新顺序,帧率控制决定刷新速度。四者配合,才能做到「该省则省,该快则快」。

我的习惯: 在实际项目中,我会把调度器的决策参数(如变化率阈值、tile 大小、帧率档位)做成可配置的,放在一个结构体里。这样在产品调试阶段,可以快速调整参数,找到功耗和流畅度的最佳平衡点。你想想看,如果这些参数写死在代码里,改一次就要重新编译烧录,多麻烦。