linux驱动学习(五)之字符设备

一、 linux设备驱动分类

1、字符设备---char

应用程序与驱动程序在进行数据传输时,数据以"字节"为单位。

特点:

[1] 按照顺序进行数据传输
[2] 数据传输是实时的,并没有缓存
[3] 字符设备是没有文件系统的

1)驱动程序的作用:是应用程序访问底层硬件设备的一座桥梁

2)应用程序如何去访问驱动程序:系统调用

   应用程序:系统IO(open、read、write、ioctl、close)

  驱动程序: 文件操作集---> struct file_operations 

3)驱动程序如何去访问底层硬件 ---> MMU

   驱动程序中的地址为虚拟地址 ----> MMU(io内存映射) ---->底层硬件的物理地址
   虚拟地址----->是通过在驱动程序中,来申请的虚拟地址

4)什么设备叫作字符设备呢?

    大多数的设备都是字符设备,比如:led, lcd, beep, 按键, SPI等

2、块设备---block

    应用程序与驱动程序在进行数据传输时,数据以"块"为单位,1block = 1024kb。

特点:

[1]有缓存

[2]有文件系统,比如:NTFS ,ext4

大容量设备都是块设备:U盘,SD卡,EMMC--电子硬盘

[root@GEC6818 /mnt]#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


//u盘
[root@GEC6818 /dev]#ls -lh sda*
brw-rw-rw-    1 root     root        8,   0 Jan  1 00:30 sda ---> 占1G
brw-rw-rw-    1 root     root        8,   1 Jan  1 00:30 sda1 ----> 31G

应用程序如何访问块设备:

第一步:挂载U盘到根文件系统上

[root@GEC6818 /]#mount -t vfat /dev/sda1 /udata

第二步:像访问普通文件一样来访问块设备的内容
   标准的IO函数

3、网络设备----socket

网卡类的设备:有线网卡、无线网卡 ...,另外,网络设备是没有设备文件的
     
应用程序:
    socket套接字:根据ip地址 + 端口号

二、字符设备驱动设计流程

以led灯为例

1、定义并初始一个字符设备(struct cdev),在驱动程序中对设备的描述和控制:

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

比如:struct cdev led_cdev;

2、根据应用程序的API来定义并且初始化字符设备的文件操作集(struct file_operations).

3、给字符设备申请一个设备号 --> 主设备号<<20 + 次设备号.

4、初始化字符设备.

5、把当前的字符设备加入到内核.

6、创建class.

7、创建设备(device)。其中,device属于class,应用程序可以通过设备文件来访问驱动程序.

8、申请物理内存区,申请SFR(Special Function Register,特殊功能寄存器)地址区,比如:GPIOEOUT,GPIOEOUTENB

9、内存的动态映射,得到物理地址的虚拟地址.

10、在驱动程序中,访问虚拟地址.

具体实现如下:

1)定义一个字符设备(struct 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;
};

关于struct cdev结构体的说明:
1)struct kobject kobj; ---->内核管理驱动时,使用的一个object对象,内核自己使用,跟驱动程序设计者无关
2)struct module *owner; ----> 模块的拥有者,当前驱动设计模块是属于谁的,固定写法:THIS_MODULE
3)const struct file_operations *ops; ---> cdev的文件操作集一般由用户或者设计者来实现
  static struct file_operations gec6818_led_ops;
4)struct list_head list; --->管理字符设备的链表,由内核本身去负责
5)dev_t dev; ---->设备号
6)unsigned int count; ---->字符设备的次设备号的数目

在linux内核中,使用cdev来描述字符设备,每个字符设备都有一个自己的cdev,比如:

static struct cdev gec6818_led;

2) 定义一个字符设备的文件操作集(struct file_operations)

#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);
};

根据文件操作集中的函数指针定义相关类型的函数:函数的格式是固定
int gec6818_led_release(struct inode *node, struct file *file)
{
	printk("gec6818_led_release\n");
	return 0;
}
static struct file_operations gec6818_led_ops = {
	.owner = THIS_MODULE,
	.open = gec6818_led_open,
	.write = gec6818_led_write,
	.release = gec6818_led_release,//当应用程序调用close时,执行驱动程序中的接口函数
};

3) 给设备申请一个设备号---dev_t

   1、什么是设备号
     每个设备文件(字符设备or块设备)都有一个设备号,相当于设备文件的ID.
     设备号由主设备和次设备号组成.  

dev_t dev; ---> 字符设备的设备号
/*
typedef __kernel_dev_t		dev_t;
typedef __u32 __kernel_dev_t; ---->无符号整型数字
*/

   2、设备号的运算函数

#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi)) ----> 设备号
dev = MKDEV(ma,mi);  //根据主设备号与次设备号得到设备号

    3、得到主设备号与次设备号

#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS)) ----> 主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK)) ----> 次设备号
unsigned int major = MAJOR(dev); //根据设备号得到主设备号
unsigned int minor = MINOR(dev); //根据设备号得到次设备号

     4、把得到的设备号注册到内核中

    一) 静态注册:由用户先确定一个设备号,通过注册函数去申请

int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数说明:
    dev_t from ----> 要申请的设备号
	unsigned count ---> 次设备号的数目
	const char *name ----> 设备名字 --->字符串
返回值:
	成功:0
	失败: <0
比如:register_chrdev_region(108,4,"ttyGS");

  二) 动态注册:由内核动态分配一个设备号给现有设备

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数说明:
	dev_t *dev --->做为输出参数,通过alloc_chrdev_region得到的设备号给dev
	unsigned baseminor -->第一个次设备号
	unsigned count -->次设备号数目
	const char *name ----> 设备名字 --->字符串
返回值:
	成功:0
	失败: <0

   5、设备号的作用
主设备号:描述一个硬件是属于哪一类的设备,比如:uart,lcd,摄像头

次设备号:描述某种类型的设备中具体的哪一个设备,比如:uart0,uart1

crw-rw----    1 root     root      247,   0 Jan  1  1970 ttyGS0
crw-rw----    1 root     root      247,   1 Jan  1  1970 ttyGS1
crw-rw----    1 root     root      247,   2 Jan  1  1970 ttyGS2
crw-rw----    1 root     root      247,   3 Jan  1  1970 ttyGS3

247 ---->主设备号 ----->表示串口
0   ----->次设备号 -----> 串口1
ttyGS -----> 表示该硬件的设备文件,如果该类型的设备有多个,由从0开始,由系统自动加1

   6、释放设备号给系统

void unregister_chrdev_region(dev_t from, unsigned count)
参数说明:
	dev_t from ----> 要申请的设备号
	unsigned count -->次设备号数目

4) 初始化字符设备

#include <linux/cdev.h>
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数说明:
	struct cdev *cdev  -----> 字符设备的结构
	const struct file_operations *fops ----> 字符设备对应的文件操作集

5) 把设备加入到内核中

#include <linux/cdev.h>
//把字符设备增加到系统
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数说明:
	struct cdev *cdev  -----> 字符设备的结构
	dev_t dev -----> 字符设备的设备号
	unsigned count ----> 次设备号数目
失败:返回一个负错误码


//从系统中,把字符设备删除
void cdev_del(struct cdev *p)

6) 创建class,一个设备会有一个class

[root@GEC6818 /sys/class]#ls ---->查看设备的class
android_usb   graphics      leds          pvrusb2       spi_master
arvo          hwmon         lirc          pyra          spidev
axppower      i2c-adapter   mdio_bus      rc            switch
backlight     i2c-dev       mem           regulator     timed_output


//创建class
#include <linux/device.h>
struct class *class_create(struct module *owner, const char *name)
参数说明:
	struct module *owner ----> 模块的拥有者
	const char *name -----> class的名字 ----> /sys/class--->查看


返回值
   成功:&struct class pointer  ----class的地址
   失败:NULL

//销毁class
void class_destroy(struct class *cls)

7) 创建device

//创建一个device
#include <linux/device.h>
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
参数说明:
	struct class *class ----> 说明device是属于哪 一个class,class_create返回值
	struct device *parent ----> 该设备的父亲,一般设置为NULL
	dev_t devt -----> 增加到系统中的字符设备的设备号
	void *drvdata ----> 增加一个device时,返回的数值,如果不需要返回,则使用NULL
	 const char *fmt ----> 该设备的名字
		 
返回值:
	成功:struct device *
	失败:NULL

//销毁device
void device_destroy(struct class *class, dev_t devt)

8) 申请物理内存区,申请SFR地址区

//申请物理内存区
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 *
	失败:NULL

//释放物理内存区
void release_mem_region(resource_size_t start, resource_size_t n)

9) IO动态映射

//IO动态映射 ---- 根据物理地址得到虚拟的起始地址
static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size)
参数说明:
	phys_addr_t offset ---- >物理地址的起始地址
	unsigned long size -----> IO映射的大小

//释放IO映射
static inline void iounmap(void __iomem *addr)

10) 测试

1 插入和删除驱动
[root@GEC6818 /6818_driver]#insmod led_drv.ko 
[  173.611000] gec6818_led_init
[root@GEC6818 /6818_driver]#rmmod led_drv.ko 
[  220.248000] gec6818led_exit

2 查看/dev下设备文件、主设备号、次设备
[root@GEC6818 /dev]#ls -l gec6818_led_device 
crw-rw----    1 root     root      100,   0 Jan  1 02:46 gec6818_led_device  //提供给应用程序来访问驱动程序

3 查看class
[root@GEC6818 /sys/class]#ls -l gec6818_led_class/
total 0
lrwxrwxrwx    1 root     root             0 Jan  1 02:51 gec6818_led_device

demo下载:

led:点击跳转

beep:点击跳转

觉得有帮助的话,打赏一下呗。。

           

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值