经过以下四个步骤,终于可以开始驱动开发了
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-
编译内核步骤:(如果中途报错了,自行百度一下就行,网上很多解决办法的,这里就不一一列举了)
make mrproper
成功现象:无内容输出
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
#
make zImage -j4
成功现象:在 内核文件/arch/arm/boot/目录下 生成 zImage 文件,且没有报错
make dtbs
成功现象:输出几行内容,无报错
cp arch/arm/boot/zImage ~/nfs_rootfs
cp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs
make modules
成功现象:输出很多.o文件,最后输出一些.ko文件,无报错
"/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 指定的驱动模块退出函数
以上便是驱动程序实现过程及应用程序调用驱动程序的原理(按照个人理解写的,大概是这么个流程,有不恰当的地方欢迎指出)
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,跳转到该结构体定义处,可以看到每个函数指针的形式
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
发现 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文件,就是加载到内核里的驱动模块文件
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
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]#