字符设备(chrdev)

一、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

十六、源码

字符设备例程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值