欢迎转载,转载请注明出处。
上一篇,简单介绍了裸机驱动与设备驱动的区别,以及Linux内核下字符设备的驱动架构;无操作系统时,上层应用直接访问驱动接口,应用工程师需要知道每一个设备的驱动接口。需要访问Flash时,就调用Drv_FlashRead()/Drv_FlashWrite()等函数;需要使用串口时,就调用Drv_SerialSend()/Drv_SerialWrite()等接口,简单说挂载了多少种不同的读写设备,上层应用就要使用多少种不同的读写接口函数。与之对应,在Linux操作系统下,用户层无需知道这些分类,对应用工程师来说,读、写操作只用read()/write()两个函数,无需关心底层有多少种读写接口。读Flash时,使用read(“flash”,…,…);读串口时,使用read(“serial”,…,…)(引号内代表驱动反映到上层时的文件名/设备名,省略号代表其它参数),操作系统和文件系统会根据传入的不同参数,去区分该调用底层哪一个驱动函数。这个过程就会使用到前面提到的主次设备号和file_oprations这个结构体。
这里我们先假设LED是通过GPIO口来控制的。
#include ... /* 一系列需要包含的头文件 */
#define GPIO_REG_CTRL 0x00000000 /* GPIO控制寄存器的物理地址(具体地址看相应数据手册) */
#define GPIO_REG_DATA 0x00000010 /* GPIO数据寄存器的物理地址(具体地址看相应数据手册) */
extern void Drv_LedInit( void ); /* LED初始化函数,外部声明,供应用层或其它模块调用 */
extern void Drv_LedOn( void ); /* LED点亮函数,外部声明,供应用层或其它模块调用 */
extern void Drv_LedOff( void ); /* LED关闭函数,外部声明,供应用层或其它模块调用 */
void Drv_LedInit( void )
{
GPIO_REG_CTRL |= ( 1 << n ); /* 假设将控制寄存器的第n为置1,可设置GPIO为输出模式 */
}
void Drv_LedOn( void )
{
GPIO_REG_DATA |= ( 1 << n ); /* 假设将数据寄存器的第n为置1,可设置GPIO为输出模式 */
}
void Drv_LedOff( void )
{
GPIO_REG_DATA &= ~( 1 << n ); /* 假设将数据寄存器的第n为清0,可设置GPIO为输出模式 */
}
当上层应用,需要操作LED时,就可以调用这三个函数。看起来好像挺简单的,底层只要实现这三个函数就可以。但上层应用必须明确的知道这三个函数的作用。这还只是LED,其它设备也有自己的接口函数,所以就需要应用工程师清楚地知道每个设备的驱动接口,显然不是很方便。
前篇提到,有操作系统时的驱动,需要按照操作系统给出的驱动架构来实现。所以Linux下的设备驱动也要按照一定的架构来实现。
同样是前面的实现LED驱动,也要实现LED的初始化、点亮、熄灭操作;但操作系统要求按照统一的驱动格式来实现,所以就不能直接给上层应用提供Drv_LedInit()、Drv_LedOn()、Drv_LedOff()这样的驱动接口;人在屋檐下,不能不低头嘛,想在操作系统下工作,就必须按操作系统要求的来。
结合前篇的驱动架构图,可以知道,用户空间是通过系统调用来访问底层设备的。操作系统需要特殊的信息(设备号)来识别应用层想要访问的是哪个设备,所以操作系统要求驱动都有自己的设备号,这样操作系统才能找到你,不然上层应用只是一个单纯的read()系统调用,操作瞬间就慌乱了,底层这么多的设备,它如何能快速定位到相应接口。如果她们能对话的话,应该是这样的一副场景:
应用层:嗨,操作系统!帮我呼叫一下Flash吧,我要从它那拿些资料。
操作系统:哦,好的!我帮你找它,你等一下。
然后操作系统去打开自己的管理资料,发现这任务根本完不成,因为它根本不知道谁是Flash,好多设备长得都差不多,它区分不开谁是Flash,谁是LED,谁是串口;更别说一些双胞胎兄弟(不同型号的同种设备),它发现也存在同名同姓的情况。这时候操作系统想了一个办法,为所有的设备都分配一个ID,就像身份证号码一样,作为唯一的标识符。而且为了提高效率,它规定所有同姓(同种)的设备,主设备号都一样,然后次设备号来具体区分某个设备,就像身份证号用前三位区分省份,紧接着三位区分城市一样。
所以在设计Linux下的设备驱动时,我们要先分配设备号。
dev_t dev = MKDEV( DRV_LEDMAJOR, 0 ); /* 将主次设备号,生成dev_t数据类型 */
/* 前面提到过,Linux内核会为每个设备注册一个固定的设备号,上层应用访问驱动时,内核就可以知道*/
/* 该访问哪一个驱动*/
然后操作系统为了更加方便的管理这些设备,为每一类设备都定义了一个用来管理设备的数据结构(可以理解为表格),驱动在注册进系统时,要先填这张表,这样操作系统在找到驱动时,就可以很快的了解到设备信息和设备的功能(操作接口)。
struct cdev {
struct kobject kobj; /* 内嵌的kobject对象 */
struct module *owner; /* 所属模块 */
const struct file_operations *ops; /* 文件操作结构体 */
struct list_head list; /* 内核链表 */
dev_t dev; /* 设备号 */
unsigned int count;
};
在设计驱动时,要完成对你这张表的填写,将设备拥有的技能展现给操作系统(struct file_operations),然后将这张表提供给操作系统,这样操作系统就可以方便高效的去管理设备。这样当上层应用想要访问某个设备时,操作系统就可以根据设备号,快速的找到这张表,定位到相应的操作(通过struct file_operations)。
#include
#include
#include ...
#define DRV_LEDMAJOR 0 /* 主设备号 */
/* LED的操作,都会映射在这个结构体中,最后填充到cedv这个结构体里 */
struct file_operations led_oprations = {
.owner = THIS_MODULE,
.read = led_read,
.write = led_write,
.open = led_open,
.ioctl = led_ioctl,
}
/* 可以理解为上诉所说的Drv_LedInit()函数 */
int led_open( struct incode *incode, struct file *filp)
{
return 1;
}
/* 读写函数,此处可要可不要 */
ssize_t led_read( struct file *filp, char _user *buf, size_t count, loff_t *f_pos )
{
return 1;
}
/* 读写函数,此处可要可不要 */
ssize_t led_write( struct file *filp, const char _user *buf, size_t count, loff_t *f_pos )
{
return 1;
}
/* ioctl函数,对LED的控制操作,在这个函数内 */
int led_ioctl( struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg )
{
struct led_dev *dev = filp->private_data;
switch( cmd ) /* 上层使用ioctl这个系统调用时,传进来的参数*/
{
case LED_ON:
Drv_LedOn(); /* 类似于上面提到了,LED点亮函数 */
break;
case LED_OFF:
Drv_LedOff(); /* 类似于上面提到了,LED关闭函数 */
break;
default:
return -ENOTTY;
}
return 0;
}
/* 驱动初始化 */
int Drv_LedInit( void )
{
int ret = 0;
struct cdev *led_devp = NULL;
dev_t dev = MKDEV( DRV_LEDMAJOR, 0 ); /* 将主次设备号,生成dev_t数据类型 */
/* 前面提到过,Linux内核会为每个设备注册一个固定的设备号,上层应用访问驱动时,内核就可以知道*/
/* 该访问哪一个驱动*/
if( 0 != DRV_LEDMAJOR ) /* 当主设备号为0时,主设备号将由操作系统分配*/
{
ret = register_chrdev_region( dev, 1, "LED"); /* 指定设备号 */
}
else
{
register_chrdev_region( &dev, 0, 1, "LED"); /* 系统分配设备号 */
}
if( ret < 0 )
{
return ret;
}
led_devp = kmalloc( sizeof( struct dev ), GFP_KERNEL );/* 为cdev结构分配空间 */
if (led_devp == NULL)
{
dev_err(&dev->dev, "No memory for device\n");
return -ENOMEM;
}
memset( led_devp, 0, sizeof( struct dev ) );
cdev_init( &led_devp->cdev, &led_oprations ); /* 初始化cdev结构体 */
led_devp->cdev.owner = THIS_MODULE;
/* 具体的函数实现,都在此结构体中 */
led_devp->cdev.ops = &led_oprations; /* 将设备操作函数,映射到cdev中 */
ret = cdev_add( &led_devp->cdev, devno, 1 ); /* 将cdev结构体,添加到系统中 */
}
/* 驱动卸载 */
int Drv_LedExit( void )
{
cdev_del( &led_devp->cdev );
kfree( led_devp->cdev );
unregister_chrdev_region( MKDEV( DRV_LEDMAJOR, 0 ), 1);
}
module_init( Drv_LedInit ); /* (insmode)加载驱动时,会自动识别 */
module_exit( Drv_LedExit ); /* (rmmode)卸载驱动时,会自动识别 */
上面给出了相对完整的驱动代码,当我们通过insmode加载驱动时,系统会自动识别去执行module_init()函数,然后驱动转而去执行Drv_LedInit ()函数,完成主次设备号的分配和cdev结构体的填充,将cdev添加到系统中。从代码中可以看到,原来在裸机驱动中的Drv_LedOn()、Drv_LedOff()函数,放在了ioctl()函数中,这样当上层应用使用ioctl()这个系统调用,就可以完成对LED的控制,而不在关心底层LED的开关操作是如何命名以及实现的。
所以到此我们应该有个大致的了解,有操作系统时的驱动,要比裸机驱动做的工作相对较多,这主要是为了实现操作系统的统一接口,来给上层应用制造便利。