制作一款可以【记录运动历史数据】的智能呼啦圈——嵌入式功能实现

简介:本文将从智能呼啦圈软件整体方案,外设驱动以及功能实现几个维度来带大家一起了解如何实现呼啦圈智能计算、切换运动模式以及运动历史曲线。


开发环境搭建

智能呼啦圈方案是基于涂鸦 BLE SDK 和 Telink 芯片平台 TLSR825x 进行开发。BLE模组开发环境的搭建方案我们在前期的Demo有介绍过,大家可以参考BLE模组开发环境搭操作步骤


软件方案介绍

完整Demo可在 tuya-iotos-embeded-demo-ble-smart-hula-hoop 中获取。

一. 总体设计

1. 功能需求

智能呼啦圈demo的功能需求定义如下:

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

2. 模块划分

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

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

3. 代码结构

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

├── 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入口 */

4. 软件框图



二. 外设驱动

为方便后续程序扩展,可以先将各外设驱动部分的代码分别编写成组件。

本demo所使用外设的基本情况如下:

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

1. 按键&霍尔驱动

  • 按键

由于按键是嵌入式开发中的常用器件,这里不做过多介绍,结合呼啦圈demo的功能需求,组件功能设置如下:

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

首先我们先来实现按键的初始化:

/* 第一步:定义供用户注册按键信息使用的结构体类型 (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);

完成了按键初始化工作之后,我们来实现按键事件的检测和处理,基本思路是每10ms检测一次每个按键的状态,并判断是否满足了按键触发事件的条件,标记事件的类型,然后执行对应的回调函数:

/* 第一步:定义相关参数值 (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;
    }
}

2. 发光二极管驱动

发光二极管同样是常用器件,结合呼啦圈demo的功能需求,组件功能设置如下:

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

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

首先还是先来实现初始化部分:

/* 第一步:定义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);

接下来实现简单的亮灭控制,根据设置的有效电平来控制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);

最后是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);
    }
}

3. 段码液晶屏驱动

段码液晶屏主要有两种引脚,公共极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口,其引脚对应笔段如下表所示:

SEG1SEG2SEG3SEG4SEG5SEG6
COM13D2D1D
COM23C3E2C2E1C1E
COM33B3G2B2G1B1G
COM43A3F2A2F1A1F

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

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

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

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

1 bytebit7bit6bit5bit4bit3bit2bit1bit0
笔段fagbecd-

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

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 bytebit7bit6bit5bit4bit3bit2bit1bit0
SEG pin--SEG6SEG5SEG4SEG3SEG2SEG1

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

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

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

bit7bit6bit5bit4bit3bit2bit1bit0SEG pin code
--SEG6SEG5SEG4SEG3SEG2SEG1(无效位默认为0)
COM1---0-1-10x05
COM2--1001100x26
COM3--1011110x2F
COM4--0010100x0A

为了方便取出每个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);
}

最后,在头文件中定义接口供用户调用:

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口的输出了。

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

/* 第一步:定义段码液晶屏引脚类型 (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);

接下来实现段码液晶屏控制,由于段码液晶屏的引脚控制需要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;
}

最后,我们来实现液晶屏的点亮、熄灭和闪烁,基本参照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;
}


三. 功能实现

1. 设备基础服务

设备基础服务模块包括设备状态迁移、模式选择、模式切换、本地定时等功能处理。功能需求中已经对可选的运动模式即模式迁移方式进行了定义,这里再对设备状态进行如下定义:

设备状态含义处理
未使用用户未使用呼啦圈保持熄屏,实时数据清除
使用中用户未转动呼啦圈保持亮屏
转动中用户转动呼啦圈开始转动时点亮屏幕,转动过程:亮屏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();	/* 检查日期变更 */
    }
}

2. 显示处理服务

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

根据功能设定,显示内容可以分为以下4个部分:

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

其中计时指示灯还作为配网指示灯使用:

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

还需实现液晶屏点亮、熄灭、闪烁的控制(当显示屏熄灭时,如果指示灯作为数据指示,则随之熄灭):

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

代码实现:

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

外设引脚有效电平
计时/配网指示灯PD4H
圈数指示灯PB6H
卡路里指示灯PD7H
段码液晶屏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);

3. 数据处理服务

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

No.数据类别数值范围说明
1实时计时实时0-999可上报
2实时圈数实时0-999可上报
3实时卡路里实时0-999可上报
4当日累计计时累计0-1440可上报
5当日累计圈数累计0-99999可上报
6当日累计卡路里累计0-999可上报
7当月累计计时累计0-44640仅用于计算当月剩余
830天累计计时累计0-43200可上报
930天累计圈数累计0-9999999可上报
1030天累计卡路里累计0-99999可上报
11当日目标时长目标0-45云端下发
12当月目标时长目标0-1350云端下发
13当日目标剩余剩余0-45可上报
14当月目标剩余剩余0-1350可上报
15每日累计时长累计0-144030个数据,仅用于计算30天累计
16每日累计圈数累计0-9999930个数据,仅用于计算30天累计
17每日累计卡路里累计0-99930个数据,仅用于计算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);

4. 用户事件处理

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

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

代码实现:

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

外设引脚有效电平
模式键PB7L
复位键PB1L
霍尔传感器PD2H

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

/* 第一步:定义按键&霍尔注册信息,编写初始化函数 (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();
}

5. 定时事件处理

本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);

6. 联网相关处理

  • 配网等待处理

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

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

首先来实现初期上电时的配网状态检测处理,即上表中的<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 BLE 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) */

除上电时允许设备配网外,用户还可以通过长按模式键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();						/* 允许用户绑定设备 */
}

配网功能开启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();				/* 禁止用户绑定设备 */
    }
}

最后是蓝牙连接状态改变时和被解绑时的处理,即上表中的<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);

/* 第三步:在BLE 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);

/* 第三步:在BLE 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;
	...
}
  • 云端时间获取

BLE SDK提供获取云端时间的功能,当设备连接APP时,可以通过获取云端时间来校准本地时间。涂鸦BLE 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);

/* 第四步:在BLE 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);


四. 优化处理

1. 关闭日志

由于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来关闭应用日志,以减少代码空间。


2. 低功耗处理

由于呼啦圈使用电池供电,为避免电量消耗过快,需对设备进行低功耗处理。

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> 当设备被定时器唤醒时:更新本地时间后再次睡眠。

代码实现:

  • 睡眠前处理

在进入睡眠状态之前,如果设备已连接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。

以上主要是对智能计数、运动模式切换以及运动历史数据记录开发方案介绍,方案不仅仅针对呼啦圈,如果同学们感兴趣可以试试开发智能跳绳,智能家用跑步机等小型运动健康类产品。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值