5.2. PikaStdDevice 标准设备

PikaStdDevice 是一个抽象的设备模型,提供了跨平台的统一外设 API 。

5.2.1. 安装

  • 在 requestment.txt 中加入 PikaStdDevice 的依赖。

PikaStdDevice
  • 运行 pikaPackage.exe

5.2.2. 为什么要有标准设备模块

什么是标准设备模块呢?我们先从其他的脚本技术说起,比如 MicroPython,并没有统一的外设调用 API,这使得用户在使用不同的平台时,都需要重新学习 API,比如下面这个是 MicroPython 在 STM32F4 平台驱动 GPIO 的代码。

_images/1638495380966-02a52d33-9986-401c-a7e1-136ce71ad53e.webp

这个是 ESP8266 的

_images/1638495381179-e6afcca5-7f32-4a2f-a531-10f6b106db15.webp

可以明显看到在选择 pin 的管脚时,一个用的是字符串,而另一个用的是整型数,控制电平时,一个使用 high()low()方法,而另一个使用on()off()方法,总之驱动的API标准很混乱。 有没有什么办法,能够统一外设的 API,使得用户只需要熟悉一套 API,就能够在任意平台通用呢? 方法是有的,就是 PikaStdDevice 标准设备驱动模块。

5.2.3. 模块结构

_images/image-20221207125315706.png

  • PikaStdDevice 模块提供了 GPIOIICPWM 等基础的外设 Python 模块。

  • PikaStdDevice 基于 pika_hal 设备抽象层,pika_hal是一个纯 c 语言的设备抽象层,将不同平台的外设操作都统一为相同的 APIPikaStdDevice 调用,这样不同的平台 (STM32、ESP32、BL602) 等都可以使用通用的 Python 代码来控制设备了。

  • pika_hal 设备抽象层需要在不同的平台进行适配 (Platform Port),通过在不同的平台重写形如 pika_hal_platform_xxxx()WEAK 函数,就可以为不同的平台提供支持。

  • 除了 PikaStdDevice 模块之外,还有一些 sensor / motorPython 模块,也基于 pika_hal 开发,这些模块使用的是 pika_halGPIOIICPWM 等适配好的功能,所以不需要除了 pika_hal 之外额外的适配就可以使用。

5.2.4. PikaStdDevice 模块示例

以 GPIO 模块为例,以下是 PikaStdDevice 定义的用户 API

class GPIO:
    def __init__(self):
        pass

    def init(self):
        pass

    def setPin(self, pinName: str):
        pass

    def setId(self, id: int):
        pass

    def getId(self) -> int:
        pass

    def getPin(self) -> str:
        pass

    def setMode(self, mode: str):
        pass

    def getMode(self) -> str:
        pass

    def setPull(self, pull: str):
        pass

    def enable(self):
        pass

    def disable(self):
        pass

    def high(self):
        pass

    def low(self):
        pass

    def read(self) -> int:
        pass

PikaStdDevice 模块的示例代码在 https://gitee.com/Lyon1998/pikapython/tree/master/examples/Device 路径下,示例中的 machine 模块是 PikaStdDevice 模块的简单重命名。

5.2.5. pika_hal 设备抽象层

5.2.5.1. 讲解视频

【Pika Python 设备抽象层】pika_hal

5.2.5.2. 设计理念

  • 高效。纯 C 语言实现,内部环节精简。

  • 标准。采用类 linux 的设计,所有类型的设备操作有且仅有类似于文件的 5 个标准 API: open()close()write()read()ioctl()

5.2.5.3. 编程模型

_images/image-20221207132547828.png

所有设备均遵循类 linux 文件的编程模型,所有类型的设备均使用 pika_dev 结构体来作为设备句柄,所有类型的设备均有且只有以下五个控制 API:

5.2.5.4. open()

  • 概述

    open() 函数用于打开一个设备,最先调用。

  • 函数原型

    pika_dev* pika_hal_open(PIKA_HAL_DEV_TYPE dev_type, char* name);
    
  • 参数

参数 类型 功能 备注
dev_type PIKA_HAL_DEV_TYPE 设备类型 如 PIKA_HAL_GPIO 为 GPIO 设备,PIKA_HAL_SPI 为 SPI 设备。
name char* 设备名 如 PA0 ,SPI2 等
(return) pika_dev 设备句柄 如果成功打开设备,将会返回设备句柄 pika_dev 的指针,如果打开失败会返回 NULL。

5.2.5.5. close()

  • 概述

    close() 函数用于关闭一个设备,最后调用,关闭设备时需要调用 pika_hal_close() 避免出现内存泄漏。

  • 函数原型

    int pika_hal_close(pika_dev* dev);
    
  • 参数

参数 类型 功能 备注
dev pika_dev* 设备句柄 要操作的设备句柄。
(return) int 错误值 错误值为 0 表示操作成功,其他返回值表示操作失败,返回值为错误码。

5.2.5.6. ioctl()

  • 概述

    ioctl() 函数用于对设备进行控制,包括:

    • 配置 - config

    • 使能 - enable

    • 失能 - disable

  • 函数原型

    int pika_hal_ioctl(pika_dev* dev, PIKA_HAL_IOCTL_CMD cmd, ...);
    
  • 参数

参数 类型 功能 备注
dev pika_dev* 设备句柄 要操作的设备句柄。
cmd PIKA_HAL_IOCTL_CMD 控制命令 可填 PIKA_HAL_IOCTL_ENABLE,PIKA_HAL_IOCTL_DISABLE,PIKA_HAL_IOCTL_CONFIG 三个命名,分别对应使能、失能和配置。
... (None)/pika_hal_config_XXXX * 控制参数 该参数可填可不填,根据 cmd 的取值而定。当 cmd 为 PIKA_HAL_IOCTL_ENABLE、PIKA_HAL_IOCTL_DISABLE 时,该参数不填。当 cmd 为 PIKA_HAL_IOCTL_CONFIG 时,该参数为 pika_hal_config_XXXX *cfg,其中 XXXX 是设备的类型,如 pika_hal_config_GPIO 、pika_hal_config_SPI 等,应和 pika_hal_open() 中使用的设备的类型相同。
(return) int 错误值 错误值为 0 表示操作成功,其他返回值表示操作失败,返回值为错误码。

5.2.5.7. read()

  • 概述

    read() 函数用于从设备中读取数据。

  • 函数原型

    int pika_hal_read(pika_dev* dev, void* buf, size_t len);
    
  • 参数

参数 类型 功能 备注
dev pika_dev* 设备句柄 要操作的设备句柄。
buf void* 读取缓冲区 对于 GPIO、ADC 这样只能读取单个数据的设备,缓冲区使用 uint32_t。
len size_t 读取的字节数 对于 GPIO、ADC 这样只能读取单个数据的设备,长度为 sizeof(uint32_t)。
(return) int 错误值 错误值为 0 表示操作成功,其他返回值表示操作失败,返回值为错误码。

5.2.5.8. write()

  • 概述

    write() 函数用于向设备写入数据。

  • 函数原型

    int pika_hal_write(pika_dev* dev, void* buf, size_t len);
    
  • 参数

参数 类型 功能 备注
dev pika_dev* 设备句柄 要操作的设备句柄。
buf void* 写入缓冲区 对于 GPIO、DAC 这样只能写入单个数据的设备,缓冲区使用 uint32_t。
len size_t 写入的字节数 对于 GPIO、DAC 这样只能读取单个数据的设备,长度为 sizeof(uint32_t)。
(return) int 错误值 错误值为 0 表示操作成功,其他返回值表示操作失败,返回值为错误码。

5.2.5.9. ioctl config

pika_hal_ioctl()cmd 参数填入 PIKA_HAL_IOCTL_CONFIG 时对设备进行配置,这一部分在设备驱动中是最关键的,因此单独说明。

cmdPIKA_HAL_IOCTL_CONFIG 时,pika_hal_ioctl() 的第三个参数为配置结构体的指针 pika_hal_config_XXXX *cfg,其中 XXXX 是设备的类型,如 pika_hal_config_GPIOpika_hal_config_SPI 等。

5.2.5.9.1. 配置结构体

创建配置结构体时,应确保结构体被清零,避免未定义行为。推荐写法:

pika_hal_config_XXXX cfg = {0};

配置结构体在 pika_hal.h 中定义,如 GPIO 的配置结构体:

typedef struct {
    PIKA_HAL_GPIO_DIR dir;
    PIKA_HAL_GPIO_PULL pull;
    PIKA_HAL_GPIO_SPEED speed;
    void (*event_callback_rising)(pika_dev* dev);
    void (*event_callback_falling)(pika_dev* dev);
} pika_hal_GPIO_config;

5.2.5.9.2. 配置项

配置结构体中的配置项的取值通常由 enum 定义,如 PIKA_HAL_GPIO_DIR 代表的 GPIO 的方向,其取值由以下的 enum 来决定:

typedef enum {
    _PIKA_HAL_GPIO_DIR_UNUSED = 0,
    PIKA_HAL_GPIO_DIR_IN,
    PIKA_HAL_GPIO_DIR_OUT,
} PIKA_HAL_GPIO_DIR;

PIKA_HAL_GPIO_DIR_INPIKA_HAL_GPIO_DIR_OUT 表示 GPIO 的方向为输入或者输出,是常规的取值。而 _PIKA_HAL_GPIO_DIR_UNUSED 是一个特殊的取值,这个取值表示不在配置结构体中使用 PIKA_HAL_GPIO_DIR 这个配置项。

_PIKA_HAL_GPIO_DIR_UNUSED 被指定时,实际的配置操作会进行下面的处理:

  • 如果没有配置过该配置项,那么该配置项会被自动赋予默认值。(默认配置)

  • 如果已经配置过该配置项,那么该配置项会维持原先的状态。(配置差分修改)

5.2.5.9.3. 默认配置

这样设计的好处在于,在使用 ioctl config 对设备进行初次配置时,不必填写所有的配置项,只需要填写实际关心的配置项即可,这降低了填写配置项的心智负担。

比如,如果在应用中不关心 GPIO 的翻转速度,那么可以这样填写配置结构体:

pika_hal_config_GPIO cfg = {0};
cfg.dir = PIKA_HAL_GPIO_DIR_OUT;
cfg.pull = PIKA_HAL_GPIO_PULL_UP;

这时 cfg.speed 取值为 _PIKA_HAL_GPIO_SPEED_UNUSED (创建清零的结构体后,所有配置项均为 0,而 _PIKA_HAL_XXXX_UNUSED 的数值总是 0)

在运行 ioct config 后,实际设备接收到的配置值如下:

cfg.dir = PIKA_HAL_GPIO_DIR_OUT;
cfg.pull = PIKA_HAL_GPIO_PULL_UP;
cfg.speed = PIKA_HAL_GPIO_SPEED_10M;
cfg.event_callback_rising = NULL;
cfg.event_callback_falling = NULL;

这时 cfg.speed 会自动被设置为默认值 PIKA_HAL_GPIO_SPEED_10M

5.2.5.9.4. 配置差分修改

在对设备进行再次配置时,通常只想要对设备的某几个值进行修改,而不是修改全部值,因此不想修改的值可以填写为 _PIKA_HAL_XXXX_UNUSED

例如:

pika_hal_config_GPIO cfg2 = {0};
cfg2.dir = PIKA_HAL_GPIO_DIR_IN;

在运行 ioct config 后,实际设备接收到的配置值如下:

cfg.dir = PIKA_HAL_GPIO_DIR_IN;
cfg.pull = PIKA_HAL_GPIO_PULL_UP;
cfg.speed = PIKA_HAL_GPIO_SPEED_10M;
cfg.event_callback_rising = NULL;
cfg.event_callback_falling = NULL;

只有被指定了的 dir 配置项被修改了,其他的配置项均维持原状。

5.2.5.9.5. 配置合并策略

配置的默认值和合并策略(新的配置结构体如何合并进旧的配置值)在 pika_hal.c 中的 pika_hal_XXXX_ioctl_merge_config() 函数可以看到,例如 GPIO 的配置合并策略:

int pika_hal_GPIO_ioctl_merge_config(pika_hal_GPIO_config* dst,
                                     pika_hal_GPIO_config* src) {
    _IOCTL_CONFIG_USE_DEFAULT(dir, PIKA_HAL_GPIO_DIR_IN);
    _IOCTL_CONFIG_USE_DEFAULT(pull, PIKA_HAL_GPIO_PULL_NONE);
    _IOCTL_CONFIG_USE_DEFAULT(speed, PIKA_HAL_GPIO_SPEED_10M);
    _IOCTL_CONFIG_USE_DEFAULT(event_callback_rising, NULL);
    _IOCTL_CONFIG_USE_DEFAULT(event_callback_falling, NULL);
    return 0;
}

_IOCTL_CONFIG_USE_DEFAULT 宏指定了配置项的默认值,例如 speed 配置项的 _PIKA_HAL_XXXX_UNUSED 默认值为 PIKA_HAL_GPIO_SPEED_10M

也有例外的合并策略,例如 PWM 的配置中 duty 配置项的合并:

int pika_hal_PWM_ioctl_merge_config(pika_hal_PWM_config* dst,
                                    pika_hal_PWM_config* src) {
    _IOCTL_CONFIG_USE_DEFAULT(period, PIKA_HAL_PWM_PERIOD_1MS * 10);
    dst->duty = src->duty;
    return 0;
}

其中 duty 配置项就是直接使用新配置结构体的配置值,而没有使用默认值(因为 duty 为 0 时也是有意义的,不能认为是 _PIKA_HAL_XXXX_UNUSED)。

5.2.5.10. 驱动适配

为平台适配 pika_hal,就是为设备重写以下的 pika_hal_platform_XXXX 为前缀的 WEAK 函数,其中XXXX 为设备类型名,如 GPIOPWM 等。

PIKA_WEAK int pika_hal_platform_XXXX_open(pika_dev* dev, char* name);
PIKA_WEAK int pika_hal_platform_XXXX_close(pika_dev* dev);
PIKA_WEAK int pika_hal_platform_XXXX_read(pika_dev* dev, void* buf, size_t count);
PIKA_WEAK int pika_hal_platform_XXXX_write(pika_dev* dev, void* buf, size_t count);
PIKA_WEAK int pika_hal_platform_XXXX_ioctl_enable(pika_dev* dev);
PIKA_WEAK int pika_hal_platform_XXXX_ioctl_disable(pika_dev* dev);
PIKA_WEAK int pika_hal_platform_XXXX_ioctl_config(pika_dev* dev, pika_hal_XXXX_config* cfg);

示例适配代码:

https://gitee.com/Lyon1998/pikapython/tree/master/package/BLIOT

https://gitee.com/Lyon1998/pikapython/tree/master/package/STM32G0

https://gitee.com/Lyon1998/pikapython/tree/master/package/ESP32

5.2.6. 参与贡献

请参考 参与社区贡献->贡献模块 部分的文档发布你编写的模块。