(三)字符设备

目录

一、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、解决
不申请物理内存区资源:

附录

代码示例:led_drv.c

    原文作者:Kownzird
    原文地址: https://blog.csdn.net/qq_38878046/article/details/109076595
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞