【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】

经过以下四个步骤,终于可以开始驱动开发了

01.安装交叉编译环境【附下载地址】
02.IMX6ULL烧写Linux系统
03.设置IMX6ULL开发板与虚拟机在同一网段
04.IMX6ULL开发板与虚拟机互传文件

目录

一、获取内核、编译内核
二、创建vscode工作区,添加内核目录和个人目录
三、了解驱动程序编写流程
四、第一个驱动程序 - hello驱动
五、IMX6ULL验证hello驱动

一、获取内核、编译内核

1、获取内核文件

获取Linux内核文件,可以从Linux Kernel官网下载,我这里为了跟开发板中的系统一致,避免出现其他问题,所以使用的韦东山老师提供的Linux-4.9.88内核文件,需要自取

链接:https://pan.baidu.com/s/111M2FsgJXAPsQ3ppeVwbFQ
提取码:p7wp

2、编译内核文件
为什么要编译内核文件,因为驱动代码的编译要基于编译好的内核文件的
在编译之前,要在~/.bashrc文件下添加两行内容,来指定编译的平台和工具链

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

编译内核步骤:(如果中途报错了,自行百度一下就行,网上很多解决办法的,这里就不一一列举了)

  1. 先删除之前编译所生成的文件和配置文件,备份文件等
make mrproper

成功现象:无内容输出

  1. 设置内核的相关配置
make 100ask_imx6ull_defconfig

成功现象:

HOSTCC scripts/basic/fixdepHOSTCC scripts/kconfig/conf.o
SHIPPED scripts/kconfig/zconf.tab.c
SHIPPED scripts/kconfig/zconf.lex.c
SHIPPED scripts/kconfig/zconf.hash.c
HOSTCCscripts /kconfig/zconf.tab.o
HOSTLDscripts/kconfig/conf
#
#configuration written to .config
#
  1. 生成镜像文件(-j4会快一些,代表使用4个核心一起编译,具体用几个核心根据自己虚拟机具体情况而定)
make zImage -j4

成功现象:在 内核文件/arch/arm/boot/目录下 生成 zImage 文件,且没有报错

  1. 生成设备树(这一步也做一下,很快的,虽然我们用不到,就跟着步骤来嘛)
make dtbs

成功现象:输出几行内容,无报错

  1. 在家目录下新建 nfs_rootfs 目录,将镜像文件、生成的设备树拷贝其目录下 (可不做)
cp arch/arm/boot/zImage ~/nfs_rootfs
cp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs
  1. 编译内核模块
make modules

成功现象:输出很多.o文件,最后输出一些.ko文件,无报错


二、配置vscode,添加内核目录和个人目录

  1. 使用vscode打开linux-4.9.88内核目录,点击左上角 “文件” ,“将工作区另存为”,选择家目录下,随便起个名字
  2. 使用vscode打开自己要编写驱动的文件目录,右键自己的文件夹,选择“ 将文件夹添加到工作区”,选择自己创建的那个工作区
  3. 至此就可以在vscode同一个工作区下看到自己的驱动文件和linux内核文件了,方便后续独照内核文件内容

【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_第1张图片

  1. 在.vscode 下的c_cpp_properties.json 文件中设置工作路径
"/home/me/Linux-4.9.88/tools/virtio",
"/home/me/Linux-4.9.88/include/**",
"/home/me/Linux-4.9.88/include/linux/**",
"/home/me/Linux-4.9.88/arch/arm/include/**",
"/home/me/Linux-4.9.88/arch/arm/include/generated/**"

三、了解驱动程序编写流程

1. 先看内核目录下原有的驱动是怎么写的
打开Linux-4.9.88/drivers/char目录(看名字就猜到该目录下存放应该是字符驱动代码
发现有个 ds1602.c ,打开看看它是怎么写的(因为我学过了,选择这个文件,展示主要代码,有助于入门理解)

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

static DEFINE_MUTEX(ds1620_mutex);
static const char *fan_state[] = { "off", "on", "on (hardwired)" };

.....
..... 省略
.....

static int __init ds1620_init(void)
{
	int ret;
	struct therm th, th_start;

	if (!machine_is_netwinder())
		return -ENODEV;

	ds1620_out(THERM_RESET, 0, 0);
.....
..... 省略
.....

static int ds1620_open(struct inode *inode, struct file *file)
{
	return nonseekable_open(inode, file);
}

.....
..... 省略
.....

static ssize_t ds1620_read(struct file *file, char __user *buf, size_t count, loff_t *ptr)
{
	signed int cur_temp;
	signed char cur_temp_degF;

	cur_temp = cvt_9_to_int(ds1620_in(THERM_READ_TEMP, 9)) >> 1;

	/* convert to Fahrenheit, as per wdt.c */
	cur_temp_degF = (cur_temp * 9) / 5 + 32;

	if (copy_to_user(buf, &cur_temp_degF, 1))
		return -EFAULT;

	return 1;
}


.....
..... 省略
.....

static const struct file_operations ds1620_fops = {
	.owner		= THIS_MODULE,
	.open		= ds1620_open,
	.read		= ds1620_read,
	.unlocked_ioctl	= ds1620_unlocked_ioctl,
	.llseek		= no_llseek,
};

.....
..... 省略
.....

static void __exit ds1620_exit(void)
{
#ifdef THERM_USE_PROC
	remove_proc_entry("therm", NULL);
#endif
	misc_deregister(&ds1620_miscdev);
}

module_init(ds1620_init);
module_exit(ds1620_exit);

MODULE_LICENSE("GPL");

2. 驱动程序主要构成(主要是的前面四个)
· file_operations 结构体 : 为系统调用提供驱动程序入口的结构体
· module_init : 定义驱动模块的入口函数
· module_exit : 定义驱动模块的退出函数
· MODULE_LICENSE(“GPL”) : 声明模块许可证,指明这是GNU General Public License的任意版本,
                                                     否则在加载此模块时,会收到内核被污染 “kernel tainted” 的警告

· ds1620_init :ds1602初始化函数
· ds1620_open : 打开ds1602设备函数
· ds1620_read : 读ds1602设备函数
· ds1620_exit : ds1602退出驱动函数

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

3. 驱动程序实现过程及调用原理
· module_init 指定驱动入口函数
· 设备对象通过 struct file_operations 结构体定义
· 入口函数中调用 register_chrdev 函数,传入注册定义好的驱动设备变量,生成设备号
· 在开发板上通过 insmod 命令将编译好的驱动模块(.ko文件,要从虚拟机传到板子上哦)载入内核
· 通过应用程序可以对设备进行读写等操作,读写等操作通过系统调用 register_chrdev 结构体中指定的驱动模块读写等函数来实现
· close设备时,系统调用 module_exit 指定的驱动模块退出函数

以上便是驱动程序实现过程及应用程序调用驱动程序的原理(按照个人理解写的,大概是这么个流程,有不恰当的地方欢迎指出)

四、第一个驱动程序 - hello驱动

1. 在个人目录下新建 hello_drv.c 文件
2. 照葫芦画瓢,把上面 ds1602 用到的
头文件**都复制过来
3. 继续照葫芦画瓢编写 file_operations 结构体**

static const struct file_operations hello_drv = {
    .owner 		= THIS_MODULE,
	.read		= hello_read,
	.write		= hello_write,
	.open		= hello_open,
	.release    = hello_release,
};

假装我们的驱动也可以读、写,当然也必须有open和release(就是close)
当然 .owner = THIS_MODULE, 也必须有,原因见博客https://blog.csdn.net/a954423389/article/details/6101369

4. 研究file_operations结构体
每个函数都有对应的模板,可不是乱写的,因为这些函数组中都会被系统调用,参数都是固定的,可以按住ctrl键,用鼠标点击file_operations,跳转到该结构体定义处,可以看到每个函数指针的形式
【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_第2张图片

5. 照葫芦画瓢实现hello_read、hello_write、hello_open、hello_release函数

/*养成好习惯,驱动程序都加static修饰*/
static int hello_open (struct inode *node, struct file *filp)
{
	printk("hello_open\n");
	printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t hello_read (struct file *filp, char *buf, size_t size, loff_t *offset)
{
	printk("hello_read\n");
	return size;  //返回读取字节数
}

static ssize_t hello_write (struct file *filp, const char *buf, size_t size, loff_t *offset)
{
	printk("hello_write\n");
	return size;  //返回写入字节数
}

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

6. 编写hello驱动的入口函数、出口函数
入口函数需要用到 register_chrdev 函数
出口函数(退出函数)需要用到 unregister_chrdev 函数,但是 ds1602.c 中没有这两个函数
没关系,我们在vscode中搜索 register_chrdev, 随便点一个看一下,研究一下用法(实在看不懂百度一下哈哈)
也可以用linux命令查找(在内核的drivers/char目录下查找)

grep "register_chrdev" * -nwr

【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_第3张图片
我们用vscode找一下

【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_第4张图片
发现 register_chrdev 函数需要三个参数
第一个参数是主设备号,0代表动态分配
第二个参数是设备的名字(自定义)
第三个参数是struct file_operations结构体类型的指针,代表申请设备的操作函数

同理,出口函数 unregister_chrdev
第一个参数是设备号
第二个参数是设备名称

照葫芦画瓢开始写

/*入口函数*/
static int major;
static int hello_init(void)
{
	/*返回设备号,定义设备名称为hello_drv*/
	major = register_chrdev(0,"hello_drv",&hello_drv);
	return 0;
}

/*退出函数*/
static int hello_exit(void)
{
	unregister_chrdev(major,"hello_drv");
	return 0;
}	

7. module_init 、module_exit和声明许可证

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

整个 hello_drv.c 代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

static int major;

static int hello_open (struct inode *node, struct file *filp)
{
	printk("hello_open\n");
	printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t hello_read (struct file *filp, char *buf, size_t size, loff_t *offset)
{
	printk("hello_read\n");
	return size;  //返回读取字节数
}

static ssize_t hello_write (struct file *filp, const char *buf, size_t size, loff_t *offset)
{
	printk("hello_write\n");
	return size;  //返回写入字节数
}

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

/*1.定义 file_operations 结构体*/
static const struct file_operations hello_drv = {
    .owner 		= THIS_MODULE,
	.read		= hello_read,
	.write		= hello_write,
	.open		= hello_open,
	.release    = hello_release,
};


/*2.register_chrdev*/

/*3.入口函数*/
static int hello_init(void)
{
	//设备号
	major = register_chrdev(0,"hello_drv",&hello_drv);
	return 0;
}

/*4.退出函数*/
static int hello_exit(void)
{
	//卸载驱动
	unregister_chrdev(major,"hello_drv");
	return 0;
}	

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

8. 编写Makefile

不多说,直接上代码

KERN_DIR = /home/me/Linux-4.9.88

PWD ?= $(shell KERN_DIR)

all:	
	make -C $(KERN_DIR) M=$(PWD) modules
	#$(CROSS_COMPILE)gcc -o hello_test hello_test.c
clean:
	make -C $(KERN_DIR) M=$(PWD) modules clean
	rm -rf modules.order
	rm -f hello_drv

obj-m += hello_drv.o

KERN_DIR = /home/me/Linux-4.9.88 : 编译程序的依赖目录

9. make一下,生成.ko文件

me@ubuntu:~/Linux_ARM/IMX6ULL/hello_driver$ ls
hello_drv.c  hello_drv.ko  hello_drv.mod.c  hello_drv.mod.o  hello_drv.o  
hello_test  hello_test.c  Makefile  modules.order  Module.symvers

主要就是.ko文件,就是加载到内核里的驱动模块文件


五、IMX6ULL验证hello驱动

1. 将虚拟机编译生成的.ko文件拷贝到IMX6ULL开发板上
这步操作需要虚拟机和开发板在同一网段下,可以借鉴以下两篇博客
03.设置IMX6ULL开发板与虚拟机在同一网段
04.IMX6ULL开发板与虚拟机互传文件
我采用的是NFS挂载的方式

mount -t nfs -o nolock,vers=3 192.168.1.200:/home/me/Linux_ARM/IMX6ULL/hello_driver /mnt

在这里插入图片描述
如果出现报错

failed: Device or resource busy

执行

unmount /mnt

2.加载内核
执行命令加载内核

insmod hello_drv.ko
[root@100ask:/mnt]# insmod hello_drv.ko 
[   80.794911] hello_drv: loading out-of-tree module taints kernel.
[root@100ask:/mnt]#

显示已载入系统的模块

lsmod

【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_第5张图片
查看 hello_drv.ko 驱动模块的设备号

cat /proc/devices
226 drm
240 hello_drv
241 adxl345
242 spidevx
243 irda
244 dht11

可以看到hello驱动的设备号是240

3.生成设备节点

mknod /dev/hello c 240 0

/dev/hello : 生成设备节点的名称
c :说明是字符设备
240 : 主设备号
0 : 子设备号(不指定子设备号,为0)

这时候查看hello设备也可以用下面的命令

[root@100ask:/mnt]# ls /dev/hello -l
crw-r--r-- 1 root root 240, 0 Jan  1 11:34 /dev/hello

4.编写应用程序验证驱动

PS:在第四节第8小节中,将 $(CROSS_COMPILE)gcc -o hello_test hello_test.c 这一行注释去掉

编写 hello_test.c 应用程序

#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    int len;
    char read_buf[10];

    if(argc < 2){
        printf("please input  at least 2 args\n");
        printf("%s  [string]\n", argv[0]);
        return -1;
    }

    /*open*/
    int fd;
    fd = open(argv[1], O_RDWR);
    if(fd < 0){
        printf("open failed\n");
        return -2;
    }

    /*read*/
    if(argc == 2){
        read(fd, read_buf, 10);    //调用read函数,只为了触发系统调用hello驱动的read函数
        printf("read operation \n");
    }

    /*write*/
    if(argc == 3){
        len = write(fd, argv[2], strlen(argv[2]));   //调用write函数,只为了触发系统调用hello驱动的write函数
        printf("write length = %d \n", len);
    }

    close(fd);

    return 0;
}

该程序的使用方式:

./hello_test   /dev/hello  123abc     两个参数:模拟写操作
./hello_test   /dev/hello             一个参数:模拟读操作

/dev/hello 是我们要打开的设备名称,在应用程序中,使用 open 函数打开,使用 close 函数关闭

如果打开成功,则系统会调用 hello 驱动的 open 函数,我们就会看到相应的打印信息(打印出当前文件名,函数名,行数)

static int hello_open (struct inode *node, struct file *filp)
{
	printk("hello_open\n");
	printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__);
	return 0;
}

注意驱动程序中使用的都是printk函数,该函数是内核调用的,想要在开发板的串口打印信息中看到输出,需要执行以下命令

echo "7 4 1 7" > /proc/sys/kernel/printk

测试驱动写操作,成功!!!

[root@100ask:/mnt]# ./hello_test /dev/hello 
[  499.512588] hello_open
[  499.516872] /home/me/Linux_ARM/IMX6ULL/hello_driver/hello_drv.c hello_open 28
[  499.525082] hello_read
read operation [  499.528427] hello_release

[root@100ask:/mnt]# 

测试驱动读操作,成功!!!

[root@100ask:/mnt]# ./hello_test /dev/hello abc123
[  500.725340] hello_open
[  500.727762] /home/me/Linux_ARM/IMX6ULL/hello_driver/hello_drv.c hello_open 28
[  500.736217] hello_write
write length = 6 [  500.739735] hello_release

[root@100ask:/mnt]# 

【IMX6ULL驱动开发学习】05.IMX6ULL驱动开发_编写第一个hello驱动【熬夜肝】_第6张图片

你可能感兴趣的:(IMX6ULL,驱动开发,linux驱动,第一个驱动程序,IMX6ULL,insmod)