编写第一个驱动,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
七、验证hello驱动。
使用make命令把上面驱动生成的.ko文件还有APP文件 通过nfs挂载的方式,传输到开发板上,然后复制到开发板的/lib/modules/4.1.15/下,然后使用insmod命令加载模块。