目录
前言:
一、背景
二、驱动程序编写流程
1.APP打开的文件在内核中如何表示?
2.编写驱动程序的流程
三、hello驱动程序实战
hello_drive.c
hello_drive_test.c
最终测试:
a.首先编译内核(如果没有编译过)
b.设置交叉编译工具链
c.编写makefile
d.上机测试
e.最终结果:
前言:
经典环节:我一直深信,带着问题思考和实践,能够更容易理解并学习到。
(1)最简单的驱动程序是怎么样的?
(2)怎么编写驱动程序?
- APP打开的文件在内核中如何表示?
- 理解编写程序的流程
- 怎么调用到自写的open/read/write函数?
- 怎么告知内核有一个新的驱动程序?内核通过什么找到驱动程序?
- 驱动程序的安装和卸载是怎么实现的?
(3)Hello驱动程序实战
写作不易。如果有所帮助,多多支持,欢迎关注,大家共同进步呀!
一、背景
回顾前文,当我们APP调用glibc中open/read/write函数,会从用户态到内核态,调用相应的sys_open、sys_read等函数访问内核中的文件,打开相应的设备节点,寻找到并启动相应的驱动程序。其中,如何从用户态转到内核态等一些相关知识,参照文章:
【Linux】文件IO---应用开发角度_希希雾里的博客-CSDN博客
最简单的驱动程序是怎么样的?
这里最简单的驱动程序调用方式,驱动程序也提供自己的drv_open、drv_read等函数,一个驱动程序对应一个设备。
二、驱动程序编写流程
基于上述,完成一个简单基本的驱动程序,我们需要去自己实现open/read/write函数,但完整实现一个驱动程序还会有一些具体问题。
具体问题:
- APP打开的文件在内核中如何表示?
- 理解编写程序的流程:
- 怎么调用到自写的open/read/write函数?
- 怎么告知内核有一个新的驱动程序?内核通过什么找到驱动程序?
- 驱动程序的安装和卸载是怎么实现的?
1.APP打开的文件在内核中如何表示?
APP打开文件时,可以得到一个整数(文件句柄),对于APP的每一个文件句柄,在内核里面都会有一个“struct file”与之对应。打开字符设备节点时,内核中也有对应的struct file。
使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags,f_mode):
去读写文件时,文件的当前偏移地址也会保存在struct file结构体的f_pos成员里。
打开字符设备节点时,内核中也有相对应的struct file。注:成员里的结构体struct file_operations *f_op,这是由驱动程序提供的。
2.编写驱动程序的流程
-
- ①确定主设备号,也可以让内核分配
- ②定义自己的 file_operations 结构体---注:里面存有驱动的open/read/write函数
- ③实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
- ④把 file_operations 结构体告诉内核:register_chrdev
- ⑤谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
- ⑥有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
- ⑦其他完善:提供设备信息,自动创建设备节点:class_create、device_create
三、hello驱动程序实战
这里初步编写一个驱动程序,这里主体写hello_drive.c驱动程序以及测试程序hello_drive_test.c。
注:驱动程序和应用程序之间数据要使用copy_from_user/copy_to_user函数。
hello_drive.c
/*
hello_drive.c
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#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;
//用于保存应用程序下发的数据,定义一个buffer
static char kernel_buffer[1024];
static struct class* hello_class;
#define MIN(a, b) (a < b ? a : b)
/*第三步:实现对应的 drv_open/drv_read/drv_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_buffer, 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_buffer, 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;
}
/*第二步:定义自己的 file_operations 结构体---注:里面存有驱动的open/read/write函数*/
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,
};
/*第四步:把 file_operations 结构体告诉内核:register_chrdev*/
/*第五步:定义一个入口函数, 调用register_chrdev*/
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv);
//创建一个类,提供设备信息
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)){
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
return 0;
}
/*第六步:定义一个出口函数,出口函数调用unregister_chrdev*/
static void __exit hello_exit(void)
{
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
major = unregister_chrdev(0, "hello");
}
/*第七步:其他完善:提供设备信息,自动创建设备节点:class_create、device_create*/
//把函数修饰成入口函数和出口函数
module_init(hello_init);
module_exit(hello_exit);
//内核遵循GPL协议,所以我们也要遵循GPL协议,才能够使用内核里的一些函数
MODULE_LICENSE("GPL");
hello_drive_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./hello_drv_test -w abc
* ./hello_drv_test -r
*/
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 ((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\n", buf);
}
close(fd);
return 0;
}
最终测试:
a.首先编译内核(如果没有编译过)
参照这篇文章【Linux】imx6ull开发板第一个驱动实验(led)_希希雾里的博客-CSDN博客
b.设置交叉编译工具链
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
c.编写makefile
注意:KERN_DIR是要对应开发板上的内核。
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f hello_drv_test
obj-m += hello_drv.o
之后make编译并将.ko文件和hello_drive_test复制到nfs挂载的文件夹下。
cp *.ko hello_drv_test ~/nfs_rootfs/
d.上机测试
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
//复制到开发板上
cp /mnt/hello_drive.ko ./
cp /mnt/hello_drive_test ./
//安装驱动模块
insmod hello_drive.ko
//查询是否有我们的hello程序
cat /proc/devices
lsmod
//查询是否有我们的设备节点
ls /dev/hello -l
//写入
./hello_drive_test -w hello_world!
//读取
./hello_drive_test -r
e.最终结果: