一、(正点原子)字符设备驱动

一、字符设备驱动简介

        字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,
LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

        Linux应用程序对驱动的调用:

        在Linux中一切皆文件。驱动加载成功以后会在“/dev”目录下生成一个相应的文件。应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

        应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。

        例如我门调用应用程序的open函数:

        我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合:

        owner拥有改结构体的模块的指针,一般设置为THIS_MODULE。

        llseek :用与修改文件当前的读写位置的函数。

        read :用于读取设备文件函数。

        write :用与向设备文件写入(发送)数据函数。

        poll :轮询函数(轮询函数是在一定时间间隔内反复执行某个函数的过程),用于查询设备是否可以进行非阻塞的读写。

        unlocked_ioctl :提供对于设备的控制功能,与应用函数中的ioctl函数对应。

        compat_ioctl :与unlocked_ioctl函数功能一样,于别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl

        mmap :将设备的内存映射到进程空间中(用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将LCD 显存映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。

        open :打开设备文件函数。

        release :释放(关闭)设备文件,与应用程序中的close函数对应。

        fasync :用于刷新待处理的数据,用于将缓存区中的数据刷新磁盘中。

        aio_fsync :与fasync函数功能一样,aio_fsync是异步刷新。

        以上是比较常用的函数,当然具体需要实现哪些函数还是得看具体的驱动要求

二、字符设备驱动开发步骤

        1、驱动模块的加载和卸载

        Linux驱动有两种运行方式,第一种是将驱动编译Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种是将驱动编译成模块(Linux下模块扩展名为".ko"),在Linux内核启动后使用命令“insmod”或者“modprobe”命令加载驱动模块。在调式驱动的时候一般都选择将其编译为模块,这样我门就不需要重新烧写Linux内核,非常方便。将驱动编译成模块是最方便开发的。

        模块有加载和卸载两种操作,在编写驱动时需要先注册这两个操作函数:

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

           我们使用vscode创建一个工程创建一个字符设备驱动的.c文件因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux源码中的头文件路径。打开VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,

         includePath表示头文件路径,我们需要将前面我们移植NXPLinux内核编译过的Linux内核源码的头文件加进来:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/zxg/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include", 
                "/home/zxg/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include", 
                "/home/zxg/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

        在Linux源码种也有其他模块的注册加载和卸载,我们可以参考写出我们的注册加载和卸载模块函数 

        注册模块加载函数和注册模块卸载函数可以完成跳转到Linux内核源码。 

        module_init :向Linux内核注册一个模块加载函数,参数chrdevbase_init就是要注册的具体函数。当我们使用“insmod”命令或“modprobe”命令加载驱动时候,chrdevbase_init函数就会被调用。

        module_exit :向Linux内核注册一个模块卸载函数,参数chrdevbase_exit就是具体的函数,当使用“rmmod”命令卸载具体驱动时候,chrdevbase_exit函数就会被调用。

        编写Makefile编译chrdevbase.c文件为.ko模块:

KERNELDIR := /home/zxg/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
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) modules

         KERNELDIR :表示开发板所使用的Linux内核源码目录,使用绝对路径。

        CURRENT_PATH :表示当前路径,直接通过pwd命令获取当前路径。

        obj-m :表示chrdevbase.c这个文件编译为chrdevbase.ko模块。

        后面的 modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
 

        驱动编译完成后扩展名为.ko,可以使用“insmod”和“modprobe”命令来加载驱动,“insmod”时最简单的模块加载命令,但是不能解决依赖关系,比如说有一个驱动lcd.ko时以来与frist.ko这个模块的,就必须使用“insmod”加载frist.ko模块,然后在加载lcd.ko模块。“modprobe”命令不存在这个问题,它会先分析加载的模块的依赖关系,然后都会将所有依赖的模块都加载到内核种,因此我们使用“modprobe”命令比较多 。

        注:第一此在Linux内核使用“modprobe”命令时,“modprobe”命令会默认到/lib/modules/<kernel-version>目录下查找编译后的驱动模块,所以需要自己在根文件系统种创建这样一个目录,比如我使用的Linux kernel使用的时4.1.15版本,所以我需要在根文件/lib下创建modules目录,在/modules目录下在创建一个4.1.15的目录。

         将编译好的驱动模块(.ko)拷贝到根文件系统广告费创建的4.1.15目录下。

        在serialCRT中使用“modprobe”加载驱动模块:

        

        第一此使用modprobe命令,需要先使用depmod命令来添加modprobe命令的依赖。到这里字符设备驱动模块的加载和卸载我们就已经完成了。

        2、支付设备的注册与注销

        对于字符设备驱动,当驱动模块加载成功后,需要注册字符设备,同时卸载驱动模块时候也需要注销掉字符设备。

头文件 
#include <linux/fs.h>

/* 注册字符设备函数 */
static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

/* 销毁字符设备函数 */
static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}

        major :主设备号。Linux下每一个设备都会有设备号,设备号分为主设备号和次设备号。后面有详细的讲。

        name :设备的名字。指向遗传字符串。

        fops :它时file_operations结构体的指针, file_operations前面我们也说过,就是 Linux 内核驱动操作函数集合。它指向设备的操作函数集合变量。

        一般字符设备的注册都是在驱动模块的入口函数中进行,字符串设备的注销也是在出口函数进行。

        在我们vscode工程中,在chrdevbase.c中,在入口函数添加注册驱动模块,在出口函数添加注销驱动模块。根据file_operations结构体,具体实现出字符设备模块打开,关闭,读和写函数:

        添加LICENSE和作者信息:

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

        LICENSE: 许可证,我们使用GPL协议。GPL协议一般指GNU通用公共许可证。 GNU通用公共许可证简称为GPL,是由自由软件基金会发行的用于计算机软件的协议证书,使用该证书的软件被称为自由软件。

        AUTHOR:作者的名字。

三、Linux设备号

        1、设备号的组成

        在Linux中,为了方便管理,每个设备都有一个设备号,设备号由主设备号和次设备号组成。主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux提供一个dev_t的数据类型表示设备号,在include/linux/types.h里面。

        可以看出dev_t其实就是unsigned int类型,无符号32位 。其中高12位表示主设备号,低20位表示次设备号。所以主设备号范围0~4095。

        2、设备号的分配

        1、静态分配设备号

        注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个设备号,比如选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,我们可以使用命令“cat /proc/devices”查看
 

        2、动态分配设备号

         静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用
的。而且静态分配设备号很容易带来冲突问题, 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 :设备名字。

 

void unregister_chrdev_region(dev_t from, unsigned count)

        from :要释放的设备号。

        count :表示从from开始,要释放的设备号数量 。

四、chrdevbase字符设备驱动实验

        1、编写驱动程序

        当字符串驱动文件中,当我们加载驱动程序时,会调用一次具体的驱动入口函数,会执行chrdevbase_init函数,chrdevbase_init函数中我们注册了字符设备,字符设备有,打开设备,关闭设备,向设备写数据和读数据的功能:

        writebuf:就是用户区要向设备写入的数据缓存区。

        readbuf: 用户从设备读取数据的缓存区。

        kerneldata[] :保存用户区要读取的数据。

         printk :函数原型:const char * fmt(…)。和C语言中的printf一样,只不过printk是在内核使用的,printf是在用户区使用的,他们都是在控制台打印信息,方便调式。printk可以根据日志级别对消息进行分类。在Linux内核源码的/include/linux/kern_levels.h中:

        一共定义8个级别,0的优先级最高,如果要设置消息级别的参数:

printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");

         上述代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。在
具体的消息前面加上 KERN_EMERG 就可以将这条消息的级别设置为 KERN_EMERG。如果不使用消息级别的话,printk会使用KERN_DEBUG这个级别。

        copy_to_user :内核空间向用户空间拷贝数据。

        函数原型:

static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

         to:表示目的,from:表示源头。n:表示要复制的数据长度。long:返回值,如果复制成功返回0,如果失败,返回没有拷贝成功的数据字节数。

        copy_from_user :用户空间向内核空间拷贝数据。

        函数原型:

unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)

        to:表示目的。from:表示源头。n :表示要复制的数据长度。ulong:返回值 如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。

         memcpy:

        函数原型:

void *memcpy(void*dest, const void *src, size_t n);

        于C语言中的用法一样。 

2、编写应用程序APP

        应用程序主要是对驱动程序设备进行读写功能的测试。编写应用程序APP也就是编写Linux应用。在Linux应用要使用C库和一些文件相关的函数。

        在ubuntu下使用man命令查看man:翻译过来就是手册的意思,一般用来查看Linux系统中的函数信息。

        比如在应用程序中,我们要使用open函数来打开驱动文件,可以使用man 2 open查看:

        编写chrdevbaseAPP.c文件。

         main函数的参数位三个参数,判断运行测试APP输入参数如果不是三个的话,则使用错误。比如现在要向chrdevbase设备中读数据,需要命令如下:

./chrdevbaseApp   /dev/chrdevbase    1

         ./chrdevbaseApp:对应argv[0],表示运行这个软件。

        /dev/chrdevbase:对应argv[1],表示测试的软件要打开/dev/chrdevbase这个设备。

        1:表示对这个设备进行扫描操作通过程序指定。

        在程序中使用到Linux的一些函数:

        open():系统调用打开由pathname指定的文件。如果指定文件不存在,它可以选择性的由open()函数创建(如果指定了O_CREATE)。

        头文件:

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

        函数原型:

int open(const char *pathname, int flags);

        pathname:要打开的设备或文件名。 

        flags:文件打开模式。有三种打开模式:O_RDONLE(只读)、O_WRONLY(只写)、0_RDWR(读写)。除了这三种必须的模式外,还可以使用逻辑或来添加其他模式

O_APPEND

每次写操作都写入文件的末尾

O_CREAT

如果指定文件不存在,则创建这个文件

O_EXCL

如果要创建的文件已存在,则返回 -1,并且修改 errno 的值

O_TRUNC

如果文件存在,并且以只写/读写方式打开,则清空文件全部内容

O_NOCTTY

如果路径名指向终端设备,不要把这个设备用作控制终端

O_NONBLOC

如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
DSYNC等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新

O_RSYNC

read 等待所有写入同一区域的写操作完成后再进行

O_SYNC

等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。

        返回值:如果文件打开成功的话返回文件的文件描述符。

        read():尝试从文件描述符fd读取到的字节数,从buf开始的缓冲区。

        头文件:#include <unistd.h>

        函数原型:

ssize_t read(int fd, void *buf, size_t count);

        fd:要读取的文件描述符。一般打开文件后会返回一个文件描述符。

        buf:数据读取存放的位置。

        count:要读取的数据长度。  

        返回值:读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返
回负值,表示读取失败。

        write():从缓冲区的buf开始写入到文件描述符fd引用的文件。

        头文件:#include <unistd.h

        函数原型:

ssize_t write(int fd, const void *buf, size_t count);

         fd:要写入的文件描述符。

         buf:要写入的数据。

        count:要写入的数据长度。

        返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回
负值,表示写入失败。

        close():关闭一个文件的文件描述符,使其不再引用任何描述符文件,可以重复使用。

       头文件:#include <unistd.h>

        函数原型:

int close(int fd);

        fd:要关闭的文件描述符。

        返回值:0 表示关闭成功,负值表示关闭失败。

3、编译驱动程序和测试APP

        驱动程序我门使用Makefile编译,前面我们已经介绍过了驱动的Makefile。测试APP的编译我们不适用Makefile编译,因为就一个文件,所以我们采用直接编译。

        驱动程序:

        测试APP:

 

        4、加载运行

        将编译好的驱动模块(.ko)和测试APP文件拷贝到nfs的根文件系统的/lib/modules/4.1.15目录下。需要加上权限。

         再serialCRT上,先加载驱动模块:

         查看当前系统中有没有chrdevbase这个设备:

        可以看出这个设备加载成功,设备号就是我们驱动代码中给的200。

         创建设备节点文件

        驱动加载成功需要再/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。

        

mknod /dev/chrdevbase c 200 0

        /dev/chrdevbase :创建的节点文件。

        c:表示这个是一个字符设备        

        200:主设备号。

        0:次设备号。

        设备操作测试

 

        非常完美!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tofu_Cabbage

你的打赏是我的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值