简介
linux驱动有三大类:字符设备驱动、块设备驱动、网络设备驱动
本文关注的是字符设备驱动-----以LED驱动为例
程序设计
头文件
下面是一些必要的头文件:
宏定义
IMMAP内部存储器映射寄存器
这一部分是IO空间对应的唯一的物理空间地址:
用户程序在运行中不能直接访问物理地址,这个地址是唯一的,我们需要将物理地址映射到0~4G大小的虚拟地址空间供用户程序使用;而驱动程序可以分为用户模式驱动和内核模式驱动:
- 地址空间划分
linux内核将虚拟地址空间划分为两部分供用户使用- 用户空间:0x00000000~0xBFFFFFFF的3G大小的低地址空间;
- 内核空间:0xC0000000~0xFFFFFFFF的1G大小的高地址空间。
设备驱动模式
用户驱动模式:
实现函数:1immr_map (&immr, IMMAP_LEN, IMMAP_BASE); //将物理地址空间映射到低3G地址的用户空间,将映射后的地址保存在immr内部存储器映射寄存器中上述函数实现物理地址映射是通过系统调用mmap函数实现的,过程如下:
12fd = open (MEM_FILE, O_RDWR)) //打开内存文件 /dev/mem*start = (VUINT32) mmap (NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FILE, fd, base)) //返回映射到用户进程虚拟地址空间的基地址注:mmap负责把文件内容映射到进程的虚拟地址空间,通过对这段内存的读取和修改来实现对文件的读取和修改,而不需要再调用read和write;这里的操作是把系统内存看作一个文件,而GPIO相关的寄存器是这个文件中的一部分内容,通过映射,GPIO的实际物理地址映射为用户空间的虚拟地址,这样返回一个虚拟空间GPIO的起始地址,便可以根据地址偏移量计算每个寄存器的虚拟地址,进而达到在用户空间程序直接访问的目的。
内核驱动模式:
实现函数:12request_mem_region(io_dev.phy_base, io_dev.phy_len, IO_DRIVER_NAME); //为该驱动向内核申请指定物理地址的使用权,一旦获得使用权其他驱动便不可以使用这段内存;(unsigned long) ioremap(io_dev.phy_base, io_dev.phy_len); //得到该段物理地址空间的权限之后还需要将该段物理地址映射到内核地址空间,供内核调用;这里内核驱动模式与用户驱动模式的区别在于用户模式下驱动的控制是直接读写IO的映射之后的虚拟地址实现的,而本部分的内核调用是先使用系统调用函数诸如read,write等等进入内核空间,再由内核使用内核调用函数(由用户编写的read,write,ioctl等函数,下面会提到)来实现对IO的控制;本次对于GPIO驱动的控制是使用的内核驱动模式。
主、次设备号
|
在Linux内核看来,主设备号标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备(也就是/dev下的设备文件)服务;而次设备号则用来标识具体且唯一的某个设备。
驱动实现 io_init & io_exit
- 实现物理地址到内核空间的映射并计算各个寄存器的地址偏移量;
获取dev_t类型的设备编号并以此向内核注册该设备:
12dev = MKDEV(io_dev.major,io_dev.minor);register_chrdev_region(dev, 1, IO_DRIVER_NAME);编写各个内核调用的函数诸如:io_open, io_close, io_read, io_write,io_ioctl,同时初始化file_operations结构体,实例如下:
1234567891011121314151617181920212223242526272829303132/*这里以io_ioctl为例*/ssize_t io_ioctl(struct inode * inode, struct file *filp, unsigned int cmd, unsigned long arg){io_dev * dev; //自定义设备结构体dev = filp->private_data;switch(cmd){case LED_CTRL_INIT:*(dev->pb.pbpar) &= ~(arg);*(dev->pb.pbdir) |= arg;break;case LED_CTRL_ON:*(dev->pb.pbdat) &= ~(arg);break;case LED_CTRL_OFF:*(dev->pb.pbdat) |= arg;break;default:return -1;break;}return 0;}struct file_operations io_fops = {.owner = THIS_MODULE,.read = io_read,.write = io_write,.open = io_open,.ioctl = io_ioctl,.release = io_release,};初始化cdev结构体,cdev结构体是设备驱动实现的关键,cdev是linux用来管理字符设备的结构体,其在内核中采用数组结构设计,这样系统中有多少个主设备号就约定了数组大小,此设备号采用链表管理,同一主设备号下可以有多个子设备。设备即文件,上层应用要访问设备,必须通过文件,cdev中包含file_operations结构体,该结构体就是驱动的文件操作集合。其分为一下几个步骤:
12345cdev_init(&dev->cdev, &io_fops);dev->cdev.owner = THIS_MODULE;dev->cdev.ops = &io_fops;cdev_add (&dev->cdev, devno, 1);创建设备类,使得当调用insmod命令加载驱动时可以自动在/dev目录下创建该设备节点文件
12io_dev.cs = class_create(THIS_MODULE,IO_DRIVER_NAME);io_dev.cd = class_device_create(io_dev.cs, NULL, dev, NULL, IO_DRIVER_NAME);卸载设备-io_exit
前面部分实现的是设备加载时调用执行的程序即io_init(),而设备退出时也需要响应的函数即io_exit:1234cdev_del(&io_dev.cdev);unregister_chrdev_region(MKDEV (io_dev.major, io_dev.minor), 1);iounmap((void __iomem *)io_dev.phy_immr);release_mem_region(io_dev.phy_base,io_dev.phy_len);
至此设备驱动基本编写完毕,但仍有一些后续工作:
编译驱动
这里需要将源文件编译为.ko的驱动模块,使用insmod加载到内核,相应的rmmod命令删除驱动模块。
下面是一个标准的Makefile:
|
|