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工作模式包含下载模式和启动模式,输出为两个文件,为完成下载 fdl2.bin和启动u-boot.bin的工作。
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 来修改设备树:
一般厂家的接口程序已经写好,我们只需要去修改设备树部分
指纹组成:指纹盖板、指纹传感器、指纹芯片、指纹存储器、相关算法;
识别方式:光学指纹、射频指纹、电容式指纹;
指纹识别模组包含 指纹识别模块&触摸唤醒模块;
1.应用层
2.OS
3.硬件层
硬件是底层基础,代码最终会落实为硬件上的组合逻辑与时序逻辑电路;
软件实现具体应用,按照不同业务需求而设计,完成最终诉求。
为了使软件工程师和硬件工程师有足够的精力和时间顾及自己的领域,实现应用软件和底层硬件的分离,设备驱动开发工程师就是这个纽带。
Linux内核驱动:这一段代码操作了硬件去动,所以这一段代码就叫硬件的驱动程序。
驱动工程师应该具备硬件基础、C语言基础、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 内核与应用程序之间的接口,而设备驱动则是 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:系统调用和驱动程序关联起来的数据结构,是一系列指针的集合。
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输入子系统提供的接口注册到内核,利用子系统提供的功能来与用户空间交互。输入设备一般包括键盘,鼠标,触摸屏等,在内核中都是以输入设备出现的。
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
#include “imx6ull.dtsi” //使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。
#include “imx6ull-14x14-evk.dts”//直接引用.dts文件。
.dts设备树文件可以引用C语言中的 .h 文件,甚至也可以直接引用 .dts 文件,因此在 .dts 设备树文件中,可以通过“#include”来引用.h .dtsi .dts 文件,只是我们在编写设备树头文件的时候最好选择 .dtsi 后缀。
设备树基本框架
设备树语法
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。
标准属性
根节点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,我们接下来看一下这两个特殊的子节点
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中。
查找节点有关的 OF 函数
查找父/子节点的 OF 函数
提取属性值的 OF 函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体 property 表示属性,Linux 内核也提供了提取属性值的 OF 函数。
其他常用的 OF 函数
pinctrl 子系统重点是设置 PIN的复用和电气属性。
Linux 驱动讲究驱动分离与分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
大多数 SOC 的 pin 都是支持复用的,我们还需要配置 pin 的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引 入的。
对于使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,子系统源码目录为 drivers/pinctrl。
pinctrl 子系统主要工作内容如下:
设备树中添加 pinctrl 节点模板
GPIO子系统的主要目的就是方便驱动开发者使用GPIO。
pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统。驱动开发者在设备树中添加 gpio 相关信息,就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉 GPIO 的设置过程,极大的方便驱动开发者使用 GPIO。
设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO,gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处。
GPIO 子系统 API 函数
设备树中添加 gpio 节点模板
以创建 test 设备的 GPIO 节点为例
检查 PIN 是否被其他外设使用 !!!
与 gpio 相关的 OF 函数
平台总线模型也叫platform总线模型,是Linux内核虚拟出来的一条总线,不是真正的导线。
平台总线模型实际上就是把原来的驱动C文件分成了两个C文件,一个是device.c,一个是driver.c 把稳定不变的放在 driver.c 里面,需要变动的放在 device.c 里面。
总线就是驱动和设备信息的月老,负责给两者牵线搭桥
不同平台的主机驱动通过核心层的统一接口API访问设备驱动,不必重复编写设备驱动,大大简化驱动程序文件,即驱动分隔。
在实际的驱动开发中,一般 I2C 主机控制器驱动由半导体厂家编写好,设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可。如 I2C 设备提供设备连接到了哪个 I2C 接口上,I2C 的速度是多少等等。 相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。这样就相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,也就是常说的驱动分离。
向系统注册驱动的时候,总线就会在设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来;注册设备时总线在驱动中查找看有没有与之匹配的设备,有的话也联系起来
阻塞与非阻塞的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。
阻塞IO:当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。
非阻塞IO:当资源不可用时,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
等待队列
轮询
用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,即轮询。
应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
基于低功耗MCU和轻量级RTOS操作系统之上的软硬件结合的解决方案,其主要功能是连接并处理来自各种传感器设备的数据。
MMU内存管理
MemoryManagementUnit 的缩写,即内存管理单元,负责的是虚拟地址与物理地址的转换,提供硬件机制的内存访问授权,现代 CPU 的应用中基本上都选择了使用 MMU。
MMU的作用为:
将虚拟地址翻译成为物理地址,然后访问实际的物理地址;
访问权限控制,保护系统安全。
ioremap 函数:把物理地址转换成虚拟地址,映射
iounmap 函数:释放掉ioremap映射的地址,解映射
使用不同开发板内核时,一定要修改 KERN_DIR
KERN_DIR中的内核要事先配置、编译,为了能编译内核,要先设置相应环境变量