同类的东西,学习起来找共性就好了。学习Linux设备驱动,需要寻找各种驱动程序与系统的共性,探索其中的规律;
设备驱动的作用
驱动程序的作用主要是实现硬件对软件工程师的隐形。
详细作用解释:
无操作系统时的设备驱动
对于功能比较单一、控制并不复杂的系统,如公交车刷卡机、电冰箱、微波、简单的手机和小灵通等,并不需要多任务调度、文件系统、内存管理等复杂功能,单任务架构完全可以很好地支持它们的工作。一个无限循环中夹杂对设备中断的检测或者对设备的轮询是这种系统中软件的典型架构。
单任务软件典型架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | int main(int argc,char* avgv[]) {   while(1)     {       if(serialInt==1)               {         ProcessSerialInt();           serialInt=0;                }       if(keyInt==1)               {         ProcessKeyInt();            keyInt=0;                 }       status=CheckXXX();       switch(status)       {         . . .       }         . . .     }   }
   | 
 
这样的系统中,虽然不存在操作系统,但是设备驱动是必须存在的。
- 一般情况下,对每一种设备驱动都会定义为一个软件模块,包含.h文件和.c文件。
.h文件定义该设备驱动的数据结构并声明外部函数,.c文件进行设备驱动的具体实现。 
 
举例:
一个串口驱动serial.c serial.h,主要是配置GPIO,串口控制寄存器,以及串口的收发(读写)寄存器,而这几个配置都是自定义函数实现的,比如串口的写(发)SerialSend 函数等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
   | 
 
  extern void SerialInit(void); extern void SerialSend(const char buf*,int count); extern void SerialRecv(char buf*,int count);
 
 
 
 
  void SerialInit(void) { ... }
  void SerialSend(const char buf*,int count) { ... }
  void SerialRecv(char buf*,int count) { ... }
  void SerialIsr(void) { ... serialInt = 1; }
 
  | 
 
其他模块需要使用这个设备的时候,只需要包含设备驱动的头文件 serial.h,然后调用其中的外部接口函数即可。如我们要从串口上发送字符串“Hello World”,使用函数SerialSend( “ Hello World “,11)即可。
这样子,在没有操作系统的情况下,设备驱动的接口被直接提交给了应用软件工程师, 应用软件没有跨越任何层次就直接访问了设备驱动的接口。 设备驱动包含的接口函数也与硬件的功能直接吻合, 没有任何附加功能。
有的工程师把单任务系统设计成设备驱动和具体的应用软件模块处于同一层次(即应用程序也在比如serial.c中实现),这显然是不合理的,不符合软件设计中高内聚低耦合的要求。
另一种不合理的设计是直接在应用中操作硬件的寄存器(单独一个main.c,所有功能都在一个函数中实现,不采用其他任何接口/函数),而不单独设计驱动模块,这种设计意味着系统中不存在或未能充分利用可被重用的驱动代码。
有操作系统时的设备驱动
当系统中包含操作系统后,设备驱动会变得怎样?
- 首先,无操作系统时设备驱动的硬件操作工作仍然是必不可少的, 没有这一部分,设备驱动不可能与硬件打交道。
 
- 其次,我们还需要将设备驱动融入内核。为了实现这种融合,必须
在所有的设备驱动中设计面向操作系统内核的接口。 
- 这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。
 
由此可见,当系统中存在操作系统的时候,设备驱动变成了连接硬件和内核的桥梁。
操作系统的存在势必要求设备驱动附加更多的代码和功能(主要是提供了很多结构),把单一的“驱使硬件设备行动”变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API,不再给应用软件工程师直接提供接口。
有了操作系统之后,设备驱动反而变得复杂,那要操作系统干什么?
- 首先,一个复杂的软件系统需要处理多个并发的任务,没有操作系统,想完成多任务并发是很困难的。
 
- 其次,操作系统给我们提供内存管理机制。一个典型的例子是,对于多数含 MMU的处理器而言,Windows、Linux 等操作系统可以让每个进程都独立地访问 4GB的内存空间。
 
上述优点似乎并没有体现在设备驱动身上,操作系统的存在给设备驱动究竟带来了什么好处呢?
- 简而言之,操作系统通过给设备驱动制造麻烦来达到给上层应用提供便利的目的。
 
- 如果设备驱动都按照操作系统给出的独立于设备的接口而设计,应用程序将可使用统一的系统调用接口来访问各种设备。
 
- 对于类UNIX的VxWorks、Linux等操作系统而言,应用程序通过write()、read()等函数读写文件就可以访问各种字符设备和块设备,而不用管设备的具体类型和工作方式,是非常方便的。
 
不管有无操作系统,不管是SerialSend,或者write,访问设备都需要对寄存器进行读写操作,比如串口,在dev目录下有个ttys0结点,我们可以通过ioctl函数对其进行读写操作,当然,write、read更为直接咯。而上层的应用可以对这些函数进行封装,定义不同的接口,从而实现更多的功能
Linux设备的分类及特点
驱动针对的对象是存储器和外设(包括CPU 内部集成的存储器和外设),而不是针对CPU 内核。
多说一点,以前的通过总线挂载到cpu的一些设备,正在被逐渐集成到CPU上。
随着IC 制作工艺的发展,目前,芯片的集成度越来越高,往往在CPU 内部就集成了存储器和外设适配器。譬如,相当多的ARM、PowerPC、MIPS 等处理器都集成了UART、I2C 控制器、SPI 控制器、USB 控制器、SDRAM 控制器等,有的处理器还集成了GPU(图形处理器)、视频编解码器等。
Linux 将存储器和外设分为3 个基础大类。
字符设备
- 字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标
等。 
 
块设备
- 块设备可以按任意顺序进行访问,以块为单位进行操作,如硬盘、eMMC 等。
 
 
网络设备
- 网络设备面向数据包的接收和发送而设计,它并不倾向于对应于文件系统的节点。内核与网络设备的通信与内核和字符设备、网络设备的通信方式完全不同,前者主要还是使用套接字接口。
 
 
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的系统调用接口编程,但也可使用C库函数,出于代码可移植性的目的,后者更值得推荐。
C库函数本身也通过系统调用接口而实现,如C库函数fopen()、fwrite()、fread()、fclose()分别会调用操作系统的API open()、write()、read()、close()。
Linux 设备驱动学习的重点、难点
- 好的硬件基础:懂得 SRAM、Flash、SDRAM、磁盘的读写方式,UART、I2C、USB 等设备的接口以及轮询、中断、DMA 的原理,PCI 总线的工作方式以及CPU 的内存管理单元(MMU)等。
 
- 好的 C 语言基础: 能灵活地运用 C 语言的结构体、指针、函数指针及内存动态申请和释放等。
 
- 一定的 Linux 内核基础:至少要明白
驱动与内核的接口。尤其是对于块设备、网络设备、Flash 设备、串口设备等复杂设备,内核定义的驱动体系结构本身就非常复杂。 
- 非常好的多任务并发控制和同步的基础:在驱动中会大量使用自旋锁、互斥、信号量、等待队列等并发与同步机制。
 
资源与工具
- 类似http://lxr.free-electrons.com/、http://lxr.oss.org.cn/ 这样的网站提供了Linux 内核源代码的交叉索引,在其中输入Linux 内核中的函数、数据结构或变量的名称就可以直接得到以超链接形式给出的定义和引用它的所有位置。
 
- 一些网站也提供了Linux 内核中函数、变量和数据结构的搜索功能,在google 中搜索“linux identifier search ”可得。
 
- Linux 主机上阅读和编辑Linux 源码的常用方式是vim + cscope 或者vim + ctags,vim是一个文本编辑器,而cscope 和ctags 则可建立代码索引
 
LED驱动示例(选看)
无操作系统时的LED 驱动
在嵌入式系统的设计中,LED 一般直接由CPU 的GPIO(通用可编程I/O)口控制。
GPIO 一般由两组寄存器控制,即一组控制寄存器和一组数据寄存器。
- 控制寄存器可设置GPIO 口的工作方式为输入或者输出。
 
- 当引脚被设置为输出时,向数据寄存器的对应位写入1和0 会分别在引脚上产生高电平和低电平;
 
- 当引脚设置为输入时,读取数据寄存器的对应位可获得引脚上的电平为高或低。
 
假设:
- 在GPIO_REG_CTRL 物理地址中控制寄存器处的第n 位写入1 可设置GPIO 口为输出
 
- 在地址GPIO_REG_DATA 物理地址中数据寄存器的第n 位写入1 或0 可在引脚上产生高或低电平
 
则在无操作系统的情况下,设备驱动见代码3。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   |  #def ine reg_gpio_ctrl *(volatile int *)(ToVirtual(GPIO_REG_CTRL)) #def ine reg_gpio_data *(volatile int *)(ToVirtual(GPIO_REG_DATA))
 
  void LightInit(void) { reg_gpio_ctrl |= (1 << n);  }
 
  void LightOn(void) { reg_gpio_data |= (1 << n);  }
 
  void LightOff(void) { reg_gpio_data &= ~ (1 << n);  }
 
  | 
 
上述程序中的LightInit()、LightOn()、LightOff() 都直接作为驱动提供给应用程序的外部接口函数。
Linux 下的LED驱动
在Linux 下,可以使用字符设备驱动的框架来编写对应于代码3的LED 设备驱动(这里仅仅是为了方便讲解,内核中实际实现了一个提供sysfs 节点的GPIO LED 驱动,位于drivers/leds/leds-gpio.c 中),操作硬件的LightInit()、LightOn()、LightOff() 函数仍然需要,
但是,遵循Linux 编程的命名习惯,重新将其命名为light_init()、light_on()、light_off()。这些函数将被LED 设备驱动中独立于设备并针对内核的接口进行调用。
代码4,Linux 操作系统下的LED 驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
   | #include ...
  struct light_dev { 	struct cdev cdev;  	unsigned char vaule;  }; 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; 	 	if (dev->value == 1) 		light_on(); 	else 		light_off(); 	return 1; }
  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: 			 			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, };
  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);  	unregister_chrdev_region(MKDEV(light_major, 0), 1);  } module_init(light_init); module_exit(light_cleanup);
   | 
 
除了代码3 中的硬件操作函数仍然需要外,代码4 中还包含了大量暂时陌生的元素,如结构体file_operations、cdev,Linux 内核模块声明用的MODULE_AUTHOR、MODULE_LICENSE、module_init、module_exit,以及用于字符设备注册、分配和注销的函数register_chrdev_region()、alloc_chrdev_region()、unregister_chrdev_region() 等。我们也不能理解为什么驱动中要包含light_init ()、light_cleanup ()、light_read()、light_write() 等函数。
此时,我们只需要有一个感性认识,那就是,上述暂时陌生的元素都是Linux 内核为字符设备定义的,以实现驱动与内核接口而定义的。Linux 对各类设备的驱动都定义了类似的数据结构和函数。
概括来说:
- 没有操作系统的时候,设备驱动编写的函数可以自己定义,对外提供给应用程序调用。
 
- 有了操作系统之后,设备驱动的内容就变多了,除了之前没有操作系统时候写的一些,还需要根据操作系统内核进行适配。这时候的驱动程序并不能直接和应用程序打交道了,而是和操作系统内核打交道,对外呈现出操作系统的API。
- 设备驱动都按照操作系统给出的独立于设备的接口而设计,应用程序将可使用统一的系统调用接口来访问各种设备。
 
- 也就是说这些接口的形式都是操作系统给出的,操作系统会对一类设备给出一致的接口,不会针对一个特定的设备给接口。
 
 
注解
C 语言结构体,以点号开头:
- C99标准中的结构体初始化方法 ,结构体的乱许赋值,好像是GCC的扩展。
 
- 和linux无关,是一个C的问题,初始化的时候指定成员的名字,即使在结构体增加,插入字段的时候代码也可以正常工作。
 
参考资料
《Linux设备驱动开发详解:基于最新的Linux 4.0内核》