OpenHarmony HDF 平台驱动框架介绍及驱动适配指导
OpenHarmony系统平台驱动概述
OpenHarmony系统平台驱动(PlatformDriver),即平台设备驱动,它用于驱动平台设备(PlatformDevice),为系统及外设驱动提供访接口。这里的平台设备,泛指I2C/UART等总线、以及GPIO/RTC等SOC片内硬件资源。
OpenHarmony系统平台驱动框架是OpenHarmony系统驱动框架的重要组成部分,它基于HDF驱动框架、操作系统适配层(OSAL, operating system abstraction layer)以及驱动配置管理机制,为各类平台设备驱动的实现提供标准模型。
OpenHarmony系统平台驱动框架为外设提供了标准的平台设备访问接口,使其不必关注具体硬件及OS平台;同时为平台设备驱动提供统一的适配接口,使其只关注自身硬件的控制。
为实现这个目标,OpenHarmony系统平台驱动框架满足如下特性:
统一的平台设备访问接口:对平台设备操作接口进行统一封装,屏蔽不同SOC平台硬件差异以及不同OS形态差异。
统一的平台驱动适配接口:为平台设备驱动提供统一的适配接口,使其只关注自身硬件的控制,而不必关注设备管理及公共业务流程。
提供设备注册、管理、访问控制等与SOC无关的公共能力。
OpenHarmony系统平台驱动框架目前支持的设备类型包括但不限于:I2C/SPI/UART/MIPI_DSI/SDIO/GPIO/PWM/WATCHDOG/RTC/DMA
OpenHarmony平台驱动框架介绍
OpenHarmony系统平台驱动框架组成
OpenHarmony系统平台驱动框架主要由平台接口层、平台核心层以及平台适配层三个部分组成。
1)平台接口层 以API的形式提供标准的平台设备访问接口。
平台接口层以设备句柄加配套API的形式对外提供统一的、标准的访问接口。
设备句柄是DevHandle类型的实例,通过不同设备模块提供的Open/Close方法进行获取、释放。成功获取设备句柄后,即可使用相应的API执行设备操作。例如通过I2cTransfer完成一次I2C数据传输。
这是一种代理模式,即接口层API不直接引用实际设备对象,而是通过DevHandle作为代理,间接访问设备;而所有来自外设驱动的访问,都建议走接口层,以获得最佳的稳定性。
不同类型设备的API使用,请参考如下官方文档的平台驱动章节:
https://device.OpenHarmony系统.com/cn/docs/develop/drive/oem_drive_hdfdev-0000001051715456
2)平台核心层 提供平台设备模型及公共业务框架。
提供统一适配接口:定义了标准的设备对象模型,驱动程序仅需关注标准对象模型的适配。
抽取公共业务框架:将不同设备模块的公共流程、算法加以抽取,使得具体设备驱动更加轻薄。
设备管理:设备注册、注销、设备查找、访问控制。
3)平台适配层 提供特定平台设备的适配驱动,并遵守核心层约束。
驱动具体平台设备硬件,并创建对应的设备模型对象,注册到核心层纳入统一管理。
平台接口层分析
前面说过,在接口层,我们用DevHandle类型的设备句柄表示一个平台设备对象,然后针对不同类型设备提供一套标准的API方法用于设备访问。那么设备句柄和真实的设备对象如何关联呢?
查看DevHandle的定义,发现它就是一个void类型指针:
/**
* @brief Defines the common device handle of the platform driver.
*
* The handle is associated with a specific platform device and is used as the
* first input parameter for all APIs of the platform driver.
*
* @since 1.0
*/
typedef void* DevHandle;
实际上,在内核态,这个指针可以直接指向实际设备对象,但是对于某些类型的平台设备,我们需要在用户态提供同样的DevHandle类型及配套API,而实际设备对象在内核空间,我们无法直接获取和使用内核空间的地址。我们的解决办法是将平台设备对象实现为一个HDF设备服务,这样借助HDF DeviceManager的设备服务机制,可以在用户态、内核态同时获取到设备服务,而用户态同内核态通信的问题交由HDF DeviceManager处理。此时,DevHandle只需要关联到这个设备服务即可,而void*类型保证了足够的灵活性。
根据DevHandle和设备对象关联方式的不同,接口层的设计有三种模式,下面将一一讲解。
1.独立服务模式
实际上,在内核态,这个指针可以直接指向实际设备对象,但是对于某些类型的平台设备,我们需要在用户态提供同样的DevHandle类型及配套API,而实际设备对象在内核空间,我们无法直接获取和使用内核空间的地址。我们的解决办法是将平台设备对象实现为一个HDF设备服务,这样借助HDF DeviceManager的设备服务机制,可以在用户态、内核态同时获取到设备服务,而用户态同内核态通信的问题交由HDF DeviceManager处理。此时,DevHandle只需要关联到这个设备服务即可,而void*类型保证了足够的灵活性。
根据DevHandle和设备对象关联方式的不同,接口层的设计有三种模式,下面将一一讲解。
2.统一服务模式
有时候,同一类型的设备对象可能会很多,例如I2C模块,可能同时有十几个控制器。如果采用独立服务的模式,每一个控制器,作为一个平台设备,为其创建一个设备服务,那么将会有十几个服务被创建,不光要配置很多设备节点,而且这些服务还会占用内存资源。
这时,我们可以为一类设备对象,创建一个平台设备管理器(PlatformManager)对象,并同时对外发布一个管理器服务,由这个管理器服务来统一处理外部访问。当用户需要打开某个设备时,先通过HDF DeviceManager获取到管理器服务,然后管理器服务根据用户指定参数查找到指定设备,并返回一个设备描述符,而这个描述符仍然可以由DevHandle类型表示。
这种模式的实践代表是I2C模块,PlatformManager实现为I2cManager,而PlatformDevice则是I2cCntlr,感兴趣的读者可以阅读一下drivers/framework下的i2c_if.c/i2c_core.c一探究竟。
3.无服务模式
这种模式用于不需要在用户态提供API的设备类型或者没有用户态、内核区分的OS系统,其关联方式是DevHandle直接指向设备对象内核态地址。而PlatformManager的实现比较自由,它不需要实现设备服务,只需做好某种类型的设备管理即可,甚至在C语言中,由于无法进行OOP编程,很多模块直接将这个功能面向过程化了,使得没有一个具体的结构体与之对应。但是,我们仍然强调PlatformManager这个概念,也期望随着后续平台驱动框架的演进,逐步完善、规范化,形成更加统一的编程风格。
平台核心层分析
平台核心层的作用是承上启下,其主要内容包括:
提供适配接口:为具体的平台设备驱动提供统一的适配接口
平台驱动框架为不同设备类型,定义了标准的设备对象模型,具体设备驱动只需要关注标准设备对象的适配即可
提供设备管理:提供设备的注册、注销、查找等功能、访问控制等能力
核心层会提供一系列内部方法,用于设备的注册、注销,设备对象的查找、获取、释放,以及处理多线程访问。例如当我们向核心层注册一个I2C控制器对象时,使用I2cCntlrAdd;当希望获取一个I2C控制器对象时,通过I2cCntlrGet并指定控制器编号;当不再使用这个对象时,还需要通过I2cCntlrPut释放。这样做的好处是将每一个具体的操作步骤高度抽象化,减小同平台接口层及平台适配层的耦合面,便于业务解耦、演进。如果后续,我们由于业务需求需要对I2cCntlr对象进行引用计数,那么只需要修改I2cCntlrGet/Put这对方法的实现即可,并不会影响平台接口层和平台适配层。
公共业务实现:抽取公共的业务流程、算法
凡是跟特定硬件无关的业务逻辑,都会被抽取到核心层,例如RTC时钟的时间格式转换算法,GPIO模块的线程中断实现等等。
平台适配层实现
适配层提供具体平台硬件设备的驱动,按照核心层定义的模型创建设备对象,并完成对象的初始化(包括必要的成员变量初始化以及钩子方法挂接,以及相关的硬件初始化操作),最后使用核心层提供的注册方法将设备对象注册到核心层纳入统一管理。
这里总结了当前各模块的适配对象:
模块 | 适配对象 | 注册方法 | 注销方法 |
GPIO | GpioCntlr | GpioCntlrAdd | GpioCntlrRemove |
I2C | I2cCntlr | I2cCntlrAdd | I2cCntlrRemove |
MIPI_DSI | MipiDsiCntlr | MipiDsiRegisterCntlr | NA |
PWM | PwmDev | PwmDeviceAdd | PwmDeviceRemove |
RTC | RtcHost | RtcHostCreate | RtcHostDestroy |
SPI | SpiCntlr | SpiCntlrCreate | SpiCntlrDestroy |
UART | UartHost | UartHostCreate | UartHostDestroy |
WATCHDOG | WatchdogCntlr | WatchdogCntlrAdd | WatchdogCntlrRemove |
DMA | DmaCntlr | DmaCntlrAdd | DmaCntlrRemove |
OpenHarmony系统平台驱动适配介绍
下面以uart/i2c/gpio三个典型模块为例介绍平台驱动适配的一般方法
UART模块的适配
UART模块适配的核心环节,是UartHost对象的创建、初始化及注册。
UART模块采用的是独立服务模式,要求每一个UartHost对象关联一个HDF设备服务,因此:
1). device_info.hcs中为每一个UART控制器配置一个HDF设备节点
device_uart :: device {
device0 :: deviceNode {
policy = 1;
priority = 40;
permission = 0644;
moduleName = "HDF_PLATFORM_UART";
serviceName = "HDF_PLATFORM_UART_0";
deviceMatchAttr = "hisilicon_hi35xx_uart_0";
}
device1 :: deviceNode {
policy = 2;
permission = 0644;
priority = 40;
moduleName = "HDF_PLATFORM_UART";
serviceName = "HDF_PLATFORM_UART_1";
deviceMatchAttr = "hisilicon_hi35xx_uart_1";
}
}
服务policy大于等于1(如需对用户态可见为2,仅内核态可见为1);
moduleName需要与驱动Entry中moduleName 保持一致;
ServiceName必须要按照HDF_PLATFORM_UART_X的格式,X为UART控制器编号
deviceMatchAttr用于配置控制器私有数据,要与uart_config.hcs中对应控制器保持一致,如不需要则忽略
2). uart_config.hcs中为每一个UART控制器配置私有数据
如果控制器需要配置一些私有数据,例如寄存器基地址,初始化波特率等等,可以在uart_config.hcs中配置,该文件将在产品配置目录的hdf.hcs中导入,具体路径可由产品自由配置。
root {
platform {
template uart_controller {
match_attr = "";
num = 0;
baudrate = 115200;
fifoRxEn = 1;
fifoTxEn = 1;
flags = 4;
regPbase = 0x120a0000;
interrupt = 38;
iomemCount = 0x48;
}
controller_0x120a0000 :: uart_controller {
match_attr = "hisilicon_hi35xx_uart_0";
}
controller_0x120a1000 :: uart_controller {
num = 1;
baudrate = 9600;
regPbase = 0x120a1000;
interrupt = 39;
match_attr = "hisilicon_hi35xx_uart_1";
}
}
要注意的一点是每个控制器要独立配置一个uart_controller节点,并且其match_attr要与device_info.hcs中的deviceMatchAttr一致。
3).驱动的Entry结构需要有Bind方法,用于绑定服务
struct HdfDriverEntry g_hdfUartDevice = {
.moduleVersion = 1,
.moduleName = "HDF_PLATFORM_UART",
.Bind = HdfUartDeviceBind,
.Init = HdfUartDeviceInit,
.Release = HdfUartDeviceRelease,
};
HDF_INIT(g_hdfUartDevice);
注意:在Bind方法中,要使用UartHostCreate创建控制器对象并完成服务绑定。
static int32_t HdfUartDeviceBind(struct HdfDeviceObject *device)
{
HDF_LOGI("%s: entry", __func__);
if (device == NULL) {
return HDF_ERR_INVALID_OBJECT;
}
return (UartHostCreate(device) == NULL) ? HDF_FAILURE : HDF_SUCCESS;
}
4).UartHostCreate的实现屏蔽了一些细节
struct UartHost *UartHostCreate(struct HdfDeviceObject *device)
{
struct UartHost *host = NULL;
host = (struct UartHost *)OsalMemCalloc(sizeof(*host));
host->device = device;
device->service = &(host->service);
host->device->service->Dispatch = UartIoDispatch;
OsalAtomicSet(&host->atom, 0);
host->priv = NULL;
host->method = NULL;
return host;
}
该方法中将UartHost对象同HdfDeviceObject进行了关联:关键环节是为HdfDeviceObject的service成员进行赋值,使其指向UartHost的IDeviceIoService类型的成员对象;同时为service成员的Dispatch方法赋值。这样:
为HdfDeviceObject对象绑定了IDeviceIoService类型的服务对象
UartHost和其IDeviceIoService类型的成员对象service可以相互转换
通过UartHost对象即可获取HdfDeviceObject对象
通过HdfDeviceObject对象即可间接获取UartHost对象(先获取service再转为host)
5).在Init方法中完成UartHost对象的初始化
int32_t HdfUartDeviceInit(struct HdfDeviceObject *device)
{
int32_t ret;
struct UartHost *host = NULL;
host = UartHostFromDevice(device);
ret = Hi35xxAttach(host, device);
host->method = &g_uartHostMethod;
return ret;
}
这里通过UartHostFromDevice从HdfDeviceObject对象获取之前关联的UartHost对象,然后调用Hi35xxAttach方法完成host对象的初始化
最后为host对象挂接钩子方法,这里不再一一分析这些钩子方法,建议读者直接去查看源码。
struct UartHostMethod g_uartHostMethod = {
.Init = Hi35xxInit,
.Deinit = Hi35xxDeinit,
.Read = Hi35xxRead,
.Write = Hi35xxWrite,
.SetBaud = Hi35xxSetBaud,
.GetBaud = Hi35xxGetBaud,
.SetAttribute = Hi35xxSetAttribute,
.GetAttribute = Hi35xxGetAttribute,
.SetTransMode = Hi35xxSetTransMode,
.pollEvent = Hi35xxPollEvent,
};
总结
UART适配的关键是要在驱动Entry的Bind方法中创建UartHost对象,而且是使用UartHostCreate创建。这个创建动作同时也是注册的动作,因为它将UartHost以HDF设备服务的形式同HdfDeviceObject进行绑定,这样就完成了服务的发布,HDF Manager对设备服务的管理也就是对UartHost的管理,核心层可以通过HDF提供的服务获取接口来访问UartHost。
UART适配采用独立服务模式,每一个UartHost对象同时也是一个设备服务,其优点是可以直接利用HDF Manager进行管理;缺点是需要在device_info.hcs为每一个UartHost对象定义设备节点。
说明
UART模块适配涉及到的代码示例片段来自device/hisilicon/drivers/uart/
您好。请教您个问题,adc是统一服务模式吧,我用hdc进入开发板系统下可以调用采集成功,用napi出现
Failed to dispatch serv call ioctl -2
AdcOpen: service call ADC lO OPEN fail, ret: <private>!
您知道什么原因吗