正点原子imx6ull-mini第一个字符驱动设备编写(1)

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

1:准备工作

1.1:把上节的模板复制到这里,为了与教程保持移植,将chrbase改名为chardevbase,并复制上一节代码

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static struct file_operations test_fops = {
 .owner = THIS_MODULE, 
 .open = chrtest_open,
 .read = chrtest_read,
 .write = chrtest_write,
 .release = chrtest_release,
 };
 
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
/* 关闭/释放设备 */
 static int chrtest_release(struct inode *inode, struct file *filp)
 {
 /* 用户实现具体功能 */
 return 0;
 }
 
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
/* 用户实现具体功能 */
 return 0;
 }
 
 /* 向设备写数据 */
 static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt,\
loff_t *offt)
 {
 /* 用户实现具体功能 */
 return 0;
 }

/* 驱动入口函数 */
static int __init  chrdevbase_init(void)
{
	int retvalue = 0;

 /* 注册字符设备驱动 */
 retvalue = register_chrdev(200, "chrdevbase", &test_fops);
 if(retvalue < 0){
 	printk("error of chrdevbase_init");
 }
    printk("chrdevbase_init");
    return 0;
}
 
/* 驱动出口函数 */
 static void __exit chrdevbase_exit(void)
 {
	 /* 注销字符设备驱动 */
	 unregister_chrdev(200, "chrdevbase");
     printk("chrdevbase_exit");
 }

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


MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");
MODULE_DESCRIPTION("test modules");

1.2:出现的报错

这里我们make一下,会发现有很多错误。这里是由于结构体成员函数没有声明导致的,也可以把下面的打开关闭读写放在结构体成员赋值前面,这里我们就调换一下顺序

make

make clean
make 

1.3:再次make,可以看见,ko文件成功被编译出来的

至此准备工作完成

2:添加我们这个demo所需的头文件

#include <linux/types.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

3:开始编写字符设备驱动

3.1:设备号及设备名定义

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

3.2:缓冲区大小及默认缓冲区内容定义

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

3.3:打开关闭读写成员函数编写

3.3.1:打开函数编写

chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用, 本例程中我们没有做任何工作,只是输出一串字符,用于调试。这里使用了 printk 来输出信息, 而不是 printf!因为在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf 运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用 printk 这个函数。不同之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级 别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:

用 printk 的 时 候 不 显 式 的 设 置 消 息 级 别 , 那 么 printk 将 会 采 用 默 认 级 别 MESSAGE_LOGLEVEL_DEFAULT,MESSAGE_LOGLEVEL_DEFAULT 默认为 4。

参数 filp 有个叫做 private_data 的成员变量,private_data 是个 void 指针,一般在驱动中将 private_data 指向设备结构体,设备结构体会存放设备的一些属性。

/*
 * @description : 打开设备
 * @param – inode : 传递给驱动的 inode
 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
 * 一般在 open 的时候将 private_data 指向设备结构体。
 * @return : 0 成功;其他 失败
 */

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}

3.3.2:读取函数编写

chrdevbase_read 函数,应用程序调用 read 函数从设备中读取数据的时候此函 数会执行。参数 buf 是用户空间的内存,读取到的数据存储在 buf 中,参数 cnt 是要读取的字节 数,参数 offt 是相对于文件首地址的偏移。kerneldata 里面保存着用户空间要读取的数据,先将 kerneldata 数组中的数据拷贝到读缓冲区 readbuf 中,通过函数 copy_to_user 将 readbuf 中的数据复制到参数 buf 中。因为内核空间不能直接操作用户空间的内存,因此需要借 助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。copy_to_user 函数原型如下: 

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

参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返 回值为 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));
 retvalue = copy_to_user(buf, readbuf, cnt);
 if(retvalue == 0){
 printk("kernel senddata ok!\r\n");
 }else{
 printk("kernel senddata failed!\r\n");
 }
 
 return 0;
 }

3.3.3: 写入函数编写

chrdevbase_write 函数,应用程序调用 write 函数向设备写数据的时候此函数 就会执行。参数 buf 就是应用程序要写入设备的数据,也是用户空间的内存,参数 cnt 是要写入 的数据长度,参数 offt 是相对文件首地址的偏移。通过函数 copy_from_user 将 buf 中的 数据复制到写缓冲区 writebuf 中,因为用户空间内存不能直接访问内核空间的内存,所以需要 借助函数 copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中。

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

3.3.4:关闭函数编写

chrdevbase_release 函数,应用程序调用 close 关闭设备文件的时候此函数会 执行,一般会在此函数里面执行一些释放操作。如果在 open 函数中设置了 filp 的 private_data 成员变量指向设备结构体,那么在 release 函数最终就要释放掉。

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

3.4:入口函数和出口函数

/* 驱动入口函数 */
static int __init  chrdevbase_init(void)
{
	int retvalue = 0;

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

chrbase.c整体

#include <linux/types.h>
 #include <linux/delay.h>
#include <linux/init.h>
 #include <linux/ide.h>
#include <linux/kernel.h>
#include <linux/module.h>

 #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)
{
/* 用户实现具体功能 */
return 0;
}
/* 关闭/释放设备 */
 static int chrdevbase_release(struct inode *inode, struct file *filp)
 {
 /* 用户实现具体功能 */
 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));
	retvalue = copy_to_user(buf, readbuf, cnt);
	if(retvalue == 0){
		printk("kernel senddata ok!\r\n");
	}else{
		printk("kernel senddata failed!\r\n");
	}
	
	return 0;
 }
 
 /* 向设备写数据 */
 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");
			}

			return 0;
 }

static struct file_operations test_fops = {
 .owner = THIS_MODULE, 
 .open = chrdevbase_open,
 .read = chrdevbase_read,
 .write = chrdevbase_write,
 .release = chrdevbase_release,
 };

/* 驱动入口函数 */
static int __init  chrdevbase_init(void)
{
	int retvalue = 0;

 /* 注册字符设备驱动 */
 retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &test_fops);
 if(retvalue < 0){
 	printk("chrdevbase driver register failed\r\n");
 }
 	printk("chrdevbase_init()\r\n");
    return 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("xxx");
MODULE_DESCRIPTION("test modules");

4:编写测试APP

编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如 open、read、write 和 close 这四个函数。

在linux_drivers/chrdevbase目录下新建一个名为chrdevbaseApp.c的文件

4.1:添加头文件

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
 文件名 : chrdevbaseApp.c
 作者 : 左忠凯
 版本 : V1.0
 描述 : chrdevbase 驱测试 APP。
 其他 : 使用方法:./chrdevbaseApp /dev/chrdevbase <1>|<2>
 argv[2] 1:读文件
 argv[2] 2:写文件 
 论坛 : www.openedv.com
 日志 : 初版 V1.0 2019/1/30 左忠凯创建
 ***************************************************************/

4.2:添加用户数据定义

数组 usrdata 是测试 APP 要向 chrdevbase 设备写入的数据。

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

4.3:添加主函数main,调用open、read、write、close函数

先定义返回值fd ,retvalue,如果返回值为负值,证明函数调用失败。

判断运行测试 APP 的时候输入的参数是不是为 3 个,main 函数的 argc 参数表示 参数数量,argv[]保存着具体的参数,如果参数不为 3 个的话就表示测试 APP 用法错误。比如, 现在要从 chrdevbase 设备中读取数据,需要输入如下命令:

/chrdevbaseApp /dev/chrdevbase 1

 上述命令一共有三个参数“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,这三个参数分别 对应 argv[0]、argv[1]和 argv[2]。第一个参数表示运行 chrdevbaseAPP 这个软件,第二个参数表示测试APP要打开/dev/chrdevbase这个设备。第三个参数就是要执行的操作,1表示从chrdevbase 中读取数据,2 表示向 chrdevbase 写数据。

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;
    }
/*获取要打开的设备文件名字,argv[1]保存着设备名字*/
        filename = argv[1]; 
        /*打开驱动文件*/
/*调用 C 库中的 open 函数打开设备文件:/dev/chrdevbase*/
        fd = open(filename,O_RDWR);
        if(fd<0){
            printf("Can't open file %s\r\n",filename);
            return -1;
        }
/*判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式
的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字*/
/*当 argv[2]为 1 的时候表示要从 chrdevbase 设备中读取数据,一共读取 50 字节的
数据,读取到的数据保存在 readbuf 中,读取成功以后就在终端上打印出读取到的数据*/
        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);
                }
        }
/*当 argv[2]为 2 的时候表示要向 chrdevbase 设备写数据*/
        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);
            }
        }
/*对 chrdevbase 设备操作完成以后就关闭设备*/
        retvalue = close(fd);
        if(retvalue<0){
            printf("Can't close file %s\r\n",filename);
            return -1;
        }

        return 0;
}

chrdevbaseApp.c整体

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

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

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;
}

4.4:(补充)open、read、write、close函数

4.4.1:open函数

4.4.2:read函数

4.4.3:write函数

4.4.4:close函数

5:编写Makefile文件

KERNELDIR :=/home/zhulinux/linux/alientek_linux/linux

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

KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根 据自己的实际情况填写即可

CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径

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

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

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

6:编译测试 APP

测试 APP 比较简单,只有一个文件,因此就不需要编写 Makefile 了,直接输入命令编译。 因为测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 来编译, 输入如下命令

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

编译完成以后会生成一个叫做 chrdevbaseApp 的可执行程序,输入如下命令查看 chrdevbaseAPP 这个程序的文件信息

file chrdevbaseApp

 chrdevbaseAPP 这个可执行文件是 32 位 LSB 格式,ARM 版本 的,因此 chrdevbaseAPP 只能在 ARM 芯片下运行。

7:运行测试

7.1:  加载测试

 

7.1.1:挂载

cd /lib/modules/4.1.15/
ls
depmod
modprobe chrdevbase.ok

7.1.2:查看挂载

lsmod

7.1.3:输入如下命令查看当前 系统中有没有 chrdevbase 这个设备:

cat /proc/devices

第200号

7.2:创建设备节点文件

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

mknod /dev/chrdevbase c 200 0

其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个 字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在 /dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看,

ls /dev/chrdevbase -l

如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。 相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。前面一直说 Linux 下 一切皆文件,包括设备也是文件

7.3:chrdevbase 设备操作测试

一切准备就绪,接下来就是“大考”的时刻了。使用 chrdevbaseApp 软件操作 chrdevbase 这 个设备,看看读写是否正常,首先进行读操作,输入如下命令:

./chrdevbaseApp /dev/chrdevbase 1

可以看出,首先输出“kernel senddata ok!”这一行信息,这是驱动程序中 chrdevbase_read 函数输出的信息,因为 chrdevbaseAPP 使用 read 函数从 chrdevbase 设备读取数 据,因此 chrdevbase_read 函数就会执行。chrdevbase_read 函数向 chrdevbaseAPP 发送“kernel data!”数据,chrdevbaseAPP 接收到以后就打印出来,“read data:kernel data!”就是 chrdevbaseAPP 打印出来的接收到的数据。说明对 chrdevbase 的读操作正常,接下来测试对 chrdevbase 设备的 写操作,输入如下命令:

./chrdevbaseApp /dev/chrdevbase 2

只有一行“kernel recevdata:usr data!”,这个是驱动程序中的 chrdevbase_write 函数输出的。 chrdevbaseAPP 使用 write 函数向 chrdevbase 设备写入数据“usr data!”。chrdevbase_write 函数接 收到以后将其打印出来。说明对 chrdevbase 的写操作正常,既然读写都没问题,说明我们编写 的 chrdevbase 驱动是没有问题的。

7.4:卸载驱动模块

如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉 chrdevbase 这 个设备

rmmod chrdevbase.ko

此时系统已经没有任何模块了,chrdevbase 这个模块也不存在了, 说明模块卸载成功。 至此,chrdevbase 这个设备的整个驱动就验证完成了,驱动工作正常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值