海思SDK驱动部分.docx
《海思SDK驱动部分.docx》由会员分享,可在线阅读,更多相关《海思SDK驱动部分.docx(10页珍藏版)》请在冰豆网上搜索。
![海思SDK驱动部分.docx](https://file1.bdocx.com/fileroot1/2022-12/14/8466570e-2038-4261-89b7-a5f31e8b53c0/8466570e-2038-4261-89b7-a5f31e8b53c01.gif)
海思SDK驱动部分
1,linux驱动程序介绍
1.1linux驱动程序在系统中的角色
Linux分为用户态和内核态,一般应用程序是在用户态执行,他们通过一系列的系统调用同内核态进行交互。
驱动程序是内核与硬件的接口,它把系统调用映射到具体设备对于实际硬件的特定操作上,关系如下图所
通过这种方法,应用程序就可以像操作普通文件一样操作硬件设备,用户程序只需要关心这个抽象出来的文件,而一切同硬件打交道的工作都交给了驱动程序。
1.2linux驱动的类型
linux系统将设备分为3类:
字符设备、块设备、网络设备,摄像机常用的外围设备(如I2C,串口,SPI,GPIO,PWM等)均属于字符设备,tf卡驱动属于块设备,网卡相关驱动属于网络设备。
字符设备与块设备的区别:
1、字符设备是面向流的,最小访问单位是字节;而块设备是面向块的,最小访问单位是512字节或2的更高次幂。
2、字符设备只能顺序按字节访问,而块设备可随机访问。
3、块设备上可容纳文件系统,访问形式上,字符设备通过设备节点访问,而块设备虽然也可通过设备节点访问,但一般是通过文件系统来访问数据的。
而网络设备没有设备节点,是因为网络设备是面向报文的,很难实现相关read、write等文件读写函数。
所以驱动的实现也与字符设备和块设备不同。
1.3linux驱动的一些重要概念
设备号
Linux把所有设备都当作文件,为了管理这些设备,系统为它们各自都编了号,而每个设备号又分为主设备号和次设备号。
主设备号用来区分不同类型的设备,而次设备号用来区分同一类型内的多个设备(及其设备分区)。
在建立字符驱动时需要做的第一件事是获取设备号。
设备号的分配方式一般有2种,静态分配和动态分配,静态分配设备号,就是驱动程序开发者,静态地指定一个设备号。
对于一部分常用的设备,linux内核开发者已经为其分配了设备号。
这些设备号可以在内核源码documentation/devices.txt文件中找到。
如果只有开发者自己使用这些设备驱动程序,那么其可以选择一个尚未使用的设备号。
当添加新硬件时,很可能造成设备号冲突,影响设备的使用。
为了解决手动分配设备号存在冲突的问题,内核开发者提出动态分配设备号的方法。
使用该方法驱动程序在加载的时候,通过linux内核提供的专门的函数动态获取设备号。
intalloc_chrdev_region(dev_t*dev, unsignedbaseminor, unsignedcount, constchar*name)
设备节点
linux系统中对所有设备的访问都是基于文件的形式。
对于每一种设备,在加载驱动程序的时候都会在/dev目录下创建一个文件,这个文件就是设备节点。
对于每一个设备节点,在实际运行时,linux系统通过VFS(虚拟文件系统)来完成将文件的各种系统调用与具体的驱动程序函数之间的映射。
设备节点可以通过mknod命令在系统启动的时候手动创建,也可以通过udev自动创建。
在驱动用加入对udev的支持主要做的就是:
在驱动初始化的代码里调用内核提供的API向内核注册驱动信息
class_create :
创建class
class_device_create :
创建device
驱动加载时会在/sys/class目录下生成与该模块相关的信息,同时用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
驱动初始化时,需要完成以下工作:
1,通过alloc_chrdev_region()及相关函数分配主/次设备号。
2,使用device_create()创建/dev和/sys节点。
3,使用cdev_init()和cdev_add()将自身注册为字符驱动程序。
混杂设备
考虑到有的系统包含很多简单字符设备驱动,单独为这些设备分配设备号比较浪费资源,同时工作量也很大,linux系统针对这些情况推出了一种叫混杂设备模型的驱动框架(miscellaneous)。
混杂设备主要有2个特征:
1)所有的misc设备被分配同一个主设备号MISC_MAJOR(10),但是可以选择一个单独的次设备号。
如果一个字符设备驱动要驱动多个设备,那么它就不应该用misc设备来实现;2)混杂设备驱动初始化时,只需要执行简单的一个注册函数,即可自动完成设备号分配,设备节点创建,向内核注册等工作,极大的简化了驱动初始化流程。
硬件IO操作
IO端口与IO内存
x86体系和ARM体系的寻址方式是有差别的:
在x86下,为了能够满足CPU高速地运行,内存与CPU之间通过北桥相连并通过地址方式访问,而外设通过南桥与CPU相连并通过端口访问。
因为这两种访问方式的不同,linux分出了两种不同的访问操作:
以地址方式访问硬件——使用IO内存操作。
以端口方式访问硬件——使用IO端口操作。
在ARM下也实现了类似的操作,通过两条不同的总线(AHBBUS和APBBUS)来连接不同访问速度的外设。
但是它与x86不同,无论是内存还是外设,ARM都是通过地址访问。
在ARM下,访问寄存器就像访问内存一样——从指定的寄存器地址获取数据,修改。
所以,ARM下一般是使用IO内存的操作。
但这并不是说IO端口的操作在ARM下不能用,它们的代码差不多,只是没有使用的必要,下面也将介绍IO内存操作。
如何使用IO内存获得硬件的地址
我们不能在linux使用实际的物理地址,要对指定的物理地址进行操作,必须要先将物理地址与虚拟地址对应,通过虚拟地址访问。
于是有了以下的物理地址映射函数:
#include
void*ioremap(unsignedlongphys_addr,unsignedlongsize);
函数传入两个参数,需要访问的物理内存(寄存器)的首地址phys_addr和这段内存区域的大小size,返回与该段物理地址对应的虚拟地址。
这段地址可以多次被映射,当然,每次映射的虚拟地址也不一样。
phys=0x200f0000;//1、指定物理地址
virt=(unsignedlong)ioremap(phys,0x0c);//2、通过ioremap获得对应的虚拟地址
//0x0c表示只要12字节的大小
GPECON=(unsignedlong*)(virt+0x40);//3、指定需要操作的寄存器的地址
*GPECON&=~(3<<24);//配置GPE12为输出端口
为了实现更好的移植性,上面的程序就有缺陷了。
内核建议,尽量使用内核提供的内存访问接口:
#include
//从内存读取数据,返回值是指定内存地址中的值
unsignedintioread8(void*addr)
unsignedintioread16(void*addr)
unsignedintioread32(void*addr)
//往指定内存地址写入数据
voidiowrite8(u8value,void*addr)
voidiowrite16(u16value,void*addr)
voidiowrite32(u32value,void*addr)
参照硬件说明,通过对寄存器的控制实现操作硬件
硬件说明文档
驱动的调试
Linux设备模型
linux系统作为开源的下系统,支持世界上大部分的硬件,导致Linux内核看上去非常臃肿、杂乱、不易维护。
为了优化,linux系统从2.6版本开始提出了全新的设备模型(也称作DriverModel)概念。
设备模型将硬件设备归纳、分类,然后抽象出一套标准的数据结构和接口。
驱动的开发,就简化为对内核所规定的数据结构的填充和实现。
1)Linux设备模型中包含四个重要概念:
Bus、Class、Device、DeviceDriver。
Bus(总线):
Bus(总线)是一类特殊的设备,它是连接处理器和其它设备之间的通道(channel)。
在设备模型中,所有的设备都通过总线相连,甚至是那些内部的虚拟平台总线(platformbus
).
Class(类):
在Linux设备模型中,Class的概念非常类似面向对象程序设计中的Class(类),它主要是集合具有相似功能或属性的设备,这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数。
因而从属于相同Class的设备的驱动程序,就不再需要重复定义这些公共资源,直接从Class中继承即可。
Device(设备):
抽象系统中所有的硬件设备,描述它的名字、属性、从属的Bus、从属的Class等信息。
DeviceDriver(驱动):
Linux设备模型用Driver抽象硬件设备的驱动程序,它包含设备初始化、电源管理相关的接口实现。
而Linux内核中的驱动开发,基本都围绕该抽象进行(实现所规定的接口函数)。
2)Linux设备模型的运行机制
配对(Match):
当总线上添加了新设备或者新驱动函数的时候,内核会调用一次或者多次这个函数。
举例,如果我现在添加了一个新的驱动函数,内核就会调用所属总线的match函数,配对总线上所有的设备,如果驱动能够处理其中一个设备,函数返回0,告诉内核配对成功。
探测(probe)当配对(match)成功后,内核就会执行device_driver中的probe回调函数,而该函数就是所有driver的入口,可以执行诸如硬件设备初始化、字符设备注册、设备文件操作ops注册等动作。
所以说,真正的驱动函数入口是在probe函数中。
卸载(remove)当该驱动函数或者驱动函数正在操作的设备被移除时,内核会调用驱动函数中的remove函数调用,进行一些设备卸载相应的操作。
platform设备
针对一些可以通过CPU直接寻址的设备(比如集成在嵌入式SOC芯片上的控制器,CPU可以直接访问其寄存器),linux内核在设备模型的基础上(device和device_driver),对这些设备进行了更进一步的封装,抽象出paltformbus、platformdevice和platformdriver,以便驱动开发人员可以方便的开发这类设备的驱动。
paltform设备对嵌入式Linux驱动开发是非常重要的,因为我们编写的大多数设备驱动,都是为了驱动plaftom设备。
由图片可知,Platform设备在内核中的实现主要包括三个部分:
PlatformBus,基于底层bus模块,抽象出一个虚拟的Platformbus,用于挂载Platform设备;
PlatformDevice,基于底层device模块,抽象出PlatformDevice,用于表示Platform设备;
PlatformDriver,基于底层device_driver模块,抽象出PlatformDriver,用于驱动Platform设备。
Linuxplatform_driver机制和传统的device_driver机制(通过driver_register函数进行注册)相比,最大的区别在于platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过platform_device提供的标准接口进行申请并使用。
这样提高了驱动和资源管理的独立性,并且拥有较好的可移植性和安全性(这些标准接口是安全的)。
platform机制的本身使用并不复杂,由两部分组成:
platform_device和platfrom_driver。
通过platform机制开发底层设备驱动的大致流程如图所示
定义并注册platformdevice
定义platformdevice时需要填充linux定义的结构体:
structplatform_device
该结构体主要包含设备相关的参数,比如设备名称,id,还有设备将要用到的资源的描述,在Linux中,系统资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型。
这些资源大多具有独占性,不允许多个设备同时使用,因此Linux内核提供了一些API,用于分配、管理这些资源。
当某个设备需要使用某些资源时,只需利用structresource组织这些资源(如名称、类型、起始、结束地址等),并保存在该设备的resource指针中即可。
然后在设备probe时,设备需求会调用资源管理接口,分配、使用这些资源。
structplatform_device{
3:
constchar*name;
4:
intid;
5:
boolid_auto;
6:
structdevicedev;
7:
u32num_resources;
8:
structresource*resource;
9:
10:
conststructplatform_device_id*id_entry;
11:
12:
/*MFDcellpointer*/
13:
structmfd_cell*mfd_cell;
14:
15:
/*archspecificadditions*/
16:
structpdev_archdataarchdata;
17:
};
struct resource
{
resource_size_tstart; //定义资源的起始地址
resource_size_tend; //定义资源的结束地址
constchar *name; //定义资源的名称
unsignedlong flags; //定义资源的类型,比如MEM,IO,IRQ,DMA类型
structresource *parent, *sibling, *child; //资源链表指针
};
然后通过调用函数platform_add_devices()向系统中添加该设备了,该函数内部调用platform_device_register()进行设备注册。
定义和注册platformdriver
用于抽象Platform设备驱动的数据结构如下
1:
/*include/linux/platform_device.h,line173*/
2:
structplatform_driver{
3:
int(*probe)(structplatform_device*);
4:
int(*remove)(structplatform_device*);
5:
void(*shutdown)(structplatform_device*);
6:
int(*suspend)(structplatform_device*,pm_message_tstate);
7:
int(*resume)(structplatform_device*);
8:
structdevice_driverdriver;
9:
conststructplatform_device_id*id_table;
10:
};
初始化时需要将对应的函数指针赋值给该结构体,并调用linux提供的API向内核注册。
另外还有两个变量:
name和owner。
那么的作用主要是为了和相关的platform_device关联起来,owner的作用是说明模块的所有者,驱动程序中一般初始化为THIS_MODULE。
probe、remove,这两个接口函数用于实现driver逻辑的开始和结束。
Driver是一段软件code,因此会有开始和结束两个代码逻辑,就像PC程序,会有一个main函数,main函数的开始就是开始,return的地方就是结束。
而内核driver却有其特殊性:
在设备模型的结构下,只有driver和device同时存在时,才需要开始执行driver的代码逻辑。
这也是probe和remove两个接口名称的由来:
检测到了设备和移除了设备(就是为热拔插起的!
)。
shutdown、suspend、resume、pm与linux定义的电源管理模型相关的接口,
probe函数的原型为
intxxx_probe(structplatform_device*pdev)
即它的返回类型为int,接收一个platform_device类型的指针作为参数。
返回类型就是我们熟悉的错误代码了,而接收的这个参数呢,我们上面已经说过,驱动程序为设备服务,就需要知道设备的信息。
而这个参数,就包含了与设备相关的信息。
probe函数接收到plarform_device这个参数后,就需要从中提取出需要的信息。
它一般会通过调用内核提供的platform_get_resource和platform_get_irq等函数来获得相关信息。
如通过platform_get_resource获得设备的起始地址后,可以对其进行request_mem_region和ioremap等操作,以便应用程序对其进行操作。
通过platform_get_irq得到设备的中断号以后,就可以调用request_irq函数来向系统申请中断。
这些操作在设备驱动程序中一般都要完成。
将设备节点和对应驱动关联。
用户态调用open的时候,VFS新建建一个文件句柄,并将之与驱动程序初始化时定义的structfile_operations结构体对象关联,
file_operation就是把系统调用和驱动程序关联起来的关键数据结构。
注册的所有回调函数与之关联,并一起传递给那个驱动(register_chrdev的那个驱动),那个驱动本身有所有调用的回调,也可以给filp建立另一套回调,无所谓,之后其他调用就直接可以指向设备本身了
实现嵌入式Linux设备驱动程序的大致流程如下
(1)查看硬件原理图,理解设备的工作原理。
海思3516SOC
(2)定义主设备号。
设备由一个主设备号和一个次设备号来标识。
主设备号唯一标识了设备类型,即设备驱动程序类型,它是块设备表或字符设备表中设备表项的索引。
次设备号仅由设备驱动程序解释,区分被一个设备驱动控制下的某个独立的设备。
(3)实现初始化函数。
在驱动程序中实现驱动的注册和卸载。
(4)设计所要实现的文件操作,定义file--operations结构。
(5)实现所需的文件操作调用,如read,write等。
(6)实现中断服务,并用request--irq向内核注册,中断并不是每个设备驱动所必需的。
(7)编译该驱动程序到内核中,或者用insmod命令加载模块。
(8)测试该设备,编写应用程序,对驱动程序进行测试。
初始化:
1,申请设备号:
字符设备有主设备号和次设备号之分,主设备号用来表示一个特定的驱动程序。
次设备号用来表示使用该驱动程序的各设备。
有两个i2c设备,i2c设备需要独立的进行读写。
那么,可以写一个i2c设备的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。
。
海思SDK支持很多的普通字符设备,其类型比较分散,如I2C,串口,SPI,GPIO,PWM等,对于这些类型的驱动,不管该驱动的主设备号是静态还是动态分配,都会消耗一个主设备号,而linux下的设备号为主设备号0-255,数量有限。
海思sdk使用Linux 提供的一种叫misc框架来处理这种问题,misc框架引入了杂项设备(miscdevice)概念,杂项设备可能包含字符设备、块设备、网络设备中的一项或者多项设备,所有杂项设备共享一个主设备号MISC_MAJOR(10),但次设备号不同。
misc框架对普通设备驱动做了一层封装,使用时无需自己去注册字符驱动,并创建字符设备class以自动在/dev下生成设备节点,只需要把一些基本信息通过structmiscdevice交给misc_register()去处理即可。