RK3568(二)——字符设备驱动开发

最基础的字符设备驱动开始,重点学习 Linux 下字符设备驱动开发框架。

驱动框架

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

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

  • open和 close 就是打开和关闭 led 驱动的函数
  • write 函数来操作,向驱动写入数据,read 函数从驱动中读取相应的状态

因为驱动是运行在内核空间中,用户空间想要对驱动进行操作,必须要通过系统调用进行。

应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。

字符驱动开发步骤

学 Linux 驱动开发重点是学习其驱动框架。

驱动加载和卸载

Linux驱动有两种运行方式:

  1. 将驱动编译到内核中,当Linux内核启动自动运行驱动程序
  2. 将驱动编译成模块(Linux下模块扩展名为.ko),在内核启动之后通过**modprobe** **insmod**命令加载驱动模块。

在调试驱动的时候一般都选择将其编译为模块,将驱动编译为模块最大的好处就是方便开发,这里我们统一用**<font style="color:#DF2A3F;">modprobe</font>**进行模块加载。

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

module_init(xxx_init);

module_exit(xxx_exit);

c 复制代码
/***************************************************************
 * chrdevbase_init - 函数名
 * description  : 驱动入口函数
 * @param       : 无
 * 返回值        : 0 成功
***************************************************************/
static int __init chrdevbase_init(void)
{
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
    if (retvalue < 0)       
    {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init() \r\n");
    return 0;
}


/***************************************************************
 * chrdevbase_exit - 函数名
 * description  : 驱动出口函数
 * @param       : 无
 * 返回值        : 0 成功
***************************************************************/
static void __exit chrdevbase_exit(void)
{

    /* 注销字符设备驱动 */
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit() \r\n");
}
  • 当使用"modprobe"命令加载驱动的时候,xxx_init 这个函数就会被调用
  • 当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用

注:加载模块命令insmod 和modprobe的区别:

  • insmod 是最简单的模块加载命令,此命令用于加载指定的.ko 模块,insmod 命令不能解决模块的依赖关系。
  • modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动

字符设备注册和注销

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

c 复制代码
/* 注册字符设备 */
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);
}

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。****如上节所示

  • test_fops 就是设备的操作函数集合
  • 要注意的一点就是,选择没有被使用的主设备号,输入命令"cat /proc/devices"可以查看当前已经被使用掉的设备号
  • 查看已使用设备号cat /proc/devices

实现设备具体操作

file_operations 结构体就是设备的具体操作函数。完成变量 test_fops 的初始化,设置好针对 chrtest 设备的操作函数。

c 复制代码
/*
* 设备操作函数结构体
* 将驱动函数映射为系统调用
*/
static struct file_operations chrdevbase_fops = 
{
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};
c 复制代码
#define CHRDEVBASE_MAJOR 200            /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase"    /* 设备名 */

static char readbuf[100];               /* 读缓冲区 */
static char writebuf[100];           /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};




/***************************************************************
 * chrdevbase_open - 函数名
 * description  : 打开设备
 * @param-inode : 传递给驱动的 inode
 * @param-filp  : 设备文件,file 结构体有个叫做 private_data 的成员变量
 * 一般在 open 的时候将 private_data 指向设备结构体。
 *
 * 返回值       : 0 成功
***************************************************************/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    printk("chrdevbase open! \r\n");
    return 0;
}

/***************************************************************
 * chrdevbase_read - 函数名
 * @description   : 从设备读取数据
 * @param-filp   : 要打开的文件设备
 * @param-buf    : 返回给用户空间的数据缓冲区
 * @param-cnt    : 读取的数据长度
 * @param-off_t  : 相对于文件首地址的偏移
 * @return       :读取的字节数
***************************************************************/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off_t)
{

    int retvalue = 0;
    /* 完成内核空间的数据到用户空间的复制 */
    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if (retvalue==0)
    {
        printk("kernel senddata success! \r\n");
    }
    else
    {
        printk("kernel senddata failed! \r\n ");
    }
    
    return 0;
}

/***************************************************************
 * chrdevbase_write - 函数名
 * @description   : 向设备写入数据
 * @param-filp   : 设备文件,要打开的文件设备
 * @param-buf    : 要写入设备的数据
 * @param-cnt    : 写入的数据长度
 * @param-off_t  : 相对于文件首地址的偏移
 * @return       :写入的文件数 
***************************************************************/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off_t)
{

    int retvalue = 0;
    /* 接收用户空间的数据并打印 */

    retvalue = copy_from_user(writebuf, buf, cnt);
    if (retvalue==0)
    {
        printk("kernel recevdata:%s \r\n", writebuf);
    }
    else
    {
        printk("kernel recevdata failed! \r\n ");
    }
    
    return 0;
}

/***************************************************************
 * chrdevbase_release - 函数名
 * description  : 关闭释放设备
 * @param-inode : 传递给驱动的 inode
 * @param-filp  : 设备文件,要关闭的文件设备描述符
 *
 * 返回值       : 0 成功
***************************************************************/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
    printk("chrdevbase release! \r\n");
    return 0;
}
c 复制代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

/***************************************************************
 * 文件名   : chrdevbase.c
 * 功能     : 这是一个简单的虚拟设备驱动程序,用于演示内核模块的初始化和清理
 * 作者     : zxk
 * 创建日期: 2024年11月18日
***************************************************************/

#define CHRDEVBASE_MAJOR 200            /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase"    /* 设备名 */

static char readbuf[100];               /* 读缓冲区 */
static char writebuf[100];           /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};




/***************************************************************
 * chrdevbase_open - 函数名
 * description  : 打开设备
 * @param-inode : 传递给驱动的 inode
 * @param-filp  : 设备文件,file 结构体有个叫做 private_data 的成员变量
 * 一般在 open 的时候将 private_data 指向设备结构体。
 *
 * 返回值       : 0 成功
***************************************************************/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    printk("chrdevbase open! \r\n");
    return 0;
}

/***************************************************************
 * chrdevbase_read - 函数名
 * @description   : 从设备读取数据
 * @param-filp   : 要打开的文件设备
 * @param-buf    : 返回给用户空间的数据缓冲区
 * @param-cnt    : 读取的数据长度
 * @param-off_t  : 相对于文件首地址的偏移
 * @return       :读取的字节数
***************************************************************/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off_t)
{

    int retvalue = 0;
    /* 完成内核空间的数据到用户空间的复制 */
    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if (retvalue==0)
    {
        printk("kernel senddata success! \r\n");
    }
    else
    {
        printk("kernel senddata failed! \r\n ");
    }
    
    return 0;
}

/***************************************************************
 * chrdevbase_write - 函数名
 * @description   : 向设备写入数据
 * @param-filp   : 设备文件,要打开的文件设备
 * @param-buf    : 要写入设备的数据
 * @param-cnt    : 写入的数据长度
 * @param-off_t  : 相对于文件首地址的偏移
 * @return       :写入的文件数 
***************************************************************/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off_t)
{

    int retvalue = 0;
    /* 接收用户空间的数据并打印 */

    retvalue = copy_from_user(writebuf, buf, cnt);
    if (retvalue==0)
    {
        printk("kernel recevdata:%s \r\n", writebuf);
    }
    else
    {
        printk("kernel recevdata failed! \r\n ");
    }
    
    return 0;
}

/***************************************************************
 * chrdevbase_release - 函数名
 * description  : 关闭释放设备
 * @param-inode : 传递给驱动的 inode
 * @param-filp  : 设备文件,要关闭的文件设备描述符
 *
 * 返回值       : 0 成功
***************************************************************/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
    printk("chrdevbase release! \r\n");
    return 0;
}

/*
* 设备操作函数结构体
* 将驱动函数映射为系统调用
*/
static struct file_operations chrdevbase_fops = 
{
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};


/***************************************************************
 * chrdevbase_init - 函数名
 * description  : 驱动入口函数
 * @param       : 无
 * 返回值        : 0 成功
***************************************************************/
static int __init chrdevbase_init(void)
{
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
    if (retvalue < 0)       
    {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init() \r\n");
    return 0;
}


/***************************************************************
 * chrdevbase_exit - 函数名
 * description  : 驱动出口函数
 * @param       : 无
 * 返回值        : 0 成功
***************************************************************/
static void __exit chrdevbase_exit(void)
{

    /* 注销字符设备驱动 */
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit() \r\n");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

Linux设备号机制

设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备

设备号组成:

  • 主设备号表示某一个具体的驱动
  • 次设备号表示使用这个驱动的各个设备

设备号分配

  • 静态分配:使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号,分配未使用的。
  • 动态分配

Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可

设备号的申请函数:

c 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)

驱动实验流程

应用程序调用 open 函数打开 chrdevbase 这个设备,打开以后可以使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),也可以使用 read 函数读取读缓冲区 readbuf 中的数据操作,操作完成以后应用程序使用 close 函数关闭 chrdevbase 设备。

添加头文件路径

c 复制代码
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/zxk/work/rk3568_linux_sdk/kernel/arch/arm64/include",
                "/home/zxk/work/rk3568_linux_sdk/kernel/include",
                "/home/zxk/work/rk3568_linux_sdk/kernel/arch/arm64/include/generated"
                
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

实现过程

  • chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用。
  • chrdevbase_read 函数,应用程序调用 read 函数从设备中读取数据的时候此函数会执行,因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
  • chrdevbase_write 函数,应用程序调用 write 函数向设备写数据的时候此函数就会执行。
  • chrdevbase_release 函数,应用程序调用 close 关闭设备文件的时候此函数会执行
  • module_init 和 module_exit 这两个函数来指定驱动的入口和出口函数。
  • 为了欺骗内核,给本驱动添加 intree 标记,如果不加就会有"loading out-of-treemodule taints kernel."这个警告

驱动测试APP-应用层

c 复制代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/***************************************************************
 * 文件名   : chrdevbaseAPP.c
 * 功能     : 这是一个chrdevbase 驱测试 APP
 * 作者     : zxk
 * 创建日期: 2024年11月18日
 * 其他     :使用方法 ./chrdevbaseAPP /dev/chrdevbase <1>|<2>
 *              argv[2] 1:读文件
 *              argv[2] 2:写文件
***************************************************************/


static char usrdata[] = {"usr data!"};

/***************************************************************
 * description  :  main主函数
 * @param-argc  :  argv 数组元素个数
 * @param-argv  :  具体参数
 * 
 * @return        : 0 成功
***************************************************************/
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

    if (argc != 3)
    {
        printf(" Error Usage!\r\n");
        return -1;
    }
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf(" Can't open the file %s \r\n", argv[1]);
        return -1;
    }   

    if (atoi(argv[2]) == 1)     /* 从驱动读取数据 */
    {
        retvalue = read(fd, readbuf, 50);
        if (retvalue < 0)
        {
            printf(" read the file %s failed!\r\n", filename);
        }
        else
        {
            printf("read data is %s \r\n", readbuf);
        }
    }
    
    if (atoi(argv[2]) == 2)     /* 写数据到驱动文件 */
    {
        memcpy(usrdata, writebuf, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);
        if (retvalue < 0)
        {
            printf("write file %s failed!\r\n", filename);
        }
    }

    /* 关闭设备 */
    retvalue = close(fd);
    if (retvalue < 0)
    {
        printf("Can't close the file %s!\r\n", filename);
        return -1;
    }
    
    return 0;
}

驱动和测试函数编译

驱动编译成模块

编写Makefile文件

makefile 复制代码
# ARCH := arm64
KERNELDIR := /home/zxk/work/rk3568_linux_sdk/kernel
CURRENT_PATH := /home/zxk/linux_driver/01_chrdevbase

obj-m := chrdevbase.o

bulid: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译模块

make ARCH=arm64 //<font style="color:#DF2A3F;">ARCH=arm64 必须指定,否则编译会失败</font>

编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块。

编译APP测试文件-交叉编译

bash 复制代码
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc chrdevbaseApp.c -o chrdevbaseApp

开发板测试驱动

上传编译后的驱动和测试文件

bash 复制代码
scp ./01_chrdevbase/chrdevbase.ko root@192.168.137.65:/lib/modules/4.19.232/
scp ./01_chrdevbase/chrdevbaseAPP root@192.168.137.65:/lib/modules/4.19.232/

加载驱动模块

  1. 输入"depmod"命令以后会自动生成 modules.alias、modules.symbols 和 modules.dep 等等一些 modprobe 所需的文件
bash 复制代码
depmod
  1. modprobe命令加载 chrdevbase.ko 驱动文件
bash 复制代码
modprobe chrdevbase.ko
  1. 查看是否加载成功
bash 复制代码
1.看到"chrdevbase init!"这一行
2.cat /proc/devices		# 查看当前系统中有没有 chrdevbase 这个设备
  1. 创建设备节点
bash 复制代码
mknod /dev/chrdevbase c 200 0
# 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用"ls /dev/chrdevbase -l"命令查看
  1. chrdevbase 设备操作测试
bash 复制代码
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2
  1. 卸载驱动模块
bash 复制代码
rmmod chrdevbase
相关推荐
明金同学5 分钟前
腾讯云海外服务器Window切换为linux系统(从Window DD 到 Linux)
linux·服务器·腾讯云
CC大煊1 小时前
【Linux】vi/vim 使用技巧
linux·运维·vim
cwtlw1 小时前
CSS学习记录11
前端·css·笔记·学习·其他
是十一月末1 小时前
Linux的基本功能和命令
linux·服务器·开发语言·数据库
暮已深1 小时前
【RTAB-Map+VINS-Fusion+euroc】(Ubuntu 20.04)三维稠密重建-实践笔记
linux·笔记·ubuntu
浮尘笔记2 小时前
在Ubuntu服务器上备份文件到自己的百度网盘
linux·服务器·ubuntu
cwtlw2 小时前
如何创建maven工程
java·笔记·后端·学习·maven
Hacker_xingchen2 小时前
影响 Linux、Unix 系统的 CUPS 漏洞可导致 RCE
linux·运维·unix
难以触及的高度2 小时前
Unix/Linux 命令行重定向操作
linux·服务器·unix
neeef_se2 小时前
【Linux】WG-Easy:基于 Docker 和 Web 面板的异地组网
linux·前端·docker