一、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盘、硬盘、…
[root@GEC6818 /tmp]#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
179 24 15482880 mmcblk1
179 25 15220736 mmcblk1p1
8 0 30277632 sda
8 1 30273600 sda1
应用程序访问块设备
[root@GEC6818 /tmp]#ls /dev/sda* -l
brw-rw-rw- 1 root root 8, 0 Jan 1 01:46 /dev/sda --->U盘
brw-rw-rw- 1 root root 8, 1 Jan 1 01:46 /dev/sda1 --->U盘的一个放数据的分区
1)挂载—块设备是有文件系统的。
[root@GEC6818 /]#mount -t vfat /dev/sda1 /data #挂载
[root@GEC6818 /]#umount-v /dev/sda1 #通过设备名卸载
[root@GEC6818 /]#umount-v /data #通过挂载点卸载
vfat(Linux文件系统) ---->fat32(Windows文件系统)
一般插上入盘会自动挂在到: /mnt/udisk
2)像访问普通文件一样访问块设备的内容。
标准IO函数:fopen()/fread()/fwrite()/fclose()
3、网络设备
网卡类的设备:有线网卡、无线网卡、…,网络设备是没有设备文件的。
应用程序:
socket套接字: IP + 端口号
二、字符设备驱动的设计流程
1.定义并初始化一个字符设备
1、定义一个字符设备—>struct cdev
2、定义并初始化字符设备的文件操作集—>struct file_operations
3、给字符设备申请一个设备号—> 设备号 = 主设备号 << 20 + 次设备号
4、初始化字符设备
5、将字符设备加入内核
2.自动生成设备文件
6、创建class
7、创建device,其中device是属于class的
3.得到物理地址对应的虚拟地址
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。
例:
#include <linux/cdev.h>
struct cdev S5P6818_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. 文件操作集
// 位于 include\linux\fs.h
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
常用的成员:
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 S5P6818_led_open(struct inode *inode, struct file *filp)
{
return 0;
}
ssize_t S5P6818_led_read(struct file *filp, char __user *user_buf, size_t size, loff_t *off)
{
}
ssize_t S5P6818_led_write(struct file *filp, const char __user *user_buf, size_t size, loff_t *off)
{
}
int S5P6818_led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static const struct file_operations S5P6818_led_fops = {
.owner = THIS_MODULE,
.open = S5P6818_led_open,
.read = S5P6818_led_read,
.write = S5P6818_led_write,
.release = S5P6818_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. 主设备号和次设备号的作用
例:
[root@GEC6818 /dev]#ls -l
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)静态注册—>指定设备号,注册到内核中。如果内核已经使用该设备号,注册就不成功。
// the source file in: \fs\char_dev.c
/**
* 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,失败返回复数错误码。
例:
[root@GEC6818 /dev]#ls -l
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)动态分配—>内核自动分配空闲的设备号
// the source file in: \fs\char_dev.c
/**
* 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)设备号的注销
// the source file in: \fs\char_dev.c
/**
* 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 —>次设备的数量
六、初始化字符设备
// the source file in: \fs\char_dev.c
/**
* 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)
七、将字符设备加入内核
1. 加入内核
/**
* 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.
2. 从内核移除
/**
* 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/***/
1. 创建device
/**
* 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)申请物理地址区作为一个资源
(2)将物理内存区做内存的动态映射,得到虚拟地址。
注意:
资源—有限的,一旦一个物理内存区已经申请了,后面就不能再次申请。
**问题:**物理内存区占用:两个驱动程序同时申请同个物理内存区资源;
解决:(1)尝试申请但错误不处理,继续往下执行;
(2)直接都不申请,也可以使用;
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内存动态映射,得到虚拟地址
#include <linux/io.h>
1. IO内存动态映射
将一段物理地址内存区映射成一段虚拟地址内存区
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 */
十四、用户空间和内核空间交互数据
#include <linux/uaccess.h>
1.从用户空间获取数据
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的函数
root@wsl-VirtualBox:/home/VMShareDir/CodeLinux/Linux_kernel/005chrdev# arm-linux-gcc -o test test.c
root@wsl-VirtualBox:/home/VMShareDir/CodeLinux/Linux_kernel/005chrdev# 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)安装module
[root@GEC6818 /tmp]#insmod s5p6818_led_drv.ko
[ 206.309000] led_dev_id = 254803968, major = 243, minor = 0
[ 206.310000] gpioc_base_va = f001c000
[ 206.313000] S5P6818 led driver init successfully
[root@GEC6818 /tmp]#lsmod
s5p6818_led_drv 3131 0 - Live 0xbf021000 (O)
2)查看主设备号和设备名称
root@GEC6818 /test]#cat /proc/devices
Character devices:
243 leds_device
3)查看设备文件
[root@GEC6818 /tmp]#ls /dev/led_drv -l
crw-rw---- 1 root root 243, 0 Jan 1 00:41 /dev/led_drv
4)查看申请的内存
[root@GEC6818 /test]#cat /proc/iomem
c001c000-c001cfff : GPIOC_MEM
5)查看class和device
[root@GEC6818 /tmp]#ls /sys/class/s5p6818_leds/ -l
total 0
lrwxrwxrwx 1 root root 0 Jan 1 00:45 led_drv -> ../../devices/virtual/s5p6818_leds/led_drv
6)卸载module
[root@GEC6818 /tmp]#rmmod s5p6818_led_drv.ko
[ 559.025000] S5P6818 led driver exit. major = 243, minor = 0