第一天学习字符设备驱动.
| 应用程序 | **原理:应用程序中首先运行我们写的程序,然后调用相关的**
---------------------------- **系统API函数,进入LINUX内核中更新相应的设备驱动程序**
| Linux内核 | **(要注意的是:设备驱动程序本身在LINUX内核中,内核不变)**
---------------------------- **而后根据设备驱动程序来更新嵌入式硬件的参数,从而软件控制**
| 设备驱动程序 | **硬件(分析的大概)**
----------------------------
| 嵌入式硬件 |
----------------------------
*********************************采用分层处理的思想******************************************
2. 设备驱动开发课程重点学习内容
{
Linux字符设备驱动框架
并发与竞态,IO控制,轮询和异步操作
中断处理和延迟机制
内核地址空间和内存的使用
设备驱动模型和虚拟文件系统,平台设备驱动
常用硬件接口驱动和总线设备驱动,例如ADC驱动,I2C设备驱动,SPI设备驱动
}
参考书籍:
{
宋宝华 Linux驱动开发详解
Linux内核的设计与实现
}
{
1. 设备驱动的地位和作用
{
设备驱动是内核的一部分,用来控制硬件的程序。
在带有操作系统的嵌入式系统中,设备驱动程序在系统中的位置
----------------------------
| 应用程序 |
----------------------------
| Linux内核 |
----------------------------
| 设备驱动程序 |
----------------------------
| 嵌入式硬件 |
----------------------------
驱动的作用
: 就是管理对应的硬件,为用户提供操作硬件的方法(接口)
}
## 二、 内核模块
{ //大多数的设备驱动都是以内核模块的形式来实现的。
1. 内核模块的概念
{
> 内核模块是具有一定功能的,可以被单独编译的一段内核代码, 它可以在需要的时候动态 加载到内核,从而动态的增加内核的功能。
> 在不需要的时候,可以动态的从内核卸载,从 而节约内核资源,不管是加载还是卸载,都不需要重新启动整个系统。
}
2. 内核模块框架
{ //最简单的内核模块
#include //包含内核模块使用的头文件 在 include 目录下
#include
int hello_init(void) //编写模块初始化函数
{
printk ("Hello world!\n");
return 0;
}
void hello_exit(void) //编写模块退出函数
{
printk ("Goodbye world\n");
}
module_init(hello_init); //指定模块初始化函数
module_exit(hello_exit); //指定模块退出函数
MODULE_LICENSE ("GPL"); //license 声明
}
//测试文件:hellotest.c 即应用程序
{
#include
#include
#include
#include
#include
#include
unsigned char buf[100]={0};
/*
./hellotest w "写的内容"
./hellotest r
./hellotest ledon
./hellotest ledoff
*/
#define HELLO_DEVICE 'A'
#define CMD_LED_ON _IO(HELLO_DEVICE,0) //打开LED灯命令
#define CMD_LED_OFF _IO(HELLO_DEVICE,1) //关闭LED灯命令
int main(int argc, char *argv[])
{
int rw_len = 0;
int fd_hello = open("/dev/hello",O_RDWR);
if(fd_hello < 0)
{
perror("open file error\n");
}
if(strcmp(argv[1],"r") == 0)
{
rw_len = read(fd_hello,buf,50);
printf("read len:%d buf:%s\n",rw_len, buf);
}
else if(strcmp(argv[1],"w") == 0)
{
memset(buf,0,100);
strcpy(buf,argv[2]);
rw_len = write(fd_hello,buf,strlen(buf));
}
else if(strcmp(argv[1],"ledon") == 0)
{
ioctl(fd_hello, CMD_LED_ON,0);
}
else if(strcmp(argv[1],"ledoff") == 0)
{
ioctl(fd_hello, CMD_LED_OFF,0);
}
close(fd_hello);
return 0;
}
}
3. 内核模块的编译
{ //通过调用linux总目录下的Makefile来实现
模块的Makefile 内容如下:
KERNELDIR := /home/wins/kernel-3.4.39 //指定内核路径在开发板
//KERNELDIR := /usr/src/linux-headers-4.4.0-141-generic //指定内核路径在内核
obj-m:=hello.o //指定要编译的模块
//PWD := $(shell pwd) //将当前路径赋值给 PWD
MDIR:= $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(MDIR) modules //内核编译
cp -f ./hello.ko /nfsroot/rootfs/home/
//gcc hellotest.c -o test
arm-none-linux-gnueabi-gcc hellotest.c -o test
cp -f ./test /nfsroot/rootfs/home/
clean:
rm -rf *.o .* *.ko *.ko.cmd test *.mod.o.cmd *.o.cmd *.order *.symvers *.mod.c *.mod.o Module* modules* hellotest\
/nfsroot/rootfs/home/* ------------*/
}
4. 内核模块的加载和卸载
{
在开发板命令行执行如下命令
// 加载模块
insmod xxx.ko
--->触发执行模块加载函数(xxx_init)
//卸载模块
rmmod xxx.ko
--->触发执行卸载函数(xxx_exit)
//查看当前系统已经加载的模块
lsmod
注意:
在开发板上调用rmmod出错,出现找不到 /lib/modules 目录的错误,执行如下命令创建目录即可。
#mkdir /lib/modules/3.4.39-farsight
}
5. 将多个源文件编译成一个内核模块
{ //假设 mul.ko 模块由 hello.c 和 bar.c文件组成
hello.c 文件内容如下:
{
#include
#include
extern void bar(void); //外部函数声明
int hello_init(void)
{
printk ("Hello world!\n");
bar();
return 0;
}
void hello_exit(void)
{
printk ("Goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE ("GPL");
}
bar.c 文件内容如下:
{
#include
void bar(void)
{
printk("bar is ok\n");
}
}
多源文件模块Makefile 如下:
{
KERNELDIR := /home/wins/kernel-3.4.39
obj-m:=mul.o #指定模块的名称
mul-objs = hello.o bar.o #指定模块mul 依赖 bar.o hello.o 目标文件
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
cp -f ./mul.ko /nfsroot/rootfs/home/
clean:
rm -rf *.o *.ko *.mod.c Module* modules*
}
}
// 内核模块参数允许用户在加载模块时通过命令行给模块传递参数 内核模块参数的定义
module_param(参数名,参数类型,参数权限);
module_param_array(数组参数名,数组元素类型,NULL,参数权限)参数类型可以为 int short long charp(字符串指针) 等。
参数权限 { 权限在include/linux/stat.h中有定义 权限位一般定义为 S_IRUGO ,表示所有用户可读 }
加载模块后,如果使用了模块参数,会存在在/sys/module/模块名/paramters/模块参数名文件
这些文件的权限和定义模块参数时给的权限一致,这些文件中保存了模块参数的值 如果我们修改这些文件值,对应模块参数的值就会发生变化。加载模块时,通过命令行给模块传递参数 #insmod mul.ko band=115200
如果是传递给数组,则这么写 #insmod mul.ko port=1,2,3,4 //port 为整型数组参数
}
//如果A模需要调用B模块的全局变量或函数时,我们就说A模块依赖B模块 内核符号表,就是在内核中可供外部引用的函数和变量的符号表。
内核的符号表文件System.map导出模块符号 EXPORT_SUMBOL //导出的内容能够被所有的模块使用 EXPORT_SYMBOL_GPL
//导出的内容只能被遵循GPL协议的模块使用 例如 { #include#include
//包含导出内核符号表宏定义 #include
void show_exportmod(void) { printk(“I am export module info\n”); }
EXPORT_SYMBOL(show_exportmod); //导出show_exportmod函数
MODULE_LICENSE (“GPL”); }
另外一个模块要使用其他模块导出的函数或变量时, 通过extern 关键字声明即可
}
}
## 三、系统调用
{
1. 系统调用原理
{
a. 应用程序调用open
b. 进程会调用C库中open实现
c. open实现会将open对应的系统调用号保存在寄存器中
d. open实现调用swi(软中断),进入软中断异常
e. 系统跳转到异常向量表中软中断处理的代码(vector_swi)
f. 该函数根据系统调用号,在内核预先定义好的系统调用表中找到open对应内核实现(sys_open)
系统调用表:arch/arm/kernel/calls.S
g. 执行该函数,执行完毕后,原路返回用户空间
}
2. 手动为内核加上一个系统调用
{
a. 在内核代码arch/arm/kernel/sys_arm.c添加一个系统调用的内核实现sys_lzembed
{
//data1 data2 为用户空间传递给内核空间的参数
asmlinkage int sys_lzembed(int data1, int data2, struct pt_regs *regs)
{
printk("Guohui Cao test System Call\n");
printk("para0:%d para1:%d\n",data1, data2);
return 0;
}
}
b. 在内核代码arch/arm/include/asm/unistd.h文件中添加一个新的系统调用号
#define __NR_lzembed (__NR_SYSCALL_BASE+378) //对应的系统调用号为 378
c. 在内核代码arch/arm/kernel/calls.S中的系统调用表中添加一项
CALL(sys_lzembed)
d. 重新编译内核,在用户空间调用syscall函数调用新添加的系统调用
{ // 用户空间通过syscall 调用内核空间的系统调用实现函数
int main(int argc, char *argv[])
{
//378 为系统调用号, 66, 88 为用户空间传递给内核空间的参数
int res = syscall(378,66,88);
printf("res = %d\n",res);
return 0;
}
}
}
}
## 四、字符设备驱动
{
**1. 设备驱动开发基础**
{
**Linux设备分类:**
**字符设备**
以字节为单位访问设备,按字节流访问,只有顺序访问能力,
不能随机读取字符设备存储器中的数据, 常见字符设备有:按键 串口 LCD屏 触摸屏...
**块设备**
按数据块访问,具有随机访问块设备存储器中数据的能力
常见块设备有: SD卡,emmc卡,U盘,Flash,硬盘等。
**网络设备**
网卡,网络接口。
内核中通常是通过TCP/IP协议栈来访问。
设备驱动在内核中的位置
----------------------------------
| 应用程序 |
----------------------------------
| 系统调用接口 |
----------------------------------
| 虚拟文件系统 VFS|
----------------------------------
| 设备驱动|文件系统|网络子系统|
----------------------------------
| 嵌入式硬件 |
----------------------------------
}
**# 2. 字符设备驱动基本概念**
{
**设备文件:**
{
在Linux系统中,一个设备文件就代表一个具体的硬件设备,Linux向管理文件一样来管理硬件设备,
Linux通过访问设备文件来访问硬件设备。
设备文件都在/dev目录下
Linux如何访问设备文件?
和访问普通文件完全一样,通过调用普通文件操作函数来访问设备文件。
open read write ioctl .....close...
> 如何通过设备文件来找到设备对应的设备驱动呢? <设备号>
}
**设备号:**
{
设备号分为主设备号和次设备号,一个占32位,其中主设备号占高12位,次设备号占低20位
设备号格式:
-------------------------------------
| 主设备号 | 次设备号 |
-------------------------------------
12bit 20bit
主设备号: 用来找到对应驱动
次设备号: 用来区分使用同一个驱动的不同硬件
设备号的操作:
在Linux内核中,设备号的数据类型是dev_t
内核提供了一些操作宏来操作设备号
MAJOR ------------ 通过设备号获取主设备号 MINOR ------------ 通过设备号获取次设备号 MKDEV ------------ 通过主设备号和次设备号生成设备号
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
}
如何获取设备号?
{
设备号属于资源,在内核中如果想要使用设备号必须向内核申请
(1)静态申请
{
a.查看 /proc/devices 文件,找到一个未被使用的主设备号
500
b.根据设备个数分配次设备号,如果只有一个设备就用一个次设备号,次设备号一般从0开始
dev_t dev = MKDEV(major,minor);
c.调用register_chrdev_region向内核申请
}
(2)动态申请
{
动态申请就是由内核自动分配一个设备号
alloc_chrdev_region
}
}
}
}
{
1. struct cdev //字符设备驱动的结构,内核中用来表示一个字符设备驱动
{ //struct cdev 结构体定义
struct cdev
{
struct kobject kobj; //内嵌内核对象
struct module *owner; //字符设备所在内核模块对象的指针
const struct file_operations *ops; //设备支持的操作函数集合
struct list_head list; //内核链表,用来将所有注册到内核的cdev结构体变量连接成链表
dev_t dev; //设备号
unsigned int count; //属于同一主设备号的次设备号的个数
};
如何往内核中添加一个cdev
{
//往内核添加一个cdev结构体
构造一个cdev
初始化cdev ----- cdev_init
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
添加cdev到内核 ----- cdev_add
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
删除内核中的cdev ------ cdev_del
void cdev_del(struct cdev *p)
}
}
**2. struct file_operations //文件操作函数结构体**
{
//struct file_operations文件操作函数结构体定义
struct file_operations
{
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
..... //其他文件操作函数
}
在字符设备驱动中有这样的一个操作函数集合,用户应用使用系统调用访问设备文件时
最终调用的就是cdev中对应的操作函数集合
实际编写字符设备驱动程序,就是编写这些操作函数,并用这些操作函数初始化struct file_operations
结构体。
}
**3. struct inode //描述一个文件的物理结构**
{
文件(普通文件或设备文件)一旦创建,内核中就会创建一个对应的inode结构,
文件销毁,对应inode就删除.
主要数据成员:
dev_t i_rdev; //设备号
struct cdev *i_cdev; //指向一个cdev
}
**4. struct file //描述文件的打开属性**
{
open打开文件成功后创建
close关闭文件后销毁
主要数据成员:
//描述文件的打开属性,包括只读,只写,阻塞或非阻塞等。
unsigned int f_flags;
}
}
{
1. 应用程序调用 open
--->内核 sys_open
--->驱动的 xxx_open 函数
2. 应用程序调用 read
--->内核 sys_read
--->驱动的 xxx_read 函数
3. 应用程序调用 write
--->内核 sys_write
--->驱动的 xxx_write 函数
4. 应用程序调用 ioctl
--->内核 sys_ioctl
--->驱动的 xxx_ioctl 函数
5. 应用程序调用 close
--->内核 sys_close
--->驱动的 xxx_close 函数
......
其他文件操作函数类似
}
{
Linux中内核空间和用户空间相互独立,不能直接互相访问
如果要进行数据传输,需要借助以下函数
copy_to_user // 内核--->用户
copy_from_user // 用户--->内核
这两个函数有可能导致睡眠,不能在中断服务处理程序中使用
}
{
//ioctl函数是设备驱动开发中一个常用的函数
//主要用来给设备发送控制命令
ioctl函数原型:
int ioctl(int fd, ind cmd, unsigned long arg)
fd: 要操作的设备的设备文件描述符
cmd: 发送给设备的控制命令
{ //如何形成 cmd
ioctl函数中的第二个cmd参数用来区分各个不同的控制操作
cmd分为4个区域
-----------------------------------------
| 方向 | 数据尺寸 | 设备类型 | 序列号 |
----------------------------------------
| 2bit | 14bit | 8bit | 8bit |
-----------------------------------------
可以使用_IO类型的宏来生成cmd
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOC_NONE 0U
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \ //30
((type) << _IOC_TYPESHIFT) | \ //8
((nr) << _IOC_NRSHIFT) | \ //0
((size) << _IOC_SIZESHIFT)) //16
}
arg: 传送给命令的参数
}
{
//编写设备驱动程序
1. 编写设备文件操作函数集合
xxx_open, xxx_close, xxx_read, xxx_write, xxx_ioctl
2. 定义并初始化内核表示设备驱动的结构体 cdev 结构体,cdev_init
3. 添加 cdev结构体到内核, cdev_add
//测试设备驱动
1. 编写设备驱动测试应用程序
2. 创建设备文件节点
#mknod /dev/led0 c 108 0
----------------------------------------------------
设备文件名 字符设备 主设备号 次设备号
}
{
**1. 并发与竟态**
{
并发(concurrency)指的是多个执行单元同时、并行被执行。
因并发的执行单元对共享资源(硬件资源和软件上的全局、静态变量)的访问
而导致的竞争状态,称为竟态(race conditions)。
内核中产生竟态的场景
{
SMP(对称多处理器)之间
单个CPU中进程和进程之间
中断和进程之间
中断与中断之间
}
}
**2.解决竞态的方法**
{
当有执行单元在访问共享资源,应该禁止其他的执行单元来访问共享资源
对于访问共享资源的代码区称之为临界区,临界区应该使用互斥机制进行保护.
内核中互斥机制:
中断屏蔽
原子操作
自旋锁
信号量
互斥量
}
**3.中断屏蔽**
{
中断屏蔽可以解决中断和进程之间的竟态
同时因为内核进程的调度也 这样内核抢占进程之间的竟态也避免了。
local_irq_disable() //屏蔽中断
......临界区代码
local_irq_enable() //恢复中断
中断屏蔽可以解决除SMP以外所有竞态,但是由于CPU很多操作依赖于中断,
如果屏蔽的时间过长或者过于频繁,将导致系统响应变慢,慎用
屏蔽时间要尽量短。
}
**4.原子操作**
{ //原子操作指的是在执行过程中不会被打断的操作,原子操作分为位原子操作和整型原子操作
//原子变量可以解决所有情况下的竞态
位原子操作函数
{
void set_bit(int nr, volatile unsigned long *addr); //将地址addr的第nr位设置1
void clear_bit(int nr, volatile unsigned long *addr);//将地址addr的第nr位清0
void change_bit(int nr, volatile unsigned long *addr);//将地址addr的第nr位翻转
int test_bit(int nr, volatile unsigned long *addr);//返回地址addr的第nr位的值
}
整数原子操作
{
a. 原子变量的定义和初始化
{
数据类型:atomic_t //定义在 arch/arm/include/asm/atomic.h
atomic_t v = ATOMIC_INIT(1);//初始化为1
或者:
atomic_t v;
atomic_set(&v,1);
}
b.内核中提供的操作整型原子变量的函数或宏定义
{
atomic_set(v,data) //设置原子变量 v 的初值为 data
atomic_read(v) //读取原子变量 v的值
void atomic_add(int i, atomic_t *v) //原子变量 v加 i
void atomic_sub(int i, atomic_t *v) //原子变量 v减 i
atomic_inc(v) //原子变量v加1
atomic_dec(v) //原子变量v减1
//原子变量自增,自减操作后,测试原子变量是否为 0,为0返回 true,否则返回 false
atomic_inc_and_test(v)
atomic_dec_and_test(v)
}
c. 原子变量应用举例
{
//使用原子变量实现设备只能被一个进程打开
static atomic_t atomic = ATOMIC_INIT(1); //定义原子变量并初始化为 1
static int open(struct inode *inode, struct file *filp)
{
...
if(!atomic_dec_and_test(&atomic )) //原子变量减1后并判断是否为0,为0返回真
{ //原子变量减1后,不为0,表示该设备已经被其他进程打开
atomic_inc(&atomic);
return -EBUSY;
}
//该设备没有被其他进程打开,打开设备成功
...
return 0;
}
static int release(struct inode *inode, struct file *filp)
{
atomic_inc(&atomic); //进程关闭该设备后,原子变量加 1
return 0;
}
}
}
}
**5.自旋锁**
{
自旋锁是内核中一种典型的对临界资源进行互斥访问的手段.
如果一个内核任务尝试获取一个已经被占用的自旋锁,那么该任务进入忙等待,原地自旋,
重复去尝试获取,直到锁重新可用,所以自旋锁可以实现对临界区代码的互斥访问。
可以解决除中断以外的所有竞态情况。
自旋锁的使用:
包含头文件 linux/spinlock.h
1)定义初始化自旋锁
spinlock_t lock;
spin_lock_init(&lock);
2)获取自旋锁
spin_lock(&lock);//获取不到,原地自旋
spin_trylock(&lock);//不管是否获取,立即返回,获取到返回真,获取不到返回假
3)执行临界区代码
临界区速度快,不能调用引起睡眠的函数
4)使用完释放自旋锁
spin_unlock(&lock)
//使用自旋锁保护字符设备驱动中的临界代码
}
**6. 信号量**
{
信号量也是一种保护临界资源的互斥机制,本质上是一种睡眠锁。当任务获取不到信号量时,
将导致该任务睡眠。这个时候处理器可以去完成其他工作。
直到这个信号量可用,任务会被唤醒,唤醒的任务就获取到了信号量
如何使用信号量
{
(1)分配初始化
添加头文件linux/semaphore.h
struct semaphore sema;
sema_init(&sema,val);
(2)获取信号量
down(&sema);//如果进程无法获取信号量,进入不可中断的睡眠状态
//如果进程无法获取信号量,进入可中断的睡眠状态,在睡眠时可以接收外来的信号
down_interruptible(&sema);
//该函数需要判断返回值,如果返回0表示获取到了信号量,返回非0表示收到了信号
down_trylock(&sema);//不进入睡眠,直接返回,0表示获取到了信号量,非0表示没有
//如果进程无法获取信号量,进入不可中断的睡眠状态,但是睡眠有时间限制
down_timeount(&sema,long jiffies);
(3)访问共享资源(执行临界区代码)
(4)释放信号量
up(&sema);
}
}
**7. 互斥锁**
{
使用方法及场合和信号量一致,只是在任意时刻只允许一个进程进入临界区
struct mutex my_mutex; //定义互斥量
mutex_init(&my_mutex); //初始化互斥量
void mutex_lock(struct mutex *lock); //上锁
void mutex_unlock(struct mutex *lock); //解锁
}
// 三种互斥机制的比较和总结
原子变量: 极少使用
自旋锁: 开销低,临界代码短,不会引起系统睡眠,中断程序可使用,适用于短期锁定。
信号量: 开销比较大,等待信号量可能导致进程睡眠,适用于长期加锁。
互斥锁: 和信号量类似
}
//阻塞,非阻塞,轮询,异步通知
{
应用程序访问设备的两种方式: 阻塞和非阻塞
阻塞访问
系统调用read/write,如果程序所需的条件不满足,则应用程序阻塞(进程进入挂起状态),
进程在阻塞这段时间应用程序不消耗CPU时间。
非阻塞访问
系统调用read/write,如果程序所需的条件不满足,应用程序不阻塞,立即返回错误码
}
{
1. 等待队列的概念:
等待队列是内核的基本功能单位,以队列作为基础数据结构,以任务调度相结合,
能够实现设备驱动的阻塞访问,异步通知等操作。
重要数据结构:
等待队列头 数据类型 ------ wait_queue_head_t
等待队列节点 数据类型 ---- wait_queue_t
2. 等待队列的基本操作
{
(1)定义并初始化等待队列头
wait_queue_head_t my_queue; //定义等待队列头
init_waitqueue_head(&my_queue); //初始化等待队列头
或者使用宏定义 DECLARE_WAIT_QUEUE_HEAD:
DECLARE_WAIT_QUEUE_HEAD(my_queue);//定义并初始化等待队列头
(2)等待事件
//在条件不成立的情况下,将当前进程阻塞,等待条件为真,睡眠的进程不可被信号打断
wait_event(queue,condition);
//跟wait_event一样,增加了超时功能
wait_event_timeout(queue,condition,timeount);
//跟wait_event一样,和wait_event不一样的地方在,通过
// wait_event_interruptible阻塞的进程能被信号打断
wait_event_interruptible(queue,condition);
//跟wait_event_interruptible 相比,增加了超时机制
wait_event_interruptible_timeout(queue,condition,timeount);
(3)唤醒等待队列
//唤醒等待队列上的所有进程
wake_up(&queue); //与wait_event配对使用
wake_up_interruptible(&queue) //与wait_event_interruptible配对使用配对使用配对使用配对使用
}
3. 使用等待队列实现设备的阻塞访问
{ //使用等待队列实现读阻塞访问
a. 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_queue);
b. 在xxx_read函数中判断是否阻塞访问,是否有数据可读,没有就阻塞
{ //阻塞进程
int data_ready_flag = 0; //1表示有数据可读,0 表示没有数据可读
int xxx_read(struct file *file,.....)
{
...
if(file->f_flags & O_NONBLOCK)
{
//非阻塞访问
printk("no data to read ,noblock!\n");
return -1;
}
else
{
//阻塞访问,让进程等待 条件 为真
wait_event(my_queue, data_ready_flag);
}
}
}
c. 在 xxx_write函数中唤醒等待队列上的进程
{ //唤醒进程
int xxx_write(struct file *file,.....)
{
...
data_ready_flag = 1;
wake_up(&my_queue); //唤醒进程
}
}
}
}
{
1. I/O多路复用概念: 应用程序在访问设备之前,先调用poll或select函数询问一下该设备是否有数据
可读或可写,然后根据询问的结果来决定是否访问该设备。
应用程序通过IO多路复用技术,可以同时轮询多个IO设备。
2. I/O多路复用应用程序的实现
{ //应用程序调用poll函数轮询
poll函数用法
{
#include
struct pollfd 结构的指针,用于描述需要对哪些文件描述符的哪种类型的操作进行监控。
struct pollfd
{
int fd; /* 需要监听的文件描述符 */
short events; /* 需要监听的事件 */
short revents; /* 返回的已发生的事件 */
}
常用的events有:
POLLIN :表示有数据可读
POLLRDNORM:普通数据可读
POLLOUT: 表示数据可写
POLLWRNORM:普通数据可写
POLLERR: 发送错误
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:等待一组文件描述符中准备好文件描述符执行输入输出
参数:fds: struct pollfd 结构体指针,在fds参数中指定要监视的文件描述符集
nfds:监控的文件描述符的个数
timeout:超时时间设置[0:立即返回不阻塞 >0:等待指定毫秒数 其他:永远等待
返回值:成功:>0 超时:=0 出错:-errno
}
应用层IO多路复用调用举例
{
//保存着我们要监控的描述符的集合
//需要监听多个文件描述符时,定义相应的数组元素即可
struct pollfd fds[1];
fd = open(...);
fds[0].fd = fd; //要监控的描述符
fds[0].events = POLLIN;
fds[0].revents = 0; //输出参数
iret = poll(fds, 1, -1); //监控描述符的状态
//如果poll函数返回,表示有数据可读
if(fds[0].revents & POLLIN)
{
//有数据可读
rlen = read(fd,rbuff,1024);
printf("read len: %d buf:%s\n", rlen,rbuff);
}
}
}
3. I/O多路复用驱动层实现
{
在应用空间调用select poll 系列函数是最终调用驱动中f_ops中的poll函数
在该函数中可以调用poll_wait向驱动poll_table中添加一个等待队列
poll函数应该返回设备当前状态(POLLIN...)
wait_queue_head_t xxx_pool_que; //定义等待队列
unsigned int xxx_poll(struct file *file, struct poll_table_struct *p)
{
int mask = 0;
poll_wait(file,&xxx_pool_que ,p);
if(xxx_flag == BUF_EMPTY)
{
mask |= POLLOUT; //可写
}
if(xxx_flag == BUF_FULL)
{
mask |= POLLIN; //可读
}
return mask;
}
//在条件满足的位置调用 wake_up函数唤醒 xxx_pool_que 队列中的进程。
}
}
}
{
1. 异步通知的概念: 一旦设备就绪,设备驱动则主动通知应用程序,这样设备驱动就不需要
查询设备的状态。
异步通知通过信号来实现,当设备就绪时,设备驱动程序给相应的应用程序发送信号。
2. 应用程序异步通知的实现
{
//struct sigaction 结构体,记录了信号的处理方式
struct sigaction {
__sighandler_t sa_sigaction ; //指定信号的处理函数
//一些重要的标志位,比较重要的是 SA_SIGINFO,当设定了该标志位时,
//表示信号附带的参数可以传递到信号处理函数中
unsigned long sa_flags;
void (*sa_restorer)(void); //不再使用
sigset_t sa_mask; /*信号集,指定在信号处理过程中,哪些信号被屏蔽 */
};
//编写信号处理函数
void xxx_sig_handler(int signum, siginfo_t *siginfo, void *act)
{
printf("enter led_sig_handle\n");
if(signum == SIGIO)
{
if(siginfo->si_band & POLLIN) //在ubuntu16.04版本上无法识别该标志
{
//表示有数据可读,读这个设备
}
if(siginfo->si_band & POLLOUT)
{
//表示该设备可写
}
}
}
//安装信号处理函数
{
//初始化SIGIO信号的处理结构体,并设置SIGIO信号的处理
struct sigaction act;
struct sigaction oldact;
memset(&act,0, sizeof(struct sigaction));
memset(&oldact,0, sizeof(struct sigaction));
act.sa_flags = SA_SIGINFO; //设置信号可以传递参数到信号处理函数中
act.sa_sigaction = xxx_sig_handler;
sigaction(SIGIO, &act,&oldact); // 安装信号处理函数
}
//设置文件的拥有者为当前进程,目的是使驱动程序根据打开的文件file结构,
//能找到对应的进程,从而向该进程发送信号
fcntl(fd, F_SETOWN, getpid())
fcntl(fd, F_SETSIG,SIGIO); //设置标志输入输出的信号
int flag = 0; //设置文件的FASYNC标志,启动异步通知机制
flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|FASYNC)
-D_GNU_SOURCE
}
3. 设备驱动程序中异步通知的实现
{
(1) 定义异步通知链表头
struct fasync_struct *fasync_xxx;
(2) 实现异步通知fasync接口函数,在该函数中调用fasync_helper构造struct fasync_struct
并加入到链表中
int xxx_fasync(int fd, struct file *file , int on)
{
return fasync_helper(fd, file,on, &fasync_xxx);
}
(3) 在资源可用时,调用kill_fasync 发送信号
kill_fasync(&fasync_xxx,SIGIO,POLL_IN);
}
}
//中断和时间管理
{
1 什么是中断?
所谓中断,是指CPU在执行程序过程中,出现了某些突发事件急待处理,CPU必须
暂停执行当前程序,转去处理突发事件,处理完毕后,CPU又返回原程序被中断的
位置并继续执行。
2 中断的作用
{
a. 提高CPU和外设之间通信的效率,当外设准备好时,通过中断机制主动通知CPU,
CPU不再需要不停的去查询设备的状态;
b. 提高外部事件处理的实时性
}
3 中断在硬件上的连接处理方式
计算机外设 ----> 中断控制器 -----> CPU
外设产生电信号,发送给中断控制器,中断控制器能够检测和处理电信号,
决定是否发送给CPU,如果发送给了CPU,CPU就可以响应这个电信号,进行后续的中断处理。
4 CPU中断处理流程
---》中断产生,中断有优先级,高优先级可以打断低优先级的中断
---》中断异常向量表
---》保护现场
---》处理中断(执行中断处理程序)
---》恢复现场
---》跳转返回程序被打断位置,继续执行
嵌入式Linux系统中的中断处理,在设备驱动程序中实现。
}
{
中断在内核中是一种资源,使用前需要申请,关于向量表和中断控制器的初始化和配置工作内核已经完成
内核中使用struct irq_desc表示描述一个中断。
1 设备驱动程序使用中断的步骤:
{
(1) 编写中断服务处理程序
(2) 向内核申请中断
(3) 允许中断
(4) 中断使用完成后,释放中断
}
2 内核中断相关操作函数
{
a. 内核申请中断函数 request_irq
{
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev)
参数:
irq:要申请的中断号
handler:向系统注册的中断处理函数,中断发生后调用该函数,同时将中断号和dev传递给他
flags:中断标志位,一般设置为IRQF_DISABLED,表示在中断处理时屏蔽所有中断
IRQF_SHARED ,则表示多个设备共享中断
IRQF_TRIGGER_RISING 上升沿触发中断
IRQF_TRIGGER_FALLING 下降沿触发中断
name:中断名称 在/proc/interrupts中可看到该名称
dev:传递给中断处理函数的参数,一般用于在共享中断时传递设备信息
成功返回0,失败返回负数
}
b. 释放中断 free_irq
{
void free_irq(unsigned int irq, void *dev_id)
第一个参数是要释放的中断号
第二个参数必须和中断申请函数的最后一个参数相同
}
c. 中断禁止、使能相关函数
{
禁止和使能所有中断
local_irq_disable() //禁止本CPU内所有中断
local_irq_enable() //使能本CPU所有中断
禁止和使能一个中断
disable_irq(unsigned int irq) //禁止中断
enable_irq(unsigned int irq) //使能中断
irq 为需要禁止和使能的中断号
}
}
3 按键中断
{
//linux-3.4.39 中断号定义文件 s5p6818_irq.h
//查看电路原理图 KEY3--->PB8 触发方式都设置为下降沿触发
// KEY4--->PB16
获取KEY对应的中断号
{
获取中断号2种方法:
(1) 通过 gpio_to_irq 获取 GPIO管脚的中断号.
在Linux内核中,对于有中断功能的GPIO管脚,可以通过gpio_to_irq获得对应GPIO管脚的中断号.
内核中gpio相关函数://实现在 drivers/gpio/gpiolib.c //对GPIO口进行统一管理
{
int gpio_request(unsigned gpio, const char *label) //向内核申请一个GPIO口资源
int gpio_direction_input(unsigned gpio); //设置GPIO口为输入模式
int gpio_direction_output(unsigned gpio, int value) //设置GPIO口为输入模式
int gpio_get_value(unsigned gpio) //获取GPIO口状态
int gpio_set_value(unsigned gpio,int value) // 设置 GPIO口状态
void gpio_free(unsigned gpio) // 释放GPIO口
int gpio_to_irq(unsigned gpio) //GPIO口转换成相应的中断号
....
参数gpio ,表示相应的GPIO口号,需要自己定义,定义方法如下:
//例如 定义PC14端口 如下
#define CFG_IO_FS6818_BEEP (PAD_GPIO_C + 14)
}
(2) 在s5p6818_irq.h 文件中定义了 S5P6818 CPU所有的中断号
}
配置内核,去掉原有的按键驱动
{ //如果不做这一步,会发生中断冲突
Device Drivers --->
Input device support --->
[ ] Keyboards --->(不选)
重新编译内核
}
编写中断服务处理程序,并注册中断
{
//中断服务处理程序
irqreturn_t key_handler(int irq, void *data)
{
printk("key interrupt ,handler irq:%d \n ",irq);
return IRQ_HANDLED;
}
iret=request_irq(key3_irq_no, key_handler,IRQ_TYPE_EDGE_FALLING,"key3",NULL);
}
}
}
{
1 中断上半部分和中断下半部分的概念
{
理想情况下我们希望中断的处理程序越快越好,但是在某些场合无法满足这个要求,比如网卡接收数据,
如果网卡长时间处于中断,占用CPU资源,影响系统的并发能力和相应能力
为了解决这个问题,Linux内核中引入中断顶半部和底半部的机制,其实就是将中断处理程序分为两部分:
上半部分: 就是中断处理程序,完成一些比较紧急,需要立即处理的事情,比如说将网卡数据拷贝到内存
这个过程不可中断,其他事情就可以在中断下半部分中完成,在上半部分登记下半部分
下半部分:做一些不紧急,相对耗时的工作,比如将数据包交给协议层处理的过程。
中断下半部分可以被别的中断打断
}
2 中断下半部分的实现
{
中断下半部分主要有3种实现方式:tasklet,工作队列 和软中断
a. tasklet机制实现中断下半部分
{
//tasklet 结构体定义
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
(1) 定义并初始化 tasklet 结构体变量
{
struct tasklet_struct mytasklet;
tasklet_init(&mytasklet,tasklet_func,data);
//tasklet_func: 中断下半部分对应的处理函数
//data: 传递给 tasklet_func 函数的参数
或者
DECLARE_TASKLET(mytasklet,tasklet_func,data);
}
(2) 在中断处理函数(上半部分)中调度tasklet
tasklet_schedule(&mytasklet);
(3) 系统会在合适的时候,调用tasklet处理函数
注:tasklet还是工作在中断上下文,不允许睡眠
}
b. 工作队列机制实现中断下半部分
{
//工作队列结构体
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; //工作队列处理函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
typedef void (*work_func_t)(struct work_struct *work);
(1) 定义并初始化 work_struct 工作队列
struct work_struct mywork;
INIT_WORK(&mywork,work_func);
(2) 在中断处理函数(上半部)调度工作队列
schedule_work(&mywork);
(3) 调度完成后,内核会在适当的时候执行中断的上半部分
}
}
}
{
1. 内核系统定时器
{
内核中有一个系统定时器(硬件定时器),可以通过软件设置他的工作频率,周期性产生时钟中断。
称为滴答时钟.
内核系统时钟中断就必然有对应的中断处理函数,中断处理函数中需要完成如下任务:
{
更新系统运行时间
更新实际时间
检查进程的时间片信息
执行超时的定时器(软件定时器)
执行一些统计信息
......
}
系统时钟相关变量
{
HZ: 记录了时钟定时器的频率,也就是代表一秒钟产生多少个时钟中断;
在ARM中,HZ=100
tick: 1/HZ,发生一次时钟中断的时间间隔, 1tick = 10ms
jiffies:内核汇总一个32位全局变量,内核一般用它表示时间,它记录了开机以来发生了多少次时钟中断
持续时间
:16个月
jiffies+2*HZ //2s后的时间
}
内核提供的时间相关函数
{
//内核表示时间的结构体
struct timespec {
__kernel_time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
//时间比较函数
time_after(a,b) //时间a在b之后,返回true
time_before(a,b) //时间a在b之前,返回true
time_after_eq(a,b) //时间a在b之后或者相等,返回true
time_before_eq(a,b) //时间a在b之前或者相等,返回true
time_in_range(a,b,c) //时间a在b和c之间,返回true
//时间转换函数
jiffies_to_msecs // jiffies-->ms
jiffies_to_usecs // jiffies-->us
msecs_to_jiffies
usecs_to_jiffies
timespec_to_jiffies
jiffies_to_timespec
timeval_to_jiffies
jiffies_to_timeval
}
内核延时函数
{
//这些延时函数都是忙等待延时,不会导致睡眠,白白消耗CPU时间
void ndelay(unsigned long x) //纳秒级延时
udelay(n) //微秒级延时
mdelay(n) //毫秒级延时
void msleep(unsigned int msecs) //会引起睡眠
}
}
2. 内核定时器的使用(软件定时器的使用)
{
//软件定时器结构体
struct timer_list
{
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;//超时时jiffies值
struct tvec_base *base;
void (*function)(unsigned long);//超时处理函数
unsigned long data;//传递给超时函数参数
int slack;
};
(1) 分配定义定时器
struct timer_list mytimer;
(2) 初始化定时器
init_timer(&mytimer);//我们关心的三个字段需要另外指定
mytimer.expires = .....//指定超时时间
mytimer.function = ....//指定超时处理函数
mytimer.data = ........//指定传递给超时处理函数的参数
(3) 向内核添加启动定时器
add_timer(&mytimer); //后续和定时器有关的操作由内核完成
一旦超时时间到,自动调用超时处理函数
(4) 如果要修改定时器
mod_timer(&mytimer,jiffies+xxx);//设置超时时间为xxx之后
(5) 删除定时器
del_timer(&mytimer);
}
}
// 内核内存的使用 getconf PAGESIZE 查看内存页面的大小
{
1. kmalloc:用户申请指定长度的连续物理内存空间,分配过程中可能导致系统睡眠
{
void *kmalloc(size_t size, gfp_t flags);
参数flags:
较常用的 flags(分配内存的方法)
GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;
GFP_KERNEL —— 正常分配内存;
GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)
size:
要分配内存的大小,单位字节
注意: 申请内存不能超过128KB。
对应的内存释放函数为:
void kfree(const void *objp);
}
2. kzalloc 函数
{
kzalloc() 函数与 kmalloc() 非常相似,参数及返回值是一样的,
可以说是前者是后者的一个变种,因为 kzalloc() 实际上只是额外附加了 __GFP_ZERO 标志。
所以它除了申请内核内存外,还会对申请到的内存内容清零。
}
3. vmalloc函数
{
void *vmalloc(unsigned long size);
存的大小
参数: size, 指定分配内
vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存
在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对
申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。
对应的内存释放函数为:
void vfree(const void *addr);
注意:vmalloc() 和 vfree() 可以睡眠,因此不能从中断上下文调用。
}
4. __get_free_pages/ __get_free_page 函数 //在内核中分配指定的页面
{
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
gfp_mask(分配内存的方法):
GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会
被(高优先级进程或中断)打断;
GFP_KERNEL —— 正常分配内存;
GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)
order: 表示分配 2^order 次方页
返回值为对应的内核内存虚拟地址
//对应的内存释放函数为 free_pages
void free_pages(unsigned long addr, unsigned int order)
//分配一页
unsigned long __get_free_page(gfp_t gfp_mask);
//对应的内存释放函数
void free_page(unsigned long addr);
}
5. IO内存访问
{
(1) IO内存的概念: IO内存通常指的是存在与计算机外设里面,用来控制计算机外设工作的寄存器,在ARM体系
结构中,这些用来控制计算机外设工作的寄存器,称为IO内存。
ARM体系结构将IO内存和计算机普通内存进行统一编址,所以访问IO内存和访问普通内存一样。
(2) IO内存映射
对于IO空间的地址(寄存器地址),不能直接访问其物理地址,需要进行映射到虚拟地址空间之后才能访问
物理地址映射到虚拟地址映射函数:ioremap
void *ioremap(unsigned long offset, unsigned long size)
offset: 要映射的IO物理地址
size:映射的大小
映射成功,返回映射的虚拟地址
完成映射后,访问映射的虚拟地址就相当于访问原物理IO地址
解除映射:iounmap
void iounmap(void *addr);
(3) 访问映射的IO内存的函数
readb(c) //读8位,c是要读的地址
readw(c) //读16位,c是要读的地址
readl(c) //读32位,c是要读的地址
//和上面等价,新接口
ioread8(c)
ioread16(c)
ioread32(c)
writeb(v,c) //写8位 ,v是要写的数据, c是地址
writew(v,c) //写16位 ,v是要写的数据, c是地址
writel(v,c) //写32位 ,v是要写的数据, c是地址
// 和上面等价,新接口
iowrite8(v,c)
iowrite16(v,c)
iowrite32(v,c)
}
}
{
内核中设备的添加,删除或修改都会向应用程序发送热差拔事件,应用程序可以通过捕获这些
事件来自动完成某些操作,例如: 自动加载驱动,自动创建设备文件节点等。
1. 应用层自动创建设备节点
{
使用mdev自动创建设备节点的2个时机。
a. 执行 #mdev -s 命令, #mdev -s通常在根文件系统挂载完成后运行一次,它递归扫描 /sys/block 目录
和 /sys/class目录下的文件,根据文件的内容来自动创建设备文件。
b. 当内核发生了热插拔事件后,mdev会自动被调用,这通过查看 /etc/init.d/rcS 可以看到
echo /sbin/mdev > /proc/sys/kernel/hotplug
内核中一种发送热插拔事件调用应用程序的方式,就是执行 /proc/sys/kernel/hotplug 文件中的程序。
}
2. 驱动程序支持自动创建设备文件节点
{
mdev创建自动创建设备节点依赖 /sys/class 目录下的文件,在驱动程序中,我们只要调用
相应的函数在 /sys/class 目录下创建相应的文件即可。
创建类的API函数
class_create(owner, name)
owner 所属模块对象指针,THIS_MODULE
name: 类名;
device_create
函数原型:
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
参数说明:
class: 由 class_create 返回的类的结构体;
parent: 父类,一般写成NULL即可
devt: 设备号
drvdata: 添加到device中的数据,一般设为NULL即可;
fmt: 对应创建设备节点的设备名称
代码举例:
xxx_class = class_create(THIS_MODULE, "xxx_class");
//对应的创建设备文件节点名为 gmem
dev_tmp = device_create(xxx_class, NULL,devno , NULL, "gmem");
}
}
{
1. 设备驱动模型基础
{
sys文件系统
Sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,
并向用户模式程序提供详细的内核数据结构信息。
sys文件系统详细列出了所有设备,驱动和硬件相关的信息, 通过sys 文件系统,用户程序可以
访问内核中设备硬件的详细信息。
sys文件系统挂载
mount -t sysfs sysfs /sys
//sys文件是根据什么依据,来创建其内容呢?他的信息来源是什么呢
sys 文件系统信息来源
{
kobject是Linux设备模型的基本结构,类似于C++中的基类。
在实际应用中会将他嵌入到更大的对象中用来描述设备模型,比如device, bus, driver
所有的这些对象都使用了kobject,通过kobject联系到一起,形成一个树状结构,
这个树状结构就和/sys目录相对应
每个在内核中注册的kobject对象都会在/sys下有一个目录与之对应。
kset kobject的集合。
kobject通过kset组织成层次化的结构,kset是具有相同类型的kobject的集合。
kobject 和 kset 是组成Linux设备驱动模型的基本数据结构。
}
}
2. Linux总线设备驱动模型意义
{
Linux设备驱动软,硬件分离设计思想
当硬件连接方式(所使用的硬件资源)发生改变后,对应的驱动也需要进行修改,但是我们只需要修改
其中和硬件有关的部分,软件实现部分基本不变;
我们之前写的驱动是一个整体,一旦硬件进行任何改动,我们需要修改整个驱动。
这种驱动的移植性很差,为了提高驱动的可移植性和可维护性,我们可以将驱动进行分解成多个部分来实现。
总线设备驱动模型就是一种将驱动分解成多个部分实现的机制。
}
3. Linux 总线设备驱动模型
{
Linux总线设备驱动模型将驱动分为两个部分:和硬件相关部分(struct device) 和
硬件无关的部分(struct device_driver)
内核中定义了一条总线(struct bus_type)来管理device和driver,实现他们匹配和管理.
设备,设备驱动和总线是构成Linux总线设备驱动模型最重要的3个数据结构。
设备、设备驱动、总线的关系
(1) 设备、设备驱动都挂接在总线上,总线上有一## 标题个设备链表和设备驱动链表,分别用来挂接设备
和设备驱动。同时总线上还有一个match函数,用来匹配设备和设备驱动。
struct bus_type
{
......
int (*match)(struct device *dev, struct device_driver *drv);//匹配函数
struct klist klist_devices; //设备链表
struct klist klist_drivers; //设备驱动链表
......
}
(2) 设备结构体包含了总线成员(表示设备所挂接的总线) 和 设备驱动成员(设备所使用的驱动)
struct device 保存了设备驱动的硬件相关信息。
struct device
{
...
struct bus_type *bus; //表示该设备所挂接的总线
struct device_driver *driver; //表示该设备对应的驱动
...
}
(3) 设备结构体包含了总线成员(表示设备所挂接的总线)和设备链表(使用该驱动的设备链表)
struct device_driver 实现了设备驱动的与硬件无关的操作
struct device_driver
{
......
struct bus_type *bus; //表示该设备驱动所挂接的总线
struct klist klist_devices; //该链表挂接使用该设备驱动的设备链表
......
}
但是Linux并不直接使用上述的结构来实现具体的设备驱动模型,而是使用它们嵌入到某种具体驱动模型中去,
从而实现软件和硬件的分离。设备,设备驱动和总线相当于父类,具体的设备,具体的设备驱动和
具体的总线相当于子类。
}
4. Platform 设备和设备驱动
{
platform 总线: 在总线设备驱动模型中,设备必须挂接在总线上,这对于I2C设备,SPI设备和USB设备
这类挂接在具体的总线上的设备没有问题。
但是对于嵌入式设备而言,很多集成的外设,比如GPIO口,ADC等并没有挂接在具体的总线上,为了解决
这类问题,Linux发明了虚拟总线,称为Platform总线,挂接Platform总线上的设备称为平台设备(platform_device),
挂接在Platform总线上的设备驱动称为平台设备驱动(platform_driver);
//platform_bus 平台总线结构体定义
struct bus_type platform_bus_type =
{
.name = "platform",
.dev_attrs = platform_dev_attrs,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
//平台设备结构体定义,用来表示平台设备
struct platform_device
{
const char * name; //用于设备和设备驱动匹配
int id; //当用于同名设备的匹配,不用给 -1
struct device dev; //内嵌struct device结构体
u32 num_resources; //资源数组 长度(资源个数)
struct resource * resource; // 设备所使用的资源
const struct platform_device_id *id_entry;
......
};
//平台设备驱动结构体定义,用来表示平台设备驱动
struct platform_driver
{
int (*probe)(struct platform_device *); //匹配成功后调用的函数
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver; //内嵌struct driver结构体,name字段用于设备和设备驱动匹配
const struct platform_device_id *id_table;
};
当往platform 总线注册设备时(往总线的设备链表添加节点),内核就会遍历platform_driver链表,
取出其中的每一个节点和添加的硬件节点比较匹配,如果匹配成功就调用platform_driver 节点的probe函数,
并且将硬件信息(platform_device)传递给该函数, probe函数应该完成什么工作,有程序员决定。
当往platform 总线注册设备驱动时(往总线的设备驱动链表添加节点),内核就会遍历platform_device链表,
取出其中的每一个节点和添加的设备驱动节点比较匹配,如果匹配成功就调用platform_driver 节点的probe函数,
并且将硬件信息(platform_device)传递给该函数,probe函数应该完成什么工作,有程序员决定。
对于platform模型,我们只需要关注platform_device和platform_driver
}
5. 如何往内核中添加platform_device和platform_driver
{
(1) 定义并初始化struct resourse结构
struct resourse xxx_res[] =
{
[0] = {
.start = 起始IO地址/中断号
.end = 结束IO地址/中断号
.flags = 资源类型 IORESOURSE_MEM/IORESOURCE_IRQ
},
.....
}
//在probe函数调用platform_get_resource 获取资源信息
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num)
参数: dev 需要获取的平台设备 platform_device 结构体指针;
type 资源类型
num 资源编号,不同类型的资源都从0开始编号
(2) 定义并初始化 platform_device结构
struct platform_device xxx_dev =
{
.name = 名字, //用于匹配
.id = -1, //用于区分同名资源,不用-1
.num_resourses = ARRAY_SIZE(xxx_res), //资源个数
.resourse = xxx_res,
...
}
调用platform_device_register向内核添加 platform_device
(3) 定义并初始化 platform_driver结构
struct platform_driver xxx_drv =
{
.driver = {
.name = 名字, //用于设备和设备驱动匹配匹配
.owner = THIS_MODULE,
},
.probe = xxx_probe,
.remove = xxx_remove,
......
};
调用platform_driver_register向内核注册
}
}
练习:
//写一个纯粹的platform驱动框架, 不进行任何硬件操作
// ARM接口驱动 GPIO口驱动 按键中断
// PWM 驱动 看门狗定时器驱动 ADC驱动
// I2C驱动 SPI驱动
//块设备驱动 ,网络设备驱动