Linux嵌入式学习小结(1)——初识字符设备驱动开发

2021/10/23 7:13:01

本文主要是介绍Linux嵌入式学习小结(1)——初识字符设备驱动开发,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Linux嵌入式学习小结(1)——初始字符设备驱动开发

前言

笔者前段时间自学了有关LinuxARM开发,但由于有关知识过于繁多,所以在此写下相关小结以作梳理。

1.所使用学习资源

1.开发板:正点原子阿尔法开发板:IMX6ULL
2.编程环境:ubuntu16.4
3.ARM版所用Linux系统内核:linux-imx-rel_imx_4.1.15_2.1.0_ga
注:相关资料大家可自行前往正点原子官网下载

2.相关理论知识

1.关于LInux驱动开发

所谓Linux驱动开发,个人理解其实就是编写相关代码,让你自己的传感器设备或其他外部资源设备被ARM识别到,并能通过相关函数来调用这些设备。
与我们之前接触的单片机裸机开发不同,Linux驱动开发需要严格按照Linux所规定的框架来进行编写,因为Linux为我们提供了众多的供开发者调用的API函数(目前定义为抽象层),但由于ARM开发板种类众多,各个寄存器的相关定义也各不相同,所以需要Linux驱动开发者根据具体所使用的ARM芯片来编写相关代码来满足抽象层的使用条件(驱动层),使得ARM板可以识别到设备并可以调用抽象层API函数来控制具体设备,而开发者编写了驱动层后还需编写应用函数来调用抽象层的函数来实现所需要的功能(暂定义为应用层)。
在这里插入图片描述如上图所示,我们开发者所需编写的即为符合Linux所规定的框架的驱动层和应用层函数。
Linux将驱动设备分为:字符设备、块驱动、网络设备驱动。其中字符设备驱动是占用篇幅最大的一类驱动,因为字符设备最多,从最简单的点灯到 I2C、 SPI、音频等都属于字符设备驱动的类型。块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。网络设备驱动就更好理解了,就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
由于字符设备驱动使用最为频繁,所以后面我以字符设备为例子,向大家介绍Linux驱动开发。
Linux驱动开发框架大致如下:
在这里插入图片描述

2.驱动层编写框架

1.驱动模块的加载与卸载

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

代码模板如下:

1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函数具体内容 */
5 return 0;
6 }
7 8
/* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函数具体内容 */
12 }
13
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);

2.字符设备的注册与注销函数

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模
块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

//major:主设备号
//name:设备名
//fops:设备操作函数集合

3.设备操作函数集合

框架规定使用设备操作函数集合变量来包括设备所能使用的操作函数,用来具体操作设备。
其在Linux内核中的原型如下:

//file_operations 结构体
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t
*);
1592 ssize_t (*write) (struct file *, const char __user *, size_t,
loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct
*);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned
long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned
long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
1606 int (*fasync) (int, struct file *, int);
1607 int (*lock) (struct file *, int, struct file_lock *);
1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t,
loff_t *, int);
1609 unsigned long (*get_unmapped_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
1610 int (*check_flags)(int);
1611 int (*flock) (struct file *, int, struct file_lock *);
1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
loff_t *, size_t, unsigned int);
1613 ssize_t (*splice_read)(struct file *, loff_t *, struct
pipe_inode_info *, size_t, unsigned int);
1614 int (*setlease)(struct file *, long, struct file_lock **, void
**);
1615 long (*fallocate)(struct file *file, int mode, loff_t offset,
1616 loff_t len);
1617 void (*show_fdinfo)(struct seq_file *m, struct file *f);
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };

file_operation 结构体中比较重要的、常用的函数:
第 1589 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 1590 行, llseek 函数用于修改文件当前的读写位置。
第 1591 行, read 函数用于读取设备文件。
第 1592 行, write 函数用于向设备文件写入(发送)数据。
第 1596 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 1599 行, mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行, open 函数用于打开设备文件。
第 1603 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像 open、 release、 write、 read 等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。

代码示例:

1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
7 8
/* 从设备读取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
10 {
11 /* 用户实现具体功能 */
12 return 0;
13 }
14
15 /* 向设备写数据 */
16 static ssize_t chrtest_write(struct file *filp,
const char __user *buf,
size_t cnt, loff_t *offt)
17 {
18 /* 用户实现具体功能 */
19 return 0;
20 }
21
22 /* 关闭/释放设备 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 用户实现具体功能 */
26 return 0;
27 }
28
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
36
37 /* 驱动入口函数 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函数具体内容 */
41 int retvalue = 0;
42
43 /* 注册字符设备驱动 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字符设备注册失败,自行处理 */
47 }
48 return 0;
49 }
50
51 /* 驱动出口函数 */
52 static void __exit xxx_exit(void)
53 {
54 /* 注销字符设备驱动 */
55 unregister_chrdev(200, "chrtest");
56 }
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);

4.添加其他信息

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数:

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
......
57
58 /* 将上面两个函数指定为驱动的入口和出口函数 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
61
62 MODULE_LICENSE("GPL");
63 MODULE_AUTHOR("yqx");

第 62 行, LICENSE 采用 GPL 协议。
第 63 行,添加作者名字。

5.关于设备号

一个设备可以被ARM板控制就必须给它分配一个设备号,便于ARM板来区分不同的设备,设备号分为主设备号和次设备号,主设备号一般用于区分不同的驱动,次设备号用于区分使用从驱动的不同具体设备。

12 typedef __u32 __kernel_dev_t;
......
15 typedef __kernel_dev_t dev_t;

dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。

设备号的分配

设备号分配方式有静态和动态两种方式,静态分配是自己认为给定设备号,动态是系统自动为设备分配主设备号和次设备号。

静态设备号分配

本章先使用静态设备号分配来为设备分配设备号,后面为大家讲解如何使用动态设备号来进行分配。
前面讲解字符设备驱动的时候说过了,注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个
设备号,比如选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。

动态设备号分配

静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题, Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//dev:所申请到的设备号
//baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 //baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
//count:要申请的设备号数量
//name:设备名称

所以字符设备具体驱动框架大致为编写模块的加载和卸载函数,使用注册与注销函数,并定义与完善字符设备操作函数集与分配设备号与设置License信息等几个步骤即可完成对一个设备驱动的编写。

3.实验程序编写

本章作者创建一个虚拟的字符设备,并实现此虚拟涉笔的读写功能,以此展现字符设备的开发框架

1.VScode的配置

这里具体看正点原子官方手册,不在此赘述

2.Makefile编写

#定义Linux内核的存储地址
KERNELDIR := /home/yqx/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
#表示为当前工作目录
CURRENT_PATH := $(shell pwd)
#所需要编译的模块设备:.c文件要与.o文件同名(eg:chrdevbase.o对应着编译chrdevbase.c)
obj-m := chrdevbase.o
#具体的编译命令,后面的 modules 表示编译模块
build: kernel_modules
# -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动
#到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

3.设备驱动程序的编写

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/module.h>

#define DEV_MAJOR 300 //主设备号
#define DEV_NAME "chrdevbase"

static char readbuf[100];//读缓存区
static char writebuf[100];//写缓存区
static char kerneldata[] = "kernel data!" ; //内核传递数据


/***************************/
/*
    Linux嵌入式字符设备实验驱动实验
    author:yqx
*/
/***************************/

static int chrdevbase_open (struct inode *inode, struct file *filp){


    return 0;
}

static int chrdevbase_close (struct inode *inode, struct file *filp){


    return 0;
}
static ssize_t chrdevbase_write (struct file *flip, const char __user *buf, size_t cnt,loff_t *offt){
    int retvalue = 0;
    retvalue = copy_from_user(writebuf,buf,cnt);
    if(retvalue == 0){
        printk("USERDATA:%s\r\n",writebuf);
    } else {
        printk("Receiving UserData is failed\r\n");
        return -1;
    }
    return 0;
}



static ssize_t chrdevbase_read(struct file * flip, char __user *buf, size_t cnt, loff_t *offt){
    int retvalue = 0;
    memcpy(readbuf,kerneldata,sizeof(kerneldata));
    retvalue = copy_to_user(buf,readbuf,cnt);
    if(retvalue == 0){
        printk("kerneldata sending is successful\r\n");
    } else {
        printk("kerneldata sending is failed\r\n");
        return -1;
    }
    return 0;
}

static struct file_operations dev_fops = {  //字符设备操作集
    .owner = THIS_MODULE,
    .open  = chrdevbase_open,
    .release = chrdevbase_close,
    .read    = chrdevbase_read,
    .write   = chrdevbase_write,

};

//驱动入口函数
static int __init chrdevbase_init(void){
    int retvalue = 0;
    retvalue = register_chrdev(DEV_MAJOR,DEV_NAME,&dev_fops);//注册字符设备驱动
    if(retvalue < 0 ){
        printk("模块注册失败\r\n");
        return -1;
    }
    printk("模块注册成功!设备名称:%s\t设备号:%d\r\n",DEV_NAME,DEV_MAJOR);
    return 0;
}

//驱动出口函数
static void __exit chrdevbase_exit(void){
    unregister_chrdev(DEV_MAJOR,DEV_NAME);//注销字符设备驱动
    printk("模块注销!\r\n");

}

//指定驱动入口出口函数
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL"); //采用GPL协议
MODULE_AUTHOR("yqx");  //添加作者,非必要 

4.设备应用程序的编写

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

/*************************/
/*
字符设备设施App
./chrdevbaseApp /dev/chrdevbase 0:读
./chrdevbaseApp /dev/chrdevbase 1:写
*/
/*************************/
static char userdata[] = "user data!";

int main(int argc,char* argv[]){
    int retvalue,fd;//fd:文件描述符
    char buf[100];
    char writebuf[100];
    char* filename;

    retvalue =0;
    if( (argc != 3)&&(argc !=4)){
        printf("输入非法\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename,O_RDWR);

    if(fd < 0){
        printf("文件打开失败\r\n");
        return -1;
    }

    if(atoi(argv[2]) == 0){ //atoi():将字符转换为整数,0:读数据
        retvalue = read(fd,buf,100);
        if(retvalue < 0){
            printf("读文件失败\r\n");
            return -1;
        } else {
            printf("读取文件成功,数据为:%s\r\n",buf);
        }
    }

    if(atoi(argv[2]) == 1){ //1:写文件
        //此处长度不能用sizeof(argv[3])来代替,因为argv[3]为指针类型,总为4字节
        memcpy(writebuf,argv[3],sizeof(writebuf));
        //此处长度不能用sizeof(argv[3])来代替,因为argv[3]为指针类型,总为4字节
        retvalue = write(fd,writebuf,sizeof(writebuf));
        if(retvalue < 0){
            printf("写文件失败\r\n");
            return -1;
        }
    }
    retvalue = close(fd);
    if(retvalue == 0){
        printf("关闭文件成功!\r\n");
    } else {
        printf("错误:无法关闭文件\r\n");
        return -1;
    }
    return 0;
}

5.程序编译

1.驱动程序编译
输入make指令完成程序编译
在这里插入图片描述如图编译成功,生成.ko文件

2.应用程序编译
输入如下命令:

arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

编译完成以后会生成一个叫做 chrdevbaseApp 的可执行程序,输入如下命令查看chrdevbaseAPP 这个程序的文件信息:

file chrdevbaseApp

在这里插入图片描述

6.模块的传输和加载

由于ARM板的根文件系统通过网络连接就构建在主机虚拟机上,故将编译好的驱动文件和应用文件复制到跟文件系统中即可。

 sudo cp -rf chrdevbase.ko chrdevbaseApp /home/yqx/linux/nfs/rootfs/lib/modules/4.1.15/

在这里插入图片描述
再通过modprobe加载模块等其他操作即可,与创建节点设备文件即可。
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:

mknod /dev/chrdevbase c 200 0

4.程序运行效果

在这里插2入图片描述
在这里插入图片描述
在这里插入图片描述
经过测试,程序运行正常,字符设备驱动框架介绍到这,本讲结束!



这篇关于Linux嵌入式学习小结(1)——初识字符设备驱动开发的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程