Linux 字符设备驱动-基于正点原子IMX6ULL开发板

1、字符设备驱动简介

     字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节
流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC SPI
LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

Linux
应用程序对驱动程序的调用如图 40.1.1 所示:


2 字符设备驱动开发步骤

2.1 驱动模块的加载和卸载

 

module_init(xxx_init); //注册模块加载函数

module_exit(xxx_exit); //注册模块卸载函数


2.2 字符设备注册与注销

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

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

 

2.3 实现设备的具体操作函数

/* 打开设备 */
static int chrtest_open(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 chrtest_release(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
 }


static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
 };


 /* 驱动入口函数 */
 static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;

/* 注册字符设备驱动 */
retvalue = register_chrdev(200, "chrtest", &test_fops);

if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
}
return 0;
}

 /* 驱动出口函数 */
 static void __exit xxx_exit(void)
 {
/* 注销字符设备驱动 */
unregister_chrdev(200, "chrtest");
 }

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


2.4 添加 LICENSE 和作者信息
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息



 

3 Linux 设备号

3.1 设备号的组成

为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分

组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。

Linux 提供了 一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095。

3.2 设备号的分配

1、静态分配设备号

使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。

2、动态分配设备号

Linux 社区推荐使用动态分配设备号,在注册字 符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。 卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)


4 chrdevbase 字符设备驱动开发实验

4.1 实验程序编写

1、创建 VSCode 工程
2、添加头文件路径

因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux
源码中的头文件路径。打开 VSCode ,按下“ Crtl+Shift+P ”打开 VSCode 的控制台,然后输入
C/C++: Edit configurations(JSON) ”,打开 C/C++ 编辑配置文件:

打开以后会自动在
.vscode 目录下生成一个名为 c_cpp_properties.json 的文件,需要将 Linux 源码里面的头文件路径添加进来,添加头文件路径以后的 c_cpp_properties.json的文件内容如下所示:
 
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include", 
                "/home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include", 
                "/home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}


3、编写实验程序

工程建立好以后就可以开始编写驱动程序了,新建 chrdevbase.c,然后在里面输入如下内容:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.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)
{
	// 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;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */

static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
	int ret = 0;
	// printk("chrdevbase_read\r\n");
	memcpy(readbuf, kerneldata, sizeof(kerneldata));

	ret = copy_to_user(buf, readbuf, count);
	if (ret == 0)
	{
	}
	else
	{
	}
	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 count, loff_t *ppos)
{
	int ret = 0;
	// printk("chrdevbase_write\r\n");
	ret = copy_from_user(writebuf, buf, count);
	if (ret == 0)
	{
		printk("kernel recevdata:%s\r\n", writebuf);
	}
	else
	{
	}
	return 0;
}

/*
 * 字符设备,操作集合
 */

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

};

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	int ret = 0;
	printk("chrdevbase_init\r\n");
	/*注册字符设备*/
	ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
						  &chrdevbase_fops);
	if (ret < 0)
	{
		printk("chrdevbase init failed!\r\n");
	}

	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{

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

/* 
 * 将上面两个函数指定为驱动的入口和出口函数 
 */
module_init(chrdevbase_init); /*入口*/
module_exit(chrdevbase_exit); /*出口*/
/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("supersmart");


 



4.2 编写测试 APP

1
C 库文件操作基本函数
①、open 函数
int open(const char *pathname, int flags)
②、
read 函数
ssize_t read(int fd, void *buf, size_t count)
③、
write 函数
ssize
t write(int fd, const void *buf, sizet count);
④、
close 函数
int close(int fd);
2
、编写测试 APP 程序
 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

/*
 *argc:应用程序参数个数
 * argv[]:具体的参数内容,字符串形式
 * ./chrdevbaseAPP <filename> <1:2> 1 表示读,2表示写
 * ./chrdevbaseAPP /dev/chrdevbase 1 表示从驱动中读数据
 * ./chrdevbaseAPP /dev/chrdevbase 2 表示向驱动中写数据
 * */

int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = 0;
    char *filename;
    char readbuf[100], writebuf[100];
    static char usrdata[] = {"usr data"};

    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)
    {
        /*read*/
        ret = read(fd, readbuf, 50);
        if (ret < 0)
        {
            printf("read file %s failed!\r\n", filename);
        }
        else
        {
            printf("APP read data:%s\r\n", readbuf);
        }
    }

    /*写*/
    if (atoi(argv[2]) == 2)
    {
        /*write*/
        memcpy(writebuf, usrdata, sizeof(usrdata));
        ret = write(fd, writebuf, 50);
        if (ret < 0)
        {
            printf("write file %s failed!\r\n", filename);
        }
        else
        {
        }
    }

    /*close*/
    ret = close(fd);
    if (ret < 0)
    {
        printf("close file %s failed!\r\n", filename);
    }
    else
    {
    }

    return 0;
}
4.3 编译驱动程序和测试 APP

1
、编译驱动程序
首先编译驱动程序,也就是 chrdevbase.c 这个文件,我们需要将其编译为.ko 模块,创建
Makefile 文件,然后在其中输入如下内容:
 
KERNELDIR := /home/znn/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PAHT := $(shell pwd)
obj-m := chrdevbase.o

build :kernel_modules

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

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PAHT) clean
Makefile 编写好以后输入“ make ”命令编译驱动模块,编译过程如图 所示:


 

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


2、编译测试 APP

因为测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 交叉编译器 来编译,
输入如下命令:
arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP
编译完成以后会生成一个叫做 chrdevbaseAPP  的可执行程序

4.4 运行测试

1
、加载驱动模块
将 chrdevbase.ko 和
chrdevbaseAPP 复制到 rootfs/lib/modules/4.1.15 目录中,命令如下:

sudo cp chrdevbase.ko chrdevbaseAPP /home/znn/linux/nfs/rootfs/lib/modules/4.1.15/ -f
拷 贝 完 成 以 后 就 会 在 开 发 板 的 /lib/modules/4.1.15 目 录 下 存 在 chrdevbase.ko
chrdevbaseAPP 这两个文件。
 


 使用 modprobe 加载 chrdevbase.ko。

查看当前系统中有没有 chrdevbase 这个设备


 

 2、创建设备节点文件

驱动加载成功需要在 /dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操
作这个设备节点文件来完成对具体设备的操作。
 
输入如下命令创建 /dev/chrdevbase 这个设备节点文件:
mknod /dev/chrdevbase c 200 0
创建完成以后就会存在 /dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看


如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对 /dev/chrdevbase 进行读写操作即可。

3
chrdevbase 设备操作测试
使用 chrdevbaseApp 对   chrdevbase 这 个设备进行读操作:
chrdevbaseAPP 使用 read 函数从 chrdevbase 设备读取数 据,因此 chrdevbase_read 函数就会执行。 chrdevbase_read 函数向 chrdevbaseAPP 发送“ kernel data!”数据, chrdevbaseAPP 接收到以后就打印出来,“ read data:kernel data! ”就是 chrdevbaseAPP 打印出来的接收到的数据。

 
接下来测试对 chrdevbase 设备的写操作:
 
chrdevbaseAPP 使用 write 函数向 chrdevbase 设备写入数据“ usr data! ”。 chrdevbase_write 函数接 收到以后将其打印出来。


4、卸载驱动模块
使用 rmmod卸载驱动模块



卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在

总结:通过以上虚拟的 chrdevbase 设备为例,学习了字符设备驱动的开发步骤,掌握了字符设备驱动的开发框架以及测试方法,

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值