3、驱动层优化:触摸驱动初始化优化、数据读取优化、去抖与滤波算法

驱动层,说白了就是硬件和 LVGL 之间的翻译官。这个翻译官要是效率低,上层再怎么优化也白搭。我见过太多项目,触摸响应慢,最后查来查去,问题就出在驱动初始化时多睡了 200 毫秒,或者读取数据时做了太多无用功。

这一节,咱们就专门收拾驱动层的三个关键环节:初始化、数据读取、还有去抖滤波。把这些搞定,你的触摸响应至少能快 30%。

3.1 触摸驱动初始化优化

很多工程师写触摸初始化,习惯上来就调用一个 touch_init(),里面塞一堆延时。嗯,这里要注意,延时不是不能用,但得讲究方法。

核心原则:初始化流程要「快进快出」,把耗时的硬件自校准放到后台,或者干脆提前完成。

我习惯把初始化拆成两个阶段:

  1. 快速阶段:配置 GPIO、I2C/SPI 时钟、复位引脚。这部分必须快,毫秒级完成。
  2. 慢速阶段:发送初始化命令序列、等待芯片内部自校准。这部分可以放到一个状态机里,或者用 lv_timer 分步执行。

举个例子,以前我用 FT5x06 系列触摸芯片,官方驱动里初始化后直接来了个 300ms 的 delay_ms(300)。为什么?因为芯片上电后需要时间稳定。但 300ms 啊,用户都能感觉到界面卡顿了。

我的做法是:

// 传统写法:阻塞 300ms
void touch_init(void) {
    // ... 配置 I2C ...
    ft5x06_write_reg(0x00, 0x01); // 软复位
    delay_ms(300);                // 等芯片稳定
    // ... 后续配置 ...
}

// 优化写法:分步执行
static uint8_t init_step = 0;

void touch_init_fast(void) {
    // 第一步:只配硬件接口
    gpio_init(TOUCH_IRQ_PIN);
    i2c_init(TOUCH_I2C, 400000);
    ft5x06_write_reg(0x00, 0x01); // 触发复位,不等
    init_step = 1;
}

void touch_init_poll(void) {
    // 第二步:由 lv_timer 周期性调用
    if (init_step == 1) {
        if (ft5x06_read_reg(0x02) == 0x55) { // 检查芯片是否就绪
            // 芯片好了,继续配置
            ft5x06_write_reg(0x01, 0x02);
            init_step = 2;
        }
        // 没就绪?下次再来,不阻塞
    }
}

这样,界面在初始化期间照样能渲染,用户根本感觉不到触摸驱动在「磨蹭」。

小技巧:如果芯片必须等待,可以用 lv_tick_get() 做超时判断,代替硬延时。比如:while((lv_tick_get() - start) < 300) { /* 轮询就绪标志 */ },这样至少能让 LVGL 的任务调度器有空隙执行。

3.2 数据读取优化

触摸数据读取,是驱动层最频繁的操作。每次触摸事件,LVGL 都会调用你的 touch_read() 函数。这个函数要是写得拖泥带水,整个 UI 的帧率都会被拉低。

我曾经接手过一个项目,触摸读取用了 I2C 的阻塞模式,每次读取 10 个字节要花 5ms。你想想看,如果用户手指在屏幕上滑动,每秒产生 100 个点,光读取就占了 500ms 的 CPU 时间,这还怎么玩?

优化方向有三个:

优化方向 具体做法 效果
减少读取次数 只在触摸中断触发时才读取,不要轮询 CPU 占用降低 80%
减少读取数据量 只读必要的坐标和触摸状态,不读厂商信息 单次读取时间减半
使用 DMA 或中断 I2C/SPI 读取用 DMA 后台传输,CPU 干别的 读取时间几乎为 0(对 CPU 而言)

具体到代码,我建议这样写:

// 优化前:每次轮询都读一堆数据
bool touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) {
    uint8_t buf[16];
    i2c_read(TOUCH_ADDR, 0x00, buf, 16); // 读 16 字节
    data->point.x = (buf[3] << 8) | buf[4];
    data->point.y = (buf[5] << 8) | buf[6];
    data->state = (buf[2] & 0x80) ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
    return false;
}

// 优化后:中断触发 + 按需读取
static volatile bool touch_irq_pending = false;
static lv_point_t touch_point;
static lv_indev_state_t touch_state;

void touch_irq_handler(void) {
    touch_irq_pending = true; // 标记有触摸事件
}

bool touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) {
    if (!touch_irq_pending) {
        // 没有新事件,直接返回上次数据
        data->point = touch_point;
        data->state = touch_state;
        return false;
    }
    touch_irq_pending = false;

    // 只读 6 个关键字节
    uint8_t buf[6];
    i2c_read(TOUCH_ADDR, 0x02, buf, 6); // 从状态寄存器开始读
    touch_point.x = (buf[1] << 8) | buf[2];
    touch_point.y = (buf[3] << 8) | buf[4];
    touch_state = (buf[0] & 0x80) ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;

    data->point = touch_point;
    data->state = touch_state;
    return false;
}

注意:中断服务函数里不要做 I2C 读取!中断里只设标志位,真正的读取放在 touch_read() 里。否则中断嵌套会导致系统崩溃,我吃过这个亏。

3.3 去抖与滤波算法

触摸芯片返回的原始数据,往往带着噪声。尤其是电容触摸屏,手指轻微抖动、环境电磁干扰,都会让坐标跳来跳去。如果不做处理,LVGL 的按钮会像「抽风」一样乱闪。

去抖和滤波,是两个不同的事:

  • 去抖:消除触摸按下/释放瞬间的毛刺信号。
  • 滤波:平滑连续触摸时的坐标轨迹。

3.3.1 去抖算法

最简单的去抖,就是「延时确认」。检测到触摸状态变化后,不立即上报,而是等几个毫秒再确认一次。如果状态没变,才认为是有效事件。

我常用的去抖代码:

#define DEBOUNCE_MS 10

static uint32_t last_change_tick = 0;
static lv_indev_state_t debounced_state = LV_INDEV_STATE_REL;

lv_indev_state_t touch_debounce(lv_indev_state_t raw_state) {
    uint32_t now = lv_tick_get();

    if (raw_state != debounced_state) {
        // 状态变化了,记录时间
        if (now - last_change_tick > DEBOUNCE_MS) {
            // 超过 10ms 还是这个状态,确认变化
            debounced_state = raw_state;
        }
        last_change_tick = now;
    }
    return debounced_state;
}

这个算法,说白了就是「不信第一下,只信持续了 10ms 的状态」。对付机械抖动和电气毛刺,效果很好。

3.3.2 滤波算法

坐标滤波,我推荐两种:

算法 原理 适用场景 计算量
滑动平均 取最近 N 个点的平均值 手指慢速滑动 低
一阶低通滤波 新值 = 旧值 × α + 原始值 × (1-α) 快速滑动、绘图应用 极低

我个人习惯用一阶低通滤波,因为它只需要一个乘法和一个加法,在 MCU 上跑起来飞快。而且 α 值可以调节灵敏度:α 越大,轨迹越平滑,但延迟也越大。

#define FILTER_ALPHA 0.7f  // 平滑系数,范围 0~1

static lv_point_t filtered_point = {0, 0};

void touch_filter(lv_point_t *raw, lv_point_t *out) {
    // 一阶低通滤波
    filtered_point.x = (int16_t)(FILTER_ALPHA * filtered_point.x + 
                                 (1 - FILTER_ALPHA) * raw->x);
    filtered_point.y = (int16_t)(FILTER_ALPHA * filtered_point.y + 
                                 (1 - FILTER_ALPHA) * raw->y);
    *out = filtered_point;
}

经验值:对于大多数电容触摸屏,α 取 0.6~0.8 比较合适。α 太小(比如 0.3),滤波效果不明显;α 太大(比如 0.95),手指划过去半秒坐标才跟上,用户会骂娘的。

3.4 整体流程:一张图看懂

说了这么多,咱们把驱动层优化的整体流程串起来。下面这张图,是我做项目时必画的架构图,每次评审都拿它说事。

触摸驱动层优化流程 硬件层 I2C/SPI 接口 · 中断引脚 · 复位时序 初始化阶段(分步执行) 快速阶段:GPIO/时钟配置 → 慢速阶段:芯片自校准(状态机轮询) 数据读取阶段(中断驱动) 中断触发 → 按需读取 6 字节 → 更新坐标/状态 去抖与滤波阶段 去抖:延时确认(10ms) 滤波:一阶低通滤波(α=0.7) 硬件无关 可复用 避免阻塞 状态机实现 中断+标志位 不读无用数据 α 值可调 延迟与平滑权衡

你看,整个流程从硬件层开始,经过初始化、数据读取、去抖滤波,最后输出给 LVGL 的坐标数据。每一层都有明确的优化目标,互不干扰。

总结一下:驱动层优化的核心就三句话——初始化别阻塞、读取别浪费、数据别抖动。把这三点做到位,你的触摸体验就已经超越了 80% 的嵌入式产品。