- 移植对接简单统一, 所有移植对接只需要移植 open close ioctrl read wirte即可
- 上层调用API更加贴近用户层
- 支持类posix接口调用
- 仅依赖xf_utils, 方便移植到任何设备上
- 所有外设都支持独立的屏蔽
- 支持对接锁,防止多任务竞争
- 方便添加新的外设
在嵌入式开发中,往往芯片不同,其sdk也不同。即使都是使用gpio去点灯,也会因为函数不同而不能通用。每个sdk都需要去熟悉,导致学习成本和代码书写成本增加。
和hal相关的设计有很多,常见的有stm32的hal库,posix的外设接口等。
stm32的hal库的接口可能适合其自身,但是拓展到如果兼容所有芯片,其接口就无法满足所有mcu的需求。 posix的接口比较统一。但是在这种高度统一化的操作背后,实际上这种操作在使用者角度上比较困扰。
于是我们结合上述的方式, 形成xf_hal的设备抽象层。
xf_hal 的源码文件夹中,分为kernel和device。其中kernel是其核心部分,device部分则可以自由新增抽象层。 整体的结构如下:
graph TB
A["Device"]:::wideBox
B["Dirver"]:::wideBox
C["BSP"]:::wideBox
subgraph BigBox [arch]
A:::wideBox
B:::wideBox
C:::wideBox
end
style A fill:#f9f,stroke:#333,stroke-width:1px;
style B fill:#bbf,stroke:#333,stroke-width:1px;
style C fill:#fbf,stroke:#333,stroke-width:1px;
细节结构图如下:
graph TB
A["dev_table[DEV_TABLE_SIZE]"]
B["GPIO"]
C["PWM"]
D["TIM"]
E["UART"]
F["I2C"]
G["SPI"]
H["ADC"]
I["DAC"]
A --> B
A --> C
A --> D
A --> E
A --> F
A --> G
A --> H
A --> I
subgraph J["xf_hal_driver_t"]
B
C
D
E
F
G
H
I
end
B --> B1["IO1"]
B1 --> B2["IO22"]
B2 --> B3["IO10"]
C --> C1["pwm1channel2"]
C1 --> C2["pwm1channel4"]
D --> D1["timer0channel1"]
E --> E1["uart0"]
E1 --> E2["uart1"]
F --> F1["i2c1"]
F1 --> F2["i2c2"]
G --> G1["spi0"]
H --> H1["adc0channel0"]
H1 --> H2["adc0channel1"]
H2 --> H3["adc0channel2"]
H3 --> H4["adc0channel3"]
I --> I1["dac1channel0"]
subgraph K["xf_hal_dev_t"]
B1
B2
B3
C1
C2
D1
E1
E2
F1
F2
G1
H1
H2
H3
H4
I1
end
static xf_hal_driver_t dev_table[DEV_TABLE_SIZE] = {0};这是一个全局静态数组,是所有设备的起点。所有设备通过它可以遍历。其个数为 DEV_TABLE_SIZE ,这个宏的定义为:
typedef enum _xf_hal_type_t {
#define XF_HAL_TABLE_TYPE
#include "../device/xf_hal_reg_table.inc"
XF_HAL_TYPE_MAX
} xf_hal_type_t;通过device文件夹中的“注册表” xf_hal_reg_table.inc,预编译阶段生成枚举对象。所以 XF_HAL_TYPE_MAX 自然是所有注册的外设。 dev_table 数组中的每个 xf_hal_driver_t 对象。对应着已经注册的外设中的每个外设。
typedef struct _xf_hal_driver_t {
xf_list_t dev_list;
uint32_t flag : 16;
uint32_t dev_count : 16;
xf_driver_ops_t driver_ops;
xf_hal_dev_create_t constructor;
#if XF_HAL_LOCK_IS_ENABLE
void *mutex;
#endif
} xf_hal_driver_t;这是一个内部的对象(外部无法访问),他代表了通用的外设驱动的类型。
- 其中包含了dev_list,这里是用于挂载具体外设的链表。
- flag表示其本身的读写的标志。
- dev_count则表示挂载在这上面的外设的数量。
- driver_ops表示其操作函数,主要有open, ioctl, read, write,close。这部分会在底层做对接。
- constructor则是有外设层带来的
- mutex 这部分则是考虑到多任务的线程保护,需要在 xf_hal_config.h中设置XF_HAL_LOCK_DISABLE为0,方可开启。
我们的静态数组中有多少外设就有多少 xf_hal_driver_t 对象。这个对象主要用来保存对接的操作函数 driver_ops 的。其下挂载的就是实际使用的设备。
每当设备(例如:GPIO)开始调用时,都要通过 xf_hal_driver_create(本质是constructor) 构造函数构造一个指定类型、指定ID的 xf_hal_dev_t 对象。这个对象会在 open 阶段,挂载到指定 xf_hal_driver_t 对象下面。然后经过 ioctrl 阶段进行配置参数。那么就可以通过 write、read 操作设备。最后,通过close,解除挂载并回收内存。
所以,针对移植者来说。无论任何的设备只需要对接好,以下五个回调函数即可:
typedef struct _xf_driver_ops_t {
xf_err_t (*open)(xf_hal_dev_t *dev);
xf_err_t (*ioctl)(xf_hal_dev_t *dev, uint32_t cmd, void *config);
int (*read)(xf_hal_dev_t *dev, void *buf, size_t count);
int (*write)(xf_hal_dev_t *dev, const void *buf, size_t count);
xf_err_t (*close)(xf_hal_dev_t *dev);
} xf_driver_ops_t;然而正如我之前所说,对于应用者来说。这五个操作函数(类posix)并不直观。
所以,在此之上,有了更加偏向应用层的封装。
kernel 基本上确定了整体的设备管理框架。但是具体设备如何方便添加呢?
这里就时我们设置device文件夹的意义。一个新的device添加是和kernel是解耦的。
kernel中的device对象:
typedef struct _xf_hal_dev_t {
xf_list_t node;
uint32_t type; /*!< 保存外设类型 */
uint32_t id; /*!< id 用于保存设备唯一标识符 */
void *platform_data; /*!< 用户通过该参数穿越不同的 ops 之间 */
#if XF_HAL_LOCK_IS_ENABLE
void *mutex;
#endif
} xf_hal_dev_t;所有的具体设备类都需要继承 xf_hal_dev_t 类。
例如:我们需要添加一个 PWM 设备:
-
创建 xf_hal_pwm.c/.h
-
将外设名称写入 xf_hal_reg_table.inc 注册表中。
XF_HAL_REG(PWM)- 在 xf_hal_pwm.c 中创建自己的 pwm 设备类:
typedef struct _xf_hal_pwm_config_t {
bool enable; /*!< 启用参数,0 为禁用,1 为启用 */
uint32_t freq; /*!< 频率参数,设置 pwm 频率单位为 hz */
uint32_t duty; /*!< 占空比参数,设置 duty ,最大值为 `2^duty_resolution - 1` */
uint32_t duty_resolution; /*!< 占空比分辨率参数,直接影响 duty 的最大值 */
xf_gpio_num_t io_num; /*!< 输出 IO 参数,指定 pwm 的输出 io */
} xf_hal_pwm_config_t;
typedef struct _xf_hal_pwm_t {
xf_hal_dev_t dev; // 一定要放到第一个,以便后续close一起free
xf_hal_pwm_config_t config;
} xf_hal_pwm_t;这个设备类中,需要我们将 dev 放到第一个做继承。config则是pwm需要ioctrl可以配置的内容
- 在 xf_hal_pwm.c 实现底层注册函数
#define XF_HAL_PWM_TYPE XF_HAL_PWM
xf_err_t xf_hal_pwm_register(const xf_driver_ops_t *driver_ops)
{
return xf_hal_driver_register(XF_HAL_PWM_TYPE, XF_HAL_FLAG_ONLY_READ, pwm_constructor, driver_ops);
}可以看到本质上调用的是 xf_hal_driver_register 其中第一个参数是类型,这个通过之前的注册表会生成 XF_HAL_PWM 宏。这里指定为 XF_HAL_PWM 就行。第二个是flag。我们采用只读模式即可。第三个是构造函数:
static xf_hal_dev_t *pwm_constructor(xf_pwm_num_t pwm_num)
{
xf_err_t err = XF_OK;
xf_hal_dev_t *dev = NULL;
xf_hal_pwm_t *dev_pwm = (xf_hal_pwm_t *)xf_malloc(sizeof(xf_hal_pwm_t));
XF_ASSERT(dev_pwm, NULL, TAG, "memory alloc failed!");
dev = (xf_hal_dev_t *)dev_pwm;
err = xf_hal_driver_open(dev, XF_HAL_PWM_TYPE, pwm_num);
if (err) {
XF_LOGE(TAG, "open failed!");
xf_free(dev);
return NULL;
}
return dev;
}构造函数本身是帮我们构造了一个 xf_hal_pwm_t 大小的内存,并返回给底层。 底层拿到这个例程后完成
- 在 xf_hal_port.h 添加自己的底层注册函数
xf_err_t xf_hal_pwm_register(const xf_driver_ops_t *driver_ops);- 封装 open close read write ioctrl 。简化应用层调用
xf_err_t xf_hal_pwm_set_duty(xf_pwm_num_t pwm_num, uint32_t duty)
{
xf_err_t err = XF_OK;
xf_hal_dev_t *dev = xf_hal_device_find(XF_HAL_PWM_TYPE, pwm_num);
xf_hal_pwm_t *dev_pwm = (xf_hal_pwm_t *)dev;
XF_HAL_PWM_CHECK(!dev_pwm, XF_ERR_INVALID_ARG, "pwm is not init!");
#if XF_HAL_LOCK_IS_ENABLE
xf_lock_lock(dev_pwm->dev.mutex);
#endif
dev_pwm->config.duty = duty;
#if XF_HAL_LOCK_IS_ENABLE
xf_lock_unlock(dev_pwm->dev.mutex);
#endif
err = xf_hal_driver_ioctl(dev, XF_HAL_PWM_CMD_DUTY, &dev_pwm->config);
XF_HAL_PWM_CHECK(err, err, "config failed!");
return XF_OK;
}对于移植层来说,xf_hal提供了统一的五种对接函数。对于应用层来说,xf_hal 提供更加贴近用户使用习惯的api,方便用户调用。
- 安装xmake
- 获取xf_utils
git clone https://github.com/x-eks-fusion/xf_utils.git- 安装gcc gdb工具链
sudo apt install gcc -y
sudo apt install gdb -y- 编译
xmake- 运行
xmake r