目录
一、linux设备驱动的分类
1、字符设备—c
应用程序和驱动程序之间进行数据读写的时候,数据是以“字节”为单位。数据交互的时候,是按照固定的顺序传输的;数据是实时传输的,是没有缓存的。字符设备是没有文件系统的。
绝大部分设备驱动是字符设备:LED、BEEP、按键、键盘、触摸屏、摄像头、液晶屏、声卡、IIC、SPI、…
应用程序:系统IO函数
open(“/dev/led_drv”, O_RDWR)
read()
write()
ioctl()
mmap()
close()
2、块设备—b
应用程序和驱动程序之间进行数据读写的时候,数据是以“块”为单位,1block=1024KB。块设备是有缓存的,块设备是有文件系统的。
大容量的存储设备一般都是块设备:nand flash、eMMC、SD、U盘、硬盘、…
#cat /proc/partitions
major minor #blocks name
179 0 7634944 mmcblk0
179 1 65536 mmcblk0p1
179 2 772096 mmcblk0p2
179 3 438272 mmcblk0p3
179 4 1 mmcblk0p4
179 5 8192 mmcblk0p5
179 6 22528 mmcblk0p6
179 7 6324224 mmcblk0p7
179 16 4096 mmcblk0boot1
179 8 4096 mmcblk0boot0
8 0 1956864 sda
8 1 1802240 sda1
应用程序访问块设备
[root@GEC6818 /]#ls /dev/sda* -l
brw-rw-rw- 1 root root 8, 0 Jan 1 00:11 /dev/sda --->U盘
brw-rw-rw- 1 root root 8, 1 Jan 1 00:11 /dev/sda1 --->U盘的一个放数据的分区
1)挂载—块设备是有文件系统的。
[root@GEC6818 /]#mount -t vfat /dev/sda1 /data
vfat —->fat32
2)像访问普通文件一样访问块设备的内容。
标准IO函数:fopen()/fread()/fwrite()/fclose()
3、网络设备
网卡类的设备:有线网卡、无线网卡、…,网络设备是没有设备文件的。
应用程序:
socket套接字: IP + 端口号
二、字符设备驱动的设计流程
——定义并初始化一个字符设备———
1、定义一个字符设备—>struct cdev
2、定义并初始化字符设备的文件操作集—>struct file_operations
3、给字符设备申请一个设备号—>设备号=主设备号<<20 + 次设备号
4、初始化字符设备
5、将字符设备加入内核
——-自动生成设备文件———
6、创建class
7、创建device,其中device是属于class的
——-得到物理地址对应的虚拟地址——-
8、申请物理内存区,申请SFR的地址区。SFR — Special Function Register: GPIOEOUT
9、内存的动态映射,得到物理地址对应的虚拟地址
10、访问虚拟地址
三、定义一个字符设备
1、描述字符设备的结构体–cdev
#include <linux/cdev.h>
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
在linux内核中,使用cdev来描述一个字符设备,每个字符设备都有一个自己的cdev。设计字符设备首先定义一个cdev。
例:
struct cdev gec6818_led_cdev;
2、cdev的成员
struct kobject kobj; —>内核管理驱动的时候,使用的一个object
struct module *owner; —>cdev是属于哪个module,一般写成THIS_MODULE
const struct file_operations *ops; —>cdev的文件操作集
struct list_head list; —>内核管理cdev的链表
dev_t dev; —>设备号
unsigned int count; —>次设备的数量
四、定义并初始化一个文件操作集
1、文件操作集
struct file_operations {
struct module *owner;
...............................
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
..............................
}
2、文件操作集的作用
每个cdev都有一个文件操作集,文件操作集是驱动程序给应用程序提供的接口。应用程序open()会找到驱动程序的open(),驱动程序的open()可以用来访问硬件。
3、例:
int gec6818_led_open(struct inode *inode, struct file *filp)
{
return 0;
}
ssize_t gec6818_led_read(struct file *filp, char __user *user_buf, size_t size, loff_t *off)
{
}
ssize_t gec6818_led_write(struct file *filp, const char __user *user_buf, size_t size, loff_t *off)
{
}
int gec6818_led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static const struct file_operations gec6818_led_fops = {
.owner = THIS_MODULE,
.open = gec6818_led_open,
.read = gec6818_led_read,
.write = gec6818_led_write,
.release = gec6818_led_release,
};
五、给字符设备申请一个设备号—dev_t dev
1、什么是设备号
每个设备文件(字符设备 or 块设备)都已一个设备号,相当于设备文件ID。
设备号有主设备号和次设备号组成的。
设备号是一个32bits的无符号整型值。
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
2、设备号运算的函数:
1)由主设备号和次设备号生成设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //MINORBITS=20
2)由设备号得到主设备号和次设备号
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
3、主设备号和次设备号的作用
例:
crw-rw---- 1 root root 204, 64 Jan 1 1970 ttySAC0 串口0
crw-rw---- 1 root root 204, 65 Jan 1 1970 ttySAC1
crw-rw---- 1 root root 204, 66 Jan 1 1970 ttySAC2
crw-rw---- 1 root root 204, 67 Jan 1 1970 ttySAC3 串口3
主设备号描述一个硬件设备的类型:如uart、IIC、摄像头、…
次设备号描述这种硬件类型下的具体某个硬件
4、如何申请设备号
1)静态注册—>指定设备号,注册到内核中。如果内核已经使用该设备号,注册就不成功。
/** * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * * Return value is zero on success, a negative error code on failure. */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数说明:
dev_t from —>注册的设备号;如果一次注册多个设备号,from就是注册设备号的开始值
unsigned count —>次设备的数量
const char *name —->设备名称,但不是设备文件的名字。#cat /proc/devices
返回值:
成功返回0,失败返回复数错误码。
例:
crw-rw---- 1 root root 204, 64 Jan 1 1970 ttySAC0 串口0
crw-rw---- 1 root root 204, 65 Jan 1 1970 ttySAC1
crw-rw---- 1 root root 204, 66 Jan 1 1970 ttySAC2
crw-rw---- 1 root root 204, 67 Jan 1 1970 ttySAC3 串口3
register_chrdev_region(MKDEV(204,64), 4, "ttySAC") //ttySAC --->设备名称
// /dev/ttySAC0 --->设备文件
2)动态分配—>内核自动分配空闲的设备号
/** * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
参数说明:
dev_t *dev —>分配后的设备号
unsigned baseminor —>次设备号的开始值
unsigned count —>次设备的数量
const char *name —->设备名称,但不是设备文件的名字。#cat /proc/devices
返回值:
成功返回0,失败返回复数错误码。
3)设备号的注销
/** * unregister_chrdev_region() - return a range of device numbers * @from: the first in the range of numbers to unregister * @count: the number of device numbers to unregister * * This function will unregister a range of @count device numbers, * starting with @from. The caller should normally be the one who * allocated those numbers in the first place... */
void unregister_chrdev_region(dev_t from, unsigned count)
参数说明:
dev_t from —>注册的设备号;如果一次注册多个设备号,from就是注册设备号的开始值
unsigned count —>次设备的数量
六、初始化字符设备
/** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev, remembering @fops, making it ready to add to the * system with cdev_add(). */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
思考:
static struct cdev gec6818_led_cdev; //有内存
cdev_init(&gec6818_led_cdev, const struct file_operations *fops);
或:
static struct cdev *gec6818_led_cdev; //没有内存
cdev_init(gec6818_led_cdev, const struct file_operations *fops);//segment fault
ok:
static struct cdev *gec6818_led_cdev;
gec6818_led_cdev = (struct cdev *)kmalloc(sizeof(struct cdev), GFP_KERNEL)
if(gec6818_led_cdev == NULL){
}
cdev_init(gec6818_led_cdev, const struct file_operations *fops);//segment fault
七、将字符设备加入内核
/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this device * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:
struct cdev *p —>定义初始化好的字符设备
dev_t dev —>设备号
unsigned count —>次设备的数量
返回值:
A negative error code is returned on failure.
/** * cdev_del() - remove a cdev from the system * @p: the cdev structure to be removed * * cdev_del() removes @p from the system, possibly freeing the structure * itself. */
void cdev_del(struct cdev *p)
八、创建class
创建class和device的目的是在安装的驱动的时候,可以自动生成设备文件,在卸载驱动的时候,可以自动的删除设备文件。
如果不自动生成设备文件:也可以手动创建:
#mkmod c /dev/led_drv 主设备号 次设备号
创建的class生成在:/sys/class/
1.创建class
#include <linux/device.h>
struct class *class_create(struct module *owner, const char *name)
参数说明:
struct module *owner —>创建的class属于哪个module,一般为THIS_MODULE。
const char *name —>自定义的class的名字
返回值:
得到的class
2.class的删除
void class_destroy(struct class *cls);
九、创建device
device是输于class的,当驱动程序有了class和device以后,内核使用mdev这个工具,根据class和device创建该驱动的设备文件。
创建的device怎么查看:/sys/class/***/
/** * device_create - creates a device and registers it with sysfs * @class: pointer to the struct class that this device should be registered to * @parent: pointer to the parent struct device of this new device, if any * @devt: the dev_t for the char device to be added * @drvdata: the data to be added to the device for callbacks * @fmt: string for the device's name * This function can be used by char device classes. A struct device * will be created in sysfs, registered to the specified class. * A "dev" file will be created, showing the dev_t for the device, if * the dev_t is not 0,0. * If a pointer to a parent struct device is passed in, the newly created * struct device will be a child of that device in sysfs. * The pointer to the struct device will be returned from the call. * Any further sysfs files that might be required can be created using this * pointer. * * Returns &struct device pointer on success, or ERR_PTR() on error. * * Note: the struct class passed to this function must have previously * been created with a call to class_create(). */
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
参数说明:
struct class *class —>device属于哪个class
struct device *parent —>device的父设备,一般为NULL
dev_t devt —>设备号
void *drvdata —>驱动的data,一般为NULL
const char *fmt —>设备文件的名字
返回值:
struct device * —>创建好的device
2.删除device
/** * device_destroy - removes a device that was created with device_create() * @class: pointer to the struct class that this device was registered with * @devt: the dev_t of the device that was previously registered * * This call unregisters and cleans up a device that was created with a * call to device_create(). */
void device_destroy(struct class *class, dev_t devt)
十、申请物理内存区
回忆:
裸机控制硬件的流程:
分析原理图–>找到控制硬件的GPIO–>找GPIO的寄存器—>分析寄存器—>理解寄存器的控制顺序—>通过寄存器的地址来访问该寄存器
注意:裸机使用的是物理地址,所以直接使用CPU手册查到的地址可以编程。
linux驱动使用的虚拟地址,不能直接使用物理地址。想办法,如果通过CPU手册查到的物理地址找到其对应虚拟地址???
一般分成两个过程:
申请物理地址区作为一个资源—–>将物理内存区做内存的动态映射,得到虚拟地址。
注意:
资源—有限的,一旦一个物理内存区已经申请了,后面就不能再次申请。
1.申请物理内存区作为资源
struct resource * request_mem_region(resource_size_t start,resource_size_t n,const char *name)
参数说明:
resource_size_t start —>物理内存区的开始地址
resource_size_t n —>物理内存区的大小
const char *name —>自定义的物理内存区的名字
返回值:
struct resource * —>物理内存区作为了资源
思考:
LED驱动,申请哪个物理内存区???
D8–>GPIOC17,D9–>GPIOC8,D10–>GPIOC7,D11–>GPIOC12
start address —>0xC001C000
address size —> 结束地址:0xC001CFFF,大小:0x1000
name —>“GPIOC_MEM”
2.释放申请的物理内存区
void release_mem_region(resource_size_t start, resource_size_t n)
十一、io内存动态映射,得到虚拟地址
1.IO内存动态映射
将一段物理地址内存区映射成一段虚拟地址内存区
#include <linux/io.h>
void __iomem *ioremap(phys_addr_t offset, unsigned long size)
参数说明:
phys_addr_t offset —>要映射的物理内存区开始地址
unsigned long size —>物理内存区的大小
返回值:
void __iomem * —>映射后,虚拟地址内存区的首地址
2、解除IO内存动态映射
void iounmap(void __iomem *addr)
十二、使用虚拟地址
1、得到虚拟地址
gpioc_base_va = ioremap(phys_addr_t offset, unsigned long size)
if(gpioc_base_va == NULL){
printk("ioremap error\n");
release_mem_region(0xC001C000, 0x1000);
device_destroy(leds_class, led_num);
class_destroy(leds_class);
cdev_del(&gec6818_led_cdev);
unregister_chrdev_region(led_num, 1);
return -EBUSY;
}
//得到每个寄存器的虚拟地址
gpiocout_va = gpioc_base_va + 0x00;
gpiocoutenb_va = gpioc_base_va + 0x04;
gpiocaltfn0_va = gpioc_base_va + 0x20;
gpiocaltfn1_va = gpioc_base_va + 0x24;
gpiocpad_va = gpioc_base_va + 0x18;
2、虚拟地址的类型:void __iomem
3、访问虚拟地址的方法:与访问物理地址的方法一样
//10.访问虚拟地址
//10.1 GPIOC7,8.12,17 --->function1,作为普通的GPIO
*(unsigned int *)gpiocaltfn0_va &=~((3<<14)|(3<<16)|(3<<24));
*(unsigned int *)gpiocaltfn1_va &=~(3<<2);
*(unsigned int *)gpiocaltfn0_va |= ((1<<14)|(1<<16)|(1<<24));
*(unsigned int *)gpiocaltfn1_va |= (1<<2);
//10.2 GPIOC7,8.12,17 --->设置为输出
*(unsigned int *)gpiocoutenb_va |= ((1<<7)|(1<<8)|(1<<12)|(1<<17));
//10.3 GPIOC7,8.12,17 --->设置为输出高电平,D8~D11 off
*(unsigned int *)gpiocout_va |= ((1<<7)|(1<<8)|(1<<12)|(1<<17));
4、虚拟地址的访问方法:使用内核提供的函数
u32 readl(const volatile void __iomem *addr)
void writel(u32 b, volatile void __iomem *addr)
或者:
void __raw_writel(u32 b, volatile void __iomem *addr)
u32 __raw_readl(const volatile void __iomem *addr)
十三、常见的错误码
#include <linux/errno.h>
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
十四、用户空间和内核空间交互数据
1.从用户空间获取数据
#include <linux/uaccess.h>
unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
放在驱动程序的write().
2.将数据拷贝给用户
unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
放在驱动程序的read()
十五、驱动程序的调试
1、应用程序—使用系统IO的函数
arm-linux-gcc -o test test.c
$ file test
test: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 3.2.0, not stripped
2、驱动程序—使用字符设备驱动模型
1)查看主设备号和设备名称
root@GEC6818 /test]#cat /proc/devices
Character devices:
100 led_device
2)查看设备文件
[root@GEC6818 /test]#ls /dev/led_drv -l
crw-rw---- 1 root root 100, 0 Jan 1 00:04 /dev/led_drv
3)查看申请的内存
[root@GEC6818 /test]#cat /proc/iomem
c001c000-c001cfff : GPIOC_MEM
4)查看class和device
[root@GEC6818 /test]#ls /sys/class/gec210_leds/
led_drv
十六、安装多个驱动出错解决
1、出错
[root@GEC6818 /test]#lsmod
led_drv 2881 0 - Live 0xbf025000 (O)
[root@GEC6818 /test]#insmod beep_drv.ko
[ 1643.707000] request memory error
insmod: can't insert 'beep_drv.ko': Device or resource busy
2、原因分析
当LED驱动安装成功后,再安装beep的驱动会出现问题:
[ 1643.707000] request memory error
insmod: can't insert 'beep_drv.ko': Device or resource busy
1)beep_drv.c
//8申请物理内存区
beeps_res = request_mem_region(0xC001C000, 0x1000,"GPIOC_MEM");
if(beeps_res == NULL){
printk("request memory error\n");
device_destroy(beeps_class, beep_num);
class_destroy(beeps_class);
cdev_del(&gec6818_beep_cdev);
unregister_chrdev_region(beep_num, 1);
return -EBUSY;
}
2)led_drv.c
//8申请物理内存区
leds_res = request_mem_region(0xC001C000, 0x1000,"GPIOC_MEM");
if(leds_res == NULL){
printk("request memory error\n");
ret = -EBUSY;
goto request_mem_error;
}
两个驱动申请的物理内存区是一个:0xC001C000, 0x1000。物理内存区作为一个resource,只能申请一次。
3、解决
不申请物理内存区资源: