一、在应用层的APP上调用glibc,Linux系统会做哪些事?
如果访问的是普通文件,则会经系统调用后访问。
如果是驱动程序则会通过调用驱动程序对应的代码进行访问(提供drv_open),如下图。
二、编写驱动程序的几个步骤
① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 实现入口函数:安装驱动程序时,就会去调用这个入口函数,执行工作:1、把 file_operations 结构体告诉内核:register_chrdev. 2、创建类class_create. 3、创建设备device_create.
⑤ 实现出口函数:卸载驱动程序时,就会去调用这个入口函数,执行工作:1、把 file_operations 结构体从内核注销:unregister_chrdev. 2、销毁类class_create. 3、销毁设备结点class_destroy.
⑥ 其他完善:GPL协议、驱动作者、驱动面熟
int open(const char *pathname,int flags,mode_t mode);/pathname:指向文件路径的指针;flags:文件权限;mode:创建模式/
/成功:返回文件描述符/
三、编写第一个驱动程序(字符设备驱动)
实现的功能:
参考 driver/char 中的程序,包含头文件,写框架,传输数据:
1、驱动中实现 open, read, write, release,APP 调用这些函数时,都打印内核信息
2、APP 调用 write 函数时,传入的数据保存在驱动中
3、APP 调用 read 函数时,把驱动中保存的数据返回给 APP
学习目的:
主要是学习别人的框架,理解其原理。
1、打开内核目录下\Linux-5.4\drivers\char中的misc.c
,这是一个比较经典的字符驱动程序
第一步、添加需要的头文件
在misc.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>
第二步、确定主设备号,也可以让内核分配
我们这里自己设个0
static int major = 0;
第三步、定义自己的 file_operations 结构体(存放驱动函数,如drv_open等)
我们可以先看一下 file_operations
结构体 有哪些成员:
我们当然需要填写那么多成员,因此我们参照misc.c
中定义的file_operations结构体,发现除了驱动函数函数外,还需要一个.owner
,那我们也在自己的程序中添加。
除了添加.owner
之外,根据我们刚才的项目要实现的功能,我们还需要在结构体中添加四个函数,分别实现open, read, write, release功能。我们继续借鉴misc.c
文件中的命名方式和函数的写法(模板很重要)
我们misc.c
文件内的模板如下:
同时参照file_operations结构体中的四个函数的定义:
第四步、实现file_operations 结构体中的函数
//③ 实现对应的 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_from_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);
//_copy_from_user(void * to, const void __user * from, unsigned long n)
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;
}
注:
1、printk是内核里进行打印的函数,printf是应用层打印的函数
2、_copy_from_user()
第五步、实现入口函数,注册file_operations 结构体
首先我们先参考misc.c
里的入口函数编写
(1)注册file_operations 结构体
(2)创建类
(3)创建设备节点(应用程序若想要访问某个驱动程序,需要依赖与某个设备结点)
实现我们自己的入口函数:
static int __init hello_init(void)
{
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
major=register_chrdev(0,"hello", &hello_drv);//注册file_operations 结构体
hello_class = class_create(THIS_MODULE, "hello_class");//创建类
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)){
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
return -1;
unregister_chrdev(major,"hello");
}
device_create(hello_class, NULL, MKDEV(major,0), NULL, "hello" );/*创建设备节点 以后可以通过/dev/hello访问该结点 */
return 0;
}
第六步、实现出口函数
但是在misc.c
并未有相应例子,我们再去找一个经典案例参考内核目录下的\Linux-5.4\drivers\char中的pc8736x_gpio.c
,我们实现我们的出口函数():
(1)注销file_operations 结构体
(2)销毁类
(3)销毁设备节点
实现我们自己的出口函数:
printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
device_destroy(hello_class,MKDEV(major,0));//销毁设备节点
class_destroy(hello_class);//销毁类
unregister_chrdev(major,"hello");//注销file_operations 结构体
return 0;
第七步、对入口、出口函数进行宏修饰
为什么需要宏修饰?因为我们需要告诉内核我们的入口和出口函数是哪一个?
但是在misc.c
并未有相应例子,我们再去找一个经典案例参考内核目录下的\Linux-5.4\drivers\char中的pc8736x_gpio.c
我们也是嫖到自己的程序里,生成自己的宏修饰。
module_init(hello_init);
module_exit(hello_exit);
第八步、添加一些协议(GPL)、作者、描述
GPL协议:
GNU通用公共许可证简称为GPL,是由自由软件基金会发行的用于计算机软件的协议证书,使用该证书的软件被称为自由软件。大多数的GNU程序和超过半数的自由软件使用它。 下面的正文是自由软件基金会GNU通用公共许可证原始文档的副本。Linux操作系统以及与它有关的大量软件是在GPL的推动下开发和发布的。你将看到:如果你打算为了发布的目的修改,更新或改进任何受通用公共许可证约束的软件,你所修改的软件同样必须受到GNU通用许可证条款的约束。
若未遵守该协议内核的函数将无法使用,因为整个linux内核都遵循该协议。
我们这步的代码:
MODULE_AUTHOR("aipolo <xxx@qq.com>");
MODULE_DESCRIPTION("hello Driver");
MODULE_LICENSE("GPL");
第九步、编译驱动文件
(1)修改Makefile文件内的内核地址
Makefile内文件内容:
/*以下为韦东山老师编写*/
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 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
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4
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
(2)生成hello_drv.ko
驱动文件和hello_drv_test
可执行文件
第十步、在开发板上安装驱动文件
将第十一步的hello_drv.ko
驱动文件和hello_drv_test
可执行文件移到开发板上(具体操作省略)
(1)安装驱动程序
insmod hello_drv.ko
(2)查看安装是否成功
lsmod
(3)查看设备节点
cat /proc/devices
(4)查看节点的主次设备号
根据图示,主设备号为241,次设备号为0
我这边是241,不同机型值都可能不同
第十一步、执行测试程序
测试程序内容:
#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;
}
./hello_drv_test -w
./hello_drv_test -r
大功告成啦
总结时刻
驱动的开发流程:
有个重要补充就是虚拟机上的Linux内核版本需要和开发板的内核版本一致