基于 OpenHarmony 5.0 的星闪轻量型设备应用开发——Ch3 设备驱动开发

写在前面:本篇是系列文章《基于 OpenHarmony 5.0 的星闪轻量型设备应用开发》的第 3 章。本篇从 GPIO、PWM、I2C、UART 以及 ADC 等方面对基于 OpenHarmony 5.0 的 WS63 设备驱动开发进行了详细的介绍。本篇的篇幅较长,建议先收藏再阅读。

3.1 OpenHarmony WS63 设备驱动开发介绍

"设备"是相对于 WS63E 模组而言的其他硬件资源。

这些"设备"可以是 LED 灯,也可以是蜂鸣器,亦或是温湿度传感器。如果想通过编写运行在 WS63E 上的 OpenHarmony 框架代码,用于控制这些设备的工作状态或能力,就需要进行设备驱动的开发。

对 WS63E 而言,这些设备与之进行数据交互的通道就是 IO 引脚。基于 OpenHarmony 的 WS63E 设备驱动开发就是通过对相应 IO 引脚进行模式配置,数据赋值,数据读取,中断配置等一系列的操作。

从本章开始,将以石院星闪物联网教学开发板为例,介绍 OpenHarmony 的设备驱动开发。主要包括:GPIO、I2C、PWM、UART、ADC等。

3.2 GPIO 基本功能------以控制 LED 亮灭为例

GPIO(General-purpose input/output)是通用输入输出的缩写,是一种通用的 I/O 接口标准。可以配置为输入或输出模式,以便控制外部设备或与其他设备通信。可用于连接各种设备,如 LED 灯、传感器、执行器等。

使用 WS63 的 GPIO 功能,需要通过与之有关的引脚控制接口(pinctrl 驱动模块)和 GPIO 功能接口(gpio 驱动模块)来实现。

本节以开发板上两个可编程的 LED 为例,介绍使用 OpenHarmony 开发GPIO 的通用 IO 功能的基本流程。

图 3.2-1 LED 灯珠(长脚为正极,短脚为负极)

图 3.2-2 贴片 LED 灯珠(带正负极说明)

LED是发光二极管,是20世纪中期发展起来的新技术。它依靠半导体异质结中的电子通过势垒产生的能量迁越直接发光。通过LED制作的灯具由于发光过程不产生热量,能量转换效率接近百分之百,寿命超长,是照明技术的发展方向。

发光二极管的核心部分是由p型半导体和n型半导体组成的晶片,在p型半导体和n型半导体之间有一个过渡层,称为p-n结。在某些半导体材料的PN结中,注入的少数载流子与多数载流子复合时会把多余的能量以光的形式释放出来,从而把电能直接转换为光能。PN结加反向电压,少数载流子难以注入,故不发光。这种利用注入式电致发光原理制作的二极管叫发光二极管,通称LED。 当它处于正向工作状态时(即两端加上正向电压),电流从LED阳极流向阴极时,半导体晶体就发出从紫外到红外不同颜色的光线,光的强弱与电流有关。

3.2.1 板载 LED 电路原理图

图 3.2-3 开发板载 LED 电路原理图

开发板上有两个可编程 LED,其正极分别与核心芯片的 GPIO-00 以及 GPIO-01 连接。若要 LED 亮,则需要在 GPIO-00 以及 GPIO-01 上分别提供正向电压,即高电平,方可使电路导通;若要 LED 灭,则反之提供低电平,电路无法导通即可。

3.2.2 GPIO 用作通用 IO 的编程步骤

GPIO 编程的一般步骤为:

  • 初始化 pinctrl
  • 设置指定 IO 的复用模式为通用 IO 功能
  • 设置指定 IO 的数据输入输出方向
    • 若指定为输出,则根据功能需求向指定 IO 发送"0"或者"1"分别输出低电平或高电平
    • 若指定为输入,则根据功能需求从指定 IO 读取当前的电平值。若开启 IO 中断功能,则还需要配置中断触发方式以及对应的中断服务程序功能。

3.2.3 功能开发所涉及的接口介绍

(1)初始化 pinctrl
  • 原型:void uapi_pin_init(void);
  • 位置:SDK 目录/include/driver/pinctrl.h
  • 参数:无
  • 返回值:无
  • 调用:该函数应该在其他 pinctrl 模块函数被调用前执行。
(2)设置指定 IO 复用模式接口
  • 原型:errcode_t uapi_pin_set_mode(pin_t pin, pin_mode_t mode);
  • 位置:SDK 目录/include/driver/pinctrl.h
  • 参数:
    • pin 的数据类型是 pin_t,该类型是一个枚举类型,定义了 WS63 可以使用的 pin 的名称编号,具体的定义在文件 "SDK 目录/drivers/chips/ws63/include/platform_core_rom.h" 中,内容见代码 3.2-1:
    • mode 的数据类型是 pin_mode_t ,该类型也是一个枚举类型,定义了 pin 可以被复用的名称编号,具体的定义在文件 "SDK 目录/dirvers/chips/ws63/porting/pinctrl/pinctrl_porting.h" 中,具体内容见代码 3.2-2:
c 复制代码
/* 代码 3.2-1 */

typedef enum {
    GPIO_00 = 0,
    GPIO_01 = 1,
    GPIO_02 = 2,
    GPIO_03 = 3,
    GPIO_04 = 4,
    GPIO_05 = 5,
    GPIO_06 = 6,
    GPIO_07 = 7,
    GPIO_08 = 8,
    GPIO_09 = 9,
    GPIO_10 = 10,
    GPIO_11 = 11,
    GPIO_12 = 12,
    GPIO_13 = 13,
    GPIO_14 = 14,
    GPIO_15 = 15,
    GPIO_16 = 16,
    GPIO_17 = 17,
    GPIO_18 = 18,
    SFC_CLK = 19,
    SFC_CSN = 20,
    SFC_IO0 = 21,
    SFC_IO1 = 22,
    SFC_IO2 = 23,
    SFC_IO3 = 24,
    PIN_NONE = 25, // used as invalid/unused PIN number
} pin_t;
c 复制代码
/* 代码 3.2-2 */

typedef enum {
    PIN_MODE_0        = 0,
    PIN_MODE_1        = 1,
    PIN_MODE_2        = 2,
    PIN_MODE_3        = 3,
    PIN_MODE_4        = 4,
    PIN_MODE_5        = 5,
    PIN_MODE_6        = 6,
    PIN_MODE_7        = 7,
    PIN_MODE_MAX      = 8
} pin_mode_t;

若要写出正确的 pin 复用代码,还需要知道 WS63 的 IO 复用关系,具体如下表所示。

表 3.2-1 IO 复用关系表

IO/MODE 0 1 2 3 4 5 6 7
GPIO_00 GPIO_00 PWM0 DIAG[0] SPI1_CSN JTAG_TDI
GPIO_01 GPIO_01 PWM1 DIAG[1] SPI1_IO0 JTAG_MODE BT_SAMPLE
GPIO_02 GPIO_02 PWM2 DIAG[2] SPI1_IO3 WIFI_TSF_SYNC WL_GLP_SYNC_PULSE BGLE_GLP_SYNC_PULSE
GPIO_03 GPIO_03 PWM3 PMU_32K_TEST SPI1_IO1 HW_ID[0] DIAG[3]
GPIO_04 SSI_CLK PWM4 GPIO_04 SPI1_IO1 JTAG_ENABLE DFT_JTAG_TMS
GPIO_05 SSI_DATA PWM5 UART2_CTS SPI1_IO2 GPIO_05 SPI0_IN DFT_JTAG_TCK
GPIO_06 GPIO_06 PWM6 UART2_RTS SPI1_SCK REFCLK_FREQ_STATUS DIAG[4] SPIO0_OUT DFT_JTAG_TDI
GPIO_07 GPIO_07 PWM7 UART2_RXD SPI0_SCK I2S_MCLK DIAG[5]
GPIO_08 GPIO_08 PWM0 UART2_TXD SPI0_CS1_N DIAG[6]
GPIO_09 GPIO_09 PWM1 RADAR_ANT0_SW SPI0_OUT I2S_DO HW_ID[1] DIAG[7] JTAG_TD0
GPIO_10 GPIO_10 PWM2 ANT0_SW SPI0_CS0_N I2S_SCLK DIAG[0]
GPIO_11 GPIO_11 PWM3 RADAR_ANT1_SW SPI0_IN I2S_LRCLK DIAG[1] HW_ID[2]
GPIO_12 GPIO_12 PWM4 ANT1_SW I2S_DI HW_ID[3]
GPIO_13 GPIO_13 UART1_CTS RADAR_ANT0_SW DFT_JTAG_TD0 JTAG_TMS
GPIO_14 GPIO_14 UART1_RTS RADAR_ANT1_SW DFT_JTAG_TRSTN JTAG_TCK
UART1_TXD GPIO_15 UART1_TXD I2C1_SDA
UART1_RXD GPIO_16 UART1_RXD I2C1_SCL
UART0_TXD GPIO_17 UART0_TXD I2C0_SDA
UART0_RXD GPIO_18 UART0_RXD I2C0_SCL
  • 返回值:errcode_t 类型的值,定义在文件 "SDK 目录/include/errcode.h" 中
    • 如果成功,返回值为 ERRCODE_SUCC(即 0)
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:如图 3.2-3 的 LED 电路原理图所示,需要将 GPIO_00 和 GPIO_01 复用为普通 IO模式,也就是模式 0,对应的代码如下所示:
c 复制代码
/* 代码 3.2-3 */

#define LED_L 0       // 根据 GPIO_00 在板载上的功能重命名为 LED_L
#define LED_R 1       // 根据 GPIO_01 在板载上的功能重命名为 LED_R
#define GPIO_MODE 0   // 将模式 0 重名为 GPIO_MODE
...
    uapi_pin_init();  // 初始化 pinctrl
    // 在功能代码中调用
    uapi_pin_set_mode(LED_L, GPIO_MODE);
    uapi_pin_set_mode(LED_R, GPIO_MODE);
(3)设置指定 IO 的数据输入输出方向
  • 原型:errcode_t uapi_gpio_set_dir(pin_t pin, gpio_direction_t dir);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用来指定 GPIO 的引脚编号。
    • 参数 dir 的数据类型是 gpio_direction_t ,该类型是枚举类型,定义了 GPIO 的输入输入方向,具体的定义在文件 "SDK 目录/drivers/drivers/hal/gpio/hal_gpio.h "中 ,内容如下:
c 复制代码
/* 代码 3.2-4 */

enum gpio_direction {
    GPIO_DIRECTION_INPUT,
    GPIO_DIRECTION_OUTPUT
} gpio_direction_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC(即 0)
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:如图 3.2-3 的 LED 电路原理图所示,需要将 GPIO_00 和 GPIO_01 的方向设置为输出,对应的代码如下:
c 复制代码
/* 代码 3.2-5 */

uapi_gpio_set_dir(LED_L, GPIO_DIRECTION_OUTPUT);
uapi_gpio_set_dir(LED_R, GPIO_DIRECTION_OUTPUT);
(4)向指定 IO 发送数据
  • 原型:errcode_t uapi_gpio_set_val(pin_t pin, gpio_level_t level);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的名称编号。
    • 参数 level 的数据类型是 gpio_level_t ,该类型是枚举类型,定义了 GPIO 的高低电平,具体的定义在文件 "SDK 目录/drivers/drivers/hal/gpio/hal_gpio.h" 中 ,内容如下:
c 复制代码
/* 代码 3.2-6 */

typedef enum gpio_level {
    GPIO_LEVEL_LOW,
    GPIO_LEVEL_HIGH
} gpio_level_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC(即 0)
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:如图 3.2-3 的 LED 电路原理图所示, 若要 LED_L 亮,需要向 GPIO_00 发送高电平,若要 LED_L 灭,需要向 GPIO_00 发送低电平(LED_R 同理)。对应的代码如下:
c 复制代码
/* 代码 3.2-7 */

uapi_gpio_set_val(LED_L, GPIO_LEVEL_HIGH);  // LED_L 亮
uapi_gpio_set_val(LED_L, GPIO_LEVEL_LOW);   // LED_L 灭
(5)向指定 IO 发送翻转数据
  • 原型:errcode_t uapi_gpio_toggle(pin_t pin);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的引脚名称编号
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC(即 0)
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:如图 3.2-3 的 LED 电路原理图所示, 若要 LED_L 的亮灭状态发生翻转,则对应的代码如下:
c 复制代码
/* 代码 3.2-8 */

uapi_gpio_toggle(LED_L);  // 代码执行前若 LED_L 亮,则代码执行后 LED_L 灭

3.2.4 功能开发实现

基于 OpenHarmony 的设备驱动开发需要创建工程、编写功能代码、编写和修改配置文件、编译和烧写等步骤,以下将逐一进行介绍。

(1)创建工程

OpenHarmony 的工程目录一般存放在 application/sample/wifi-iot/app 目录下,以下将该目录统称为"工程目录"。

在工程目录下新建 "01_LED_DEMO" 文件夹,并在该文件夹中新建"led_demo.c"文件和"BUILD.gn"文件。

(2)编写功能代码

led_demo.c 的代码如下:

c 复制代码
/* 代码 3.2-9 */

#include "soc_osal.h"
#include "app_init.h"
#include "tcxo.h"
#include "pinctrl.h"
#include "gpio.h"

#define LED_L 0
#define LED_R 1
#define GPIO_MODE 0

#define TEST_DELAY_1000MS 1000

#define LED_TASK_PRIO 24
#define LED_TASK_STACK_SIZE 1024

static void *led_task(const char *arg)
{
    UNUSED(arg);
    
    // 初始化pinctrl
    uapi_pin_init();

    // 设置引脚功能为MODE_0(GPIO)
    uapi_pin_set_mode(LED_L, GPIO_MODE);
    uapi_pin_set_mode(LED_R, GPIO_MODE);

    // 设置GPIO为输出方向
    uapi_gpio_set_dir(LED_L, GPIO_DIRECTION_OUTPUT);
    uapi_gpio_set_dir(LED_R, GPIO_DIRECTION_OUTPUT);

    // 配置GPIO引脚号和输出值
    uapi_gpio_set_val(LED_L, GPIO_LEVEL_LOW);
    uapi_gpio_set_val(LED_R, GPIO_LEVEL_LOW);

    while(1) {
        // LED_L 亮,LED_R 灭
        uapi_gpio_set_val(LED_L, GPIO_LEVEL_HIGH);
        uapi_gpio_set_val(LED_R, GPIO_LEVEL_LOW);
        // 延迟 1s
        uapi_tcxo_delay_ms(TEST_DELAY_1000MS);
        // LED_L 灭,LED_R 亮
        uapi_gpio_set_val(LED_L, GPIO_LEVEL_LOW);
        uapi_gpio_set_val(LED_R, GPIO_LEVEL_HIGH);
        // 延迟 1s
        uapi_tcxo_delay_ms(TEST_DELAY_1000MS);
    }
}

static void led_entry(void)
{
    osal_task *task_handle = NULL;
    osal_kthread_lock();
    task_handle = osal_kthread_create((osal_kthread_handler)led_task, 0, "LedTask", LED_TASK_STACK_SIZE);
    if (task_handle != NULL) {
        osal_kthread_set_priority(task_handle, LED_TASK_PRIO);
        osal_kfree(task_handle);
    }
    osal_kthread_unlock();
}

app_run(led_entry);
(3)编写配置文件

该项目目录下的 BUILD.gn 的代码如下:

cmake 复制代码
# 代码 3.2-10

static_library("ledDemo") {
  sources = [
    "led_demo.c"
  ]

  include_dirs = [
    "//commonlibrary/utils_lite/include",
    "//kernel/liteos_m/kal/cmsis",
    "//device/soc/hisilicon/ws63v100/sdk/include/driver",
    "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init",
    "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio/v150",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/rom/drivers/chips/ws63/porting/pinctrl",
  ]
}
(4)修改工程配置文件

1) 修改"工程目录"下的 BUILD.gn 文件,在 features 数组中添加 "01_LED_DEMO",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "ledDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "ledDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写
  • 使用串口线将 **石院星闪物联网教学开发板 **的 USB 端口与 PC 机的 USB 端口连接(烧写前请先确认是否安装串口驱动,若没有,请参考第一章的内容完成驱动的安装)
  • 参考"第 1 章 1.4 镜像文件的烧写"一节的内容完成烧写。
(7)运行效果
  • 烧写结束后,会看到 开发板上的 LED_L 和 LED_R 在交替闪烁。

3.3 GPIO 中断功能------以按键(轻触开关)控制 LED 为例

本节介绍的案例功能是通过按动开发板上的"KEY"按键(轻触开关),控制两个 LED 的亮灭状态切换。

图 3.3-1 轻触开关实物图

如图 3.3-1 所示,在许多电路板中,都会使用这种正方形的轻触开关来实现电路的导通或断开功能。这类轻触开关一般都有四个焊接引脚,图 3.2-5 是轻触开关的外观尺寸以及内部结构示意图。

图 3.3-2 轻触开关内部结构示意图

3.3.1 开发板上 KEY 电路原理图

图 3.3-3 开发板上 KEY 的电路原理图

  • 通过原理图可以看出,在通常情况下,按键中的弹簧片悬高,1、2 引脚所在电路与 3、4 引脚所在电路属于断开状态。当开发板通电时,此时的 GPIO-02 处应当是高电平,若按下按键后,电路导通,则 GPIO-02 处是低电平。
  • 以上的描述也可以理解为默认情况下,GPIO-02 处始终是保持高电平状态,一旦按键被按下,则GPIO-02 的状态发生了变化,即从高电平变化为低电平。

【说明】这种状态的变化,可以作为一个触发信号,向 CPU 提出需要执行某个特定功能的请求,一旦 CPU

捕获到这种请求,则会根据实际情况决定是否要响应该请求。

如果 CPU

决定响应该请求,则会做一些"现场保护"的工作后(这些都是由编译器自动完成的固定动作,无需开发人员进行代码编写),转向"被请求的功能"处执行代码,当功能执行完毕后,又会"返回到"之前的"断点"处继续执行原来的代码。这,就是系统的外部中断机制。

  • 通常,将电平信号从高电平变化为低电平的这种过程称之为"下降沿",反之为"上升沿"。

3.3.2 GPIO 的外部中断功能编程步骤

GPIO 的外部中断功能编程步骤是在其作为"通用 IO"编程步骤的基础上增加了:

  • 中断服务程序功能代码编写
  • 注册 IO 引脚的中断服务

3.3.3 功能开发所涉及的接口介绍

(1)设置指定 IO 的中断触发方式
  • 原型:errcode_t uapi_gpio_set_isr_mode(pin_t pin, uint32_t trigger);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的名称编号。
    • 参数 trigger 是 uint32_t 类型的数据,其值表示 GPIO 的中断触发方式:
      • 1:上升沿触发
      • 2:下降沿触发
      • 3:双边沿触发
      • 4:低电平触发
      • 8:高电平触发
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC(即 0)
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:如图 3.3-3 的 KEY 用户按键电路原理图所示,GPIO-02 需要设置下降沿触发中断。对应的代码如下:
c 复制代码
/* 代码 3.3-1 */

/* 以上代码省略 */
#define KEY 2 	// 将 GPIO-02 重命名为 KEY
static void *button_task(const char *arg){
    (void)arg;
    /* 这里省略部分代码 */
    uapi_gpio_set_isr_mode(KEY, 2); // KEY 采用下降沿触发中断请求
    /* 这里省略部分代码 */
}
(2) 注册指定 IO 的中断服务
  • 原型:errcode_t uapi_gpio_register_isr_func(pin_t pin, uint32_t trigger, gpio_callback_t callback);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的引脚名称编号。
    • 参数 trigger 的值表示 GPIO 的中断触发方式(取值与uapi_gpio_set_isr_mode接口中规定的相同)。
    • 参数 callback 是 gpio_callback_t 类型,该类型是一个函数指针类型,具体定义在文件"SDK 目录/drivers/drivers/hal/gpio/hal_gpio.h"中,内容如下:
c 复制代码
/* 代码 3.3-2 */

/**
 * @brief  GPIO回调的类型定义,它返回发起中断的PIN的编号。
 * @param  [in]  pin 中断的PIN的编号.
 * @param  [in]  param 回调函数.
 */
typedef void (*gpio_callback_t)(pin_t pin, uintptr_t param);
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:注册 IO 引脚中断服务功能的函数在调用前,需要先定义好中断服务功能函数。以下代码是调用举例。
c 复制代码
/* 代码 3.3-3 */

/* 以上代码省略 */
#define KEY 2 	// 将 GPIO-02 重命名为 KEY

/* 定义 GPIO 的中断服务功能函数 */
static void gpio_callback_func(pin_t pin, uintptr_t param)
{
    (void)pin;
    (void)param;
    printf("Button pressed.\r\n");
}

static void *button_task(const char *arg){
    (void)arg;
    /* 这里省略部分代码 */
    errcode_t ret = uapi_gpio_register_isr_func(KEY, 2, gpio_callback_func);
    if (ret != 0) {
        uapi_gpio_unregister_isr_func(KEY); // 注册失败时需要取消注册
    }
    /* 这里省略部分代码 */
}
(3) 取消指定 IO 所注册的中断服务
  • 原型:errcode_t uapi_gpio_unregister_isr_func(pin_t pin);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的引脚名称编号。
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:参考代码 3.2.2-3 中的代码。
(4) 使能指定 IO 的中断请求
  • 原型:errcode_t uapi_gpio_enable_interrupt(pin_t pin);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的引脚名称编号。
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:默认情况下 GPIO 的中断请求都是开放的,但是在某些情况下由于开发需要,可能会将某些 IO 引脚的中断服务请求屏蔽,等需要的时候再开放,该接口就是用于开启 IO 引脚的中断请求,这样在 IO 引脚上产生的中断请求信号会被 CPU 捕捉,进而对其进行响应(即调用注册的中断服务函数)。
(5) 屏蔽指定 IO 的中断请求
  • 原型:errcode_t uapi_gpio_disable_interrupt(pin_t pin);
  • 位置:SDK 目录/include/driver/gpio.h
  • 参数:
    • 参数 pin 用于指定 GPIO 的引脚名称编号。
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:在需要屏蔽 IO 中断请求的代码处调用该接口即可。

3.3.4 功能开发实现

(1)创建工程

在工程目录下新建 "02_KEY_DEMO" 文件夹,并在该文件夹中新建"key_demo.c"文件和"BUILD.gn"文件。

(2)编写功能代码

key_demo.c 的代码如下:

c 复制代码
/* 代码 3.3-4 */

#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "tcxo.h"
#include "pinctrl.h"
#include "gpio.h"

#define LED_L 0
#define LED_R 1
#define KEY 2
#define GPIO_MODE 0

/* 定义 GPIO 的中断服务功能函数 */
static void gpio_callback_func(pin_t pin, uintptr_t param)
{
    (void)pin;
    (void)param;
    uapi_gpio_toggle(LED_L);
    uapi_gpio_toggle(LED_R);
    printf("Button pressed.\r\n");
}

static void *KeyCntrolDemo(const char *arg){
    (void)arg;

    uapi_tcxo_delay_ms(1000);//等待uart2(gpio7,gpio8)初始化完成后,再设置GPIO7为gpio功能
    printf("KEYTask start\r\n");

    // 初始化pinctrl
    uapi_pin_init();

    // 设置引脚功能为MODE_0(GPIO)
    //V100 版本SDK中,有其它地方会修改GPIO_7的MODE。需要检查或者延迟。
    uapi_pin_set_mode(LED_L, GPIO_MODE);
    uapi_pin_set_mode(LED_R, GPIO_MODE);
    uapi_pin_set_mode(KEY, GPIO_MODE);

    // 设置GPIO为输出方向
    uapi_gpio_set_dir(LED_L, GPIO_DIRECTION_OUTPUT);
    uapi_gpio_set_dir(LED_R, GPIO_DIRECTION_OUTPUT);
    uapi_gpio_set_dir(KEY, GPIO_DIRECTION_INPUT);

    // 配置GPIO引脚号和输出值
    uapi_gpio_set_val(LED_L, GPIO_LEVEL_LOW);
    uapi_gpio_set_val(LED_R, GPIO_LEVEL_LOW);
    
    errcode_t ret = uapi_gpio_register_isr_func(KEY, 2, gpio_callback_func);
    if (ret != 0) {
        uapi_gpio_unregister_isr_func(KEY); // 注册失败时需要取消注册
    }
    while(1);
}

static void KeyControlTask(void)
{
    osThreadAttr_t attr;

    attr.name = "KeyCntrolDemo";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 1024; /* 堆栈大小为1024 */
    attr.priority = osPriorityNormal;
    // 报错
    if (osThreadNew((osThreadFunc_t)KeyCntrolDemo, NULL, &attr) == NULL) {
        printf("[KeyControlTask] Failed to create KeyTask!\n");
    }
}

SYS_RUN(KeyControlTask);
(3)编写配置文件

该项目目录下的 BUILD.gn 的代码如下:

cmake 复制代码
# 代码 3.3-5

static_library("keyDemo") {
  sources = [
    "key_demo.c"
  ]

  include_dirs = [
    "//commonlibrary/utils_lite/include",
    "//kernel/liteos_m/kal/cmsis",
    "//device/soc/hisilicon/ws63v100/sdk/include/driver",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio/v150",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/rom/drivers/chips/ws63/porting/pinctrl",
  ]
}
(4)修改工程配置文件

1) 修改"工程目录"下的 BUILD.gn 文件,在 features 数组中添加 "02_KEY_DEMO:keyDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "keyDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "keyDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写
  • 使用串口线将 石院星闪物联网教学开发板 的 USB 端口与 PC 机的 USB 端口连接(烧写前请先确认是否安装串口驱动,若没有,请参考第一章的内容完成驱动的安装)
  • 参考"第 1 章 1.4 镜像文件的烧写"一节的内容完成烧写。
(7)运行效果
  • 烧写结束后,按下开发板上的 KEY 后,LED_L 和 LED_R 的状态会发生变化,再次按下,LED_L 和 LED_R 的状态再次发生变化。

3.4 PWM 功能开发------以控制蜂鸣器为例

3.4.1 什么是 PWM

脉宽调制(Pulse-Width

Modulation,PWM)是利用微处理器的数字输出,来对模拟电路进行控制的一种非常有效的技术,通过对一系列脉冲的宽度进行调制,来等效的获得所需要的波形(含形状和幅值),即通过改变导通时间占总时间的比例,也就是占空比,达到调整电压和频率的目的。广泛应用在从测量、通信到功率控制与变换的许多领域中,用于调压调频,最突出的是针对各种类型的电机应用。

WS63 的 GPIO 可以复用为 PWM 功能,支持 8 路 PWM 输出,支持固定周期数发送模式,支持发送完成中断功能等。

通过设置 PWM 占空比控制 GPIO 的输出电压值,进而实现控制 LED 的亮度、蜂鸣器的发声频率以及TT 减速直流电机的转速等功能。

3.4.2 板载蜂鸣器电路原理图

图 3.4-1 蜂鸣器电路原理图

从图中可以看出在 BUZZER 端送上不同的电压值,则蜂鸣器会发出不同频率的声音。图中 BUZZER 与 GPIO-03 连接。

3.4.3 PWM 编程步骤

  • 初始化 pinctrl。
  • 设置指定 IO 的复用模式为 PWM 功能。
  • 对 PWM 初始化。
  • 配置 PWM 参数,打开指定通道。
  • 为 PWM 通道分配 PWM 组。
  • 开启指定 PWM 组的波形输出。
  • 关闭指定 PWM 组的波形输出。
  • 去初始化 PWM。

3.4.4 功能开发所涉及到的 API 介绍

(1)初始化 PWM
  • 原型:errcode_t uapi_pwm_init(void);
  • 位置:SDK 目录/include/driver/pwm.h
  • 参数:无
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:该接口函数应当在 pwm 功能设置之前调用,代码略。
(2)去初始化 PWM
  • 原型:void uapi_pwm_deinit(void);
  • 位置:SDK 目录/include/driver/pwm.h
  • 参数:无
  • 返回值:无
  • 调用:在开发某个 IO 的 PWM 功能时,最好先调用该接口,再对 IO 设置复用为 PWM 后,然后进行 PWM 初始化。
(3)打开具有指定配置的 PWM 通道
  • 原型:errcode_t uapi_pwm_open(uint8_t channel, const pwm_config_t *cfg);
  • 位置:SDK 目录/include/driver/pwm.h
  • 参数:
    • 参数 channel 表示 PWM 通道号,针对 WS63 的 GPIO 的各个 IO 引脚复用为 PWM 功能时对应的通道号,在表 3.2-1 IO 复用关系表中已经列出,查表即可。
    • 参数 *cfg 是一个 pwm_config_t 类型的常量,这个类型的定义与该接口声明在同一个文件中,内容如下:
c 复制代码
/* 代码 3.4-1 */

typedef struct pwm_config {
    uint32_t low_time;      /* PWM工作时钟周期计数个数低电平部分,
                            频率参考 @ref uapi_pwm_get_frequency()。*/
    uint32_t high_time;     /* PWM工作时钟周期计数个数高电平部分,
                            频率参考 @ref uapi_pwm_get_frequency()。*/
    uint32_t offset_time;   /* PWM相位。 */
    uint16_t cycles;        /* PWM重复周期,范围:0~32767 (15bit)。  */
    bool repeat;            /* 指示PWM应连续输出的标志。*/
} pwm_config_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:参考代码 3.4-2
(4)为指定的 PWM 通道分配分组
  • 原型:errcode_t uapi_pwm_set_group(uint8_t group, uint16_t channel_id);
  • 位置:SDK 目录/include/driver/pwm.h
  • 参数:
    • 参数 group 表示 PWM 分组号,WS63 可以设置的最大分组号为 8。
    • 参数 channel_id 的值为分配到同一个组里的所有通道的 id,即 channel_id 中的每一位 bit 对应相对应的一个通道。
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:当为指定 PWM 通道设置好 PWM 参数并开启通道后,需要先将该 PWM 通道加入 PWM 分组,然后才能开始 PWM 信号的输出。具体调用详见代码 3.4-2。
(5)开启指定 PWM 组的波形输出
  • 原型:errcode_t uapi_pwm_start(uint8_t group);
  • 位置:SDK 目录/include/driver/pwm.h
  • 参数:
    • 参数 group 表示 PWM 分组号.
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:具体调用详见代码 3.4-2。
(6)关闭指定 PWM 组的波形输出
  • 原型:errcode_t uapi_pwm_close(uint8_t group);
  • 位置:SDK 目录/include/driver/pwm.h
  • 参数:
    • 参数 group 表示 PWM 分组号.
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:具体调用详见代码 3.4-2。
c 复制代码
/* 代码 3.4-2 */

/* 以上代码省略 */
#include "pwm.h"

#define BUZZER_PIN		3  // 蜂鸣器占用 GPIO-03
#define PWM_PIN_MODE	1  // GPIO 复用为 PWM 时的 mode 值为 1
#define BUZZER_CHANNEL	3  // GPIO-03 复用为 PWM 时通道号为 3
#define PWM_GROUP_ID	0  // 指定 PWM 的分组号

static void PWMTestDemo(const char *arg)
{
  (void)arg;
  printf("PWMTask start.\r\n");

  uapi_pwm_deinit();
  uapi_pin_set_mode(BUZZER_PIN, PWM_PIN_MODE);
  uapi_pwm_init();

  pwm_config_t pwm_cfg = {10000, // 低电平持续tick 时间 = tick * (1/32000000)
                          90000, // 高电平持续tick 时间 = tick * (1/32000000)
                          0,     // 相位偏移位
                          0,     // 后面参数指定循环后,这里设置的值无效
                          true}; // 是否循环
  uapi_pwm_open(BUZZER_CHANNEL, &pwm_cfg); // 为 PWM3 通道指定 PWM 配置
  uapi_pwm_set_group(PWM_GROUP_ID, (1 << BUZZER_CHANNEL)); // 将 PWM3 添加到分组中

  while(1){
    uapi_pwm_start(PWM_GROUP_ID); // 开启 PWM 组的波形输出。
    printf("pwm start.");
    uapi_tcxo_delay_ms(1000);
      
    uapi_pwm_close(PWM_GROUP_ID); // 关闭 PWM 组的波形输出。
    printf("pwm stop.\r\n");
    uapi_tcxo_delay_ms(1000);
  }
}

3.4.5 功能开发实现

(1)创建工程

在工程目录下新建 "03_BUZZER_DEMO" 文件夹,并在该文件夹中新建"buzzer_demo.c"文件和"BUILD.gn"文件。

(2)编写功能代码

buzzer_demo.c 的代码如下:

c 复制代码
/* 代码 3.4-3 */

#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "tcxo.h"
#include "pinctrl.h"
#include "gpio.h"
#include "pwm.h"

#define BUZZER_PIN		3  // 蜂鸣器占用 GPIO-03
#define PWM_PIN_MODE	1  // GPIO 复用为 PWM 时的 mode 值为 1
#define BUZZER_CHANNEL	3  // GPIO-03 复用为 PWM 时通道号为 3
#define PWM_GROUP_ID	0  // 指定 PWM 的分组号

static void *PWMTestDemo(const char *arg)
{
  (void)arg;
  printf("PWMTask start.\r\n");

  uapi_pwm_deinit();
  uapi_pin_set_mode(BUZZER_PIN, PWM_PIN_MODE);
  uapi_pwm_init();

  pwm_config_t pwm_cfg = {1000, // 低电平持续tick 时间 = tick * (1/32000000)
                          1000, // 高电平持续tick 时间 = tick * (1/32000000)
                          0,     // 相位偏移位
                          0,     // 后面参数指定循环后,这里设置的值无效
                          true}; // 是否循环
  uapi_pwm_open(BUZZER_CHANNEL, &pwm_cfg); // 为 PWM3 通道指定 PWM 配置
  uapi_pwm_set_group(PWM_GROUP_ID, (1 << BUZZER_CHANNEL)); // 将 PWM3 添加到分组中

  while(1){
    uapi_pwm_start(PWM_GROUP_ID); // 开启 PWM 组的波形输出。
    printf("pwm start.");
    uapi_tcxo_delay_ms(1000);
      
    uapi_pwm_close(PWM_GROUP_ID); // 关闭 PWM 组的波形输出。
    printf("pwm stop.\r\n");
    uapi_tcxo_delay_ms(1000);
  }
}

static void PWMTestTask(void)
{
    osThreadAttr_t attr;

    attr.name = "PWMTestDemo";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 1024 * 10; /* 堆栈大小为1024 */
    attr.priority = osPriorityNormal;
    // 报错
    if (osThreadNew((osThreadFunc_t)PWMTestDemo, NULL, &attr) == NULL) {
        printf("[PWMTestTask] Failed to create PWMTestDemo!\n");
    }
}

SYS_RUN(PWMTestTask);
(3)编写配置文件

该项目目录下的 BUILD.gn 的代码如下:

cmake 复制代码
# 代码 3.4-4

static_library("buzzerDemo") {
  sources = [
    "buzzer_demo.c"
  ]

  include_dirs = [
    "//commonlibrary/utils_lite/include",
    "//kernel/liteos_m/kal/cmsis",
    "//device/soc/hisilicon/ws63v100/sdk/include/driver",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio/v150",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/rom/drivers/chips/ws63/porting/pinctrl",
  ]
}
(4)修改工程配置文件

1) 修改"工程目录"下的 BUILD.gn 文件,在 features 数组中添加 "03_BUZZER_DEMO:buzzerDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "buzzerDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "buzzerDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写
  • 使用串口线将 **石院星闪物联网教学开发板 **的 USB 端口与 PC 机的 USB 端口连接(烧写前请先确认是否安装串口驱动,若没有,请参考第一章的内容完成驱动的安装)
  • 参考"第 1 章 1.4 镜像文件的烧写"一节的内容完成烧写。
(7)运行效果
  • 烧写结束后,可以听到开发板上蜂鸣器发出间歇性的鸣叫。

通过修改代码 3.4-3 中 pwm_cfg 的高电平和低电平的数据,重新编译并烧写后,蜂鸣器会发出不同频率的鸣叫。

3.5 I2C 功能开发------以温湿度传感器驱动开发为例

3.5.1 I2C 简介

I2C是由Philips公司在上世纪80年代设计的一种板载器件之间进行简易通信的总线协议。这种总线是需要两根线和很简单的电路设计就能是连接在总线上的设备之间进行通信。它与UART一样都属于串行通信,但与UART不同的是,I2C是"同步串行通信",而UART是"异步串行通信"。

I2C可以看成是"Inter IC",也有人称它们是"IIC"或者"I2C"。

(1)I2C 总线结构

图 3.5-1 I2C 总线结构示意图

图 3.5-1 是典型的 I2C 总线结构示意图,图中展示了两种不同的总线设备:主设备(Master)和从设备(Slave)。所有设备都以并联的方式挂接在串行数据线SDA(Serial Data)和串行同步时钟信号线SCL(Serial Clock Line)上。

SDA和SCL都是双向I/O线,接口电路为开漏输出,需要通过上拉电阻接正向电源Vdd。当总线空闲时,SDA和SCL上都是高电平,连接在总线上的任一器件输出低电平都会使总线的信号变为低电平,即各个器件的SDA及SCL都是线"与"关系。

(2)I2C 总线中的主设备和从设备

挂接在I2C总线上的每一个设备都有一个唯一的"地址",这个地址可以由软件通过编程的方式来进行设置。这样主设备只需要通过地址码就可以建立"多机"通信的机制,因此I2C总线省去了外围器件的"片选线",这样无论总线上挂接多少个器件,其系统仍然可以保持简单的"两线结构"。

主设备一般是带有CPU的逻辑部件,它掌握总线的控制权,负责初始化并发送同步时钟信号,向总线发送启动位,决定数据的传输方向以及数据传输终止标志。

通俗一些来说:在通信时必需的时钟信号是由主设备初始化并送到SCL上的,而在SDA上的数据传送方向也由主设备决定,当开始传送数据时,需要由主设备往SDA总线上"送"一个启动信号,之后"送"数据传送方向以及从设备的地址。当从设备在SDA总线上获取了自己的地址码后,就按照主设备的"要求"向主设备发送数据或者从主设备接收数据。当主设备决定终止数据传输时,向SDA总线"送"终止信号,这时从设备会结束与主设备之间的数据传送过程。

I2C总线中允许同时有"多个"主设备,但某一时刻只能由"一个"主设备来获得总线的控制权。具体由"哪个"主设备来控制总线,是通过带有竞争检测和仲裁的电路来决定。

I2C总线上的主设备和从设备之间以字节为单位进行双向的数据传输。

(3)I2C 总线的传输速率

I2C 总线工作在普通模式时的数据传输速率可达 100KHz;在快速模式下为 400KHz;在高速模式下为 3.4MHz。为了保证数据传输的效果,I2C总线一般都是工作在普通模式下。

3.5.2 I2C总线协议介绍

所谓协议,就是在通信双方之间约定的传输规则,这里包括如何定义起始标志、数据传输方向标志、应答标志、传输结束标志以及这些标志在数据线上保持的时间等信息。为了能够使用 I2C 总线进行数据传输,必须要掌握它的总线协议。

I2C 总线协议规定:

  • 总线上数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。
  • 起始和结束信号总是由主设备产生。
  • 在起始条件产生后,总线处于"占用"状态,由本次数据传输的主设备和从设备独占,其他挂接在I2C总线上的器件无法访问总线。
  • 在停止条件产生后,本次数据传输的主设备和从设备将总线释放,使总线处于"空闲"状态。
(1)起始条件和停止条件

图 3.5-2 I2C 起始条件和停止条件的 SDA 和 SCL 的时序图

  • SCL 上是"高电平"并且SDA由"高电平"变为"低电平"时,这种情况表示起始条件。也相当于在 SDA 和 SCL 上出现了"起始信号",这时总线就处于被占用的状态。
  • SCL 上是"高电平"并且SDA由"低电平"变为"高电平"时,这种情况表示停止条件。也相当于在 SDA 和 SCL 上出现了"停止信号",之后总线就处于空闲状态。
(2)I2C 的数据传送位序

图 3.5-3 I2C 总线数据传送位序时序图

I2C 总线支持数据的双向传递,所以主设备和从设备都可以作为"发送器"和"接收器"。但无论数据传递的方向是怎样的,总线上的"起始信号"和"停止信号"都必须由主设备来产生。

图 3.5-3 所示的是在 I2C 总线有数据传输时的数据位时序,图中 MSB 表示的是一个字节中的最高位,LSB表示一个字节中的最低位。也就是说在 I2C 总线上开始传输字节数据时是按照由高到底的次序依次将字节中的数据为进行传送。每传送完一个字节的数据,就要在总线上等待应答信号 ACK (低电平为有效应答)。

(3)I2C 的数据传输过程

图 3.5-4 I2C 总线数据传输时序图

主设备在总线上产生"起始信号",相当于通知总线上的所有设备传输开始了,接下来主设备在总线上发送设备地址,与这一地址匹配的从设备将返回主设备一个应答位,并准备开始接下来的数据传输过程;与这一地址不匹配的从设备将会忽略接下来的传输并等待下一次在总线上出现的"起始信号"。

挂接在 I2C 总线中的每一个设备都对应一个唯一的地址,当主设备需要与从设备之间进行数据传输前,需要通过总线传递从设备的地址。由于从设备的地址都是7位的,因此主设备在总线上传送完从设备的地址后,还要添加一个数据传输方向位:

  • 0表示数据传输方向为"主设备→从设备",相当于"写数据";
  • 1表示数据传输方向为"从设备→主设备",相当于"读数据"。

从设备一旦在总线上截获的地址与自己的地址匹配后,需要在接下来的一个时钟脉冲里向主设备发送应答位。当主机接受到从机的 ACK(低电平为有效应答)后:

图 3.5-5 I2C 总线主机"写操作"过程数据传送示意图

  • 如图 3.5-5 所示,如果当前是"主机写",那么主机接管总线,开始向从机发送有效数据,并且每发送 1Btye就释放总线,等待从机应答,只有接收到有效应答 ACK 后,才会继续发送下一个 Byte 的数据,并再次等待从机应答,直到所有字节数据发送完毕后,主机在接收到从机的 ACK 后,直接在总线上发送停止信号,结束本次 "写操作"。

图 3.5-6 I2C 总线主机"写操作"过程数据传送示意图

  • 如图 3.5-6 所示,如果当前是"主机读",那么主机释放总线,并从 SDA 上读取数据(这些数据是由从机发送到 SDA 上的),主机每接收 1Byte 的有效数据,就会接管总线给从机发送有效应答 ACK,直到接收完所有主机"认为足够"的数据后,主机发送 NACK(非应答信号)后直接在总线上产生"停止信号",结束当前传输。
(4)有效数据

图 3.5-7 I2C 总线协议中的有效数据

I2C 总线协议规定,只有在 SCL 为高电平时,SDA 上的数据才是有效数据。

在传递有效数据的过程中,"发送器"以每个时钟脉冲传递一个二进制位的速度,将所传输的字节按照数据位从高到低的顺序依次送到总线上。在每传送完一个字节有效数据后,"发送器"就会释放总线控制权,这时"接收器"在接下来的一个时钟脉冲里将 SDA 拉低来对"发送器"发送"应答位",表明该字节数据接收有效。在应答位产生后,"接收器"释放总线,由"发送器"再次接管总线,开始下一个字节数据的发送过程。

3.5.3 WS63 I2C 设备驱动开发步骤

WS63 提供了 I2C0~I2C1 共 2 组支持 Master 模式的 I2C 外设,其规格如下:

  • 支持标准模式(100Kbit/s)和快速模式(400Kbit/s)。
  • 支持位宽为 32bit × 8 的 FIFO。
  • 支持 7bit/10bit 寻址模式。

WS63 I2C 设备驱动开发的一般步骤为:

  • 将 IO 引脚复用为 I2C 功能(复用关系参考表 2.2-1)。
  • 调用 uapi_i2c_init 接口,初始化 I2C 资源。
  • 根据开发需要调用 uapi_i2c_master_write 接口或者 uapi_i2c_master_read 接口执行数据写功能或读功能。

3.5.4 WS63 的 I2C 接口介绍(Master 模式)

(1)初始化 I2C
  • 原型:errcode_t uapi_i2c_master_init(i2c_bus_t bus, uint32_t baudrate, uint8_t hscode);
  • 位置:SDK 目录/include/driver/i2c.h
  • 参数:
    • bus 表示 I2C 的编号,该参数的数据类型 i2c_bus_t 是一个枚举类型,其定义在文件"SDK 目录/drivers/chips/ws63/include/platform_core.h"中,具体内容见代码 3.5-1。
    • baudrate 表示当前 I2C 通信的传输速率。
    • hscode 表示I2C高速模式主机码, 每个主机有自己唯一的主机码,有效取值范围 0 ~ 7,仅在高速模式下需要配置。
c 复制代码
/* 代码 3.5-1 */

typedef enum {
    I2C_BUS_0,               // !< I2C0
    I2C_BUS_1,               // !< I2C1
#if I2C_BUS_MAX_NUMBER > 2
    I2C_BUS_2,               // !< I2C2
#if I2C_BUS_MAX_NUMBER > 3  // !< I2C3
    I2C_BUS_3 = 3,
    I2C_BUS_4 = 4,
#endif
#endif
    I2C_BUS_NONE = I2C_BUS_MAX_NUMBER
} i2c_bus_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:以 I2C1 的传输速率为标准模式为例,其初始化代码如下:
c 复制代码
/* 代码 3.5-2 */

/*以上代码省略*/
#include "pinctrl.h"
#include "gpio.h"
#include "i2c.h"

#define I2C1_PIN_SDA	15		// GPIO-15 复用为 I2C1 的 SDA
#define I2C1_PIN_SCL	16		// GPIO-16 复用为 I2C1 的 SCL
#define I2C1_PIN_MODE	2		// IO 复用为 I2C 的编号
#define I2C1_IDX		1		// 选择 I2C1
#define I2C1_SPEED		100000	//100KHz

errcode_t board_i2c_init(void) {
    uapi_pin_set_mode(I2C1_PIN_SDA, I2C1_PIN_MODE);
    uapi_pin_set_mode(I2C1_PIN_SCL, I2C1_PIN_MODE);
    return uapi_i2c_master_init(I2C1_IDX, I2C1_SPEED, 0);
}
(2)I2C 主机写数据
  • 原型:errcode_t uapi_i2c_master_write(i2c_bus_t bus, uint16_t dev_addr, i2c_data_t *data);
  • 位置:SDK 目录/include/driver/i2c.h
  • 参数:
    • 参数 bus 表示 I2C 的编号。
    • 参数 dev_addr 表示 I2C 从机的地址,默认 7bit。
    • 参数 data 是 i2c_data_t 类型的指针,该类型的定义与接口定义在同一个文件内,具体如下:
c 复制代码
/* 代码 3.5-3 */

typedef struct i2c_data {
    uint8_t *send_buf;              /* 发送数据的buffer指针。*/
    uint32_t send_len;              /* 发送数据的buffer长度。*/
    uint8_t *receive_buf;           /* 接收数据的buffer指针。*/
    uint32_t receive_len;           /* 接收数据的buffer长度。*/
} i2c_data_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:该接口在使用之前,需要提前将 data 数据准备好,以下是调用示例:
c 复制代码
/* 代码 3.5-4 */

/*以上代码省略*/
#define I2C1_IDX		1		// 选择 I2C1
#define SLAVE_ADDR		0x8
#define RESET_COMMAND	0xFE

static void *I2CMasterDemo(const char *arg) {
    (void)arg;
    uint16_t dev_addr = SLAVE_ADDR;
    uint8_t buffer[] = {RESET_COMMAND};
    i2c_data_t data = {0};
    data.send_buf = buffer;
    data.send_len = sizeof(buffer);
    errcode_t retval = board_i2c_init(); // 初始化 I2C
    if (retval != 0) {
        printf("I2C1 init failed!\n");
        return NULL;
    }
    retval = uapi_i2c_master_write(I2C1_IDX, dev_addr, &data);
    if (retval != 0) {
        printf("I2cWrite(%02X) failed, %0X!\n", buffer[0], retval);
        return NULL;
    }
    /*以下代码省略*/
}
(2)I2C 主机读数据
  • 原型:errcode_t uapi_i2c_master_read(i2c_bus_t bus, uint16_t dev_addr, i2c_data_t *data);
  • 位置:SDK 目录/include/driver/i2c.h
  • 参数:
    • 参数 bus 表示 I2C 的编号。
    • 参数 dev_addr 表示 I2C 从机的地址,默认 7bit。
    • 参数 data 是 i2c_data_t 类型的指针。
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:该接口在使用之前,需要提前将 data 数据准备好,以下是调用示例:
c 复制代码
/* 代码 3.5-5 */

/*以上代码省略*/
#define I2C1_IDX		1		// 选择 I2C1
#define SLAVE_ADDR		0x8
#define BUFFER_LENGTH	4

static void *I2CMasterDemo(const char *arg) {
    /*以上代码省略*/
    uint16_t dev_addr = SLAVE_ADDR;
    uint8_t buffer[BUFFER_LENGTH] = {0};
    memset_s(&buffer, sizeof(buffer), 0x0, sizeof(buffer));
    
    i2c_data_t data = {0};
    data.receive_buf = buffer;
    data.receive_len = sizeof(buffer);
    uint32_t retval = uapi_i2c_master_read(I2C1_IDX, dev_addr, &data);
    if (retval != 0) {
        printf("I2cRead() failed, %0X!\n", retval);
    }
    /*以下代码省略*/
}

3.5.5 温湿度传感器 SHT20 驱动编程

图 3.5-8 SHT20 温湿度传感器外观及功能简述

(1)简介

SHT2X 是一款基于 I2C 通信的低功耗温湿度传感器,其芯片7位默认地址为0x40。

  • 当对芯片执行"读操作"时,需要将地址左移1位,并将最后1位设置为"1",即0x81为"读地址"。
  • 当对芯片执行"写操作"时,需要将地址左移1位,并将最后1位设置为"0",即0x80为"写地址"。
(2)操作指令

图 3.5-9 SHT20 操作指令列表图

  • hold master 表示"主机保持"模式,该模式下,传感器在进行测量的过程中,SCL 时钟线被主机控制,不允许在总线上执行其他的通信。
  • no hold master表示"非主机保持"模式,该模式下,传感器在进行测量的过程中,SCL被允许对其他通信提供时钟信号。

这两种不同的测量模式所使用的命令也不一样。具体要根据应用开发需求进行选择。

(3)SHT20 驱动

SHT20 的驱动代码包括初始化、写数据、读数据以及对获取的温湿度数据进行处理的功能,以下将 SHT20 的驱动代码整合到"sht20.h"和"sht20.c"中。

c 复制代码
/* 代码 3.5-6 */

// sht20.h
#ifndef __SHT20_H__
#define __SHT20_H__

#include "errcode.h"

#define SHT20_I2C_IDX 1

/**
 * @brief SHT20 读取器件的温湿度
 * @param temp 温度值 
 * @param humi 湿度值 
 * @retval ERRCODE_SUCC 成功。
 * @retval Other        失败,参考 @ref errcode_t 。
 */
errcode_t sht20_getData(float *temp, float *humi);

/**
 * @brief SHT20 初始化
 * @retval ERRCODE_SUCC 成功。
 * @retval Other        失败,参考 @ref errcode_t 。
 */
errcode_t sht20_init(void);

#endif // !__SHT20_H__
c 复制代码
/* 代码 3.5-7 */

// sht20.c
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "tcxo.h"
#include "errcode.h"
#include "pinctrl.h"
#include "gpio.h"
#include "i2c.h"
#include "sht20.h"

#define SHT20_I2C_ADDR  0x40

#define SHT20_HoldMaster_Temp_REG_ADDR 0xE3 // 主机模式会阻塞其他IIC设备的通信
#define SHT20_HoldMaster_Humi_REG_ADDR 0xE5
#define SHT20_NoHoldMaster_Temp_REG_ADDR 0xF3
#define SHT20_NoHoldMaster_Humi_REG_ADDR 0xF5
#define SHT20_W_USER_REG_ADDR 0xE6
#define SHT20_R_USER_REG_ADDR 0xE7
#define SHT20_SW_REG_ADDR 0xFE

// sht20写指令
static errcode_t sht20_write(uint8_t *buffer, uint32_t buffLen) {
  uint16_t dev_addr = SHT20_I2C_ADDR;
  i2c_data_t data = {0};
  data.send_buf = buffer;
  data.send_len = buffLen;
  errcode_t retval = uapi_i2c_master_write(SHT20_I2C_IDX, dev_addr, &data);
  if (retval != 0) {
    printf("I2cWrite(%02X) failed, %0X!\n", buffer[0], retval);
    return retval;
  }
  return 0;
}

// sht20 读数据
static errcode_t sht20_read(uint8_t *buffer, uint32_t buffLen) {
  uint16_t dev_addr = SHT20_I2C_ADDR;
  i2c_data_t data = {0};
  data.receive_buf = buffer;
  data.receive_len = buffLen;
  errcode_t retval = uapi_i2c_master_read(SHT20_I2C_IDX, dev_addr, &data);
  if (retval != 0) {
      printf("I2C Read() failed, %0X!\n", retval);
      return retval;
  }
  return 0;
}

// 对获取的温湿度的值进行处理
errcode_t sht20_getData(float *temp, float *humi)
{
  uint8_t buffer[4] = {0};

  /* 发送检测温度命令 */
  uint8_t tmpCmd[] = {SHT20_NoHoldMaster_Temp_REG_ADDR};
  errcode_t retval = sht20_write(tmpCmd, sizeof(tmpCmd));
  
  if (retval != 0){
    printf("I2C SHT20 status = 0x%x!!!", retval);
    return retval;
  }

  uapi_tcxo_delay_ms(85); /* datasheet: typ=66, max=85 */
  // 读数据
  retval = sht20_read(buffer, 3);
  if (retval != 0){
    printf("SHT20 read tempData failed,status = 0x%x!!!", retval);
    return retval;
  }

  *temp = 175.72 * (((((int)buffer[0]) << 8) + buffer[1]) / 65536.0) - 46.85;
  
  memset(buffer, 0, sizeof(buffer));

  /* 发送检测湿度命令 */
  uint8_t humiCmd[] = {SHT20_NoHoldMaster_Humi_REG_ADDR};
  
  retval = sht20_write(humiCmd, sizeof(humiCmd));
  
  if (retval != 0){
    printf("I2C SHT20 status = 0x%x!!!", retval);
    return retval;
  }
  
  uapi_tcxo_delay_ms(30); /* datasheet: typ=22, max=29 */
  // 读数据
  retval = sht20_read(buffer, 3);
  if (retval != 0){
    printf("SHT20 read humiData failed,status = 0x%x!!!", retval);
    return retval;
  }
  
  *humi = 125 * (((((int)buffer[0]) << 8) + buffer[1]) / 65536.0) - 6;
  return 0;
}

// sht20 初始化
errcode_t sht20_init(void)
{
  // 软复位
  uint8_t rstCmd[] = {SHT20_SW_REG_ADDR};
  errcode_t retval = sht20_write(rstCmd, sizeof(rstCmd));
  if (retval != 0) {
    printf("I2C SHT20 status = 0x%x!!!", retval);
    return retval;
  }else{
    uapi_tcxo_delay_ms(1000);
    printf("I2C SHT20 Init is succeeded!!!");
  }
  return 0;
}

3.5.6 功能开发实现

(1)创建工程

在工程目录下新建 "04_SHT20_DEMO" 文件夹,并在该文件夹中新建"sht20_demo.c"文件、"sht20.c"、"sht20.h"和"BUILD.gn"文件。

(2)编写功能代码

sht20.h 的代码内容为代码 3.5-6。

sht20.c 的代码内容为代码 3.5-7。

sht20_demo.c 的代码如下:

c 复制代码
/* 代码 3.5-8 */

#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "tcxo.h"
#include "errcode.h"
#include "pinctrl.h"
#include "gpio.h"
#include "i2c.h"
#include "sht20.h"

#define I2C1_PIN_SDA	15		// GPIO-15 复用为 I2C1 的 SDA
#define I2C1_PIN_SCL	16		// GPIO-16 复用为 I2C1 的 SCL
#define I2C1_PIN_MODE	2		// IO 复用为 I2C 的编号
#define I2C1_IDX		1		// 选择 I2C1
#define I2C1_SPEED		100000	//100KHz

errcode_t board_i2c_init(void) {
  uapi_pin_init();
  uapi_pin_set_mode(I2C1_PIN_SDA, I2C1_PIN_MODE);
  uapi_pin_set_mode(I2C1_PIN_SCL, I2C1_PIN_MODE);
  return uapi_i2c_master_init(I2C1_IDX, I2C1_SPEED, 0);
}

static void *SHT20Demo(const char *arg) {
  (void)arg;
  errcode_t retval;
  float temp = 0.0f;
  float humi = 0.0f;
  static char templine[32] = {0};
  static char humiline[32] = {0};

  board_i2c_init();
  sht20_init();

  while(1){
    retval = sht20_getData(&temp, &humi);
    if (retval != 0) {
        printf("get temp humidity data failed!\r\n");
    }
    sprintf(templine, ": %.2f", temp);
    sprintf(humiline, ": %.2f", humi);
    printf("temp = %s, humi = %s\r\n", templine, humiline);
    uapi_tcxo_delay_ms(1000);
  }
}

static void SHT20TestTask(void)
{
    osThreadAttr_t attr;

    attr.name = "SHT20Demo";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 0x1000; 
    attr.priority = osPriorityNormal;
    // 报错
    if (osThreadNew((osThreadFunc_t)SHT20Demo, NULL, &attr) == NULL) {
        printf("[SHT20TestTask] Failed to create SHT20Demo!\n");
    }
}

SYS_RUN(SHT20TestTask);
(3)编写配置文件

该项目目录下的 BUILD.gn 的代码如下:

cmake 复制代码
# 代码 3.5-9

static_library("sht20Demo") {
  sources = [
    "sht20_demo.c",
    "sht20.c",
  ]

  include_dirs = [
    "./",
    "//commonlibrary/utils_lite/include",
    "//kernel/liteos_m/kal/cmsis",
    "//device/soc/hisilicon/ws63v100/sdk/include/driver",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/i2c",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/i2c",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio/v150",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/rom/drivers/chips/ws63/porting/pinctrl",
  ]
}
(4)修改工程配置文件

1) 修改"工程目录"下的 BUILD.gn 文件,在 features 数组中添加 "04_SHT20_DEMO:sht20Demo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "sht20Demo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "sht20Demo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写
  • 使用串口线将** 石院星闪物联网教学开发板 **的 USB 端口与 PC 机的 USB 端口连接(烧写前请先确认是否安装串口驱动,若没有,请参考第一章的内容完成驱动的安装)
  • 参考"第 1 章 1.4 镜像文件的烧写"一节的内容完成烧写。
(7)运行效果
  • 烧写结束后, 打开串口调试助手,选择正确的 COM 编号,设置波特率为 115200,数据位 8 位,并打开串口,按下开发板上的复位按键后,可以在串口调试助手界面的接收区域看到返回的温度及湿度数据。
(8)编译错误解决

本项目代码使用了鸿蒙IoT硬件子系统的 I2C API 接口,需要连接到 hi3863 的 I2C 相关接口;默认情况下,Hi3863的I2C编译配置没有打开,需要在 //device/soc/hisilicon/ws63v100/sdk 目录下,运行 python3 build.py -c ws63-liteos-app menuconfig 脚本, Menuconfig 程序启动后,可通过 Menuconfig 对编译和系统功能进行配置:

切换到 Drivers ---> Drivers --->I2C I2C Configuration ,选中I2C SUPPORT MASTER 和 Using I2C V150,退出并保存。

3.6 UART 功能开发------以 UART2 收发数据为例

3.6.1 UART 介绍

UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)。

图 3.6-1 UART芯片内置控制器数据通信示意图

图 3.6-1 是两个UART芯片内置的控制器之间进行"一对一"的数据通信示意图,其中:"Tx"是数据发送端口,"Rx"是数据接收端口,因而只需要两根线就可以在两个设备之间进行数据通信。作为异步串行通信接口,UART数据传输需要使用"起始位"、"停止位"等这样的标志位对数据进行"格式化",这种格式化后的数据也叫做"帧"。UART通信时通用的帧格式如图 3.6-2 所示。

图 3.6-2 UART数据通信通用帧格式

  • 起始位:位于字符帧的开头,只占一位,始终为逻辑0低电平。
  • 数据位:数据位的个数不固定,可取5位、6位、7位或8位。数据位中低位在前,高位在后。若传输的是ASCII码,则按7位取。
  • 奇偶校验位:仅占一位,通过对所传输的数据进行奇偶校验,可以知道数据位在传输过程中是否发生改变。在传输过程中由于有可能受到"电磁辐射干扰"、"波特率不匹配"、"传输距离过长"等因素的影响,数据位有可能会发生变化。如果校验位是"0",代表偶校验,表示如果数据位中"1"的个数为"偶数",那么数据正确,否则数据错误;如果校验位是"1",代表奇校验,表示如果数据位中"1"的个数为"奇数",那么数据正确,否则数据错误。
  • 停止位:位于字符帧的末尾,通常可取1位、1.5位或2位,采用逻辑"1"高电平,用来表示当前"帧"传递完毕。

3.6.2 WS63 中的 UART

WS63 芯片提供了 3 个可配置的 UART 外设单元,分别是:UART0、UART1 和 UART2。

UART 规格如下:

  • 支持可编程数据位(5-8bit)、可编程停止位(1-2bit)、可编程校验位(奇/偶校验,无校验)。
  • UART 支持无流控,RTS/CTS 流控模式
  • 提供 64×8 的 TX,64×8 的 RX FIFO
  • 支持接收 FIFO 中断、发送 FIFO 中断、接收超时中断、错误中断等中断屏蔽与响应。
  • 支持 DMA 数据搬移方式。

UART 使用说明:

  • SDK 中,UART0 默认作为程序烧写和 DebugKites 工具维测数据通道。
  • SDK 中,UART1 默认作为 Testsuite、AT 以及数据打印共享串口。
  • SDK 中 drivers/chips/ws63/include/platform_core.h 文件定义了 UART 使用情况,TEST_SUITE_UART_BUS 定义了 testsuite 调试使用的串口,LOG_UART_BUS 定义了 HSO 工具使用的串口。

3.6.3 UART 编程步骤

WS63 UART 在 UART 模式下数据收发功能编程的一般步骤为:

  • 将 IO 引脚复用为 UART1 或 UART2 功能(复用关系参考表 2.2-1)。
  • 调用 uapi_uart_deinit 接口,先去初始化 UART。
  • 调用 uapi_uart_init 接口,再初始化 UART。
  • 根据开发需要调用 uapi_uart_write 接口执行往 UART 写数据,或者 uapi_uart_read 接口执行从 UART 读数据功能。

3.6.4 WS63 的 UART 接口介绍(仅限工作在 UART 模式下)

(1)去初始化与初始化 UART
  • 原型:
c 复制代码
/* 代码 3.6-1 */

/* 去初始化 UART */
errcode_t uapi_uart_deinit(uart_bus_t bus);
/* 初始化 UART */
errcode_t uapi_uart_init(uart_bus_t bus, 
                         const uart_pin_config_t *pins,
                         const uart_attr_t *attr, 
                         const uart_extra_attr_t *extra_attr,
                         uart_buffer_config_t *uart_buffer_config);
  • 位置:SDK 目录/include/driver/uart.h
  • 参数:
    • 参数 bus 表示 UART 的编号,该参数的数据类型 uart_bus_t 是一个枚举类型,其定义在文件"SDK 目录/drivers/chips/ws63/include/platform_core.h"中,具体内容见代码 3.6-2。
    • 参数 *pins 表示用于指定复用为 UART 功能的引脚信息,其数据类型 uart_pin_config_t 是 hal_uart_pin_config_t 的别名,其定义在文件"SDK 目录/drivers/drivers/hal/uart/hal_uart.h"中,具体内容见代码 3.6-3。
    • 参数 *attr 表示基础 UART 的参数配置,包括波特率、数据位等,其数据类型 uart_attr_t 是 hal_uart_attr_t 的别名,hal_uart_attr_t 是个结构体类型,其内部数据的取值也有限定范围,并通过枚举类型的数据来规范,该结构体以及内部数据所参考的枚举类型的定义都在文件"SDK 目录/drivers/drivers/hal/uart/hal_uart.h"中,具体内容见代码 3.6-4。
    • 参数 *extra_attr 表示 UART 扩展配置,包括是否使用 DMA 模式,是否采用中断方式等,其数据类型 uart_extra_attr_t 是 hal_uart_extra_attr_t 的别名,该类型是一个结构类型,其定义在文件"SDK 目录/drivers/drivers/hal/uart/hal_uart.h"中,具体内容见代码 3.6-5。
    • 参数 *uart_buffer_config 表示 UART 的缓存数据结构,其类型 uart_buffer_config_t 是一个结构体,其定义在文件"SDK 目录/drivers/drivers/hal/uart/hal_uart.h"中,具体内容见代码 3.6-6。
c 复制代码
/* 代码 3.6-2 */

typedef enum {       // !< Hi1132 | Hi1135
    UART_BUS_0 = 0,  // !< UART L | UART L0
#if UART_BUS_MAX_NUMBER > 1
    UART_BUS_1 = 1,  // !< UART H | UART H0
#endif
#if UART_BUS_MAX_NUMBER > 2
    UART_BUS_2 = 2,  // !< M UART | UART H1
#endif
    UART_BUS_NONE = UART_BUS_MAX_NUMBER  // !< Value used as invalid/unused UART number
} uart_bus_t;
c 复制代码
/* 代码 3.6-3 */

/**
 * @brief  UART的PIN配置数据结构定义
 */
typedef struct {
    pin_t tx_pin;                   /* 发送引脚 */
    pin_t rx_pin;                   /* 接收引脚 */
    pin_t cts_pin;                  /* 发送就绪引脚 */
    pin_t rts_pin;                  /* 接收就绪引脚 */
} hal_uart_pin_config_t;
c 复制代码
/* 代码 3.6-4 */

/**
 * @brief  UART数据位定义
 */
typedef enum hal_uart_data_bit {
    UART_DATA_BIT_5,       /* UART数据位为5-bit */
    UART_DATA_BIT_6,       /* UART数据位为6-bit */
    UART_DATA_BIT_7,       /* UART数据位为7-bit */
    UART_DATA_BIT_8        /* UART数据位为8-bit */
} hal_uart_data_bit_t;

/**
 * @brief  UART奇偶校验位定义
 */
typedef enum hal_uart_parity {
    UART_PARITY_NONE,      /* UART无奇偶校验 */
    UART_PARITY_ODD,       /* UART奇校验 */
    UART_PARITY_EVEN       /* UART偶校验 */
} hal_uart_parity_t;

/**
 * @brief  UART停止位定义
 */
typedef enum hal_uart_stop_bit {
    UART_STOP_BIT_1,       /* UART停止位为1-bit */
    UART_STOP_BIT_2        /* UART停止位为2-bit */
} hal_uart_stop_bit_t;

/**
 * @brief  UART 硬件控制流定义.
 */
typedef enum {
    UART_FLOW_CTRL_NONE = 0x0,
    UART_FLOW_CTS = 0x1,
    UART_FLOW_RTS = 0x2,
    UART_FLOW_CTRL_RTS_CTS = UART_FLOW_CTS | UART_FLOW_RTS,
    UART_FLOW_CTRL_SOFT = 0x4
} hal_uart_flow_ctrl_t;

/**
 * @brief  UART基础配置参数定义
 */
typedef struct uart_attr {
    uint32_t baud_rate;    /* UART 波特率 */
    uint8_t data_bits;     /* UART 数据位,参考 @ref hal_uart_data_bit_t */
    uint8_t stop_bits;     /* UART 停止位,参考 @ref hal_uart_stop_bit_t */
    uint8_t parity;        /* UART 奇偶校验位,参考 @ref hal_uart_parity_t */
    uint8_t flow_ctrl;     /* UART 流控,参考 @ref hal_uart_flow_ctrl_t */
} hal_uart_attr_t;
c 复制代码
/* 代码 3.6-5 */

/**
 * @brief  UART扩展配置参数定义
 */
typedef struct uart_extra_attr {
    bool tx_dma_enable;          /* false: TX没有使用DMA,使用 @ref uapi_uart_write 发送数据 \n
                                    true:  TX使用DMA,使用 @ref uapi_uart_write_by_dma 发送数据 */
    uint8_t tx_int_threshold;    /* 触发中断的txfifo水线 */
    bool rx_dma_enable;          /* false: RX没有使用DMA,使用 @ref uapi_uart_write 发送数据 \n
                                    true:  RX使用DMA,使用 @ref uapi_uart_write_by_dma 发送数据 */
    uint8_t rx_int_threshold;    /* 触发中断的rxfifo水线 */
} hal_uart_extra_attr_t;
c 复制代码
/* 代码 3.6-6 */

/**
 UART 缓存数据结构定义。
 */
typedef struct uart_buffer_config {
    void *rx_buffer;            /* 接收数据的 buffer 指针*/
    size_t rx_buffer_size;      /* 接收 buffer 的长度 */
} uart_buffer_config_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:以 UART2 的传输速率为115200, 8位数据位,其初始化代码如下:
c 复制代码
/* 代码 3.6-7 */

/*以上代码省略*/
#include "pinctrl.h"
#include "gpio.h"
#include "uart.h"

#define UART2_PIN_RXD		7		// GPIO-07 复用为 UART2 的 RXD
#define UART2_PIN_TXD		8		// GPIO-08 复用为 UART2 的 TXD
#define UART2_PIN_MODE		2		// IO 复用为 UART2 的编号
#define UART2_IDX			2		// 选择 UART2
#define UART2_BUAD			115200	// 波特率为 115200

/* 定义缓存数据结构 */
uart_buffer_config_t g_uart_buffer_config = {
    .rx_buffer = g_uart_rx_buff,
    .rx_buffer_size = UART2_RX_BUFF_SIZE
};
/*  uart2 初始化 */
errcode_t board_uart2_init(void) {
    uapi_pin_set_mode(UART2_PIN_RXD, UART2_PIN_MODE);  // GPIO-07复用为 UART2RXD
    uapi_pin_set_mode(UART2_PIN_TXD, UART2_PIN_MODE);  // GPIO-08复用为 UART2TXD
    /* 定义 uart 基础配置 */
    uart_attr_t attr = {
        .baud_rate = UART2_BUAD, /* 波特率 */
        .data_bits = 8, /* 数据位 */
        .stop_bits = 1, /* 停止位 */
        .parity = 0 /* 校验位 */
    };
    /* 定义 uart 引脚配置 */
    uart_pin_config_t pin_config = {
        .tx_pin = UART2_PIN_TXD, /* uart2 tx */
        .rx_pin = UART2_PIN_RXD, /* uart2 rx */
    };
    /* 重点,UART初始化之前需要去初始化,否则会报0x80001044 */
    uapi_uart_deinit(UART2_IDX); 
    
    errcode_t retval = uapi_uart_init(UART2_IDX, &pin_config, &attr, NULL, &g_uart_buffer_config);
    if (retval != ERRCODE_SUCC) {
        printf("uart2 init fail\r\n");
    }
    return retval;
}
(2)UART 发送数据
  • 原型:
c 复制代码
/* 代码 3.6-8 */

int32_t uapi_uart_write(uart_bus_t bus, 
                        const uint8_t *buffer, 
                        uint32_t length, 
                        uint32_t timeout);
  • 位置:SDK 目录/include/driver/uart.h
  • 参数:
    • 参数 bus 表示 UART 的编号。
    • 参数 buffer 指向待发送数据所在存储器的地址。
    • 参数 length 表示待发送数据的字节数。
    • 参数 timeout 表示发送数据的超时时间
  • 返回值:实际发送数据的字节数。
  • 调用:以通过 UART2 以轮询方式每隔 1 秒发送一次"Hello" 为例,代码如下:
c 复制代码
/* 代码 3.6-9 */

/*以上代码省略*/
#define UART_TRANSFER_SIZE 18

/* 以上代码省略 */
static uint8_t g_app_uart_tx_buff[UART_TRANSFER_SIZE] = "Hello\r\n";

static void *uart2Demo(const char *arg) {
    (void)arg;
    board_uart2_init(); // 初始化 UART2
    while(1){
        uapi_tcxo_delay_ms(1000);
        if (uapi_uart_write(UART2_IDX, g_app_uart_tx_buff, UART_TRANSFER_SIZE, 0) == UART_TRANSFER_SIZE) {
            printf("uart%d poll mode send back succ!\r\n", UART2_IDX);
        }
    }  
}
(3)UART 读取数据
  • 原型:
c 复制代码
/* 代码 3.6-9 */

int32_t uapi_uart_read(uart_bus_t bus, 
                       const uint8_t *buffer, 
                       uint32_t length, 
                       uint32_t timeout);
  • 位置:SDK 目录/include/driver/uart.h
  • 参数:
    • 参数 bus 表示 UART 的编号。
    • 参数 buffer 指向接收到的数据准备存储到的存储器地址。
    • 参数 length 表示打算接收数据的字节数。
    • 参数 timeout 表示接收数据的超时时间
  • 返回值:实际接收数据的字节数。
  • 调用:以通过 UART2 以轮询方式每隔 1 秒接收一次数据为例,代码如下:
c 复制代码
/* 代码 3.6-10 */

/* 以上代码省略 */
static uint8_t g_app_uart_rx_buff[UART_TRANSFER_SIZE] = {0};

static void *uart2Demo(const char *arg) {
    (void)arg;
    board_uart2_init(); // 初始化 UART2
    while(1){
        uapi_tcxo_delay_ms(1000);
        /* 这里代码省略 */
        if (uapi_uart_read(UART2_IDX, g_app_uart_rx_buff, UART_TRANSFER_SIZE, 0) > 0) {
            printf("uart%d poll mode receive succ!, g_app_uart_rx_buff = %s\r\n", UART2_IDX,
                        g_app_uart_rx_buff);
        }
    }  
}

3.6.5 功能开发实现

(1)创建工程

在工程目录下新建 "05_UART2_DEMO" 文件夹,并在该文件夹中新建"uart2_demo.c"文件。

(2)编写功能代码

uart2_demo.c 的代码如下:

c 复制代码
/* 代码 3.6-11 */

#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "tcxo.h"
#include "errcode.h"
#include "pinctrl.h"
#include "gpio.h"
#include "uart.h"

#define UART2_PIN_RXD		  7		    // GPIO-07 复用为 UART2 的 RXD
#define UART2_PIN_TXD		  8		    // GPIO-08 复用为 UART2 的 TXD
#define UART2_PIN_MODE		  2		    // IO 复用为 UART2 的编号
#define UART2_IDX			  2		    // 选择 UART2
#define UART2_BUAD			  115200	// 波特率为 115200
#define UART2_TRANSFER_SIZE   18

/* 定义数据缓冲区 */
static uint8_t uart2_buff[UART2_TRANSFER_SIZE] = "Hello UART2\r\n";

/* 定义缓存数据结构 */
uart_buffer_config_t uart2_buffer_config = {
    .rx_buffer = uart2_buff,
    .rx_buffer_size = UART2_TRANSFER_SIZE
};
/*  uart2 初始化 */
errcode_t board_uart2_init(void) {
    uapi_pin_set_mode(UART2_PIN_RXD, UART2_PIN_MODE);  // GPIO-07复用为 UART2RXD
    uapi_pin_set_mode(UART2_PIN_TXD, UART2_PIN_MODE);  // GPIO-08复用为 UART2TXD
    /* 定义 uart 基础配置 */
    uart_attr_t attr = {
        .baud_rate = UART2_BUAD, /* 波特率 */
        .data_bits = 8, /* 数据位 */
        .stop_bits = 1, /* 停止位 */
        .parity = 0 /* 校验位 */
    };
    /* 定义 uart 引脚配置 */
    uart_pin_config_t pin_config = {
        .tx_pin = UART2_PIN_TXD, /* uart2 tx */
        .rx_pin = UART2_PIN_RXD, /* uart2 rx */
    };
    /* 重点,UART初始化之前需要去初始化,否则会报0x80001044 */
    uapi_uart_deinit(UART2_IDX); 
    
    errcode_t retval = uapi_uart_init(UART2_IDX, &pin_config, &attr, NULL, &uart2_buffer_config);
    if (retval != ERRCODE_SUCC) {
        printf("uart2 init fail\r\n");
    }
    return retval;
}

static void *UART2Demo(const char *arg) {
    (void)arg;
    uapi_tcxo_delay_ms(1000);
    board_uart2_init(); // 初始化 UART2
    
    uapi_uart_write(UART2_IDX, uart2_buff, UART2_TRANSFER_SIZE, 0);
    memset(uart2_buff, 0, UART2_TRANSFER_SIZE);

    while(1){
        uapi_tcxo_delay_ms(500);
        if (uapi_uart_read(UART2_IDX, uart2_buff, UART2_TRANSFER_SIZE, 0) == UART2_TRANSFER_SIZE) {
            // osal_printk("uart%d poll mode receive succ!\r\n", CONFIG_UART0_BUS_ID);
            if(uapi_uart_write(UART2_IDX, uart2_buff, UART2_TRANSFER_SIZE, 0) == UART2_TRANSFER_SIZE){
              printf("uart2 work OK");
            }
        }
    }  
}

static void UART2TestTask(void)
{
    osThreadAttr_t attr;

    attr.name = "UART2Demo";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 0x1000; 
    attr.priority = osPriorityNormal;
    // 报错
    if (osThreadNew((osThreadFunc_t)UART2Demo, NULL, &attr) == NULL) {
        printf("[UART2TestTask] Failed to create UART2Demo!\n");
    }
}

SYS_RUN(UART2TestTask);
(3)编写配置文件

该项目目录下的 BUILD.gn 的代码如下:

cmake 复制代码
# 代码 3.6-12

static_library("uart2Demo") {
  sources = [
    "uart2_demo.c"
  ]

  defines = [
    "CONFIG_UART_SUPPORT_TX",
    "CONFIG_UART_SUPPORT_RX",]

  include_dirs = [
    "//commonlibrary/utils_lite/include",
    "//kernel/liteos_m/kal/cmsis",
    "//device/soc/hisilicon/ws63v100/sdk/include/driver",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio/v150",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/uart",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/uart",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/rom/drivers/chips/ws63/porting/pinctrl",
    "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/common_headers/native",
  ]
}
(4)修改工程配置文件

1) 修改"工程目录"下的 BUILD.gn 文件,在 features 数组中添加 "05_UART2_DEMO:uart2Demo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在ws63-liteos-app'<'ram_component': []'添加 "uart2Demo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "uart2Demo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写
  • 使用串口线将 **石院星闪物联网教学开发板 **的 USB 端口与 PC 机的 USB 端口连接(烧写前请先确认是否安装串口驱动,若没有,请参考第一章的内容完成驱动的安装)
  • 参考"第 1 章 1.4 镜像文件的烧写"一节的内容完成烧写。
(7)运行效果

将 Type-C USB 线端与 PC 机连接,一端与开发板的 UART2 连接,打开串口调试助手,设置正确的 COM 编号,波特率为 115200,其他默认,打开串口,并重启开发板,通过串口调试助手的发送窗口发送数据后,在接收窗口可以看到 WS63 通过 UART2 收到的数据内容。

3.7 ADC 功能开发------以 THB001P 摇杆驱动编程为例

3.7.1 模拟信号与数字信号

信号是指通信双方所传递的数据信息。

(1)模拟信号信号

模拟信号是指在时间和空间上数据都是连续的信号。模拟信号可以简单到就是一个"正弦波"的样子,也可以复杂到由许多不同指标的正弦波叠加后的样子。图 3.7-1 就是一个不太复杂的模拟信号的波形在一定时间内的展示。水平方向的t代表时间,垂直方向的X代表随着时间的推移这个波形振幅的取值。这幅图中的"波形"是连续的,它可以看成是由若干个简单的"正弦波"叠加而成。模拟信号的波形在二维坐标空间中展示了它的"振幅"变化、"频率"变化以及"相位"变化。

图 3.7-1 某个模拟信号示意图

模拟信号容易受到其他模拟信号的干扰,这些干扰信号往往被称为"噪声"。在一个房间里只有一个人说话时,这个人说的话我们能清楚的听到;如果有两个人同时说话,我们仔细分辨也差不多能听清楚;可房间足够大的情况下有40个人同时说话,估计你想听清楚每个人说什么就不可能了。这就是模拟信号的一个特点。根据这个特点人们制造除了一种设备叫做"手机信号屏蔽仪",用在某些需要屏蔽手机信号的场合,例如:考场、保密机构等。手机信号在空气中传播是也是模拟信号,只要在手机所在的环境中发射很多"噪声"信号来制造干扰,手机就无法通信了。

模拟信号的"频率"取值范围很广,不同的频带范围应用也不同。以"无线通信"来说,VLF频带范围是用于远距离导航以及海底通信的3KHz-30KHz,EHF频带范围是用于卫星通信以及雷达探测的30GHz-300GHz。在医学领域中使用的紫外线甚至达到了3×1015Hz。

(2)数字信号

数字信号是在电子设备内部传递的信号。以图6-2所示的数字信号图为例,图中横坐标表示的是时间,纵坐标是电压值。随着时间的变化,电压的取值只有两种情况,一种是"高电平"(在图中标识为1),一种是"低电平"(在图中标识为0)。随着时间的推移,在信号线上每隔一定时间(或者叫周期)就出现一个确定的电压值,根据这个电压值转换成对应的数字值,当把这些出现的数字值组合在一起后,我们得到了一个二进制数据"0100 1011"。如果把这个二进制数据看做是ASCII编码的话,那么在信号线上传递的信息就是大写字母"K"。

图 3.7-2 电子设备内部传递的数字信号示意图

数字信号几乎是对"噪声"免疫的。你用手机进行微信聊天时,除非你自己打错字,不然你明明用手机上的屏幕键盘输入了"你好",在点击发送按钮的同时你不小心咳嗽了一声,结果发出去的文字依然是"你好",而不会是"尔子"对吧。

单片机内部的元件之间所传递的信号就是数字信号。在第二章介绍GPIO时,提到了GPIO的管脚上传递的值有两种:这两种值对应到电路中表现出来的就是两种不同的"电压"值,而对应软件开发人员来说就是"0"和"1"这两个二进制数符。当通过GPIO在某个管脚上按照一定的频率输入了对应为"0100 1011"这样的电压组合后,单片机的MCU经过内部的总线获取到电压组合后,再经过内部部件的处理,最后被识别为"K",这样的过程就是数字信号在单片机内部的传输过程。

(3)模拟信号与数字信号之间的转换

日常生活中有很多信息都是以"模拟信号"来体现的。例如每天的气温变化、光照强度变化、空气湿度变化、人体血压的变化等。有些时候我们需要对这些变化的数据进行实时监测,如果全靠人工来完成,工作量就太大了。

传感器是一种感知元件,它们是利用自身的一些物理或者化学的特性对外部世界的某个指标进行标识。例如"光敏电阻传感器",就是利用某些材质在接受强光照射后电阻值增大而光线变弱后电阻值减小的特性所制作出来的感知元件。这些传感器所感知的数据都是模拟的,它们往往是将获取到的外部世界的线性模拟量转换为线性的电压值。即便是线性电压值,对于 MCU 来说也无法直接处理,这就需要其内部有一个将传感器采集到的模拟信号"翻译"成数字信号的部件来帮忙了,它就是ADC。

3.7.2 ADC 介绍

ADC(Analog to Digital Convertor),模数转换器。它是将连续变化的模拟信号转换为离散的数字信号的器件。

(1)ADC 转换原理

图 3.7-3 模拟信号转换为数字信号的过程

ADC将模拟电压转换为数字量时,整个转换过程可以分为"采样"、"量化"和"编码"三个大的环节。

  • 图 3.7-3(a)中是一个带转换的模拟信号。
  • 图 3.7-3(b)中的箭头符号代表在当前时刻对图 3.7-3(a)中的模拟信号进行"采样",这个"采样"操作是按照相同的频率来执行的,也叫"采样频率"。
  • 图 3.7-3(c)是对采样后的数据进行量化,也就是在坐标空间中对采样的值进行标注。最后将这些量化后的数据进行二进制编码。

采样的频率越高,量化后的数据量就越大,编码时所需的二进制位数就越多,而转换后的数字信号就越接近实际的模拟信号。

(2)ADC 的转换方法

ADC转换的方法有很多,包括"逐次逼近法"、"双积分法"、"电压频率转换法"等。其中最好理解的一种叫做"逐次逼近法"。

图 3.7-4 就是采用逐次逼近法进行AD转换的电路示意图。

转换开始时,首先将"逐次逼近寄存器"的值先清空,然后将其最高位置为"1"后送入"D/A转换器",经"D/A转换器"转换后会产生一个输出电压Vo,这个值与送入"比较器"待转换的采样电压值Vi进行比较:

  • 如果Vo<Vi,那么"逐次逼近寄存器"的最高位的"1"被保留,将其次高位置为"1",再经过"D/A转换器"输出一个新的Vo与Vi比较;
  • 如果Vo>Vi,则将比较前置位的"1"清零,而将后一位置为"1"后将输出的Vo与Vi进行比较。

直到"逐次逼近寄存器"中的数据经过"D/A"转换器输出的Vo=Vi时,由"控制逻辑电路"产生转换结束信号,将"逐次逼近寄存器"中的数字量送入缓冲寄存器中输出,完成一次AD转换。

图 3.7-4 逐次逼近法AD转换原理图

(3)ADC 转换的分辨率

:::success

【举个例子】

一把量程为10cm且最小刻度为1mm的尺子它的分辨率就是1mm。你用这把尺子可以相对精确的画出15mm和16mm两种不同长度的直线,但如果你想用这把尺子画出15.8mm长度的直线的话就比较困难了。换一种说法,当两条直线之间的长度差最小为1mm时,你可以用这把分辨率为1mm的尺子量出来,如果两条直线之间的长度差小于1mm时,你就无法量出来了。

:::

ADC的分辨率就是指可以分辨出来模拟量变化之间的最小值。用1个二进制数据来描述0~5v的电压变化时,分辨率就是5v;用2个二进制位来描述的话,分辨率就是1.25v;用3个二进制位,那么分辨率就是0.625v。也就是说ADC内部用来存放转换后二进制数据的寄存器位数越高,这个ADC的分辨率就越高。

(4)ADC 的转换精度

理论上经过AD转换后生成的电压值与实际ADC工作中产生的电压之间的误差,叫做ADC的转换精度。例如用一个8位的ADC对电压范围为0~5V的模拟电压信号进行转换,理论上无论什么情况下输入电压为2.5V是,得到的输出数据都应该是1000 0000才对,但是实际上每次输出的二进制数据与这个理论值之间有一些误差,这个误差就是ADC的转换精度。

(5)ADC 转换的参考电压

也叫基准电压,是指ADC中二进制数据位取最大值时表示的电压值。如果没有这个参考电压,ADC就无法确定被测信号的准确幅值。

3.7.3 WS63 中的 ADC

WS63 中的 ADC 部件是 LSADC,是一款 SAR(Successive Approximations Register) ADC(逐次逼近型数模转换设备),实现将模拟信号转变成数字信号的功能。

  • WS63 中 ADC 模块的功能特点:
    • 输入时钟 32MHz,12bit 分辨率,单通道采样率最大为 1Msps。
    • 共 6 个通道,支持软件配置 0~5 任意通道使能,逻辑按通道编号先低后高发起切换,完成单通道采样并完成平均值滤波后自动进行通道切换。。
    • 支持 128×17bit FIFO 用于数据缓存,数据存储格式:高 3bit 为通道编号,低14bit 为有效数据。
    • 支持对 ADC 采样数据进行平均滤波处理,平均次数支持 1(不进行平均)、2、4、8;多通道时,每个通道接收 N 个数据(平均滤波个数)再切换通道。
    • 支持 FIFO 水线中断、满中断上报,ADC 忙状态、控制器 FIFO 空满状态查询。
  • 关于 ADC 的量程:

受限于数模复用的 GPIO 供电电压,ADC 参考电压为 0-3.3V,有六个端口可以输入电压值。

  • 关于 ADC 管脚:

LSADC 通道与 GPIO 功能只支持其中 1 种功能,即 WS63 的 IO 管脚如果用作 ADC 通道的话,只需要直接初始化即可,此时 IO 管脚无法再用于 GPIO。这种 IO 也成为外设 IO,或者外设专属 IO。而 GPIO 是一般通用 IO。ADC 通道管脚与 GPIO管脚的对应关系如表 3.7-1 所示。

表 3.7-1 ADC 通道管脚与复用管脚对应关系

复用管脚名称 ADC 通道
GPIO_07 ADC0
GPIO_08 ADC1
GPIO_09 ADC2
GPIO_10 ADC3
GPIO_11 ADC4
GPIO_12 ADC5

3.7.4 ADC 功能开发编程步骤

若要在 WS63 进行 ADC 功能开发,往往采取 ADC 对指定的通道进行自动扫描,在自动扫描中断回调中处理数据,其开发步骤如下:

  • 首先确认需要 ADC 的传感器所连接的复用管脚名称,以确定其对应的 ADC 通道号。
  • 编写自动扫描中断回调服务函数功能。
  • 初始化 ADC。
  • 上电启用 ADC。
  • 编写 ADC 自动扫描配置参数。
  • 开启 ADC 自动扫描。
  • 关闭 ADC 自动扫描。

3.7.5 WS63 的 ADC 功能主要接口介绍

(1)ADC 初始化
  • 原型:errcode_t uapi_adc_init(adc_clock_t clock);
  • 位置:SDK 目录/include/driver/adc.h
  • 参数:
    • 参数 clock 表示 ADC 的采样频率,该参数的数据类型 adc_clock_t 是一个枚举类型,其定义在文件"SDK 目录/include/driver/adc.h"中,具体内容如下:
c 复制代码
/* 代码 3.7-1 */

/* ADC一次采样需要16个时钟周期,因此采样速率 = adc_clk / 16。 */
/* ADC的源时钟为 2MHz。 */
typedef enum adc_clock {
    ADC_CLOCK_500KHZ = 0,  /* ADC时钟频率: 500KHZ。 */
    ADC_CLOCK_250KHZ = 1,  /* ADC时钟频率: 250KHZ。 */
    ADC_CLOCK_125KHZ = 2,  /* ADC时钟频率: 125KHZ。 */
    ADC_CLOCK_015KHZ = 3,  /* ADC时钟频率: 015KHZ。 */
    ADC_CLOCK_MAX,
    ADC_CLOCK_NONE = ADC_CLOCK_MAX
} adc_clock_t;
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:参考代码 3.7-6。
(2)ADC 去初始化
  • 原型:errcode_t uapi_adc_deinit(void);
  • 位置:SDK 目录/include/driver/adc.h
  • 参数: 无
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:当程序功能中不再需要 ADC,并且 ADC 通道号与后续功能中使用的 GPIO 是复用情况时,应首先执行 ADC 的去初始化,再进行 GPIO 功能开发。
(3)ADC 启用或关闭
  • 原型:void uapi_adc_power_en(afe_scan_mode_t afe_scan_mode, bool en);
  • 位置:SDK 目录/include/driver/adc.h
  • 参数:
    • 参数 afe_scan_mode 表示 ADC 选择使用的AFE精度模式,该参数的数据类型 afe_scan_mode_t 是一个枚举类型,其定义在文件"SDK 目录/include/driver/adc.h"中,具体内容见代码 3.7-2。
    • 参数 en 是 bool 类型的数据,取值为 true 表示开启 ADC;取值 false 表示关闭 ADC。
c 复制代码
/* 代码 3.7-2 */

typedef enum afe_scan_mode {
    AFE_GADC_MODE = 0,                  /* 模拟前端ADC常规精度模式。 */
#if defined (CONFIG_ADC_SUPPORT_HAFE)
    AFE_HADC_MODE,                      /* 模拟前端ADC高精度模式。 */
#elif (defined CONFIG_ADC_SUPPORT_AMIC)
    AFE_AMIC_MODE,                      /* 模拟前端ADC麦克风模式。 */
    AFE_BIO_MODE,                      /* 模拟前端ADC生物测量模式。 */
#endif
    AFE_SCAN_MODE_MAX_NUM
} afe_scan_mode_t;
  • 返回值:无
  • 调用:参考代码 3.7-6。
(4)启动 ADC 自动扫描通道
  • 原型:
c 复制代码
/* 代码 3.7-3 */
errcode_t uapi_adc_auto_scan_ch_enable(
    uint8_t channel, 
    adc_scan_config_t config, 
    adc_callback_t callback);
  • 位置:SDK 目录/include/driver/adc.h
  • 参数:
    • 参数 channel 表示 ADC 的通道编号,取值为 0~5。
    • 参数 config 表示 ADC 的扫描配置,其类型 adc_scan_config_t 是一个结构体,其定义在文件"SDK 目录/include/driver/adc.h"中,具体内容参考代码 3.7-4。
    • 参数 callback 是一个函数指针,表示 ADC 扫描中断的回调函数地址,该函数指针的原型定义见代码 3.7-5。
c 复制代码
/* 代码 3.7-4 */

typedef struct adc_scan_config {
    uint8_t type;                       /* FIFO全扫描或阈值扫描。 */
    float threshold_l;                  /* 阈值扫描电压(v)下限。 */
    float threshold_h;                  /* 阈值扫描电压(v)上限。 */
    uint8_t freq;                       /* ADC扫描频率,用于所有频道。 */
#if defined(CONFIG_ADC_SUPPORT_LONG_SAMPLE)
    uint32_t long_sample_time;           /* ADC长采样上报周期(单位:毫秒)。 */
#endif
} adc_scan_config_t;
c 复制代码
/* 代码 3.7-5 */

typedef void (*adc_callback_t)(
    uint8_t channel,  /* 自动扫描通道 */
    uint32_t *buffer, /* 自动扫描采样结果存放 */
    uint32_t length,  /* 扫描失败时,长度为0;
                         fifo全扫描时,长度为128;
                         阈值扫描时,长度为1。 */
    bool *next        /* 继续自动扫描或停止自动扫描。 */
);
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:参考代码 3.7-6。
(5)禁止 ADC 通道自动扫描
  • 原型:errcode_t uapi_adc_auto_scan_ch_disable(uint8_t channel);
  • 位置:SDK 目录/include/driver/adc.h
  • 参数:
    • 参数 channel 表示 ADC 的通道编号,取值为 0~5。
  • 返回值:
    • 如果成功,返回值为 ERRCODE_SUCC
    • 如果失败,返回值非 0,可根据返回值在 errcode_t 类型中查看出错类型。
  • 调用:参考代码 3.7-6。

3.7.6 微型指拨模拟开关 THB001P 驱动编程

本例以 THB001P 微型指拨模拟开关为例,讲解具体功能开发。

(1)THB001P 介绍

图 3.7-5 THB001P 微型指拨模拟开关实物外观正视图

THB001P 型摇杆开关是一款具有双轴,可 360° 操控的摇杆开关。 THB 是 ThumbStick 的简写,表示拇指操控杆。额定电压 DC 5V。

(2)THB001P 工作原理

约定图 3.7-5 的阅读方向为:两个橙色部分分别在摇杆的上侧和右侧,黑色的按键开关为摇杆的下侧。其工作原理如下:

  • 摇杆左右移动时,上侧橙色部分的中心旋子发生旋转,带动内部的可变电阻值发生变化,并引起 ADC_Y 引脚上电压值的变化;
  • 摇杆上下移动时,右侧橙色部分的中心旋子发生旋转,带动内部的可变电阻值发生变化,并引起 ADC_X 引脚上电压值的变化;
  • 摇杆被垂直按下时,黑色的按键开关处于闭合状态,摇杆抬起时处于断开状态。

以摇杆的 VCC 接入 +5V 的工作电压,由此反映出的引脚电压变化为:

  • 摇杆处于中心未按下状态时,ADC_X 以及 ADC_Y 的电压在 2.3V 左右。
  • 摇杆往上推动, ADC_X 的电压值逐渐降低,往右移动 ADC_X 的电压值逐渐升高。
  • 摇杆往上推动,ADC_Y 的电压逐渐降低,往下推 ADC_Y 的电压值逐渐升高。

由此可以利用该摇杆的工作原理及特性制作游戏外设、遥控车操作杆、导航控制操作杆、工业控制操作杆等。

(3)THB001P 驱动开发

本例以 WS63 石院星闪无线遥控手柄开发板上的左摇杆开关为例,该摇杆的 ADC-X 接 ADC1 通道, ADC-Y 接 ADC2 通道,代码 3.7-6 给出了通过 ADC 自动采集结束中断回调方式处理电压值的功能。

c 复制代码
/* 代码 3.7-6 */

#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "los_base.h"
#include "errcode.h"
#include "pinctrl.h"
#include "adc.h"
#include "adc_porting.h"

#define ADC_X_CHANNEL  1 // ADC1 - GPIO_08
#define ADC_Y_CHANNEL  2 // ADC2 - GPIO_09

uint32_t adcx, adcy;  // 全局变量,存放 ADC-X 和 ADC-Y 的电压值

// adc 自动采样结束中断回调
static void thb001p_adc_callback(uint8_t ch, uint32_t *buffer, uint32_t length, bool *next)
{
  uint32_t sum = 0;
  if (length > 1)
  {
    for (uint32_t i = 0; i < length; i++)
    {
        sum += buffer[i];
    }
    if(ch == ADC_X_CHANNEL){
      adcx = sum / (length - 1);
      printf("ADC_X vol: %dmv\r\n", adcx);
    }
    if(ch == ADC_Y_CHANNEL) {
      adcy = sum / (length - 1);
      printf("ADC_Y vol: %dmv\r\n", adcy);
    }
  }
}

// 主线程函数
static void Thb001pCtrlDemo(void *arg)
{
  (void)arg;

  // ADC一次采样需要16个时钟周期,因此采样速率 = adc_clk / 16
  uapi_adc_init(ADC_CLOCK_NONE); // ADC_CLOCK_MAX

  // 上电并启用ADC
  uapi_adc_power_en(AFE_GADC_MODE, true); // 模拟前端ADC常规精度模式

  adc_scan_config_t config = {
      .type = 0,          // FIFO全扫描或阈值扫描
      .threshold_l = 0.5, // 阈值扫描电压(v)下限
      .threshold_h = 4.5, // 阈值扫描电压(v)上限
      .freq = 0,          // ADC扫描频率,用于所有频道
  };

  // 工作循环,每隔10ms获取一次ADC通道的值
  while (1)
  {
      uapi_adc_auto_scan_ch_enable(ADC_X_CHANNEL, config, thp001p_adc_callback);
      uapi_adc_auto_scan_ch_disable(ADC_X_CHANNEL);

      uapi_adc_auto_scan_ch_enable(ADC_Y_CHANNEL, config, thp001p_adc_callback);
      uapi_adc_auto_scan_ch_disable(ADC_Y_CHANNEL);

      LOS_Msleep(10);
      
      /* 这里可以追加根据 ADC-X 和 ADC-Y 电压值,构建不同业务处理的功能代码 */
  }
}

3.7.7 功能开发实现

(1)创建工程

在工程目录下新建 "06_THB001P_DEMO" 文件夹,并在该文件夹中新建"thb001p_demo.c"文件和"BUILD.gn"文件。

(2)编写功能代码

thb001p_demo.c 的代码如下:

c 复制代码
/* 代码 3.7-7 */

#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "los_base.h"
#include "errcode.h"
#include "pinctrl.h"
#include "adc.h"
#include "adc_porting.h"

#define ADC_X_CHANNEL  1 // ADC1 - GPIO_08
#define ADC_Y_CHANNEL  2 // ADC2 - GPIO_09

uint32_t adcx, adcy;

static void thb_adc_callback(uint8_t ch, uint32_t *buffer, uint32_t length, bool *next)
{
  uint32_t sum = 0;
  if (length > 1)
  {
    for (uint32_t i = 0; i < length; i++)
    {
        sum += buffer[i];
    }
    if(ch == ADC_X_CHANNEL){
      adcx = sum / (length - 1);
      // printf("ADC_X vol: %dmv\r\n", adcx);
    }
    if(ch == ADC_Y_CHANNEL) {
      adcy = sum / (length - 1);
      // printf("ADC_Y vol: %dmv\r\n", adcy);
    }
  }
}

// 主线程函数
static void THBCtrlDemo(void *arg)
{
  (void)arg;

  // ADC一次采样需要16个时钟周期,因此采样速率 = adc_clk / 16
  uapi_adc_init(ADC_CLOCK_NONE); // ADC_CLOCK_MAX

  // 上电并启用ADC
  uapi_adc_power_en(AFE_GADC_MODE, true); // 模拟前端ADC常规精度模式

  adc_scan_config_t config = {
      .type = 0,          // FIFO全扫描或阈值扫描
      .threshold_l = 0.5, // 阈值扫描电压(v)下限
      .threshold_h = 4.5, // 阈值扫描电压(v)上限
      .freq = 0,          // ADC扫描频率,用于所有频道
  };

  // 工作循环,每隔10ms获取一次ADC通道的值
  while (1)
  {
      uapi_adc_auto_scan_ch_enable(ADC_X_CHANNEL, config, thb_adc_callback);
      uapi_adc_auto_scan_ch_disable(ADC_X_CHANNEL);

      uapi_adc_auto_scan_ch_enable(ADC_Y_CHANNEL, config, thb_adc_callback);
      uapi_adc_auto_scan_ch_disable(ADC_Y_CHANNEL);

      LOS_Msleep(10);

      if(adcx < 1900){ // 上推
        if(adcy < 1900) { // 左推
          printf("UP LEFT\r\n");
        }else if(adcy > 2100){ // 右推
          printf("UP RIGHT\r\n");
        }else{
          printf("UP\r\n");
        }
      }else if( adcx > 2100){ // 下推
        if(adcy < 1900) { // 左推
          printf("DOWN LEFT\r\n");
        }else if(adcy > 2100){ // 右推
          printf("DOWN RIGHT\r\n");
        }else{
          printf("DOWN\r\n");
        }
      }
  }
}

static void THBControlTask(void)
{
    osThreadAttr_t attr;

    attr.name = "THBCtrlDemo";
    attr.attr_bits = 0U;
    attr.cb_mem = NULL;
    attr.cb_size = 0U;
    attr.stack_mem = NULL;
    attr.stack_size = 1024 * 10; 
    attr.priority = osPriorityNormal;
    // 报错
    if (osThreadNew((osThreadFunc_t)THBCtrlDemo, NULL, &attr) == NULL) {
        printf("[THBControlTask] Failed to create THBTask!\n");
    }
}

SYS_RUN(THBControlTask);
(3)编写配置文件

BUILD.gn 的代码如下:

cmake 复制代码
# 代码 3.7-8

static_library("THBDemo") {
  sources = [
    "thb001p_demo.c"
  ]

  include_dirs = [
    "//commonlibrary/utils_lite/include",
    "//kernel/liteos_m/kal/cmsis",
    "//device/soc/hisilicon/ws63v100/sdk/include/driver",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/drivers/hal/gpio/v150",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/gpio",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/rom/drivers/chips/ws63/porting/pinctrl",
    "//device/soc/hisilicon/ws63v100/sdk/drivers/chips/ws63/porting/adc",
    "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/interrupt",
  ]
}
(4)修改工程配置文件

1) 修改"工程目录"下的 BUILD.gn 文件,在 features 数组中添加 "06_THB001P_DEMO:THBDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "THBDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "THBDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写
  • 使用串口线将 **WS63 石院星闪无线遥控手柄开发板 **的 USB 端口与 PC 机的 USB 端口连接(烧写前请先确认是否安装串口驱动,若没有,请参考第一章的内容完成驱动的安装)
  • 参考"第 1 章 1.4 镜像文件的烧写"一节的内容完成烧写。
(7)运行效果
  • 烧写结束后,打开串口调试助手,选择正确的 COM 编号,设置波特率为 115200,数据位 8 位,并打开串口,按下手柄遥控板上的复位按键后,用手指拨动左边的摇杆开关,能看到串口返回了摇杆位置信息,表明本次功能开发成功。
相关推荐
少年的云河月1 天前
OpenHarmony Camera开发指导(二):相机设备管理(ArkTS)
harmonyos·openharmony·camera·相机开发
少年的云河月2 天前
OpenHarmony 5.0版本视频硬件编解码适配
音视频·harmonyos·视频编解码·openharmony·codec hdi
Industio_触觉智能13 天前
鸿蒙北向开发OpenHarmony5.0 DevEco Studio开发工具安装与配置
harmonyos·鸿蒙系统·openharmony·开源鸿蒙·鸿蒙开发·嵌入式开发板
沧海一笑-dj23 天前
【鸿蒙开发】Hi3861学习笔记- NFC
harmonyos·鸿蒙·openharmony·nfc·海思·鸿蒙开发·hi3861
CrazyMo_23 天前
OpenHarmony 入门——ArkUI 跨页面数据同步和页面级UI状态存储LocalStorage小结(二)
harmonyos·装饰器·openharmony·状态管理·arkui
迅为电子24 天前
【北京迅为】iTOP-RK3568开发板OpenHarmony系统南向驱动开发UART接口运作机制
openharmony·rk3568开发板
蓝白咖啡1 个月前
在制作电脑的过程中,如何区分整机性能问题和应用自身性能问题
openharmony·应用性能·整机性能
沧海一笑-dj1 个月前
【鸿蒙开发】Hi3861学习笔记-Visual Studio Code安装(New)
vscode·harmonyos·鸿蒙·openharmony·鸿蒙开发·hi3861
迅为电子1 个月前
北京迅为RK3568开发板OpenHarmony系统南向驱动开发内核HDF驱动框架架构
openharmony·rk3568开发板