Linux驱动常用API整理
并发与竞争
原子操作
定义:
1 | typedef struct { |
整形操作:
1 | ATOMIC_INIT(int i);//定义时初始化 |
位操作(直接对内存操作):
1 | void set_bit(int nr, void *p)//将p地址的第nr位置 1 |
自旋锁
基本API
1 | DEFINE_SPINLOCK(spinlock_t lock)//定义并初始化 |
中断相关:
一般在线程中使用 spin_lock_irqsave
/spin_unlock_irqrestore
,在中断中使用spin_lock
/spin_unlock
1 | void spin_lock_irq(spinlock_t *lock)//禁止本地中断,并获取自旋锁 |
中断下半部相关:
1 | void spin_lock_bh(spinlock_t *lock)//关闭下半部,并获取自旋锁 |
信号量
1 | DEFINE_SEAMPHORE(name)//定义信号量 并设值为1 |
互斥体
会导致休眠,不能在中断中使用 mutex
,中断中只能使用自旋锁
1 | DEFINE_MUTEX(name)//定义并初始化 |
字符设备驱动
常用宏
1 | module_init(xxx_init); |
注册与注销
老方法,不推荐:
1 | //注册字符设备(浪费设备号) |
- name:设备名字,指向一串字符串
新方法,推荐:
1 | /*注册(方法一,自己确定设备号)*/ |
设备节点
类相关:
1 | class_create(owner, name)//创建类 owner一般为THIS_MODULE |
设备相关:
1 | struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);//创建设备 parent=NULL drvdata=NULL |
设备号
相关宏:
1 | MAJOR(dev)//dev_t -> 主设备号 |
动态分配设备号:
系统分配设备号+自动注册设备号
1 | int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) |
- dev:存放起始设备编号的指针,当注册成功,*dev就会等于分配到的起始设备编号,可以通过
MAJOR()
和MINNOR()
宏来提取主次设备号 - baseminor:次设备号基地址,也就是起始次设备号
- count:要申请的数量,一般都是一个
- name:字符设备名称
- 返回值小于0,表示注册失败
注册设备号:
手动确定设备号+手动注册设备号
1 | int register_chrdev_region(dev_t from, unsigned count, const char *name) |
- from:要申请的起始设备号,也就是给定的设备号
- count:要申请的数量,一般都是一个
- name:设备名字
- 当返回值小于0,表示注册失败
用户空间与内核空间
内核空间–>用户空间
1 | static inline long copy_to_user(void __user *to, const void *from, unsigned long n) |
- to:用户空间指针
- from:内核空间指针
- n:从内核空间向用户空间拷贝数据的字节数
- 成功返回0,失败返回失败数目
用户空间–>内核空间
1 | static inline long copy_from_user(void *to, const void __user * from, unsigned long n) |
- to:内核空间指针
- from:用户空间指针
- n:从用户空间向内核空间拷贝数据的字节数
- 成功返回0,失败返回失败数目
地址映射
1 |
- phys_addr:要映射的物理起始地址。
- size:要映射的内存空间大小。
1 | void iounmap (volatile void __iomem *addr) |
对映射后的内存进行读写操作的函数:
1 | //读 |
Linux常用命令
1 | #查看设备号 |
内核定时器
1 | extern u64 __jiffy_data jiffies_64; |
相关变量:
HZ
:宏,就是CONFIG_HZ,系统频率,通过menuconfig配置,默认100jiffies
、jiffies_64
:系统运行的节拍数,由宏HZ
确定,默认10ms累加一次jiffies/HZ
:系统运行时间(s)
时间先后函数:
1 | //unkown 通常为 jiffies,known 通常是需要对比的值 |
时间转换函数:
1 | int jiffies_to_msecs(const unsigned long j)//jiffies类型j --> ms |
短延时函数:
1 | void ndelay(unsigned long nsecs) |
定时器类型:
1 | struct timer_list { |
定时器函数:
1 | void init_timer(struct timer_list *timer);//初始化 |
- 返回值: 0,定时器未被使能; 1,定时器已经使能
中断
上半部
申请
1 | int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) |
- irq:要申请中断的中断号。
- handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
- flags:中断标志,可以在文件
include/linux/interrupt.h
里面查看所有的中断标志。
常用的中断标志:
标志 描述 IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。 如果使用共享中断的话,request_irq函数的 dev 参数是唯一区分他们的标志 IRQF_ONESHOT 单次中断,中断执行一次就结束 IRQF_TRIGGER_NONE 无触发 IRQF_TRIGGER_RISING 上升沿触发 IRQF_TRIGGER_FALLING 下降沿触发 IRQF_TRIGGER_HIGH 高电平触发 IRQF_TRIGGER_LOW 低电平触发
- name:中断名字,设置以后可以在
/proc/interrupts
文件中看到对应的中断名字 - dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数
- 返回值: 0 中断申请成功;其他负值:中断申请失败;-EBUSY:中断已经被申请了
对于一种不太重要而又比较耗时的中断,可以使用以下函数进行申请:
1 | int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id) |
- 该函数会使得中断线程化(中断使用下半部虽然可以被延迟处理,但是依旧先于线程执行,中断线程化可以让这些比较耗时的下半部与进程进行公平竞争)
- 带
devm_
前缀表明申请到的资源可以由系统自动释放,无需手动处理 - 注意:并不是所有的中断都可以被线程化。该函数有利有弊,具体是否使用需要根据实际情况来衡量(触摸中断一般会用)
释放
1 | void free_irq(unsigned int irq, void *dev) |
- irq: 要释放的中断
- dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉
中断处理函数
格式:
1 | irqreturn_t (*irq_handler_t) (int, void *) |
返回值:
一般为return IRQ_RETVAL(IRQ_HANDLED)
1 | enum irqreturn { |
使能与禁止
1 | void enable_irq(unsigned int irq) |
下半部
推荐使用tasklet
实现
软中断
定义:
1 | struct softirq_action |
系统定义的10个软中断:
多核处理器中,各个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断,但是所执行的软中断服务函数却是相同的:
1 | enum |
注册软中断:
软中断必须在编译的时候静态注册。Linux内核默认会在软中断初始化函数softirq_init
中打开TASKLET_SOFTIRQ
和HI_SOFTIRQ
软中断
1 | void open_softirq(int nr, void (*action)(struct softirq_action *)) |
- nr:要开启的软中断,在上述
enum
中选择一个 - action:软中断对应的处理函数
触发软中断:
1 | void raise_softirq(unsigned int nr) |
tasklet
定义:
1 | struct tasklet_struct |
初始化:
1 | void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);//初始化 |
- func:tasklet 的处理函数
- data:要传递给 func 函数的参数
调度函数:
一般在中断的上半部中调用
1 | void tasklet_schedule(struct tasklet_struct *t) |
- t:要调度的 tasklet,也就是
DECLARE_TASKLET
宏里面的name
工作队列
工作队列在进程上下文执行,即,将要推后的工作交给一个内核线程去执行,因此工作队列允许睡眠或重新调度。
如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet
定义:
工作–>工作队列–>工作者线程
1 | //工作(重点关注): |
初始化:
1 |
调度:
一般在中断的上半部中调用
1 | bool schedule_work(struct work_struct *work) |
- work: 要调度的工作
- 返回值: 0 成功,其他值 失败
阻塞与非阻塞
阻塞:访问资源时,如果资源不可用,将会挂起线程,直到资源可用再唤醒。open函数中大部分flag均为阻塞访问
非阻塞:访问资源时,如果资源不可用,将会直接返回错误码。open函数中flag参数O_NONBLOCK
为非阻塞访问
等待队列
一般用于在中断中唤醒阻塞操作而引起的线程挂起
等待队列头
等待队列的头部
定义:
1 | struct __wait_queue_head { |
初始化:
1 | void init_waitqueue_head(wait_queue_head_t *q)//仅初始化 |
等待队列项
每个访问设备的进程都需要创建一个队列项,当设备不可用的时候需要将这些进程对应的等待队列项添加到等待队列里面
定义:
1 | struct __wait_queue { |
初始化:
1 | DECLARE_WAITQUEUE(name, tsk)//宏,定义并初始化 |
- name:等待队列项的名字
- tsk:这个等待队列项属于哪个任务(进程),一般设置为
current
(全局变量,表示当前进程)
添加/移除等待队列
当设备不可用的时候需要将该进程对应的等待队列项添加到等待队列里面
1 | void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)//添加 |
- q: 等待队列项要加入的等待队列头
- wait:要加入/删除的等待队列项
等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程
1 | void wake_up(wait_queue_head_t *q)//唤醒队列中的所有进程(包括TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE状态) |
等待事件
当事件满足以后能够自动唤醒等待队列中的进程
1 | wait_event(wq, condition)//若condition为真,则唤醒等待队列,否则一直阻塞。(会将进程设置为TASK_UNINTERRUPTIBLE状态) |
轮询
一般用于非阻塞操作
poll
、 epoll
和 select
用于处理轮询。应用程序通过 select
、 epoll
或 poll
来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select
、 epoll
或 poll
函数的时候设备驱动程序中的 poll
函数就会执行
select
单线程中 select 函数能够监视的文件描述符数量一般最大为1024,可以修改该数量但会降低效率
1 | int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) |
- nfds: 要操作的文件描述符个数
- readfds:指向描述符集合,fd_set类型。用于监视指定描述符集的读变化,所指定集合中有一个文件可以读取,则返回大于 0 的值,否则根据 timeout 参数来判断是否超时。设置为 NULL,表示不关心任何文件的读变化
- writefds:指向描述符集合,fd_set类型。用于监视指定描述符集的写变化,所指定集合中有一个文件可以写入,则返回大于 0 的值,否则根据 timeout 参数来判断是否超时。设置为 NULL,表示不关心任何文件的写变化
- exceptfds:指向描述符集合,fd_set类型。用于监视指定描述符集的异常变化,所指定集合中有一个文件异常,则返回大于 0 的值,否则根据 timeout 参数来判断是否超时。设置为 NULL,表示不关心任何文件的异常变化
fd_set变量定义相关宏:
1
2
3
4 void FD_ZERO(fd_set *set)//将 fd_set 变量的所有位都清零
void FD_SET(int fd, fd_set *set)//将 fd_set 变量的某个位置 1(向 fd_set 添加一个文件描述符), fd:要加入的文件描述符
void FD_CLR(int fd, fd_set *set)//将 fd_set 变量的某个位清零(向 fd_set 删除一个文件描述符), fd:要删除的文件描述符
int FD_ISSET(int fd, fd_set *set)//测试 fd_set 的某个位是否置 1(判断某个文件是否可以进行操作), fd:要判断的文件描述符
-
timeout:超时时间,设为 NULL 表示无限期等待。timeval结构体类型
1
2
3
4struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
}; -
返回值: 0,超时发生,没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数
select使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 void main(void)
{
int ret, fd; /* 要监视的文件描述符 */
fd_set readfds; /* 读操作文件描述符集 */
struct timeval timeout; /* 超时结构体 */
fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
FD_ZERO(&readfds); /* 清除 readfds */
FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */
/* 构造超时时间 */
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms */
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (ret) {
case 0: /* 超时 */
......
break;
case -1: /* 错误 */
......
break;
default: /* 可以读取数据 */
if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 */
/* 使用 read 函数读取数据 */
}
break;
}
}
poll
本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制
1 | int poll(struct pollfd *fds, nfds_t nfds, int timeout) |
-
fds: 要监视的文件描述符集合以及要监视的事件,为结构体
pollfd
数组类型,pollfd
类型定义:1
2
3
4
5struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};-
fd:要监视的文件描述符,如果 fd 无效则 events 监视事件也无效,并且 revents 返回 0
-
events:要监视的事件,类型如下:
1
2
3
4
5
6
7POLLIN 有数据可以读取
POLLPRI 有紧急的数据需要读取
POLLOUT 可以写数据
POLLERR 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起
POLLNVAL 无效的请求
POLLRDNORM 等同于POLLIN -
revents:返回参数,由 Linux 内核设置具体的返回事件
-
-
nfds:poll 函数要监视的文件描述符数量
-
timeout:超时时间,单位为 ms
-
返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,即发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,并设置相应的错误码
poll使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 void main(void)
{
int ret;
int fd; /* 要监视的文件描述符 */
struct pollfd fds;
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
/* 构造结构体 */
fds.fd = fd;
fds.events = POLLIN; /* 监视数据是否可以读取 */
ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
if (ret) { /* 数据有效 */
......
/* 读取数据 */
......
} else if (ret == 0) { /* 超时 */
......
} else if (ret < 0) { /* 错误 */
......
}
}
epoll
selcet 和 poll 函数会随着所监听的 fd 数量的增加,而导致效率的降低,且遍历所有的描述符比较浪费时间。epoll 专为处理大并发而准备,一般用于网络编程中。 其相关函数如下:
创建 epoll 句柄:
1 | int epoll_create(int size) |
- size:Linux2.6.8 后此参数已无意义,大于 0 即可
- 返回值: epoll 句柄,如果为-1 的话表示创建失败
向 epoll 句柄中添加要监视的文件描述符以及监视的事件:
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) |
-
epfd:epoll 句柄(即epoll_create的返回值)
-
op:对 epfd 的操作,可以设置为:
1
2
3EPOLL_CTL_ADD 向epfd添加文件参数fd表示的描述符
EPOLL_CTL_MOD 修改参数fd的event事件
EPOLL_CTL_DEL 从epfd中删除fd描述符 -
fd:要监视的文件描述符
-
event:要监视的事件类型,epoll_event结构体指针类型:
1
2
3
4struct epoll_event {
uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
};-
events:要监视的事件:
1
2
3
4
5
6
7EPOLLIN 有数据可以读取
EPOLLPRI 有紧急的数据需要读取
EPOLLOUT 可以写数据
EPOLLERR 指定的文件描述符发生错误
EPOLLHUP 指定的文件描述符挂起
EPOLLET 设置epoll为边沿触发(默认为水平触发)
EPOLLONESHOT 一次性监视(当监视完成以后需要再次监视某个fd,就需要将fd重新添加到epoll里面)
-
-
返回值: 0,成功; -1,失败,并设置相应的错误码
等待事件发生:
1 | int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) |
- epfd:epoll 句柄
- events:指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件
- maxevents:events 数组大小(必须大于 0)
- timeout:超时时间,单位为 ms
- 返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量
驱动中的poll函数
函数原型:
1 | unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait) |
-
filp: 要打开的设备文件(文件描述符)
-
wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给
poll_wait 函数 -
返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:
1
2
3
4
5
6
7POLLIN 有数据可以读取
POLLPRI 有紧急的数据需要读取
POLLOUT 可以写数据
POLLERR 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起
POLLNVAL 无效的请求
POLLRDNORM 等同于POLLIN,普通数据可读
添加到poll_table:
非阻塞函数,不会引起阻塞,只是将应用程序添加到 poll_table 中
1 | void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p) |
- wait_address:要添加到 poll_table 中的等待队列头
- p:poll_table,就是file_operations 中 poll 函数的 wait 参数
驱动通用模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 struct xxxx_dev {
......
struct wait_queue_head_t r_wait; /* 等待队列头 */
};
unsigned int xxx_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct xxxx_dev *dev = (struct xxxx_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait);
if(......) { /* 相关条件满足 */
mask = POLLIN | POLLRDNORM; /* 返回 PLLIN */
}
return mask;
}
异步通知(信号)
Linux中的信号宏:
1 |
|
上述信号中,除了 SIGKILL
(9)和 SIGSTOP
(19)这两个信号不能被忽略外,其他的信号都可以忽略
应用程序API
指定信号的处理函数:
1 | sighandler_t signal(int signum, sighandler_t handler) |
-
signum:要设置处理函数的信号
-
handler: 信号的处理函数,函数原型:
1
typedef void (*sighandler_t)(int)
-
返回值: 设置成功返回信号的前一个处理函数,设置失败返回 SIG_ERR
应用程序的一般模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
/* 设置信号 SIGIO 的处理函数 */
signal(SIGIO, sigio_signal_func);
fcntl(fd, F_SETOWN, getpid()); /* 将当前进程的进程号告诉给内核 */
flags = fcntl(fd, F_GETFD); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC);/* 设置进程状态为 FASYNC 启用异步通知功能(此时会调用驱动中的fasync函数) */
while(1) {
sleep(2);
}
close(fd);
return 0;
驱动程序API
异步相关结构体:
1 | struct fasync_struct { |
向应用程序发送中断信号:
1 | void kill_fasync(struct fasync_struct **fp, int sig, int band) |
- fp:要操作的 fasync_struct 结构体
- sig: 要发送的信号
- band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT
- 返回值: 无
file_operations操作集中异步接口函数:
应用程序通过fcntl(fd, F_SETFL, flags | FASYNC)
改变fasync标记的时候,该函数就会执行
1 | int (*fasync) (int fd, struct file *filp, int on) |
初始化 fasync_struct 结构体:
1 | int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp) |
- 前三个参数与
fasync
函数中参数一致 - fapp:要初始化的 fasync_struct 结构体指针变量
驱动一般模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 struct xxx_dev {
......
struct fasync_struct *async_queue; /* 异步相关结构体 */
};
static int xxx_fasync(int fd, struct file *filp, int on)
{
struct xxx_dev *dev = (xxx_dev)filp->private_data;
if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)//直接调用该函数即可,不用干其他事
return -EIO;
return 0;
}
static struct file_operations xxx_ops = {
......
.fasync = xxx_fasync,
.release = xxx_release,
......
};
static int xxx_release(struct inode *inode, struct file *filp)
{
return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
}
常用框架API
platform
platform设备相关:
1 | void platform_device_unregister(struct platform_device *pdev)//卸载platform设备 |
platform驱动相关:
1 | void platform_driver_unregister(struct platform_driver *drv)//卸载platform驱动 |
MISC
MISC 驱动也叫做杂项驱动,当某些外设无法进行分类的时候就可以使用 MISC 驱动。 MISC 驱动其实就是最简单的字符设备驱动,通常嵌套在 platform 总线驱动中,实现复杂的驱动
所有的 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。MISC 设备会自动创建 cdev
1 | int misc_register(struct miscdevice * misc)//注册MISC驱动 |
INPUT
初始化及卸载
申请及释放:
1 | struct input_dev *input_allocate_device(void)//申请input_dev |
注册及注销:
1 | int input_register_device(struct input_dev *dev)//注册 |
修改input_dev:
1 | struct input_dev *inputdev; |
上报数据
上报指定的事件以对应值:
1 | void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) |
- dev:需要上报的 input_dev
- type:上报的事件类型,比如 EV_KEY
- code:事件码,即注册的按键值,比如 KEY_0、 KEY_1 等等
- value:事件值,比如 1 表示按键按下, 0 表示按键松开
专用上报函数(本质是调用input_event):
1 | void input_report_key(struct input_dev *dev, unsigned int code, int value)//上报按键 |
同步事件:
1 | void input_sync(struct input_dev *dev) |
应用层相关
应用层通过获得 input_event
结构体来获取input子系统发送的输入事件
1 | struct input_event { |
-
time:时间,即此事件发生的时间,timeval 结构体类型:
1
2
3
4
5
6
7
8typedef long __kernel_long_t;
typedef __kernel_long_t __kernel_time_t;
typedef __kernel_long_t __kernel_suseconds_t;
struct timeval {
__kernel_time_t tv_sec; /* 秒 */
__kernel_suseconds_t tv_usec; /* 微秒 */
}; -
type: 事件类型,比如 EV_KEY 表示此次事件为按键事件
-
code: 事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如: KEY_0、KEY_1等
-
value:值,比如 EV_KEY 事件中 value 就是按键值,为 1 表示按键按下,为 0 的话表示按键没有被按下
应用层读取方式:
1 | static struct input_event inputevent; |
多点触摸
- linux内核中讲解多点电容触摸屏协议文档路径:
Documentation/input/multitouch-protocol.txt
- 老版本(2.x 版本)的 linux内核不支持多点电容触摸(Multi-touch,简称 MT)
MT 协议分为两种类型:
- Type A:适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据(此类型在实际使用中非常少)
- Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过 slot 更新某一个触摸点的信息,一般的多点电容触摸屏 IC 都有此能力
Type A
步骤(时序)如下:
1 | ABS_MT_POSITION_X x[0]//上报第一个点的x坐标 input_report_abs() |
例子:drivers/input/touchscreen/st1232.c
Type B
步骤(时序)如下:
1 | ABS_MT_SLOT 0// input_mt_slot() |
例子:drivers/input/touchscreen/ili210x.c
相关函数:
初始化 MT 的输入 slots(初始化时使用):
1 | int input_mt_init_slots(struct input_dev *dev, unsigned int num_slots, unsigned int flags) |
-
dev: MT 设备对应的 input_dev,因为 MT 设备隶属于 input_dev
-
num_slots:设备要使用的 SLOT 数量,也就是触摸点的数量
-
flags: 其他一些 flags 信息
1
2
3
4
5 -
返回值: 0,成功;负值,失败
产生 ABS_MT_SLOT 事件:
1 | static inline void input_mt_slot(struct input_dev *dev, int slot) |
- dev: MT 设备对应的 input_dev
- slot:当前发送的是哪个 slot 的坐标信息,也就是哪个触摸点
产生ABS_MT_TRACKING_ID和ABS_MT_TOOL_TYPE事件:
1 | void input_mt_report_slot_state(struct input_dev *dev, unsigned int tool_type, bool active) |
- dev: MT 设备对应的 input_dev
- tool_type:上报触摸工具类型(即ABS_MT_TOOL_TYPE事件),目前的协议支持MT_TOOL_FINGER(手指)、 MT_TOOL_PEN(笔)和 MT_TOOL_PALM(手掌)
- active:true,连续触摸,添加一个新的触摸点,linux 内核自动分配一个 ABS_MT_TRACKING_ID;false,触摸点抬起,移除一个触摸点,ABS_MT_TRACKING_ID由内核设为-1
上传真实的触摸点数量:
1 | void input_mt_report_pointer_emulation(struct input_dev *dev, bool use_count) |
- dev: MT 设备对应的 input_dev
- use_count:true,有效的触摸点数量(上报数量就是当前数模点数量); false,追踪到的触摸点数量多于当前上报的数量(使用 BTN_TOOL_TAP 事件通知用户空间当前追踪到的触摸点总数量)
- 举个例子:硬件能够追踪5个触摸点,无论是否有触摸,硬件都会有5个值输出,此时use_count就是false,即无论触摸的数量为多少,追踪到的(硬件输出的5个值)总比上报的(真实触摸数量)多
例子:
1 | static irqreturn_t ft5x06_handler(int irq, void *dev_id) |
Framebuffer
fb_info结构体:
1 | struct fb_info *framebuffer_alloc(size_t size, struct device *dev) |
- size:分配完framebuffer结构体后附加的额外空间(一般用于存放用户私有数据)
- dev:最终会绑定到fb_info->device上,可以设为NULL
注册与卸载:
1 | int register_framebuffer(struct fb_info *fb_info) |
显存分配与释放:
1 | static inline void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *dma_addr, gfp_t gfp) |
RTC
申请并注册rtc_device:
1 | struct rtc_device *rtc_device_register(const char *name, struct device *dev, const struct rtc_class_ops *ops, struct module *owner) |
- name:设备名字
- dev: 设备
- ops: RTC 底层驱动函数集
- owner:驱动模块拥有者
注销rtc_device:
1 | void rtc_device_unregister(struct rtc_device *rtc) |
IIC
适配器注册和注销:
一般不会用到,SOC厂商会写好这部分代码
1 | //注册 |
i2c驱动注册与注销:
1 |
|
iic通信
内核驱动
内核文档:Documentation\i2c\i2c-protocol
、Documentation\i2c\smbus-protocol
其中smbus-protocol是i2c-protocol的一个子集,官方更加推荐使用后者的smbus-protoco中的函数
收发函数:
1 | //S Addr Rd [A] [Data] NA S Addr Wr [A] Data [A] P |
-
adap: 所使用的 I2C 适配器, i2c_client 会保存其对应的 i2c_adapter
-
msgs: I2C 要发送的一个或多个消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14struct i2c_msg {
__u16 addr; /* 从机地址 */
__u16 flags; /* 标志 */
__u16 len; /* 消息(本 msg)长度 */
__u8 *buf; /* 消息数据 */
}; -
num: 消息数量,即 msgs 的数量
-
返回值: 负值,失败,其他非负值,发送的 msgs 数量
发送函数(最终调用i2c_transfer):
1 | //S Addr Wr [A] Data [A] Data [A] ... [A] Data [A] P |
- client: I2C 设备对应的 i2c_client
- buf:要发送的数据
- count: 要发送的数据字节数,必须小于 64KB(i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据)
- 返回值: 负值,失败,其他非负值,发送的字节数
接收函数(最终调用i2c_transfer):
1 | //S Addr Rd [A] [Data] A [Data] A ... A [Data] NA P |
- client: I2C 设备对应的 i2c_client
- buf:要接收的数据
- count: 要接收的数据字节数,必须小于 64KB(i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据)
- 返回值: 负值,失败,其他非负值,发送的字节数
smbus-protoco中的函数:
1 | i2c_smbus_read_byte()//S Addr Rd [A] [Data] NA P |
应用层直接访问
内核文档:Documentation\i2c\dev-interface
通常,I2C设备由设备驱动来控制,但是通过/dev接口也可以提供用户空间直接访问适配器上的设备。前提是需要加载I2C-DEV内核模块
在应用层,i2c-tools工具包帮你写好了接口,下载链接,镜像仓库,只需要包含该库的头文件include\linux\i2c-dev.h
即可直接控制iic(最新版本的4.1没有发现该文件)。其本质是调用的内核自带的驱动模块I2C-DEV。该模块位于\drivers\i2c\i2c-dev.c
,通过宏CONFIG_I2C_CHARDEV
配置,内核menuconfig路径为Device Drivers-> I2C support
,一般默认为通过模块加载
i2c-tools工具包的本质是通过调用ioctl
打开实现各种功能:
1 | //include\linux\i2c-dev.h部分源码 |
上述部分源码中file就是需要访问的iic控制器,如:/dev/i2c-0
,与自己写的驱动程序类似
模板
1 | /* 设备结构体 */ |
SPI
spi_master注册和注销:
一般不会用到,SOC厂商会写好这部分代码
1 | //注册 |
spi驱动注册与注销:
1 | int spi_register_driver(struct spi_driver *sdrv); |
相关结构体:
1 | /*-----------------------spi_message-----------------------*/ |
spi通信
初始化:
1 | int spi_setup(struct spi_device *spi)//初始化时钟和SPI模式 |
同步传输(阻塞,会等待SPI数据传输完成)
1 | int spi_sync(struct spi_device *spi, struct spi_message *message) |
异步传输(不会阻塞,不会等到SPI数据传输完成)
异步传输需要设置 spi_message 中的complete
成员变量,当 SPI 异步传输完成以后complete
函数就会被调用
1 | int spi_async(struct spi_device *spi, struct spi_message *message) |
模板
1 | /* SPI 多字节发送 */ |
PWM
设备树绑定内核参考文档:Documentation/devicetree/bindings/pwm/imx-pwm.txt
设备树
通用
查找节点
-
通过名字查找
1
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
- from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
- name:要查找的节点名字(不是table和name属性)。
- 返回值: 找到的节点,如果为 NULL 表示查找失败。
-
通过device_type 属性查找
1
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
- from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
- type:要查找的节点对应的 type 字符串,即 device_type 属性值。
- 返回值: 找到的节点,如果为 NULL 表示查找失败。
-
根据 device_type 和 compatible查找
1
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
- from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
- type:要查找的节点对应的 type 字符串,即 device_type 属性值(若为 NULL则表示忽略 device_type 属性)
- compatible: 要查找的节点所对应的 compatible 属性列表。
- 返回值: 找到的节点,如果为 NULL 表示查找失败
-
通过 of_device_id 匹配表来查找
1
struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)
- from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
- matches: of_device_id 匹配表,也就是在此匹配表里面查找节点。
- match: 找到的匹配的 of_device_id。
- 返回值: 找到的节点,如果为 NULL 表示查找失败
-
通过路径查找
1
inline struct device_node *of_find_node_by_path(const char *path)
- path:带有全路径的节点名,可以使用节点的别名。
- 返回值: 找到的节点,如果为 NULL 表示查找失败
-
查找指定节点的父节点
1
struct device_node *of_get_parent(const struct device_node *node)
- node:要查找的父节点的节点。
- 返回值: 找到的父节点。
-
查找指定节点的子节点
1
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
- node:父节点。
- prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
- 返回值: 找到的下一个子节点。
提取属性
-
查找节点中的指定属性
1
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
- np:设备节点。
- name: 属性名字。
- lenp:属性值的字节数,一般为NULL
- 返回值: 找到的属性。
-
获取属性中元素的数量
1
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
- np:设备节点。
- proname: 需要统计元素数量的属性名字。
- elem_size:每个元素的长度。(如果元素为u32类型则此处填sizeof(u32))
- 返回值: 得到的属性元素数量。
-
从属性中获取指定标号的 u32 类型数据值
1
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
- np:设备节点。
- proname: 要读取的属性名字。
- index:要读取的值标号。
- out_value:读取到的值
- 返回值: 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。
-
读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
1
2
3
4int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)- np:设备节点。
- proname: 要读取的属性名字。
- out_values:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
- sz: 要读取的数组元素数量。
- 返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。
-
读取只有一个整形值的属性
1
2
3
4int of_property_read_u8(const struct device_node *np,const char *propname, u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value)- np:设备节点。
- proname: 要读取的属性名字。
- out_value:读取到的数组值。
- 返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。
-
读取属性中字符串值
1
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
- np:设备节点。
- proname: 要读取的属性名字。
- out_string:读取到的字符串值。
- 返回值: 0,读取成功,负值,读取失败。
-
获取#address-cells 属性值
1
int of_n_addr_cells(struct device_node *np)
- np:设备节点。
- 返回值: 获取到的#address-cells 属性值。
-
获取#size-cells 属性值
1
int of_n_size_cells(struct device_node *np)
- np:设备节点。
- 返回值: 获取到的#size-cells 属性值。
其他常用函数
-
查看节点的 compatible 属性是否有包含指定的字符串
1
int of_device_is_compatible(const struct device_node *device, const char *compat)
- device:设备节点。
- compat:要查看的字符串。
- 返回值: 0,节点的 compatible 属性中不包含 compat 指定的字符串; 正数,节点的compatible属性中包含 compat 指定的字符串。
-
获取地址相关属性
主要是“reg”或者“assigned-addresses”属性值
1
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
- dev:设备节点。
- index:要读取的地址标号。
- size:地址长度。
- flags:参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等
- 返回值: 读取到的地址数据首地址,为 NULL 的话表示读取失败。
-
将从设备树读取到的地址转换为物理地址
1
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
- dev:设备节点。
- in_addr:要转换的地址。
- 返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
-
从设备树里面提取资源值
本质上是将 reg 属性值转换为 resource 结构体类型
1
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
- dev:设备节点。
- index:地址资源标号。
- r:得到的 resource 类型的资源值。
- 返回值: 0,成功;负值,失败。
-
直接内存映射(获取内存地址所对应的虚拟地址 )
本质上是将 reg 属性中地址信息转换为虚拟地址(将原来的先提取属性在映射结合起来),如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段
1
void __iomem *of_iomap(struct device_node *np, int index)
- np:设备节点。
- index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为0。(从0开始,一次映射一对,即一个地址一个长度)
- 返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
GPIO子系统
of函数
-
获取设备树某个属性中定义 GPIO 的个数(空的 GPIO 信息(即值为0)也会被统计到)
1
int of_gpio_named_count(struct device_node *np, const char *propname)
- np:设备节点。
- propname:要统计的 GPIO 属性。
- 返回值: 正值,统计到的 GPIO 数量;负值,失败。
-
获取设备树
gpios
属性中定义 GPIO 的个数(空的 GPIO 信息(即值为0)也会被统计到)1
int of_gpio_count(struct device_node *np)
-
获取 GPIO 编号
1
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
- index: GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO 的编号,如果只有一个 GPIO 信息的话此参数为 0
驱动层函数
-
申请GPIO
1
int gpio_request(unsigned gpio, const char *label)
- gpio:要申请的 gpio 标号,使用
of_get_named_gpio
函数返回值 - label:给 gpio 设置个名字。
- 返回值: 0,申请成功;其他值,申请失败。
- gpio:要申请的 gpio 标号,使用
-
释放GPIO
1
void gpio_free(unsigned gpio)
-
设置方向
1
2int gpio_direction_input(unsigned gpio)
int gpio_direction_output(unsigned gpio, int value)- 返回值: 0,设置成功;负值,设置失败
-
设置值
1
2
int __gpio_get_value(unsigned gpio)- 返回值: 非负值,得到的 GPIO 值;负值,获取失败
-
获取值
1
2
void __gpio_set_value(unsigned gpio, int value) -
获取 gpio 对应的中断号
1
int gpio_to_irq(unsigned int gpio)
- gpio: 要获取的 GPIO 编号
- 返回值: GPIO 对应的中断号
中断相关
提取 interrupts 属性中的中断号
1 | unsigned int irq_of_parse_and_map(struct device_node *dev, int index) |
- dev: 设备节点
- index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息
- 返回值:中断号
获取 gpio 对应的中断号(与上面的函数功能一样)
1 | int gpio_to_irq(unsigned int gpio) |
- gpio: 要获取的 GPIO 编号,由
gpio_request
申请而来 - 返回值: GPIO 对应的中断号