I.MX6U嵌入式Linux驱动开发(1)chrdevbase 字符设备驱动开发实验

正点原子Linux驱动开发笔记

字符设备就是按照字节流进行读写操作的设备,例如:点灯,按键,IIC等。
Linux下的应用程序是如何调用驱动程序的?
在这里插入图片描述
例如:/dev/led。
应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。
如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。

在Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。

1、字符设备驱动开发步骤

重点是学习其驱动框架。

1.1、驱动模块的加载和卸载模板

Linux驱动有2种运行方式。
第一种:将驱动编译进 Linux 内核中,当 Linux 内核启动的时候就会自动运行驱动程序。
第二种:将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。

(1)在Ubuntu中创建1_chrdevbase子目录:

cd /home/yang/linux/IMX6ULL/
mkdir Linux_Drivers
cd Linux_Drivers
mkdir 1_chrdevbase

打开VSCode,新建工程,新建chrdevbase.c文件。
在Window下用VSCode打开编译过的Linux内核源码,搜索”module_init()“函数,打开其中一个文件,将其复制到chrdevbase.c文件。可以得到如下的框架:

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

 /* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	/* 出口函数具体内容 */
}

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

将xxx修改为我们的文件名字:chrdevbase。
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数。
添加内核头文件:

#include <linux/module.h>

(2)添加头文件路径
将资料中例程源码中的这个实验代码打开,将.vscode文件拷贝过来。修改路径。打开文件 c_cpp_properties.json,将前面自己移植后的Linux内核所在路径添加进去,分别是开发板所使用的Linux 源码下的 include、arch/arm/include 和 arch/arm/include/generated 这三个目录的路径。
注意:也可以使用资料中移植后的Linux内核源码。

"/home/yang/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include", 
"/home/yang/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include", 
"/home/yang/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"

(3)编译文件
在这个工程中新建一个Makefile文件

KERNELDIR := /home/yang/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek //开发板所使用的 Linux 内核源码目录,使用绝对路径.
CURRENT_PATH := $(shell pwd)//当前路径,直接通过运行“pwd”命令来获取当前所处路径.
obj-m := chrdevbase.o//obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块

build: kernel_modules

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

打开终端,输入一下命令,编译生成 .ko 文件。

make clean
make

(4)通过SecureCRT操作移植好的Linux系统
准备的文件:uboot.bin、zImage、.dtb文件、根文件系统。
可以使用我们自己移植后的这四个文件,也可以使用资料中的。我们以资料中的为例:
将压缩包linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek.tar,解压,然后运行.sh文件,编译,得到zImage和.dtb文件,将这两个文件放入tftp目录中。
将uboot.bin使用imxdownload烧录到SD卡中,启动系统;
在uboot模式下,设置bootcmd和boottargs变量:
为了方便测试,Linux 系统选择通过 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统

=> setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.217.129:/home/yang/linux/nfs/rootfs,proto=tcp rw ip=192.168.217.120:192.168.217.129:192.168.217.1:255.255.255.0::eth0:off' //设置 bootargs

=> setenv bootcmd 'tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000'
=> saveenv //保存环境变量

在SecureCRT中创建“/lib/modules/4.1.15”这个目录,用来存放驱动模块。“/lib/modules”是通用的,不管你用的什么板子、什么内核,这部分是一样的。不一样的是后面的“4.1.15”,这里要根据你所使用的 Linux 内核版本来设置。

通过NFS将根文件系统(rootfs目录)挂载,所以将chrdevbase.ko文件复制到rootfs/lib/modules/4.1.15,在Ubuntu中的VSCode的终端里面使用如下命令:

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

(5)通过SecureCRT操作模块加载与卸载
进入/lib/modules/4.1.15目录中输入:

modprobe xxx.ko  //加载模块
rmmod xxx.ko     //卸载模块

modprobe 命令默认会到/lib/modules/4.1.15 这个目录中查找相应的驱动模块。

1.2、字符设备注册与注销

(1)字符设备注册与注销的源码:
可以去Linux内核源码里面查找。可以仿照去使用。

static int __init chrdevbase_init(void)
{
	static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
}
static void __exit chrdevbase_exit(void)
{
	static inline void unregister_chrdev(unsigned int major, const char *name);
}

这个函数中需要设备号、设备名称,在定义设备号之前,我们需要查找这个号是否被用了。使用如下命令:

/ # cat /proc/devices

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

(2)设备号的分配
第一种:静态分配。直接设置
第二种:动态分配。在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号。
设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
#define CHRDEVBASE_MAJOR 200
#define CHRDEVBASE_NAME  "chrdevbase"
ret = register_chrdev(200, "chrdevbase", &test_fops);
ret = unregister_chrdev(200, "chrdevbase");

1.3、实现设备的具体操作函数

对file_operations结构体类型的变量chrdevbase_fops,进行初始化。常用的就4种,打开、关闭、读、写。
这些函数的实现,也是仿照Linux内核源码中的来实现的。

static const struct file_operations chrdevbase_fops={
	.owner = THIS_MODULE,
	.open = chrdevbase_open,
	.release = chrdevbase_close,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
};

1.3.1、能够对 chrdevbase进行打开和关闭操作

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	printk("chrdevbase_open\r\n");//用来测试这个函数是否被调用
	return 0;
}

static int chrdevbase_close(struct inode *inode, struct file *filp)
{
	printk("chrdevbase_close\r\n");
	return 0;
}

1.3.2、能够对 chrdevbase进行读写操作

假设 chrdevbase这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函数对 chrdevbase 的缓冲区进行读写操作。

static ssize_t chrdevbase_read(struct file *filp, char __user *buf,size_t count, loff_t *ppos)
{	
	printk("kernel send data ok!\r\n");	
	return 0;
}

static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{
	
	printk("kernel receive data:%s\r\n");
	return 0;
}

2、编写chrdevbase.c中的程序

2.1、从设备读取数据

这个函数是:读数据,读取设备中的数据,然后将读取的数据发送给用户空间。
过程:先将要读取的设备中的数据(kerneldata[])拷贝到数据缓冲区(readbuf[100]),然后调用函数copy_to_user()(来完成内核空间的数据到用户空间的复制),将数据缓冲区的数据发送到用户空间的数据缓冲区(buf[])。

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

/* filp	: 要打开的设备文件(文件描述符)
 * buf	: 返回给用户空间的数据缓冲区
 * count: 要读取的数据长度
 * ppos	: 相对于文件首地址的偏移
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf,size_t count, loff_t *ppos)
{
	int ret = 0;

	memcpy(readbuf, kerneldata,sizeof(kerneldata));
	ret = copy_to_user(buf, readbuf, count);
	if(ret == 0){
		printk("kernel send data ok!\r\n");
	}else{
		printk("kernel send data failed!\r\n");
	}
	
	return 0;
}

2.2、向设备写数据

这个函数是:向设备写入数据,接收用户空间的数据,传递给内核,内核空间打印出来。
过程:调用函数copy_from_user()(buf 中的数据复制到写缓冲区 writebuf 中,因为用户空间内存不能直接访问内核空间的内存),接收用户空间传递给内核的数据并且打印出来。

static char writebuf[100]; /* 写缓冲区 */

/* filp	: 要打开的设备文件(文件描述符)
 * buf	: 要写给设备写入的数据,也是用户空间的内存
 * count: 要写入的数据长度
 * ppos	: 相对于文件首地址的偏移
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{
	int ret = 0;
	ret = copy_from_user(writebuf, buf, count);
	if(ret == 0){
		printk("kernel receive data:%s\r\n",writebuf);
	}else{
		printk("kernel recevdata failed!\r\n");
	}
	
	return 0;
}

3、编写测试APP

就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数。open、read、write 和 close 这四个函数。
可以在Ubuntu中借助于man命令查看这几个函数的用法。

man 1 ls        //1表示命令
man 2 open      //2表示系统调用
man 2 write
man 2 read
man 2 close
man 3 printf

测试 APP 运行在用户空间。测试 APP 很简单通过输入相应的指令来对 chrdevbase 设备执行读或者写操作。
在1_chrdevbase 目录中新建 chrdevbaseApp.c 文件。

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

/*
 * @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];
	
	/* 测试的参数为3个,
	比如要从设备中读取数据,需要输入3个参数 “./chrdevbaseApp”、“/dev/chrdevbase”和“1”
	这三个参数分别对应 argv[0]、argv[1]和 argv[2] 。
	第一个参数表示运行 chrdevbaseAPP 这个软件;
	第二个参数表示测试APP要打开/dev/chrdevbase这个设备;
	第三个参数就是要执行的操作,1表示从chrdevbase中读取数据,2 表示向 chrdevbase 写数据。
	*/
	if(argc != 3){ //
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];//获取要打开的设备文件名字,argv[1]保存着设备名字

	/* 打开驱动文件 */
	fd  = open(filename, O_RDWR);//调用 C 库中的 open 函数打开设备文件:/dev/chrdevbase。
	if(fd < 0){
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

	/* 因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字 */
	if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
		retvalue = read(fd, readbuf, 50);/* 一共读取 50 字节的数据,读取到的数据保存在 readbuf 中 */
		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、编译驱动程序和测试 APP

4.1、编译驱动程序

将chrdevbase.c文件编译为.ko模块。编写完之后,输入“make”或“make -j12”命令编译驱动模块。

4.2、编译测试 APP

测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 来编译

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

编译完成以后会生成一个叫做 chrdevbaseAPP 的可执行程序。

4.3、运行测试

4.3.1、加载驱动模块

在Ubuntu中,将编译好的chrdevbase.ko 和 chrdevbaseAPP 复制到 rootfs/lib/modules/4.1.15 目录中。

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

在SecureCRT中进入/lib/modules/4.1.15 中,查看这两个文件

/lib/modules/4.1.15 # ls
/lib/modules/4.1.15 # modprobe chrdevbase.ko //加载 chrdevbase.ko 驱动文件
chrdevbase init!
/lib/modules/4.1.15 # lsmod   //当前系统中存在的模块
/lib/modules/4.1.15 # cat /proc/devices //查看当前系统中有没有 chrdevbase 这个设备

4.3.2、创建设备节点文件

驱动加载成功之后,需要在/dev 目录下创建一个与之对应的设备节点文件。

/lib/modules/4.1.15 # mknod /dev/chrdevbase c 200 0

“mknod”是创建节点命令,
“/dev/chrdevbase”是要创建的节点文件,
“c”表示这是个字符设备,
“200”是设备的主设备号,
“0”是设备的次设备号。

创建完成以后就会存在/dev/chrdevbase 这个文件。

/lib/modules/4.1.15 # ls /dev/chrdevbase -l  

如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。

4.3.3、chrdevbase 设备操作测试

使用 chrdevbaseApp 软件操作 chrdevbase 这个设备。

/lib/modules/4.1.15 # ./chrdevbaseAPP /dev/chrdevbase 1   //读操作
kernel send data ok!    //驱动程序中chrdevbase_read函数输出的信息
read data:kernel data!  //测试APP中输出的接收到的数据:kernel data
/lib/modules/4.1.15 # ./chrdevbaseAPP /dev/chrdevbase 2  //写操作
kernel receive data:usr data!

分析:
chrdevbaseAPP 使用 read 函数从 chrdevbase 设备读取数据,因此 chrdevbase_read 函数就会执行。chrdevbase_read 函数向 chrdevbaseAPP 发送“kernel data!”数据,chrdevbaseAPP 接收到以后就打印出来。

chrdevbaseAPP 使用 write 函数向 chrdevbase 设备写入数据“usr data!”。chrdevbase_write 函数接收到以后将其打印出来。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值