0.小计
IRQ:中断 RST:存储 FP:寄存器
backlight:linux背光子系统;
SOC:系统及芯片,即片上系统;
UART:通用异步收发传输号,串行异步收发协议,二进制按位为单位传输; XT:发送数据线
BB:Base Band基带;
SPI:串行外围设备接口,四线、全双工、高速、同步;
LTE:3G演化系统,通信;
GPIO:通用功能的输入输出线,可通软件控制其输入输出;
AF:调频 RF:解调 RF:射频 LC:振荡电路
A/D:模数转换-->ADC器 D/A:数模转换-->DAC器
LVDS:低压差分信号接口技术,平衡传输信号
mipi:移动行业处理器接口 DSI:串行显示接口
Chipram配置文件:bsp\bootloader\chipram\include\configs\
source:点命令,执行刚修改的初始化文件 lunch:选择编译参数
Framebuffer:简称fb,帧缓冲 -->显示设备 RGBLCD
LK:小内核 DRM:Linux图形渲染器
tty:UART中的终端设备也被称为tty设备
SPL:第二阶段程序加载器 FDL:文件下载加载程序
1.外设控制器与cpu通过AHB等总线连接
2.cpu与外设控制器组成soc
常见外设控制器:GPIO控制器 MIPI控制器 I2C控制器
soc中外设控制器与外设间的总线:MIPI总线 I2C总线 SPI总线 I2S总线 I3C总线
取指执行:控制器从存储器中取出数据后,分析指令,运算器执行逻辑运算。
数字电路根据逻辑功能的不同特点,可以分成组合逻辑电路和时序逻辑电路。
组合逻辑电路在逻辑功能上的特点是:任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。
时序逻辑电路在逻辑功能上的特点是:任意时刻的输出不仅取决于当时的输入信号,还与以前的输入有关 。
U-Boot
u-boot工作模式包含下载模式和启动模式,输出为两个文件,为完成下载 fdl2.bin和启动u-boot.bin的工作。
LCD驱动
1.初始化 eLCDIF 控制器,重点是 LCD 屏幕宽(width)、高(height)、hspw、hbp、hfp、vspw、vbp 和 vfp 等信息。
2.初始化 LCD 像素时钟。
3.设置 RGBLCD 显存。
4.应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。
帧缓冲fb:解决内存管理的虚拟内存问题。
Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体,包含了 Framebuffer 设备的完整属性和操作集合,
每一个 Framebuffer 设备都必须有一个 fb_info。
LCD的驱动就是构建 fb_info,并且向系统注册 fb_info的过程。
-
mxsfb_probe 函数的主要工作内容为:
1.申请 fb_info。
2.初始化 fb_info 结构体中的各个成员变量。
3.初始化 eLCDIF 控制器。
4.使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。
register_framebuffer函数原型如下:
int register_framebuffer(struct fb_info *fb_info); -
按照所使用的 LCD 来修改设备树:
一般厂家的接口程序已经写好,我们只需要去修改设备树部分- LCD 所使用的 IO 配置;
–>子节点 pinctrl_lcdif_dat,为 RGB LCD 的 24 根数据线配置项;
–>子节点 pinctrl_lcdif_ctrl,RGB LCD 的 4 根控制线配置项,包括 CLK、ENABLE、VSYNC 和 HSYNC;
–>子节点 pinctrl_pwm1,LCD 背光 PWM 引脚配置项。 - LCD 屏幕节点修改,修改相应的属性值,换成我们所使用的 LCD 屏幕参数;
–>lcdif 节点 - LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。
在设备树中添加背光节点信息:
a. 节点名称要为“backlight”;
b. 节点的 compatible 属性值要为“pwm-backlight”,驱动程序文件为drivers/video/backlight/pwm_bl.c
c. pwms 属性用于描述背光所使用的PWM以及PWM频率;
d. brightness-levels 属性描述亮度级别。
e. default-brightness-level 属性为默认亮度级别。
- LCD 所使用的 IO 配置;
指纹模块
指纹组成:指纹盖板、指纹传感器、指纹芯片、指纹存储器、相关算法;
识别方式:光学指纹、射频指纹、电容式指纹;
指纹识别模组包含 指纹识别模块&触摸唤醒模块;
Linux驱动简介
1.应用层
2.OS
3.硬件层
硬件是底层基础,代码最终会落实为硬件上的组合逻辑与时序逻辑电路;
软件实现具体应用,按照不同业务需求而设计,完成最终诉求。
为了使软件工程师和硬件工程师有足够的精力和时间顾及自己的领域,实现应用软件和底层硬件的分离,设备驱动开发工程师就是这个纽带。
Linux内核驱动:这一段代码操作了硬件去动,所以这一段代码就叫硬件的驱动程序。
驱动工程师应该具备硬件基础、C语言基础、Linux内核基础、多任务并发控制和同步基础。
驱动设计的思想:面向对象、分层、分离
Linux 内核
-
简介
作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。Linux进程1.采用层次结构,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接起源于该进程。
APP–>Kenrel–>CPU、Memroy、Devices、宏内核:Linux 内核采用宏内核架构。即 Linux 大部分功能都会在内核中实现, 如进程管理、内存管理、设备管理、文件管理以及网络管理 等功能,它们是运行在内核空间中。
微内核:仅仅是将内核的基本功能放入内核中,如进程管理、进程调度等。 -
Linux 内核组成:
5部分组成,为进程管理子系统、内存管理子系统、文件子系统、网络子系统、设备子系统。
内核各系统由系统调用层进行统一管理,应用层通过系统调用层的函数接口与内核进行交互。
用户应用程序执行的地方是用户空间,用户空间之下则是内核空间
内核组成图示: https://img-blog.csdnimg.cn/20200508153521218.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3AxMjc5MDMwODI2,size_16,color_FFFFFF,t_70 -
内核源代码三个主要部分:
- 内核核心代码,如各个子系统和子模块,以及其它的支撑子系统,例如电源管理、Linux初始化等;
- 其它非核心代码:如库文件、固件集合、KVM(虚拟机技术)等;
3.编译脚本、配置文件、帮助文档、版权说明等辅助性文件。
主讲设备子系统:
系统调用层是 Linux 内核与应用程序之间的接口,而设备驱动则是 Linux 内核与硬件之间的接口,
设备驱动程序为应用程序屏蔽了硬件的细节,在应用程序看来,硬件设备只是一个设备文件,
应用程序可以象操作普通文件一样对硬件设备进行操作(打开、读、写和关闭)。
Linux内核源代码有三种形态:- 不编译进去
- 编译进去
- 编译成一个模块
-
内核模块
内核模块也称为内核加载模块,在保持内核在不消耗所可用内存的情况下与所有硬件一起工作是必不可少的。
模块通常向基本内核添加设备、文件系统和系统调用等功能,文件扩展名是.ko -
应用到驱动的调用
APP–>C库–>内核(系统调用层SCI 虚拟文件系统VFS)–>驱动(设备驱动程序)–>内核核心模块–>硬件
应用层open、read、write 等函数调用Linux中相应 sys_open、sys_read、sys_write
sys_open、sys_read、sys_write 又去调用驱动中的 drv_open、dev_read、drv_write等 -
驱动和应用程序之间需要用这两个函数传输数据:
unsigned long
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n;
}unsigned long
copy_from_user(void *to, const void __user *from, unsigned long n)
{
might_sleep();
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else
memset(to, 0, n);
return n;
} -
驱动编写流程
-
确定主设备号
static int major = 0;//主设备号
static char kernel_buf[1024];//用于保存应用程序发送到驱动程序的数据
static struct class *xxx_class; //定义xxx_class -
定义自己的 file_operations 结构体
static struct file_operations xxx_drv = {
.owner = THIS_MODULE,
.open = xxx_drv_open,
.read = xxx_drv_read,
.write = xxx_drv_write,
.release= xxx_drv_class;
}; -
实现对应的 open/read/write 等函数,填入 file_operations 结构体
// 想让驱动程序保存应用程序发送过来的数据,需要定义一个 char kernel_buf[1024];
// static ssize_t xxx_drv_write() 函数中, 用户空间的 buf 拷贝到 kernel_buf不能直接拷贝,需要使用函数 copy_form_user() -
把file_operations 结构体告诉内核:注册驱动程序
-
安装驱动程序,调用入口函数,注册驱动程序
static int __init xxx_init(void)
{
major = register_chrdev(0, “xxx” ,&xxx_drv); /注册驱动,将驱动告诉内核,返回主设备号,主设备号设置0为系统分配/
xxx_class = class_create(THIS_MODULE,“xxx_class”);/*创建 class /
device_create(xxx_class,NULL,MKDEV(major,0),NULL,“xxx”);/ 可通过/dev/xxx 访问驱动 */
} -
卸载驱动程序,调用出口函数,
static void __exit xxx_exit(void)
{
device_destroy(xxx_class,MKDEV(major,0));
class_destroy(xxx_class);
unregister_chrdev(major,“xxx”);}
-
其他完善:提供设备信息,自动创建设备节点
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE(“GPL”);
-
驱动分类
根据硬件设备本身读写特性的差异分为三类:字符设备驱动、块设备驱动、网络设备驱动。
字符设备:操作设备时以字节流进行,如鼠标、键盘、LED、LCD、触摸屏...
块设备:块设备是通过内存缓冲区访问,可以随机存取的设备,一般为存储类设备,如硬盘、SD卡...
网络设备:Linux中的网络设备主要是为了支持API中socket相关函数的工作,硬件为网络设备。
杂项设备:是字符设备的一种,可以自动生成设备节点,比字符设备代码简单。
系统中也有很多的杂项设备,输入 cat /proc/misc 命令查看。
杂项设备的主设备号相同,均为10,次设备号不同,主设备号相同就可以节省内核的资源,主设备号可通过 cat /proc/devices 查看。
注册杂项设备流程:
a. 填充miscdevice结构体成员
b. 填充file_operations结构体
c. 注册杂项设备并生成设备节点
文件操作结构体file_operations:系统调用和驱动程序关联起来的数据结构,是一系列指针的集合。
- 字符设备驱动设计流程
- 申请设备号–>设备号分为主设备号和次设备号
静态申请:register_chrdev_region(dev_t from, unsigned count,const char* name);
动态申请:alloc_chrdev_region(dev_t* dev, unsigned baseminor,unsigned count,const char* name);
注销设备号:unregister_chrdev_region(dev_t from, unsigned count); - 定义字符设备并初始化
分配一个结构体描述字符设备:struct cdev{}; - 将cdev加入内核
- 创建设备节点
创建类class --> struct class *my_class(struct module *owner, const char *name);
注销类class --> void class_destroy(struct class *my_class);
创建设备cdev --> struct device *device_create(struct class *class, struct device *parent, dev_t devt,void *drvdata, const char *name);
注销设备cdev --> void device_destroy(struct class *class, dev_t devt); - 创建device–>在设备类中 创建设备,产生设备目录
- IO内存动态映射,得到物理地址对应的虚拟地址并访问
- 申请设备号–>设备号分为主设备号和次设备号
I2C
SCL:时钟信号 SDA:数据线
Linux下,I2C adapter为主控制器,其他都是从设备slave
主设备:产生I2C总线时钟信号的设备;
从设备:被控制的设备
-
三大组成部分
a. I2C核心(i2c-core)
I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法,I2C通信方法(algorithm)上层的、与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等。
b. I2C总线驱动(I2Cadapter/Algo driver)
I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力。
I2C总线驱动由i2c_adapter和i2c_algorithm来描述
c. I2C客户驱动程序(I2Cclient driver)
I2C客户驱动是对I2C从设备的软件实现,一个具体的I2C客户驱动包括两个部分:一部分是i2c_driver,用于将设备挂接于i2c总线;另一部分是设备本身的驱动。
I2C客户驱动程序由i2c_driver和i2c_client来描述 -
I2C驱动代码位于drivers/i2c目录
I2c-core.c 实现I2C核心的功能
I2c-dev.c 通用的从设备驱动
Chips 特定的I2C设备驱动
Busses I2C适配器的驱动
Algos 实现了一些I2C总线适配器的algorithm
input子系统
Input子系统处理输入事务,任何输入设备的驱动程序都可以通过Input输入子系统提供的接口注册到内核,利用子系统提供的功能来与用户空间交互。输入设备一般包括键盘,鼠标,触摸屏等,在内核中都是以输入设备出现的。
linux中input系统主设备号是13
-
Input子系统结构
-
Input子系统是分层结构的,总共分为三层:硬件驱动层–>子系统核心层–>事件处理层
硬件驱动层:负责操作具体的硬件设备,这层的代码是针对具体的驱动程序的,需要驱动程序的作者来编写。
子系统核心层:是链接其他两个层之间的纽带与桥梁,向下提供驱动层的接口,向上提供事件处理层的接口。
事件处理层:负责与用户程序打交道,将硬件驱动层传来的事件报告给用户程序。 -
Input子系统的三个重要结构体:
input_dev 是硬件驱动层,代表一个input设备;
input_handler 是事件处理层,代表一个事件处理器;
input_handle 代表一个配对的input设备与input事件处理器。
a. 在内核中,input_dev 表示一个 input设备;input_handler 来表示input设备的 interface。 所有的input_dev 用双向链表 input_dev_list 连起来。
b. 在调用 int input_register_device(struct input_dev *dev) 的时候,会将新的 input_dev 加入到这个链表中。所有的input_handler 用双向链表 input_handler_list 连起来。
c. 在调用 int input_register_handler(struct input_handler *handler) 的时候, 会将新的 input_handler 加入到这个链表中。
每个 input_dev 和 input_handler 是-要关联上才能工作的,
在注册 input_dev 或者 input_handler的时候,就遍历上面的列表,找到相匹配的,
然后调用 input_handler 的 connect函数来将它们联系到一起。d. 通常在input_handler 的 connect函数中,就会创建 input_handle, input_handle就是负责将 input_dev 和input_handler 联系在一起。
-
-
输入事件
层之间通信的基本单位就是事件,任何一个输入设备的动作都可以抽象成一种事件。
事件有三种属性:类型(type),编码(code),值(value),Input子系统支持的所有事件都定义在input.h中,包括所有支持的类型,所属类型支持的编码等。
事件传送的方向是 硬件驱动层–>子系统核心–>事件处理层–>用户空间 -
Input API
1. 分配/释放一个输入设备:
struct input_dev *input_allocate_device(void);
void input_free_device(struct input_dev *dev);
2. 注册/注销输入设备:
int __must_check input_register_device(struct input_dev *);
void input_unregister_device(struct input_dev *);
3. 报告输入事件:
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);/* 报告指定type、code的输入事件 */
void input_report_key(struct input_dev *dev, unsigned int code, int value);/* 报告键值 */
void input_report_rel(struct input_dev *dev, unsigned int code, int value);/* 报告相对坐标 */
void input_report_abs(struct input_dev *dev, unsigned int code, int value);/* 报告绝对坐标 */
void input_sync(struct input_dev *dev);/* 报告同步事件 */
设备树
Device Tree 是一种描述硬件的数据结构,由一系列被命名的节点(node)和属性(property)组成,而节点本身可包含子节点。
属性就是成对出现的 name 和 value。
-
设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹,这些文件夹包含根节点“/”的所有属性和子节点 -
DTSI、DTS、DTC和DTB
dtsi: 一般用于描述SOC的内部外设信息,一般.dtsi 文件用于描述SOC的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等类似于C语 言的头文件;
dts: 一种 ASCII 文件格式设备树描述,Linux中一个.dts文件对应一个设备;
dtb: 文件是dts文件被编译后生成的二进制文件;
dtc: 将dts编译为dtb的工具,类似于C语言中将.c文件编译成二进制.o文件的gcc编译器。
dt工具依赖于 dtc.c flattree.c stree.c等文件,最终编译并链接出dtc这个主机文件,在源码根目录下执行命令:make all(编译全部),make dtbs(编译设备树) -
dts代码示例
#include <dt-bindings/input/input.h> //使用“#include”来引用“input.h”这个.h 头文件。
#include “imx6ull.dtsi” //使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。
#include “imx6ull-14x14-evk.dts”//直接引用.dts文件。
.dts设备树文件可以引用C语言中的 .h 文件,甚至也可以直接引用 .dts 文件,因此在 .dts 设备树文件中,可以通过“#include”来引用.h .dtsi .dts 文件,只是我们在编写设备树头文件的时候最好选择 .dtsi 后缀。 -
设备树基本框架
- 设备树从根节点开始,每一个设备都是一个节点
- 节点和节点之间可以互相嵌套,形成父子关系
- 设备的属性用key-value对(键值对)来描述,每个属性用分号来结束
-
设备树语法
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。- 节点语法
/{ //根节点
node1 //子节点1
{
};
node2 //子节点2
{
child-node1 //子子节点1
{
};
};
}; - 节点名称
节点名称固定格式:<名称>[@<设备地址>]
<名称>一般要体现设备的类型,<设备地址>为用来访问该设备的基地址,主要用来区分用。 - 节点别名
例如: uart8:serial@02288000
uart8就是节点名称 serial@02288000 的别名。 - 节点的引用
一般情况下往节点添加内容时不会直接把添加的内容写到节点中,而是通过节点的引用来添加
&uart8{
pinctrl-names = “default”
pinctrl-0 = <&pinctrl_uart8>;
status = “okay”;
};
表示引用节点别名为uart8的节点,并且往这个节点里添加 { } 中的内容。
- 节点语法
-
标准属性
- compatible 属性:
属性的值是一个字符串列表,也叫做“兼容性”属性,性用于将设备和驱动绑定起来; - model 属性:
属性值是一个字符串,一般描述设备模块信息; - status 属性:
属性值是字符串,设备状态信息; - #address-cells 和 #size-cells属性:
属性值为无符号32位整形,可以用在任何拥有子节点的设备中,用于描述子节点的地址信息,和reg属性搭配使用; - reg 属性:
属性的值一般是(address,length)对,用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,地址相关的信息分为起始地址和地址长度,格式例如:reg = ; - ranges 属性:
属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges 是一个地址映射/转换表,每个项目由child-bus-address(子总线地址空间的物理地址)、parent-bus-address(父总线地址空间的物理地址)和length(地址空间长度)这三部分组成; - name 属性:
name 属性值为字符串,name 属性用于记录节点名字; - device_type 属性:
属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode。此属性只能用于 cpu 节点或者 memory 节点;
- compatible 属性:
-
根节点compatible 属性
每个节点都有 compatible 属性,根节点“/”也不例外。
示例代码:
/{
model = “Freescale i.MX6 ULL 14x14 EVK Board”;
compatible = “fsl,imx6ull-14x14-evk”, “fsl,imx6ull”;
}
通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。
Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程示意图,见《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.1.pdf》中:
图 43.3.4.2 查找匹配设备的过程 -
特殊节点
在根节点“/”中有两个特殊的子节点:aliases 和 chosen,我们接下来看一下这两个特殊的子节点- aliases 子节点
主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过 &label 来访问节点,这样也很方便,而且设备树里面大量的使用 &label 的形式来访问节点。 - chosen 子节点
chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。
- aliases 子节点
查找节点的 OF 函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中。
-
查找节点有关的 OF 函数
- of_find_node_by_name 函数
通过节点名字查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树;
name:要查找的节点名字; - of_find_node_by_type 函数
通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树;
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值;
返回值:找到的节点,如果为 NULL 表示查找失败。 - of_find_compatible_node 函数
根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:
struct device_node *of_find_compatible_node(struct device_node *from,const char *type,const char *compatible);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉device_type 属性;
compatible:要查找的节点所对应的 compatible 属性列表;
返回值:找到的节点,如果为 NULL 表示查找失败。 - of_find_matching_node_and_match 函数
通过 of_device_id 匹配表来查找指定的节点,函数原型如下:
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。 - of_find_node_by_path 函数
通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path);
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径;
返回值:找到的节点,如果为 NULL 表示查找失败。
- of_find_node_by_name 函数
-
查找父/子节点的 OF 函数
- of_get_parent 函数
用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node);
node:要查找的父节点的节点。 - of_get_next_child 函数
用迭代的方式查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node,struct device_node *prev);
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为 NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。
- of_get_parent 函数
-
提取属性值的 OF 函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体 property 表示属性,Linux 内核也提供了提取属性值的 OF 函数。- of_find_property 函数
查找<指定属性>,函数原型如下:
property *of_find_property(const struct device_node *np,const char *name,int *lenp);
np:设备节点。
name: 属性名字。
lenp:属性值的字节数 - of_property_count_elems_of_size 函数
用于获取属性中<元素的数量>,比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np,const char *propname, int elem_size);
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。 - of_property_read_u32_index 函数
用于从属性中获取指定标号的<u32类型数据值>,比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:
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 表示属性值列表太小。 - of_property_read_u8_array 函数
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
分别是读取属性中 u8、u16、u32 和 u64 类型的<数组数据>,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。 - of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
这四个函数就是用于读取只有一个<整形值>的属性,分别用于读取 u8、u16、u32 和 u64 类型属性值。 - of_property_read_string 函数
读取属性中<字符串值>,函数原型如下:
int of_property_read_string(struct device_node *np,const char *propname,const char **out_string); - of_n_addr_cells 函数
获取#address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np); - of_n_size_cells 函数
获取< #size-cells 属性值>,函数原型如下:
int of_n_size_cells(struct device_node *np);
其他常用的 OF 函数
- of_device_is_compatible 函数
用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,也就是检查设备节点的兼容性。 - of_get_address 函数
获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值。 - of_translate_address 函数
将从设备树读取到的地址转换为物理地址。 - of_address_to_resource 函数
函数看名字像是从设备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为resource 结构体类型。 - of_iomap 函数
用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。
- of_find_property 函数
pinctrl 子系统
-
pinctrl 子系统重点是设置 PIN的复用和电气属性。
Linux 驱动讲究驱动分离与分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
大多数 SOC 的 pin 都是支持复用的,我们还需要配置 pin 的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引 入的。
对于使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,子系统源码目录为 drivers/pinctrl。 -
pinctrl 子系统主要工作内容如下:
- 获取设备树中 pin 信息;
- 根据获取到的 pin 信息来设置 pin 的复用功能;
- 根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。
-
设备树中添加 pinctrl 节点模板
- 创建对应的节点
同一个外设的 PIN 都放到一个节点里面,节点前缀一定要为“pinctrl_”
pinctrl_test: testgrp {
/* 具体的 PIN 信息 */
}; - 添加“fsl,pins”属性
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为“fsl,pins”,因为对于 I.MX 系列 SOC 而言,pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息
pinctrl_test: testgrp {
fsl,pins = <
/* 设备所使用的 PIN 配置信息 */
>;
}; - 在“fsl,pins”属性中添加 PIN 配置信息
pinctrl_test: testgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /config 是具体设置值/
>;
}
- 创建对应的节点
GPIO 子系统
-
GPIO子系统的主要目的就是方便驱动开发者使用GPIO。
pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统。驱动开发者在设备树中添加 gpio 相关信息,就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉 GPIO 的设置过程,极大的方便驱动开发者使用 GPIO。
设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO,gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处。 -
GPIO 子系统 API 函数
- gpio_request 函数
申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request 进行申请
int gpio_request(unsigned gpio, const char *label);
gpio:要申请的 gpio 标号,使用<of_get_named_gpio> 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号。
label:设置 gpio 名称。 - gpio_free 函数
释放不再使用的 GPIO
void gpio_free(unsigned gpio);
gpio:要释放的 gpio 标号。 - gpio_direction_input 函数
设置某个 GPIO 为输入
int gpio_direction_input(unsigned gpio);
gpio:要设置为输入的 GPIO 标号。
返回值:0,设置成功;负值,设置失败。 - gpio_direction_output 函数
设置某个 GPIO 为输出,并且设置默认输出值
int gpio_direction_output(unsigned gpio, int value);
gpio:要设置为输出的 GPIO 标号。
value:GPIO 默认输出值。
返回值:0,设置成功;负值,设置失败。 - gpio_get_value 函数
获取某个 GPIO 的值(0 或 1),此函数是个宏
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
gpio:要获取的 GPIO 标号。
返回值:非负值,得到的 GPIO 值;负值,获取失败。 - gpio_set_value 函数
设置某个 GPIO 的值,此函数是个宏
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
gpio:要设置的 GPIO 标号。
value:要设置的值。
- gpio_request 函数
-
设备树中添加 gpio 节点模板
以创建 test 设备的 GPIO 节点为例- 创建 test 设备节点
在根节点“/”下创建 test 设备子节点。
test {
/* 节点内容 */
}; - 添加 pinctrl 信息
在 pinctrl 子系统中创建 pinctrl_test 节点,此节点描述 test 设备所使用 GPIO 对应的 PIN 信息,需将这节点添加到 test 设备节点。
test {
pinctrl-names = “default”;
pinctrl-0 = <&pinctrl_test>;
/* 其他节点内容 */
}; - 添加 GPIO 属性
最后在 test 节点中添加 GPIO 属性信息,表明 test 所使用的 GPIO 是哪个引脚。
test {
pinctrl-names = “default”;
pinctrl-0 = <&pinctrl_test>;
gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
};
- 创建 test 设备节点
-
检查 PIN 是否被其他外设使用 !!!
- 检查 pinctrl 设置。
- 如果这个 PIN 配置为 GPIO 的话,检查这个 GPIO 有没有被别的外设使用。
-
与 gpio 相关的 OF 函数
- of_gpio_named_count 函数
- of_gpio_count 函数
- of_get_named_gpio 函数
平台总线模型 platform
平台总线模型也叫platform总线模型,是Linux内核虚拟出来的一条总线,不是真正的导线。
平台总线模型实际上就是把原来的驱动C文件分成了两个C文件,一个是device.c,一个是driver.c 把稳定不变的放在 driver.c 里面,需要变动的放在 device.c 里面。
-
总线就是驱动和设备信息的月老,负责给两者牵线搭桥
不同平台的主机驱动通过核心层的统一接口API访问设备驱动,不必重复编写设备驱动,大大简化驱动程序文件,即驱动分隔。
在实际的驱动开发中,一般 I2C 主机控制器驱动由半导体厂家编写好,设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可。如 I2C 设备提供设备连接到了哪个 I2C 接口上,I2C 的速度是多少等等。 相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。这样就相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,也就是常说的驱动分离。 -
向系统注册驱动的时候,总线就会在设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来;注册设备时总线在驱动中查找看有没有与之匹配的设备,有的话也联系起来
阻塞和非阻塞 IO
阻塞与非阻塞的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。
阻塞IO:当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。
非阻塞IO:当资源不可用时,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
-
等待队列
- 等待队列头
阻塞访问当设备文件不可操作的时候进程可以进入休眠态,Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果要在驱动中使用等待队列,必须创建并初始化一个等待队列头。
使用结构体 wait_queue_head_t 表示等待队列头,定义好等待队列头以后使用 init_waitqueue_head 函数初始化等待队列头。
可以使用宏 DECLARE_WAIT_QUEUE_HEAD 一次性完成等待队列头的定义的初始化。 - 等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。
结构体 wait_queue_t 表示等待队列项;使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项。 - 将队列项添加/移除等待队列头
等待队列项添加 API 函数:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
等待队列项移除 API 函数:
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait); - 等待唤醒
wake_up 可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q); - 等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。
wait_event(wq, condition);
wait_event_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition);
wait_event_interruptible_timeout(wq,condition, timeout);
- 等待队列头
-
轮询
用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,即轮询。
应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
Sensorhub 通用智能传感集线器
基于低功耗MCU和轻量级RTOS操作系统之上的软硬件结合的解决方案,其主要功能是连接并处理来自各种传感器设备的数据。
-
MMU内存管理
MemoryManagementUnit 的缩写,即内存管理单元,负责的是虚拟地址与物理地址的转换,提供硬件机制的内存访问授权,现代 CPU 的应用中基本上都选择了使用 MMU。
MMU的作用为:
将虚拟地址翻译成为物理地址,然后访问实际的物理地址;
访问权限控制,保护系统安全。
ioremap 函数:把物理地址转换成虚拟地址,映射
iounmap 函数:释放掉ioremap映射的地址,解映射 -
使用不同开发板内核时,一定要修改 KERN_DIR
KERN_DIR中的内核要事先配置、编译,为了能编译内核,要先设置相应环境变量