本文主要是对TP驱动框架的学习。
一、概述
1、触摸IC的工作原理
tp与主控间的通信接口一般是i2c,即scl、sda、gnd、vcc。在正常工作时需要加上rst、int脚。
整个过程是:通过点击屏幕,tp ic端会将int 脚电平拉低,等待主控的读取。当然主控端需先注册中断,然后在中断回调函数中先进行关中断(或上拉)再读取ic端的数据,最后将数据通过input子系统上报。
注:在触发点击事件后,int 脚的波形(矩形波)会一直存在,只有当数据被读取或者主控端的int脚输出上拉电平。
2、input 子系统介绍
输入子系统由驱动层、输入子系统核心、事件处理层三部分组成。一个输入事件,如鼠标移动通过Driver->Input core->Event handler->user space的顺序到达用户控件的应用程序。
驱动层:将底层的硬件输入转化为统一事件形式,向输入核心(Input Core)汇报。
Input Core:承上启下, 为驱动层 提供输入设备注册与操作接口,如:input_register_device;通知事件处理层对事件进行处理;在/Proc下产生相应的设备信息。
事件处理层:主要是和用户空间交互。(Linux中在用户空间将所有的设备都当初文件来处理,由于在一般的驱动程序中都有提供fops接口,以及在/dev下会生成相应的设备文件nod,这些操作在输入子系统中由事件处理层完成)。
3、工作队列介绍
工作队列(work queue)是Linux kernel中将工作推后执行的一种机制。这种机制和BH或Tasklets不同之处在于工作队列是把推后的工作交由一个内核线程去执行,因此工作队列的优势就在于它允许重新调度甚至睡眠。
4、中断部分
主要是通过注册int脚的中断函数,当有点击事件发生时,在主控端会被触发进入中断,因此可以在中断回调函数中获取点击的坐标数据等。
二、各功能详细介绍
1、input子系统介绍
1)input子系统的使用
1.1)先使用 input_allocate_device 函数申请一个 input_dev
struct input_dev *input_allocate_device(void)
input_dev的结构体大致如下:
struct input_dev {
const char *name;
const char *phys;
const char *uniq;
struct input_id id;
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
... ...
};
1.2)再初始化支持的事件
set_bit()告诉input输入子系统支持哪些事件,哪些按键。例如:
set_bit(EV_KEY,button_dev.evbit) (其中button_dev是 struct input_dev类型)
evbit: 事件类型(包括 EV_RST,EV_REL,EV_MSC,EV_KEY,EV_ABS,EV_REP等)
keybit:与某种事件类型对应的按键类型(eg. 当事件类型为EV_KEY时, 包括KEY_F11,BTN_LEFT, BTN_0, BTN_1 ,BTN_MIDDLE等 )
1.3)然后使用input_register_device注册输入设备函数
int input_register_device(struct input_dev *dev)
1.4)报告事件
在解析出x、y坐标后,通过input_report_xxx进行上报,用于报告EV_KEY,EV_REL,EV_ABS事件的函数分别为:
void input_report_key (struct input_dev *dev, unsigned int code, int value)
void input_report_rel (struct input_dev *dev, unsigned int code, int value)
void input_report_abs (struct input_dev *dev, unsigned int code, int value)
1.5)报告结束
使用input sync进行报告状态的结束
input_sync()同步用于告诉input core子系统报告结束。
void input_mt_sync(struct input_dev *dev);
void input_mt_slot(struct input_dev *dev, int slot);
1.6)使用input_unregister_device注销输入设备函数
在卸载阶段进行取消注册
void input_unregister_device(struct input_dev *dev)
1.7)使用input_free_device函数释放一个input_dev
在卸载阶段进行释放资源
void input_free_device(struct input_dev *dev)
2)getevent用法
getevent 指令用于获取 input 输入事件,比如获取按键上报信息、获取触摸屏上报信息等。
2.1)getevent 命令
显示当前有那些输入设备,数量与 /dev/input 目录下相同
root@rk3288:/ # getevent
getevent
add device 1: /dev/input/event3
name: "ILITEK Multi-Touch-V3020"
add device 2: /dev/input/event2
name: "PC Camera"
add device 3: /dev/input/event1
name: "gsensor"
add device 4: /dev/input/event0
name: "rk29-keypad"
root@rk3288:/ # ls /dev/input
ls /dev/input
event0
event1
event2
event3
2.2)getevent -l
以文本形式输出事件类型和名称,比 -t 更清楚直观
// 读取 event3 数据(触摸屏)
root@rk3288:/ # getevent -l /dev/input/event3
getevent -l /dev/input/event3
// 事件类型 事件码 事件值
EV_ABS ABS_MT_TRACKING_ID 0000000f
EV_ABS ABS_MT_POSITION_X 00002bbc
EV_ABS ABS_MT_POSITION_Y 00001b6d
EV_KEY BTN_TOUCH DOWN
EV_ABS ABS_X 00002bbc
EV_ABS ABS_Y 00001b6d
EV_SYN SYN_REPORT 00000000
EV_ABS ABS_MT_TRACKING_ID ffffffff
EV_KEY BTN_TOUCH UP
EV_SYN SYN_REPORT 00000000
3)协议方面
A协议:如果从Device获取的当前数据与上一个数据相同,而不管两次数据是否一致都上报,那就是A协议。
B协议:如果从Device获取的当前数据与上一个数据相同,而如果我们选择不上报,那么既然需要比较,总需要把上一次数据存起来吧,slot就是做这个事情的,显然这就是Slot(B)协议。
2、工作队列接口
1)创建工作队列
1.1)create_workqueue
用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程。
struct workqueue_struct *create_workqueue(const char *name);
1.2)create_singlethread_workqueue
用于创建workqueue,只创建一个内核线程
注: 相对于create_singlethread_workqueue, create_workqueue同样会分配一个wq的工作队列,但是不同之处在于,对于多CPU系统而言,对每一个CPU,都会为之创建一个per-CPU的cwq结构,对应每一个cwq,都会生成一个新的worker_thread进程。但是当用queue_work向cwq上提交work节点时,是哪个CPU调用该函数,那么便向该CPU对应的cwq上的worklist上增加work节点。
当用户调用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue对workqueue队列进行初始化时,内核就开始为用户分配一个workqueue对象,并且将其链到一个全局的workqueue队列中。然后Linux根据当前CPU的情况,为workqueue对象分配与CPU个数相同的cpu_workqueue_struct对象,每个cpu_workqueue_struct对象都会存在一条任务队列。紧接着,Linux为每个cpu_workqueue_struct对象分配一个内核thread,即内核daemon去处理每个队列中的任务。至此,用户调用初始化接口将workqueue初始化完毕,返回workqueue的指针。
workqueue初始化完毕之后,将任务运行的上下文环境构建起来了,但是具体还没有可执行的任务,所以,需要定义具体的work_struct对象。然后将work_struct加入到任务队列中,Linux会唤醒daemon去处理任务。
2)创建工作
2.1)静态创建
通过DECLARE_WORK在编译时静态地建一个工作:
DECLARE_WORK(name,void (*func) (void *), void *data);
这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。
2.2)动态创建
通过INIT_WORK在运行时通过指针创建一个工作:
INIT_WORK(structwork_struct *work, woid(*func) (void *), void *data);
3)调度工作
在大多数情况下, 并不需要自己建立工作队列,而是只定义工作, 将工作结构挂接到内核预定义的事件工作队列中调度, 在kernel/workqueue.c中定义了一个静态全局量的工作队列static struct workqueue_struct *keventd_wq;默认的工作者线程叫做events/n,这里n是处理器的编号,每个处理器对应一个线程。比如,单处理器的系统只有events/0这样一个线程。而双处理器的系统就会多一个events/1线程。
调度工作结构, 将工作结构添加到全局的事件工作队列keventd_wq,调用了queue_work通用模块。对外屏蔽了keventd_wq的接口,用户无需知道此参数,相当于使用了默认参数。keventd_wq由内核自己维护,创建,销毁。这样work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。
3.1)schedule_work
调度执行一个具体的任务,执行的任务将会被挂入Linux系统提供的workqueue
int schedule_work(struct work_struct *work);
3.2)schedule_delayed_work
延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
4)执行
4.1)queue_work
调度执行一个指定workqueue中的任务。
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
4.2)queue_delayed_work
延迟调度执行一个指定workqueue中的任务,功能与queue_work类似,输入参数多了一个delay。
int queue_delayed_work(struct workqueue_struct *wq, struct work_struct *work, unsigned long delay);
数据结构如下:
struct work_struct
{
unsigned long pending;//这个工作是否正在等待处理
struct list_head entry;//链接所有工作的链表,形成工作队列
void (*func)(void *);//处理函数
void *data;//传递给处理函数的参数
void *wq_data;//内部使用数据
struct timer_list timer;//延迟的工作队列所用到的定时器
};
5)中断上下文介绍
上半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将下半部处理程序挂到该设备的下半部执行队列中去。这样,上半部执行的速度就会很快,可以服务更多的中断请求。因为中断要尽可能耗时比较短,尽快恢复系统正常调试,所以把中断触发、中断执行分开,也就是所说的“上半部分(中断触发)、底半部(中断执行)”,下半部分一般有tasklet、工作队列实现。
3、中断处理
1)request_irq
注册中断
int request_irq(unsigned int irq, irq_handler, unsigned long flags, const char *name, void *dev);
其中:
irq: 申请的硬件中断号
handler: 向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数
irqflags: 中断处理的属性,指定中断的触发方式及处理方式
触发方式有:IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW
处理方式:IRQF_SHARED, 共享中断标志。 一般设置为NULL
返回值:0—成功, -EINVAL 中断号无效,-EBUSY中断被占用或不能共享
配合工作队列即为:
//中断一般都是这样流程
ts_probe()
|
| //在单cpu上创建一个名字时goodix_wq的工作队列
----- ts->goodix_wq = create_singlethread_workqueue("goodix_wq");
|
| //创建一个推后的任务,任务标识为ts->work, 这个任务的工作内容时goodix_ts_work_func这个函数
----- INIT_WORK(&ts->work, goodix_ts_work_func);
|
----- gtp_request_irq()
----- 中断号(线),中断发生时执行的函数,irq handler,中断标志,设备名,设备id
----- int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id)
----- ret = request_threaded_irq(ts->client->irq, NULL, goodix_ts_irq_handler,
ts->pdata->irq_gpio_flags, ts->client->name, ts);
static irqreturn_t goodix_irq_handler(int irq, void *dev_id)
{
struct goodix_ts_data *ts = dev_id;
gtp_irq_disable(ts);
//将工作(任务)放到队列中,之后cpu在合适的时候执行任务函数
queue_work(ts->goodix_wq, &ts->work);
return IRQ_HANDLED;
}
总结:
1.注册好中断下半部任务,也就是work对应的工作任务函数goodix_ts_work_func
2.申请中断,把中断号(线)与中断处理函数(上半部),也就是goodix_ts_irq_handler连系起来,硬件上中断触发,cpu就会执行中断处理函数
3.中断处理函数里面,把任务放到工作队列,这时候宏观上任务函数就执行了
注:一般在中断上半部是处理和硬件、状态指示等实时的操作,而在中断的下半部中处理耗时的部分。
三、总结
tp驱动的涉及的内容一般是:
1、解析设备树,获取硬件信息
2、i2c句柄、i2c私有数据的初始化
3、input子系统的初始化
4、中断注册、工作中队列的初始化
5、额外的一些配置属性的初始化