设备驱动概述(2)
成于坚持,败于止步
Linux 设备驱动
设备的分类及特点
计算机系统的硬件主要由 CPU、存储器和外设组成。随着 IC 制造工艺的发展,目前,芯片的集成度越来越高,往往在 CPU 内部就集成了存储器和外设适配器。ARM、PowerPC、MIPS 等处理器都集成了 UART、I2C 控制器、USB 控制器、SDRAM 控制器等,有的处理器还集成了片内 RAM 和 Flash。
驱动针对的对象是存储器和外设(包括 CPU 内部集成的存储器和外设),而不是针对 CPU 核。Linux 将存储器和外设分为 3 个基础大类:
字符设备;
块设备;
网络设备。
字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。块设备可以用任意顺序进行访问,以块为单位进行操作,如硬盘、软驱等。
字符设备不经过系统的快速缓冲,而块设备经过系统的快速缓冲。但是,字符设备和块设备并没有明显的界限,如 Flash 设备符合块设备的特点,但是我们仍然可以把它作为一个字符设备来访问。
字符设备和块设备的驱动设计呈现出很大的差异,但是对于用户而言,他们都使用文件系统的操作接口 open()、close()、read()、write()等函数进行访问。
在 Linux 系统中,网络设备面向数据包的接收和发送而设计,它并不对应于文件系统的节点。内核与网络设备的通信和内核与字符设备、块设备的通信方式完全不同。 另外,TTY 驱动、I2C 驱动、USB 驱动、PCI 驱动、LCD 驱动等本身大体可归纳入 3 个基础大类,但是对于这些复杂的设备,Linux 系统还定义了独特的驱动体系结构。
Linux 设备驱动与整个软硬件系统的关系
如图 1.5 所示,除网络设备外,字符设备与块设备都被映射到 Linux 文件系统的文件和目录,通过文件系统的系统调用接口 open()、write()、read()、close()等函数即可访问字符设备和块设备。所有的字符设备和块设备都被统一地呈现给用户。块设备比字符设备复杂,在它上面会首先建立一个磁盘/Flash文件系统,如 FAT、Ext3、YAFFS、JFFS 等。FAT、Ext3、YAFFS、JFFS 规范了文件和目录在存储介质上的组织。
应用程序可以使用 Linux 的系统调用接口编程,也可以使用 C 库函数,出于代码可移植性的考虑,后者更值得推荐。C 库函数本身也通过系统调用接口而实现,如库函数中的 fopen()、fwrite()、fread()、fclose()分别会调用操作系统 API 的 open()、write()、read()、close()函数。
设备驱动的 Hello World:LED 驱动
无操作系统时的 LED 驱动
在嵌入式系统的设计中,LED 一般直接由 CPU 的 GPIO(通用可编程 I/O 口)控制。GPIO 一般由两组寄存器控制,即一组控制寄存器和一组数据寄存器。控制寄存器可设置 GPIO 口的工作方式为输入或输出。当引脚被设置为输出时,向数据寄存器的对应位写入 1 和 0 会分别在引脚上产生高电平和低电平;当引脚设置为输入时,读取数据寄存器的对应位可获得引脚上相应的电平信号。
在本例子中,我们屏蔽具体 CPU 的差异,假设在 GPIO_REG_CTRL 物理地址处的控制寄存器处的第 n 位写入 1 可设置 GPIO 为输出,在 GPIO_REG_DATA 物理地址处的数据寄存器的第 n 位写入 1 或 0 可在引脚上产生高或低电平,则在无操作系统的情况下,设备驱动代码如下:
- 1 #define reg_gpio_ctrl *(volatile int *)(ToVirtual(GPIO_REG_CTRL))
- 2 #define reg_gpio_data *(volatile int *)(ToVirtual(GPIO_REG_DATA))
- 3 /*初始化 LED*/
- 4 void LightInit(void)
- 5 {
- 6 reg_gpio_ctrl |= (1 << n); /*设置 GPIO 为输出*/
- 7 }
- 8
- 9 /*点亮 LED*/
- 10 void LightOn(void)
- 11 {
- 12 reg_gpio_data |= (1 << n); /*在 GPIO 上输出高电平*/
- 13 }
- 14
- 15 /*熄灭 LED*/
- 16 void LightOff(void)
- 17 {
- 18 reg_gpio_data &= ~(1 << n); /*在 GPIO 上输出低电平*/
- 19 }
Linux 系统下的 LED 驱动
在 Linux 操作系统下编写 LED 设备的驱动时,操作硬件的 LightInit()、LightOn()、LightOff()这些函数仍然需要,但是,需要遵循 Linux 编程的命名习惯,重新将其命名为 light_init()、light_on()、light_off()。这些函数将被 LED 驱动中独立于设备的针对内核的接口进行调用,代码如下给出了 Linux 系统下的 LED 驱动,现在读者并不需要能读懂这些代码。
- 1 #include .../*包含内核中的多个头文件*/
- 2
- 3 /*设备结构体*/
- 4 struct light_dev
- 5 {
- 6 struct cdev cdev; /*字符设备 cdev 结构体*/
- 7 unsigned char value; /*LED 亮时为 1,熄灭时为 0,用户可读写此值*/
- 8 };
- 9
- 10 struct light_dev *light_devp;
- 11 int light_major = LIGHT_MAJOR;
- 12
- 13 MODULE_AUTHOR("Song Baohua");
- 14 MODULE_LICENSE("Dual BSD/GPL");
- 15
- 16 /*打开和关闭函数*/
- 17 int light_open(struct inode *inode, struct file *filp)
- 18 {
- 19 struct light_dev *dev;
- 20 /* 获得设备结构体指针 */
- 21 dev = container_of(inode->i_cdev, struct light_dev, cdev);
- 22 /* 让设备结构体作为设备的私有信息 */
- 23 filp->private_data = dev;
- 24 return 0;
- 25 }
- 26 int light_release(struct inode *inode, struct file *filp)
- 27 {
- 28 return 0;
- 29 }
- 30
- 31 /*写设备:可以不需要 */
- 32 ssize_t light_read(struct file *filp, char _ _user *buf, size_t count,
- 33 loff_t*f_pos)
- 34 {
- 35 struct light_dev *dev = filp->private_data; /*获得设备结构体 */
- 36
- 37 if (copy_to_user(buf, &(dev->value), 1))
- 38 {
- 39 return - EFAULT;
- 40 }
- 41 return 1;
- 42 }
- 43
- 44 ssize_t light_write(struct file *filp, const char _ _user *buf, size_t count,
- 45 loff_t *f_pos)
- 46 {
- 47 struct light_dev *dev = filp->private_data;
- 48
- 49 if (copy_from_user(&(dev->value), buf, 1))
- 50 {
- 51 return - EFAULT;
- 52 }
- 53 /*根据写入的值点亮和熄灭 LED*/
- 54 if (dev->value == 1)
- 55 light_on();
- 56 else
- 57 light_off();
- 58
- 59 return 1;
- 60 }
- 61
- 62 /* ioctl 函数 */
- 63 int light_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
- 64 unsigned long arg)
- 65 {
- 66 struct light_dev *dev = filp->private_data;
- 67
- 68 switch (cmd)
- 69 {
- 70 case LIGHT_ON:
- 71 dev->value = 1;
- 72 light_on();
- 73 break;
- 74 case LIGHT_OFF:
- 75 dev->value = 0;
- 76 light_off();
- 77 break;
- 78 default:
- 79 /* 不能支持的命令 */
- 80 return - ENOTTY;
- 81 }
- 82
- 83 return 0;
- 84 }
- 85
- 86 struct file_operations light_fops =
- 87 {
- 88 .owner = THIS_MODULE,
- 89 .read = light_read,
- 90 .write = light_write,
- 91 .ioctl = light_ioctl,
- 92 .open = light_open,
- 93 .release = light_release,
- 94 };
- 95
- 96 /*设置字符设备 cdev 结构体*/
- 97 static void light_setup_cdev(struct light_dev *dev, int index)
- 98 {
- 99 int err, devno = MKDEV(light_major, index);
- 100
- 101 cdev_init(&dev->cdev, &light_fops);
- 102 dev->cdev.owner = THIS_MODULE;
- 103 dev->cdev.ops = &light_fops;
- 104 err = cdev_add(&dev->cdev, devno, 1);
- 105 if (err)
- 106 printk(KERN_NOTICE "Error %d adding LED%d", err, index);
- 107 }
- 108
- 109 /*模块加载函数*/
- 110 int light_init(void)
- 111 {
- 112 int result;
- 113 dev_t dev = MKDEV(light_major, 0);
- 114
- 115 /* 申请字符设备号*/
- 116 if (light_major)
- 117 result = register_chrdev_region(dev, 1, "LED");
- 118 else
- 119 {
- 120 result = alloc_chrdev_region(&dev, 0, 1, "LED");
- 121 light_major = MAJOR(dev);
- 122 }
- 123 if (result < 0)
- 124 return result;
- 125
- 126 /* 分配设备结构体的内存 */
- 127 light_devp = kmalloc(sizeof(struct light_dev), GFP_KERNEL);
- 128 if (!light_devp) /*分配失败*/
- 129 {
- 130 result = - ENOMEM;
- 131 goto fail_malloc;
- 132 }
- 133 memset(light_devp, 0, sizeof(struct light_dev));
- 134 light_setup_cdev(light_devp, 0);
- 135 light_init();
- 136 return 0;
- 137
- 138 fail_malloc: unregister_chrdev_region(dev, light_devp);
- 139 return result;
- 140 }
- 141
- 142 /*模块卸载函数*/
- 143 void light_cleanup(void)
- 144 {
- 145 cdev_del(&light_devp->cdev); /*删除字符设备结构体*/
- 146 kfree(light_devp); /*释放在 light_init 中分配的内存*/
- 147 unregister_chrdev_region(MKDEV(light_major, 0), 1); /*删除字符设备*/
- 148 }
- 149
- 150 module_init(light_init);
- 151 module_exit(light_cleanup);
此时,我们只需要有一个感性认识,那就是,上述元素都是 Linux 驱动与内核的接口。Linux 对各类设备的驱动都定义了类似的数据结构和函数。