最全Linux驱动开发全流程详细解析(持续更新)
Linux驱动开发详细解析
一、驱动概念
驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁。
- 具体任务
- 读写设备寄存器(实现控制的方式)
- 完成设备的轮询、中断处理、DMA通信(CPU与外设通信的方式)
- 进行物理内存向虚拟内存的映射(在开启硬件MMU的情况下)
- 说明:设备驱动的两个任务方向
- 操作硬件(向下)
- 将驱动程序通入内核,实现面向操作系统内核的接口内容,接口由操作系统实现(向上)
(驱动程序按照操作系统给出的独立于设备的接口设计,应用程序使用操作系统统一的系统调用接口来访问设备)
Linux系统主要部分:内核、shell、文件系统、应用程序
- 内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统
- 分层设计的思想让程序间松耦合,有助于适配各种平台
- 驱动的上面是系统调用,下面是硬件
二、驱动分类
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
- 字符设备(Char Device)
- 字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。
- 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生。
- 字符设备驱动程序通常至少要实现open、close、read和write系统调用。
- 比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输。
- 块设备(Block Device)
- 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。
- 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。
- 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。
- 只有一个块设备可以支持一个安装的文件系统。
- 比如我们常见的电脑硬盘、SD卡、U盘、光盘等。
- 网络设备(Net Device)
- 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。
- 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。
- 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是read、write等。
- 比如我们常见的网卡设备、蓝牙设备。
三、驱动程序的功能
- 对设备初始化和释放
- 把数据从内核传送到硬件和从硬件读取数据
- 读取应用程序传送给设备文件的数据和回送应用程序请求的数据
- 检测和处理设备出现的错误
四、驱动开发前提知识
4.1 内核态和用户态
- Kernel Mode(内核态)
- 内核模式下(执行内核空间的代码),代码具有对硬件的所有控制权限。可以执行所有CPU指令,可以访问任意地址的内存
- User Mode(用户态)
- 在用户模式下(执行用户空间的代码),代码没有对硬件的直接控制权限,也不能直接访问地址的内存。
- 只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址。
- 程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存
Linux利用CPU实现内核态和用户态
- ARM:内核态(svc模式),用户态(usr模式)
- x86 : 内核态(ring 0 ),用户态(ring 3)// x86有ring 0 - ring3四种特权等级
Linux实现内核态和用户态切换
-
ARM Linux的系统调用实现原理是采用swi软中断从用户态切换至内核态
-
X86是通过int 0x80中断进入内核态
Linux只能通过系统调用和硬件中断从用户空间进入内核空间
- 执行系统调用的内核代码运行在进程上下文中,他代表调用进程执行操作,因此能够访问进程地址空间的所有数据
- 处理硬件中断的内核代码运行在中断上下文中,他和进程是异步的,与任何一个特定进程无关通常,一个驱动程序模块中的某些函数作为系统调用的一部分,而其他函数负责中断处理
4.2 Linux下应用程序调用驱动程序流程
- Linux下进行驱动开发,完全将驱动程序与应用程序隔开,中间通过C标准库函数以及系统调用完成驱动层和应用层的数据交换。
- 驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对“/dev/xxx” (xxx 是具体的驱动文件名字) 的文件进行相应的操作即可实现对硬件的操作。
- 用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法 来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作
- 每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。
大致流程
- 加载一个驱动模块,产生一个设备文件,有唯一对应的inode结构体
- 应用层调用open函数打开设备文件,对于上层open调用到内核时会发生一次软中断,从用户空间进入到内核空间。
- open会调用到sys_open(内核函数),sys_open根据文件的地址,找到设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
- 根据struct inode结构体里面记录的主设备号和次设备号,在驱动链表(管理所有设备的驱动)里面,根据找到字符设备驱动
- 每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口
- 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
- 执行xxx_open驱动函数。
4.3 内核模块
Linux 驱动有两种运行方式
- 将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。
- 将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用相应命令加载驱动模块。
- 内核模块是Linux内核向外部提供的一个插口
- 内核模块是具有独立功能的程序,他可以被单独编译,但不能单独运行。他在运行时被链接到内核作为内核的一部分在内核空间运行
- 内核模块便于驱动、文件系统等的二次开发
内核模块组成
-
模块加载函数
module_init(xxx_init);
- module_init 函数用来向 Linux 内核注册一个模块加载函数,
- 参数 xxx_init 就是需要注册的具体函数(理解是模块的构造函数)
- 当加载驱动的时, xxx_init 这个函数就会被调用
-
模块卸载函数
module_exit(xxx_exit);
- module_exit函数用来向 Linux 内核注册一个模块卸载函数,
- 参数 xxx_exit 就是需要注册的具体函数(理解是模块的析构函数)
- 当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用
-
模块许可证明
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息 ,LICENSE 采用 GPL 协议
-
模块参数(可选)
模块参数是一种内核空间与用户空间的交互方式,只不过是用户空间 --> 内核空间单向的,他对应模块内部的全局变量 -
模块信息(可选)
MODULE_AUTHOR("songwei") //添加模块作者信息
-
模块打印 printk
printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。
printk函数主要做两件事情:①将信息记录到log中 ②调用控制台驱动来将信息输出
-
printk 可以根据日志级别对消息进行分类,一共有 8 个日志级别
#define KERN_SOH "\\001" #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */ #define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */ #define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/ #define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */ #define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */ #define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */ #define KERN_INFO KERN_SOH "6" /* 提示性的信息 */ #define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
-
以下代码就是设置“gsmi: Log Shutdown Reason\\n”这行消息的级别为 KERN_EMERG。
printk(KERN_DEBUG"gsmi: Log Shutdown Reason\\n");
如果使用 printk 的时候不显式的设置消息级别,那 么printk 将会采用默认级别MESSAGE_LOGLEVEL_DEFAULT,默认为 4。
-
在 include/linux/printk.h 中有个宏 CONSOLE_LOGLEVEL_DEFAULT,定义如下:
#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。
这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制台上的。
模块操作命令
- 加载模块
- insmod XXX.ko
- 为模块分配内核内存、将模块代码和数据装入内存、通过内核符号表解析模块中的内核引用、调用模块初始化函数(module_init)
- insmod要加载的模块有依赖模块,且其依赖的模块尚未加载,那么该insmod操作将失败
- modprobe XXX.ko
- 加载模块时会同时加载该模块所依赖的其他模块,提供了模块的依赖性分析、错误检查、错误报告
- insmod XXX.ko
- 卸载模块
- rmmod XXX.ko
- 查看模块信息
- lsmod
- 查看系统中加载的所有模块及模块间的依赖关系
- modinfo (模块路径)
- 查看详细信息,内核模块描述信息,编译系统信息
- lsmod
4.4 设备号
- Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成
- 主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
- Linux 提供了一个名为 dev_t 的数据类型表示设备号其中高 12 位为主设备号, 低 20 位为次设备
- 使用"cat /proc/devices"命令即可查看当前系统中所有已经使用了的设备号(主)
MAJOR // 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
MINOR //用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
MKDEV //用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
4.5 地址映射
MMU(Memory Manage Unit)内存管理单元
- 完成虚拟空间到物理空间的映射
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
- 对于 32 位的处理器来说,虚拟地址(VA,Virtual Address)范围是 2^32=4GB
内存映射函数
CPU只能访问虚拟地址,不能直接向寄存器地址写入数据,必须得到寄存器物理地址在Linux系统中对应的虚拟地址。
物理内存和虚拟内存之间的转换,需要用到: ioremap 和 iounmap两个函数
-
ioremap,用于获取指定物理地址空间对应的虚拟地址空间
/* phys_addr:要映射给的物理起始地址(cookie) size:要映射的内存空间大小 mtype: ioremap 的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC, ioremap 函数选择 MT_DEVICE 返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址 */ #define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype) {return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0)); }
例:获取某个寄存器对应的虚拟地址
#define addr (0X020E0068) // 物理地址 static void __iomem* va; //指向映射后的虚拟空间首地址的指针 va=ioremap(addr, 4); // 得到虚拟地址首地址
-
iounmap,卸载驱动使用 iounmap 函数释放掉 ioremap 函数所做的映射。
参数 addr:要取消映射的虚拟地址空间首地址iounmap(va);
I/O内存访问函数
当外部寄存器或外部内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。
使用 ioremap 函数将寄存器的物理地址映射到虚拟地址后,可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
-
读操作函数
u8 readb(const volatile void __iomem *addr) u16 readw(const volatile void __iomem *addr) u32 readl(const volatile void __iomem *addr)
readb、 readw 和 readl 分别对应 8bit、 16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值是读取到的数据
-
写操作函数
void writeb(u8 value, volatile void __iomem *addr) void writew(u16 value, volatile void __iomem *addr) void writel(u32 value, volatile void __iomem *addr)
writeb、 writew 和 writel分别对应 8bit、 16bit 和 32bit 写操作,参数 value 是要写入的数值, addr 是要写入的地址。
五、设备树
Device Tree是一种描述硬件的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。
Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性
-
引入设备树之前:一些与硬件设备相关的具体信息都要写在驱动代码中,如果外设发生相应的变化,那么驱动代码就需要改动。
-
引入设备树之后:通过设备树对硬件信息的抽象,驱动代码只要负责处理逻辑,而关于设备的具体信息存放到设备树文件中。如果只是硬件接口信息的变化而没有驱动逻辑的变化,开发者只需要修改设备树文件信息,不需要改写驱动代码。
5.1 DTS、DTB和DTC
- DTS
- 设备树源码文件,硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts
- DTSI
- 对于一些相同的dts配置可以抽象到dtsi文件中,然后可以用include的方式到dts文件中
- 同一芯片可以做一个dtsi,不同的板子不同的dts,然后include同一dtsi
- 对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置
- DTC
- dtc是编译dts的工具
- DTB
- dts经过dtc编译之后会得到dtb文件,设备树的二进制执行文件
- dtb通过Bootloader引导程序加载到内核。
5.2 设备树框架
1.根节点:\\2.设备节点:nodex①节点名称:node②节点地址:node@0, @后面即为地址3.属性:属性名称(Property name)和属性值(Property value)4.标签
- “/”是根节点,每个设备树文件只有一个根节点。在设备树文件中会发现有的文件下也有“/”根节点,这两个“/”根节点的内容会合并成一个根节点。
- Linux 内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹
5.3 DTS语法
dtsi头文件
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts 设备树文件中,还可以通过“#include”来引用.h、 .dtsi 和.dts 文件。
设备节点
-
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,
-
每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。
label: node-name@unit-address label:节点标签,方便访问节点:通过&label访问节点,追加节点信息 node-name:节点名字,为字符串,描述节点功能 unit-address:设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略
-
设备树源码中常用的几种数据形式
1.字符串: compatible = "arm,cortex-a7";设置 compatible 属性的值为字符串“arm,cortex-a7” 2.32位无符号整数:reg = <0>; 设置reg属性的值为0 3.字符串列表:字符串和字符串之间采用“,”隔开 compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand"; 设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
属性
-
compatible属性(兼容属性)
cpp "manufacturer,model" manufacturer:厂商名称 model:模块对应的驱动名字
例:
imx6ull-alientekemmc.dts 中 sound 节点是 音频设备节点,采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:
cpp compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
在根节点来说,Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。如果不支持的话那么这个设备就没法启动 Linux 内核。
-
model属性
model 属性值是一个字符串,一般 model 属性描述设备模块信息。 -
status属性
status 属性和设备状态有关的, status 属性值是字符串,描述设备的状态信息。
-
#address-cells 和#size-cells 属性
用于描述子节点的地址信息,reg属性的address 和 length的字长。
- #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),
- #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
- 子节点的地址信息描述来自于父节点的#address-cells 和#size-cells的值,而不是该节点本身的值(当前节点的信息是描述子节点的,自己的信息在父节点里)
//每个“address length”组合表示一个地址范围, //其中 address 是起始地址, length 是地址长度, //#address-cells 表明 address 这个数据所占用的字长, // #size-cells 表明 length 这个数据所占用的字长. reg = <address1 length1 address2 length2 address3 length3……>
-
reg属性
reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息, reg 属性的值一般是(address, length)对.例
uart1: serial@02020000 {compatible = "fsl,imx6ul-uart","fsl,imx6q-uart", "fsl,imx21-uart";reg = <0x02020000 0x4000>;interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_UART1_IPG>,<&clks IMX6UL_CLK_UART1_SERIAL>;clock-names = "ipg", "per";status = "disabled"; };
uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、 #sizecells = <1>,因此 reg 属性中 address=0x02020000, length=0x4000。都是字长为1.
-
ranges属性
-
ranges属性值可以为空或者按照( child-bus-address , parent-bus-address , length )格式编写的数字
-
ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成。
-
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长 parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长 length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
-
-
特殊节点
在根节点“/”中有两个特殊的子节点: aliases 和 chosen
-
aliases
aliases {can0 = &flexcan1;can1 = &flexcan2;...usbphy0 = &usbphy1;usbphy1 = &usbphy2; };
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。
但是,一般会在节点命名的时候会加上 label,然后通过&label来访问节点。
-
chosen
chosen 不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据(bootargs 参数)。
-
5.4 OF操作函数
Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_” (称为OF 函数)
查找节点
Linux 内核使用 device_node 结构体来描述一个节点:
struct device_node {const char *name; /* 节点名字 */const char *type; /* 设备类型 */phandle phandle;const char *full_name; /* 节点全名 */struct fwnode_handle fwnode;struct property *properties; /* 属性 */struct property *deadprops; /* removed 属性 */struct device_node *parent; /* 父节点 */struct device_node *child; /* 子节点...
}
-
通过节点名字查找指定的节点:of_find_node_by_name
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败。 -
通过 device_type 属性查找指定的节点:of_find_node_by_type
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串, device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败 -
通过device_type 和 compatible两个属性查找指定的节点:of_find_compatible_node
struct device_node *of_find_compatible_node(struct device_node *from,const char *type,const char *compatible)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL
compatible: 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败 -
通过 of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match
struct device_node *of_find_matching_node_and_match(struct device_node *from,const struct of_device_id *matches,const struct of_device_id **match)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches: of_device_id 匹配表,在此匹配表里面查找节点。
match: 找到的匹配的 of_device_id。
返回值: 找到的节点,如果为 NULL 表示查找失败 -
通过路径来查找指定的节点:of_find_node_by_path
inline struct device_node *of_find_node_by_path(const char *path)
path:设备树节点中绝对路径的节点名,可以使用节点的别名
返回值: 找到的节点,如果为 NULL 表示查找失败
获取属性值
Linux 内核中使用结构体 property 表示属性
struct property {char *name; /* 属性名字 */int length; /* 属性长度 */void *value; /* 属性值 */struct property *next; /* 下一个属性 */unsigned long _flags;unsigned int unique_id;struct bin_attribute attr;
}
-
查找指定的属性:of_find_property
property *of_find_property(const struct device_node *np,const char *name,int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数,一般为NULL
返回值: 找到的属性。 -
获取属性中元素的数量(数组):of_property_count_elems_of_size
int of_property_count_elems_of_size(const struct device_node *np,const char *propnameint elem_size)
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值: 得到的属性元素数量 -
从属性中获取指定标号的 u32 类型数据值:of_property_read_u32_index
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index,u32 *out_value)
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值: 0 读取成功;
负值: 读取失败,
-EINVAL 表示属性不存在
-ENODATA 表示没有要读取的数据,
-EOVERFLOW 表示属性值列表太小 -
读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
of_property_read_u8_array of_property_read_u16_array of_property_read_u32_array of_property_read_u64_array int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values,size_t sz)
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
sz: 要读取的数组元素数量。
返回值: 0:读取成功;
负值: 读取失败
-EINVAL 表示属性不存在
-ENODATA 表示没有要读取的数据
-EOVERFLOW 表示属性值列表太小 -
读取属性中字符串值:of_property_read_string
int of_property_read_string(struct device_node *np,const char *propname,const char **out_string)
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值: 0,读取成功,负值,读取失败 -
获取 #address-cells 属性值:of_n_addr_cells ,获取 #size-cells 属性值:of_size_cells 。
int of_n_addr_cells(struct device_node *np) int of_n_size_cells(struct device_node *np)
np:设备节点。
返回值: 获取到的#address-cells 属性值。
返回值: 获取到的#size-cells 属性值。 -
内存映射
of_iomap 函数用于直接内存映射,前面通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存。of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。例
#if 1/* 1、寄存器地址映射 */IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);GPIO1_DR = ioremap(regdata[6], regdata[7]);GPIO1_GDIR = ioremap(regdata[8], regdata[9]); #else //第一对:起始地址+大小 -->映射 这样就不用获取reg的值IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);GPIO1_DR = of_iomap(dtsled.nd, 3);GPIO1_GDIR = of_iomap(dtsled.nd, 4); #endif
六、字符设备驱动
6.1 字符设备基本驱动框架
字符设备最基本框架
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp) {}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt) {}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt) {}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp) {}
//设备操作函数
static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE,.open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息