<>1. Linux驱动程序的分类

Linux 中主要分为三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。

1、字符设备驱动:因为软件操作设备是是以字节为单位进行的,是按照字节流进行读写操作的一种设备。典型的如LCD、蜂鸣器、SPI、触摸屏等驱动,都属于字符设备驱动的范畴。大部分的驱动程序都是属于字符设备驱动。

2、块设备驱动:块设备驱动是相对于字符设备驱动而定义的,因为块设备被软件操作时,是以块为单位进行操作的(块指的是多个字节组成一个块)。块设备大多指的都是各种存储类类设备,比如EMMC、SD卡、NANDFlash、U盘等等。

3、网络设备驱动:专门针对网络设备而设计的一种驱动,不管是有线还是无线网络,都属于网络设备驱动。

另外,一个设备可以属于多种设备驱动类型,比如 USB WIFI设备,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

<>2. 与Linux驱动开发相关的介绍

1、Linux下的应用程序是如何调用驱动程序的

应用程序在使用C库函数所提供的 open/read/write 等等函数时,最终会进入到内核里面,调用内核所提供的
sys_open/sys_read/sys_write 等等函数。而此时如果内核发现应用程序需要访问的是驱动的话,那么就会调用该驱动程序所提供的
drv_open/drv_read/drv_write
等函数;如果发现应用程序访问的不过是普通文件的话,那么内核就会调用访问普通文件的那套函数。下图形象的给出了调用关系:

驱动程序实际上起到承上启下的作用,上承应用程序,对下则实现了具体的硬件操作。

2、Linux驱动程序的两种运行方式

* 可以把驱动程序编译进内核里面,这样内核启动后就会自动运行驱动程序了;
* 将驱动程序编译成以.ko为后缀模块文件,然后在Linux启动后,我们自己手动安装驱动程序。一般来说,这种方式在开发驱动阶段常用。
3、Linux驱动开发中常用的几个命令

* insmod(install module):用于安装以Linux的驱动模块
* rmmod(remove module):卸载驱动模块
* lsmod(list module):打印出当前内核中已经安装的模块
* modinfo(module information):打印出某个 xxx.ko 文件的模块信息。用法:modinfo xxx.ko
<>3. Linux驱动开发需要准备的工作

1、已经安装好的交叉编译工具链


我们开发的驱动程序是要运行在ARM架构上的,所以需要准备好ARM架构的编译工具链。一般来说我们使用开发板厂商,或者SoC原厂提供交叉编译工具链即可。具体如何安装交叉编译工具链这里不多啰嗦了。

2、准备已经配置和编译好你对应板子的内核源码

​ 驱动程序是运行在内核空间的,不同于应用程序运行在用户空间。驱动程序已经是属于内核的一部分了,而编译驱动程序需要借助于内核源码来编译。

​ 另外,内核源码的版本一定要和你板子上实际运行的版本相一致,否则编译出来的驱动程序会因为版本不同而无法在你的板子上运行。

3、你的开发板接线正常,网络正常(要保证开发板和ubuntu之间可以相互ping通,因为我们通过nfs方式把ubuntu编译好的 xxx.ko
等文件传输到开发板)。

<>4. show出你的代码

<>4.1 hello驱动的编写

我们在编写应用程序的时候,首先也是先学会如何再电脑屏幕上输出 “helllo
world”。同样的,我们编写的第一个驱动程序,也是先学会hello驱动,该驱动不涉及任何的硬件操作,而且也是属于字符设备驱动的范畴。主要实现的功能是:

1、应用程序调用 open、read、write 等函数时,对应的驱动函数都打印出内核信息;

2、应用程序调用 write 函数时,传入的数据保存在驱动中;

3、应用程序调用 read 函数时,把驱动中保存的数据再返回给应用程序,并打印出来

驱动程序的编写其实也是有迹可循的,主要的编写步骤如下:

* 确定主设备号,也可以让内核自动分配
* 定义自己的 file_operations 结构体
* 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
* 把 file_operations 结构体告诉内核:register_chrdev
* 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
* 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
* 其他完善:提供设备信息,自动创建设备节点:class_create, device_create
其中,驱动程序核心中的核心就是 file_operations 这个结构体了。在这个结构体里面,就是要实现这个驱动程序自己的
open、read、write等函数,并通过Linux内核提供的接口注册到内核里面去。而其他的一些步骤都是为了遵循LInux驱动程序的编写规范,用于完善这个驱动程序的。

驱动代码的编写也不用完全都自己写,我们可以参考Linux内核提供的一些已有的驱动程序,下面我们就参考内核的一份 misc
驱动,编写我们自己的hello驱动程序,hello驱动代码如下:
#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结构体 */ /* * @description : 从设备读取数据 *
@param - file : 内核中的文件描述符 * @param - buf : 要存储读取的数据缓冲区(就是用户空间的内存地址) * @param -
size : 要读取的长度 * @param - offset : 相对于文件首地址的偏移量(一般读取信息后,指针都会偏移读取信息的长度) * @return
: 返回读取的字节数,如果读取失败则返回-1 */ 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); } /* * @description : 向设备写数据 * @param - file :
内核中的文件描述符 * @param - buf : 要写给设备驱动的数据缓冲区 * @param - size : 要写入的长度 * @param -
offset : 相对于文件首地址的偏移量 * @return : 返回写入的字节数,如果写入失败则返回-1 */ 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);
} /* * @description : 打开设备 * @param - node : 设备节点 * @param - file : 文件描述符 *
@return : 打开成功返回0,失败返回-1 */ static int hello_drv_open (struct inode *node,
struct file *file) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__)
; return 0; } /* * @description : 关闭设备 * @param - node : 设备节点 * @param - file :
文件描述符 * @return : 关闭成功返回0,失败返回-1 */ 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 */ /* 7.
其他完善:提供设备信息,自动创建设备节点 */ 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
__exithello_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"); } /*
指定驱动的入口和出口,以及声明自己的驱动遵循GPL协议(不声明的话无法把驱动加载进内核) */ module_init(hello_init);
module_exit(hello_exit); MODULE_LICENSE("GPL");
hello驱动程序的几点说明:

*
驱动程序要打印信息的时候,调用的函数是 printk ,而应用程序调用的是 printf

*
应用程序和驱动程序之间传递数据,不能使用简单的赋值或者memcpy等,要使用内核提供的 copy_from_user/copy_to_user
函数。当然如果需要传递大量数据的时候,还可以使用内存映射的方式

*
阅读一个驱动程序,首先要找到驱动程序的入口函数。上面的驱动入口函数就是hello_init函数,该函数做的事情就是向内核注册了一个
file_oprations 结构体,并且完成自动创建设备节点相关的代码

*
file_oprations 结构体是驱动程序的核心,里面提供了本驱动 open/read/write/release
等成员,当应用程序调用了open/read/write/release 等函数时,就会导致对应驱动的这些成员函数被调用

<>4.2 编写hello应用程序测试

下面我们编写一个hello应用程序来测试我们编写好的驱动程序。该应用程序要实现的功能是:

* 向驱动程序写入一串字符串
* 把驱动程序保存起来的字符串读出来
hello_drv_test 应用程序代码如下:
#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驱动写入字符串 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;
}
<>4.3 驱动程序的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 是指定内核源码的路径的,需要根据不同开发环境指定
KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.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 # obj-m
指定具体要编译的驱动程序
<>5. 在开发板上测试运行hello驱动

编写完上述的代码之后,就可以进行编译了。

直接在hello驱动所在的目录下,输入 make 即可编译了。编译完之后就会看到生成对应的 .ko 文件了。

然后我们把编译好的 .ko 文件,和测试驱动的应用程序都拷到 nfs 共享目录下,我的 nfs 共享目录是在 /home/lbh/nfs 下。

然后我们打开开发板后,进入到开发板的控制台。挂载 ubuntu 中的 nfs 共享目录到开发板的 /mnt 目录下,在开发板输入如下命令:
mount -t nfs -o nolock,nfsvers=3 192.168.1.33:/home/lbh/nfs /mnt
其中 192.168.1.33 这个IP地址是你的ubuntu的IP地址。

挂载完成之后,就可以去 /mnt 目录下看到了自己编译好的 .ko 文件和对应的应用程序文件了。

我们执行 insmod hello_drv.ko 命令,就可以把该驱动程序安装到内核中了。而且可以看到内核打印出了相应的信息,如下:
[ 293.594910] hello_drv: loading out-of-tree module taints kernel. [
293.616051] /home/lbh/linux/drv/hello_drv.c hello_init line 70
说明驱动加载成功了。

注意:如果板子没有看到打印信息的话,那么就输入如下命令把内核的打印信息打开:
echo "7 4 1 7" > /proc/sys/kernel/printk
当然有些板子内核打印信息是默认已经打开了的。或者我们输入 demsg 命令也可以看到内核的打印信息。

然后我们运行 hello_drv_test 应用程序来测试内核,都可以看到内核的打印信息,和我们读取到应用程序写给内核的字符串。

<>6. 和驱动调试有关的其他知识

* cat /proc/devices 命令可以查看当前系统是否有我们刚刚安装的驱动程序
* 安装了设备驱动之后,就会在我们的Linux系统 /dev目录下生成对应的设备文件了。linux中没一个驱动程序都有一个与之对应的设备文件,可以使用
ls /dev/hello -l 命令查看该驱动文件

技术
今日推荐
下载桌面版
GitHub
百度网盘(提取码:draw)
Gitee
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:766591547
关注微信