智能呼啦圈原型开发

更新时间Invalid date

概况

智能呼啦圈因其小巧便携、尺寸可调节的特点,以及其为用户带来的多功能体验,深受因工作原因无法去健身房的上班族喜爱。本教程以一款能够记录运动圈数、运动时间以及消耗卡路里,并且能够将三种数据显示在显示屏上的智能呼啦圈为例,将其改造为一款能够记录运动数据,设置运动目标,进行运动倒计时,并能够使用 App 打卡记录的智能呼啦圈。


功能设计
  • App 打卡
  • 记录运动数据
  • 设置运动目标
  • 设置运动倒计时
  • 记录运动状态

功能逻辑
物料详情

拆解图
物料清单
原物料 是否可用 不可用原因
外壳 -
上盖 按键更换,无需使用原上盖。
主板 改为 App 打卡记录。
按键 橡胶按键改为轻触按键,方便采购且能够更好地适配结构。
霍尔开关 -
显示屏 无法找到逻辑走线图。

物料清单

软件 (1)

    步骤

    • 第 1 步:硬件方案设计

      选择设备连接 App 方式

      Wi-Fi 和蓝牙都是通过无线电信号无线发送和接收数据,且手机上基本都有这两项功能。

      Wi-Fi

      • 优点:速度快、范围广。
      • 缺点:耗电量大,且占用手机 Wi-Fi 通道。

      蓝牙

      • 优点:低功耗、低成本,且连接方便。
      • 缺点:速度较慢、信号范围较小。

      由于蓝牙突出的低功耗特性,且完全满足呼啦圈这类运动设备的数据传输和距离要求。我们选择蓝牙作为设备连接手机 App 的连接方式,选用涂鸦智能自主研发的低功耗蓝牙模组作为核心控制器。

      选择液晶屏

      液晶屏驱动芯片

      芯片 优点 缺点
      Bluetooth LE 专用液晶屏驱动芯片 实现显示驱动较为简单,只需通过只需通过 SPI 下发显示的内容数据。 设备的结构空间有限,PCB 空间无法容纳液晶屏、蓝牙模块和驱动芯片。
      MCU 自带 LCD 驱动外设和蓝牙无线通 外围简单。 成本较高。
      使用 Bluetooth LE 的 I/O 口驱动 LCD 对 Bluetooth LE 芯片的外设要求不高,通用低成本的蓝牙芯片就能满足。 只能适用偏压比 1/2 的液晶屏。

      液晶屏的主要参数

      参数名 定义 备注
      工作电压 断码屏驱动的最大允许电压。 LCD 功耗约 10μA。蓝牙模组作为核心控制器,其工作电压在 1.8V~3.6V,所以液晶屏选型优先选择工作电压在这个范围内。
      偏压比(bias) 一般是以最低一档与输出最高电压的比值来表示。偏压比是调节显示的黑色明亮字符和周围液晶点间的对比度。 如 3V 1/3 偏压的液晶屏,其阀值电压是 1V;液晶屏驱动电压(PIN 脚和 COM 脚之间的电压差)越大于阀值电压,液晶屏显示的点越明显。
      占空比(duty ratio) 也称为 COM 数。在一个脉冲循环内,通电时间相对于总时间所占的比例。 由于 STN/TN 的 LCD 一般是采用时分动态扫描的驱动模式,在此模式下,每个 COM 的有效选通时间与整个扫描周期的比值即占空比(Duty)是固定的,等于 1/COM 数。

      本方案所选液晶屏

      根据硬件设计要求,整理好液晶屏的参数,即可设计出断码液晶屏的显示内容。但由于定制费用较高,我们可以直接购买现货。本教程以 1/2 偏压比、3V 供电的 3 位数字显示液晶屏为例。液晶屏及其走线如下图所示:

      I/O 口模拟驱动液晶屏

      本 Demo 采用 Bluetooth LE 芯片自带的 I/O 口模拟驱动芯片来驱动液晶屏幕。

      驱动设置

      1. 在硬件的 COM 和 PIN 口上加上拉、下拉各一个电阻。
      2. 阻值选择 100K。
      3. I/O 口可设置为推挽输出和浮空输入的状态。

      驱动逻辑

      • 上电后,液晶屏驱动的 I/O 口设置为浮空输入,此时分压电阻将液晶屏的引脚电压设置在 1/2 工作电压点,即 1.5V。

      • COM 口时分扫描时,相应的 COM 脚 I/O 口设置为推挽输出,输出恒定时间的高电平和低电平。其他时间段设置为浮空输入。

      • 如某段液晶需要显示,在扫描相应的 COM 口时,对应的 PIN 口同时输出恒定时间的低电平和高电平。

        注意:如果液晶屏长时间单向供电,会造成液晶屏不可恢复的损坏,因此,我们都采用交流供电的方式驱动。

        在显示屏上显示数字 1 输出波形如下图所示:

      指示灯与按键

      在液晶屏旁增加三个指示灯,分别显示时间值、计数值以及能量值。在显示屏下方的左右两侧分别有两个按键,用作模式选择和数据复位的功能。

      说明

      • T 显示时间值。
      • C 显示计数值。
      • J 显示能量值。
      • 左边按键用于模式选择。
      • 右边按键用于数据复位。

      原理图与 PCB

      原理图

      主控板

      PCB 图

      实物图

      电源板

      结构空间有限,采用叠层的方式,将电源板放在主控板的后面。

      PCB 图

      实物图

      上壳图

      3D 建模图


      实物图

    • 第 2 步:创建产品

      第 1 步 创建产品

      1. 登录涂鸦 IoT 开发平台
      2. 在平台首页,单击 创建产品
      3. 下拉网页界面,在 标准类目 左侧栏中单击 找不到品类? 创建呼啦圈产品。
      4. 填入 产品名称产品描述
      5. 选择通讯协议为 蓝牙
      6. 单击 创建产品

      第 2 步 功能定义

      产品创建后,进入到开发界面,可以看到左上角有产品的 PID,我们将在接下来的开发环境搭建中用到它。

      1. 单击添加功能进行功能定义,本案例的功能点定义如下图所示。

      第 3 步 配置面板

      根据喜好选择一种面板进行编辑,编辑完成后可以按照提示使用涂鸦智能 App 扫码体验手机控制。

      第 4 步 硬件开发

      1. 选择 涂鸦标准模组 SDK 开发BT3L Bluetooth 模组

      2. 单击 采购模组 下方的 免费领取 10 个激活码 即可获取该产品对应的 10 组激活码,其中包含 UUIDauthkeyMAC 地址

    • 第 3 步:搭建开发环境

      本案例使用涂鸦 BLE SDK 和 Telink 芯片平台 TLSR825x 进行开发,下面我们开始搭建开发环境。

      1. 获取 BLE SDK。

      下载 TLSR825x 对应的 BLE SDK Demo:tuya_ble_sdk_Demo_Project_tlsr8253.git
      我们将在\ble_sdk_multimode\tuya_ble_app中进行智能呼啦圈应用代码的编写。

      1. 下载与安装 IDE。

        下载 Telink 官方 IDE 并安装:Eclipse (IDE for TLSR8 Chips)

        注意:IDE 必须安装在 C 盘。

      2. 修改代码并编译。

        将代码导入 Eclipse,可以直接在 Eclipse 进行代码修改,也可以先使用自己熟悉的代码编辑器。

        1. 修改 PID。

        tuya_ble_app_demo.h 中填入创建好的智能呼啦圈的 PID:

        #define APP_PRODUCT_ID      "xxxxxxxx"
        
        1. 修改 uuid 、Auth Key、Mac 地址。

        tuya_ble_app_demo.c 填入申请的激活码(取自下载的 excel文件)。

        static const char auth_key_test[] = "yyyyyyyy";
        static const char device_id_test[] = "zzzzzzzz";
        static const uint8_t mac_test[6] = {0x12, 0x34, 0x56, 0x4D, 0x23, 0xDC}; // The actual MAC address is : DC:23:4D:56:34:12
        
        1. 修改以下代码使授权码生效。

        tuya_ble_app_demo.c 中找到 tuya_ble_app_init() 函数,将 device_param.device_id_len = 0; 改为 device_param.device_id_len = 16;(可以参考此行代码的注释)。

        1. 修改日志口(根据调试需要修改)。

        vendor\8258_module\app_config.h 中将日志口修改为 GPIO_PD7

        #define DEBUG_INFO_TX_PIN          GPIO_PD7    /* 日志口,下面四行宏名同步修改 */
        #define PD7_FUNC                   AS_GPIO
        #define PD7_INPUT_ENABLE           0
        #define PD7_OUTPUT_ENABLE          1
        #define PD7_DATA_OUT               1
        
        1. 编译代码

        使用 Eclipse 对代码进行编译,输出文件目录为ble_sdk_multimode\8258_module\8258_module.bin。编译前需修改工程配置中的头文件包含路径,根据 SDK 中的文件夹名称进行相应修改,修改方法参考下图:

      3. 下载并安装烧录工具。

        下载 Telink 官方烧录工具并安装:Burning and Debugging Tools for all Series

      4. 下载并调试程序。

        1. 按下图将电路板与 Telink 烧录器连接:

        1. 打开 BDT,按下图步骤进行芯片选择、打开文件、下载、复位(Tips:如果需要烧录的文件路径不变,重新编译后不需要重复”打开文件“这一步;如果下载时提示“Swire err”可以点击“SWS”进行刷新):

        1. 程序载入后可以使用串口调试工具进行日志查看,按照之前修改的日志口进行连接,波特率选择 230400,复位后将看到以下日志输出,包括我们之前修改的 PID、uuid 、auth key、mac 地址,以及版本号等其他信息:

        程序启动后,我们还可以使用涂鸦智能 App 搜索到我们的设备并进行绑定,可以看到设备名称显示为我们创建的产品名称“智能呼啦圈”。至此,智能呼啦圈 Demo 开发前的准备工作就完成了。

    • 第 4 步:软件方案设计

      完整 Demo 可访问 智能呼啦圈仓库 获取。

      总体设计

      功能需求

      智能呼啦圈 demo 的功能需求列表如下:

      功能 需求描述
      模式选择
      • 运动模式:普通模式(默认)、目标模式。
      • 操作:短按模式键选择模式,长按≥2s 确认模式。
      • 段码液晶屏:普通模式显示 010,目标模式显示 020
      • App:下发模式选择指令。
      目标设定
      • 当日运动目标设置最长时间不超过 45 分钟。
      • 当月运动目标设置最长时间不超过 1,350 分钟(45 分钟*30 天)。
      • App:下发目标数据。
      目标完成提醒 段码液晶屏显示 并闪烁 3 次即为提醒。
      智能计数
      • 支持运动时间(min)、圈数(圈)、卡路里(kcal)累计。
      • 设备转动情况下计数,实时数据>999 后重新开始计数。
      运动数据显示
      • 段码液晶屏:高位为 0 时不显示。
      • 普通模式:时长、圈数、卡路里数据轮流显示,每间隔 5 分钟显示一轮;
      • 目标模式:显示时长为 6 秒,每间隔 5 分钟显示一次。
      • App:接收本地数据并显示。
      运动数据记录 本地记录 30 天内累计运动数据(时长、圈数、卡路里)。
      数据指示 本时长、圈数、卡路里指示灯根据当前屏显数据依次点亮。
      屏幕状态更新 开始转动时屏幕亮起,停止转动 30s 后屏幕熄灭。
      设备配网
      • 设备连接电源时可配网,1 分钟后若未被用户绑定,禁止配网。
      • 模式键:长按≥5 秒,允许设备配网,1 分钟后若未被用户绑定,禁止配网。
      • 配网提醒:配网(时长)指示灯快闪。
      数据上报 本地上报模式数据、运动数据至 App 显示。
      设备复位
      • 备复位键长按≥2s,清空所有运动数据(恢复原始状态);
      • 段码液晶屏显示000并闪烁 3 次。

      模块划分

      对以上功能需求进行分析梳理后,确定本智能呼啦圈教程程序可划分为以下七大模块:

      No. 模块 处理内容
      1 外设驱动组件 按键、霍尔传感器、指示灯、段码液晶屏的驱动程序。
      2 设备基础服务 设备状态迁移处理、模式切换、本地定时等。
      3 显示处理服务 段码液晶屏显示内容更新和状态控制、LED 状态控制 。
      4 数据处理服务 运动数据(计时、圈数、卡路里)的更新和存储。
      5 用户事件处理 按键事件检测和处理、霍尔传感器事件检测和处理。
      6 定时事件处理 各定时事件的判断和处理。
      7 联网相关处理 配网相关处理、数据上报与接收处理、云端时间获取。

      代码结构

      根据模块层级关系,设定代码文件结构如下,后续将分别介绍外设驱动模块和应用层六大功能模块:

      ├── src         /* 源文件目录 */
      |    ├── common
      |    |    └── tuya_local_time.c                 /* 本地定时 */
      |    ├── sdk
      |    |    └── tuya_uart_common_handler.c        /* UART 通用对接实现代码 */
      |    ├── driver
      |    |    ├── tuya_hall_sw.c                    /* 霍尔开关驱动 */
      |    |    ├── tuya_key.c                        /* 按键驱动 */
      |    |    ├── tuya_led.c                        /* 指示灯驱动 */
      |    |    └── tuya_seg_lcd.c                    /* 段码液晶屏驱动 */
      |    ├── platform
      |    |    ├── tuya_gpio.c                       /* GPIO 驱动 */
      |    |    └── tuya_timer.c                      /* Timer 驱动 */
      |    ├── tuya_ble_app_demo.c                    /* 应用层入口文件 */
      |    ├── tuya_hula_hoop_ble_proc.c              /* 呼啦圈联网相关处理 */
      |    ├── tuya_hula_hoop_evt_user.c              /* 呼啦圈用户事件处理 */
      |    ├── tuya_hula_hoop_evt_timer.c             /* 呼啦圈定时事件处理 */
      |    ├── tuya_hula_hoop_svc_basic.c             /* 呼啦圈基础服务 */
      |    ├── tuya_hula_hoop_svc_data.c              /* 呼啦圈数据服务 */
      |    ├── tuya_hula_hoop_svc_disp.c              /* 呼啦圈显示服务 *
      |    └── tuya_hula_hoop.c                       /* 呼啦圈 demo 入口 */
      |
      └── include     /* 头文件目录 */
           ├── common
           |    ├── tuya_common.h                     /* 通用类型和宏定义 */
           |    └── tuya_local_time.h                 /* 本地定时 */
           ├── sdk
           |    ├── custom_app_uart_common_handler.h  /* UART 通用对接实现代码 */
           |    ├── custom_app_product_test.h         /* 自定义产测项目相关实现 */
           |    └── custom_tuya_ble_config.h          /* 应用配置文件 */
           ├── driver
           |    ├── tuya_hall_sw.h                    /* 霍尔开关驱动 */
           |    ├── tuya_key.h                        /* 按键驱动 */
           |    ├── tuya_led.h                        /* 指示灯驱动 */
           |    └── tuya_seg_lcd.h                    /* 段码液晶屏驱动 */
           ├── platform
           |    ├── tuya_gpio.h                       /* GPIO 驱动 */
           |    └── tuya_timer.h                      /* Timer 驱动 */
           ├── tuya_ble_app_demo.h                    /* 应用层入口文件 */
           ├── tuya_hula_hoop_ble_proc.h              /* 呼啦圈联网相关处理 */
           ├── tuya_hula_hoop_evt_user.h              /* 呼啦圈用户事件处理 */
           ├── tuya_hula_hoop_evt_timer.h             /* 呼啦圈定时事件处理 */
           ├── tuya_hula_hoop_svc_basic.h             /* 呼啦圈基础服务 */
           ├── tuya_hula_hoop_svc_data.h              /* 呼啦圈数据服务 */
           ├── tuya_hula_hoop_svc_disp.h              /* 呼啦圈显示服务 */
           └── tuya_hula_hoop.h                       /* 呼啦圈 demo 入口 */
      

      软件框图

      外设驱动

      为方便后续程序扩展,可以先将各外设驱动部分的代码分别编写成组件。本教程所使用外设的基本情况如下:

      外设 数量&规格 驱动方式
      按键 2 个 开关量检测
      霍尔传感器 1 个;开关型 开关量检测
      发光二极管 3 个;红色 电平或 PWM 驱动
      段码液晶屏 1 个;3 位数字8;4 个 COM 口,6 个 SEG 口 COM 口和 SEG 口两端施加一定压差的交流电,压差大于阈值时对应笔段点亮。

      按键&霍尔驱动

      按键

      按键组件功能设置:

      <1> 可在初期注册按键信息,包括引脚、有效电平、长按时间(2 种可选)、按键触发时响应的回调函数;
      <2> 可检测按键触发事件,包括短按、长按(2 种可选),并在按键确认触发时,执行用户设置的回调函数;
      <3> 可实现多个按键同时检测;
      
      1. 实现按键功能的初始化

        /* 第一步:定义供用户注册按键信息使用的结构体类型 (tuya_key.h) */
        /* 按键事件类型 */
        typedef BYTE_T KEY_PRESS_TYPE_E;
        #define SHORT_PRESS             0x00	/* 短按 */
        #define LONG_PRESS_FOR_TIME1    0x01    /* 长按超过时间 1 */
        #define LONG_PRESS_FOR_TIME2    0x02	/* 长按超过时间 2 */
        /* 按键回调函数类型 */
        typedef VOID_T (*KEY_CALLBACK)(KEY_PRESS_TYPE_E type);
        /* 按键注册信息类型 */
        typedef struct {
            TY_GPIO_PORT_E port;        		/* 端口 */
            BOOL_T active_low;          		/* 有效电平 (1-低电平有效,0-高电平有效) */
            UINT_T long_press_time1;    		/* 长按时间 1 (ms) */
            UINT_T long_press_time2;    		/* 长按时间 2 (ms) */
            KEY_CALLBACK key_cb;        		/* 触发按键回调函数 */
        } KEY_DEF_T;
        
        /* 第二步:定义用来存储按键状态的结构体类型 (tuya_key.c) */
        typedef struct {
            BOOL_T cur_stat;            		/* 今回状态 */
            BOOL_T prv_stat;            		/* 前回状态 */
            UINT_T cur_time;            		/* 今回计时 */
            UINT_T prv_time;            		/* 前回计时 */
        } KEY_STATUS_T;
        
        /* 第三步:定义用于按键信息管理的结构体类型和该类型的指针 (tuya_key.c) */
        typedef struct key_manage_s {
            struct key_manage_s *next;			/* 下一个按键信息存储的地址,实现多按键检测 */
            KEY_DEF_T *key_def_s;
            KEY_STATUS_T key_status_s;
        } KEY_MANAGE_T;
        STATIC KEY_MANAGE_T *sg_key_mag_list = NULL;
        
        /* 第四步:定义按键错误信息代码 (tuya_key.h) */
        typedef BYTE_T KEY_RET;
        #define KEY_OK                  0x00
        #define KEY_ERR_MALLOC_FAILED   0x01
        #define KEY_ERR_CB_UNDEFINED    0x02
        
        /* 第五步:编写按键注册函数,包括对按键的初始化工作 (tuya_key.c) */
        STATIC VOID_T __key_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
        {
            tuya_gpio_init(port, TRUE, active_low);
        }
        KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def)
        {
            /* 检查是否定义了回调函数,未定义则返回错误信息 */
            if (key_def->key_cb == NULL) {
                return KEY_ERR_CB_UNDEFINED;
            }
            /* 为 key_mag 分配空间并初始化,分配失败则返回错误信息 */
            KEY_MANAGE_T *key_mag = (KEY_MANAGE_T *)tuya_ble_malloc(SIZEOF(KEY_MANAGE_T));
            if (NULL == key_mag) {
                return KEY_ERR_MALLOC_FAILED;
            }
            /* 记录用户设置的按键信息,并存放至按键管理列表 */
            key_mag->key_def_s = key_def;
            if (sg_key_mag_list) {
                key_mag->next = sg_key_mag_list;
            }
            sg_key_mag_list = key_mag;
            /* 根据用户设置的有效电平对引脚进行初始化 */
            __key_gpio_init(key_def->port, key_def->active_low);
            /* 返回成功信息 */
            return KEY_OK;
        }
        
        /* 第六步:在头文件中定义按键注册接口 (tuya_key.h) */
        KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def);
        
      2. 完成了按键初始化工作之后,需要实现按键事件的检测和处理。基本思路是每 10ms 检测一次每个按键的状态,并判断是否满足了按键触发事件的条件,标记事件的类型,然后执行对应的回调函数:

        ```c
        /* 第一步:定义相关参数值 (tuya_key.c) */
        #define KEY_SCAN_CYCLE_MS       10	/* 扫描周期 */
        #define KEY_PRESS_SHORT_TIME    50	/* 短按确认时间 */
        
        /* 第二步:编写用于更新单个按键状态的相关函数 (tuya_key.c) */
        /* 获取按键实时状态,1-按压,0-释放 */
        STATIC BOOL_T __get_key_stat(IN CONST TY_GPIO_PORT_E port, IN CONST UCHAR_T active_low)
        {
            BOOL_T key_stat;
            if (active_low) {
                key_stat = tuya_gpio_read(port) == 0 ? TRUE : FALSE;
            } else {
                key_stat = tuya_gpio_read(port) == 0 ? FALSE : TRUE;
            }
            return key_stat;
        }
        /* 更新按键状态 */
        STATIC VOID_T __update_key_status(INOUT KEY_MANAGE_T *key_mag)
        {
            BOOL_T key_stat;
            /* 保存前回状态 */
            key_mag->key_status_s.prv_stat = key_mag->key_status_s.cur_stat;
            key_mag->key_status_s.prv_time = key_mag->key_status_s.cur_time;
            /* 获取实时状态 */
            key_stat = __get_key_stat(key_mag->key_def_s->pin, key_mag->key_def_s->active_low);
            /* 更新今回状态 */
            if (key_stat != key_mag->key_status_s.cur_stat) {
                key_mag->key_status_s.cur_stat = key_stat;
                key_mag->key_status_s.cur_time = 0;
            } else {
                key_mag->key_status_s.cur_time += KEY_SCAN_CYCLE_MS;
            }
        }
        
        /* 第三步:编写用于判断单个按键事件的相关函数 (tuya_key.c) */
        /* 判断按键保持按压状态的时间是否达到了 over_time */
        STATIC BOOL_T __is_key_press_over_time(IN CONST KEY_STATUS_T key_status_s, IN CONST UINT_T over_time)
        {
            if (key_status_s.cur_stat == TRUE) {
                if ((key_status_s.cur_time >= over_time) &&
                    (key_status_s.prv_time < over_time)) {
                    return TRUE;
                }
            }
            return FALSE;
        }
        /* 判断按键从开始被按压到被释放经过的时间是否达到了 over_time 并且少于 less_time */
        STATIC BOOL_T __is_key_release_to_release_over_time_and_less_time(IN CONST KEY_STATUS_T key_status_s, IN CONST UINT_T over_time, IN CONST UINT_T less_time)
        {
            if ((key_status_s.prv_stat == TRUE) &&
                (key_status_s.cur_stat == FALSE)) {
                if ((key_status_s.prv_time >= over_time) &&
                    (key_status_s.prv_time < less_time)) {
                    return TRUE;
                }
            }
            return FALSE;
        }
        /* 判断与处理按键事件 */
        STATIC VOID_T __detect_and_handle_key_event(INOUT KEY_MANAGE_T *key_mag)
        {
            KEY_PRESS_TYPE_E type;
            BOOL_T time_exchange;
            UINT_T long_time1, long_time2;
        
            /* 比较用户设置的长按时间 1 和长按时间 2 的大小,并标记是否交换 */
            if (key_mag->key_def_s->long_press_time2 >= key_mag->key_def_s->long_press_time1) {
                long_time1 = key_mag->key_def_s->long_press_time1;
                long_time2 = key_mag->key_def_s->long_press_time2;
                time_exchange = FALSE;
            } else {
                long_time1 = key_mag->key_def_s->long_press_time2;
                long_time2 = key_mag->key_def_s->long_press_time1;
                time_exchange = TRUE;
            }
            /* 判断按键状态,标记事件类型并跳转到 KEY_EVENT (根据长按时间设置情况使用对应的判断方式) */
            if ((long_time2 != 0) && (long_time1 != 0)) {
                if (__is_key_press_over_time(key_mag->key_status_s, long_time2)) {
                    type = LONG_PRESS_FOR_TIME2;
                    goto KEY_EVENT;
                }
                if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, long_time1, long_time2)) {
                    type = LONG_PRESS_FOR_TIME1;
                    goto KEY_EVENT;
                }
                if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time1)){
                    type = SHORT_PRESS;
                    goto KEY_EVENT;
                }
            } else if ((long_time2 != 0) && (long_time1 == 0)) {
                if (__is_key_press_over_time(key_mag->key_status_s, long_time2)) {
                    type = LONG_PRESS_FOR_TIME2;
                    goto KEY_EVENT;
                }
                if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time2)){
                    type = SHORT_PRESS;
                    goto KEY_EVENT;
                }
            } else if ((long_time2 == 0) && (long_time1 != 0)) {
                if (__is_key_press_over_time(key_mag->key_status_s, long_time1)) {
                    type = LONG_PRESS_FOR_TIME1;
                    goto KEY_EVENT;
                }
                if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time1)){
                    type = SHORT_PRESS;
                    goto KEY_EVENT;
                }
            } else {
                if (__is_key_press_over_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME)) {
                    type = SHORT_PRESS;
                    goto KEY_EVENT;
                }
            }
            return;
            /* 处理按键事件 */
        KEY_EVENT:
            /* 如果在判断前进行了时间参数的交换,则将标记的事件类型进行交换 */
            if (time_exchange) {
                if (type == LONG_PRESS_FOR_TIME2) {
                    type = LONG_PRESS_FOR_TIME1;
                } else if (type == LONG_PRESS_FOR_TIME1) {
                    type = LONG_PRESS_FOR_TIME2;
                } else {
                    ;
                }
            }
            /* 执行用户设置的回调函数 */
            key_mag->key_def_s->key_cb(type);
        }
        
        /* 第四步:编写 10ms 处理函数 (tuya_key.c) */
        STATIC INT_T __key_timeout_handler(VOID_T)
        {
            /* 获取按键信息管理列表,无按键注册则返回 */
            KEY_MANAGE_T *key_mag_tmp = sg_key_mag_list;
            if (NULL == key_mag_tmp) {
                return 0;
            }
            /* 循环处理每个按键 */
            while (key_mag_tmp) {
                __update_key_status(key_mag_tmp);			/* 更新按键状态 */
                __detect_and_handle_key_event(key_mag_tmp);	/* 判断并处理按键事件 */
                key_mag_tmp = key_mag_tmp->next;			/* 加载下一个按键信息 */
            }
            return 0;
        }
        
        /* 第五步:在按键注册函数中创建定时器 (tuya_key.c) */
        KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def)
        {
            ...
            if (sg_key_mag_list) {
                key_mag->next = sg_key_mag_list;
            } else {	/* 注册第一个按键时创建 10ms 定时器,注册回调函数 */
                tuya_software_timer_create(KEY_SCAN_CYCLE_MS*1000, __key_timeout_handler);
            }
            ...
        }
        ```
        
      • 霍尔传感器

        这次使用的霍尔传感器是开关型的,一般情况下也可视为按键处理。考虑到呼啦圈快速转动时霍尔传感器与磁铁的接触时间较短,并且上述按键驱动组件使用了 10ms 软件定时器进行处理,易被外部程序执行时间影响,可能会导致呼啦圈计数漏检的情况,所以这里我们采取外部中断的方式处理:

        /* 【tuya_hall_sw.h】 */
        /* 错误信息代码 */
        typedef BYTE_T HSW_RET;
        #define HSW_OK                  0x00
        #define HSW_ERR_MALLOC_FAILED   0x01
        #define HSW_ERR_CB_UNDEFINED    0x02
        /* 霍尔开关数据结构 */
        typedef VOID_T (*HALL_SW_CALLBACK)();
        typedef struct {
            TY_GPIO_PORT_E port;            /* 端口 */
            BOOL_T active_low;              /* 有效电平 */
            HALL_SW_CALLBACK hall_sw_cb;    /* 触发时回调函数 */
            UINT_T invalid_intv;            /* 两次触发间隔如果小于该时间则无效 */
        } HALL_SW_DEF_T;
        
        /* 【tuya_hall_sw.c】 */
        /* 霍尔开关信息管理 */
        typedef struct hall_sw_manage_s {
            struct hall_sw_manage_s *next;
            HALL_SW_DEF_T *def;
            UINT_T wk_tm;
        } HALL_SW_MANAGE_T;
        STATIC HALL_SW_MANAGE_T *sg_hsw_mag_list = NULL;
        
        /* 霍尔开关引脚初始化 */
        STATIC VOID_T __hall_sw_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
        {
            tuya_gpio_init(port, TRUE, active_low);
            if (active_low) {
                tuya_gpio_irq_init(port, TY_GPIO_IRQ_FALLING, __hall_sw_irq_handler);
            } else {
                tuya_gpio_irq_init(port, TY_GPIO_IRQ_RISING, __hall_sw_irq_handler);
            }
        }
        
        /* 霍尔开关注册 */
        HSW_RET tuya_reg_hall_sw(IN HALL_SW_DEF_T *hsw_def)
        {
            /* 检查是否定义了回调函数,未定义则返回错误信息 */
            if (hsw_def->hall_sw_cb == NULL) {
                return HSW_ERR_CB_UNDEFINED;
            }
            /* 为 hall_sw_mag 分配空间并初始化,分配失败则返回错误信息 */
            HALL_SW_MANAGE_T *hall_sw_mag = (HALL_SW_MANAGE_T *)tuya_ble_malloc(SIZEOF(HALL_SW_MANAGE_T));
            if (NULL == hall_sw_mag) {
                return HSW_ERR_MALLOC_FAILED;
            }
            hall_sw_mag->def = hsw_def;
            /* 记录用户设置的霍尔开关信息,并存放至霍尔开关管理列表 */
            if (sg_hsw_mag_list) {
                hall_sw_mag->next = sg_hsw_mag_list;
            }
            sg_hsw_mag_list = hall_sw_mag;
            /* 引脚初始化 */
            __hall_sw_gpio_init(hsw_def->port, hsw_def->active_low);
        
            return HSW_OK;
        }
        
        /* 霍尔开关触发时处理 */
        STATIC VOID_T __hall_sw_trigger_handler(IN HALL_SW_MANAGE_T *hsw_mag)
        {
            /* 两次触发间隔检查 */
            if (!tuya_is_clock_time_exceed(hsw_mag->wk_tm, hsw_mag->def->invalid_intv)) {
                return;
            }
            hsw_mag->wk_tm = tuya_get_clock_time();
            /* 执行用户设置的回调函数 */
            hsw_mag->def->hall_sw_cb();
        }
        
        /* 霍尔开关外部中断回调 */
        STATIC VOID_T __hall_sw_irq_handler(TY_GPIO_PORT_E port)
        {
            HALL_SW_MANAGE_T *hsw_mag_tmp = sg_hsw_mag_list;
            while (hsw_mag_tmp) {
                if (hsw_mag_tmp->def->port == port) {
                    __hall_sw_trigger_handler(hsw_mag_tmp);
                    break;
                }
                hsw_mag_tmp = hsw_mag_tmp->next;
            }
        }
        

      发光二极管驱动

      发光二极管组件功能设置:

      <1> 可在初期注册 LED 信息,包括引脚、有效电平;
      <2> 可控制 LED 点亮或熄灭或闪烁;
      <3> 可设置 LED 闪烁模式,包括闪烁方式(指定时长/指定次数/永远闪烁)、闪烁开始时和闪烁结束后的状态、点亮阶段的时间、熄灭阶段的时间、闪烁结束时的回调函数;
      <4> 可实现多个 LED 同时控制;
      

      这次没有设置亮度调节、呼吸灯控制等功能,因此只需要使用电平驱动方式。

      1. 实现初始化:

        /* 第一步:定义 LED 句柄,用户通过该句柄来控制单个 LED (tuya_led.h) */
        typedef VOID_T *LED_HANDLE;
        
        /* 第二步:定义初期需注册的 LED 驱动相关信息 (tuya_led.c) */
        typedef struct {
            TY_GPIO_PORT_E pin;	/* LED 引脚 */
            BOOL_T active_low;  /* LED 有效电平 (1-低电平点亮,0-高电平点亮) */
        } LED_DRV_T;
        
        /* 第三步:定义 LED 信息管理列表 (tuya_led.c) */
        typedef struct led_manage_s {
            struct led_manage_s *next;
            LED_DRV_T drv_s;
        } LED_MANAGE_T;
        STATIC LED_MANAGE_T *sg_led_mag_list = NULL;/* 下一个 LED 信息存储的地址,实现多 LED 控制 */
        
        /* 第四步:编写 LED 引脚初始化函数和 LED 注册函数 (tuya_led.c) */
        STATIC VOID_T __led_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
        {
            tuya_gpio_init(port, FALSE, active_low);
        }
        LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle)
        {
            /* 检查句柄,未指定则返回错误参数 */
            if (NULL == handle) {
                return LED_ERR_INVALID_PARM;
            }
            /* 为 led_mag 分配空间并初始化,分配失败则返回错误信息 */
            LED_MANAGE_T *led_mag = (LED_MANAGE_T *)tuya_ble_malloc(SIZEOF(LED_MANAGE_T));
            if (NULL == led_mag) {
                return LED_ERR_MALLOC_FAILED;
            }
            /* 记录用户设置的 LED 信息,并存放至 LED 管理列表,同时将存储地址 */
            led_mag->drv_s.pin = pin;
            led_mag->drv_s.active_low = active_low;
            *handle = (LED_HANDLE)led_mag;
            if (sg_led_mag_list) {
                led_mag->next = sg_led_mag_list;
            }
            sg_led_mag_list = led_mag;
            /* 根据用户设置的有效电平对引脚进行初始化 (注册时默认不点亮) */
            __led_gpio_init(pin, active_low);
            /* 返回成功信息 */
            return LED_OK;
        }
        
        /* 第五步:在头文件中定义 LED 注册接口 (tuya_led.h) */
        LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle);
        

        2.实现亮灭控制,根据设置的有效电平来控制 LED 引脚的输出状态即可:

        /* 第一步:编写 LED 亮灭控制函数 (tuya_led.c) */
        STATIC VOID_T __set_led_light(IN CONST LED_DRV_T drv_s, IN CONST BOOL_T on_off)
        {
            if (drv_s.active_low) {
                tuya_gpio_write(drv_s.pin, !on_off);
            } else {
                tuya_gpio_write(drv_s.pin, on_off);
            }
        }
        LED_RET tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off)
        {
            LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
            __set_led_light(led_mag->drv_s, on_off);
            return LED_OK;
        }
        
        /* 第二步:在头文件中定义 LED 亮灭控制接口 (tuya_led.h) */
        LED_RET tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off);
        

        3.实现 LED 闪烁控制,闪烁功能将在用户配置闪烁参数后开启,每 100ms 处理一次:

        /* 第一步:定义用于管理 LED 闪烁信息的结构体类型 (tuya_led.c) */
        typedef struct {
            LED_FLASH_MODE_E mode;          /* 闪烁方式 */
            LED_FLASH_TYPE_E type;          /* 闪烁类型 */
            USHORT_T on_time;               /* 点亮阶段时间 */
            USHORT_T off_time;              /* 熄灭阶段时间 */
            UINT_T total;                   /* 指定的时间或指定的次数 */
            LED_CALLBACK end_cb;            /* 闪烁结束时执行的回调函数 */
            UINT_T work_timer;              /* 闪烁工作用计时变量 */
        } LED_FLASH_T;
        
        /* 第二步:在头文件对闪烁方式和闪烁类型的可选项进行定义,用户配置时可直接使用这些宏 (tuya_led.h) */
        /* 闪烁方式 */
        typedef BYTE_T LED_FLASH_MODE_E;
        #define LFM_SPEC_TIME           0x00    /* 闪烁指定时间 */
        #define LFM_SPEC_COUNT          0x01    /* 闪烁指定次数 */
        #define LFM_FOREVER             0x02    /* 永远闪烁 */
        /* 闪烁类型 */
        typedef BYTE_T LED_FLASH_TYPE_E;
        #define LFT_STA_ON_END_ON       0x00    /* 开始时:亮;结束后:亮 */
        #define LFT_STA_ON_END_OFF      0x01    /* 开始时:亮;结束后:灭 */
        #define LFT_STA_OFF_END_ON      0x02    /* 开始时:灭;结束后:亮 */
        #define LFT_STA_OFF_END_OFF     0x03    /* 开始时:灭;结束后:灭 */
        
        /* 第三步:将 LED 闪烁信息加入 LED 管理,同时定义闪烁结束处理相关的变量 (tuya_led.c) */
        typedef struct led_manage_s {
            struct led_manage_s *next;
            LED_DRV_T drv_s;
            LED_FLASH_T *flash;
            BOOL_T stop_flash_req;          /* 停止闪烁请求 */
            BOOL_T stop_flash_light;        /* 停止闪烁后的亮灭状态 */
        } LED_MANAGE_T;
        
        /* 第四步:编写以下函数用于解析用户配置,方便闪烁处理时使用 (tuya_led.c) */
        /* 获取闪烁开始时的亮灭状态,1-亮,0-灭 */
        STATIC BOOL_T __get_led_flash_sta_light(IN CONST LED_FLASH_TYPE_E type)
        {
            BOOL_T ret = TRUE;
            switch (type) {
            case LFT_STA_ON_END_ON:
            case LFT_STA_ON_END_OFF:
                ret = TRUE;
                break;
            case LFT_STA_OFF_END_ON:
            case LFT_STA_OFF_END_OFF:
                ret = FALSE;
                break;
            default:
                break;
            }
            return ret;
        }
        /* 获取闪烁结束后的亮灭状态,1-亮,0-灭 */
        STATIC BOOL_T __get_led_flash_end_light(IN CONST LED_FLASH_TYPE_E type)
        {
            BOOL_T ret = TRUE;
            switch (type) {
            case LFT_STA_ON_END_ON:
            case LFT_STA_OFF_END_ON:
                ret = TRUE;
                break;
            case LFT_STA_ON_END_OFF:
            case LFT_STA_OFF_END_OFF:
                ret = FALSE;
                break;
            default:
                break;
            }
            return ret;
        }
        
        /* 第五步:编写 LED 闪烁配置函数 (tuya_led.c) */
        LED_RET tuya_set_led_flash(IN CONST LED_HANDLE handle, IN CONST LED_FLASH_MODE_E mode, IN CONST LED_FLASH_TYPE_E type, IN CONST USHORT_T on_time, IN CONST USHORT_T off_time, IN CONST UINT_T total, IN CONST LED_CALLBACK flash_end_cb)
        {
            LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
            led_mag->stop_flash_req = FALSE;
            if (led_mag->flash == NULL) {
                LED_FLASH_T *led_flash = (LED_FLASH_T *)tuya_ble_malloc(SIZEOF(LED_FLASH_T));
                if (NULL == led_flash) {
                    return LED_ERR_MALLOC_FAILED;
                }
                led_mag->flash = led_flash;
            }
            led_mag->flash->mode = mode;
            led_mag->flash->type = type;
            led_mag->flash->on_time = on_time;
            led_mag->flash->off_time = off_time;
            led_mag->flash->total = total;
            led_mag->flash->work_timer = 0;
            led_mag->flash->end_cb = flash_end_cb;
            __set_led_light(led_mag->drv_s, __get_led_flash_sta_light(type));
            return LED_OK;
        }
        
        /* 第六步:编写 LED 闪烁处理函数 (tuya_led.c) */
        STATIC VOID_T __led_flash_proc(INOUT LED_MANAGE_T *led_mag)
        {
            BOOL_T one_cycle_flag = FALSE;
            UINT_T sum_time;
            BOOL_T start_light;
            USHORT_T start_time;
            /* 解析闪烁配置 */
            sum_time = led_mag->flash->on_time + led_mag->flash->off_time;
            start_light = __get_led_flash_sta_light(led_mag->flash->type);
            start_time = (start_light) ? led_mag->flash->on_time : led_mag->flash->off_time;
            /* 闪烁周期性处理,实现按照指定时间点亮和熄灭 */
            led_mag->flash->work_timer += LED_TIMER_VAL_MS;
            if (led_mag->flash->work_timer >= sum_time) {
                led_mag->flash->work_timer -= sum_time;
                __set_led_light(led_mag->drv_s, start_light);
                one_cycle_flag = TRUE;
            } else if (led_mag->flash->work_timer >= start_time) {
                __set_led_light(led_mag->drv_s, !start_light);
            } else {
                ;
            }
            /* 闪烁倒计时/数处理,闪烁方式为“永远闪烁”时不处理 */
            if (led_mag->flash->mode == LFM_FOREVER) {
                return;
            }
            if (led_mag->flash->mode == LFM_SPEC_TIME) {
                if (led_mag->flash->total > LED_TIMER_VAL_MS) {
                    led_mag->flash->total -= LED_TIMER_VAL_MS;
                } else {
                    led_mag->flash->total = 0;
                }
            } else if (led_mag->flash->mode == LFM_SPEC_COUNT) {
                if (one_cycle_flag) {
                    if (led_mag->flash->total > 0) {
                        led_mag->flash->total--;
                    }
                }
            } else {
                ;
            }
            /* 闪烁结束处理 */
            if (led_mag->flash->total == 0) {
                /* 如果设置了闪烁回调函数,则执行该函数 */
                if (led_mag->flash->end_cb != NULL) {
                    led_mag->flash->end_cb();
                }
                /* 发起停止闪烁请求,并设置停止后的亮灭状态 */
                led_mag->stop_flash_req = TRUE;
                led_mag->stop_flash_light = __get_led_flash_end_light(led_mag->flash->type);
            }
        }
        
        /* 第七步:编写 LED 超时处理函数 (tuya_led.c) */
        STATIC INT_T __led_timeout_handler(VOID_T)
        {
            /* 获取 LED 信息管理列表,无 LED 注册则返回 */
            LED_MANAGE_T *led_mag_tmp = sg_led_mag_list;
            if (NULL == led_mag_tmp) {
                return;
            }
            /* 循环处理每个 LED */
            while (led_mag_tmp) {
                /* 停止闪烁请求处理 */
                if (led_mag_tmp->stop_flash_req) {
                    __set_led_light(led_mag_tmp->drv_s, led_mag_tmp->stop_flash_light);
                    tuya_ble_free((UCHAR_T *)led_mag_tmp->flash);
                    led_mag_tmp->flash = NULL;
                    led_mag_tmp->stop_flash_req = FALSE;
                }
                /* 如果闪烁功能未开启则不处理 */
                if (NULL != led_mag_tmp->flash) {
                    __led_flash_proc(led_mag_tmp);
                }
                /* 加载下一个 LED 信息 */
                led_mag_tmp = led_mag_tmp->next;
            }
            return 0;
        }
        
        /* 第八步:在 LED 注册函数中创建定时器 (tuya_led.c) */
        #define LED_TIMER_VAL_MS 100
        LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle)
        {
            ...
            if (sg_led_mag_list) {
                led_mag->next = sg_led_mag_list;
            } else {	/* 注册第一个 LED 时创建 100ms 定时器,注册回调函数 */
                tuya_software_timer_create(LED_TIMER_VAL_MS*1000, __led_timeout_handler);
            }
            ...
        }
        
        /* 第九步:修改 LED 亮灭控制函数 (可能会存在闪烁未结束时调用了该函数的情况) (tuya_led.c) */
        VOID_T tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off)
        {
            LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
            /* 闪烁未结束时,发起停止闪烁请求,并暂存用户设置的亮灭状态 */
            if (led_mag->flash != NULL) {
                led_mag->stop_flash_req = TRUE;
                led_mag->stop_flash_light = on_off;
            } else {
                __set_led_light(led_mag->drv_s, on_off);
            }
        }
        

      段码液晶屏驱动

      段码液晶屏主要有两种引脚,公共极 COM 和段电极 SEG,当 COM 口和 SEG 口的电压差大于液晶屏饱和电压时就能够点亮对应的笔段。

      注意:压差必须交替变化。

      举例

      • 我们要点亮某个笔段时,只需要保证给其电极两端加的电压差为 3.3V(如 COM1=3.3V,SEG1=0V),并且间隔合适的时间,将这两极的电压反转输出(如 COM1=0V,SEG1=3.3V)。
      • 不点亮某个笔段时,只需要保证给其电极两端加的电压差为 0V(如 COM1=3.3V,SEG1=3.3V),并且间隔合适的时间,将这两极的电压反转输出(如 COM1=0V,SEG1=0V)。

      本 Demo 使用的段码液晶屏是 3 位数字8的样式,有 4 个 COM 口,6 个 SEG 口,其引脚对应笔段如下表所示:

      No. SEG1 SEG2 SEG3 SEG4 SEG5 SEG6
      COM1 3D 2D 1D
      COM2 3C 3E 2C 2E 1C 1E
      COM3 3B 3G 2B 2G 1B 1G
      COM4 3A 3F 2A 2F 1A 1F

      模拟出如下图所示波形对 COM1~COM4 进行动态扫描,再根据要显示的各个笔段对应的 SEG 口,在每个 COM 口的扫描周期内,控制 6 个 SEG 口的输出即可驱动段码液晶屏显示:

      了解了如何驱动段码液晶屏之后即可开始编写驱动组件,结合呼啦圈 demo 的功能需求,组件功能设置如下:

      <1> 可在初期设置 COM 口和 SEG 口的引脚;
      <2> 可设置液晶屏显示内容的方式有数字、字符串、字符、自定义字符;
      <3> 可控制液晶屏点亮或熄灭或闪烁;
      <4> 可设置液晶屏闪烁模式,包括全屏闪烁或位闪烁、闪烁开始时和闪烁结束后的状态、闪烁间隔、闪烁指定次数或永远闪烁、闪烁结束时的回调函数;
      

      由于段码液晶屏要控制的引脚较多,且每个引脚对应的笔段也有所不同,为了能简化代码,我们先来检讨 7 个笔段和 SEG 口输出值的存放方式。根据各数据位笔段的分布规律,我们可以使用 1 字节数据来存储 1 个数据位的段码:

      1 byte bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
      笔段 f a g b e c d -

      那么常用字符就可以定义为:

      CONST UCHAR_T ch_seg_code[] = {
          0xde,       /* 0 */
          0x14,       /* 1 */
          0x7a,       /* 2 */
          0x76,       /* 3 */
          0xb4,       /* 4 */
          0xe6,       /* 5 */
          0xee,       /* 6 */
          0x54,       /* 7 */
          0xfe,       /* 8 */
          0xf6,       /* 9 */
          0x20,       /* - */
          0x00        /*   */
      };
      

      另外,6 个 SEG 口的输出值也可以使用 1 字节数据存储,驱动引脚时就可以通过 for 循环实现:

      1 byte bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
      SEG pin - - SEG6 SEG5 SEG4 SEG3 SEG2 SEG1

      那么 4 个 COM 口对应的 SEG 口输出值就可以定义一个数组来存储:

      #define COM_NUM		4			/* COM 口数量 */
      UCHAR_T seg_pin_code[COM_NUM];	/* 4 组:6 个 SEG 口的输出值,1 字节 code */
      

      在上述定义基础上,如果要让液晶屏显示123,只需要从0x140x7a0x76 中分别取出每个 COM 口对应的段,再存放至seg_pin_code[COM_NUM]中,就可以得到每个 COM 口对应的 SEG 口输出 code:

      bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 SEG pin code
      - - SEG6 SEG5 SEG4 SEG3 SEG2 SEG1 (无效位默认为 0)
      COM1 - - - 0 - 1 - 1 0x05
      COM2 - - 1 0 0 1 1 0 0x26
      COM3 - - 1 0 1 1 1 1 0x2F
      COM4 - - 0 0 1 0 1 0 0x0A

      为了方便取出每个 COM 口对应的笔段,我们再定义一个数组来存储 4 个 COM 口对应的笔段在 1 字节数据中的位置:

      CONST UCHAR_T ch_com_code[COM_NUM] = {
          0x03,       /* COM1: (bit0)-, (bit1)d */
          0x0c,       /* COM2: (bit2)c, (bit3)e */
          0x30,       /* COM3: (bit4)b, (bit5)g */
          0xc0        /* COM4: (bit6)a, (bit7)f */
      };
      

      基于以上设定,SEG pin code 生成函数就可以写为:

      /**
       * @brief 按数据位更新 SEG 引脚输出 code
       * @param[in] seg_code: 以"bit7~bit0: fagbecd-"顺序存储的段码
       * @param[in] digit: 数据位,0 表示最低位
       * @return none
       */
      STATIC VOID_T __generate_seg_pin_output_code(IN CONST UCHAR_T seg_code, IN CONST UCHAR_T digit)
      {
          UCHAR_T i, tmp;
          /* 循环处理 4 个 COM 口 */
          for (i = 0; i < COM_NUM; i++) {
              sg_seg_lcd_mag.seg_pin_code[i] &= ~(ch_com_code[digit]);
              tmp = (seg_code & ch_com_code[i]) >> (i*2);
              sg_seg_lcd_mag.seg_pin_code[i] |= tmp << (digit*2);
          }
      }
      

      接下来编写将数字、字符串、字符和自定义字符转换为 SEG pin code 的函数:

      1. 显示数字

        /* 可显示数字上限 */
        #define SEG_LCD_DISP_MAX_NUM        999
        
        /**
        * @brief 显示数字
        * @param[in] num: 要显示的十进制数字
        * @param[in] high_zero: 高位为“0”时是否显示 0 (1-显示,0-不显示)
        * @return none
        */
        VOID_T tuya_seg_lcd_disp_num(IN CONST USHORT_T num, IN CONST BOOL_T high_zero)
        {
            UCHAR_T i, num_index[SEG_LCD_DISP_DIGIT];
            UCHAR_T disp_digit = SEG_LCD_DISP_DIGIT;
            USHORT_T tmp_num;
        
            /* 检查上限,超过则返回 */
            if (num > SEG_LCD_DISP_MAX_NUM) {
                return;
            }
            /* 计算百位、十位、个位的值 */
            tmp_num = num;
            for (i = 0; i < SEG_LCD_DISP_DIGIT; i++) {
                num_index[i] = tmp_num % 10;
                tmp_num /= 10;
            }
            /* 高位为“0”时处理 */
            if (!high_zero) {
                for (i = (SEG_LCD_DISP_DIGIT-1); i > 0; i--) {
                    if (num_index[i] == 0) {
                        disp_digit--;
                    }
                }
            }
            /* 生成 SEG pin code */
            for (i = 0; i < SEG_LCD_DISP_DIGIT; i++) {
                if (i < disp_digit) {
                    __generate_seg_pin_output_code(ch_seg_code[num_index[i]], i);
                } else {
                    __generate_seg_pin_output_code(0x00, i);
                }
            }
        }
        
      2. 显示字符串

        /* 段码液晶屏数据位个数 */
        #define SEG_LCD_DISP_DIGIT          3
        
        /* 可用字符表,顺序存放与 ch_seg_code[]保持一致 */
        CONST CHAR_T lcd_str_tbl[] = "0123456789- ";
        
        /**
        * @brief 显示字符串
        * @param[in] str: 所有字符串都已经在<lcd_str_tbl>中定义的字符串
        * @return none
        */
        VOID_T tuya_seg_lcd_disp_str(IN CONST CHAR_T *str)
        {
            UINT_T i, len;
            UCHAR_T ch_index[SEG_LCD_DISP_DIGIT];
        
            /* 获取字符串长度 */
            len = strlen(str);
            if (len > SEG_LCD_DISP_DIGIT) {
                len = SEG_LCD_DISP_DIGIT;
            }
            /* 查找每个字符在<lcd_str_tbl>中的位置以取出该字符的段码,并生成 SEG pin code */
            for (i = 0; i < len; i++) {
                ch_index[i] = strchr(lcd_str_tbl, *(str++)) - lcd_str_tbl;
                __generate_seg_pin_output_code(ch_seg_code[ch_index[i]], (SEG_LCD_DISP_DIGIT-1-i));
            }
        }
        
      3. 显示字符

        /**
        * @brief 在指定数据位显示字符
        * @param[in] ch: <lcd_str_tbl>中定义的字符
        * @param[in] digit: 数据位,0 表示最低位
        * @return none
        */
        VOID_T tuya_seg_lcd_disp_ch(IN CONST CHAR_T ch, IN CONST UCHAR_T digit)
        {
            UCHAR_T ch_index;
        
            /* 查找字符在<lcd_str_tbl>中的位置以取出该字符的段码,并生成 SEG pin code */
            ch_index = strchr(lcd_str_tbl, ch) - lcd_str_tbl;
            __generate_seg_pin_output_code(ch_seg_code[ch_index], digit);
        }
        
      4. 显示自定义字符

        /* 自定义字符类型 */
        typedef struct {
            UCHAR_T a : 1;
            UCHAR_T b : 1;
            UCHAR_T c : 1;
            UCHAR_T d : 1;
            UCHAR_T e : 1;
            UCHAR_T f : 1;
            UCHAR_T g : 1;
            UCHAR_T dp : 1;
        } SEG_LCD_CH_T;
        
        /**
        * @brief 在指定数据位显示自定义字符
        * @param[in] cus_ch: 自定字符
        * @param[in] digit: 数据位,0 表示最低位
        * @return none
        */
        VOID_T tuya_seg_lcd_disp_custom_ch(IN CONST SEG_LCD_CH_T cus_ch, IN CONST UCHAR_T digit)
        {
            UCHAR_T i, cus_seg_code, ch_index, tmp;
            UCHAR_T seg_code = 0x00;
            CHAR_T cus_seg_seq[8] = "abcdefg-";
            CHAR_T *seg_seq = "-dcebgaf";
        
            /* 段码转换 */
            memcpy(&cus_seg_code, &cus_ch, 1);
            for (i = 0; i < 8; i++) {
                tmp = cus_seg_code & 0x01;
                ch_index = strchr(seg_seq, cus_seg_seq[i]) - seg_seq;
                seg_code |= (tmp << ch_index);
                cus_seg_code >>= 1;
            }
            /* 生成 SEG pin code */
            __generate_seg_pin_output_code(seg_code, digit);
        }
        
      5. 在头文件中定义接口供用户调用:

        VOID_T tuya_seg_lcd_disp_num(IN CONST USHORT_T num, IN CONST BOOL_T high_zero);
        VOID_T tuya_seg_lcd_disp_str(IN CONST CHAR_T *str);
        VOID_T tuya_seg_lcd_disp_ch(IN CONST CHAR_T ch, IN CONST UCHAR_T digit);
        VOID_T tuya_seg_lcd_disp_custom_ch(IN CONST SEG_LCD_CH_T cus_ch, IN CONST UCHAR_T digit);
        

      完成了显示内容的转换,我们就可以在引脚驱动时使用seg_pin_code[COM_NUM]来控制 SEG 口的输出了。

      1. 先来处理段码液晶屏的初始化工作,即各引脚的初期配置,但具体使用哪些引脚由用户定义后传入组件:

        /* 第一步:定义段码液晶屏引脚类型 (tuya_seg_lcd.h) */
        typedef struct {
            TY_GPIO_PORT_E com[COM_NUM];    /* COM 口: COM1-COM4 */
                                            /* COM1: -, d */
                                            /* COM2: c, e */
                                            /* COM3: b, g */
                                            /* COM4: a, f */
            TY_GPIO_PORT_E seg[SEG_NUM];    /* SEG 口: SEG1-SEG6 */
                                            /* SEG1: --, 3c, 3b, 3a */
                                            /* SEG2: 3d, 3e, 3g, 3f */
                                            /* SEG3: --, 2c, 2b, 2a */
                                            /* SEG4: 2d, 2e, 2g, 2f */
                                            /* SEG5: --, 1c, 1b, 1a */
                                            /* SEG6: 1d, 1e, 1g, 1f */
        } SEG_LCD_PIN_T;
        
        /* 第二步:定义用于段码液晶屏信息管理的结构体类型和该类型的指针 (tuya_seg_lcd.c) */
        typedef struct {
            SEG_LCD_PIN_T pin;              /* 引脚管理 */
            UCHAR_T seg_pin_code[COM_NUM];  /* SEG pin code */
            BOOL_T light;                   /* 亮灭状态 */
        } SEG_LCD_MANAGE_T;
        STATIC SEG_LCD_MANAGE_T sg_seg_lcd_mag;
        
        /* 第三步:编写段码液晶屏引脚控制相关函数,方便后续调用 (tuya_seg_lcd.c) */
        /* 初始化段码液晶屏引脚 */
        STATIC VOID_T __seg_lcd_gpio_init(IN CONST TY_GPIO_PORT_E port)
        {
            tuya_gpio_input_init(port, TY_GPIO_FLOATING);
        }
        /* 设置段码液晶屏引脚方向为输出 */
        STATIC VOID_T __seg_lcd_gpio_shutdown(IN CONST TY_GPIO_PORT_E port)
        {
            tuya_gpio_set_inout(port, TRUE);
        }
        /* 设置段码液晶屏引脚状输出值 */
        STATIC VOID_T __seg_lcd_gpio_write(IN CONST TY_GPIO_PORT_E port, IN CONST UINT_T level)
        {
            tuya_gpio_set_inout(port, FALSE);
            tuya_gpio_write(port, level);
        }
        
        /* 第四步:编写段码液晶屏驱动初始化函数 (tuya_seg_lcd.c) */
        SEG_LCD_RET tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def)
        {
            UCHAR_T i;
            /* 初始化管理信息 */
            memset(&sg_seg_lcd_mag, 0, sizeof(SEG_LCD_MANAGE_T));
            sg_seg_lcd_mag.pin = pin_def;
            /* COM 引脚初始化 */
            for (i = 0; i < COM_NUM; i++) {
                __seg_lcd_gpio_init(pin_def.com[i]);
            }
            /* SEG 引脚初始化 */
            for (i = 0; i < SEG_NUM; i++) {
                __seg_lcd_gpio_init(pin_def.seg[i]);
            }
            return SEG_LCD_OK;
        }
        
        /* 第五步:在头文件中定义初始化接口 (tuya_seg_lcd.h) */
        VOID_T tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def);
        
      2. 实现段码液晶屏控制。由于段码液晶屏的引脚控制需要 3ms 内处理一次,为避免液晶显示被外部程序影响,需设置一个硬件定时器,并在定时器中断中进行相关处理:

        /* 第一步:定义 COM 口扫描相关宏 (tuya_seg_lcd.h) */
        typedef BYTE_T SEG_LCD_STEP_E;
        #define STEP_COM_HIGH               0x00
        #define STEP_COM_LOW                0x01
        #define STEP_COM_HI_Z               0x02
        
        /* 第二步:添加 COM 口扫描相关变量 (tuya_seg_lcd.c) */
        typedef struct {
            UCHAR_T scan_com_num;           /* 当前扫描 COM 口 */
            SEG_LCD_STEP_E scan_step;       /* 当前扫描阶段 */
        } SEG_LCD_MANAGE_T;
        
        /* 第三步:编写引脚输出控制函数 (tuya_seg_lcd.c) */
        /**
        * @brief 段码液晶屏输出控制,3ms 处理一次
        * @param[in] none
        * @return none
        */
        STATIC VOID_T __seg_lcd_output_ctrl(VOID_T)
        {
            UCHAR_T i, active_pin, actl_code;
            BOOL_T seg_pin_level;
            /* 获取有效引脚 */
            active_pin = (sg_seg_lcd_mag.scan_com_num == 0) ? 0x2a : 0x3f;
            /* 获取实际输出 code */
            actl_code = __get_actual_output_code(sg_seg_lcd_mag.seg_pin_code[sg_seg_lcd_mag.scan_com_num]);
            /* 每个 COM 口按输出高-输出低-高阻态顺序进行控制,根据 SEG 口输出 code 设置每个 SEG 口的输出电平 */
            switch (sg_seg_lcd_mag.scan_step) {
            case STEP_COM_HIGH:	/* COM 输出高 */
                __seg_lcd_gpio_write(sg_seg_lcd_mag.pin.com[sg_seg_lcd_mag.scan_com_num], TRUE);
                for (i = 0; i < SEG_NUM; i++) {
                    if (active_pin & (1 << i)) {
                        seg_pin_level = ((actl_code & (1 << i)) > 0) ? FALSE : TRUE;
                        __seg_lcd_gpio_write(sg_seg_lcd_mag.pin.seg[i], seg_pin_level);
                    }
                }
                sg_seg_lcd_mag.scan_step = STEP_COM_LOW;
                break;
            case STEP_COM_LOW:	/* COM 输出低 */
                __seg_lcd_gpio_write(sg_seg_lcd_mag.pin.com[sg_seg_lcd_mag.scan_com_num], FALSE);
                for (i = 0; i < SEG_NUM; i++) {
                    if (active_pin & (1 << i)) {
                        seg_pin_level = ((actl_code & (1 << i)) > 0) ? TRUE : FALSE;
                        __seg_lcd_gpio_write(sg_seg_lcd_mag.pin.seg[i], seg_pin_level);
                    }
                }
                sg_seg_lcd_mag.scan_step = STEP_COM_HI_Z;
                break;
            case STEP_COM_HI_Z:	/* COM 高阻态 */
                __seg_lcd_gpio_shutdown(sg_seg_lcd_mag.pin.com[sg_seg_lcd_mag.scan_com_num]);
                for (i = 0; i < SEG_NUM; i++) {
                    if (active_pin & (1 << i)) {
                        __seg_lcd_gpio_shutdown(sg_seg_lcd_mag.pin.seg[i]);
                    }
                }
                sg_seg_lcd_mag.scan_com_num++;
                if (sg_seg_lcd_mag.scan_com_num >= COM_NUM) {
                    sg_seg_lcd_mag.scan_com_num = 0;
                }
                sg_seg_lcd_mag.scan_step = STEP_COM_HIGH;
                break;
            default:
                break;
            }
        }
        
        /* 第四步:定义 COM 口扫描时间 (tuya_seg_lcd.c) */
        #define SEG_LCD_COM_SCAN_CYCLE_MS   3
        
        /* 第五步:添加硬件定时器初始化,注册回调函数 (tuya_seg_lcd.c) */
        SEG_LCD_RET tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def)
        {
            ...
            /* 定时器初始化 */
            tuya_hardware_timer_create(TY_TIMER_0, SEG_LCD_COM_SCAN_CYCLE_MS*1000, __seg_lcd_output_ctrl, TY_TIMER_REPEAT);
            return SEG_LCD_OK;
        }
        
      3. 实现液晶屏的点亮、熄灭和闪烁功能。基本参照 LED 的处理方式,但考虑到液晶屏在闪烁方面的需求没有那么频繁和复杂,所以简化参数设置,下面直接来看代码:

        【tuya_seg_lcd.h】
        /* 闪烁方式相关宏定义 */
        #define SEG_LCD_FLASH_DIGIT_ALL     0xFF
        #define SEG_LCD_FLASH_FOREVER       0xFFFF
        
        /* 闪烁类型定义 */
        typedef BYTE_T SEG_LCD_FLASH_TYPE_E;
        #define SLFT_STA_ON_END_ON          0x01    /* 开始时:亮;结束后:亮 */
        #define SLFT_STA_ON_END_OFF         0x02    /* 开始时:亮;结束后:灭 */
        #define SLFT_STA_OFF_END_ON         0x04    /* 开始时:灭;结束后:亮 */
        #define SLFT_STA_OFF_END_OFF        0x05    /* 开始时:灭;结束后:灭 */
        
        /* 回调函数类型定义 */
        typedef VOID_T (*SEG_LCD_CALLBACK)();
        
        /* 相关接口定义 */
        SEG_LCD_RET tuya_seg_lcd_set_light(BOOL_T on_off);
        SEG_LCD_RET tuya_seg_lcd_set_flash(IN CONST UCHAR_T digit, IN CONST SEG_LCD_FLASH_TYPE_E type, IN CONST USHORT_T intv, IN CONST USHORT_T count, IN CONST SEG_LCD_CALLBACK end_cb);
        
        【tuya_seg_lcd.c】
        /* 段码液晶屏闪烁信息管理 */
        typedef struct {
            UCHAR_T digit;                  /* 闪烁的位,0xFF 表示所有位同时闪烁 */
            SEG_LCD_FLASH_TYPE_E type;      /* 闪烁类型 */
            USHORT_T intv;                  /* 闪烁间隔 */
            USHORT_T count;                 /* 闪烁次数 */
            SEG_LCD_CALLBACK end_cb;        /* 闪烁结束时的回调函数 */
            UINT_T work_timer;              /* 闪烁工作用计时变量 */
        } SEG_LCD_FLASH_T;
        
        /* 段码液晶屏信息管理 */
        typedef struct {
            SEG_LCD_PIN_T pin;              /* 引脚管理 */
            UCHAR_T seg_pin_code[COM_NUM];  /* SEG pin code */
            BOOL_T light;                   /* 亮灭状态 */
            UCHAR_T scan_com_num;           /* 当前扫描 COM 口 */
            SEG_LCD_STEP_E scan_step;       /* 当前扫描阶段 */
            SEG_LCD_FLASH_T *flash;			/* 闪烁信息管理 */
            BOOL_T stop_flash_req;          /* 停止闪烁请求 */
            BOOL_T stop_flash_light;        /* 停止闪烁后的亮灭状态 */
        } SEG_LCD_MANAGE_T;
        STATIC SEG_LCD_MANAGE_T sg_seg_lcd_mag;
        
        /**
        * @brief 获取实际输出的 SEG pin code,在__seg_lcd_output_ctrl()中调用
        * @param[in] seg_pin_code: 用户设置的显示内容转换得到的 SEG pin code
        * @return none
        */
        STATIC UCHAR_T __get_actual_output_code(UCHAR_T seg_pin_code)
        {
            UCHAR_T code = seg_pin_code;
            /* 当前亮灭状态为熄灭时进行处理 */
            if (!sg_seg_lcd_mag.light) {
                if (sg_seg_lcd_mag.flash == NULL) {
                    code =  0x00;
                } else {
                    if (sg_seg_lcd_mag.flash->digit == SEG_LCD_FLASH_DIGIT_ALL) {
                        code = 0x00;
                    } else {
                        code &= ~(ch_com_code[sg_seg_lcd_mag.flash->digit]);
                    }
                }
            }
            return code;
        }
        
        /**
        * @brief 亮灭控制 (内部调用)
        * @param[in] on_off: 1-亮, 0-灭
        * @return none
        */
        VOID_T __set_seg_lcd_light(IN CONST BOOL_T on_off)
        {
            sg_seg_lcd_mag.light = on_off;
        }
        
        /**
        * @brief 亮灭控制 (外部调用)
        * @param[in] on_off: 1-亮, 0-灭
        * @return SEG_LCD_RET
        */
        SEG_LCD_RET tuya_seg_lcd_set_light(IN CONST BOOL_T on_off)
        {
            if (sg_seg_lcd_mag.flash != NULL) {
                sg_seg_lcd_mag.stop_flash_req = TRUE;
                sg_seg_lcd_mag.stop_flash_light = on_off;
            } else {
                __set_seg_lcd_light(on_off);
            }
            return SEG_LCD_OK;
        }
        
        /**
        * @brief 获取闪烁开始时的亮灭状态
        * @param[in] type: 闪烁类型
        * @return 1-亮,0-灭
        */
        STATIC BOOL_T __get_seg_lcd_flash_sta_light(IN CONST SEG_LCD_FLASH_TYPE_E type)
        {
            BOOL_T ret = TRUE;
            switch (type) {
            case SLFT_STA_ON_END_ON:
            case SLFT_STA_ON_END_OFF:
                ret = TRUE;
                break;
            case SLFT_STA_OFF_END_ON:
            case SLFT_STA_OFF_END_OFF:
                ret = FALSE;
                break;
            default:
                break;
            }
            return ret;
        }
        
        /**
        * @brief 获取闪烁结束后的亮灭状态
        * @param[in] type: 闪烁类型
        * @return 1-亮,0-灭
        */
        STATIC BOOL_T __get_seg_lcd_flash_end_light(IN CONST SEG_LCD_FLASH_TYPE_E type)
        {
            BOOL_T ret = TRUE;
            switch (type) {
            case SLFT_STA_ON_END_ON:
            case SLFT_STA_OFF_END_ON:
                ret = TRUE;
                break;
            case SLFT_STA_ON_END_OFF:
            case SLFT_STA_OFF_END_OFF:
                ret = FALSE;
                break;
            default:
                break;
            }
            return ret;
        }
        
        /**
        * @brief 段码液晶屏闪烁配置
        * @param[in] digit: 闪烁数据位, "0xFF"表示所有位同时闪烁
        * @param[in] type: 闪烁类型
        * @param[in] intv: 闪烁间隔(ms)
        * @param[in] count: 闪烁次数, "0xFFFF"表示永远闪烁
        * @param[in] end_cb: 闪烁结束时的回调函数
        * @return SEG_LCD_RET
        */
        SEG_LCD_RET tuya_seg_lcd_set_flash(IN CONST UCHAR_T digit, IN CONST SEG_LCD_FLASH_TYPE_E type, IN CONST USHORT_T intv, IN CONST USHORT_T count, IN CONST SEG_LCD_CALLBACK end_cb)
        {
            sg_seg_lcd_mag.stop_flash_req = FALSE;
            if (sg_seg_lcd_mag.flash == NULL) {
                SEG_LCD_FLASH_T *seg_lcd_flash = (SEG_LCD_FLASH_T *)tuya_ble_malloc(SIZEOF(SEG_LCD_FLASH_T));
                if (NULL == seg_lcd_flash) {
                    return SEG_LCD_ERR_MALLOC_FAILED;
                }
                sg_seg_lcd_mag.flash = seg_lcd_flash;
            }
            sg_seg_lcd_mag.flash->digit = digit;
            sg_seg_lcd_mag.flash->type = type;
            sg_seg_lcd_mag.flash->intv = intv;
            sg_seg_lcd_mag.flash->count = count;
            sg_seg_lcd_mag.flash->work_timer = 0;
            sg_seg_lcd_mag.flash->end_cb = end_cb;
            __set_seg_lcd_light(__get_seg_lcd_flash_sta_light(type));
            return SEG_LCD_OK;
        }
        
        /**
        * @brief 段码液晶屏闪烁处理
        * @param[inout] none
        * @return none
        */
        STATIC VOID_T __seg_lcd_flash_proc(VOID_T)
        {
            BOOL_T one_cycle_flag = FALSE;
            BOOL_T start_light = __get_seg_lcd_flash_sta_light(sg_seg_lcd_mag.flash->type);
            /* 闪烁周期性处理,实现按照指定时间点亮和熄灭 */
            sg_seg_lcd_mag.flash->work_timer += SEG_LCD_FLASH_PROC_CYCLE_MS;
            if (sg_seg_lcd_mag.flash->work_timer >= sg_seg_lcd_mag.flash->intv*2) {
                sg_seg_lcd_mag.flash->work_timer -= sg_seg_lcd_mag.flash->intv*2;
                __set_seg_lcd_light(start_light);
                one_cycle_flag = TRUE;
            } else if (sg_seg_lcd_mag.flash->work_timer >= sg_seg_lcd_mag.flash->intv) {
                __set_seg_lcd_light(!start_light);
            } else {
                ;
            }
            /* 闪烁倒计数处理,闪烁方式为“永远闪烁”时不处理 */
            if (sg_seg_lcd_mag.flash->count == SEG_LCD_FLASH_FOREVER) {
                return;
            }
            if (one_cycle_flag) {
                if (sg_seg_lcd_mag.flash->count > 0) {
                    sg_seg_lcd_mag.flash->count--;
                }
            }
            /* 闪烁结束处理 */
            if (sg_seg_lcd_mag.flash->count == 0) {
                if (sg_seg_lcd_mag.flash->end_cb != NULL) {
                    sg_seg_lcd_mag.flash->end_cb();
                }
                sg_seg_lcd_mag.stop_flash_req = TRUE;
                sg_seg_lcd_mag.stop_flash_light = __get_seg_lcd_flash_end_light(sg_seg_lcd_mag.flash->type);
            }
        }
        
        /* 定义超时处理时间 */
        #define SEG_LCD_FLASH_PROC_CYCLE_MS 10
        
        /* 添加软件定时器初始化,注册回调函数 */
        SEG_LCD_RET tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def)
        {
            ...
            /* 定时器初始化 */
            tuya_software_timer_create(SEG_LCD_FLASH_PROC_CYCLE_MS*1000, __seg_lcd_timeout_handler);
            ...
        }
        
        /**
        * @brief 段码液晶屏超时处理
        * @param[in] none
        * @return 0
        */
        STATIC INT_T __seg_lcd_timeout_handler(VOID_T)
        {
            /* 停止闪烁请求处理 */
            if (sg_seg_lcd_mag.stop_flash_req) {
                __set_seg_lcd_light(sg_seg_lcd_mag.stop_flash_light);
                tuya_ble_free((UCHAR_T *)sg_seg_lcd_mag.flash);
                sg_seg_lcd_mag.flash = NULL;
                sg_seg_lcd_mag.stop_flash_req = FALSE;
            }
            /* 如果闪烁功能未开启则不处理 */
            if (sg_seg_lcd_mag.flash != NULL) {
                __seg_lcd_flash_proc();
            }
            return 0;
        }
        

      功能实现

      设备基础服务

      设备基础服务模块包括设备状态迁移、模式选择、模式切换、本地定时等功能。
      定义设备状态

      设备状态 含义 处理
      未使用 用户未使用呼啦圈 保持熄屏,实时数据清除
      使用中 用户未转动呼啦圈 保持亮屏
      转动中 用户转动呼啦圈 开始转动时点亮屏幕,转动过程:亮屏 6 秒,熄屏 5 分钟
      复位 设备复位 所有数据清除

      设备状态迁移情况如下(第一列为迁移前状态,第一行为迁移后状态):

      未使用 使用中 转动中 复位中
      未使用 / 触发按键 呼啦圈转动 /
      使用中 30 秒内无动作 / 呼啦圈转动 操作复位键
      转动中 / 未检测到磁铁 2 秒后 / /
      复位 / 复位结束后 / /

      代码实现

      /* 第一步:定义基础服务数据类型 (tuya_hula_hoop_svc_basic.h) */
      /* 工作模式 */
      typedef BYTE_T MODE_E;
      #define MODE_NORMAL         0x00	/* 普通模式 */
      #define MODE_TARGET         0x01	/* 目标模式 */
      /* 设备状态 */
      typedef BYTE_T STAT_E;
      #define STAT_USING          0x00	/* 使用中 */
      #define STAT_ROTATING       0x01	/* 转动中 */
      #define STAT_UNUSED         0x02	/* 未使用 */
      #define STAT_RESET          0x03	/* 复位 */
      /* 基础服务信息管理 */
      typedef struct {
          MODE_E mode;		/* 工作模式 */
          MODE_E mode_temp;	/* 预选模式 */
          STAT_E stat;		/* 设备状态 */
      } HULA_HOOP_T;
      
      /* 第二步:定义基础服务信息管理结构体并初始化 (tuya_hula_hoop_svc_basic.c) */
      HULA_HOOP_T g_hula_hoop;
      
      /**
       * @brief 基础服务模块初始化
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_basic_service_init(VOID_T)
      {
          memset(&g_hula_hoop, 0, SIZEOF(HULA_HOOP_T));
          __set_device_status(STAT_USING);	/* 上电设备状态:使用中 */
          __set_work_mode(MODE_NORMAL);		/* 默认工作模式:普通模式 */
      }
      
      /* 第三步:编写基础服务相关处理函数 (tuya_hula_hoop_svc_basic.c) */
      /**
       * @brief 设置工作模式 (内部调用)
       * @param[in] mode: 工作模式
       * @return none
       */
      STATIC VOID_T __set_work_mode(IN CONST MODE_E mode)
      {
          g_hula_hoop.mode = mode;
          switch (mode) {
          case MODE_NORMAL:
              TUYA_APP_LOG_INFO("Work mode is normal mode now.");
              hula_hoop_disp_switch_to_normal_mode();
              break;
          case MODE_TARGET:
              TUYA_APP_LOG_INFO("Work mode is target mode now.");
              hula_hoop_disp_switch_to_target_mode();
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 切换预选模式,在模式键短按处理时调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_switch_temp_mode(VOID_T)
      {
          switch (g_hula_hoop.mode_temp) {
          case MODE_NORMAL:
              g_hula_hoop.mode_temp = MODE_TARGET;
              break;
          case MODE_TARGET:
              g_hula_hoop.mode_temp = MODE_NORMAL;
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 进入模式选择,在模式键短按处理时调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_enter_mode_select(VOID_T)
      {
          g_hula_hoop.mode_temp = g_hula_hoop.mode;
          hula_hoop_disp_switch_to_mode_select();
      }
      
      /**
       * @brief 退出模式选择,在复位键短按处理时调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_quit_mode_select(VOID_T)
      {
          __set_work_mode(g_hula_hoop.mode);
      }
      
      /**
       * @brief 切换到预选模式,在模式键长按 2 秒处理时调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_switch_to_select_mode(VOID_T)
      {
          hula_hoop_clear_realtime_data();
          __set_work_mode(g_hula_hoop.mode_temp);
      }
      
      /**
       * @brief 设置工作模式,在云端下发工作模式时调用
       * @param[in] mode: 工作模式
       * @return none
       */
      VOID_T hula_hoop_set_work_mode(IN CONST MODE_E mode)
      {
          if (mode != g_hula_hoop.mode) {
              __set_work_mode(mode);
          }
      }
      
      /**
       * @brief 设置设备状态 (内部调用)
       * @param[in] stat: 设备状态
       * @return none
       */
      STATIC VOID_T __set_device_status(IN CONST STAT_E stat)
      {
          hula_hoop_get_device_status() = stat;
          switch (stat) {
          case STAT_UNUSED:
              TUYA_APP_LOG_INFO("Device status is 'unused'.");
              hula_hoop_clear_realtime_data();
              hula_hoop_disp_sleep();
              break;
          case STAT_USING:
              TUYA_APP_LOG_INFO("Device status is 'using'.");
              hula_hoop_disp_wakeup();
              break;
          case STAT_ROTATING:
              TUYA_APP_LOG_INFO("Device status is 'rotating'.");
              hula_hoop_disp_wakeup();
              break;
          case STAT_RESET:
              start_reboot();
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 设置设备状态,在状态更新时调用
       * @param[in] stat: 设备状态
       * @return none
       */
      VOID_T hula_hoop_set_device_status(IN CONST STAT_E stat)
      {
          if (stat != hula_hoop_get_device_status()) {
              __set_device_status(stat);
          }
      }
      
      /**
       * @brief 获取设备状态
       * @param[in] none
       * @return 设备状态
       */
      STAT_E hula_hoop_get_device_status(VOID_T)
      {
          return hula_hoop_get_device_status();
      }
      
      /* 第四步:在头文件中定义基础服务处理接口 (tuya_hula_hoop_svc_basic.h) */
      VOID_T hula_hoop_switch_temp_mode(VOID_T);
      VOID_T hula_hoop_enter_mode_select(VOID_T);
      VOID_T hula_hoop_quit_mode_select(VOID_T);
      VOID_T hula_hoop_switch_to_select_mode(VOID_T);
      VOID_T hula_hoop_set_work_mode(IN CONST MODE_E mode);
      VOID_T hula_hoop_set_device_status(IN CONST STAT_E stat);
      STAT_E hula_hoop_get_device_status(VOID_T);
      VOID_T hula_hoop_basic_service_init(VOID_T);
      

      除以上基础服务外,为了实现每日数据和每月数据的累计功能,需添加本地定时功能,以实现日期和月份变更的检查和处理。

      本地定时模块的代码实现

      /* 第一步:定义本地时间数据类型 (tuya_local_time.h) */
      typedef struct {
          USHORT_T year;
          UCHAR_T month;
          UCHAR_T day;
          UCHAR_T hour;
          UCHAR_T minute;
          UCHAR_T second;
      } LOCAL_TIME_T;
      
      /* 第二步:定义本地时间并初始化 (tuya_local_time.c) */
      LOCAL_TIME_T g_local_time = {
          .year = 1,
          .month = 1,
          .day = 1,
          .hour = 0,
          .minute = 0,
          .second = 0,
      };
      
      /* 第三步:定义每月天数表,用于判断月份变更 (tuya_local_time.c) */
      STATIC UCHAR_T sg_day_tbl[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
      
      /* 第四步:编写本地时间更新相关处理函数 (tuya_local_time.c) */
      /**
       * @brief 闰年判断并更新 2 月份天数
       * @param[in] year: 要判断的年份
       * @return 2 月份天数
       */
      STATIC UCHAR_T __local_time_leap_year_judgment(USHORT_T year)
      {
          if ((year % 400 == 0) ||
              ((year % 4 == 0) && (year / 100 != 0))) {
              return 29;
          } else {
              return 28;
          }
      }
      
      /**
       * @brief 每年更新
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __local_time_update_per_year(VOID_T)
      {
          g_local_time.year++;
          sg_day_tbl[1] = __local_time_leap_year_judgment(g_local_time.year);
      }
      
      /**
       * @brief 每月更新
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __local_time_update_per_month(VOID_T)
      {
          g_local_time.month++;
          if (g_local_time.month > 12) {
              g_local_time.month = 1;
              __local_time_update_per_year();
          }
      }
      
      /**
       * @brief 每天更新
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __local_time_update_per_day(VOID_T)
      {
          g_local_time.day++;
          if (g_local_time.day > sg_day_tbl[g_local_time.month-1]) {
              g_local_time.day = 1;
              __local_time_update_per_month();
          }
      }
      
      /**
       * @brief 每小时更新
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __local_time_update_per_hour(VOID_T)
      {
          g_local_time.hour++;
          if (g_local_time.hour >= 24) {
              g_local_time.hour = 0;
              __local_time_update_per_day();
          }
      }
      
      /**
       * @brief 每分钟更新
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __local_time_update_per_minute(VOID_T)
      {
          g_local_time.minute++;
          if (g_local_time.minute >= 60) {
              g_local_time.minute = 0;
              __local_time_update_per_hour();
          }
      }
      
      /**
       * @brief 每秒钟更新
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __local_time_update_per_second(VOID_T)
      {
          g_local_time.second++;
          if (g_local_time.second >= 60) {
              g_local_time.second = 0;
              __local_time_update_per_minute();
          }
      }
      
      /**
       * @brief 本地时间更新
       * @param[in] none
       * @return none
       */
      VOID_T tuya_local_time_update(VOID_T)
      {
          __local_time_update_per_second();
      }
      
      /**
       * @brief 获取本地时间
       * @param[in] none
       * @return 本地时间
       */
      LOCAL_TIME_T tuya_get_local_time(VOID_T)
      {
          return g_local_time;
      }
      
      /* 第五步:在头文件中定义接口 (tuya_local_time.h) */
      VOID_T tuya_local_time_update(VOID_T);
      LOCAL_TIME_T tuya_get_local_time(VOID_T);
      
      /* 第六步:编写本地时间变更检查处理函数 (tuya_hula_hoop_svc_basic.c) */
      /**
       * @brief 检查日期是否变更并处理
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_check_date_change(VOID_T)
      {
          STATIC UCHAR_T s_day = 0;
          STATIC UCHAR_T s_month = 0;
          LOCAL_TIME_T local_time;
      	/* 获取本地时间 */
          local_time = tuya_get_local_time();
          /* 日期变更检查 */
          if (local_time.day != s_day) {
              s_day = local_time.day;
              hula_hoop_update_total_data_day();
          }
          /* 月份变更检查 */
          if (local_time.month != s_month) {
              s_month = local_time.month;
              hula_hoop_update_total_data_month();
          }
      }
      
      /* 第七步:在头文件中定义接口 (tuya_hula_hoop_svc_basic.h) */
      VOID_T hula_hoop_check_date_change(VOID_T);
      
      /* 第八步:在本地时间更新定时函数中调用相关接口进行处理 (tuya_hula_hoop_evt_timer.c) */
      /**
       * @brief 本地时间更新定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __upd_local_time_timer(IN CONST UINT_T time_inc)
      {
          g_timer.upd_local_time += time_inc;
          if (g_timer.upd_local_time >= LOCAL_TIME_UPDATE_INTV_MS) {
              g_timer.upd_local_time -= LOCAL_TIME_UPDATE_INTV_MS;
              tuya_local_time_update();		/* 本地时间更新 */
              hula_hoop_check_date_change();	/* 检查日期变更 */
          }
      }
      

      显示处理服务

      显示处理模块包括显示屏内容的设定、显示屏状态控制和指示灯状态控制。

      显示内容

      显示模式 段码液晶屏显示 LED 显示
      普通模式界面 显示实时数据,按”实时计时->实时圈数->实时卡路里“顺序切换 点亮数据对应指示灯
      目标模式界面 当日目标剩余时长≠0 时,显示该数据,否则显示”—“ 点亮计时指示灯
      模式选择界面 普通模式显示”010“,目标模式显示”020“ 全灭
      复位提示界面 显示”000“ 全灭

      计时指示灯

      指示灯功能 指示灯状态 优先级
      配网指示功能 配网指示灯快闪
      数据指示功能 根据显示模式控制状态

      液晶屏

      段码液晶屏状态 处理
      熄灭 段码液晶屏熄灭
      亮屏 段码液晶屏点亮
      闪烁(目标完成) 段码液晶屏闪烁 3 次,结束时点亮(保持“—”显示)(目标模式界面才执行)
      闪烁(复位提示) 段码液晶屏闪烁 3 次,结束时执行“设备复位”

      代码实现

      外设驱动的部分可以使用之前编写的 LED 驱动组件和 Segment LCD 驱动组件进行开发,本 demo 硬件方案如下:

      外设 引脚 有效电平
      计时/配网指示灯 PD4 H
      圈数指示灯 PB6 H
      卡路里指示灯 PD7 H
      段码液晶屏 COM1 - COM4 : PA1, PC2, PC3, PB4
      SEG1 - SEG6 : PA0, PC0, PD3, PC1, PC4, PB5
      /

      实现显示处理模块的各项功能

      /* 第一步:定义显示数据类型 (tuya_hula_hoop_svc_disp.h) */
      /* 显示模式 */
      typedef BYTE_T DISP_MODE_E;
      #define DISP_NORMAL_MODE    0x00	/* 普通模式界面 */
      #define DISP_TARGET_MODE    0x01	/* 目标模式界面 */
      #define DISP_MODE_SELECT    0x02	/* 模式选择界面 */
      #define DISP_RESET_REMIND   0x03	/* 复位提醒界面 */
      /* 显示数据 */
      typedef BYTE_T DISP_DATA_E;
      #define DISP_DATA_TIME      0x00	/* 显示计时数据 */
      #define DISP_DATA_COUNT     0x01	/* 显示圈数数据 */
      #define DISP_DATA_CALORIES  0x02	/* 显示卡路里数据 */
      #define DISP_DATA_NONE      0x03	/* 不显示数据 */
      /* 指示灯功能 */
      typedef BYTE_T LED_FUNC_E;
      #define LED_FUNC_DATA       0x00	/* 数据指示功能 */
      #define LED_FUNC_BIND       0x01	/* 配网指示功能 */
      /* 屏幕状态 */
      typedef BYTE_T SEG_LCD_STAT_E;
      #define SEG_LCD_STAT_OFF    0x00	/* 屏幕点亮 */
      #define SEG_LCD_STAT_ON     0x01	/* 屏幕熄灭 */
      #define SEG_LCD_STAT_FLASH  0x02	/* 屏幕闪烁 */
      
      /* 第二步:定义显示处理数据结构和外设引脚 (tuya_hula_hoop_svc_disp.c) */
      /* 显示处理数据类型 */
      typedef struct {
          DISP_MODE_E mode;
          DISP_DATA_E data;
          LED_FUNC_E led_func;
          SEG_LCD_STAT_E seg_lcd_stat;
      } HULA_HOOP_DISP_T;
      
      /* 指示灯引脚 */
      STATIC TY_GPIO_PORT_E sg_user_led_pin[] = {
          TY_GPIOD_4,   /* 计时/配网指示灯 */
          TY_GPIOB_6,   /* 圈数指示灯 */
          TY_GPIOD_7    /* 卡路里指示灯 */
      };
      /* 指示灯句柄 */
      LED_HANDLE g_user_led_handle[(SIZEOF(sg_user_led_pin) / SIZEOF(sg_user_led_pin[0]))];
      /* 段码液晶屏引脚 */
      SEG_LCD_PIN_T seg_lcd_pin_s = {
          .com = {TY_GPIOA_1, TY_GPIOC_2, TY_GPIOC_3, TY_GPIOB_4},
          .seg = {TY_GPIOA_0, TY_GPIOC_0, TY_GPIOD_3, TY_GPIOC_1, TY_GPIOC_4, TY_GPIOB_5}
      };
      /* 显示信息管理 */
      STATIC HULA_HOOP_DISP_T sg_disp;
      
      /* 第三步:编写显示处理初始化函数 (tuya_hula_hoop_svc_disp.c) */
      /**
       * @brief 显示处理模块初始化
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_proc_init(VOID_T)
      {
          UCHAR_T i;
          LED_RET ret;
          /* 指示灯初始化 */
          for (i = 0; i < (SIZEOF(sg_user_led_pin) / SIZEOF(sg_user_led_pin[0])); i++) {
              ret = tuya_create_led_handle(sg_user_led_pin[i], FALSE, &g_user_led_handle[i]);
              if (ret != LED_OK) {
                  TUYA_APP_LOG_ERROR("led init err:%d", ret);
              }
          }
          /* 段码液晶屏初始化 */
          tuya_seg_lcd_init(seg_lcd_pin_s);
          /* 变量初始化 */
          memset(&sg_disp, 0, SIZEOF(HULA_HOOP_DISP_T));
      }
      
      /* 第四步:编写指示灯和段码液晶屏状态控制函数 (tuya_hula_hoop_svc_disp.c) */
      /**
       * @brief 设置指示灯状态(作为配网指示时)
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __set_net_led_status(VOID_T)
      {
          tuya_set_led_flash(g_user_led_handle[DISP_DATA_TIME], LFM_FOREVER, LFT_STA_ON_END_ON, LED_FLASH_INTV_MS, LED_FLASH_INTV_MS, 0, NULL);
          tuya_set_led_light(g_user_led_handle[DISP_DATA_COUNT], FALSE);
          tuya_set_led_light(g_user_led_handle[DISP_DATA_CALORIES], FALSE);
      }
      
      /**
       * @brief 设置指示灯状态(作为数据指示时)
       * @param[in] data: 显示数据
       * @return none
       */
      STATIC VOID_T __set_data_led_status(IN CONST DISP_DATA_E data)
      {
          UCHAR_T i;
      
          if (sg_disp.led_func == LED_FUNC_BIND) {
              return;
          }
          for (i = 0; i < (SIZEOF(sg_user_led_pin) / SIZEOF(sg_user_led_pin[0])); i++) {
              if (i == data) {
                  tuya_set_led_light(g_user_led_handle[i], TRUE);
              } else {
                  tuya_set_led_light(g_user_led_handle[i], FALSE);
              }
          }
      }
      
      /**
       * @brief 目标完成提示结束时的回调函数
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __disp_target_remind_end_cb()
      {
          __set_seg_lcd_status(SEG_LCD_STAT_ON);
      }
      
      /**
       * @brief 复位提示结束时的回调函数
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __disp_reset_remind_end_cb()
      {
          hula_hoop_set_work_stat(STAT_RESET);
      }
      
      /**
       * @brief 设置段码液晶屏状态
       * @param[in] stat: 段码液晶屏状态
       * @return none
       */
      STATIC VOID_T __set_seg_lcd_status(IN CONST SEG_LCD_STAT_E stat)
      {
          sg_disp.seg_lcd_stat = stat;
          switch (stat) {
          case SEG_LCD_STAT_OFF:
              tuya_seg_lcd_set_light(FALSE);
              break;
          case SEG_LCD_STAT_ON:
              tuya_seg_lcd_set_light(TRUE);
              break;
          case SEG_LCD_STAT_FLASH:
              if (sg_disp.mode == DISP_TARGET_MODE) {
                  tuya_seg_lcd_set_flash(SEG_LCD_FLASH_DIGIT_ALL, SLFT_STA_ON_END_ON, SEG_LCD_FLASH_INTV_MS, SEG_LCD_FLASH_COUNT, __disp_target_remind_end_cb);
              }
              if (sg_disp.mode == DISP_RESET_REMIND) {
                  tuya_seg_lcd_set_flash(SEG_LCD_FLASH_DIGIT_ALL, SLFT_STA_ON_END_OFF, SEG_LCD_FLASH_INTV_MS, SEG_LCD_FLASH_COUNT, __disp_reset_remind_end_cb);
              }
              break;
          default:
              break;
          }
      }
      
      /* 第四步:编写各界面的显示内容设定函数和显示处理循环 (tuya_hula_hoop_svc_disp.c) */
      /**
       * @brief 设置【普通模式界面】显示内容
       * @param[in] data: 显示数据
       * @return none
       */
      STATIC VOID_T __set_seg_lcd_disp_normal_mode(IN CONST DISP_DATA_E data)
      {
          switch (data) {
          case DISP_DATA_TIME:		/* 显示计时数据 */
              tuya_seg_lcd_disp_num(g_sport_data.time_realtime, 0);
              break;
          case DISP_DATA_COUNT:		/* 显示圈数数据 */
              tuya_seg_lcd_disp_num(g_sport_data.count_realtime, 0);
              break;
          case DISP_DATA_CALORIES:	/* 显示卡路里数据 */
              tuya_seg_lcd_disp_num(g_sport_data.calories_realtime, 0);
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 设置【目标模式界面】显示内容
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __set_seg_lcd_disp_target_mode(VOID_T)
      {
          /* 当日目标剩余时长等于 0 时显示“---”,否则显示“当日目标剩余时长” */
          if (g_sport_data.time_remain_today == 0) {
              tuya_seg_lcd_disp_str("---");
          } else {
              tuya_seg_lcd_disp_num(g_sport_data.time_remain_today, 0);
          }
      }
      
      /**
       * @brief 设置【模式选择界面】显示内容
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __set_seg_lcd_disp_mode_select(VOID_T)
      {
          switch (g_hula_hoop.mode_temp) {
          case MODE_NORMAL:	/* 预选模式为普通模式时,显示“010” */
              tuya_seg_lcd_disp_str("010");
              break;
          case MODE_TARGET:	/* 预选模式为目标模式时,显示“020” */
              tuya_seg_lcd_disp_str("020");
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 设置【复位提醒界面】显示内容
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __set_seg_lcd_disp_reset_remind(VOID_T)
      {
          /* 显示“000” */
          tuya_seg_lcd_disp_str("000");
      }
      
      /**
       * @brief 显示处理循环
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_proc_loop(VOID_T)
      {
          /* 根据当前显示模式设置显示内容 */
          switch (sg_disp.mode) {
          case DISP_NORMAL_MODE:
              __set_seg_lcd_disp_normal_mode(sg_disp.data);
              break;
          case DISP_TARGET_MODE:
              __set_seg_lcd_disp_target_mode();
              break;
          case DISP_MODE_SELECT:
              __set_seg_lcd_disp_mode_select();
              break;
          case DISP_RESET_REMIND:
              __set_seg_lcd_disp_reset_remind();
              break;
          default:
              break;
          }
      }
      
      /* 第五步:编写显示模式迁移相关函数 (tuya_hula_hoop_svc_disp.c) */
      /**
       * @brief 切换到【普通模式界面】
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_switch_to_normal_mode(VOID_T)
      {
          sg_disp.mode = DISP_NORMAL_MODE;
          sg_disp.data = DISP_DATA_TIME;
          __set_led_status(sg_disp.data);
          __set_seg_lcd_status(SEG_LCD_STAT_ON);
          __set_seg_lcd_disp_normal_mode(DISP_DATA_TIME);
      }
      
      /**
       * @brief 切换到【目标模式界面】
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_switch_to_target_mode(VOID_T)
      {
          sg_disp.mode = DISP_TARGET_MODE;
          sg_disp.data = DISP_DATA_TIME;
          __set_led_status(sg_disp.data);
          __set_seg_lcd_status(SEG_LCD_STAT_ON);
          __set_seg_lcd_disp_target_mode();
      }
      
      /**
       * @brief 切换到【模式选择界面】
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_switch_to_mode_select(VOID_T)
      {
          sg_disp.mode = DISP_MODE_SELECT;
          sg_disp.data = DISP_DATA_NONE;
          __set_led_status(sg_disp.data);
          __set_seg_lcd_status(SEG_LCD_STAT_ON);
          __set_seg_lcd_disp_mode_select();
      }
      
      /**
       * @brief 切换到【复位提醒界面】
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_switch_to_reset_remind(VOID_T)
      {
          sg_disp.mode = DISP_RESET_REMIND;
          sg_disp.data = DISP_DATA_NONE;
          __set_led_status(sg_disp.data);
          __set_seg_lcd_status(SEG_LCD_STAT_FLASH);
          __set_seg_lcd_disp_reset_remind();
      }
      
      /* 第六步:编写其他显示处理相关函数 (tuya_hula_hoop_svc_disp.c) */
      /**
       * @brief 设置指示灯功能,在联网处理模块中调用
       * @param[in] func: 指示灯功能
       * @return none
       */
      VOID_T hula_hoop_disp_set_led_func(IN CONST LED_FUNC_E func)
      {
          if (func == sg_disp.led_func) {
              return;
          }
          sg_disp.led_func = func;
          if (func == LED_FUNC_BIND) {
              __set_net_led_status();
          } else {
              __set_data_led_status(sg_disp.data);
          }
      }
      
      /**
       * @brief 显示唤醒,在需要点亮屏幕时调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_wakeup(VOID_T)
      {
          if (sg_disp.mode <= DISP_TARGET_MODE) {
              sg_disp.data = DISP_DATA_TIME;
          }
          __set_data_led_status(sg_disp.data);
          __set_seg_lcd_status(SEG_LCD_STAT_ON);
      }
      
      /**
       * @brief 显示休眠,在需要熄灭屏幕时调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_disp_sleep(VOID_T)
      {
          __set_data_led_status(DISP_DATA_NONE);
          __set_seg_lcd_status(SEG_LCD_STAT_OFF);
      }
      
      /**
       * @brief 获取当前显示状态,在检查当前显示状态时调用
       * @param[in] none
       * @return 1-点亮,0-熄灭
       */
      BOOL_T hula_hoop_disp_is_wakeup(VOID_T)
      {
          if (sg_disp.seg_lcd_stat == SEG_LCD_STAT_OFF) {
              return FALSE;
          } else {
              return TRUE;
          }
      }
      
      /**
       * @brief 检查屏幕是否正在闪烁,在熄屏计时中调用,避免闪烁时被外部设置为熄屏
       * @param[in] none
       * @return TRUE - 闪烁, FALSE - 不闪烁
       */
      BOOL_T hula_hoop_disp_is_flash(VOID_T)
      {
          if (sg_disp.seg_lcd_stat == SEG_LCD_STAT_FLASH) {
              return TRUE;
          } else {
              return FALSE;
          }
      }
      
      /**
       * @brief 获取当前显示模式,在判断当前显示界面时调用
       * @param[in] none
       * @return 显示模式
       */
      DISP_MODE_E hula_hoop_get_disp_mode(VOID_T)
      {
          return sg_disp.mode;
      }
      
      /**
       * @brief 切换显示数据,在显示数据切换定时中调用
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_switch_disp_data(VOID_T)
      {
          if (sg_disp.data == DISP_DATA_CALORIES) {
              if (hula_hoop_get_device_status() != STAT_ROTATING) {
                  sg_disp.data = DISP_DATA_TIME;
              }
          } else {
              sg_disp.data++;
          }
          if (sg_disp.seg_lcd_stat != SEG_LCD_STAT_OFF) {
              __set_led_status(sg_disp.data);
          }
      }
      
      /* 目标完成时闪烁处理 */
      VOID_T hula_hoop_disp_target_finish(VOID_T)
      {
          if (sg_disp.mode == DISP_TARGET_MODE) {
              __set_seg_lcd_status(SEG_LCD_STAT_FLASH);
          }
      }
      
      /* 第七步:在头文件中定义显示处理接口,供外部调用 (tuya_hula_hoop_svc_disp.h) */
      VOID_T hula_hoop_disp_proc_init(VOID_T);
      VOID_T hula_hoop_disp_proc_loop(VOID_T);
      VOID_T hula_hoop_disp_switch_to_normal_mode(VOID_T);
      VOID_T hula_hoop_disp_switch_to_target_mode(VOID_T);
      VOID_T hula_hoop_disp_switch_to_mode_select(VOID_T);
      VOID_T hula_hoop_disp_switch_to_reset_remind(VOID_T);
      VOID_T hula_hoop_disp_set_led_func(IN CONST LED_FUNC_E func);
      VOID_T hula_hoop_disp_wakeup(VOID_T);
      VOID_T hula_hoop_disp_sleep(VOID_T);
      BOOL_T hula_hoop_disp_is_wakeup(VOID_T);
      BOOL_T hula_hoop_disp_is_flash(VOID_T);
      DISP_MODE_E hula_hoop_get_disp_mode(VOID_T);
      VOID_T hula_hoop_switch_disp_data(VOID_T);
      VOID_T hula_hoop_disp_target_finish(VOID_T);
      

      数据处理服务

      智能呼啦圈相关的运动数据设定如下:

      No. 数据 类别 数值范围 说明
      1 实时计时 实时 0~999 可上报
      2 实时圈数 实时 0~999 可上报
      3 实时卡路里 实时 0~999 可上报
      4 当日累计计时 累计 0~1440 可上报
      5 当日累计圈数 累计 0~99999 可上报
      6 当日累计卡路里 累计 0~999 可上报
      7 当月累计计时 累计 0~44640 仅用于计算当月剩余
      8 30 天累计计时 累计 0~43200 可上报
      9 30 天累计圈数 累计 0~9999999 可上报
      10 30 天累计卡路里 累计 0~99999 可上报
      11 当日目标时长 目标 0~45 云端下发
      12 当月目标时长 目标 0~1350 云端下发
      13 当日目标剩余 剩余 0~45 可上报
      14 当月目标剩余 剩余 0~1350 可上报
      15 每日累计时长 累计 0~1440 30 个数据,仅用于计算 30 天累计
      16 每日累计圈数 累计 0~99999 30 个数据,仅用于计算 30 天累计
      17 每日累计卡路里 累计 0~999 30 个数据,仅用于计算 30 天累计

      数据更新处理的功能需求梳理如下:

      更新节点 更新数据 数据类别 更新方式
      每分钟 时间 实时、累计
      剩余
      正向计数,实时超过 999 时从 0 开始计数
      反向计数,等于 0 时停止
      每圈 圈数/卡路里 实时、累计 圈数:正向计数;卡路里:按 1kcal=0.1 圈计算
      目标设定时 时间 剩余 目标>累计时,目标剩余=目标-累计
      日期变更时 计时/圈数/卡路里 30 天累计
      当日累计
      减去 30 天前的数据,每日累计数据迁移
      清零
      月份变更时 时间 当月累计 清零
      模式更新时 计时/圈数/卡路里 实时 清零
      停止使用时 计时/圈数/卡路里 实时 清零

      代码实现

      /* 第一步:定义数据类型 (tuya_hula_hoop_svc_data.h) */
      typedef struct {
          USHORT_T time_realtime;
          USHORT_T count_realtime;
          USHORT_T calories_realtime;
          USHORT_T time_total_today;
          UINT_T count_total_today;
          USHORT_T calories_total_today;
          USHORT_T time_total_month;
          USHORT_T time_total_30days;
          UINT_T count_total_30days;
          UINT_T calories_total_30days;
          UCHAR_T time_target_today;
          USHORT_T time_target_month;
          UCHAR_T time_remain_today;
          USHORT_T time_remain_month;
          USHORT_T time_total_days[SPORT_DATA_HISTORY_SIZE];
          UINT_T count_total_days[SPORT_DATA_HISTORY_SIZE];
          USHORT_T calories_total_days[SPORT_DATA_HISTORY_SIZE];
      } HULA_HOOP_SPORT_DATA_T;
      
      /* 第二步:定义运动数据并初始化 (tuya_hula_hoop_svc_data.c) */
      HULA_HOOP_SPORT_DATA_T g_sport_data;
      /**
       * @brief 运动数据处理模块初始化
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_data_proc_init(VOID_T)
      {
          memset(&g_sport_data, 0, SIZEOF(HULA_HOOP_SPORT_DATA_T));
      }
      
      /* 第三步:编写每分钟处理的运动数据更新函数 (tuya_hula_hoop_svc_data.c) */
      /**
       * @brief 更新运动数据 --时间
       * @param[in] none
       * @return 1-目标完成,0-目标未完成
       */
      BOOL_T hula_hoop_update_sport_data_time(VOID_T)
      {
          /* 实时计时 */
          if (g_sport_data.time_realtime < 999) {
              g_sport_data.time_realtime++;
          } else {
              g_sport_data.time_realtime = 0;
          }
          /* 累计计时 */
          g_sport_data.time_total_today++;
          g_sport_data.time_total_month++;
          g_sport_data.time_total_30days++;
          /* 目标剩余时长 */
          if (g_sport_data.time_remain_today > 0) {
              g_sport_data.time_remain_today--;
              if (g_sport_data.time_remain_today == 0) {
                  return TRUE;/* 目标剩余倒计时为 0 时,告知目标完成 */
              }
          }
          if (g_sport_data.time_remain_month > 0) {
              g_sport_data.time_remain_month--;
          }
          return FALSE;
      }
      
      /* 第四步:编写每圈处理的运动数据更新函数 (tuya_hula_hoop_svc_data.c) */
      /**
       * @brief 更新运动数据 --圈数
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_update_sport_data_count(VOID_T)
      {
          /* 实时圈数 */
          g_sport_data.count_realtime++;
          /* 累计圈数 */
          g_sport_data.count_total_today++;
          g_sport_data.count_total_30days++;
      }
      
      /**
       * @brief 更新运动数据 --卡路里
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_update_sport_data_calories(VOID_T)
      {
          /* 实时卡路里 */
          g_sport_data.calories_realtime = g_sport_data.count_realtime / 10;
          /* 累计卡路里 */
          g_sport_data.calories_total_today = g_sport_data.count_total_today / 10;
          g_sport_data.calories_total_30days = g_sport_data.count_total_30days / 10;
      }
      
      /* 第五步:编写目标设定时的运动数据处理函数 (tuya_hula_hoop_svc_data.c) */
      /**
       * @brief 设置当日目标时间
       * @param[in] tar_tm: 目标时间
       * @return none
       */
      VOID_T hula_hoop_set_time_target_today(IN CONST UCHAR_T tar_tm)
      {
          if (tar_tm == g_sport_data.time_target_today) {
              return;
          }
          g_sport_data.time_target_today = tar_tm;
          if (tar_tm > g_sport_data.time_total_today) {
              g_sport_data.time_remain_today = tar_tm - g_sport_data.time_total_today;
          } else {
              g_sport_data.time_remain_today = 0;
          }
      }
      
      /**
       * @brief 设置当月目标时间
       * @param[in] tar_tm: 目标时间
       * @return none
       */
      VOID_T hula_hoop_set_time_target_month(IN CONST USHORT_T tar_tm)
      {
          if (tar_tm == g_sport_data.time_target_month) {
              return;
          }
          g_sport_data.time_target_month = tar_tm;
          if (tar_tm > g_sport_data.time_total_month) {
              g_sport_data.time_remain_month = tar_tm - g_sport_data.time_total_month;
          } else {
              g_sport_data.time_remain_month = 0;
          }
      }
      
      /* 第六步:编写日期变更和月份变更时的运动数据处理函数 (tuya_hula_hoop_svc_data.c) */
      /**
       * @brief 更新当日累计数据
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_update_total_data_day(VOID_T)
      {
          UCHAR_T i;
      
          g_sport_data.time_total_30days -= g_sport_data.time_total_days[0];
          g_sport_data.count_total_30days -= g_sport_data.count_total_days[0];
          g_sport_data.calories_total_30days -= g_sport_data.calories_total_days[0];
          for (i = 0; i < (SPORT_DATA_HISTORY_SIZE-1); i++) {
              g_sport_data.time_total_days[i] = g_sport_data.time_total_days[i+1];
              g_sport_data.count_total_days[i] = g_sport_data.count_total_days[i+1];
              g_sport_data.calories_total_days[i] = g_sport_data.calories_total_days[i+1];
          }
          g_sport_data.time_total_days[i] = 0;
          g_sport_data.count_total_days[i] = 0;
          g_sport_data.calories_total_days[i] = 0;
          g_sport_data.time_total_today = 0;
          g_sport_data.count_total_today = 0;
          g_sport_data.calories_total_today = 0;
      }
      
      /**
       * @brief 更新当月累计数据
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_update_total_data_month(VOID_T)
      {
          g_sport_data.time_total_month = 0;
      }
      
      /* 第七步:编写用于运动数据清零的相关函数 (tuya_hula_hoop_svc_data.c) */
      /**
       * @brief 清除实时数据
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_clear_realtime_data(VOID_T)
      {
          g_sport_data.time_realtime = 0;
          g_sport_data.count_realtime = 0;
          g_sport_data.calories_realtime = 0;
      }
      
      /**
       * @brief 清除所有运动数据
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_clear_sport_data(VOID_T)
      {
          memset(&g_sport_data, 0, SIZEOF(g_sport_data));
      }
      
      /* 第八步:在头文件中定义外部调用接口 (tuya_hula_hoop_svc_data.h) */
      VOID_T hula_hoop_data_proc_init(VOID_T);
      BOOL_T hula_hoop_update_sport_data_time(VOID_T);
      VOID_T hula_hoop_update_sport_data_count(VOID_T);
      VOID_T hula_hoop_update_sport_data_calories(VOID_T);
      VOID_T hula_hoop_set_time_target_today(IN CONST UCHAR_T tar_tm);
      VOID_T hula_hoop_set_time_target_month(IN CONST USHORT_T tar_tm);
      VOID_T hula_hoop_update_total_data_day(VOID_T);
      VOID_T hula_hoop_update_total_data_month(VOID_T);
      VOID_T hula_hoop_clear_realtime_data(VOID_T);
      VOID_T hula_hoop_clear_sport_data(VOID_T);
      

      用户事件处理

      用户可触发的部件有按键和霍尔传感器,前者用于模式切换、配网请求和设备复位,后者用于检测呼啦圈转动圈数。根据功能需求描述,用户事件处理模块的处理内容梳理如下:

      事件 事件类型 限制条件 处理内容
      触发模式键 短按 模式选择界面 切换“预选模式”
      其他 进入“模式选择”处理
      长按 2 秒 模式选择界面 模式更新为“预选模式”,数据上报
      长按 5 秒 - 允许配网
      触发复位键 短按 模式选择界面 退出“模式选择”处理
      长按 2 秒 【使用中】 切换到复位提示界面
      触发任意键 任意类型 熄屏状态 点亮屏幕
      亮屏状态 复位“熄屏计时”和“停用计时”
      磁铁接触霍尔 短触 - 数据更新,→【转动中】,“复位停转计时”

      代码实现

      外设驱动的部分可以使用之前编写的 KEY 驱动组件和 HALL_SW 驱动组件快速进行开发,本 demo 硬件方案如下:

      外设 引脚 有效电平
      模式键 PB7 L
      复位键 PB1 L
      霍尔传感器 PD2 H

      下面开始编写用户事件处理模块:

      /* 第一步:定义按键&霍尔注册信息,编写初始化函数 (tuya_hula_hoop_evt_user.c) */
      /* 回调函数定义 */
      STATIC VOID_T __mode_key_cb(KEY_PRESS_TYPE_E type);
      STATIC VOID_T __reset_key_cb(KEY_PRESS_TYPE_E type);
      STATIC VOID_T __hall_switch_cb(KEY_PRESS_TYPE_E type);
      /* 模式键注册信息定义 */
      KEY_DEF_T mode_key_def_s = {
          .port = TY_GPIOB_7,
          .active_low = TRUE,
          .long_press_time1 = 2000,
          .long_press_time2 = 5000,
          .key_cb = __mode_key_cb
      };
      /* 复位键注册信息定义 */
      KEY_DEF_T reset_key_def_s = {
          .port = TY_GPIOB_1,
          .active_low = TRUE,
          .long_press_time1 = 2000,
          .long_press_time2 = 0,
          .key_cb = __reset_key_cb
      };
      /* 霍尔传感器注册信息定义 */
      HALL_SW_DEF_T hall_sw_def_s = {
          .port = TY_GPIOD_2,
          .active_low = FALSE,
          .hall_sw_cb = __hall_sw_cb,
          .invalid_intv = 200000
      };
      
      /**
       * @brief 按键&霍尔处理模块初始化
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_key_hall_init(VOID_T)
      {
          UCHAR_T ret;
          /* 注册模式键 */
          ret = tuya_reg_key(&mode_key_def_s);
          if (KEY_OK != ret) {
              TUYA_APP_LOG_ERROR("mode key init error: %d", ret);
          }
          /* 注册复位键 */
          ret = tuya_reg_key(&reset_key_def_s);
          if (KEY_OK != ret) {
              TUYA_APP_LOG_ERROR("reset key init error: %d", ret);
          }
          /* 注册霍尔传感器 */
          ret = tuya_reg_hall_sw(&hall_sw_def_s);
          if (HSW_OK != ret) {
              TUYA_APP_LOG_ERROR("hall switch init error: %d", ret);
          }
      }
      
      
      /* 第二步:在头文件中定义用户事件模块初始化接口 (tuya_hula_hoop_evt_user.h) */
      VOID_T hula_hoop_key_hall_init(VOID_T);
      
      /* 第三步:编写按键回调函数和各事件处理函数 (tuya_hula_hoop_evt_user.c) */
      /**
       * @brief 模式键短按处理
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __mode_key_short_press_handler(VOID_T)
      {
          if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {/* 当前显示界面不是[模式选择] */
              hula_hoop_enter_mode_select();					/* 进入模式选择 */
          } else {
              hula_hoop_switch_temp_mode();					/* 退出模式选择 */
          }
      }
      
      /**
       * @brief 模式键长按 2s 处理
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __mode_key_long_press_handler(VOID_T)
      {
          /* 当前显示界面不是[模式选择]时不处理 */
          if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
              return;
          }
          /* 切换到预选模式并上报工作模式相关 DP 数据 */
          hula_hoop_switch_to_select_mode();
          hula_hoop_reset_upd_time_data_timer();
          hula_hoop_report_mode();
      }
      
      /**
       * @brief 模式键长按 5s 处理
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __mode_key_longer_press_handler(VOID_T)
      {
          /* 允许用户绑定 */
          hula_hoop_allow_binding();
      }
      
      /**
       * @brief 复位键短按处理
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __reset_key_short_press_handler(VOID_T)
      {
          /* 当前显示界面不是[模式选择]时不处理 */
          if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
              return;
          }
          /* 退出模式选择 */
          hula_hoop_quit_mode_select();
      }
      
      /**
       * @brief 复位键长按 2s 处理
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __reset_key_long_press_handler(VOID_T)
      {
          /* 设备当前状态不是使用中时不处理 */
          if (hula_hoop_get_device_status() != STAT_USING) {
              return;
          }
          /* 切换到复位提醒 */
          hula_hoop_disp_switch_to_reset_remind();
      }
      
      /**
       * @brief 霍尔传感器触发时处理
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __hall_switch_handler(VOID_T)
      {
          hula_hoop_update_sport_data_count();	/* 更新圈数数据 */
          hula_hoop_update_sport_data_calories();	/* 更新卡路里数据 */
          hula_hoop_set_work_stat(STAT_ROTATING);	/* 设置设备状态为[旋转中] */
          hula_hoop_reset_timer_for_hall_event();	/* 复位霍尔事件发生禁止的定时器计时 */
      }
      
      /**
       * @brief 按键事件通用处理
       * @param[in] none
       * @return TRUE - 处理完成 FALSE - 处理未完成
       */
      STATIC BOOL_T __key_event_handler(VOID_T)
      {
          /* 熄屏时,唤醒屏幕 */
          if (FALSE == hula_hoop_disp_is_wakeup()) {
              hula_hoop_disp_wakeup();
              /* 未使用->使用中 */
              if (hula_hoop_get_device_status() == STAT_UNUSED) {
                  hula_hoop_set_device_status(STAT_USING);
              }
              return TRUE;
          /* 亮屏时,复位熄屏定时器计时 */
          } else {
              hula_hoop_reset_timer_for_key_event();
          }
          return FALSE;
      }
      
      /**
       * @brief 模式键回调函数
       * @param[in] type: 事件类型
       * @return none
       */
      STATIC VOID_T __mode_key_cb(KEY_PRESS_TYPE_E type)
      {
          /* 按键唤醒屏幕时不处理短按事件 */
          BOOL_T ret = __key_event_handler();
          if ((ret) &&
              (type == SHORT_PRESS)) {
              return;
          }
      	/* 事件类型判断 */
          switch (type) {
          case SHORT_PRESS:
              TUYA_APP_LOG_INFO("mode key pressed.");
              __mode_key_short_press_handler();
              break;
          case LONG_PRESS_FOR_TIME1:
              TUYA_APP_LOG_INFO("mode key long pressed for time1.");
              __mode_key_long_press_handler();
              break;
          case LONG_PRESS_FOR_TIME2:
              TUYA_APP_LOG_INFO("mode key long pressed for time2.");
              __mode_key_longer_press_handler();
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 复位键回调函数
       * @param[in] type: 事件类型
       * @return none
       */
      STATIC VOID_T __reset_key_cb(KEY_PRESS_TYPE_E type)
      {
          /* 按键唤醒屏幕时不处理短按事件 */
          BOOL_T ret = __key_event_handler();
          if ((ret) &&
              (type == SHORT_PRESS)) {
              return;
          }
      	/* 事件类型判断 */
          switch (type) {
          case SHORT_PRESS:
              TUYA_APP_LOG_INFO("reset key short pressed.");
              __reset_key_short_press_handler();
              break;
          case LONG_PRESS_FOR_TIME1:
              TUYA_APP_LOG_INFO("reset key long pressed for time1.");
              __reset_key_long_press_handler();
              break;
          case LONG_PRESS_FOR_TIME2:
              break;
          default:
              break;
          }
      }
      
      /**
       * @brief 霍尔传感器回调函数
       * @param[in] none
       * @return none
       */
      STATIC VOID_T __hall_sw_cb()
      {
          __hall_switch_handler();
      }
      

      定时事件处理

      本 demo 的功能设定中涉及较多的定时需求,具体情况整理如下:

      No. 定时器名称 执行条件 定时时间 超时处理内容
      1 屏幕熄灭确认 亮屏状态、【转动中】、无按键事件 6 秒 熄灭屏幕
      2 屏幕点亮确认 熄屏状态、【转动中】 5 分钟 点亮屏幕
      3 停止转动确认 【转动中】、无霍尔事件 3 秒 设置设备状态为【使用中】
      4 停止使用确认 【使用中】、无按键事件 30 秒 设置设备状态为【未使用】
      5 数据显示切换 普通模式界面 2 秒 切换显示数据
      6 计时数据更新 【转动中】 1 分钟 更新计时数据并上报
      7 本地时间更新 - 1 秒 更新本地时间、检查日期变更
      8 数据上报更新 - 5 秒 上报圈数&卡路里数据
      9 配网等待结束 设备未被用户绑定且正在等待配网 1 分钟 禁止配网

      代码实现

      /* 第一步:定义定时器数据结构 (tuya_hula_hoop_evt_timer.c) */
      typedef struct {
          UINT_T disp_sleep;
          UINT_T disp_wakeup;
          UINT_T stop_rotating;
          UINT_T stop_using;
          UINT_T switch_disp_data;
          UINT_T upd_time_data;
          UINT_T upd_local_time;
          UINT_T repo_dp_data;
          UINT_T wait_bind;
      } HULA_HOOP_TIMER_T;
      
      /* 第二步:定义定时时间相关宏 (tuya_hula_hoop_evt_timer.c) */
      #define TIMER_PERIOD_MS                 (100)       /* 100ms */
      #define DISP_SLEEP_CONFIRM_TIME_MS      (6*1000)    /* 6s */
      #define DISP_WAKEUP_CONFIRM_TIME_MS     (5*60*1000) /* 5min */
      #define STOP_ROTATING_CONFIRM_TIME_MS   (3000)      /* 3s */
      #define STOP_USING_CONFIRM_TIME_MS      (30*1000)   /* 30s */
      #define DISP_DATA_SWITCH_INTV_MS        (2000)      /* 2s */
      #define TIME_DATA_UPDATE_INTV_MS        (1*60*1000) /* 1min */
      #define LOCAL_TIME_UPDATE_INTV_MS       (1000)      /* 1s */
      #define DP_DATA_REPO_INTV_MS            (5*1000)    /* 5s */
      #define WAIT_BIND_END_TIME_MS           (1*60*1000) /* 1min */
      
      /* 第三步:定义定时器变量并编写初始化函数 (tuya_hula_hoop_evt_timer.c) */
      STATIC HULA_HOOP_TIMER_T sg_timer;
      
      /**
       * @brief 定时处理模块初始化,创建 1 个 100ms 软件定时器
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_timer_init(VOID_T)
      {
          memset(&sg_timer, 0, SIZEOF(HULA_HOOP_TIMER_T));
          tuya_software_timer_create(TIMER_PERIOD_MS*1000, __timer_timeout_handler);
      }
      
      /* 第四步:编写各个定时处理函数 (tuya_hula_hoop_evt_timer.c) */
      /**
       * @brief 屏幕休眠确认定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __disp_sleep_timer(IN CONST UINT_T time_inc)
      {
          if ((FALSE == hula_hoop_disp_is_wakeup()) ||
              (hula_hoop_get_device_status() != STAT_ROTATING)   ||
              (F_WAIT_BINDING == SET)) {
              sg_timer.disp_sleep = 0;
              return;
          }
          sg_timer.disp_sleep += time_inc;
          if (sg_timer.disp_sleep >= DISP_SLEEP_CONFIRM_TIME_MS) {
              if (FALSE == hula_hoop_disp_is_flash()) {
                  hula_hoop_disp_sleep();
              }
          }
      }
      
      /**
       * @brief 屏幕唤醒确认定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __disp_wakeup_timer(IN CONST UINT_T time_inc)
      {
          if ((TRUE == hula_hoop_disp_is_wakeup()) ||
              (hula_hoop_get_device_status() != STAT_ROTATING)) {
              sg_timer.disp_wakeup = 0;
              return;
          }
          sg_timer.disp_wakeup += time_inc;
          if (sg_timer.disp_wakeup >= DISP_WAKEUP_CONFIRM_TIME_MS) {
              sg_timer.disp_wakeup = 0;
              hula_hoop_disp_wakeup();
          }
      }
      
      /**
       * @brief 停止转动确认定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __stop_rotating_timer(IN CONST UINT_T time_inc)
      {
          if (hula_hoop_get_device_status() != STAT_ROTATING) {
              sg_timer.stop_rotating = 0;
              return;
          }
          sg_timer.stop_rotating += time_inc;
          if (sg_timer.stop_rotating >= STOP_ROTATING_CONFIRM_TIME_MS) {
              sg_timer.stop_rotating = 0;
              hula_hoop_set_device_status(STAT_USING);
          }
      }
      
      /**
       * @brief 停止使用确认定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __stop_using_timer(IN CONST UINT_T time_inc)
      {
          if ((hula_hoop_get_device_status() != STAT_USING) ||
              (F_WAIT_BINDING == SET)) {
              sg_timer.stop_using = 0;
              return;
          }
          sg_timer.stop_using += time_inc;
          if (sg_timer.stop_using >= STOP_USING_CONFIRM_TIME_MS) {
              sg_timer.stop_using = 0;
              hula_hoop_set_device_status(STAT_UNUSED);
          }
      }
      
      /**
       * @brief 显示数据切换定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __switch_disp_data_timer(IN CONST UINT_T time_inc)
      {
          if ((hula_hoop_get_disp_mode() != DISP_NORMAL_MODE) ||
              (FALSE == hula_hoop_disp_is_wakeup())) {
              sg_timer.switch_disp_data = 0;
              return;
          }
          sg_timer.switch_disp_data += time_inc;
          if (sg_timer.switch_disp_data >= DISP_DATA_SWITCH_INTV_MS) {
              sg_timer.switch_disp_data -= DISP_DATA_SWITCH_INTV_MS;
              hula_hoop_switch_disp_data();
          }
      }
      
      /**
       * @brief 计时数据更新定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __upd_time_data_timer(IN CONST UINT_T time_inc)
      {
          if (hula_hoop_get_device_status() != STAT_ROTATING) {
              return;
          }
          sg_timer.upd_time_data += time_inc;
          if (sg_timer.upd_time_data >= TIME_DATA_UPDATE_INTV_MS) {
              sg_timer.upd_time_data -= TIME_DATA_UPDATE_INTV_MS;
              if (hula_hoop_update_sport_data_time()) {
                  hula_hoop_disp_target_finish();
              }
              hula_hoop_report_sport_data1();
          }
      }
      
      /**
       * @brief 本地时间更新定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __upd_local_time_timer(IN CONST UINT_T time_inc)
      {
          sg_timer.upd_local_time += time_inc;
          if (sg_timer.upd_local_time >= LOCAL_TIME_UPDATE_INTV_MS) {
              sg_timer.upd_local_time -= LOCAL_TIME_UPDATE_INTV_MS;
              tuya_local_time_update();
              hula_hoop_check_date_change();
          }
      }
      
      /**
       * @brief DP 数据上报定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __repo_dp_data_timer(IN CONST UINT_T time_inc)
      {
          sg_timer.repo_dp_data += time_inc;
          if (sg_timer.repo_dp_data >= DP_DATA_REPO_INTV_MS) {
              sg_timer.repo_dp_data -= DP_DATA_REPO_INTV_MS;
              hula_hoop_report_sport_data2();
          }
      }
      
      /**
       * @brief 配网等待停止定时
       * @param[in] time_inc: 时间增量
       * @return none
       */
      STATIC VOID_T __wait_bind_timer(IN CONST UINT_T time_inc)
      {
          if ((F_BLE_BOUND == SET) || (F_WAIT_BINDING == CLR)) {
              sg_timer.wait_bind = 0;
              return;
          }
          sg_timer.wait_bind += time_inc;
          if (sg_timer.wait_bind >= WAIT_BIND_END_TIME_MS) {
              sg_timer.wait_bind = 0;
              hula_hoop_prohibit_binding();
          }
      }
      
      /* 第五步:编写 100ms 超时处理函数 (tuya_hula_hoop_evt_timer.c) */
      /**
       * @brief 定时处理
       * @param[in] none
       * @return none
       */
      STATIC INT_T __timer_timeout_handler(VOID_T)
      {
          __disp_sleep_timer(TIMER_PERIOD_MS);
          __disp_wakeup_timer(TIMER_PERIOD_MS);
          __stop_rotating_timer(TIMER_PERIOD_MS);
          __stop_using_timer(TIMER_PERIOD_MS);
          __switch_disp_data_timer(TIMER_PERIOD_MS);
          __upd_time_data_timer(TIMER_PERIOD_MS);
          __upd_local_time_timer(TIMER_PERIOD_MS);
          __repo_dp_data_timer(TIMER_PERIOD_MS);
          __wait_bind_timer(TIMER_PERIOD_MS);
          return 0;
      }
      
      /* 第六步:编写定时器计时复位函数,供外部复位 (tuya_hula_hoop_evt_timer.c) */
      /**
       * @brief 按键事件发生时相关计时复位
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_reset_timer_for_key_event(VOID_T)
      {
          sg_timer.disp_sleep = 0;
          sg_timer.stop_using = 0;
      }
      
      /**
       * @brief 霍尔事件发生时相关计时复位
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_reset_timer_for_hall_event(VOID_T)
      {
          sg_timer.stop_rotating = 0;
      }
      
      /**
       * @brief 复位“计时更新”定时器
       * @param[in] none
       * @return none
       */
      VOID_T hula_hoop_reset_upd_time_data_timer(VOID_T)
      {
          sg_timer.upd_time_data = 0;
      }
      
      /* 第七步:在头文件中定义相关接口 (tuya_hula_hoop_evt_timer.c) */
      VOID_T hula_hoop_timer_init(VOID_T);
      VOID_T hula_hoop_reset_timer_for_key_event(VOID_T);
      VOID_T hula_hoop_reset_timer_for_hall_event(VOID_T);
      VOID_T hula_hoop_reset_upd_time_data_timer(VOID_T);
      

      联网相关处理

      • 配网等待处理

        要实现和云端进行数据交互,首先要使设备配网。配网功能已经通过 Bluetooth LE SDK 实现,下面来介绍本 demo 的配网等待处理和配网提醒机制:

        No. 设备状态 执行动作 配网指示灯
        1 上电时,检测到已被用户绑定 不等待配网(保持蓝牙广播) 不闪烁
        2 上电时,检测到未被用户绑定 开始等待配网(保持蓝牙广播) 开始闪烁
        3 长按模式键 5 秒时 开始等待配网,打开蓝牙广播 开始闪烁
        4 开始等待配网 1 分钟后,检测到仍未被用户绑定 停止等待配网,关闭蓝牙广播 停止闪烁
        5 开始等待配网后 1 分钟内,检测到已被用户连接 停止等待配网(保持蓝牙广播) 停止闪烁
        6 检测到被用户解绑 不等待配网,关闭蓝牙广播 不闪烁

        第 1 步:实现初期上电时的配网状态检测处理,即上表中的<1>和<2>:

        /* 第一步:定义配网相关处理标志 (tuya_hula_hoop_ble_proc.c/.h) */
        FLAG_BIT g_ble_proc_flag;
            #define F_BLE_BOUND     g_ble_proc_flag.bit0
            #define F_WAIT_BINDING  g_ble_proc_flag.bit1
        
        /* 第二步:编写初期配网状态检测处理函数 (tuya_hula_hoop_ble_proc.c) */
        /**
        * @brief 蓝牙处理模块初始化
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_ble_proc_init(VOID_T)
        {
            /* 使用 TUYA Bluetooth LE SDK 提供的 API 获取当前蓝牙连接状态 */
            tuya_ble_connect_status_t ble_conn_sta;
            ble_conn_sta = tuya_ble_connect_status_get();
            TUYA_APP_LOG_DEBUG("ble connect status: %d", ble_conn_sta);
            /* 判断与标记 */
            if ((ble_conn_sta == BONDING_UNCONN) ||
                (ble_conn_sta == BONDING_CONN)   ||
                (ble_conn_sta == BONDING_UNAUTH_CONN)) {
                F_BLE_BOUND = SET;							/* 标记为[已绑定] */
                F_WAIT_BINDING = CLR;						/* 标记为[禁止等待配网] */
            } else {
                F_BLE_BOUND = CLR;							/* 标记为[未绑定] */
                F_WAIT_BINDING = SET;						/* 标记为[允许等待配网] */
                hula_hoop_disp_set_led_func(LED_FUNC_BIND);	/* 设置 LED 功能为[配网指示] */
            }
        }
        
        /* 第三步:在头文件中定义初始化接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_ble_proc_init(VOID_T);
        
        /* 第四步:在设备初始化函数 tuya_hula_hoop_init()中调用 (tuya_hula_hoop.c) */
        

        第 2 步:实现长按模式键打开配网功能。除上电时允许设备配网外,用户还可以通过长按模式键 5 秒来打开配网功能,即上表中的<3>:

        /* 第一步:编写允许配网处理函数 (tuya_hula_hoop_ble_proc.c) */
        /**
        * @brief 允许用户绑定设备
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_allow_binding(VOID_T)
        {
            if (F_BLE_BOUND) {								/* [已绑定]? */
                tuya_ble_device_factory_reset();			/* 设备端解绑 */
            }
            F_WAIT_BINDING = SET;							/* 标记为[允许等待配网] */
            bls_ll_setAdvEnable(1);							/* 打开蓝牙广播 */
            hula_hoop_disp_set_led_func(LED_FUNC_BIND);		/* 修改 LED 功能为[配网指示] */
        }
        
        /* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_allow_binding(VOID_T);
        
        /* 第三步:在模式键处理函数中调用 (tuya_hula_hoop_evt_user.c) */
        /**
        * @brief 模式键长按 5 秒处理函数
        * @param[in] none
        * @return none
        */
        STATIC VOID_T __mode_key_longer_press_handler(VOID_T)
        {
            hula_hoop_allow_binding();						/* 允许用户绑定设备 */
        }
        

        第 3 步:实现关闭配网功能。配网功能开启 1 分钟后,如果仍未被绑定则需要关闭配网功能,即上表中的<4>:

        /* 第一步:编写禁止配网处理函数 (tuya_hula_hoop_ble_proc.c) */
        /**
        * @brief 禁止用户绑定设备
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_prohibit_binding(VOID_T)
        {
            F_WAIT_BINDING = CLR;							/* 标记为[禁止等待配网] */
            bls_ll_setAdvEnable(0);							/* 关闭蓝牙广播 */
            hula_hoop_disp_set_led_func(LED_FUNC_DATA);		/* 修改 LED 功能为[数据指示] */
        }
        
        /* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_prohibit_binding(VOID_T);
        
        /* 第三步:在定时模块中定义的 1 分钟配网等待定时处理函数中调用  (tuya_hula_hoop_tiemr.c) */
        /**
        * @brief 等待配网定时
        * @param[in] time_inc: 时间增量
        * @return none
        */
        STATIC VOID_T __ble_wait_bind_timer(IN CONST UINT_T time_inc)
        {
            /* [已绑定]或[禁止等待配网]时不处理 */
            if ((F_BLE_BOUND == SET) || (F_WAIT_BINDING == CLR)) {
                g_timer.ble_wait_bind = 0;
                return;
            }
            /* 1 分钟配网等待定时 */
            g_timer.ble_wait_bind += time_inc;
            if (g_timer.ble_wait_bind >= BLE_WAIT_BIND_END_MS) {
                g_timer.ble_wait_bind = 0;
                hula_hoop_prohibit_binding();				/* 禁止用户绑定设备 */
            }
        }
        

        第 4 步:实现蓝牙连接状态改变时和被解绑时的处理,即上表中的<5>和<6>:

        /* 第一步:编写蓝牙连接状态改变时和设备被解绑时的处理函数 */
        /**
        * @brief 蓝牙连接状态更新处理
        * @param[in] status: 蓝牙连接状态
        * @return none
        */
        VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
        {
            if (status == BONDING_CONN) {               			/* 蓝牙已连接? */
                __report_all_dp_data();                 			/* 上报所有 DP 数据 */
                tuya_ble_time_req(BLE_TIME_TYPE_NORMAL);			/* 发送获取云端时间请求 */
                if (F_WAIT_BINDING == SET) {            			/* 之前未绑定? */
                    F_BLE_BOUND = SET;                  			/* 标记为[已绑定] */
                    F_WAIT_BINDING = CLR;               			/* 标记为[禁止等待配网] */
                    hula_hoop_disp_set_led_func(LED_FUNC_DATA);		/* 修改 LED 功能为[数据指示] */
                }
                if (hula_hoop_get_device_status() == STAT_USING) {	/* [使用中]? */
                    hula_hoop_reset_timer_for_key_event();			/* 复位相关计时 */
                }
            }
        }
        
        /**
        * @brief 设备被解绑处理
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_ble_unbound_handler(VOID_T)
        {
            F_BLE_BOUND = CLR;		/* 标记为[未绑定] */
            bls_ll_setAdvEnable(0);	/* 停止蓝牙广播 */
        }
        
        /* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status);
        VOID_T hula_hoop_ble_unbound_handler(VOID_T);
        
        /* 第三步:在 Bluetooth LE SDK 消息处理 callback 函数中调用 (tuya_ble_app_demo.c) */
        static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
        {
            ...
            case TUYA_BLE_CB_EVT_CONNECTE_STATUS:	/* 蓝牙连接状态改变时 */
                hula_hoop_ble_conn_stat_handler(event->connect_status);	/* 蓝牙连接状态更新处理 */
                TUYA_APP_LOG_INFO("received tuya ble conncet status update event, current connect status = %d", event->connect_status);
                break;
            ...
            case TUYA_BLE_CB_EVT_UNBOUND:			/* App 端解绑设备时 */
                hula_hoop_ble_unbound_handler();	/* 设备被解绑处理 */
                TUYA_APP_LOG_INFO("received unbound req");
                break;
            case TUYA_BLE_CB_EVT_ANOMALY_UNBOUND:	/* 异常解绑时 */
                hula_hoop_ble_unbound_handler();	/* 设备被解绑处理 */
                TUYA_APP_LOG_INFO("received anomaly unbound req");
                break;
            ...
        }
        
      • 本地数据上报

        为了能让用户在 App 上实时查看设备状态,设备需在与 App 连接时,将可上报的功能点数据上报至云端。根据呼啦圈的功能特点,本 demo 的上报机制设定如下:

        No. 上报节点 上报功能点 备注
        1 设备连接到 App 时 所有功能点 初始化 App 显示
        2 设备工作模式改变时 工作模式和实时类功能点 工作模式变更时会清除实时数据
        3 设备状态为转动中时,每隔 1 分钟 计时类功能点 即计时数据更新时上报
        4 设备状态为转动中时,每隔 5 秒 圈数&卡路里类功能点 数据变化较频繁,隔一段时间上报

        代码实现

        /* 第一步:对功能定义时设定的 ID 进行宏定义 */
        #define DP_ID_MODE                  101
        #define DP_ID_TIME_REALTIME         102
        #define DP_ID_COUNT_REALTIME        103
        #define DP_ID_CALORIES_REALTIME     104
        #define DP_ID_TIME_TOTAL_TODAY      105
        #define DP_ID_COUNT_TOTAL_TODAY     106
        #define DP_ID_CALORIES_TOTAL_TODAY  107
        #define DP_ID_TIME_TOTAL_30DAYS     108
        #define DP_ID_COUNT_TOTAL_30DAYS    109
        #define DP_ID_CALORIES_TOTAL_30DAYS 110
        #define DP_ID_TIME_TARGET_TODAY     111
        #define DP_ID_TIME_TARGET_MONTH     112
        #define DP_ID_TIME_REMAIN_TODAY     113
        #define DP_ID_TIME_REMAIN_MONTH     114
        
        /* 第二步:对 DP 数据存放时各项内容的偏移量进行宏定义 */
        #define DP_DATA_INDEX_OFFSET_ID     0
        #define DP_DATA_INDEX_OFFSET_TYPE   1
        #define DP_DATA_INDEX_OFFSET_LEN    2
        #define DP_DATA_INDEX_OFFSET_DATA   3
        
        /* 第三步:定义用于存放 DP 数据的数组 */
        UCHAR_T dp_data_array[255+3];
        
        /* 第四步:编写 DP 数据上报相关函数 */
        /**
        * @brief 上报 1 个 DP 数据
        * @param[in] dp_id: DP ID
        * @param[in] dp_type: DP 类型
        * @param[in] dp_len: DP 数据长度
        * @param[in] dp_data: DP 数据存放地址
        * @return none
        */
        STATIC VOID_T __report_one_dp_data(IN CONST UCHAR_T dp_id, IN CONST UCHAR_T dp_type, IN CONST UCHAR_T dp_len, IN CONST UCHAR_T *dp_data)
        {
            UCHAR_T i;
            sg_repo_array[DP_DATA_INDEX_OFFSET_ID] = dp_id;
            sg_repo_array[DP_DATA_INDEX_OFFSET_TYPE] = dp_type;
            sg_repo_array[DP_DATA_INDEX_OFFSET_LEN] = dp_len;
            for (i = 0; i < dp_len; i++) {
                sg_repo_array[DP_DATA_INDEX_OFFSET_DATA + i] = *(dp_data + (dp_len-i-1));
            }
            tuya_ble_dp_data_report(sg_repo_array, dp_len + 3);
        }
        
        /**
        * @brief 添加 1 个 DP 数据
        * @param[in] dp_id: DP ID
        * @param[in] dp_type: DP 类型
        * @param[in] dp_len: DP 数据长度
        * @param[in] dp_data: DP 数据存放地址
        * @param[in] addr: DP 数据添加到上报数组中的位置
        * @return 该 DP 数据总长度
        */
        STATIC UCHAR_T __add_one_dp_data(IN CONST UCHAR_T dp_id, IN CONST UCHAR_T dp_type, IN CONST UCHAR_T dp_len, IN CONST UCHAR_T *dp_data, IN UCHAR_T *addr)
        {
            UCHAR_T i;
            *(addr + DP_DATA_INDEX_OFFSET_ID) = dp_id;
            *(addr + DP_DATA_INDEX_OFFSET_TYPE) = dp_type;
            *(addr + DP_DATA_INDEX_OFFSET_LEN) = dp_len;
            for (i = 0; i < dp_len; i++) {
                *(addr + DP_DATA_INDEX_OFFSET_DATA + i) = *(dp_data + (dp_len-i-1));
            }
            return (dp_len + 3);
        }
        
        /**
        * @brief 上报实时类 DP 数据
        * @param[in] none
        * @return none
        */
        VOID_T __report_sport_data_realtime(VOID_T)
        {
            UCHAR_T total_len = 0;
            total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
            tuya_ble_dp_data_report(sg_repo_array, total_len);
        }
        
        /**
        * @brief 上报工作模式变更时的 DP 数据
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_report_mode(VOID_T)
        {
            __report_one_dp_data(DP_ID_MODE, DT_ENUM, SIZEOF(g_hula_hoop.mode), &g_hula_hoop.mode);
            __report_sport_data_realtime();
        }
        
        /**
        * @brief 上报计时类 DP 数据
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_report_sport_data1(VOID_T)
        {
            UCHAR_T total_len = 0;
            total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_total_today), (UCHAR_T *)&g_sport_data.time_total_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.time_total_30days), (UCHAR_T *)&g_sport_data.time_total_30days, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month, (sg_repo_array + total_len));
            tuya_ble_dp_data_report(sg_repo_array, total_len);
        }
        
        /**
        * @brief 上报圈数&卡路里类 DP 数据
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_report_sport_data2(VOID_T)
        {
            UCHAR_T total_len = 0;
            total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.count_total_today), (UCHAR_T *)&g_sport_data.count_total_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.calories_total_today), (UCHAR_T *)&g_sport_data.calories_total_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.count_total_30days), (UCHAR_T *)&g_sport_data.count_total_30days, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.calories_total_30days), (UCHAR_T *)&g_sport_data.calories_total_30days, (sg_repo_array + total_len));
            tuya_ble_dp_data_report(sg_repo_array, total_len);
        }
        
        /**
        * @brief 上报所有 DP 数据
        * @param[in] none
        * @return none
        */
        STATIC VOID_T __report_all_dp_data(VOID_T)
        {
            UCHAR_T total_len = 0;
            total_len += __add_one_dp_data(DP_ID_MODE, DT_ENUM, SIZEOF(g_hula_hoop.mode), &g_hula_hoop.mode, sg_repo_array);
            total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_total_today), (UCHAR_T *)&g_sport_data.time_total_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.count_total_today), (UCHAR_T *)&g_sport_data.count_total_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.calories_total_today), (UCHAR_T *)&g_sport_data.calories_total_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.time_total_30days), (UCHAR_T *)&g_sport_data.time_total_30days, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.count_total_30days), (UCHAR_T *)&g_sport_data.count_total_30days, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.calories_total_30days), (UCHAR_T *)&g_sport_data.calories_total_30days, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today, (sg_repo_array + total_len));
            total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month, (sg_repo_array + total_len));
            tuya_ble_dp_data_report(sg_repo_array, total_len);
        }
        
        /* 第五步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_report_mode(VOID_T);
        VOID_T hula_hoop_report_sport_data1(VOID_T);
        VOID_T hula_hoop_report_sport_data2(VOID_T);
        
        /* 第六步:分别在各上报节点处调用 */
        /* <1> 设备连接到 App 时 (tuya_hula_hoop_ble_proc.c) */
        VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
        {
            if (status == BONDING_CONN) {               	/* 蓝牙已连接? */
                __report_all_dp_data();                 	/* 上报所有 DP 数据 */
                ...
            }
            ...
        }
        
        /* <2> 设备工作模式改变时 (tuya_hula_hoop_evt_user.c) */
        STATIC VOID_T __mode_key_long_press_handler(VOID_T)
        {
            if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
                return;
            }
            hula_hoop_switch_to_select_mode();
            hula_hoop_report_mode();			/* 上报工作模式和实时数据 */
        }
        
        /* <3> 计时数据更新时 (tuya_hula_hoop_evt_timer.c) */
        STATIC VOID_T __upd_time_data_timer(IN CONST UINT_T time_inc)
        {
            if (hula_hoop_get_device_status() != STAT_ROTATING) {
                g_timer.upd_time_data = 0;
                return;
            }
            g_timer.upd_time_data += time_inc;
            if (g_timer.upd_time_data >= TIME_DATA_UPDATE_INTV_MS) {
                g_timer.upd_time_data -= TIME_DATA_UPDATE_INTV_MS;
                if (hula_hoop_update_sport_data_time()) {
                    hula_hoop_disp_target_finish();
                }
                hula_hoop_report_sport_data1();	/* 上报计时数据 */
            }
        }
        
        /* <4> 上报定时处理中 (tuya_hula_hoop_evt_timer.c) */
        STATIC VOID_T __repo_dp_data_timer(IN CONST UINT_T time_inc)
        {
            g_timer.repo_dp_data += time_inc;
            if (g_timer.repo_dp_data >= DP_DATA_REPO_INTV_MS) {
                g_timer.repo_dp_data -= DP_DATA_REPO_INTV_MS;
                hula_hoop_report_sport_data2();	/* 上报圈数和卡路里数据 */
            }
        }
        
      • 接收数据处理

        当用户操作 App 设置可下发类型的功能点时,云端会将该功能点的 ID、类型、属性值、属性值长度下发给设备,此时设备只要在检测到云端下发数据事件后进行相应处理,即可实现云端任务。本 demo 可下发的功能点和接收到该功能点数据时需处理的内容如下:

        No. 下发功能点 接收到数据后需处理的内容 备注
        1 工作模式 更新设备工作模式,上报实时数据 工作模式变更时会清除实时数据
        2 当日目标时长 更新设备当日目标时长,上报当日目标剩余时长 当日目标变更时会改变当日剩余
        3 当月目标时长 更新设备当月目标时长,上报当月目标剩余时长 当月目标变更时会改变当月剩余

        代码实现

        /* 第一步:编写 DP 数据接收处理函数 */
        /**
        * @brief 呼啦圈 DP 数据接收处理
        * @param[in] dp_data: DP 数据存放数组
        * @return none
        */
        VOID_T hula_hoop_ble_dp_write_handler(IN UCHAR_T *dp_data)
        {
            if (hula_hoop_get_device_status() == STAT_USING) {
                hula_hoop_reset_timer_for_key_event();
            }
            switch (dp_data[0]) {
            case DP_ID_MODE:
                {
                    hula_hoop_set_work_mode(dp_data[3]);
                    __report_sport_data_realtime();
                    TUYA_APP_LOG_INFO("App set the mode : %d.", dp_data[3]);
                }
                break;
            case DP_ID_TIME_TARGET_TODAY:
                {
                    hula_hoop_set_time_target_today(dp_data[6]);
                    __report_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today);
                    TUYA_APP_LOG_INFO("App set today's target : %dmin.", dp_data[6]);
                }
                break;
            case DP_ID_TIME_TARGET_MONTH:
                {
                    USHORT_T tar_tm = (((USHORT_T)dp_data[5]) << 8) | ((USHORT_T)dp_data[6]);
                    hula_hoop_set_time_target_month(tar_tm);
                    __report_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month);
                    TUYA_APP_LOG_INFO("App set this month's target : %dmin.", tar_tm);
                }
                break;
            default:
                break;
            }
        }
        
        /* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_ble_dp_write_handler(IN UCHAR_T *dp_data);
        
        /* 第三步:在 Bluetooth LE SDK 消息处理 callback 函数中调用 (tuya_ble_app_demo.c) */
        static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
        {
            ...
            case TUYA_BLE_CB_EVT_DP_WRITE:	/* 在接收到 DP 数据时执行 */
                dp_data_len = event->dp_write_data.data_len;
                memset(dp_data_array, 0, sizeof(dp_data_array));
                memcpy(dp_data_array, event->dp_write_data.p_data, dp_data_len);
                hula_hoop_ble_dp_write_handler(dp_data_array);  /* 呼啦圈 DP 数据处理 */
                TUYA_APP_LOG_HEXDUMP_DEBUG("received dp write data :", dp_data_array, dp_data_len);
                sn = 0;
                tuya_ble_dp_data_report(dp_data_array, dp_data_len);
                break;
            ...
        }
        
      • 云端时间获取

        Bluetooth LE SDK 提供获取云端时间的功能,当设备连接 App 时,可以通过获取云端时间来校准本地时间。涂鸦 Bluetooth LE SDK 提供两种格式的时间获取,分别为 13 字节 ms 级字符串格式和年月日时分秒星期格式时间,这里我们使用后者。

        代码实现

        /* 第一步:在设备连接时发起获取云端时间请求 (tuya_hula_hoop_ble_proc.c) */
        VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
        {
            if (status == BONDING_CONN) {
                ...
                tuya_ble_time_req(BLE_TIME_TYPE_NORMAL);
                ...
            }
            ...
        }
        
        /* 第二步:编写云端时间下发后的处理函数 (tuya_hula_hoop_ble_proc.c) */
        /**
        * @brief 云端时间处理
        * @param[in] time_normal: 普通格式的云端时间
        * @return none
        */
        VOID_T hula_hoop_ble_time_normal_handler(IN CONST tuya_ble_time_noraml_data_t time_normal)
        {
            /* 类型转换并覆盖本地时间 */
            LOCAL_TIME_T time_now = {
                .year = time_normal.nYear + 2000,/* noraml 格式的年份为 2 位数形式,这里做简单处理 */
                .month = time_normal.nMonth,
                .day = time_normal.nDay,
                .hour = time_normal.nHour,
                .minute = time_normal.nMin,
                .second = time_normal.nSec
            };
            tuya_set_local_time(time_now, (time_normal.time_zone / 100));
            /* 日期变更检查处理 */
            hula_hoop_check_date_change();
            /* 本地时间更新日志打印 */
            TUYA_APP_LOG_INFO("Local time has been updated to %04d.%02d.%02d %02d:%02d:%02d.",
                            g_local_time.year, g_local_time.month, g_local_time.day,
                            g_local_time.hour, g_local_time.minute, g_local_time.second);
        }
        
        /* 第三步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
        VOID_T hula_hoop_ble_time_normal_handler(IN CONST tuya_ble_time_noraml_data_t time_normal);
        
        /* 第四步:在 Bluetooth LE SDK 消息处理 callback 函数中调用 (tuya_ble_app_demo.c) */
        static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
        {
            ...
            case TUYA_BLE_CB_EVT_TIME_NORMAL:	/* 云端下发时间时 */
                hula_hoop_ble_time_normal_handler(event->time_normal_data);	/* 云端时间处理 */
                break;
            ...
        }
        

        需要注意的是,云端下发的时间为零时区时间,同时云端会下发表示实际时区的数据,如 800 表示 800/100=东八区,如果要取得本地时间,需对时区问题进行处理:(tuya_local_time.c/.h)

        /**
        * @brief 设置本地时间,需处理时区问题
        * @param[in] time: 设置时间
        * @param[in] time_zone: 时区
        * @return none
        */
        VOID_T tuya_set_local_time(IN CONST LOCAL_TIME_T time, IN CONST INT_T time_zone)
        {
            /* 更新本地时间 */
            INT_T tmp_hour = time.hour;
            tmp_hour += time_zone;	/* 经过时区计算后的小时数 */
            g_local_time = time;
            /* 大于等于 24 时需做加一天处理 */
            if (tmp_hour >= 24) {
                g_local_time.hour = 0;
                __local_time_update_per_day();
                return;
            }
            /* 小于等于 0 时需做减一天处理 */
            if (tmp_hour <= 0) {
                g_local_time.hour = tmp_hour + 24;
                if (g_local_time.day > 1) {
                    g_local_time.day--;
                    return;
                }
                if (g_local_time.month > 1) {
                    g_local_time.month--;
                    sg_day_tbl[1] = __local_time_leap_year_judgment(g_local_time.year);
                    g_local_time.day = sg_day_tbl[g_local_time.month-1];
                } else {
                    g_local_time.year--;
                    g_local_time.month = 12;
                    g_local_time.day = sg_day_tbl[g_local_time.month-1];
                }
                return;
            }
            g_local_time.hour = tmp_hour;
        }
        
        /* 头文件中接口定义 */
        VOID_T tuya_set_local_time(IN CONST LOCAL_TIME_T time, IN CONST INT_T time_zone);
        

      优化处理

      关闭日志

      由于 I/O 资源有限,在完成功能调试后,需关闭日志打印功能,可通过将tuya_ble_app_init()函数中的elog_set_output_enabled(true);语句修改为elog_set_output_enabled(false);来实现禁止调试信息输出,同时还可以将custom_tuya_ble_config.hTUYA_APP_LOG_ENABLE的值修改为 0 来关闭应用日志,以减少代码空间。

      低功耗处理

      由于呼啦圈使用电池供电,为避免电量消耗过快,需对设备进行低功耗处理。
      BTU 模组原厂芯片的电源管理模块提供了 3 种低功耗工作模式

      工作模式 特点 唤醒方式
      暂停模式 MCU 暂停,PM 模块处于活动状态时,所有 SRAM 仍可访问,RF 收发器、音频和 USB 等模块断电,唤醒后从暂停处继续执行。 PAD/32k Timer/RESET Pin
      SRAM 保留的深度睡眠模式 PM 模块处于活动状态时,除两个 8KB 和一个 16KB 保留 SRAM 外,大多数模拟和所有数字模块都掉电。 PAD/32k Timer/RESET Pin
      深度睡眠模式 只有 PM 模块处于活动状态时,包括保持 SRAM 在内的大多数模拟和所有数字模块都处于断电状态。 PAD/32k Timer/RESET Pin

      对于 BTU 模组来说,这里的 SRAM 保留指的是 0x840000 - 0x847FFF 的存储空间,目前协议栈和应用都是默认优先使用这块空间,从编译生成的**.lst 文件**中也可以看到相关数据都存储在该范围内。也就是说,如果使用 SRAM 保留的深度睡眠模式,可以保证数据在唤醒后能继续使用,而不需要频繁地进行闪存读写操作。

      为了不影响呼啦圈的本地定时功能,这次选择了 SRAM 保留的深度睡眠模式作为低功耗模式,基本处理思路如下:

      <1> 在设备[未使用]时,设置唤醒方式为引脚唤醒或定时器唤醒,然后进入 SRAM 保留的深度睡眠模式;
      <2> 当设备被引脚唤醒时:更新本地时间后,设备状态切换到[使用中];
      <3> 当设备被定时器唤醒时:更新本地时间后再次睡眠。
      

      代码实现

      • 第 1 步:睡眠前处理

        在进入睡眠状态之前,如果设备已连接 App,需在睡眠前主动断开连接,否则唤醒后可能会无法连接 APP。我们可以在设备切换为未使用状态后,通过调用tuya_ble_gap_disconnect()接口实现断连;需要注意的是,调用该 API 后,断连状态不会立即生效,所以要在收到蓝牙状态变更事件回调后再执行睡眠动作。如果设备未连接 APP,则不需要进行上述处理,直接设置设备状态为睡眠状态即可。

        /* tuya_hula_hoop_evt_timer.c */
        /**
        * @brief 停用确认定时
        * @param[in] time_inc: 时间增量
        * @return none
        */
        STATIC VOID_T __stop_using_timer(IN CONST UINT_T time_inc)
        {
            if ((hula_hoop_get_device_status() != STAT_USING) ||
                (F_WAIT_BINDING == SET)) {
                g_timer.stop_using = 0;
                return;
            }
            g_timer.stop_using += time_inc;
            if (g_timer.stop_using >= STOP_USING_CONFIRM_TIME_MS) {
                g_timer.stop_using = 0;
                hula_hoop_set_device_status(STAT_UNUSED);		/* 设置设备状态为[未使用] */
                if (FALSE == hula_hoop_is_need_disconnect()) {	/* 判断是否需要进行主动断连操作 */
                    hula_hoop_set_device_status(STAT_SLEEP);	/* 设置设备状态为[睡眠] */
                }
            }
        }
        
        /* tuya_hula_hoop_ble_proc.c */
        /**
        * @brief 是否需要进行主动断连操作
        * @param[in] none
        * @return TRUE - 需要, FALSE - 不需要
        */
        BOOL_T hula_hoop_is_need_disconnect(VOID_T)
        {
            bls_ll_setAdvEnable(0);
            /* 已连接时处理 */
            if (tuya_ble_connect_status_get() == BONDING_CONN) {
                tuya_ble_gap_disconnect();	/* 断开蓝牙连接 */
                return TRUE;
            }
            return FALSE;
        }
        
        /**
        * @brief 蓝牙连接状态变更处理
        * @param[in] status: 蓝牙连接状态
        * @return none
        */
        VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
        {
            if (hula_hoop_get_device_status() == STAT_UNUSED) {	/* 设备状态为[未使用]? */
                if (status == BONDING_UNCONN){					/* 蓝牙状态为未连接 */
                    hula_hoop_set_device_status(STAT_SLEEP);	/* 设置设备状态为[睡眠] */
                }
                return;
            }
            ...
        }
        
      • 进入 SRAM 保留的深度睡眠模式

        接下来编写让设备进入SRAM 保留的深度睡眠模式的相关代码。我们把复位键引脚作为唤醒引脚使用,SDK 的配置文件中已经定义了相关的宏,如有需要也可以同时设置多个引脚作为唤醒引脚。

        /* app_config.h */
        #define BLE_MODULE_PM_ENABLE				1
        #define PM_DEEPSLEEP_RETENTION_ENABLE		1	/* SRAM 保留睡眠模式相关代码块,默认是打开的 */
        
        #define GPIO_WAKEUP_MODULE					GPIO_PB1/* 修改唤醒引脚,下面 4 行宏名对应修改 */
        #define	PB1_FUNC							AS_GPIO
        #define PB1_INPUT_ENABLE					1
        #define	PB1_OUTPUT_ENABLE					0
        #define	PB1_DATA_OUT						0
        #define GPIO_WAKEUP_MODULE_HIGH				gpio_setup_up_down_resistor(GPIO_WAKEUP_MODULE, PM_PIN_PULLUP_10K);
        #define GPIO_WAKEUP_MODULE_LOW				gpio_setup_up_down_resistor(GPIO_WAKEUP_MODULE, PM_PIN_PULLDOWN_100K);
        
        /* App.c */
        void main_loop(void)
        {
            ...
            if(1) {	/* ← 将低功耗处理代码段打开 */
                if((tuya_get_ota_status() == TUYA_OTA_STATUS_NONE)&&(ty_factory_flag==0)&&(ble_tx_is_busy()!=1)) {
                    app_power_management();
                }
            }
        }
        void app_power_management ()
        {
            ...
            if (app_module_busy()) {
                return;
            }
            /* 这两句用于实现连接时的低功耗,需要循环调用,由于会影响液晶屏显示,这次不使用 */
            //bls_pm_setSuspendMask (SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);
            //bls_pm_setWakeupSource(PM_WAKEUP_PAD);
            /* 进入睡眠模式的处理函数 */
            hula_hoop_set_device_sleep();
        }
        
        /* tuya_hula_hoop_svc_basic.c */
        VOID_T hula_hoop_set_device_sleep(VOID_T)
        {
            /* 由于该函数在循环中调用,要设置限制条件 */
            if (hula_hoop_get_device_status() != STAT_SLEEP) {
                return;
            }
            /* 保存当前时间 */
            sg_sys_time = clock_time();
            /* 设置唤醒引脚、唤醒定时时间并进入 SRAM 保留的低功耗模式 */
            GPIO_WAKEUP_MODULE_HIGH;
            cpu_set_gpio_wakeup(GPIO_WAKEUP_MODULE, Level_Low, 1);
            cpu_sleep_wakeup(DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_PAD|PM_WAKEUP_TIMER, clock_time()+SLEEP_TIME_SEC*CLOCK_16M_SYS_TIMER_CLK_1S);
        }
        
        /* SDK 默认使用的唤醒引脚是高电平有效的,所以有关唤醒引脚极性的地方要稍做修改 */
        int app_module_busy()
        {
            mcu_uart_working = gpio_read(GPIO_WAKEUP_MODULE);
            module_uart_working = UART_TX_BUSY || UART_RX_BUSY;
            //module_task_busy = mcu_uart_working || module_uart_working;	/* 修改前 */
            module_task_busy = (!mcu_uart_working) || module_uart_working;	/* 修改后 */
            return module_task_busy;
        }
        void user_init_normal(void)
        {
            ...
            /* 修改前 */
            //cpu_set_gpio_wakeup (GPIO_WAKEUP_MODULE, Level_High, 1);
            //GPIO_WAKEUP_MODULE_LOW;
            /* 修改后 */
            cpu_set_gpio_wakeup (GPIO_WAKEUP_MODULE, Level_Low, 1);
            GPIO_WAKEUP_MODULE_HIGH;
            ...
        }
        
      • 唤醒后处理

        设备唤醒后会从main()函数开始执行,然后通过判断是否从 SRAM 保留的睡眠模式唤醒来进入对应的初始化函数user_init_deepRetn(),我们在这里加入呼啦圈的处理函数:

        /* main.c */
        _attribute_ram_code_ int main(void)
        {
            ...
            int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();/* 是否从 SRAM 保留的睡眠模式唤醒 */
            ...
            gpio_init( !deepRetWakeUp );/* 如果是从 SRAM 保留的睡眠模式唤醒,gpio 相关模拟寄存器无需初始化 */
            ...
            if ( deepRetWakeUp ) {
                user_init_deepRetn();	/* 从 SRAM 保留的睡眠模式唤醒时的初始化函数 */
            } else {
                user_init_normal();		/* 上电/复位后的初始化函数 */
            }
            ...
        }
        
        /* App.c */
        _attribute_ram_code_ void user_init_deepRetn(void)
        {
            tuya_ble_app_init_deepRetn();
            ...
        }
        
        /* tuya_ble_app_demo.c */
        void tuya_ble_app_init_deepRetn(void)
        {
            tuya_software_timer_init();
            tuya_hula_hoop_init_deepRetn();
        }
        
        /* tuya_hula_hoop.c */
        VOID_T tuya_hula_hoop_init_deepRetn(VOID_T)
        {
            hula_hoop_device_wakeup_handler();		/* 唤醒后根据唤醒方式进行处理 */
            if (hula_hoop_get_device_status() >= STAT_UNUSED) {	/* 如果设备状态没有变化则不进行下面的初始化工作 */
                return;
            }
            hula_hoop_key_hall_init_deepRetn();		/* 按键引脚、霍尔传感器引脚的初始化 */
            hula_hoop_disp_proc_init_deepRetn();	/* 指示灯引脚、段码液晶屏引脚/定时器的初始化 */
            hula_hoop_ble_proc_init_deepRetn();		/* 睡眠前如果是已绑定则重新打开蓝牙广播 */
            hula_hoop_timer_reset();				/* 定时器复位 */
        }
        

        其中,hula_hoop_device_wakeup_handler()是唤醒后的处理函数,用于判断设备是被何种方式唤醒并进行相应的处理。如果是被引脚唤醒,则说明呼啦圈被用户使用,所以需要切换到工作状态,切换前还要计算睡眠经过的时间,用来更新本地时间;如果是定时唤醒,则根据设定的睡眠时间更新本地时间即可,此时设备会继续保持STAT_SLEEP状态,进入主循环后设备会再次睡眠。

        /* tuya_hula_hoop_svc_basic.c */
        /**
        * @brief 更新本地时间,并检查日期是否变更
        * @param[in] time_diff_sec: 时间差,单位“秒”
        * @return none
        */
        VOID_T __update_local_time(IN UCHAR_T time_diff_sec)
        {
            while(time_diff_sec--) {
                tuya_local_time_update();
            }
            hula_hoop_check_date_change();
        }
        
        /**
        * @brief 切换设备到工作状态
        * @param[in] none
        * @return none
        */
        VOID_T __set_device_work(VOID_T)
        {
            /* 本地时间处理,计算时间差,四舍五入 */
            UINT_T time_diff = clock_time() - sg_sys_time;
            if ((time_diff % CLOCK_16M_SYS_TIMER_CLK_1S) >= (CLOCK_16M_SYS_TIMER_CLK_1S / 2)) {
                time_diff = time_diff / CLOCK_16M_SYS_TIMER_CLK_1S + 1;
            } else {
                time_diff = time_diff / CLOCK_16M_SYS_TIMER_CLK_1S;
            }
            __update_local_time(time_diff);
            TUYA_APP_LOG_INFO("Time difference: %ds.", time_diff);
            TUYA_APP_LOG_INFO("Local time has been updated to %04d.%02d.%02d %02d:%02d:%02d.\n",
                        g_local_time.year, g_local_time.month, g_local_time.day,
                        g_local_time.hour, g_local_time.minute, g_local_time.second);
            /* 设置状态为使用中,同时设置默认工作模式 */
            __set_device_status(STAT_USING);
            __set_work_mode(MODE_NORMAL);
        }
        
        /**
        * @brief 唤醒后处理函数
        * @param[in] none
        * @return none
        */
        VOID_T hula_hoop_device_wakeup_handler(VOID_T)
        {
            if (pm_get_wakeup_src() == (WAKEUP_STATUS_PAD | WAKEUP_STATUS_CORE)) {
                /* 引脚唤醒时处理 */
                __set_device_work();
            } else {
                /* 定时唤醒时处理 */
                __update_local_time(SLEEP_TIME_SEC);
            }
        }
        

        经上述处理后,呼啦圈正常运行时电流平均值约为 5.6uA,睡眠时约为 0.157uA。