本次定义的HAL(硬件抽象层),其目标是总结openUBMC对BMC SOC硬件的依赖,提供一层抽象,屏蔽BMC芯片差异。 达成的效果:用户(这里指驱动开发者,后文同)在实现完HAL层制定的API后,能够把openUBMC的核心服务拉起。
- 核心服务
- 框架,包括:
- 设备驱动框架:hwproxy/hwdiscovery/devmon/component_drivers,基本的资源树接口(Chip, Bus)
- 应用管理框架:libmcpp/libmc4lua
- 基础服务子系统
- 闭源组件
- 对硬件的哪些依赖应该纳入HAL层?
- 没有硬性规则,当前以libsoc_adapter为蓝本。 如果用户需要扩展HAL层定义,应当在社区进行评审。
- 核心服务的依赖应该纳入
- 只服务于特定硬件实现的,不应当纳入,比如SD RAID
- 未被纳入HAL层的部分,在移植时,应当由使用这部分依赖的组件负责移植
HAL定义的方针
针对现有的使用情况,简化API接口(明确输入输出,参数正交化),去除和特定实现强相关的形式。
注意,本HAL层的目标不是定义通用的接口(如Linux内核驱动),理由:
- HAL的基础目标是明确openUBMC对硬件的依赖 通用驱动面对的业务场景是不确定的,因此接口定义尽可能地通用; openUBMC的业务已经确定,因此仅针对现有业务定义接口; 如果定义通用的接口:
- 没有使用场景 比如,Linux 定义的 Canbus 驱动可以配置传输的bit timing,openUBMC根本不会使用这个特性
- 增加移植的工作量
- 驱动必须支撑资源树接口的实现。 有些资源树接口定义本就不通用,比如Uart的串口互联功能,事实上不是一个通用的特性,但因为
bmc.kepler.Managers.Uart接口定义了这个特性,必须有驱动支撑,所以有HAL定义。
- 针对通用驱动有利于未来扩展这一点:
- 如果是扩展的框架特性需要某一接口,则可以在增加框架功能时扩充HAL层定义
- 如果是特定业务组件(非核心业务)需要某一接口,则用户自行扩展驱动实现即可,openUBMC的核心业务仍旧使用原来的接口。
类图
驱动接口:IDriver 具体驱动的接口示例:Adc
classDiagram
%% ================================
%% 驱动工厂 - 单例+工厂方法模式
%% ================================
class IDriverFactory {
<<interface>>
<<factory>>
+set_driver_path(driver_path: string_t&) void
+get_driver(const string_t& driver_name) IDriver&
+get_driver_load_info(void) string_t void
+remove_driver(driver_name: string_t&) void
get_instance()$ IDriverFactory&
}
class driver_factory {
<<singleton>>
<<factory>>
-m_drivers: map~string_t,driver_ptr~
-m_driver_path: string_t
+set_driver_path(driver_path: string_t&) void
+get_driver(const string_t& driver_name) IDriver&
+get_driver_load_info(void) string_t void
+remove_driver(driver_name: string_t&) void
+get_instance()$ driver_factory&
}
%% ================================
%% ABI接口层
%% ================================
class IDriver {
<<interface>>
+driver_name: char*
+init(args: void*) void
+config(config: void*) void
+lock() void
+unlock() void
}
%% ================================
%% ABI接口层 - 驱动抽象类
%% ================================
class Adc {
<<interface>>
+read(index: uint32_t) uint32_t
}
class Bt {
<<interface>>
+read(index: uint32_t, len: uint32_t, timeout: uint32_t) string_t
+write(index: uint32_t, val: string_view&, timeout: uint32_t) void
}
class AdcImpl {
+read() uint32_t
}
class BtImpl {
+read(index: uint32_t, len: uint32_t, timeout: uint32_t) string_t
+write(index: uint32_t, val: string_view&, timeout: uint32_t) void
}
%% ================================
%% 接口实现关系
%% ================================
IDriverFactory <|.. driver_factory : 继承
IDriver <|.. Adc : 继承
IDriver <|.. Bt : 继承
Adc <|.. AdcImpl : 实现
Bt <|.. BtImpl : 实现
%% ================================
%% 组合关系
%% ================================
driver_factory *-- IDriver : 管理
%% ================================
%% 依赖关系
%% ================================
driver_factory --> IDriver : 创建
%% ================================
%% 样式定义
%% ================================
style IDriverFactory fill:#fff3e0
style driver_factory fill:#ff9800
style IDriver fill:#fff3e0
style Adc fill:#fff3e0
style Bt fill:#fff3e0
style AdcImpl fill:#c9f2c7
style BtImpl fill:#c9f2c7
IDriver的设计
- 语言,沿用libsoc_adapter,使用C++作为接口语言
- 类命名,统一使用
Xyz作为Xyz驱动的接口类名,采用大驼峰
- namespace,沿用libsoc_adapter的处理,驱动
Xyz的namespace为DRIVER_XYZ
- 每个
Xyz实例,都是xyz设备的总管,可以操作所有的xyz设备
- 设备操作接口,统一增加一个
index入参,用于区分xyz设备。 index的分配由驱动实现决定;
- 对比 libsoc_adapter的处理,
Xyz实例
- 有的是指单个xyz设备,或者单例(比如Ipmb, Efuse)
- 有的是xyz设备的总管(比如I2c)
- 不提供
ioctl方式的API ioctl(fd, cmdcode, param) 该形式和定义函数接口没有本质差别 且API面向的业务场景是确定的,可以明确定义函数接口
ctor 取消在构造函数传递device的做法,要不要打开设备、打开什么设备、打开几个设备,这些均取决于实现
init 用于驱动的初始化,允许传递任意入参
- 框架本身对驱动总管并没有配置诉求
- 业务组件开发者在知晓具体驱动实现的情况下,可以传递配置给驱动 这里仅支持通过初始化配置驱动的行为,不支持在具体操作接口,比如
read,传递自定义参数。因为
- 框架对驱动的调用是固定的
- 如果用户自定义的业务想要传参,可以自己扩展接口
config 用于设备实例的初始化,允许传递任意入参。
- 框架使用的入参形式,在每个接口类处定义,其命名为
XyzConfig。比如AdcConfig 该结构体主要是为了支撑MDS属性的实际生效
lock/unlock 锁操作,但不限定锁的形式,也不限定锁和设备的对应关系
get_version 为了兼容考虑
锁、并发控制
对驱动API的调用,遵循以下原则:
- 初始类API,如 init, config, open,在读写操作、读写设置动作之前,且这些初始化动作一般是单线程
- 销毁类API,close, free,在读写操作、读写设置动作完全停止之后,且一般是单线程
- 写设置API,在读写操作完全停止之后。写设置 通常由修改发起者 进行并发控制
- 读设置API,一般没有限定
因此,只需要对读写动作进行加锁,由IDriver的lock()/unlock()方法提供。 由于openUBMC的基本运行单元是组件,不是进程/线程,无法断定两个组件是否运行于同一个进程,因此,要求使用进程锁。
HAL定义手法
- 接口基本沿用libsoc_adapter
- 入参和返回值的形式:尽量删除为数据传递而定义的结构体,比如I2c的读操作定义的
I2C_READ_S
- 对于读写操作(读出的报文、写入的报文),如果BMC对其有格式要求,明确作出结构体定义。比如
I2c::write,写入的内容实际上是有格式的,定义I2cWriteMsg结构,并标注在注释中。
- 删除/调整部分接口,但是确保MDS和MDB接口可用
用户开发示例
class AdcImpl : Adc {
private:
std::mutex m_mutex;
int32_t m_fd;
public:
AdcImpl(){}
uint32_t read(int32_t index) override;
};
void AdcImpl::init(void *args)
{
m_mutex = new std::mutex;
}
void AdcImpl::config(int32_t index, void *config)
{
AdcConfig *cfg = (AdcConfig *)config;
}
void AdcImpl::lock(int32_t index)
{
m_mutex.lock;
}
void AdcImpl::unlock(int32_t index)
{
m_mutex.unlock;
}
uint32_t AdcImpl::read(int32_t index)
{
uint32_t ret = ioctl(m_fd, ADC_CMD_READ, param)
return ret;
}
Driver* create_driver()
{
AdcImpl *drv = new AdcImpl();
return (Driver*)drv;
}
void destroy_driver(Driver* drv)
{
delete (AdcImpl *)drv;
}
组件使用
int main()
{
ret = drv.read();
}
static IDriverFactory & get_instance()
获取驱动工厂实例单例
Definition driver.cpp:207
virtual IDriver & get_driver(const string_t &driver_name)=0
获取驱动实例 加载driver_path下的 lib{driver_name}.so,返回驱动实例
HAL层应用处理策略
- 用户自行增加驱动定义 如果一个驱动需要用户定义,由两种可能:
- 驱动涉及openUBMC核心组件/闭源组件。这个属于HAL定义的缺陷,应当避免。如有该情况,应当尽快扩展HAL定义。
- 非核心业务组件依赖特定驱动,只需要业务组件和驱动实现协商好接口即可。 用户在任意位置从
IDriver类派生出一个新的接口类并实现,并将生成的so放置在指定路径下,即可使用。
- 不支持的驱动 如果一个驱动不支持,则用户不提供对应的so,
IDriverFactory::get_driver接口抛错,使用方必须处理错误 通过IDriverFactory::get_driver_load_info检查已经加载的驱动
- 驱动中有不支持的接口 应提供打桩
HAL的纳入范围
| libsoc_adapter驱动名称 | HAL纳入 | 备注 |
| adc | Y | |
| bt | Y | |
| btc | N | 和BT是同类,没有必要要求开发者提供两种实现 |
| canbus | Y | |
| comm | N | 引脚复用,非通用;接口形式难以确定 |
| cp | N | 协处理器,非通用 |
| dboot_info | N | uboot信息,不同soc形式不同 |
| dfx | N | 不同soc不同 |
| edma | Y |
| efuse | Y |
| gpio | Y |
| hisport | Y |
| i2c | Y |
| innerbus | Y |
| ipmb | Y |
| jtag | Y |
| kcs | Y |
| localbus | Y |
| localbus_mmap | N | 和localbus是同类 |
| mctp | Y |
| mdio | Y |
| mem | Y |
| mmc | Y |
| peci | Y |
| pwm | Y |
| rmii | N | 实际上是一个lib,不是硬件驱动 |
| sd_raid | N | 没有人使用 |
| sol | Y |
| spi | Y |
| trng | Y |
| uart | Y |
| usb_drd | Y |
| usb_driver | Y |
| wdt | Y |
关于<tt>read</tt>/<tt>write</tt>的设计
read/write不作为基类IDriver的方法,每个类读写操作的入参、返回值都不一样。 类似string_t read(void *args)的定义,用户仍需自己进行类型转换,本质上没有对read/write做任何抽象,徒增理解难度。
判断写操作是否成功