Linux设备驱动开发
Linux系统调用实现原理
- 作用:实现用户应用程序和内核程序的交互。
- 原理:基于软终端实现。
- 结论:应用程序调用和内核函数之间的调用关系。
应用open->C库open->软中断->内核sys_open->应用open返回。
应用cloase->C库close->软中断->内核sys_close->应用close返回。
应用read->C库read->软中断->内核sys_read->应用read返回。
应用write->C库write->软中断->内核sys_write->应用write返回。
Linux内核设备驱动开发相关内容
何为设备驱动?
- 切记设备驱动两个核心内容:
驱动一定要操作硬件。
驱动一定要能给用户提供访问操作硬件的接口(函数)。将来应用程序能给利用这些接口访问硬件。
Linux内核设备驱动的分类:三大类
- 字符设备驱动
特点:字符设备硬件操作的数据按照字节流形式访问。
例如:LED、按键、蜂鸣器、LCD显示屏(RGB)、触摸屏(绝对坐标X,Y)、鼠标(相对坐标)、摄像头、声卡、GPS(高度,时间,经纬度)、GPRS(AT指令:字符串)、蓝牙、
只要是UART接口的外设都是字符设备,各种硬件传感器都是字符设备。
- 块设备驱动
特点:块设备操作的数据按照数据块进行访 问,一般数据块为512字节,一次性访问512字节。
例如:硬盘、U盘、TF卡、SD卡、EMMC、Nonflash、nandflash。
注意:此驱动Linux内核完美支持,通常不需要进行修改。
- 网络设备驱动
特点:数据按照网络协议进行。网络设备驱动属于数据链路层。
例如:有线网卡驱动和无限网卡驱动。
注意:这些驱动芯片厂家提供源码,无需开发。
Linux系统的理念(信仰):一切皆文件
- “一切”:就是指任何硬件外设。
- “一切皆文件”:计算机中任何硬件在Linux系统中都是以文件的形式访问操作。
- 核心:Linux应用程序要想访问某个硬件,必须找到这个硬件对应的文件,将来访问这个文件本质就是在访问硬件。
Linux系统设备对应的文件分两类:字符设备文件和块设备文件。
- 网络设备无设备文件,通过socket套接字访问。
字符设备文件属性
- 字符设备文件本身就是字符设备硬件。
访问字符设备文件本身就是访问硬件。
- 字符设备文件只能存在于根文件系统必要目录dev目录下。
例如:下位机操作执行:ls /dev/ttySAC* -lh。
crw-rw---- 204, 64 /dev/ttySAC0
“c”:表示此文件为字符设备文件
“204”:表示此字符设备文件包含的主设备号。
“64”:表示第一个串口的字符设备文件的次设备号。
“ttySAC0”:表示第一个串口的字符设备文件名。
- 将来访问字符设备硬件只需要访问对应的字符设备文件即可。
问:怎么访问?
答:利用系统调用函数。
int fd = open("/dev/ttySAC0", O_RDWR); char buf[1024] = {0}; //读数据 read(fd, buf, sizeof(buf)); //写数据 write(fd, "hello", 5); //关闭设备 close(fd);
主设备号、次设备号、设备号
- 设备号:同时包含了主设备号和次设备号
- 设备号的数据类型:dev_t(unsigned int)
- 设备号的高12个bit位表示主设备号。
- 设备号的低20个bit位表示次设备号
- Linux内核提供的三者转换的宏:
(已知主、次设备号来合并设备号),设备号=MKDEV(已知的主设备号,已知的次设备号)
(从设备号中提取主设备号)主设备号=MAJOR(已知的设备号)
(从设备号中提取次设备号)次设备号=MINOR(已知的设备号)
主设备号作用:
- 应用程序设备文件的主设备号在茫茫的Linux内核驱动中找到对应的唯一驱动程序。
- 简称:应用根据主设备号找驱动。
- 结论:一个驱动仅有唯一的主设备号。
此设备号作用:
- 如果一个驱动管理多个同类型的硬件设备驱动将来根据次设备号来找到要访问的具体某个硬件设备
- 简称:驱动根据次设备号找硬件。
- 结论:一个硬件个体仅有唯一的次设备。
由于主、次设备号对于Linux内核来说是一种宝贵的资源,所以驱动程序或者硬件要关联某个主设备号和次设备号必须向内核操作系统申请设备号资源
申请和释放设备号的函数分别是:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
- 函数功能:向Linux内核申请设备号。
- 形参:
dev:保存将来内核给你分配的设备号信息。
baseminor:内核希望起始的次设备号。一般给0也就是次设备号从0开始分配。例如:现在有四个硬件,对应的设备号分配:0,1,2,3
count:次设备号的个数。
name:指定设备的名称,名字随便取,作为调试手段,将来通过执行cat /proc/devices命令来获取名称。
void unregister_chrdev_region(dev_t from, unsigned count)
- 函数功能:释放申请到的设备号。
- 参数:
from:传递申请到的设备号,from中包含唯一的主设备号和起始设备号。
count:传递次设备号的个数。
如何编写一个字符设备驱动呢?
- 再次明确:驱动程序属于Linux内核的一部分。也就是内核的其他代码是可以调用自己写的驱动程序的函数或者变量,也就是内核代码可以互相直接调用。
- 再次明确:不管是什么驱动。都必须有两个核心内容:
必须操作硬件。
必须给应用程序提供操作硬件的接口。(函数)
Linux字符设备驱动都要关联唯一的主设备号,如果驱动管理多个硬件,每个硬件还要关联多个次设备号。
- 结论:通过以上信息,发现字符设备驱动本身就是一个“事物”,这个事物本身还包含一了一些属性(设备号)和方法(硬件操作接口函数)。所以驱动必然有对应的数据结构(类)。
自行设计Linux内核字符设备驱动的数据结构
struct char_device { char *name;//驱动的名称 dev_t dev;//驱动申请的设备号 int count;//驱动申请的次设备号的个数 int (*open)(...);//提供打开硬件接口 int (*close)(...);//提供关闭硬件的接口 int (*read)(...);//提供读硬件数据接口 int (*write)(...);//提供向硬件写入数据接口 };
*· 缺点:将来根据用户的需求经常要改动操作接口,一会加了lseek,一会加个mmap等,不便于维护,提取出来单独管理。
- 优化:
//声明描述字符设备驱动的数据结构 strucet file_operations{ int (*open)(..); int (*close)(...); int (*read)(...); int (*write)(...); }; //声明描述字符设备驱动的数据结构 struct char_device { char *name;//驱动的名称 dev_t dev;//驱动申请到的设备号。 int count;//驱动申请的次设备号的个数。 struct file_operations *ops;//给字符设备驱动定义的结构体。 }; }
利用自行设计的数据结构来实现一个字符设备驱动
- 1、定义初始化硬件操作接口对象(实例化硬件操作接口对象)
struct file_operations led_fops = { .open = led_open, //打开设备 .close = led_close,//关闭设备 .read = led_read,//读设备 .write = led_write//写设备 };
- 定义初始化一个字符设备驱动对象(实例化对象)
struct char_device led_dev = { .name = "led", .dev = (申请好的设备号)(alloc_chrdev_region) .count = 次设备号的个数、 .ops = &led_fops //添加硬件操作接口 };
- 2、吹毛求疵:初始化有点不爽,进行优化——提供一个函数来进行初始化,将两个对象关联起来。
- 定义初始化函数:
void char_device_init(struct char_device *dev, struct file_operations *fops) { dev->ops = fops; //两者结合,提供操作接口。 } //优化之后的程序使用: char_device_init(&led_dev, &led_fops);
- 3、目前内核还不认这个字符设备驱动,还需要向内核注册登记这个字符设备驱动对象,并且这个对象里面有操作接口:
自行设计一个注册函数:
void char_device_add(struct char_device *dev, dev_t dev, int count, char *name) { //1、先将其余没有初始化的字段初始化 dev->name = name; dev->dev = dev; dev->count = count; //2、向内核注册登记 //思路:事先在内核中准备一个大数组,数组的下标就是主设备号,数组元素就是字符设备对象的地址。 //结论:以主设备号为下标,将要注册的字符设备对象的地址放到这个大数组中即可。 //联想:应用如何调用底层驱动提供的操作接口函数。应用open->C库open ->软中断->内核sys_open->应用open,这里要明确:内核的sys_open只有一个。 //问:内核唯一的函数sys_open如何能找到众多驱动中对应的open接口呢? //应用open->软中断->内核sys_open->驱动的beep_open(如何找到beep_open呢?) //答:由于字符设备文件本身包含了主设备号,当应用open时也可以获取主设备号,最终跑到内核的sys_open,进程在内核的sys_open中找到内核事前准备好的大数组,然后以主设备号为下标找到应用的字符设备驱动对象,例如led_dev,一旦内核的sys_open找到字符设备驱动对象led_dev然后直接调用代码即可:int 内核:sys_open(...){ //1、在大数组中以主设备号为下标找字符设备对象地址, //2、直接调用字符设备对象中的操作函数接口。 &led_dev->ops->open();//最终调用驱动的led_dev的函数 } //以此类推: }
* 结论:一旦注册成功,将来这个驱动静静在内存中等待着应用程序利用系统调用函数来访问驱动注册的接口函数
- 4、踏踏实实的根据用户需求编写各个操作接口函数:
int led_open();
int led_close();
int led_read();
int led_write();
- 5、设计一个函数将字符设备对象从内核卸载,也就是从大数组中删除
void char_device_del(struct char_device *dev) { //1、以dev->dev的主设备号为下标,以大数组中删除字符设备对象即可。 } 一旦删除,除非重启,否则不可恢复。