Linux驱动系列---1.字符设备驱动

驱动开发前提

  • 开发板已经移植好合适的Uboot,Linux内核,rootfs根文件系统。
  • 虚拟机中已经有开发板的Linux内核源码,且编译通过。编译驱动时需要用到。
  • 开发板和主机间可以传输文件,用于转移驱动文件

驱动开发步骤

整体驱动包含如下内容:

  • 加载函数卸载函数注册
  • 注册注销设备
  • 操作函数
    • open
    • release
    • write
    • read
    • 。。。。
  • LICENSE信息
  • 作者信息

驱动模块加载卸载

驱动运行方式有两种:

  • 编译进内核,Linux启动时自动运行驱动
  • 编译成模块,指令加载驱动

调试驱动时一般选择编译成模块。因此就需要加载和卸载驱动函数。

加载卸载函数

在编写驱动时需要在源码中注册这两种操作函数如下:

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

其中xxx_init是需要注册的具体函数,当使用insmod指令加载时,会调用次函数。xxx_exit是需要注册的具体函数,当使用rmmod指令卸载时,此函数调用。函数模板如下:

/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

加载卸载指令

编译好的驱动扩展名为.ko,加载模块指令如下:

  • insmod :简单加载指令,不解决依赖关系。insmod drv.ko
  • modprobe :会分析依赖关系,并加载相应依赖模块。
    卸载指令:
  • rmmod:rmmod drv.ko
  • modprobe -r :modprobe -r drv.ko,会卸载未被占用的依赖模块

字符设备注册,注销

对于字符型设备,加载成功后需要注册字符设备,卸载时需要注销字符设备。注册注销函数原型如下:

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)

register_chrdev函数用于注册设备,参数含义如下:

  • major:主设备号
  • name:设备名
  • fops:操作函数集合
    unregister_chrdev函数用于注销设备,参数含义如下:
  • major:主设备号
  • name:设备名

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块
的出口函数 xxx_exit 中进行。例如:

static struct file_operations test_fops;
/* 驱动入口函数 */
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);

开始定义了操作函数集合结构体test_fops,一般指向所有操作函数集合,包括open,release,write,read等。驱动入口函数调用了注册函数,注册设备号200,设备名chrtest,操作函数集合test_fops。设备号必须未被占用,使用 cat /proc/devices 指令查看当前已用设备号。

操作函数

常见的操作函数有:open,release,read,write等。函数模板如下:

/* 打开设备 */
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);

添加LICENSE和作者信息

后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Turing");

关于设备号

设备号组成

设备号是32位整数类型,其中高12位作为主设备号,低20位做次设备号,因此主设备号范围0~4095。

设备号分配

静态分配:

指令查询已用设备号,选用一个未被占用的设备号。

动态分配设备号

申请函数:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  • dev:保存申请到的设备号
  • baseminor:次设备号起始地址,一般为0
  • count:要申请的设备号数量
  • name:设备名字
    注销字符设备后要释放设备号,函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
  • from:要释放的设备号
  • count:从from开始,要释放的设备号数量

程序编写

创建VScode工程

  • 新建1.chardevbase文件夹
  • 新建chrdevbase.c文件

添加头文件路径

打开vscode,按下“Crtl+Shift+P”打开控制台,输入“C/C++: Edit configurations(JSON) ”打开配置文件,编辑文件如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/book/arm/imx6ull/ebf_6ull_linux/include", 
                "/home/book/arm/imx6ull/ebf_6ull_linux/arch/arm/include", 
                "/home/book/arm/imx6ull/ebf_6ull_linux/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

主要是修改其中的三个路径,必须为开发板内核的绝对路径。

编写驱动程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.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)
{
	//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));
	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;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
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("Turing");

编写测试APP

#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!"};



/*

 * @description		: main主程序

 * @param - argc 	: argv数组元素个数

 * @param - argv 	: 具体参数

 * @return 			: 0 成功;其他 失败

 */

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/book/arm/imx6ull/ebf_6ull_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

编译模块

输入指令:

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

即可编译出.ko文件。

编译测试APP

输入指令:

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

即可编译出chrdevbaseApp可执行文件。

加载模块

将编译好的.ko文件和测试app可执行文件拷贝到开发板目录:/lib/modules/4.1.15-2.1.0+下。输入指令:

insmod chrdevbase.ko

即可加载驱动,此时查看已加载设备号:cat /proc/devices发现有设备号为200的cardevbase设备。

发布了24 篇原创文章 · 获赞 6 · 访问量 3969

猜你喜欢

转载自blog.csdn.net/weixin_43482414/article/details/104689480