3、触摸屏驱动架构:Linux/RTOS下的触摸屏驱动框架、中断与轮询模式选择
各位工程师朋友,咱们接着聊触摸屏驱动。这一节我打算把驱动架构这块掰开揉碎了讲清楚。说实话,很多刚入行的朋友觉得驱动就是配配寄存器、调调参数,其实没那么简单。驱动架构选得好,后面抗干扰、校准这些工作才能事半功倍。
3.1 Linux下的触摸屏驱动框架
Linux下的触摸屏驱动,核心是Input子系统。我习惯把它理解成三层结构:设备驱动层、核心层、事件处理层。说白了,驱动层负责跟硬件打交道,核心层做中转,事件处理层把数据送给用户空间。
先看一个典型的驱动入口代码:
static const struct of_device_id my_touch_of_match[] = {
{ .compatible = "mycompany,my-touch", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_touch_of_match);
static struct platform_driver my_touch_driver = {
.probe = my_touch_probe,
.remove = my_touch_remove,
.driver = {
.name = "my_touch",
.of_match_table = my_touch_of_match,
.pm = &my_touch_pm_ops,
},
};
module_platform_driver(my_touch_driver);
嗯,这里要注意,of_match_table是设备树匹配的关键。我在项目中遇到过有人忘了配这个,结果驱动死活加载不上,查了半天才发现是设备树节点名跟驱动不匹配。
probe函数里,我们要做几件事:
- 分配input_dev结构体
- 设置触摸屏的坐标范围、压力值范围
- 注册中断或启动轮询定时器
- 注册input设备
看个简化版的probe:
static int my_touch_probe(struct platform_device *pdev)
{
struct input_dev *input;
int ret;
input = devm_input_allocate_device(&pdev->dev);
if (!input)
return -ENOMEM;
input->name = "My Touch Screen";
input->id.bustype = BUS_I2C;
__set_bit(EV_ABS, input->evbit);
input_set_abs_params(input, ABS_X, 0, 4095, 0, 0);
input_set_abs_params(input, ABS_Y, 0, 4095, 0, 0);
input_set_abs_params(input, ABS_PRESSURE, 0, 255, 0, 0);
ret = input_register_device(input);
if (ret) {
dev_err(&pdev->dev, "Failed to register input device\n");
return ret;
}
platform_set_drvdata(pdev, input);
return 0;
}
你想想看,这里input_set_abs_params的四个参数分别是:轴类型、最小值、最大值、模糊值、平坦值。模糊值我一般设0,平坦值看具体芯片,有些芯片在零点附近有抖动,设个4-8能过滤掉。
3.2 RTOS下的触摸屏驱动框架
RTOS下就没Linux那么复杂了。说白了,RTOS的驱动就是裸机编程的升级版。我建议的做法是:
- 封装一个触摸屏设备结构体
- 提供初始化、读取、校准三个接口
- 用信号量或消息队列做数据同步
看个FreeRTOS下的示例:
typedef struct {
uint16_t x;
uint16_t y;
uint16_t pressure;
uint8_t touch_event; // 0: release, 1: press, 2: move
} touch_point_t;
typedef struct {
I2C_HandleTypeDef *i2c;
touch_point_t point;
SemaphoreHandle_t data_sem;
TaskHandle_t task_handle;
} touch_device_t;
static void touch_read_task(void *arg)
{
touch_device_t *dev = (touch_device_t *)arg;
uint8_t buf[8];
while (1) {
// 读取触摸数据
HAL_I2C_Master_Receive(dev->i2c, TOUCH_ADDR, buf, 8, 100);
dev->point.x = (buf[0] << 8) | buf[1];
dev->point.y = (buf[2] << 8) | buf[3];
dev->point.pressure = buf[4];
// 通知其他任务
xSemaphoreGive(dev->data_sem);
vTaskDelay(pdMS_TO_TICKS(10)); // 100Hz采样
}
}
我个人习惯在RTOS下用独立任务做触摸读取,而不是在中断里处理。为什么?因为I2C通信本身就有延迟,在中断里做I2C读写会阻塞其他中断,得不偿失。
3.3 中断模式 vs 轮询模式
这是个老生常谈的问题,但每次选型都有人翻车。我直接给结论:
| 对比项 | 中断模式 | 轮询模式 |
|---|---|---|
| CPU占用率 | 低(事件触发) | 高(持续占用) |
| 响应延迟 | 快(微秒级) | 取决于轮询周期 |
| 抗干扰能力 | 弱(易误触发) | 强(可软件滤波) |
| 实现复杂度 | 中等 | 简单 |
| 典型场景 | 消费电子、快速响应 | 工业HMI、高可靠性 |
我曾经在一个工业平板项目里选了中断模式,结果现场电磁干扰严重,触摸屏频繁误触发。后来改成轮询模式,配合软件去抖,问题就解决了。所以我的建议是:
工业HMI场景,优先选轮询模式。 除非你的触摸控制器有硬件滤波且中断信号干净,否则别冒险。
3.4 中断模式的设计要点
如果你非要用中断模式,那有几个坑必须避开:
- 中断触发方式:用双边沿触发,别用电平触发。电平触发在干扰下会一直触发中断,把CPU拖死。
- 中断处理要快:中断服务函数里只做标志位设置,真正的数据读取放到任务或工作队列里。
- 去抖处理:硬件上加RC滤波,软件上做延时确认。
看个Linux下的中断处理示例:
static irqreturn_t my_touch_irq_handler(int irq, void *dev_id)
{
struct my_touch_data *ts = dev_id;
// 只设置标志,不读数据
ts->irq_pending = 1;
// 唤醒工作队列
schedule_work(&ts->work);
return IRQ_HANDLED;
}
static void my_touch_work_handler(struct work_struct *work)
{
struct my_touch_data *ts = container_of(work, struct my_touch_data, work);
uint8_t buf[8];
// 在这里读I2C数据
i2c_master_recv(ts->client, buf, 8);
// 处理并上报
input_report_abs(ts->input, ABS_X, (buf[0] << 8) | buf[1]);
input_report_abs(ts->input, ABS_Y, (buf[2] << 8) | buf[3]);
input_sync(ts->input);
}
嗯,这里要注意,工作队列的优先级要设高一点,否则触摸响应会感觉卡顿。我一般设成HIGH_PRIORITY_WORK。
3.5 轮询模式的设计要点
轮询模式在工业HMI里更常见。核心就是定时器或任务循环,周期性地读取触摸数据。关键参数是轮询频率:
- 触摸屏:50-100Hz就够了,人手指的移动速度没那么快
- 电阻屏:100-200Hz,因为电阻屏需要检测压力变化
- 电容屏:50-80Hz,太高了反而增加功耗
小技巧:轮询周期不要用固定的delay,用定时器或系统tick。这样能保证采样间隔均匀,不会因为其他任务阻塞导致采样抖动。
看个Linux下用hrtimer做轮询的例子:
static enum hrtimer_restart my_touch_timer_callback(struct hrtimer *timer)
{
struct my_touch_data *ts = container_of(timer, struct my_touch_data, timer);
uint8_t buf[8];
// 读取触摸数据
i2c_master_recv(ts->client, buf, 8);
// 软件去抖:连续两次采样一致才上报
if (abs(ts->last_x - ((buf[0] << 8) | buf[1])) < 5 &&
abs(ts->last_y - ((buf[2] << 8) | buf[3])) < 5) {
input_report_abs(ts->input, ABS_X, ts->last_x);
input_report_abs(ts->input, ABS_Y, ts->last_y);
input_sync(ts->input);
}
ts->last_x = (buf[0] << 8) | buf[1];
ts->last_y = (buf[2] << 8) | buf[3];
hrtimer_forward_now(timer, ms_to_ktime(10)); // 100Hz
return HRTIMER_RESTART;
}
这里我加了个简单的去抖逻辑。为什么这么做?因为工业现场电磁干扰会导致单次采样异常,连续两次采样一致能过滤掉大部分毛刺。我曾经在电机驱动器旁边测试,不加去抖时坐标跳动有几十个像素,加上去抖后基本稳定在1-2个像素内。
3.6 混合模式:中断唤醒 + 轮询读取
这是我个人比较推荐的一种方案,尤其适合低功耗场景。思路是:
- 触摸屏休眠时,用中断唤醒系统
- 唤醒后切换到轮询模式,持续读取数据
- 检测到触摸释放后,再切回中断等待模式
这样做的好处很明显:平时功耗低,触摸时响应快,而且轮询阶段可以加各种滤波算法。我在一个电池供电的HMI项目里用了这个方案,待机功耗从50mA降到了5mA,客户非常满意。
注意:切换模式时要处理好状态机,避免出现中断和轮询同时操作I2C总线的情况。我建议用互斥锁保护总线访问。
3.7 驱动调试的实用技巧
最后分享几个调试技巧,都是血泪教训换来的:
- 先确认硬件通信正常:用i2c-tools或逻辑分析仪抓一下,确保I2C读写没问题
- 打印原始数据:在驱动里加printk或串口输出,看触摸坐标是否合理
- 测试边界条件:四个角落、边缘区域、快速滑动,这些地方最容易出问题
- 压力测试:连续点击1000次,看有没有丢数据或卡死
我曾经遇到一个坑:触摸屏在低温环境下I2C通信会偶尔失败。后来在驱动里加了重试机制,失败时重新初始化I2C控制器,问题就解决了。所以,驱动里一定要有错误处理和恢复机制,别指望硬件永远不出问题。
好了,这一节就讲到这里。下一节我们聊聊触摸校准算法,这是HMI手感好坏的关键,敬请期待。