嵌入式开发 | 字符设备驱动开发流程

39 阅读5分钟

驱动程序开发流程,以虚拟字符设备chrdevbase为例

这篇笔记是基于瑞芯微的RK3568开发板,初次尝试字符设备驱动开发,浅浅记录一下开发流程和踩到的坑。

  • 1、在ubuntu下编写.c驱动程序,其中应该指明主设备号和设备名,并且包含open、read、write、release等方法,包含一个设备操作函数结构体,在这个结构体中包含了上述的方法。包含一个驱动入口函数用于注册字符设备驱动,一个驱动出口函数用于注销字符设备驱动。并将这两个函数分别定义为驱动的入口和出口函数。最后需要包含LICENSE和作者信息。
#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编写完整的字符设备驱动模块
chrdevbase 设备有两个缓冲区,一个读缓冲区,
一个写缓冲区,这两个缓冲区的大小都为 100 字节
*/

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

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

/*
* @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 : 要打开的设备文件(文件描述符)
* @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));        //将 kerneldata 数组中的数据拷贝到读缓冲区 readbuf 中
    retvalue = copy_to_user(buf, readbuf, cnt);             //将readbuf 中的数据复制到参数 buf 中(内核空间不能直接操作用户空间的内存)
    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;
    // 接收用户空间传递给内核的数据并且打印
    retvalue = copy_from_user(writebuf, buf, cnt);      //将 buf 中的数据复制到写缓冲区 writebuf 中(因为用户空间内存不能直接访问内核空间的内存)
    if(retvalue == 0)
    {
        printk("kernel recevdata:%s\r\n", writebuf);
    }
    else
    {
        printk("kernel recevdata failed!\r\n");
    }

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

/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
* 应用程序调用close时执行
*/
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,
};

/*
* @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);

/*
* 添加LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FANHUA");
MODULE_INFO(intree, "Y");    //为了欺骗内核,给本驱动添加 intree 标记
  • 2、编写测试APP
    测试APP是linux应用,所以调用的是系统调用和c标准库,在测试APP中调用open、read、write、close等函数,负责对驱动文件的文件操作测试。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

/*
用于测试chrdevbase.c的测试APP
*/
static char usrdata[] = {"usr data!"};  //测试 APP 要向 chrdevbase 设备写入的数据。

int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

    if(argc != 3)       //希望传入3个参数,第一个是./chrdevbaseApp, 第二个是/dev/chrdevbase, 第三个是1或2
    {
        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)              // 借助 atoi 函数将字符串格式的数字转换为真实的数字
    {
        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("close file %s failed\r\n", filename);
        return -1;
    }

    return 0;
}
  • 3、编译驱动程序
    上面写的驱动程序需要现在服务器上编译成.ko文件,然后再复制到开发板的/lib/modules/4.19.232 目录下。这里先创建一个Makefile文件,使用make ARCH=arm64命令编译,程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
KERNELDIR := /home/fanhua/rk3568_linux_sdk/kernel
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

  • 4、编译测试文件
    测试文件使用交叉编译器进行编译:/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc chrdevbaseApp.c -o chrdevbaseAPP,编译完成之后生成一个chrdevbaseAPP文件

  • 5、将编译好的驱动文件和测试文件发送到开发板
    这里使用adb工具,不知道为什么用数据线连接ubuntu上识别不了adb设备,所以选择用网络连接 adb connect 192.168.1.108:5555其中ip地址为开发板的ip地址 然后使用以下命令将文件发送到开发板
    adb push chrdevbase.ko chrdevbaseAPP /lib/modules/4.19.232

  • 6、加载驱动模块 依次使用下面命令: depmod modprobe chrdevbase 使用lsmod查看当前系统的驱动模块 使用cat /proc/devices查看当前系统中的设备

  • 7、创建设备节点文件
    驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。 mknod /dev/chrdevbase c 200 0; c表示字符设备,200是主设备号,0是次设备号

  • 8、卸载驱动模块 rmmod chrdevbase