应用程序:应用程序以文件形式访问各种资源,应用程序调用应用程序函数库完成各种功能。调用的应用程序函数库中,部分函数直接完成功能,部分函数(文件、进程、线程、网络)通过系统调用。
内核:处理系统调用,调用设备驱动程序。
驱动:负责直接与硬件通信。
注:系统调用,是一种特殊的接口,通过这个接口,用户可以访问内核空间。系统调用会向内核发出请求,实现内核提供的某些服务。一个API如果需要和内核打交道,就会需要一个或多个系统调用来完成特定的功能。
系统调用是内核与应用程序之间的接口,设备驱动程序是内核与硬件之间的接口。
Linux系统将常见的设备划分为三类:字符设备,块设备,网络设备。
字符设备:LED、KEY、UART、SPI、IIC、RTC、LCD 是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流。他不具备缓冲区,所以对这种设备的读写是实时的。
块设备:FLASH、SD卡、emmc 是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的, 他使用缓冲区来存放暂时的数据,待条件成熟后,将缓冲区数据一次性写入设备或者从设备一次性读取数据到缓冲区中。
网络设备:网卡、WIFI 网络设备是一类特殊的设备,它不像字符设备或块设备那样通过对应的设备文件节点访问。 网络设备是通过套接字来进行访问的。
注:字符设备是驱动开发中重点研究的对象。块设备和网络设备驱动程序虽然更复杂,但是这些驱动程序已经标准化,芯片厂商都已经做好了,我们直接使用即可。对于字符设备而言,虽然简单,但是比较杂,无法形成统一标准,需要用户自行开发。
用户是通过设备文件(/dev/xxx)来访问对应硬件设备。每个设备文件都有其文件属性:c代表字符设备,b代表块设备。每个设备文件都有两个设备号,主设备号和次设备号。
主设备号:用于标识驱动程序。LED
次设备号:用于标识使用同一个设备驱动程序的不同硬件设备。LED1、LED2、LED3
内核通过设备号(32bit,主设备号(12bit)+次设备号(20bit))来唯一的标识一个设备。在/dev目录下使用ls -l命令可以查看各个设备的设备类型、主从设备号等。cat /proc/devices可以查看系统中所有设备对应的主设备号。
注:设备文件的主设备号必须与设备驱动程序在申请时主设备号保持一致,这样就能保证应用程序通过访问设备文件调用指定的设备驱动程序,进而控制硬件操作。
1.对设备进行初始化和关闭
2.把数据从内核传送到硬件,从硬件读取数据到内核
3.读取应用程序传送给设备文件的数据和回送应用程序请求的数据
4.检测和处理设备出现的错误等。
当在应用层调用open,read,write,ioctl等函数接口对/dev目录下的设备文件进行访问时,硬件设备对应驱动程序会相应的调用xxx_open,xxx_read,xxx_write,xxx_ioctl等函数接口,驱动程序通过这些函数接口来完成对硬件的操作。
注:写驱动的核心就是实现xxx_open,xxx_read,xxx_write,xxx_ioctl等函数的编写。
在目前(2.6版本以上)的内核版本中,存在三种流行的字符设备编程模型:杂项设备驱动模型,早期经典标准字符设备驱动模型,Linux2.6标准字符设备驱动模型。本次主要讲解早期经典标准字符设备驱动模型,其它两个模型在之后章节进行讲解。
Linux系统借鉴了面向对象的思想来管理设备驱动,每一类设备都会有一个特定的结构体来描述它,这个结构体包含了设备的基本信息,以及操作设备方法(函数指针),所以,编写程序实际上就是实现核心结构,然后把这个结构注册到内核中。
所以,学习一种设备驱动,第一步就是要学习该设备对应数据结构,弄清楚成员时什么意思,什么作用,以及是否一定需要实现成员。
1)查看原理图,数据手册,了解设备的操作方法
2)在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始
3)注册驱动程序,给用户空间提供访问的接口,比如在/dev/目录下生成设备文件
4)设计所要实现的操作:比如open,close,write,ioctl等函数
5)实现中断服务(中断并不是每个设备驱动所听到的);
6)编译该驱动程序到内核中,或者用insmod命令加载
7)编写应用程序测试驱动程序
头 文 件:#include <linux/fs.h>
函数原型:int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
函数功能:字符设备注册函数,用于申请设备号,同时将驱动接口函数加载到内核。
函数参数:
major 申请的主设备号,指定为0代表自动分配,返回值就是分配的设备号。
name 设备名,用来描述设备。/proc/devices列举出所有已经注册的设备。
fops 文件操作对象(结构体),提供驱动接口函数
函数返回值:成功返回系统分配的主设备号,出错返回负数。
注: 1.很显然,register_chrdev函数应该在驱动加载函数中调用。
2.经典方式注册,只需要指定主设备号。次设备号(0-255)默认全部被注册,也就是0-255之间任意一个次设备号都能匹配到该驱动。
int xxx_open(struct inode *inode, struct file *file)
{
printk("xxx_open is run\n");
return 0;
}
ssize_t xxx_read(struct file *file, char __user *app_buff, size_t size, loff_t *loff)
{
printk("xxx_read is run\n");
return 0;
}
ssize_t xxx_write(struct file *file, const char __user *app_buff, size_t size, loff_t *loff)
{
printk("xxx_write is run\n");
return 0;
}
int xxx_close(struct inode *inode, struct file *file)
{
printk("xxx_close is run\n");
return 0;
}
struct file_operations fops =
{
.owner = THIS_MODULE,
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_close
};
头 文 件:#include <linux/fs.h>
函数原型:void unregister_chrdev(unsigned int major,const char *name);
函数功能:字符设备注销函数,用于释放设备号,同时将驱动接口函数从内核中卸载。
函数参数:
major 要注销驱动的主设备号
name 要注销驱动的名字
函数返回值 无
注: 很显然,该函数在驱动卸载函数中调用。
在编写应用程序之前,我们首先为硬件设备创建对应的设备文件节点。创建设备文件命令使用如下:
mknod <设备节点名> <设备类型> <主设备号> <次设备号>
示例:mknod /dev/xxx_dri c 250 0
注意:/dev/目录中的文件都是在内存中,断电后/dev/文件就会消失。 应用程序实例代码如下:
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int fd;
fd = open("/dev/xxx_dri",O_RDWR);
if(fd < 0)
{
perror("open");
return -1;
}
close(fd);
return 0;
}
系统运行时一般情况下,分用户态和内核态,这两种运行态下的数据互不可见的。
驱动程序是内核的一部分,工作在内核态,应用程序工作在用户态。这样就存在数据空间访问的问题:无法通过指针直接将二者的数据地址进行传递。
问题的解决办法是:系统提供一系列函数帮助完成数据空间转换:
例如: copy_from_user 、copy_to_user。
头 文 件:#include <asm/uaccess.h>
函数原型:long copy_from_user(void *to,const void __user *from, unsigned long n)
函数功能:将数据从用户空间拷贝到内核空间
函数参数:
to 数据会存储到该指针指向的空间
from 从应用层传输来的数据
n 传输数据的字节个数
函数返回值:成功返回值为0,失败返回值大于0,表示还剩下多少个没有拷贝成功
头 文 件:#include <asm/uaccess.h>
函数原型:long copy_to_user(void __user *to,const void *from, unsigned long n)
函数功能:将数据从内核空间拷贝到用户
函数参数:
to 传输到该指针指向的空间
from 向应用层传输的数据
n 传输数据的字节个数
函数返回值 成功返回值为0,失败返回值大于0,表示还剩下多少个没有拷贝成功
使用示例见示例代码2_example
创建设备文件:
头 文 件:#include <linux/device.h>
函数原型:struct class * class_create(struct module *owner, const char *name)
函数功能:创建一个类
函数参数:
owner 直接写THIS_MODULE
name 类名,自定义
函数返回值:成功返回指向类的指针,失败返回NULL
头 文 件:#include <linux/device.h>
函数原型:struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
函数功能:创建一个设备文件
函数参数:
class class结构体,class_create调用之后得到的返回值。
parent 表示父亲,一般直接填NULL。
devt 完整设备号(32位),只要是类型为dev_t的,该变量就是存放完整设备号的。
drvdata 私有数据,一般直接填NULL。
fmt... 表示可变参数,字符串,表示设备节点名字。
函数返回值:成功返回设备指针,失败返回NULL
删除设备文件:
头 文 件:#include <linux/device.h>
函数原型:void class_destroy(struct class *class)
函数功能:摧毁类
函数参数:class 类指针
函数返回值:无
头 文 件:#include <linux/device.h>
函数原型:void device_destroy(struct class *class, dev_t devt)
函数功能:摧毁设备文件
函数参数:
class 类指针
devt 完整设备号
函数返回值:无
设备号的合成与分解:
合成完整设备号: MKDEV(主设备号,次设备号)
分解主设备号: MAJOR(完整设备号)
分解次设备号: MINOR(完整设备号)