Linux驱动开发(速记版)--驱动基础

第一章 初识内核源码

Linux系统源码提供了操作系统的核心功能,如进程管理、内存管理、文件系统等。

BusyBox这类的文件系统构建工具,则提供了在这些核心功能之上运行的一系列实用工具和命令,使得用户能够执行常见的文件操作、文本处理、网络配置等任务。文件系统构建工具的运行,依赖于内核源码。

1.1 内核源码简介

Linux内核源码是开源的,其官方网站为https://www.kernel.org/,在该网站上可以下载到最新的Linux内核源码。进入该网站后,你会看到多个版本的内核分支,主要包括:

主线版本(Mainline):这是由Linux之父Linus Torvalds领导的开发团队维护的最新、最前沿的内核版本。它包含了最新的功能和改进,但也可能包含未完全稳定的代码。

稳定版本(Stable):这些版本在主线版本的基础上进行了进一步的测试和修复,以确保稳定性和可靠性。它们通常用于生产环境。

长期支持版本(Longterm):这些版本在发布后会得到较长时间的支持和维护,通常用于需要长期稳定性和兼容性的场景。

嵌入式开发板与Linux内核源码

在嵌入式系统开发中,开发板如迅为开发板,通常使用特定版本的Linux内核源码,这些源码可能是基于官方源码进行定制和优化的。半导体厂商或开发板制造商会提供经过适配和优化的内核源码包,以便在特定的硬件平台上运行。

迅为开发板与Linux内核

底板+核心板设计:迅为开发板采用这种设计,方便用户进行扩展和定制。用户可以根据需要选择不同的核心板,以适应不同的应用场景。

定制内核源码:迅为提供基于Linux官方源码定制的内核源码包,这些源码包已经过优化,以适应迅为开发板的硬件特性。

开发便捷性:迅为开发板提供丰富的接口和文档,帮助用户快速上手并减少开发阶段的工作量。用户可以使用这些资料来编译和调试Linux内核,以满足自己的开发需求。

1.2 内核源码结构

内核源码解压后,得到很多文件夹,这些文件夹各司其职。

arch:跨平台架构代码,处理各CPU架构特定功能。

block:块设备驱动与管理,如硬盘读写。

crypto:加密、压缩及校验算法,保障数据安全。

Documentation:内核设计与开发指南相关文档。

drivers:设备驱动大全,按类别组织。

firmware:特殊固件,用于硬件初始化或功能实现。

fs:文件系统代码,支持多文件系统。

include:内核编程基础头文件,定义宏、类型及函数原型。

init:系统启动初始化代码,准备运行环境。

ipc:进程间通信代码,如管道、消息队列。

kernel:内核核心,含进程调度等关键子系统。

lib:内核通用库函数,如字符串处理、数学运算。

1.3 编译内核源码

  1. 准备编译环境
    • 确保您已经安装了迅为电子有限公司提供的编译环境,该环境已经过测试,可以直接用于编译内核源码。
    • 如果您使用的是虚拟机Ubuntu,请确保虚拟机配置满足编译要求,包括足够的内存和存储空间。
  2. 内核源码获取
    • 内核源码存放在指定路径:"iTOP-RK3568 开发板【底板 V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\02_Linux_SDK 源码"。
    • 将此目录下的内核源码(通常是压缩包,如linux_sdk.tar.gz)拷贝到虚拟机Ubuntu的某个目录下,例如/home/ubuntu/linux_sdk
  3. 解压内核源码
    • 使用终端(Terminal)进入存放内核源码压缩包的目录。
    • 执行解压命令:tar -vxf linux_sdk.tar.gz。这将解压出linux_sdk目录,其中包含内核源码和其他相关文件。
  4. 进入内核源码目录
    • 使用cd命令进入解压后的内核源码目录:cd linux_sdk
  5. 编译内核源码
    • 在内核源码目录下,通常会有一个编译脚本,如build.sh。对于迅为iTOP-RK3568开发板,您可以使用./build.sh kernel命令来编译内核源码。
    • 编译过程可能需要一些时间,具体取决于您的计算机性能。编译过程中,脚本会自动处理依赖关系、配置选项等,并生成编译后的内核镜像文件。
  6. 检查编译结果
    • 编译完成后,您可以在指定的输出目录中找到编译后的内核镜像文件(如zImageImage等),以及可能的模块文件(.ko文件)。
    • 确保没有编译错误,并检查编译日志以获取任何警告或提示信息。
  7. 后续步骤
    • 一旦内核源码编译成功,您可以将编译后的内核镜像烧录到开发板中,以进行进一步的测试和开发。
    • 如果需要,您还可以根据项目的具体需求,修改内核配置选项,并重新编译内核。

讯为提供的 linux内核源码包解压后,包含有 kerner源码和 buildroot、debian、tocto等构建工具,build.sh编译脚本,prebuilds交叉编译工具链,u-boot源码等。

运行 build.sh编译脚本后出现图形化操作界面。

按照官方文档给的顺序执行编译操作。

完了生成 uboot.img到 uboot目录,内核生成 boot.img到 kernel目录。

值得注意的是第四步,在Build.sh界面选择rootfs,会进入文件系统镜像选择界面。

发现这里有 5种镜像供我们选择,没有 BusyBox,查阅官方资料后发现 BusyBox要单独解压BusyBox工具进行编译。

这里选择 buildroot镜像,编译完成后

打包 firmware下的固件包,选择 Build.sh界面的firmware选项,脚本会自动运行将所有固件移动到 rockdev目录下。

打包成 update.img所需要的各种功能镜像就都拷贝到了 rockdev目录下,

最后,还是在 Build.sh界面,选择 updateing,整个 rockdev目录下的全部固件,都会被打包成一个整体的 update.img镜像。最后用 官方提供的程序烧录工具,将镜像烧录到开发板。简单的文件管理系统,其实就是 linux操作系统,的移植,就做好了。

使用 NFS,将 讯为板子文件系统上的文件夹挂载到 ubuntu上,

cpp 复制代码
mount -t nfs -o nfsvers=3,nolock 192.168.x.xx:/home/nfs /mnt/

/*
mount                  挂载指令。
-t nfs                 指定挂载NFS类型文件系统。
-o nfsvers=3,nolock    设置了NFS版本3并禁用文件锁定
192.168.x.x:/home/nfs  是NFS服务器的IP和共享目录。可自定义文件目录
/mnt/                  是本地挂载点.可自定义文件目录
*/

umount /mnt/   //解除挂载

判断客户端和主机能不能Ping通,要用 ip&子网掩码,看是否相等,相等才是处于同一网段,才能Ping通。 不同网段需要配置指定网关才能实现互通。

记得检查板子和虚拟机是不是都联网了。没联网检查虚拟机模式设置(桥接、NAT、仅主机)。

1、可使用 dhclient指令设置网卡自动获取 ip地址。

2、或者直接修改虚拟机网络配置。
NFS将讯为板子挂载到虚拟机上成功

第二章 helloworld驱动实验

在本小节中,我们将编写一个简单的Linux内核驱动------helloworld驱动。

这个驱动主要用于演示Linux内核模块的基本结构和注册/注销流程。以下是对提供的helloworld.c代码的解释和必要的背景知识。

模块初始化函数 static int __init helloworld_init(void)

模块清理函数 static void __exit helloworld_exit(void)

模块注册和注销宏 module_init() module_exit()

模块许可证 MODULE_LICENSE()

头文件包含

cpp 复制代码
#include <linux/module.h> 包含模块操作所需的所有定义和宏。
#include <linux/kernel.h> 包含内核的基本类型定义和常用的内核函数(如printk)。

模块初始化函数

cpp 复制代码
static int __init helloworld_init(void)

/*
这是模块的r入口,
insmod或modprobe命令在加载模块时会调用此函数。
*/

模块清理函数

cpp 复制代码
static void __exit helloworld_exit(void)

/*
模块的清理函数。
rmmod命令卸载模块时会调用此函数。
*/

模块注册和注销宏

cpp 复制代码
module_init(helloworld_init); //宏用于注册模块的初始化函数。
module_exit(helloworld_exit); //宏用于注册模块的清理函数。

模块许可证

cpp 复制代码
MODULE_LICENSE("GPL v2");
//指定模块的许可证,这里使用的是GPL版本2。可选。
cpp 复制代码
#include <linux/module.h>  // 包含模块的基本函数和宏定义  
#include <linux/kernel.h>  // 包含内核常用函数和宏定义,如printk  
  
// 定义驱动入口函数,该函数在系统加载模块时调用  
static int __init helloworld_init(void)  
{  
    printk(KERN_EMERG "helloworld_init\r\n"); // 使用printk输出信息到内核日志,级别为KERN_EMERG  
    return 0; // 返回0表示初始化成功  
}  
  
// 定义驱动出口函数,该函数在系统卸载模块时调用  
static void __exit helloworld_exit(void)  
{  
    printk(KERN_EMERG "helloworld_exit\r\n"); // 卸载时输出信息  
}  
  
// 使用module_init宏注册入口函数  
module_init(helloworld_init);  
  
// 使用module_exit宏注册出口函数  
module_exit(helloworld_exit);  
  
// 声明模块许可证,GPL v2是GNU General Public License版本2  
MODULE_LICENSE("GPL v2");

第三章 内核模块实验

4.1 设置交叉编译器

下载交叉编译器并解压到/usr/local目录。

解压命令:

bash 复制代码
tar -vxf gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu.tar.gz

修改环境变量,在/etc/profile文件末尾添加:

bash 复制代码
export PATH=$PATH:/usr/local/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin

#将路径添加到环境变量PATH中,
#当你在终端输入一个命令时,系统会首先在当前PATH变量中指定的目录中查找这个命令的可执行文件

4.2 编写makefile

首先,确保 Makefile和 helloworld.o位于同一级目录下,并参照以下模板编写Makefile:

bash 复制代码
# 指定架构和交叉编译工具链  
export ARCH=arm64                          #架构类型  注意这里不能有空格
export CROSS_COMPILE=aarch64-linux-gnu-    #交叉编译工具链前缀
  
# 指定要编译的模块文件(.o 文件),名称要和.c文件名称一致 
obj-m += helloworld.o  
  
# 定义内核源码的路径,这里假设路径为/home/topeet/Linux/linux_sdk/kernel  
# 请根据实际情况修改此路径  
KDIR := /home/topeet/Linux/linux_sdk/kernel  
  
# 定义当前工作目录的路径,使用shell命令获取  
PWD := $(shell pwd)  
  
# 默认目标all,执行make命令时编译模块  
all:  
	$(MAKE) -C $(KDIR) M=$(PWD) modules   
  
# 清理目标clean,执行make clean命令时清理编译生成的文件  
clean:  
	$(MAKE) -C $(KDIR) M=$(PWD) clean


#obj += xx.o/xx.c 向makefile声明了一个目标,xx这个文件是需要被编译成内核模块(.ko)的目标文件
#$(MAKE)用于表示make命令的完整路径,用于递归调用make.安装了make指令可直接替换成 make.
#-C用于切换到指定源码目录
#M用于指定外部模块源码目录

4.3 编译模块

makefile文件和要编译成模块的源码文件要在一个目录下,在该目录下执行make命令,得到helloworld.ko 模块文件。
源码用makefile编译后得到.ko模块文件

4.4 模块加载与卸载

要运行模块有两种方式,一种是将模块编译进内核,内核运行模块自动运行;另一种是手动加载和卸载模块,这样模块加载时会调用模块入口函数,模块卸载时会调用模块出口函数。

insmod命令用于加载模块,若 A.ko依赖B.ko,则须先加载B.ko。

rmmod命令用于卸载模块。
modprobe命令用于加载模块,自动加载依赖模块。

modprobe -r 命令用于卸载模块,自动卸载依赖模块。

modprobe要求驱动文件位于/lib/modules/generic目录下

bash 复制代码
insmod helloworld.ko  #加载模块
rmmode  helloworld     #卸载模块

modprobe helloworld.ko       #加载模块,并自动加载模块依赖的模块
modprobe -r helloword.ko     #卸载模块,并自动卸载模块依赖的模块

insmod加载模块触发入口函数
rmmod卸载模块触发出口函数

4.5 查看模块信息

bash 复制代码
#驱动模块加载以后可以使用 modinfo查看模块信息,包括模块作者、模块说明、模块支持的参数等
modinfo helloworld.ko

#列出已经载入内核的模块
lsmod

第四章 驱动模块传参实验

4.1 驱动模块传参简介

驱动模块传参在嵌入式系统开发中的重要性

在嵌入式系统开发中,驱动模块传参是一个关键的技术点,它允许在运行时动态地调整或配置硬件设备的参数,而无需重新编译整个内核或模块。
驱动模块的参数数据类型

Linux内核提供了几个宏来支持不同类型的参数传递:

  1. 基本类型参数 :使用module_param(name, type, perm)宏。

name:模块参数的名称。

type:参数的数据类型(如int, bool, charp等)。

perm:参数在sysfs中的访问权限(如S_IRUGO表示可读)。

cpp 复制代码
/*基本类型参数 参数名  参数类型  权限*/
module_param(baudrate, int, S_IRUGO);
  1. 数组类型参数 :使用module_param_array(name, type, nump, perm)宏。

name:模块参数的名称。

type:数组元素的数据类型。

nump:指向存储数组元素个数的变量的指针。

perm:访问权限。

cpp 复制代码
static int num_ports = 0;  
/*数组类型传参      参数名        类型   数组长度指针  权限*/
module_param_array(port_numbers, int, &num_ports, S_IRUGO);
  1. 字符串类型参数 :使用module_param_string(name, string, len, perm)宏。

name:外部传入的参数名。

string:程序内定义的字符串变量名。

len:字符串buffer的大小。

perm:访问权限。

cpp 复制代码
static char serial_device[256];  
/*字符类型传参       参数名   字符串变量名     字符串buffer大小       权限*/
module_param_string(device, serial_device, sizeof(serial_device), S_IRUGO);

权限宏定义

权限宏定义在 include/linux/stat.h中,如:

cpp 复制代码
S_IRUSR  //文件所有者可读。
S_IWUSR  //文件所有者可写。
S_IXUSR  //文件所有者可执行。

S_IRGRP、S_IWGRP、S_IXGRP //与文件所有者同组的用户权限,读、写、执行。

S_IROTH、S_IWOTH、S_IXOTH //其他用户的权限。读、写、执行。

模块参数的访问权限,代表了参数在 sysfs 文件系统中所对应文件节点的属性。

sysfs是一个虚拟文件系统,用于导出内核对象的信息给用户空间。内核模块可以利用sysfs来暴露其配置参数,使得这些参数可以在运行时被用户空间程序读取和修改。

4.2 实验程序的编写

cpp 复制代码
#include <linux/module.h>  
#include <linux/kernel.h>  
#include <linux/init.h>  
  
// 定义一个整型参数baudrate  
static int baudrate = 9600;  
module_param(baudrate, int, S_IRUGO);  
MODULE_PARM_DESC(baudrate, "Baud rate for serial communication");  
  
// 定义一个整型数组port_numbers和它的计数器num_ports  
static int port_numbers[10]; // 假设最多支持10个端口号  
static int num_ports = 0;  
module_param_array(port_numbers, int, &num_ports, S_IRUGO);  
MODULE_PARM_DESC(port_numbers, "Array of port numbers for serial devices");  
  
// 定义一个字符串参数serial_device  
static char serial_device[256];  
module_param_string(device, serial_device, sizeof(serial_device), S_IRUGO);  
MODULE_PARM_DESC(device, "Device name or path for serial communication");  
  
// 模块初始化函数  
static int __init my_module_init(void)  
{  
    int i;  
  
    // 打印模块参数的值  
    printk(KERN_INFO "Baud rate: %d\n", baudrate);  
    printk(KERN_INFO "Number of ports: %d\n", num_ports);  
    for (i = 0; i < num_ports; i++) {  
        printk(KERN_INFO "Port %d: %d\n", i + 1, port_numbers[i]);  
    }  
    printk(KERN_INFO "Serial device: %s\n", serial_device);  
  
    // 这里可以添加更多的初始化代码,比如配置串口等  
  
    return 0; // 成功初始化  
}  
  
// 模块清理函数  
static void __exit my_module_exit(void)  
{  
    // 这里可以添加清理代码,比如释放资源等  
    printk(KERN_INFO "Exiting module\n");  
}  
  
// 模块初始化和清理宏  
module_init(my_module_init);  
module_exit(my_module_exit);  
  
// 模块信息  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("Your Name");  
MODULE_DESCRIPTION("A simple example of module parameters");

/*
MODULE_PARM_DESC 宏在 Linux 内核模块编程中用于为模块参数提供描述性文本。
这个宏通常与 module_param、module_param_array 或 module_param_string 等宏一起使用,
以便在模块被加载时,用户可以通过 modinfo 命令或其他工具查看这些参数的描述信息。
*/

parameter.c文件编写完以后,执行 makefile进行编译,得到 .ko驱动模块文件。

使用 insmod xxx.ko或者 probemode xxx.ko 装载驱动时,由于通过 module_param 定义了参数宏来接收传递的参数,因此 装载驱动时可以传递参数。

第五章 内核模块符号导出实验

6.1 内核模块符号导出简介

符号导出 :允许一个模块中的函数或变量对其他模块可见。

EXPORT_SYMBOL():导出参数指定的符号,函数或变量,使其对所有模块可见。

EXPORT_SYMBOL_GPL():导出符号,但仅对符合GPL许可的模块可见。
extern 关键字引用外部变量

(GNU General Public License,GPL,GNU通用公共许可)

cpp 复制代码
// 声明一个要导出的函数  
int my_exported_function(int a, int b) {  
    return a + b;  
}  
EXPORT_SYMBOL(my_exported_function); // 导出函数  

6.2 实验程序的编写

mathmodule.c 文件定义了一个整数 num 和一个加法函数 add(),并将其导出以供其他模块使用。

cpp 复制代码
/*mathmodule.c模块*/
int num = 10; // 定义参数 num  
EXPORT_SYMBOL(num); // 导出参数 num,使其可被其他模块访问  
  
int add(int a, int b) // 定义数学函数 add(),用来实现加法  
{  
    return a + b;  
}  
EXPORT_SYMBOL(add); // 导出 add 函数,确保 hello.c 能够引用  

hello.c 文件引用 mathmodule.c 中的 num 和 add() 函数,并将结果打印到串口终端。

cpp 复制代码
extern int num; // 声明外部变量 num  
extern int add(int, int); // 声明外部函数 add  
  
static int __init hello_init(void)  
{  
  .....使用外部变量
}  

6.1 图形化界面简介

linux内核可以输入make menuconfig来使用 mconf程序打开图形化配置界面。

内核的图形化配置界面需要 ncurses库支持。

图形化配置界面主要有以下四种,在这四种方式中,最推荐的是 make menuconfig,它不依赖于 QT 或 GTK+,且非常直观。

make config (基于文本的最为传统的配置界面,不推荐使用)

make menuconfig (基于文本菜单的配置界面)

make xconfig (要求 QT 被安装)

make gconfig (要求 GTK+ 被安装)

6.2 Kconfig 语法简介

图形化配置界面中的每一个界面都会对应一个 Kconfig 文件。所以图形化配置界面的每一级菜单是由 Kconfig 文件来决定的。

图形化配置界面有很多菜单。所以就会有很多 Kconfig 文件,因此内核源码的每个子目录下都存在Kconfig文件。

6.3 .config 配置文件介绍

我们在图形化配置界面配置好了以后,使用 make config以后,

会通过 scripts/kconfig 目录下的 mconf程序去解析 Kconfig文件,在编译内核的时候会根据这个**.config 文件**来编译内核。

.config 会通过 syncconfig 目标将.config 作为输入然后输出编译需要的文件,重点关注 auto.confautoconf.h

auto.conf 中存放的是配置信息。在内核源码的顶层 Makefile中会包含 auto.conf文件,并引用其中的变量来控制编译哪些驱动。

6.4 defconfig 配置文件

defconfig 文件是 Linux 系统默认的用来生成.config文件的配置文件。

defconfig文件位于 arch/$(ARCH)/configs目录下。

.config文件位于内核源码顶层目录,用于存储内核编译时的配置选项。编译内核时,make命令会根据.config文件中的配置来生成内核镜像。

如果.config文件已存在,make menuconfig会加载.config中的配置作为默认配置,允许用户通过图形界面进行修改。修改并保存make menuconfig中的配置后,.config文件会被更新以反映新的配置。

如果.config文件不存在,可以使用make XXX_defconfig命令来生成它。

make XXX_defconfig命令会根据 XXX_defconfig文件中的配置自动生成 .config文件。

6.5 自定义菜单实验

图形化配置界面的每个页面都是由一个Kconfig文件来生成的。

Kernel下的Kconfig文件,是主页面,其他KConfig是子菜单。

修改主页的Kconfig,就能实现自定义菜单。

第七章 字符驱动模块编译进内核

我们自己写了 helloworld驱动,想把自己的驱动编译进内核,每次内核运行驱动自动运行。

内核的 drivers/char文件夹存放字符设备驱动,

创建一个 hello 文件夹,将,将 hello.c拷贝进去。

在 hello文件夹下,创建 Kconfig文件,

bash 复制代码
config HELLO                           #定义了一个配置选项的名称
    tristate "hello world"             #指定了这个选项是一个三态选项,并给出描述
                                       #三态选项有三个值,Y编译为内核,M编译为模块,N不编译
    help                               #help下的内容是对配置项的详细描述
    hello helloY

然后在hello文件夹下创建 Makefile文件,

bash 复制代码
obj-$(CONFIG_HELLO)+=helloworld.o   #根据内核配置 CONFIG_HELLO
                                    #来决定是否将 hello.o对象文件加入内核构建过程
                                    #如果配置内核时,HELLO选项配置为 Y或 M,
                                    #则CONFIG_HELLO为空

接着修改上一级目录,也就是 driver/char 目录,的 Kconfig文件和 Makefile文件,添加如下内容。

bash 复制代码
#修改上一级的Kconfig
source "drivers/char/hello/Kconfig"   #将目录下的Kconfig文件引入到当前Kconfig文件中
bash 复制代码
#修改上一级的Makefile
obj-y += hello/    #将hello/目录下所有文件添加进内核构建过程
                   #obj-y是一个特殊的变量
                   #用于收集应该被编译进内核的对象文件。

最后打开 menuconfig图形化配置工具,

在配置界面选择 helloworld驱动,选择 *,代表设置成Y。
内核配置界面

然后选择 save,将配置保存到 .config文件,然后退出配置界面,

然后输入以下命令,

bash 复制代码
make savedefconfig     #等效于图形化界面的save选项
                       #会根据当前的.config文件生成一个新的defconfig文件
cp defconfig arch/arm64/configs/rockchip_linux_defconfig  #将defconfig覆盖到 xx_defconfig
cd ../
./build.sh kernel    #执行编译脚本

编译成功后,在 drivers/char/hello目录下生成对应的 .o文件。说明已经将驱动编译进内核。

将编译好的内核镜像烧写到开发板上后,开发板系统启动就可看到 helloworld驱动成功加载,初始化函数__initxxx执行。

第八章 申请字符设备号

字符设备是Linux中以字符为单位进行I/O传输的设备,支持标准的文件操作(如打开、关闭、读写等)。

LED、按键、IIC、SPI、LCD等常见硬件在Linux中通常被实现为字符设备。通过设备号可以定位并操作这些设备。

8.1 申请驱动设备号

8.1.1 设备号申请

在Linux系统中,每个设备都有唯一的设备号,分为主设备号(用于标识驱动)和次设备号(用于区分同一驱动下的不同设备)。

设备号的申请可通过静态或动态两种方式:

register_chrdev_region() 函数静态申请设备号。

alloc_chrdev_region() 函数动态申请设备号。
unregister_chrdev_region() 函数注销设备号。

字符设备号其实是字符设备区域号。

静态申请

使用 register_chrdev_region()函数静态申请设备号,需指定起始设备号和申请的设备数量。

cpp 复制代码
#include <linux/fs.h>  

int ret = register_chrdev_region(start_dev, //用来存放申请到的设备号
                                 num_devs,  //申请设备的数量
                                 dev_name); //申请设备的名称

动态申请

使用 alloc_chrdev_region()函数,内核会自动分配一个未使用的设备号。

cpp 复制代码
#include <linux/fs.h>  

dev_t dev_num; // 用于存储分配的设备号 
  
int ret = alloc_chrdev_region(&dev_num,          //保存申请到的设备号
                              start_minor,       //次设备号最小值
                              num_devs,          //申请设备的数量 
                              dev_name);//       //申请设备的名称

注销设备号

cpp 复制代码
/*注销设备号*/
void unregister_chrdev_region(dev_t from,          //设备号
                              unsigned int count); //设备数量

8.1.2 设备号类型

Linux内核中,设备号(dev_t)是一个32位无符号整数,用于唯一标识系统中的设备。

这个类型在内核源码中定义,其中高12位代表主设备号,低 20位代表次设备号。

dev_t 类型通过 u32定义,位于不同的头文件中。设备号相关的宏定义在 linux/kdev_t.h 中,这些宏包括:

cpp 复制代码
MINORBITS  //定义了次设备号的位数(20位)。

MINORMASK  //次设备号的掩码,用于从设备号中提取次设备号。

MAJOR(dev) //宏,用于从设备号中提取主设备号。

MINOR(dev) //宏,用于从设备号中提取次设备号。

MKDEV(ma, mi) //宏,用于根据给定的主设备号和次设备号生成设备号。

8.2 实验程序的编写

如果驱动模块加载时,传入了设备号,则通过静态方式申请设备号;

如果驱动模块加载时,没有传设备号,则通过动态方式分配设备号。

cpp 复制代码
dev_num = MKDEV(major,minor);//通过 MKDEV 函数将驱动传参的主设备号和次设备号
                             //转换成 dev_t 类型的设备号

ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_num");//通过动态方式进行设备号注册

第九章 注册字符设备实验

9.1 注册字符设备

注册字符设备可以分为两个步骤:

1.字符设备初始化

2.字符设备的添加

9.1.1 字符设备初始化

在 Linux内核中,struct cdev结构体表示字符设备。
cdev_init() 初始化字符设备结构体cdev,将设备结构体与 file_operations结构体关联。

该结构体包含了字符设备的核心信息,如内核对象、所属的内核模块、设备操作函数集、设备号、全局字符设备列表的连接等。

cpp 复制代码
#include <linux/kdev_t.h>  
#include <linux/kobject.h>  

/* struct cdev结构体,字符设备结构体 */
struct cdev {  
    struct kobject kobj;        // 内嵌的内核对象  
    struct module *owner;       // 拥有该字符设备的内核模块  
    const struct file_operations *ops; // 设备操作函数集  
    struct list_head list;      // 链接到全局字符设备列表  
    dev_t dev;                  // 设备号  
    unsigned int count;         // 次设备号数量  
};
cpp 复制代码
// 精简版 cdev_init 函数定义  
void cdev_init(struct cdev *cdev, const struct file_operations *fops)  
{  
    memset(cdev, 0, sizeof *cdev);    // 将cdev结构体清零  
    INIT_LIST_HEAD(&cdev->list);      // 初始化cdev的链表头 
    kobject_init(&cdev->kobj, ...);  // 初始化cdev的内核对象 
    cdev->ops = fops;                 // 将cdev与设备的操作函数集关联  
}

9.1.2 字符设备的注册

cdev_add()函数用来注册字符设备。为设备号赋值,将内核结构体与设备号关联。

cdev_del() 函数用来注销字符设备。将设备号归零,从内核结构体删除设备号。

字符设备的注册

cdev_add() 函数用于为字符设备struct cdev结构体的设备号成员赋值,也就是将内核结构体与设备号关联起来。

cpp 复制代码
//将字符设备添加到内核.并指定一个设备号,和子设备的数量.成功返回0,失败返回负数.
int cdev_add(struct cdev *p, //字符设备结构体指针
             dev_t dev,      //设备号
             unsigned count);//子设备数量

字符设备的注销

cdev_del()函数用于从内核中删除一个之前已经注册的 struct cdev结构体。

cpp 复制代码
//从内核删除字符设备结构体
void cdev_del(struct cdev *p);

在驱动程序卸载时,应调用 cdev_del()来注销设备,以确保系统资源的正确释放。

9.2 实验程序的编写

cpp 复制代码
#include <linux/cdev.h>  
#include <linux/fs.h>  
  
// 自定义的文件操作函数集  
static const struct file_operations my_fops = {  
    .owner = THIS_MODULE,  
    .open = my_open,  
    .release = my_release,  
    // ... 其他操作函数  
};  
  
// 字符设备结构体变量  
static struct cdev my_cdev;  
  
// 初始化函数  
static int __init my_init(void)  
{  
    // 初始化cdev结构体  
    cdev_init(&my_cdev, &my_fops);  
    // ... 其他初始化代码,如分配设备号、注册设备等  
    return 0;  
}  
  
// 模块退出函数  
static void __exit my_exit(void)  
{  
    cdev_del(&my_cdev);  // 注销设备、释放资源等  
}  
  
module_init(my_init);  
module_exit(my_exit);  
  
MODULE_LICENSE("GPL");

第十章 创建设备节点

10.1 创建设备节点

Linux 操作系统一切皆文件,设备访问也是通过文件的方式来进行的,用来进行设备访问的文件称之为设备节点,设备节点被创建在/dev 目录下。

设备节点的创建过程,是先创建设备类结构体,然后向设备类结构体对象添加设备号。

10.1.1 手动创建设备节点

mknod命令用于创建设备节点。

设备类型可以是 b(块设备)、c(字符设备)或 p(管道)。

需要指定设备的主设备号和从设备号。

bash 复制代码
mknod /dev/device_test c 236 0   #在/dev/目录下创建设备节点, c字符设备,主设备号,次设备号

10.1.2 自动创建设备节点

在Linux内核中,设备文件的自动创建通常通过udev(嵌入式系统中mdev作为简化版)机制实现。这一机制能够根据系统中硬件设备的状态自动创建或删除设备文件。

设备类(device class)。这个"class"是结构体名,含义是逻辑上的分组,用于将具有相似功能的设备组织在一起。

class_create() //动态创建设备节点,并将其添加到Linux内核系统中。

class_destroy() //从Linux内核系统中删除指定的设备节点。
device_create() //在指定设备节点中创建一个设备。

device_destroy() //删除指定设备节点中的设备。

cpp 复制代码
#include <linux/device.h> 

/* 动态创建设备节点 struct class*/  
struct class *class_create(struct module *owner,  //节点所属的模块
                           const char *name);      //代表节点的变量的名字

/* 删除设备节点体 */
void class_destroy(struct class *cls);
cpp 复制代码
/*在指定设备节点下创建一个设备,返回创建的设备指针*/
struct device *device_create(struct class *cls,      //设备节点 
                             struct device *parent,  //父设备,没有则NULL
                             dev_t devt,             //设备号                                        
                             void *drvdata,          //与设备相关的私有数据,没有则NULL
                             const char *fmt,        //用于格式化设备文件名
                             ...);

/*在指定设备节点对象下删除一个设备*/
void device_destroy(struct class *cls,  //指定的设备类结构体指针
                    dev_t devt);

udev是用户空间的守护进程,它通过netlink机制监听内核空间的设备事件来动态管理设备节点。

当内核检测到设备变化时,会发送 uevent事件到用户空间,udevd守护进程接收到事件后执行相应的操作。

10.2 实验程序的编写

cpp 复制代码
// 假设在驱动初始化函数中  
struct class *my_class;  
struct device *my_device;  
  
my_class = class_create(THIS_MODULE, "my_device_class");  
if (IS_ERR(my_class)) {  
    // 错误处理  
}  
  
// 假设已经定义了设备号devt  
dev_t devt = MKDEV(MAJOR_NUMBER, 0); // MAJOR_NUMBER是主设备号  
  
my_device = device_create(my_class, NULL, devt, NULL, "my_device%d", 0);  
if (IS_ERR(my_device)) {  
    // 错误处理,并销毁类(如果之前创建成功)  
    class_destroy(my_class);  
}  
  
// 在驱动卸载函数中  
device_destroy(my_class, devt); //先销毁设备 
class_destroy(my_class);        //再销毁设备类结构体

第十一章 字符设备驱动框架实验

在Linux内核中,字符设备通过申请设备号、注册字符设备(使用struct cdev结构体)并与设备号关联来注册。为了使用户空间访问这些设备,需要创建设备节点(通常位于/dev目录下)。

11.1 文件操作集简介

在Linux字符设备驱动开发中,struct file_operations 结构体是用来操作设备节点。

定义了一系列操作函数的指针,每个操作函数指针都被设置为指向相应的处理函数。

这些函数指针,如 read、write、open、release 和 unlocked_ioctl 等,对应着用户空间对设备的读写、打开、关闭和控制操作。 通常定义 file_operations 结构体后,会让 open 指向 chrdev_open,read 指向 chrdev_read 等。

当 close文件时,会调用 release函数执行清理工作,释放模块资源、减少模块使用计数等。

unlocked_ioctl 是 ioctl的变体,在调用时不用再上大内核锁(BKL),也就是调用之前不再先调用 lock_kernel()然后再 unlock_kernel()。

struct cdev 结构体则表示一个字符设备,通过 cdev_init() 初始化与 file_operations结构体的关联,然后通过 cdev_add() 将该字符设备注册到内核中。

cpp 复制代码
/*文件操作结构体,包含一系列文件操作的函数指针*/
struct file_operations {  
    struct module *owner;           // 指向拥有这个结构的模块的指针  
  
    loff_t (*llseek) (struct file *, loff_t, int);       // 改变文件当前的读写位置  
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 从设备中读取数据  
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 向设备写入数据  
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // 异步读  
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // 异步写  
  
    int (*readdir) (struct file *, void *, filldir_t);   // 读取目录内容(设备文件通常不使用)  
    unsigned int (*poll) (struct file *, struct poll_table_struct *); // 查询文件描述符状态  
  
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 解锁的ioctl  
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);   // 兼容的ioctl  
  
    int (*mmap) (struct file *, struct vm_area_struct *);               // 内存映射  
  
    int (*open) (struct inode *, struct file *);                        // 打开设备  
    int (*flush) (struct file *, fl_owner_t id);                        // 刷新待处理数据  
    int (*release) (struct inode *, struct file *);                     // 关闭设备  
  
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);          // 同步文件内容  
    int (*aio_fsync) (struct kiocb *, int datasync);                    // 异步文件同步  
  
    int (*fasync) (int, struct file *, int);                            // 文件异步通知  
  
    // 其他可能的成员根据内核版本和具体需求可能有所不同  
};

11.2 实验程序的编写

cpp 复制代码
#include <linux/init.h>  
#include <linux/module.h>  
#include <linux/fs.h>  
#include <linux/cdev.h>  
  
// 定义 file_operations 结构体  
static struct file_operations cdev_fops_test = {  
    .owner = THIS_MODULE,       //结构体所属模块
    .open = chrdev_open,        //open指针
    .read = chrdev_read,        //read指针
    .write = chrdev_write,      //write指针
    .release = chrdev_release,  //release指针
};  
  
static dev_t dev_num;  
static struct cdev cdev_test;  
static struct class *class_test;  
  
// 驱动入口函数  
static int __init chrdev_fops_init(void) {  
    int ret, major, minor;  

    /*动态分配设备号            设备号  设备号最小值  申请的设备数量    设备名称    */
    ret = alloc_chrdev_region(&dev_num,    0,             1,        "chrdev_name");  
    if (ret < 0) {  
        printk("alloc_chrdev_region is error \n");  
        return ret;  
    }  
    
    major = MAJOR(dev_num);  //获取主设备号  
    minor = MINOR(dev_num);  //获取次设备号
    printk("major is %d, minor is %d \n", major, minor);  
  
    /*字符设备初始化 将字符设备cdev和file_operation结构体绑定*/
    cdev_init(&cdev_test, &cdev_fops_test);  
    cdev_test.owner = THIS_MODULE;  

    /*字符设备注册,将cdev与设备号绑定*/
    ret = cdev_add(&cdev_test, dev_num, 1);  
    if (ret < 0) {  
        printk("cdev_add is error \n");  
        goto err_cdev_add;  
    }  
    
    /*设备节点struct class创建*/
    class_test = class_create(THIS_MODULE, "class_test");  

    /*创建设备节点  设备节点   父设备  设备号 私有数据  设备名*/
    device_create(class_test, NULL, dev_num, NULL, "device_test");  
  
    return 0;  
  
err_cdev_add:  
    /*移除注册的设备          设备号 设备数量*/
    unregister_chrdev_region(dev_num, 1);  //
    return ret;  
}  
  
// 驱动出口函数  
static void __exit chrdev_fops_exit(void) {  
    device_destroy(class_test, dev_num);  //销毁节点中的设备
    class_destroy(class_test);  //销毁设备节点
    cdev_del(&cdev_test);       //字符设备删除,将struct cdev与设备号解绑
    unregister_chrdev_region(dev_num, 1);  //设备号销毁
    printk("module exit \n");  
}  
  
module_init(chrdev_fops_init);  
module_exit(chrdev_fops_exit);  
MODULE_LICENSE("GPL v2");  
MODULE_AUTHOR("topeet");

第十二章 杂项设备驱动实验

12.1 杂项设备驱动简介

在Linux内核中,杂项设备(misc devices)提供了一种简化字符设备注册的方式,特别适用于那些不需要复杂设备号管理且操作相对简单的设备。使用杂项设备可以节省主设备号,并简化了设备驱动的编写过程。

定义一个杂项设备主要涉及填充 miscdevice结构体,该结构体通常包含以下关键成员:

minor:次设备号,可以设置为MISC_DYNAMIC_MINOR来让系统自动分配。

name:设备名称,注册成功后,在/dev目录下会生成以该名称命名的设备节点。

fops:指向 file_operations结构体的指针,定义了设备的操作函数集。

list:链表节点。

cpp 复制代码
/*杂项设备结构体*/
struct miscdevice {  
    int minor;                /* 子设备号,值若为 MISC_DYNAMIC_MINOR则自动分配 */  
    const char *name;         /* 设备名字,注册后会在/dev目录下生成相应的设备节点 */  
    const struct file_operations *fops; /* 设备操作集,函数指针 */  
    struct list_head list;    /* 链表节点,用于将所有miscdevice设备链接起来 */  
    struct device *parent;    /* 指向父设备的指针*/  
    struct device *this_device;/* 指向当前设备的指针*/  
    const struct attribute_group **groups; /* 属性组,用于设备属性管理 */  
    const char *nodename;     /* 设备节点名,通常与name相同 */  
    umode_t mode;             /* 设备模式,如文件权限等 */   
};

12.2 杂项设备的注册和卸载

int misc_register(); //杂项设备注册。将 miscdevice结构体挂载到 misc_list链表。
int misc_deregister(); //杂项设备卸载。将 miscdevice结构体从 misc_list链表移除。

cpp 复制代码
/*杂项设备注册.成功0,失败负数*/
int misc_register(struct miscdevice *misc);

/*杂项设备卸载,成功0,失败负数*/
int misc_deregister(struct miscdevice *misc);

12.3 杂项设备驱动框架

杂项设备驱动通常使用 file_operations结构体来定义设备操作(如读、写等),

并使用 miscdevice结构体来注册设备。

cpp 复制代码
// 定义文件操作函数集  
static struct file_operations xxx_fops = {  
    .owner = THIS_MODULE,  
    .read = xxx_read,  // 假设已实现xxx_read函数  
    // 其他操作函数如.write, .open, .release等根据需要定义  
};  
  
// 定义杂项设备  
static struct miscdevice xxx_dev = {  
    .minor = MISC_DYNAMIC_MINOR,  
    .name = "xxx",  
    .fops = &xxx_fops,  
};  
  
// 入口函数 
static int __init xxx_init(void) {  
    int ret;  
    printk(KERN_EMERG "xxx_init\n");  
    ret = misc_register(&xxx_dev);  // 注册杂项设备  
    if (ret < 0) {  
        printk(KERN_ERR "misc_register failed\n");  
    } else {  
        printk(KERN_INFO "misc_register ok\n");  
    }  
    return ret;  // 返回注册结果  
}  
  
// 出口函数  
static void __exit xxx_exit(void) {  
    printk(KERN_EMERG "xxx_exit\n");  
    misc_deregister(&xxx_dev);  // 卸载杂项设备  
}  
  
// 注册入口和出口函数  
module_init(xxx_init);  
module_exit(xxx_exit);  
  
// 模块信息  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("topeet");

第十三章 内核空间与用户空间数据交互实验

13.1 内核空间与用户空间

内核空间直接管理硬件资源,

用户空间的应用程序不能直接访问硬件,必须通过内核提供的系统调用来间接访问。
系统调用:应用程序主动请求内核服务,通过系统调用接口实现,如read()、write()等。

软中断:由当前正在执行的指令产生,如定时器到期、程序异常等,触发内核处理。

硬中断:由外部设备产生,通知CPU处理事件,CPU响应后执行相应的中断处理程序。

13.2 用户空间和内核空间数据交换

为了安全地在用户空间和内核空间传输数据,

Linux内核提供了 copy_to_user() 和 copy_from_user()这两个函数。

cpp 复制代码
/*从内核空间复制数据到用户空间缓冲区*/
unsigned long copy_to_user(void __user *to,     //用户空间缓冲区指针
                           const void *from,    //内核空间数据指针
                           unsigned long n);    //字节数
cpp 复制代码
/*从用户空间缓冲区复制数据到内核数据*/
unsigned long copy_from_user(void *to,                //内核数据
                             const void __user *from, //用户空间缓冲区指针
                             unsigned long n);        //字节数

13.3 实验程序编写

数据写入

当用户空间通过写操作(如write系统调用)向设备文件写入数据时,cdev_test_write函数会被调用。该函数使用 copy_from_user()函数将用户空间的数据安全复制到内核空间的缓冲区中。

cpp 复制代码
/*写操作函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)  
{  
    char kbuf[32] = {0}; // 定义内核缓冲区  
    if (copy_from_user(kbuf, buf, size) != 0) {  
        printk("copy_from_user error\r\n");  
        return -EFAULT; // 使用更标准的错误码  
    }  
    printk("kbuf is %s\r\n", kbuf);  
    return size; // 返回实际写入的字节数  
}

数据读取

当用户空间通过读操作(如read系统调用)从设备文件读取数据时,cdev_test_read函数会被调用。将内核空间的一个字符串通过 copy_to_user()函数安全地复制到用户空间提供的缓冲区中。

cpp 复制代码
/*读操作函数*/
static ssize_t cdev_test_read(struct file *file, //struct file结构体指针
                              char __user *buf,  //用户缓冲区
                              size_t size,       //字节数      
                              loff_t *off)       //loff_t类型的指针,
                                                //loff_t是大文件偏移类型,表示文件中的位置
{  
    const char kbuf[] = "This is cdev_test_read!"; // 定义内核空间的数据  
    size_t len = strlen(kbuf);  
    if (copy_to_user(buf, kbuf, min(len, size)) != 0) {  
        printk("copy_to_user error\r\n");  
        return -EFAULT; // 使用更标准的错误码  
    }  
    return len; // 返回实际读取的字节数(注意,不能超过用户提供的缓冲区大小)  
}

struct file表示一个打开的文件。

每当用户在用户空间通过系统调用(如 open)打开一个文件或设备时,内核都会创建一个 struct file 的实例来跟踪这个打开的文件或设备的相关信息。

cpp 复制代码
/*每当open一个文件时,都会创建一个Struct file来跟踪文件状态*/
struct file {  
    /* 文件的模式(只读、只写、读写等)和状态标志(如非阻塞、同步等) */  
    unsigned int f_mode;  
    unsigned int f_flags;  
  
    /* 当前的文件位置指针 */  
    loff_t f_pos;  
  
    /* 指向文件操作函数的指针表 */  
    const struct file_operations *f_op;  
  
    /* 文件的预读状态 */  
    struct f_ra_state f_ra;  
  
    /* 文件的状态标志,如是否已被删除、是否正在被写入等 */  
    unsigned long f_flags_high;  
    unsigned int f_count;  
  
    /* 文件的inode对象(表示文件在文件系统中的实际数据) */  
    struct dentry *f_dentry;  
    struct vfsmount *f_vfsmnt;  
  
    /* 文件的私有数据,设备驱动可以使用这个字段来存储特定于文件或设备的数据 */  
    void *f_private_data;  
  
    /* ... 其他字段,如错误状态、锁定信息等 ... */  
};

file_operations绑定

cpp 复制代码
/*设备操作函数,定义 file_operations 结构体类型的变量 cdev_test_fops*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将 open 字段指向 chrdev_open(...)函数
    .read = cdev_test_read, //将 open 字段指向 chrdev_read(...)函数
    .write = cdev_test_write, //将 open 字段指向 chrdev_write(...)函数
    .release = cdev_test_release, //将 open 字段指向 chrdev_release(...)函数
};

测试APP

cpp 复制代码
#include <stdio.h>  
#include <fcntl.h>  
#include <unistd.h>  
  
int main() {  
    int fd = open("/dev/test", O_RDWR); // 打开字符设备文件  
    if (fd < 0) {  
        perror("open error");  
        return 1; // 返回错误码  
    }  
  
    char buf1[32] = {0}; // 定义读取缓冲区  
    read(fd, buf1, sizeof(buf1)); // 从设备文件读取数据  
    printf("buf1 is %s\n", buf1); // 打印读取的数据  
  
    char buf2[] = "nihao"; // 定义写入缓冲区  
    write(fd, buf2, sizeof(buf2)); // 向设备文件写入数据  
  
    close(fd); // 关闭设备文件  
    return 0; // 成功执行  
}

从这里也能看出,每个设备驱动的设备号都跟 file_oprations结构体绑定,那些常见的系统调用如read、write、release等方法其实底层调用的是 file_operations结构体的成员指向的方法。

像 read、write这种系统调用更底层 是 copy_to_user()和 copy_from_user()。

第十四章 文件私有数据实验

14.1 文件私有数据简介

在Linux设备驱动开发中,使用 struct file结构体的 private_data字段来存储设备相关的私有数据是一种非常常见的做法。这种机制允许驱动程序在文件操作的不同阶段(如打开、读取、写入、关闭等)访问设备特定的信息。

open 函数中的设置

cpp 复制代码
/*open函数中的设置*/
static int cdev_test_open(struct inode *inode, struct file *file) {  
    file->private_data = &dev1;  
    return 0;  
}

14.2 驱动程序编写

cpp 复制代码
/*驱动文件*/
#include <linux/module.h>  
#include <linux/fs.h>  
#include <linux/cdev.h>  
#include <linux/uaccess.h>  
  
/*用于存储字符设备cdev和内核缓冲区*/
struct device_test {  
    struct cdev cdev;  
    char kbuf[32];  
};  
  
static struct device_test dev1;  

/*自己写文件操作函数*/
static int cdev_test_open(struct inode *inode, struct file *file)  
{  
    file->private_data = &dev1; // 设置私有数据  
    return 0;  
}  
  
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)  
{  
    struct device_test *test_dev = file->private_data;  
    if (copy_from_user(test_dev->kbuf, buf, size) != 0)  
        return -EFAULT;  
    return size;  
}  
  
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)  
{  
    struct device_test *test_dev = file->private_data;  
    if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf) + 1) != 0)  
        return -EFAULT;  
    return strlen(test_dev->kbuf);  
}  
  
static int cdev_test_release(struct inode *inode, struct file *file)  
{  
    return 0;  
}  

/*让 file_operations指向文件操作*/
static struct file_operations cdev_test_fops = {  
    .owner = THIS_MODULE,  
    .open = cdev_test_open,  
    .read = cdev_test_read,  
    .write = cdev_test_write,  
    .release = cdev_test_release,  
};  

/*入口函数*/
static int __init chr_fops_init(void)  
{  
    int ret;  
    ret = alloc_chrdev_region(&dev1.cdev.dev, 0, 1, "test_dev");  
    if (ret < 0)  
        return ret;  
    cdev_init(&dev1.cdev, &cdev_test_fops);  
    ret = cdev_add(&dev1.cdev, dev1.cdev.dev, 1);  
    if (ret)  
        goto fail;  
    // 类和设备的创建(为简洁起见,这里省略)  
    return 0;  
fail:  
    unregister_chrdev_region(dev1.cdev.dev, 1);  
    return ret;  
}  

/*出口函数*/
static void __exit chr_fops_exit(void)  
{  
    cdev_del(&dev1.cdev);  
    unregister_chrdev_region(dev1.cdev.dev, 1);  
    // 类和设备的销毁(为简洁起见,这里省略)  
}  
  
module_init(chr_fops_init);  
module_exit(chr_fops_exit);  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("topeet");

14.3 测试程序编写

cpp 复制代码
#include <stdio.h>  
#include <fcntl.h>  
#include <unistd.h>  
  
int main() {  
    int fd = open("/dev/test", O_RDWR); // 尝试打开设备文件  
    if (fd < 0) {  
        perror("Failed to open /dev/test");  
        return 1; // 错误退出  
    }  
  
    char buf1[32] = "nihao"; // 定义并初始化写入缓冲区  
    write(fd, buf1, sizeof(buf1)); // 向设备文件写入数据  
  
    close(fd); // 关闭设备文件  
    return 0; // 成功退出  
}

第十五章 一个驱动兼容不同设备实验

Linux设备驱动通过主设备号来标识驱动类型,通过次设备号来区分同一类型下的不同设备。当多个设备共享同一驱动但次设备号不同时,驱动内部通过为每个设备分配并维护一个独立的私有数据结构(private_data)来区分和管理这些设备。

15.1 container_of 函数简介

container_of 宏通过结构体成员的指针、结构体的类型以及该成员在结构体中的名称,计算出并返回包含该成员的结构体指针。

cpp 复制代码
#define container_of(ptr, type, member) ({ \  
    const typeof(((type *)0)->member) *__mptr = (ptr); \    
    (type *)((char *)__mptr - offsetof(type, member)); \    //用已知地址减去偏移量,得起始地址
})

/*
(type *)0:将数字 0 强制转换为 type 类型的指针。
这里 0 仅仅是一个占位符,表示一个不存在的地址,但类型是正确的。
*/

15.2 驱动程序编写

定义用来管理设备信息的结构体,包含struct cdev成员

cpp 复制代码
/*使用结构体管理设备的信息*/
struct device_test {  
    dev_t dev_num;             //设备号 
    int major, minor;          //主设备号
    struct cdev cdev_test;     //字符设备结构体
    struct class *class;       //设备节点
    struct device *device;     //设备指针,使用device_create在节点下创建的设备
    char kbuf[32];            //内核缓冲区
};

struct device_test dev1; //定义一个 device_test 结构体变量 dev1
struct device_test dev2; //定义一个 device_test 结构体变量 dev2

file_operations定义与初始化

cpp 复制代码
/*设备操作函数,定义 file_operations 结构体类型的变量 cdev_test_fops*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将 open 字段指向 chrdev_open(...)函数
    .read = cdev_test_read, //将 open 字段指向 chrdev_read(...)函数
    .write = cdev_test_write, //将 open 字段指向 chrdev_write(...)函数
    .release = cdev_test_release, //将 open 字段指向 chrdev_release(...)函数
};

驱动入口函数

一个驱动兼容不同的设备,申请设备号时指明子设备的范围,多个设备的设备号注册到同一个设备节点下。

cpp 复制代码
static int __init chr_fops_init(void) // 驱动入口函数  
{  
    /* 注册字符设备驱动 */  
    int ret;  
  
    /* 1 创建设备号, 注册 2 个设备号 */  
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name"); // 动态分配设备号  
    if (ret < 0) {  
        printk("alloc_chrdev_region is error\n");  
        return ret; // 添加返回值,以便在错误时退出  
    }  
    printk("alloc_chrdev_region is ok\n");  
  
    /*获取 设备 1 的主设备号和次设备号*/
    dev1.major = MAJOR(dev1.dev_num); // 获取主设备号  
    dev1.minor = MINOR(dev1.dev_num); // 获取次设备号  
    printk("major is %d \r\n", dev1.major); // 打印主设备号  
    printk("minor is %d \r\n", dev1.minor); // 打印次设备号  
  
    // 对设备 1 进行初始化和注册
    /* 2 初始化 cdev,将file_operations与cdev绑定 */  
    dev1.cdev_test.owner = THIS_MODULE;  
    cdev_init(&dev1.cdev_test, &cdev_test_fops);  
  
    /* 3 将 cdev注册到内核, 指定设备号和子设备数量 */  
    cdev_add(&dev1.cdev_test, dev1.dev_num, 1);  
  
    /* 4 创建字符节点 */  
    dev1.class = class_create(THIS_MODULE, "test1");  
  
    /* 5 字符节点下创建设备 */  
    dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");  
  
    //获取设备 2 的主设备号和次设备号 
    dev2.major = MAJOR(dev1.dev_num + 1); // 获取主设备号  
    dev2.minor = MINOR(dev1.dev_num + 1); // 获取次设备号
    printk("major for dev2 is %d \r\n", dev2.major); // 打印主设备号  
    // 注意:对于 dev2 的次设备号,您可能需要单独处理  
  
    // 对设备2进行初始化和注册处理
    /* 2 初始化 cdev */  
    dev2.cdev_test.owner = THIS_MODULE;  
    cdev_init(&dev2.cdev_test, &cdev_test_fops);  
  
    /* 3 添加一个 cdev, 完成字符设备注册到内核 */  
    cdev_add(&dev2.cdev_test, MKDEV(dev2.major, 0), 1); // 使用 MKDEV 来生成设备号  
  
    /* 4 创建类(通常两个设备可以使用同一个类,除非有特别的理由分开) */  
    // 如果 dev2 需要独立的类,则取消注释以下行  
    // dev2.class = class_create(THIS_MODULE, "test2");  
  
    /* 5 创建设备(这里 dev2 使用与 dev1 相同的类,也就是相同的设备节点) */  
    dev2.device = device_create(dev1.class, NULL, MKDEV(dev2.major, 0), NULL, "test2");  
  
    return 0;  
}

打开设备

想要一个驱动兼容多个不同的设备,该驱动装载时,要在入口函数注册不同的子设备,因此/dev下会有不同设备名的设备文件。

通过open+文件名的方式打开不同子设备文件时,进入这些子设备绑定的同一个 file_operations结构体获取操作方法。

因此可以通过 struct file* file->privatedata,在打开文件时,获取设备文件的用来管理 cdev结构体的结构体,从而判断打开的是哪个设备,在写入时执行不同的操作。实现一个驱动兼容不同设备。(其实完全可以通过 inode直接获取cdev然后获取设备号来区分设备,这里主要是讲container_of和 inode的用法)

在 open文件时,使用 container_of宏传入 inode结构体的 i_cdev指针,得到指针指向的包含 cdev指针成员的结构体,并传入文件私有数据 file->privatedata。

当你看到

cpp 复制代码
container_of(inode->i_cdev,
             struct device_test,
             cdev_test) 

它是在说:"我有一个指向 cdev 结构体的指针(即 inode->i_cdev),

我知道这个 cdev 结构体实际上是 struct device_test 结构体中的一个成员(即 cdev_test)。

因此,我可以通过这个 cdev 指针和 cdev_test 成员在 struct device_test 中的偏移量,反向计算出整个 struct device_test 结构体的地址。"

可以这么理解,这个指针是inode的成员,而指针指向的cdev不是inode的成员,而是cdevrest的成员,而通过 container_of得到的是 cdev成员的所属结构体。

注意,这里 container_of从 inode的 cdev指针成员,得到的不是 inode的首地址,而是 cdev指针成员指向的 cdev实例的所属结构体的首地址。

所注册的 cdev实例对于整个设备文件应该是唯一的。

cpp 复制代码
/*打开设备*/
static int cdev_test_open(struct inode *inode, struct file *file) { 
    /*通过container_of,根据设备的inode节点的设备号,获取结构体指针*/
    file->private_data = container_of(inode->i_cdev,      //结构体成员指针.
                                      struct device_test, //结构体类型
                                      cdev_test);         //结构体成员名字
    printk("This is cdev_test_open\n");  
    return 0;  
}

/*该函数与 file_operators绑定后,在系统调用Open该设备时调用,参数自动传入*/

//每个文件被open的时候,都会产生一个inode结构体来跟踪文件
//inode结构体的i_cdev指向之前通过cdev_add(cdev,设备号,设备数量)添加进内核的设备结构体cdev

inode 结构体包含了文件的元数据(如权限、所有者、代表的字符设备cdev 等),而 file 结构体则包含了打开文件的状态信息(如当前的读写位置、打开的文件模式等)。

在 Linux 内核的字符设备驱动中,inode 结构体代表了文件系统中的一个节点,而 inode->i_cdev 指向该驱动文件通过 cdev_add()注册进内核的 cdev结构体。

cpp 复制代码
int cdev_add(struct cdev *p, //字符设备结构体指针
             dev_t dev,      //设备号
             unsigned count);//子设备数量

写入设备

cpp 复制代码
/* 向设备写入数据函数 */  
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)  
{  
    struct device_test *test_dev = (struct device_test *)file->private_data;  
  
    // 如果次设备号是 0,则为 dev1  
    if (test_dev->minor == 0) {  
        if (copy_from_user(test_dev->kbuf, buf, size) != 0) { // copy_from_user: 用户空间向内核空间传数据  
            printk("copy_from_user error\r\n");  
            return -1;  
        }  
        printk("test_dev->kbuf (dev1) is %s\r\n", test_dev->kbuf);  
    }  
    // 如果次设备号是 1,则为 dev2  
    else if (test_dev->minor == 1) {  
        if (copy_from_user(test_dev->kbuf, buf, size) != 0) { // copy_from_user: 用户空间向内核空间传数据  
            printk("copy_from_user error\r\n");  
            return -1;  
        }  
        printk("test_dev->kbuf (dev2) is %s\r\n", test_dev->kbuf);  
    }  
  
    return 0;  
}

15.3 测试程序编写

cpp 复制代码
#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
  
int main(int argc, char *argv[]) {  
    int fd1; // 定义设备 1 的文件描述符  
    int fd2; // 定义设备 2 的文件描述符  
    char buf1[32] = "nihao /dev/test1"; // 定义写入缓存区 buf1  
    char buf2[32] = "nihao /dev/test2"; // 定义写入缓存区 buf2  
  
    fd1 = open("/dev/test1", O_RDWR); // 打开设备 1:test1  
    if (fd1 < 0) {  
        perror("open error \n");  
        return fd1;  
    }  
  
    write(fd1, buf1, sizeof(buf1)); // 向设备 1 写入数据  
    close(fd1); // 取消文件描述符到文件的映射  
  
    fd2 = open("/dev/test2", O_RDWR); // 打开设备 2:test2  
    if (fd2 < 0) {  
        perror("open error \n");  
        return fd2;  
    }  
  
    write(fd2, buf2, sizeof(buf2)); // 向设备 2 写入数据  
    close(fd2); // 取消文件描述符到文件的映射  
  
    return 0;  
}

第十六章 Linux 错误处理实验

16.1 goto 语句错误处理示例

cpp 复制代码
int init my_init_function(void)
{
    int err;
    err = register_this(ptr1, "skull");
    if (err)
        goto fail_this;
    err = register_that(ptr2, "skull");
    if (err)
        goto fail_that;
    err = register_those(ptr3, "skull");
    if (err)
        goto fail_those;
    return 0;

    fail_those:
        unregister_that(ptr2, "skull");
    fail_that:
        unregister_this(ptr1, "skull");
    fail_this:
        return err;
}

16.2 IS_ERR()简介

在 Linux 内核中,对于错误处理有一个独特且高效的机制,它利用了内核地址空间的一部分来存储错误信息,而不是直接返回错误码。

这种机制通过 ERR_PTR、IS_ERR 和 PTR_ERR 等宏来实现,使得错误处理和返回值检查变得更加直接和统一。

1. 错误指针的概念

​​​​​​​ 错误指针(也称为无效指针)是指向内核地址空间特定区域(通常是内核地址空间的高端部分)的指针。这些区域并不实际映射到任何物理内存或设备,而是被保留用作错误码的表示。对于 64 位系统,通常 0xfffffffffffff000 到 0xffffffffffffffff 这段地址就是这样一个区域。

2. 相关宏的解释

ERR_PTR(error):

接受一个错误码,返回错误指针来报告错误。
IS_ERR(ptr):

这个宏检查给定的指针是否是一个错误指针。就是看地址是否在特定区域。
PTR_ERR(ptr):

用于从错误指针中提取原始的错误码。

它通过一些位操作(如掩码和位移)来实现,因为错误指针实际上是通过将错误码转换为特定范围内的地址来创建的。

16.3 驱动程序编写

cpp 复制代码
/*驱动程序的一部分*/
/*5 创建设备*/
dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
if(IS_ERR(dev1.device)) //判断是否错误指针
{
    ret=PTR_ERR(dev1.device);//提取错误码
    goto err_device_create;  //跳转到错误处理
}
return 0;

err_device_create:
    class_destroy(dev1.class); //删除类

err_class_create:
    cdev_del(&dev1.cdev_test); //删除 cdev

err_chr_add:
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

return ret;

第十七章 点亮 LED 灯实验

RK3568一共有 5组GPIO,也就是GPIO0~4。

每组GPIO为一个Bank,每个Bank有 32引脚,包括 Group (GPIOA(0~7) ~ D( 0~7)) 。

也就是一共 5*4*8=160个引脚。

17.1 查看原理图

LED原理图例子

由上图可以看出,LED 灯是由 GPIO0_B7 控制的。

当 GPIO0_B7 为高电平时,三极管 Q16导通,LED9 点亮。当 GPIO0_B7 为低电平时,三极管 Q16 截止,LED9 不亮。

17.2 查询寄存器地址

一般情况下需要对 GPIO 的复用寄存器方向寄存器数据寄存器进行配置。

有五个GPIO(GPIO0在PD_PMU中,GPIO1/GPIO2/GPIO3/GPIO4在PD_BUS中),并且它们每个都拥有相同的寄存器组。因此,这五个GPIO的寄存器组拥有五个不同的基地址

本章以 GPIO0举例。

17.2.1 查询复用寄存器

查询 复用寄存器的基地址
查询复用寄存器的基地址

查询 GPIOB的复用寄存器的偏移地址
查询复用寄存器的偏移地址

复用寄存器功能设置位域
查看复用寄存器功能设置的位域

我们要控制 led灯,因此要将引脚复用为 GPIO功能。

复用寄存器的位域功能,高16位为低16位的写使能,低16位的[14:12]为 GPIO0B7的复用功能设置。

复用寄存器地址 = 基地址+偏移地址=0xFDC2000C

使用 io 命令查看此寄存器的地址:

bash 复制代码
#使用io命令查看寄存器地址的值
io -r -4 0xFDC2000C

#-r 代表读取
#-4 代表4字节

可知寄存器值为 00000001,[14:12]位为 000,如下图(图 18-6)所示,

所以默认设置的为 gpio 功能。

17.2.2 查询方向寄存器

方向寄存器DDR有32位,分了高 16位和 低 16位。高16位使能低16位。

查找GPIO0的基地址
查看address mapping

GPIO0 的基地址为 0xFDD60000。

方向寄存器的地址 = GPIOx基地址+方向寄存器偏移地址

=0xFDD60000+0x0008=0xFDD60008

使用IO命令查看方向寄存器地址的值:
io命令查看方向寄存器DDR的值

转换成二进制,发现DDR_L第15位默认为1,也就是默认GPIO方向为输出。

查找方向寄存器的偏移地址

方向寄存器的偏移地址为 0x0008。
方向寄存器偏移地址

接着查看 GPIO_SWPORT_DDR_L 寄存器的具体描述,

[31:16]位属性是 WO,也就是只可写入。

[31:16]位是写标志位,是低 16 位的写使能。如果低 16 位中某一位要设置输入输入输出,则对应高位写标志也应该设置为 1。

[15:0] 是数据方向控制寄存器低位,如果要设置某个 GPIO 为输出,则对应位置 1,如果要设置某个 GPIO 为输入,则对应位置 0。

对于GPIO0 B7 ,就是 DDR寄存器的第16位控制。

17.2.3 查询数据寄存器

查询 GPIO0的基地址。

查询数据寄存器偏移地址。

所以数据寄存器的地址为GPIO0基地址+DR偏移地址=0xFDD60000。

数据寄存器也是 高16位控制低16位的写使能,低16位对应 ABCD的分组引脚。

IO指令查看数据寄存器的原值。发现为 0x0000c040。因此要写入数据寄存器对应引脚位给1来点亮灯,则需要写入 0x80004040。

17.3 驱动程序编写

cpp 复制代码
/*物理地址映射成虚拟地址,返回unsigned int*/
ioremap(GPIO_DR, 4); //返回映射后的虚拟地址,对虚拟地址复制等于对物理地址赋值

通过在应用层传入 0/1 数据到内核,如果传入数据是 1,则设置 GPIO 的数据寄存器值为 0x8000c040,如果应用层传入 0,则设置 GPIO的数据寄存器值为 0x80004040,这样就可以达到控制 led 的效果。

cpp 复制代码
#include <linux/io.h>

#define GPIO_DR 0xFDD60000

struct device_test{
    dev_t dev_num; //设备号
    int major ; //主设备号
    int minor ; //次设备号
    struct cdev cdev_test; // cdev
    struct class *class; //类
    struct device *device; //设备
    char kbuf[32];
    unsigned int *vir_gpio_dr;
};

struct device_test dev1;

//驱动入口函数
static int __init chr_fops_init(void) 
{
    ...
    /*本实验重点*****/
    dev1.vir_gpio_dr=ioremap(GPIO_DR,4); //将物理地址映射到4字节的虚拟地址
                                         //之后可以直接通过虚拟地址访问物理地址
    ...
}


/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    //获取用来管理cdev的结构体
    struct device_test *test_dev=(struct device_test *)file->private_data;

    // copy_from_user:用户空间向内核空间传数据
    if (copy_from_user(test_dev->kbuf, buf, size) != 0) 
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    if(test_dev->kbuf[0]==1){                     //如果应用层传入的数据是 1,则打开灯
        *(test_dev->vir_gpio_dr) = 0x8000c040;    //设置数据寄存器的值
        printk("test_dev->kbuf [0] is %d\n",test_dev->kbuf[0]); //打印传入的数据
    }
    else if(test_dev->kbuf[0]==0){                 //如果应用层传入的数据是 0,则关闭灯
        *(test_dev->vir_gpio_dr) = 0x80004040;   //设置数据寄存器的值
        printk("test_dev->kbuf [0] is %d\n",test_dev->kbuf[0]); //打印传入的数据
    }
    return 0;
}
相关推荐
糖豆豆今天也要努力鸭7 分钟前
torch.__version__的torch版本和conda list的torch版本不一致
linux·pytorch·python·深度学习·conda·torch
烦躁的大鼻嘎15 分钟前
【Linux】深入理解GCC/G++编译流程及库文件管理
linux·运维·服务器
乐大师15 分钟前
Deepin登录后提示“解锁登陆密钥环里的密码不匹配”
运维·服务器
ac.char22 分钟前
在 Ubuntu 上安装 Yarn 环境
linux·运维·服务器·ubuntu
敲上瘾22 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
长弓聊编程41 分钟前
Linux系统使用valgrind分析C++程序内存资源使用情况
linux·c++
cherub.1 小时前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
梅见十柒1 小时前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热1 小时前
路由基础(全)
linux·网络·网络协议·安全
传而习乎1 小时前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos