一、Linux设备驱动概述

一、设备驱动的作用

对设备驱动最通俗的解释就是“驱使硬件设备行动”。 驱动与底层硬件直接打交道, 按照硬件设备的具体工作方式, 读写设备的寄存器, 完成设备的轮询、 中断处理、 DMA通信, 进行物理内存向虚拟内存的映射等, 最终让通信设备能收发数据, 让显示设备能显示文字和画面, 让存储设备能记录文件和数据。
由此可见, 设备驱动充当了硬件和应用软件之间的纽带, 应用软件时只需要调用系统软件的应用编程接口(API) 就可让硬件去完成要求的工作。 在系统没有操作系统的情况下, 工程师可以根据硬件设备的特点自行定义接口, 如对串口定义SerialSend() 、 SerialRecv() , 对LED定义LightOn() 、LightOff() , 对Flash定义FlashWr() 、 FlashRd() 等。 而在有操作系统的情况下, 驱动的架构则由相应的操作系统定义, 驱动工程师必须按照相应的架构设计驱动, 这样, 驱动才能良好地整合入操作系统的内核中。

驱动:向下驱动具体设备,向上提供调用接口。

二、无操作系统时的设备驱动

对于功能比较单一、 控制并不复杂的系统,并不需要多任务调度、 文件系统、 内存管理等复杂功能。一个无限循环中夹杂着对设备中断的检测或者对设备的轮询是这种系统中软件的典型架构, 如代码1.1所示:

// 代码清单1.1 单任务软件典型架构
int main(int argc, char const *argv[])
{
	while(1)
	{
		if (serialInt == 1)
		/* 有串口中断 */
		{
			ProcessSerialInt();	/* 处理串口中断 */
			serialInt = 0;		/* 中断标志清零 */
		}
		if (keyInt == 1)
		{
			ProcessKeyInt();	/* 处理按键中断 */
			keyInt = 0;			/* 中断标志清零 */
		}
		status = CheckXXX();
		switch(status)
		{
			...
		}
		...
	}
	return 0;
}

在这样的系统中, 虽然不存在操作系统, 但是设备驱动则无论如何都必须存在。 一般情况下, 每一种设备驱动都会定义为一个软件模块, 包含.h文件和.c文件, 前者定义该设备驱动的数据结构并声明外部函数, 后者进行驱动的具体实现。 譬如, 可以像代码清单1.2那样定义一个串口的驱动。

/*******************************
* serial.h文件
********************************/
extern void SerialInit(void);
extern void SerialSend(const char *buf, int count);
extern void SerialRecv(char *buf, int count);
void SerialIsr(void);

/*******************************
* serial.c文件
*******************************/
/* 初始化串口 */
void SerialInit(void)
{
	...
}

/* 串口发送 */
void SerialSend(const char *buf, int count)
{
	...
}

/* 串口接收 */
void SerialRecv(char *buf, int count)
{
	...
}

/* 串口中断处理函数 */
void SerialIsr(void)
{
	...
	SerialInt = 1;
}

其他模块想要使用这个设备的时候, 只需要包含设备驱动的头文serial.h, 然后调用其中的外部接口函数。
在没有操作系统的情况下, 设备驱动的接口被直接提交给应用软件工程师, 应用软件没有跨越任何层次就直接访问设备驱动的接口。 驱动包含的接口函数也与硬件的功能直接吻合, 没有任何附加功能。 图1.1所示为无操作系统情况下硬件、 设备驱动与应用软件的关系。
无操作系统情况下硬件、 设备驱动与应用软件的关系有的工程师把单任务系统设计成了如图1.2所示的结构, 即设备驱动和具体的应用软件模块之间平等, 驱动中包含了业务层面上的处理, 这显然是不合理的, 不符合软件设计中高内聚、 低耦合的要求
在这里插入图片描述另一种不合理的设计是直接在应用中操作硬件的寄存器, 而不单独设计驱动模块, 如图1.3所示。 这种设计意味着系统中不存在或未能充分利用可重用的驱动代码

无操作系统时,架构一般为:无限循环中对中断检测和任务轮询。设备的驱动代码写在.c文件中,接口在.h文件中声明,使得软件工程师包含头文件就可以直接使用。设备驱动操作具体设备,并向上提供接口,才符合高内聚,低耦合的设计要求。

三、有操作系统时的设备驱动

有操作系统时,驱动的硬件操作工作必不可少,还需要将驱动融入内核。为了实现这种融合, 必须在所有设备的驱动中设计面向操作系统内核的接口, 这样的接口由操作系统规定, 对一类设备而言结构一致, 独立于具体的设备。
当系统中存在操作系统的时候, 驱动变成了连接硬件和内核的桥梁。 如图1.4所示, 操作系统的存在势必要求设备驱动附加更多的代码和功能, 把单一的“驱使硬件设备行动”变成了操作系统内与硬件交互的模块,不再给应用软件工程师直接提供接口。

将驱动写入内核并没有给驱动带来多大好处,但操作系统的存在能使得资源的分配和利用更加高效,同时驱动必须提供由OS内核规定的接口,这样(类UNIX系统)应用程序就可以通过read()、write()来读写硬件。

简而言之, 操作系统通过给驱动制造麻烦来达到给上层应用提供便利的目的。

四、Linux设备驱动

1.设备的分类及特点

计算机系统的硬件主要由CPU、 存储器和外设组成。 随着IC制作工艺的发展, 目前, 芯片的集成度越来越高, 往往在CPU内部就集成了存储器和外设适配器。
驱动针对的对象是存储器和外设(包括CPU内部集成的存储器和外设) ,而不是针对CPU内核。Linux将存储器和外设分为3个基础大类:字符设备、块设备、网络设备。
字符设备指那些必须以串行顺序依次进行访问的设备, 如触摸屏、 磁带驱动器、 鼠标等。 块设备可以按任意顺序进行访问, 以块为单位进行操作, 如硬盘、 eMMC等。

驱动针对的对象是存储器和外设,存储器和外设分为3个基础大类:字符设备(串行顺序访问)、块设备(任意顺序访问),网络设备(使用套接字接口)

2.Linux设备驱动与整个软硬件系统的关系

除网络设备外, 字符设备与块设备都被映射到Linux文件系统的文件和目录, 通过文件系统的系统调用接口open() 、 write() 、 read() 、 close() 等即可访问字符设备和块设备。
Linux的块设备有两种访问方法: 一种是类似dd命令对应的原始块设
备, 如“/dev/sdb1”等; 另外一种方法是在块设备上建立FAT、 EXT4、 BTRFS等文件系统, 然后以文件路径如“/home/barry/hello.txt”的形式进行访问。
在Linux中, 针对NOR、 NAND等提供了独立的内存技术设备(Memory Technology Device, MTD) 子系统, 其上运行YAFFS2、 JFFS2、 UBIFS等具备擦除和负载均衡能力的文件系统。 针对磁盘或者Flash设备的FAT、 EXT4、 YAFFS2、 JFFS2、 UBIFS等文件系统定义了文件和目录在存储介质上的组织。 而Linux的虚拟文件系统则统一对它们进行了抽象。
在这里插入图片描述

字符设备和块设备被映射到Linux的文件系统上,通过系统调用open()、read()、write()、close()等访问。

块设备的两种访问方法:1、dd命令;2、在块设备上建立文件系统,并通过路径名访问

五、设备驱动Hello World:LED驱动

1.无操作系统时的LED驱动

在嵌入式系统的设计中, LED一般直接由CPU的GPIO(通用可编程I/O) 口控制。 GPIO一般由两组寄存器控制, 即一组控制寄存器和一组数据寄存器。 控制寄存器可设置GPIO口的工作方式为输入或者输出。 当引脚被设置为输出时, 向数据寄存器的对应位写入1和0会分别在引脚上产生高电平和低电平; 当引脚设置为输入时, 读取数据寄存器的对应位可获得引脚上的电平为高或低。
屏蔽具体CPU的差异, 假设在GPIO_REG_CTRL物理地址中控制寄存器处的第n位写入1可设置GPIO口为输出, 在地址GPIO_REG_DATA物理地址中数据寄存器的第n位写入1或0可在引脚上产生高或低电平, 则在无操作系统的情况下, 设备驱动见代码清单1.3。

/* 代码清单1.3 无操作系统时的LED驱动 */ 
#define reg_gpio_ctrl *(volatile int *)(ToVirtual(GPIO_REG_CTRL))
#define reg_gpio_data *(volatile int *)(ToVirtual(GPIO_REG_DATA))

/* 初始化LED */
void LightInit(void)
{
	reg_gpio_ctrl |= (1 << n);	/* 设置GPIO为输出 */
}

/* 点亮LED */ 
void LightOn(void)
{
	reg_gpio_data |= (1 << n);	/* 在GPIO上输出高电平 */
}

/* 熄灭LED */
void LightOff(void)
{
	reg_gpio_data &= ~(1 << n);	/* 在GPIO上输出低电平 */
}

上述程序中的LightInit() 、 LightOn() 、 LightOff() 都直接作为驱动提供给应用程序的外部接口函数。 程序中ToVirtual() 的作用是当系统启动了硬件MMU之后, 根据物理地址和虚拟地址的映射关系, 将寄存器的物理地址转化为虚拟地址。

2.Linux下的LED驱动

在Linux下, 可以使用字符设备驱动的框架来编写对应于代码清单1.3的LED设备驱动。操作硬件的LightInit() 、 LightOn() 、 LightOff() 函数仍然需要, 但是, 遵循Linux编程的命名习惯, 重新将其命名为light_init() 、 light_on() 、 light_off() 。 这些函数将被LED设备驱动中独立于设备并针对内核的接口进行调用, 代码清单1.4给出了Linux下的LED驱动

#include .../* 包含内核中的多个头文件 */
/* 设备结构体 */
struct light_dev {
	struct cdev cdev; /* 字符设备cdev结构体 */
	unsigned char vaule; /* LED亮时为1, 熄灭时为0, 用户可读写此值 */
};
struct light_dev *light_devp;
int light_major = LIGHT_MAJOR;
MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
MODULE_LICENSE("Dual BSD/GPL");

/* 打开和关闭函数 */
int light_open(struct inode *inode, struct file *filp)
{
	struct light_dev *dev;
	/* 获得设备结构体指针 */
	dev = container_of(inode->i_cdev, struct light_dev, cdev);
	/* 让设备结构体作为设备的私有信息 */
	filp->private_data = dev;
	return 0;
}

int light_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 读写设备:可以不需要 */
ssize_t light_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	struct light_dev *dev = filp->private_data; /* 获得设备结构体 */
	if (copy_to_user(buf, &(dev->value), 1))
	return -EFAULT;
	return 1;
}
ssize_t light_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
{
	struct light_dev *dev = filp->private_data;
	if (copy_from_user(&(dev->value), buf, 1))
	return -EFAULT;
	/* 根据写入的值点亮和熄灭LED */
	if (dev->value == 1)
	light_on();
	else
	light_off();
	return 1;
}
/* ioctl函数 */
int light_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct light_dev *dev = filp->private_data;
	switch (cmd) {
		case LIGHT_ON:
		dev->value = 1;
		light_on();
		break;
		case LIGHT_OFF:
		dev->value = 0;
		light_off();
		break;
		default:
		/* 不能支持的命令 */63 return -ENOTTY;
	}
	return 0;
}

struct file_operations light_fops = {
	.owner = THIS_MODULE,
	.read = light_read,
	.write = light_write,
	.ioctl = light_ioctl,
	.open = light_open,
	.release = light_release,
};

/* 设置字符设备cdev结构体 */
static void light_setup_cdev(struct light_dev *dev, int index)
{
	int err, devno = MKDEV(light_major, index);
	cdev_init(&dev->cdev, &light_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &light_fops;
	err = cdev_add(&dev->cdev, devno, 1);
	if (err)
	printk(KERN_NOTICE "Error %d adding LED%d", err, index);
}

/* 模块加载函数 */
int light_init(void)
{
	int result;
	dev_t dev = MKDEV(light_major, 0);
	/* 申请字符设备号 */
	if (light_major)
		result = register_chrdev_region(dev, 1, "LED");
	else {
		result = alloc_chrdev_region(&dev, 0, 1, "LED");
		light_major = MAJOR(dev);
	}
	if (result < 0)
		return result;
	/* 分配设备结构体的内存 */
	light_devp = kmalloc(sizeof(struct light_dev), GFP_KERNEL);
	if (!light_devp) {
		result = -ENOMEM;
		goto fail_malloc;
	}
	memset(light_devp, 0, sizeof(struct light_dev));
	light_setup_cdev(light_devp, 0);
	light_gpio_init();
	return 0;
fail_malloc:
	unregister_chrdev_region(dev, light_devp);
	return result;
}

/* 模块卸载函数 */
void light_cleanup(void)
{
	cdev_del(&light_devp->cdev); /* 删除字符设备结构体 */
	kfree(light_devp); /* 释放在light_init中分配的内存 */
	unregister_chrdev_region(MKDEV(light_major, 0), 1); /* 删除字符设备 */
}

module_init(light_init);
module_exit(light_cleanup);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值