嵌入式LINUX驱动开发入门之hello驱动(基于IMX6ULL-MINI开发板)

发布于:2025-02-11 ⋅ 阅读:(15) ⋅ 点赞:(0)

1.驱动前提

编译驱动程序之前要编译内核,原因主要是:

(1)驱动程序要用到内核文件:

比如驱动程序中这样包含头文件: #include <asm/io.h>, 其中的asm是一个链接文件,指向asm-arm或asm-mips,这需要先配置、编译内核才会生成asm这个链接文件。

(2)编译驱动时用的内核,开发板上运行到内核,要一致:

开发板上运行到内核是出厂时烧录的,你编译驱动时用的内核是你自己编译的,这两个内核不一致时会导致- -些问题。所以我们编译驱动程序前,要把自己编译出来到内核放到板子上去,替代原来的内核。

(3)更换板子上的内核之后,板子上的其他驱动也要更换

板子使用新编译出来的内核时,板子上原来的其他驱动也要更换为新编译出来的。所以在编译我们自己的第1个驱动程序之前,要先编译内核、模块,并且放到板子上去。

2.编译内核

首先切换到内核所在目录

cd /home/xxl/100ask_imx6ull_mini-sdk/Linux-4.9.88

之后进行下面的操作

make mrproper
make 100ask_imx6ull_mini_defconfig
make zImage -j4

编译后生成的文件如下:

然后进行下面操作,也就是

在arch/arm/boot/dts 目 录 下 生 成 设 备 树 的 二 进 制 文 件 100ask_imx6ull_mini.dtb 。把这 2 文件复制到 /home/book/nfs_rootfs 录下备用
make dtbs
cp arch/arm/boot/zImage ~/nfs_rootfs
cp arch/arm/boot/dts/100ask_imx6ull_mini.dtb ~/nfs_rootfs

3.编译安装内核模块

3.1编译内核模块

首先进入内核源码目录,编译内核模块

cd /home/xxl/100ask_imx6ull_mini-sdk/Linux-4.9.88

 然后输入下面的指令,编译完成如下图

make modules

 

 3.2安装内核模块到Ubuntu某个目录下面备用

可以先把内核模块安装到 nfs 目录 (/home/book/nfs_rootfs)
然后:
cd /home/xxl/100ask_imx6ull_mini-sdk/Linux-4.9.88

 然后执行

make ARCH=arm INSTALL_MOD_PATH=/home/xxl/nfs_rootfs modules_install

 安装好如下图所示:

 4.安装内核和模块到开发板上面

假设:在Ubuntu的/home/book/nfs_ _rootfs 目录下,已经有了zImage、dtb文件,并且有1ib/modules子目录(里面含有各种模块)。

接下来要把这些文件复制到开发板上。假设Ubuntu IP 为192.168.5.11, .在开发板上执行以下命令,首先进行挂载,挂载之前记得先检查是否可以“ping 192.168.5.11”可以通顺然后是每次重启开发板之后都要进行这个挂载操作

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/xxl/nfs_rootfs /mnt

然后执行:

cp /mnt/zImage /boot
cp /mnt/100ask_imx6ull_mini.dtb /root
cp /mnt/lib/modules /lib -rfd
sync

最后重启开发板之后,就可以使用zImage、dtb模块了

5.hello驱动

5.1驱动入门知识

1.首先我们通常都是在Linux的终端上打开一个可执行文件,然后可执行文件就会执行程序。那么这个可执行文件做了什么呢?

2.可执行文件先是在应用层读取程序,其中会有很多库函数,库函数是属于内核之中。而内核又会往下调用驱动层程序。最终驱动层控制具体硬件。

    其实应用程序到库是比较容易理解的,比如我们刚学习C语言的时候,使用了printf,scanf等等这些函数。而这些函数就在库中。
    库可以和系统内核相连接,我们写了一个驱动程序,就需要告诉内核,这个过程叫做注册。我们注册了驱动之后,内核里面就会有这个驱动程序的信息,然后上层应用就可以调用。

3.所以我们只需要知道,需要编写两个程序,一个是驱动层的,一个是应用层的最后驱动层需要注册进入内核,应用层才能够使用。其他的先不要管。

4.我们在应用层调用read函数,对应驱动层的read函数。write函数和write函数对应。open函数和open函数对应。close函数和release函数对应(这个为什么不一样我们也不用管)。

5.我们对 Linux 应用程序对驱动程序的调用流程有一个简单了解之后,我得知道整个程序编写流程应该怎么做。至于流程为什么是这样的,我们记住即可。因为这些都是人规定的,如果之后学的深了再进行深究也不迟,现在我们主要是入门。

5.2整体框架和步骤

1.整体框架流程图

编写驱动主要为以下七个步骤:

  1.     确定主设备号,也可以让内核分配
  2.     定义自己的 file_operations 结构体
  3.     实现对应的 drv_open/drv_read/drv_write 等函数,填入 file operations 结构体
  4.     把 file_operations 结构体告诉内核: register_chrdev
  5.     谁来注册驱动程序啊? 得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
  6.     有入口函数就应该有出口函数: 卸载驱动程序时,出口函数调用unregister_chrdev
  7.     其他完善:提供设备信息,自动创建设备节点: class_create,device_create

5.3hello程序的实现

主要包括三个部分,驱动程序,测试程序,MakeFile文件

(1)驱动程序-hello_drv.c

#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/*确定主设备号*/
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;

#define MIN(a,b)(a<b?a:b)

/*3.实现对应的open/read/write等函数,填入
file_operations结构体*/
//read
static ssize_t hello_drv_read(struct file *file,char _user *buf,
size_t size,loff_t *offset)
{
	int err;
	printk("s line %d\n",_FILE_,_FUNCTION_,_LINE_);
	err = copy_to_user(buf,kernel_buf,MIN(1024,size));
	return MIN(1024,size);
}
//write
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);
}
//open
static int hello_drv_open(struct inode *node,struct file *file)
{
	printk("%s %s line %d\n",_FILE_,_FUNCTION_,_LINE_);
	return 0;
}
//close
static int hello_drv_open(struct inode *node,struct file *file)
{
	printk("%s %s line %d\n",_FILE_,_FUNCTION_,_LINE_);
	return 0;
}

/*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.将file_operations结构体告诉内核:注册驱动程序*/
/*5.需要入口函数,也就是注册驱动程序,安装驱动程序时
就会去调用这个入口函数*/
static int __init hello_init(void)
{
	int err;

	printk("%s %s line %d\n",_FILE_,_FUNCTION_,_LINE_);
	major = register_hrdev(0,"hello",&hello_drv); /*/dev/hello*/
	hello_class = class_create(THIS_MODULE,"hello_class");
	err = PTR_ERR(hello_class);
	if(IS_ERR(hello_class)){
		printk("%s %s %d\n",__FILE__,__FUNCTION__,__LINE__);
		unregister_chrdev(major,"hello");
		return -1;
	}
	device_create(hello_class,NULL,MKDEV(major,0),NULL,"hello");/*/dev/hel*/

	return 0;
}

/*6.出口函数,卸载驱动程序时就会调用这个出口函数*/
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
	device_destroy(hello_class,MKDEV(major,0));
	class_destroy(hello_class);
	unregister_chrdev(major,"hello");
}

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

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

(2)测试程序-hello_drv_test.c

//编写测试程序,实现读写功能

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.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/hello",O_RDWR);
	if(fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}
	/*3.写文件或者读文件*/
	if((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\n",buf);
	}
	close(fd);
	return 0;
}

(3)Makefile文件

  1 
  2 # 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
  3 # 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
  4 # 2.1 ARCH,          比如: export ARCH=arm64
  5 # 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
  6 # 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc    /ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
  7 # 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
  8 #       请参考各开发板的高级用户使用手册
  9 
 10 KERN_DIR = /home/xxl/100ask_imx6ull_mini-sdk/Linux-4.9.88
 11 
 12 all:
 13     make -C $(KERN_DIR) M=`pwd` modules 
 14     $(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c 
 15 
 16 clean:
 17     make -C $(KERN_DIR) M=`pwd` modules clean
 18     rm -rf modules.order
 19     rm -f hello_drv_test
 20 
 21 obj-m   += hello_drv.o

 在make之前要进行以下的检查

也就是对环境变量进行检查(这里以IMX6ULL-MINI为例)

vim ~/.bashrc

在最后三行添加上

export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/xxl/100ask_imx6ull_mini-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin

然后输入下面指令确保生效

source ~/.bashrc

然后就可以输入make进行编译

生成如下:

将这两个文件移动到UBUNTU的挂载目录下

cp *.o hello_drv_test /home/xxl/nfs_rootfs

这时候在开发板进行如下操作

挂载操作

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/xxl/nfs_rootfs /mnt

 然后进行下面的操作即可: