IMX6ULL学习笔记(三) 字符设备驱动hello_drv

编写第一个驱动,hello_drv

一、获取内核、编译内核。

这里为什么要获取内核呢,因为我们写的是驱动程序,而不是裸机程序。也就是我们的板子已经烧入进去了uboot、内核,根文件。然后我们要在这个板子的内核的基础上,来编写实现我们需要功能的代码,那么也就是说,我们的驱动代码是依赖于我们的内核的,那为什么需要编译我们的内核呢,这里简单点说就是我们的内核是经过编译后,生成了一些文件,然后烧入进去板子的,那么我们的驱动文件是依赖于这份编译后的内核的,那么我们的驱动代码也需要编译,这里的内核你可以理解成,就像51单片机写代码的时候,需要引入reg52.h,等头文件一样。不多说上正文。

获取内核文件

        获取Linux内核文件,可以从Linux Kernel官网下载,这里我使用的是正点原子的mx6ull nand板子,为了跟开发板中的系统一致,避免出现其他问题,所以使用的是linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek,需要的话直接去正点原子liunx开发板A光盘下的例程源码下的03、正点原子Uboot和Linux出厂源码下载就可以了,用韦东山的也可以,不过需改改下网卡驱动口,比较麻烦就不弄了,需要注意的一点是,这里获取的内核,必须和开发板的烧入的内核一样,如果不一样可能出现驱动代码不兼容的情况。

链接:https://pan.baidu.com/s/1f3nxEbToe-FVZc1oBHDw3A?pwd=0106 
提取码:0106

编译内核

在编译之前,要在~/.bashrc文件下添加两行内容,来指定编译的平台和工具链

vim ~/.bashrc

在行尾添加或修改,加上下面几行

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

 然后是打开内核压缩包所在文件夹,如图

 输入如下命令解压文件

 tar -vxjf linux-imx-4.1.15-2.1.0-g3dc0a4b-v2.7.tar.bz2

 然后进入文件中,打开终端,开始编译内核,输入如下命令:

新建一个shell脚本:

touch imx6ull_alientek_nand.sh

 在脚本文件里面添加以下内容:

#!/bin/sh 
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alientek_nand_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

 运行脚本:

./imx6ull_alientek_nand.sh

注意期间要是出现图形化界面配置,直接点击两次Esc退出就可以了,然后等待编译完成。
 

二、建立VScode文件,添加路径。

新建一个文件夹,用于保存我们的驱动文件,也就是工程目录。然后利用VSCode打开。然后在文件夹下面,新建一个.vscode文件夹。

 使用快捷键shift+ctrl+p,进入配置,点击下面这个配置选择,会自动生成c_cpp_properties.josn文件.

 把里面内容换成这个

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/odf/linux-imx/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include", 
                "/home/odf/linux-imx/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include", 
                "/home/odf/linux-imx/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/",
                "/home/odf/linux-imx/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/asm/mach/"              
            ],
            "defines": [],
            "compilerPath": "/usr/bin/clang",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

 注意,这里的/home/odf/linux-imx/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/,也就是你们内核所在的路径,如果你们路径不是这个的话就自己手动改一下。

三、了解字符型驱动的编写步骤.

Linux 下的应用程序是如 何调用驱动程序的,Linux 应用程序对驱动程序的调用如图

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启 动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在

Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译 为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。 而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编 译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进

Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。

今天我们编写的数字符型驱动,步骤大概如下

1.引入头文件

2.定义并创建设备信息结构体

3.定义并创建字符设备的文件操作函数结构体

4.编写对应的字符设备的文件操作函数

5.编写入口函数来注册驱动程序,告诉内核:我来啦。

编写入口函数具体实现:

(1)动态分配设备号使用alloc_chrdev_region()函数或register_chrdev_region()函数动态分配字符设备号。这些函数将为字符设备分配主设备号和次设备号。

(2)初始化字符设备结构体:调用cdev_init()函数来初始化字符设备结构体。该函数需要传入要初始化的struct cdev变量以及指向文件操作函数表的指针。

(3)创建字符设备节点:调用cdev_add()函数将字符设备添加到内核中,在/dev/目录下创建相应的字符设备节点。

(4) 使用 class_create() 函数创建一个设备类,设备类用于在/sys/class目录下创建子目录,以组织同一类设备的相关信息。

(5) 使用 device_create() 函数创建一个设备,并在/dev目录下创建相应的设备节点

6.有入口函数就有出口函数:卸载驱动程序时就会去调用这个出口函数。在模块卸载时,使用 cdev_del() 函数注销字符设备驱动。

(1)使用 unregister_chrdev_region() 函数注销设备号,释放设备号资源。

(2)使用device_destroy销毁设备,删除相应的设备节点

(3)使用class_destroy销毁设备类,释放相关资源

7.调用函数把我们写的出口函数和入口函数告诉内核。

(1)使用module_init(hello_init)把我们编写的入口函数添加进去

(2)使用module_exit(hello_exit)把我们编写的出口函数添加进去

四、编写字符型驱动代码。

1.引入头文件。

这里我们可以仿照下别人的写法,打开内核目录下的drivers/char/1302目录(看名字就猜到该目录下存放应该是字符驱动代码

接下来就是按照我上面的步骤走了。

1.引入头文件

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/major.h>

2.定义并创建设备信息结构体

/* newchrl设备信息结构体 */
struct newchr_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct newchr_dev newchr;

3.定义并创建字符设备的文件操作函数结构体

/* 2. 定义自己的file_operations结构体                                              */

static struct file_operations hello_drv = {
	.owner	 = THIS_MODULE,
	.open    = hello_drv_open,
	.read    = hello_drv_read,
	.write   = hello_drv_write,
	.release = hello_drv_close,
};

4.编写对应的字符设备的文件操作函数

/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */

static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)

{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}

static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)

{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

static int hello_drv_open (struct inode *node, struct file *file)

{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int hello_drv_close (struct inode *node, struct file *file)

{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

5.编写入口函数来注册驱动程序,告诉内核:我来啦。

static int __init hello_init(void)

{

	/* 动态注册字符设备的流程一般如下:
	1.调用 alloc_chrdev_region() 函数申请设备编号。
	2.使用 cdev_init() 函数初始化设备描述结构体。
	3.使用 cdev_add() 函数将设备号与设备描述结构体关联起来,注册字符设备驱动。
	4.使用 class_create() 函数创建一个设备类.
	5.使用 device_create() 函数创建一个设备
	*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/*1 创建设备号
	   根据是否定义了设备号,通过条件判断选择不同的创建方式。

	   如果定义了设备号,则使用MKDEV宏将主设备号和次设备号合成为设备号,并调用register_chrdev_region()函数注册字符设备号。

	   如果没有定义设备号,则使用alloc_chrdev_region()函数动态分配设备号,并通过MAJOR和MINOR宏获取分配得到的主设备号和次设备号。*/
	if (newchr.major) 

	{		/*  定义了设备号 */

		newchr.devid = MKDEV(newchr.major, 0);

		/*register_chrdev_region() 是 Linux 内核中用于向系统申请指定范围内的字符设备编号的函数*/

		register_chrdev_region(newchr.devid, NEWCHR_CNT, NEWCHR_NAME);

	} 
	else 
	{						/* 没有定义设备号 */

		alloc_chrdev_region(&newchr.devid, 0, NEWCHR_CNT, NEWCHR_NAME);	/* 申请设备号 */

		newchr.major = MAJOR(newchr.devid);	/* 获取分配号的主设备号 */

		newchr.minor = MINOR(newchr.devid);	/* 获取分配号的次设备号 */

	}

	/* 2 初始化cdev

	   设置cdev结构体的拥有者为当前模块(THIS_MODULE),然后使用 cdev_init() 函数初始化cdev结构体。

	   参数包括待初始化的cdev结构体和用于操作该设备的file_operations结构体(hello_drv) */

	newchr.cdev.owner = THIS_MODULE;
	cdev_init(&newchr.cdev, &hello_drv);



	/* 3、添加一个cdev */

	cdev_add(&newchr.cdev, newchr.devid, NEWCHR_CNT);


	/*4 创建设备类

	   使用 class_create() 函数创建一个设备类,设备类用于在/sys/class目录下创建子目录,以组织同一类设备的相关信息。

	   该函数的参数包括所属的模块(THIS_MODULE)和设备类的名称(NEWCHR_NAME)。

	   如果创建失败,IS_ERR() 函数将返回true,表示出错,此时使用 PTR_ERR() 函数返回错误码。 */

	newchr.class = class_create(THIS_MODULE, NEWCHR_NAME);
	if (IS_ERR(newchr.class)) {
		return PTR_ERR(newchr.class);
	}



	/*5 创建设备

	   使用 device_create() 函数创建一个设备,并在/dev目录下创建相应的设备节点。

	   参数包括设备所属的类(newchr.class)、父设备(NULL,如果没有父设备)、设备号(newchr.devid)、设备私有数据(NULL,一般为设备驱动程序提供一个指针)和设备名称(NEWCHR_NAME)。

	   如果创建失败,IS_ERR() 函数将返回true,表示出错,此时使用 PTR_ERR() 函数返回错误码。 */

	newchr.device = device_create(newchr.class, NULL, newchr.devid, NULL, NEWCHR_NAME);

	if (IS_ERR(newchr.device)) {
		return PTR_ERR(newchr.device);
	}
	printk("newchr init!\r\n");
	return 0;

}

6.有入口函数就有出口函数:卸载驱动程序时就会去调用这个出口函数。在模块卸载时,使用 cdev_del() 函数注销字符设备驱动。

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */

static void __exit hello_exit(void)

{

	/*在模块卸载时,使用 cdev_del() 函数注销字符设备驱动,并使用 unregister_chrdev_region() 函数释放设备号资源。*/

	/* 注销字符设备驱动 */

	cdev_del(&newchr.cdev);/*  删除cdev */

	unregister_chrdev_region(newchr.devid, NEWCHR_CNT); /* 注销设备号 */



	device_destroy(newchr.class, newchr.devid);// 销毁设备,删除相应的设备节点

	class_destroy(newchr.class);// 销毁设备类,释放相关资源

	printk("hello_drv exit!\r\n");

}

 7.调用函数把我们写的出口函数和入口函数告诉内核。

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否 则的话编译的时候会报错,作者信息可以添加也可以不添加。

/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(hello_init);

module_exit(hello_exit);



MODULE_LICENSE("GPL");

MODULE_AUTHOR("oudafa");

五、编写驱动对应应用代码。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdio.h>

#include <string.h>

#include "stdlib.h"



int main(int argc, char **argv)

{

	int fd;

	char buf[1024];

	int len;

	/* 1. 判断参数 */

	if (argc < 2) 

	{

		printf("Usage: %s -w <string>\n", argv[0]);

		printf("       %s -r\n", argv[0]);

		return -1;

	}

	/* 2. 打开文件 */

	fd = open("/dev/newchr", O_RDWR);

	if (fd == -1)

	{

		printf("can not open file /dev/newchr\n");

		return -1;

	}



	/* 3. 写文件或读文件 */

	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))

	{

		len = strlen(argv[2]) + 1;

		len = len < 1024 ? len : 1024;

		write(fd, argv[2], len);

	}

	else

	{

		len = read(fd, buf, 1024);		

		buf[1023] = '\0';

		printf("APP read : %s\r\n", buf);

	}

	close(fd);

	

	return 0;

}

六、编写Makefile。

KERN_DIR = /home/odf/linux-imx/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o newcharApp newcharApp.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f newcharApp

obj-m	+= newchar.o

使用make命令生成驱动的.ko文件还有APP文件

七、验证hello驱动。

使用cp命令把上面驱动生成的.ko文件还有APP文件复制到我们ubuntu上的根文件系统挂载目录

insmod nwechar.ko
cp newchar.ko newcharApp /home/odf/nfs_rootfs/rootfs/lib/modules/4.1.15/

打开开发板,通过nfs挂载根文件系统的方式,加载到开发板上。

使用insmod命令去加载模块:

insmod newchar.ko

然后使用App程序去测试

./newcharApp /dev/newchar 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值