一、Linux设备分类
linux 是文件型系统,所有硬件都会在对应的目录 (/dev) 下面用相应的文件表示。直接读文件,写文件就可以向设备发送、接收数据。
按照读写存储数据方式,设备分为以下几种:字符设备、块设备和网络设备。
字符设备: 指应用程序按字节/字符来读写数据的设备。
块设备: 通常支持随机存取和寻址,并使用缓存器。
网络设备: 是一种特殊设备,它并不存在于/dev 下面,主要用于网络数据的收发。
二、字符设备驱动简介
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。
Linux 应用程序对驱动程序的调用如下图:
Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,
cdev 记录了字符设备的相关信息 (设备号、内核对象),字符设备的打开、读写、关闭等操作接口
(file_operations),在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一
个文件 (设备节点) 绑定对象的 cdev,当我们对这个文件进行读写操作时,就可以通过虚拟文件
系统,在内核中找到这个对象及其操作接口,从而控制设备。
三、字符设备驱动程序框架
驱动程序流程:
1、分配设备号,获取设备的唯一ID
2、实现 file_operation,保存到 cdev
3、实现 cdev 的初始化
4、使用cdev_add() 注册 cdev,告诉内核
5、创建设备节点,方便APP层调用 file_operation 接口
6、注销设备,注销设备需释放内核中的 cdev,归还申请的设备号,删除创建的设备节点。
四、驱动初始化和注销
1、设备号的申请和归还
1.1 定义字符设备
//第一种方式
static struct cdev chrdev;
//第二种方式
struct cdev *cdev_alloc(void);
1.2 移除某个字符设备
void cdev_del(struct cdev *p
1.3 静态字符设备申请
int register_chrdev_region(dev_t from, unsigned count, const char *name)
1.4 动态分配设备编号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
1.5 注销的字符设备的设备编号
void unregister_chrdev_region(dev_t from, unsigned count)
static inline void unregister_chrdev(unsigned int major, const char *name)
1.6 分配设备号
不仅支持静态申请设备号,也支持动态申请设备号,并将主设备号返回
static inline** **int** register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
1.7 初始化 cdev
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
1.8 设备注册和注销
# 添加一个新的字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
# 注销
void cdev_del(struct cdev *p)
1.9 设备节点的创建和销毁
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
void device_destroy(struct class *class, dev_t devt)
这里注意,节点的创建可以使用代码,也可以使用mknod 命令创建设备节点
用法:mknod 设备名设备类型主设备号次设备号
如:mkmod /dev/test c 2 0
创建一个字符设备/dev/test,其主设备号为 2,次设备号为 0。
创建流程:创建了一个字符设备文件时,实际上就是创建了一个设备节点 inode 结构
体,并且将该设备的设备编号记录在成员 i_rdev,将成员 f_op 指针指向了 def_chr_fops 结构体。
五、open 函数到底做了什么
户空间使用 open() 系统调用函数打开一个字符设备时 (int fd = open(“dev/xxx”, O_RDWR)) 大致
有以下过程:
• 在虚拟文件系统 VFS 中的查找对应与字符设备对应 struct inode 节点
• 遍历散列表 cdev_map,根据 inod 节点中的 cdev_t 设备号找到 cdev 对象
• 创建 struct file 对象 (系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件
秒速符作为数组下标标识了一个设备对象)
• 初始化 struct file 对象,将 struct file 对象中的 file_operations 成员指向 struct cdev 对象中的
file_operations 成员 (file->fops = cdev->fops)
• 回调 file->fops->open 函数
chrdev_open 函数代码实现:
总结整个过程,当使用 open 函数,打开设备文件时,会根据该设备的文件的设备号找到
相应的设备结构体,从而得到了操作该设备的方法。也就是说如果我们要添加一个新设备的话,
我们需要提供一个设备号,一个设备结构体以及操作该设备的方法 (file_operations 结构体)。
六、代码程序
1、驱动代码
创建文件chrdevbase.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_NAME "chrdevbase"
#define DEV_CNT (1)
#define BUFF_SIZE 128
//定义字符设备的设备号
static dev_t devno;
//定义字符设备结构体chr_dev
static struct cdev chr_dev;
//数据缓冲区
static char vbuf[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
static struct file_operations chr_dev_fops =
{
.owner = THIS_MODULE,
.open = chr_dev_open,
.release = chr_dev_release,
.write = chr_dev_write,
.read = chr_dev_read,
};
static int chr_dev_open(struct inode *inode, struct file *filp)
{
printk("\nopen\n");
return 0;
}
static int chr_dev_release(struct inode *inode, struct file *filp)
{
printk("\nrelease\n");
return 0;
}
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
if(p > BUFF_SIZE)
return 0;
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_from_user(vbuf, buf, tmp);
*ppos += tmp;
return tmp;
}
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
if(p >= BUFF_SIZE)
return 0;
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}
static int __init chrdev_init(void)
{
int ret = 0;
printk("chrdev init\n");
//第一步
//采用动态分配的方式,获取设备编号,次设备号为0,
//设备名称为EmbedCharDev,可通过命令cat /proc/devices查看
//DEV_CNT为1,当前只申请一个设备编号
ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
if(ret < 0){
printk("fail to alloc devno\n");
goto alloc_err;
}
//第二步
//关联字符设备结构体cdev与文件操作结构体file_operations
cdev_init(&chr_dev, &chr_dev_fops);
//第三步
//添加设备至cdev_map散列表中
ret = cdev_add(&chr_dev, devno, DEV_CNT);
if(ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
return 0;
add_err:
//添加设备失败时,需要注销设备号
unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
return ret;
}
static void __exit chrdev_exit(void)
{
printk("chrdev exit\n");
unregister_chrdev_region(devno, DEV_CNT);
cdev_del(&chr_dev);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
makefile
# 内核目录
KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
# rk3568是arm64架构
ARCH=arm64
# 交叉工具链
CROSS_COMPILE=/opt/atk-dlrk356x-toolchain/usr/bin/aarch64-buildroot-linux-gnu-
export ARCH CROSS_COMPILE
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
2、测试 APP
创建文件chrdevbaseApp.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
char *wbuf = "chrdevbase test\n";
char rbuf[128];
int main(void)
{
printf("chrdevbase test\n");
//打开文件
int fd = open("/dev/chrdevbase", O_RDWR);
//写入数据
write(fd, wbuf, strlen(wbuf));
//写入完毕,关闭文件
close(fd);
//打开文件
fd = open("/dev/chrdevbase", O_RDWR);
//读取文件内容
read(fd, rbuf, 128);
//打印读取的内容
printf("The content : %s \n", rbuf);
//读取完毕,关闭文件
close(fd);
return 0;
}
编译:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc chrdevbaseApp.c -o
chrdevbaseApp
3、测试
把生成的ko文件和chrdevbaseApp文件通过tftp等方式上传到开发板上测试。