文章目录
前言
linux驱动的开发一般都是在一下框架之下实现的,先从最简单的Hello World驱动开始,可以帮助我们了解Linux驱动开发的基本框架,为后续学习打下基础。
一、Linux驱动说明
什么是Linux驱动?Linux驱动跟我们平常所说的驱动有什么区别?
我们在单片机等一些裸机场合下编写的一些LED、按键等驱动,都是直接通过我们的程序去调用一些与硬件相关的函数去实现的,比如我们需要去驱动一盏LED灯,我们会有以下函数:
- LED_Init();//LED初始化函数,一般都是对LED所在的IO进行一些初始化
- GPIO_SetBits();//设置IO口电平,从而是LED状态改变的函数
这些函数都是我们在程序中可以直接使用的,操作它们我们就可以直接实现对硬件的操作。但是当我们引入Linux操作系统时,我们对硬件的操作就不能直接使用这些函数,因为在Linux下一切皆文件,文件的操作一般都是开关读写,也就是四个基本的函数:open/close/write/read,因此我们去操作底层硬件就至少要实现这四个函数来对接Linux内核,而不能直接使用我们上面说的那些函数。
关系图如下:
1.1 Linux驱动分类
Linux的设备驱动分为三类:字符设备驱动、块设备驱动和网络
设备驱动。
字符设备是基于字节流、数据的交互以字节为单位的设备,这类设备驱动经典的有GPIO驱动、IIC、SPI等驱动。
块设备一般是一些存储器设备,这些设备的数据一般按存储块进行交互,比如: EMMC、 NAND、 SD 卡和 U 盘等存储设备。
网络设备属于比较特殊的设备,是关于网络的设备,比如以太网卡、无线网卡等设备称为网络设备,它们的驱动称为网络设备驱动。
注意:一个设备可以归属于多个多种类型设备,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备。
二、字符设备驱动实现
Linux驱动的实现是在Linux系统的框架之内实现的,需要包含Linux下一些相关内容和方法。
2.1 设备号
Linux下每一个设备都有自己的设备号,设备号又分为主设备号和次设备号。
主设备号:表示具体的驱动程序
次设备号:表示这个驱动下面的设备,就是可以使用这个驱动的设备
注:我们编写驱动时可以自己确定设备号,也可以让系统分配设备号。
2.2 file_operation结构体
file_operation是关于文件操作的结构体,需要将我们实现的设备操作函数填入下面的结构体,
.owner = THIS_MODULE
默认就可以。
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,
};
2.3 设备操作函数
设备操作函数就是drv_open/drv_read/drv_write 等函数,这些函数需要填入 file_operations 结构体。
实现这些操作函数就是对接底层硬件的接口,也为上层的系统调用提供方法。
2.4 设备注册和注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模
块的时候也需要注销掉字符设备。函数源码如下:chrdev
就说明是字符设备。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)//字符设备注册
static inline void unregister_chrdev(unsigned int major, const char *name)//字符设备注销
2.5 驱动加载和卸载
驱动还需要提供加载/安装和卸载的方法,很简单只需要:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
2.6 入口和出口函数
入口函数和出口函数就是填入module_init
和module_exit
的参数。这两个函数实现的功能是刚好相反的。
入口函数用来初始化安装驱动程序,相当于驱动的入口。出口函数则是卸载驱动程序,相当于出口。
我们来看看入口函数和出口函数分别做了什么。
入口函数:
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(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 line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
return 0;
}
①register_chrdev注册字符设备
②class_create为模块创建一个相关类
③device_create使用上面的类为模块创建设备
根据入口函数所做的内容我们可以猜到出口函数的内容:
①device_destroy销毁设备
②class_destroy销毁设备驱动相关类
③unregister_chrdev注销设备
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");
}
2.7 添加相关信息
我们还需要为我们的驱动添加一些信息:
- LICENSE
- AUTHOR
也就是许可证和作者,作者信息可以写也可以不写,但是许可证信息必须添加,遵守GPL协议:
MODULE_LICENSE("GPL");
三、hello驱动
3.1 驱动程序
#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>
/* 1. 确定主设备号 */
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结构体 */
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;
}
/* 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_chrdev(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 line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
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");
3.2 应用测试程序
#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;
}
3.3 Makefile
编译当前的驱动会使用到Linux内核源码,因此,我们需要将编译好的Linux内核源码路径修改为自己Linux内核源码的路径。
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/boy/linux/100ask/100ask_imx6ull-sdk/Linux-4.9.8
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
3.4 安装驱动
将Ubuntu服务器的nfs网络文件系统目录挂载到开发板的/mnt
目录下:
mount -t nfs -o nolock,vers=3 192.168.1.79:/home/boy/nfs_rootfs /mnt
在Ubuntu编译好驱动程序和测试程序后,输出以下文件:
.ko
文件就是驱动文件,把.ko
和应用测试程序hello_drv_test
复制nfs文件夹下,ARM板挂载nfs后就可以看到这些文件,然后使用insmod
命令安装我们的hello_drv.ko
驱动:
在这里如果板子上的内核版本或者一些其他信息与我们编译内核时使用的内核不一致,容易发生错误下面的错误,开发板上最好使用我们编译驱动时的内核。错误如下:
安装是否成功呢,我们列出当前的驱动模块就知道了:
可以看到我们的hello_drc
驱动已经安装成功。
4.5 测试驱动
执行我们的测试程序,写入后读出:
读出了我们写入的消息,说明我们的驱动可以正常使用。
4.6 卸载驱动
使用rmmod+驱动名称
可以删除卸载当前安装的驱动:
删除后我们的驱动模块节点就不见了,删除成功。
总结
Linxu的驱动分为三类:字符设备驱动、块设备驱动和网络设备驱动。
这些驱动都是在一些基本的框架之下进行编写开发的,下一步需要认真学习这些框架和这三类设备的驱动开发!