Linux驱动开发:字符设备驱动开发实战

一、工程创建

VSCode 创建工程,设置 C/C++ 配置,导入 linux kernel 源码目录,方便 vscode 写代码自动补全,vscode 配置

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

二、驱动程序编写

新建一个文件 char_drive.c 文件:

驱动程序主要包含:驱动的注册和卸载,以及基本驱动操作,比如打开、关闭、读取、写入等操作,这里使用原子的例程分析:

2.1 驱动操作接口

打开字符设备,因为驱动在 linux 下就是一个文件,我们传入文件的 inode 和文件结构体用于关联设备。

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

关闭字符设备,关闭设备就是打开设备的逆操作,断开设备结构体和文件结构体的关联。

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
    // printk("chrdevbase release!\r\n");
    return 0;
}

读字符设备,传入驱动设备文件,这里我们并没有使用文件指针,模拟进行操作,使用 copy_to_user 内核函数,将数据打印到用户数据空间。

/*
 * @description		: 从设备读取数据
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, 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("kernel senddata ok!\r\n");
    }
    else
    {
        printk("kernel senddata failed!\r\n");
    }

    // printk("chrdevbase read!\r\n");
    return 0;
}

写字符设备,和读字符串设备相同,只不过数据读写方向相反。

/*
 * @description		: 向设备写数据
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    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");
    }
    // printk("chrdevbase write!\r\n");
    return 0;
}

2.2 注册、注销接口

驱动基本操作程序写话后,编写驱动注册和注销接口:

驱动注册本质上就是将操作函数作为指针传递进去,挂载到调用结构体上,注册时要传入设备号和设备名称

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 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;
}

设备注销就只需要传入设备号和名称就行,注销函数会根据设备号清除相关结构体

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
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("zuozhongkai");

三、应用程序编写

编写 main 函数,根据传入参数进行驱动代码的测试,main 函数的参数也叫做命令参数,在 linux 下敲命令的时候就有用了,argv 指的是 char * argc[] 的大小 char * argc[]是一个指针数组,里面存放的是各个传入字符串的首地址,测试代码如下:

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 file %s\r\n", filename);
        return -1;
    }

    if (atoi(argv[2]) == 1)
    { /* 从驱动文件读取数据 */
        retvalue = read(fd, readbuf, 50);
        if (retvalue < 0)
        {
            printf("read file %s failed!\r\n", filename);
        }
        else
        {
            /*  读取成功,打印出读取成功的数据 */
            printf("read data:%s\r\n", readbuf);
        }
    }

    if (atoi(argv[2]) == 2)
    {
        /* 向设备驱动写数据 */
        memcpy(writebuf, usrdata, 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 file %s\r\n", filename);
        return -1;
    }

    return 0;
}

这段应用程序就是简单的传入设备文件名称,然后再根据后续参数进行读或者写的操作。

四、程序编译

驱动程序使用 makefile 用交叉工具链进行编译:

KERNELDIR := /home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := char_drive.o

build: kernel_modules

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

KERNELDIR 为 Linux 内核目录,编译指令:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

编译完成,得到 ko 文件可用于内核加载:

image-20220923231353917

然后直接在 linux 用交叉工具链将测试程序编译链接成可执行文件:

arm-linux-gnueabihf-gcc char_device_app.c -o test_app

image-20220923231605368

模块编译成功后,创建内核模块目录:/lib/modules/4.1.15,其中 4.1.15 是内核版本,可以通过 uname -r 查询。

复制 ko 文件和测试程序到文件系统目录下面,加载 ko 模块:

image-20220925152054083

加载指令:

insmod char_drive.ko

然后使用 test_app 进行测试就行

Logo

更多推荐