字符设备驱动开发框架

一、创建工程

​ 创建驱动文件夹,然后创建 VSCode 工程,创建 xx.c 驱动源码文件,xx为驱动名。一般编写驱动的框架都是参考 linux 内核写好的驱动。

1、添加头文件路径

​ 因为是编写 linux 驱动,因此会用到 linux 源码中的函数。我们需要在 VSCode 中添加 linux 源码中的头文件路径。按下 “Ctrl + Shift + P” 打开控制台,然后输入 “C/C++: Edit configurations(JSON)”,打开C/C++编辑配置文件,打开以后会自动在 .vscode 文件下生成一个名为 c_cpp_properties.json 的文件,然后添加 linux 源码的头文件路径如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/liuzhikai/linux/kernel/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": "clang-x64"
        }
    ],
    "version": 4
}

2、编写加载和卸载注册函数

​ linux 驱动有两种运行方式,一是编译进 linux 内核中,内核启动时自动运行驱动程序;二是将驱动编译成模块(模块扩展名为 xx.ko),然后内核启动后才用命令加载驱动模块。模块有加载卸载两种操作,加载和卸载注册函数如下:

module_init(chardevbase_init);   //注册模块加载函数
module_exit(chardevbase_exit);   //注册模块卸载函数

​ 参数 chardevbase_init 和 chardevbase_exit 是需要注册的具体函数,加载驱动的时候就会调用 chardevbase_init ,卸载驱动的时候会调用 chardevbase_exit 函数。两个函数的模板如下:

#define DEVICE_NAME "chrdevbase"
#define MAJOR_NUMBER 200

/* 驱动入口函数,加载驱动时调用 */
static int __init chardevbase_init(void)
{
	int retvalue = 0; //注册字符设备函数返回值
	printk("chardevbase_init\r\n"); //打印进入驱动注册信息
	retvalue = register_chrdev(MAJOR_NUMBER, DEVICE_NAME, &chrdevbase_fops); //实际的字符设备注册函数
	if(retvalue < 0) //如果字符注册失败
	{
		return -1;
	}
	return 0;
}
  • 第 1 行定义了驱动名:chrdevbase

  • 第 2 行定义了主设备号

  • 第 7 行 retvalue 用于存储注册函数的返回值,小于 0 则注册失败。

  • 第 8 行打印调试信息,在终端提示成功进入驱动加载函数,linux 下使用的是 printk 函数,使用 man printk 命令可以查看该函数需要包含哪些头文件。

  • 第 9 行是实际的注册函数,该函数的原型为:

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

    major:驱动的主设备号,可以在 linux 系统下用 cat /proc/devices 命令来查看哪些主设备号可用。

    name:驱动设备名称,指向一串字符串。

    fops:结构体 file_operations 类型指针,指向设备的操作函数集合。

/* 驱动出口函数,卸载驱动时调用 */
static void __exit chardevbase_exit(void)
{
	printk("chardevbase_exit\r\n"); //打印进入驱动卸载信息
	unregister_chrdev(MAJOR_NUMBER, DEVICE_NAME); //实际的字符设备注销函数
}
  • 第 5 行是实际的注销函数,该函数原型为:

    static inline void unregister_chrdev(unsigned int major, const char *name)
    

    major:驱动的主设备号,同上注册时使用的主设备号。

    name:驱动设备名称,同上注册时使用的名称。

3、编写设备的具体操作函数

​ 注册函数中 file_operations 类型的结构体定义了实际注册的操作函数,file_operations 结构体有很多成员函数,这些函数不一定全都需要注册,挑选用到的注册即可,各成员函数的模板可以参考 linux 内核的驱动。在注册函数前定义 chrdevbase_fops 结构体和操作函数:

static char readbuf[100]; //读数据缓冲区
static char writebuf[100]; //写数据缓冲区
static char read_data[] = {"data form linux!"}; //应用程序读取的数据

/* 
读操作函数,应用程序向内核空间读数据时调用 
使用到两个入口参数:
buf:将需要读出的数据写入buf中,应用程序读取buf即可
count:一次读操作读取的字节数,应用程序指定
*/
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
	int ret = 0; //拷贝函数返回值
	memcpy(readbuf, read_data, sizeof(read_data)); //将需要读取的数据存进读缓冲区
    /*
    拷贝函数,不能直接将内核数据传递给用户空间,要使用 copy_to_user 函数
    函数原型:unsigned long copy_to_user(void *to, const void *from, unsigned long n)
    to:用户空间的 buf
    from:内核空间的数据缓冲区
    n:一次拷贝字节数 count
    返回值:0 成功,非零不成功
    */
	ret = copy_to_user(buf, readbuf, count); 
	if(ret == 0){
		
	}
	else{
		
	}
	return 0;
}

/* 
写操作函数,应用程序向内核空间写数据时调用 
buf:用户空间传递进来的数据
count:一次写入的字节数
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
	int ret = 0; //拷贝函数返回值
    /*
    拷贝函数,从用户空间拷贝数据到内核空间
    函数原型:unsigned long copy_from_user(void *to, const void *from, unsigned long n);
    to:内核空间的数据缓冲区
    from:用户空间的 buf
    n:一次写入的字节数 count
    返回值:0 成功,非零失败
    */
	ret = copy_from_user(writebuf, buf, count);
	if(ret == 0){
		printk("the recieve data: %s\r\n", writebuf); //打印接收到的数据
	}
	else{
		
	}
	return 0;
}

/*
open 函数,驱动打开后调用
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
close 函数,驱动结束后调用
*/
static int chrdevbase_close(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
操作函数注册结构体
*/
static const struct file_operations chrdevbase_fops = {
	.owner	= THIS_MODULE,
	.read	= chrdevbase_read,
	.write	= chrdevbase_write,
	.open	= chrdevbase_open,
	.release= chrdevbase_close,
};

4、添加头文件

​ 参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
​ man 命令数字含义:1:标准命令 2:系统调用 3:库函数
​ 添加以下头文件:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>		//两个拷贝函数所需头文件

5、添加 License 和作者信息

​ 驱动的 License 是必须的,缺少的话会报错,在文件最末端添加以下代码:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzk");

二、编写测试应用程序

​ 在驱动文件夹下创建 chrdevbaseAPP.c 文件:

#include <sys/types.h>		// open 函数所需头文件
#include <sys/stat.h>		//open 函数所需头文件
#include <fcntl.h>			//open 函数所需头文件
#include <stdio.h>           //printf 函数所需头文件
#include <unistd.h>			//read write close 函数所需头文件
#include <string.h>			//memcpy 函数所需头文件
#include <stdlib.h>			//atoi 函数所需头文件

/*
应用程序使用 arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP来编译,生成可执行文件
执行应用程序时使用命令行:./chrdevbaseAPP /dev/chrdevbaseAPP 1
命令信息保存在 main 函数的两个入口参数中
argc:为命令参数个数,为 3
argv:为命令各个参数的具体内容:
argv[0]:打开应用程序
argv[1]:open读取的文件,驱动文件都在/dev目录下
argv[2]:定义读写功能,1 为读 ,2为写
命令所带参数可自行定义
*/
int main(int argc, char *argv[])
{
    int fd = 0; //文件描述符,读取文件之前要用open函数打开文件,打开成功后得到文件描述符
    int ret = 0; //read write函数返回值
    char *filename; //open函数读取的文件,为argv[2]
    char readbuf[100]; //读数据缓冲区
    char writebuf[100]; //写数据缓冲区
    char write_data[] = {"Hallo!"}; //写入内核空间的数据

    if(argc != 3) //如果命令参数不等于3,表明输入命令格式不对
    {
        printf("missing parameter!\r\n");
        return -1;
    }

    filename = argv[1]; //获取驱动文件名
    fd = open(filename , O_RDWR); //打开驱动文件,O_RDWR表明读写模式(man 2 open 查看具体)
    if(fd < 0) //如果文件描述符小于0,则表明打开文件失败
    {
        printf("open file %s failed\r\n", filename);
        return -1;
    }
	
    /*
    读操作
    命令行传递的参数均为字符,atoi函数把字符数字转化成整型
    */
    if(atoi(argv[2]) == 1) 
    {
        ret = read(fd, readbuf, 50); //read函数参数1为文件描述符,参数2为读缓冲区,参数3为一次读字节数
        if(ret < 0) //返回值小于0失败
        {
            printf("read file %s failed\r\n", filename);
            return -1;
        }
        else
        {
            printf("read data: %s\r\n", readbuf); //读取成功打印出数据
        }
        
    }
    
    /*
    写操作
    命令行传递的参数均为字符,atoi函数把字符数字转化成整型
    */
    if(atoi(argv[2]) == 2)
    {
        memcpy(writebuf, write_data, sizeof(write_data)); //将写数据拷贝到写入缓冲区
        ret = write(fd, writebuf, 50); //参数1为文件描述符,参数2为写数据缓冲区,参数3为一次写入字节数
        if(ret < 0) //返回值小于0写入失败
        {
            printf("write file %s failed\r\n", filename);
            return -1;
        }
    }
    
    ret = close(fd); //close函数只有一个参数,为文件描述符
    if(ret < 0) //返回值小于0关闭文件失败
    {
        printf("close file %s failed\r\n", filename);
        return -1;
    }

}

三、编译和测试

1、编写Makefile,编译驱动程序

​ 驱动程序源码需要编译成.ko模块,创建Makefile:

KERNELDIR := /home/liuzhikai/linux/kernel/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) clean
  • 第1行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径。

  • 第2行,CURRENT_PATH 表示当前路径。

  • 第3行,obj-m 表示将 chrdevbase.c 这个文件编译为模块。

  • 第5行,默认目标为 kernel_modules。

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

    编译成功以后会生成一个 chrdevbase.ko 文件,这个文件就是驱动模块。

2、编译驱动程序

​ 测试 APP 要在 ARM 开发板上运行,所以使用交叉编译器编译:

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

​ 编译完成生成 chrdevbaseAPP 的可执行程序。

3、运行测试

​ linux 系统选择网络启动,并且用 nfs 挂载根文件系统,U-Boot 设置如下:
​ bootcmd 的值:

tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000

​ bootargs 的值:

console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.138:/home/liuzhikai/linux/nfs/rootfs ip=192.168.1.123:192.168.1.138:192.168.1.2:255.255.255.0::eth0:off

​ 将 chrdevbase.kochrdevbaseAPP 拷贝到以下目录(不存在的目录创建):

sudo cp chrdevbase.ko chrdevbaseApp /home/liuzhikai/linux/nfs/rootfs/lib/modules/4.1.15/ -f

​ 使用如下命令加载驱动模块:

depmod
modprobe chrdevbase.ko  //加载驱动模块
lsmod  //查看当前系统中存在的模块
cat /proc/devices  //查看系统中有没有chrdevbase这个设备
mknod /dev/chrdevbase c 200 0 //创建设备节点,c表示设备是字符设备,200表示主设备号,0表示次设备号
ls /dev/chrdevbsae  //查看是否创建节点成功
./chrdevbaseAPP /dev/chrdevbase 1  //执行应用程序
rmmod chrdevbase.ko  //卸载驱动模块
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值