智能呼啦圈因其小巧便携、尺寸可调节的特点,以及其为用户带来的多功能体验,深受因工作原因无法去健身房的上班族喜爱。本教程以一款能够记录运动圈数、运动时间以及消耗卡路里,并且能够将三种数据显示在显示屏上的智能呼啦圈为例,将其改造为一款能够记录运动数据,设置运动目标,进行运动倒计时,并能够使用 App 打卡记录的智能呼啦圈。
原物料 | 是否可用 | 不可用原因 |
---|---|---|
外壳 | 是 | - |
上盖 | 否 | 按键更换,无需使用原上盖。 |
主板 | 否 | 改为 App 打卡记录。 |
按键 | 否 | 橡胶按键改为轻触按键,方便采购且能够更好地适配结构。 |
霍尔开关 | 是 | - |
显示屏 | 否 | 无法找到逻辑走线图。 |
Wi-Fi 和蓝牙都是通过无线电信号无线发送和接收数据,且手机上基本都有这两项功能。
Wi-Fi
蓝牙
由于蓝牙突出的低功耗特性,且完全满足呼啦圈这类运动设备的数据传输和距离要求。我们选择蓝牙作为设备连接手机 App 的连接方式,选用涂鸦智能自主研发的低功耗蓝牙模组作为核心控制器。
芯片 | 优点 | 缺点 |
---|---|---|
Bluetooth LE 专用液晶屏驱动芯片 | 实现显示驱动较为简单,只需通过只需通过 SPI 下发显示的内容数据。 | 设备的结构空间有限,PCB 空间无法容纳液晶屏、蓝牙模块和驱动芯片。 |
MCU 自带 LCD 驱动外设和蓝牙无线通 | 外围简单。 | 成本较高。 |
使用 Bluetooth LE 的 I/O 口驱动 LCD | 对 Bluetooth LE 芯片的外设要求不高,通用低成本的蓝牙芯片就能满足。 | 只能适用偏压比 1/2 的液晶屏。 |
参数名 | 定义 | 备注 |
---|---|---|
工作电压 | 断码屏驱动的最大允许电压。 | LCD 功耗约 10μA。蓝牙模组作为核心控制器,其工作电压在 1.8V~3.6V,所以液晶屏选型优先选择工作电压在这个范围内。 |
偏压比(bias) | 一般是以最低一档与输出最高电压的比值来表示。偏压比是调节显示的黑色明亮字符和周围液晶点间的对比度。 | 如 3V 1/3 偏压的液晶屏,其阀值电压是 1V;液晶屏驱动电压(PIN 脚和 COM 脚之间的电压差)越大于阀值电压,液晶屏显示的点越明显。 |
占空比(duty ratio) | 也称为 COM 数。在一个脉冲循环内,通电时间相对于总时间所占的比例。 | 由于 STN/TN 的 LCD 一般是采用时分动态扫描的驱动模式,在此模式下,每个 COM 的有效选通时间与整个扫描周期的比值即占空比(Duty)是固定的,等于 1/COM 数。 |
根据硬件设计要求,整理好液晶屏的参数,即可设计出断码液晶屏的显示内容。但由于定制费用较高,我们可以直接购买现货。本教程以 1/2 偏压比、3V 供电的 3 位数字显示液晶屏为例。液晶屏及其走线如下图所示:
本 Demo 采用 Bluetooth LE 芯片自带的 I/O 口模拟驱动芯片来驱动液晶屏幕。
上电后,液晶屏驱动的 I/O 口设置为浮空输入,此时分压电阻将液晶屏的引脚电压设置在 1/2 工作电压点,即 1.5V。
COM 口时分扫描时,相应的 COM 脚 I/O 口设置为推挽输出,输出恒定时间的高电平和低电平。其他时间段设置为浮空输入。
如某段液晶需要显示,在扫描相应的 COM 口时,对应的 PIN 口同时输出恒定时间的低电平和高电平。
注意:如果液晶屏长时间单向供电,会造成液晶屏不可恢复的损坏,因此,我们都采用交流供电的方式驱动。
在显示屏上显示数字 1 输出波形如下图所示:
在液晶屏旁增加三个指示灯,分别显示时间值、计数值以及能量值。在显示屏下方的左右两侧分别有两个按键,用作模式选择和数据复位的功能。
说明:
PCB 图
实物图
结构空间有限,采用叠层的方式,将电源板放在主控板的后面。
PCB 图
实物图
3D 建模图
实物图
产品创建后,进入到开发界面,可以看到左上角有产品的 PID,我们将在接下来的开发环境搭建中用到它。
根据喜好选择一种面板进行编辑,编辑完成后可以按照提示使用涂鸦智能 App 扫码体验手机控制。
选择 涂鸦标准模组 SDK 开发 和 BT3L Bluetooth 模组。
单击 采购模组 下方的 免费领取 10 个激活码 即可获取该产品对应的 10 组激活码,其中包含 UUID、authkey 和 MAC 地址。
本案例使用涂鸦 BLE SDK 和 Telink 芯片平台 TLSR825x 进行开发,下面我们开始搭建开发环境。
下载 TLSR825x 对应的 BLE SDK Demo:tuya_ble_sdk_Demo_Project_tlsr8253.git
我们将在\ble_sdk_multimode\tuya_ble_app
中进行智能呼啦圈应用代码的编写。
下载与安装 IDE。
下载 Telink 官方 IDE 并安装:Eclipse (IDE for TLSR8 Chips)
注意:IDE 必须安装在 C 盘。
修改代码并编译。
将代码导入 Eclipse,可以直接在 Eclipse 进行代码修改,也可以先使用自己熟悉的代码编辑器。
在 tuya_ble_app_demo.h
中填入创建好的智能呼啦圈的 PID:
#define APP_PRODUCT_ID "xxxxxxxx"
在 tuya_ble_app_demo.c
填入申请的激活码(取自下载的 excel文件)。
static const char auth_key_test[] = "yyyyyyyy";
static const char device_id_test[] = "zzzzzzzz";
static const uint8_t mac_test[6] = {0x12, 0x34, 0x56, 0x4D, 0x23, 0xDC}; // The actual MAC address is : DC:23:4D:56:34:12
在 tuya_ble_app_demo.c
中找到 tuya_ble_app_init()
函数,将 device_param.device_id_len = 0;
改为 device_param.device_id_len = 16;
(可以参考此行代码的注释)。
在 vendor\8258_module\app_config.h
中将日志口修改为 GPIO_PD7
。
#define DEBUG_INFO_TX_PIN GPIO_PD7 /* 日志口,下面四行宏名同步修改 */
#define PD7_FUNC AS_GPIO
#define PD7_INPUT_ENABLE 0
#define PD7_OUTPUT_ENABLE 1
#define PD7_DATA_OUT 1
使用 Eclipse 对代码进行编译,输出文件目录为ble_sdk_multimode\8258_module\8258_module.bin
。编译前需修改工程配置中的头文件包含路径,根据 SDK 中的文件夹名称进行相应修改,修改方法参考下图:
下载并安装烧录工具。
下载 Telink 官方烧录工具并安装:Burning and Debugging Tools for all Series
下载并调试程序。
程序启动后,我们还可以使用涂鸦智能 App 搜索到我们的设备并进行绑定,可以看到设备名称显示为我们创建的产品名称“智能呼啦圈”。至此,智能呼啦圈 Demo 开发前的准备工作就完成了。
完整 Demo 可访问 智能呼啦圈仓库 获取。
智能呼啦圈 demo 的功能需求列表如下:
功能 | 需求描述 |
---|---|
模式选择 |
|
目标设定 |
|
目标完成提醒 | 段码液晶屏显示 — 并闪烁 3 次即为提醒。 |
智能计数 |
|
运动数据显示 |
|
运动数据记录 | 本地记录 30 天内累计运动数据(时长、圈数、卡路里)。 |
数据指示 | 本时长、圈数、卡路里指示灯根据当前屏显数据依次点亮。 |
屏幕状态更新 | 开始转动时屏幕亮起,停止转动 30s 后屏幕熄灭。 |
设备配网 |
|
数据上报 | 本地上报模式数据、运动数据至 App 显示。 |
设备复位 |
|
对以上功能需求进行分析梳理后,确定本智能呼啦圈教程程序可划分为以下七大模块:
No. | 模块 | 处理内容 |
---|---|---|
1 | 外设驱动组件 | 按键、霍尔传感器、指示灯、段码液晶屏的驱动程序。 |
2 | 设备基础服务 | 设备状态迁移处理、模式切换、本地定时等。 |
3 | 显示处理服务 | 段码液晶屏显示内容更新和状态控制、LED 状态控制 。 |
4 | 数据处理服务 | 运动数据(计时、圈数、卡路里)的更新和存储。 |
5 | 用户事件处理 | 按键事件检测和处理、霍尔传感器事件检测和处理。 |
6 | 定时事件处理 | 各定时事件的判断和处理。 |
7 | 联网相关处理 | 配网相关处理、数据上报与接收处理、云端时间获取。 |
根据模块层级关系,设定代码文件结构如下,后续将分别介绍外设驱动模块和应用层六大功能模块:
├── src /* 源文件目录 */
| ├── common
| | └── tuya_local_time.c /* 本地定时 */
| ├── sdk
| | └── tuya_uart_common_handler.c /* UART 通用对接实现代码 */
| ├── driver
| | ├── tuya_hall_sw.c /* 霍尔开关驱动 */
| | ├── tuya_key.c /* 按键驱动 */
| | ├── tuya_led.c /* 指示灯驱动 */
| | └── tuya_seg_lcd.c /* 段码液晶屏驱动 */
| ├── platform
| | ├── tuya_gpio.c /* GPIO 驱动 */
| | └── tuya_timer.c /* Timer 驱动 */
| ├── tuya_ble_app_demo.c /* 应用层入口文件 */
| ├── tuya_hula_hoop_ble_proc.c /* 呼啦圈联网相关处理 */
| ├── tuya_hula_hoop_evt_user.c /* 呼啦圈用户事件处理 */
| ├── tuya_hula_hoop_evt_timer.c /* 呼啦圈定时事件处理 */
| ├── tuya_hula_hoop_svc_basic.c /* 呼啦圈基础服务 */
| ├── tuya_hula_hoop_svc_data.c /* 呼啦圈数据服务 */
| ├── tuya_hula_hoop_svc_disp.c /* 呼啦圈显示服务 *
| └── tuya_hula_hoop.c /* 呼啦圈 demo 入口 */
|
└── include /* 头文件目录 */
├── common
| ├── tuya_common.h /* 通用类型和宏定义 */
| └── tuya_local_time.h /* 本地定时 */
├── sdk
| ├── custom_app_uart_common_handler.h /* UART 通用对接实现代码 */
| ├── custom_app_product_test.h /* 自定义产测项目相关实现 */
| └── custom_tuya_ble_config.h /* 应用配置文件 */
├── driver
| ├── tuya_hall_sw.h /* 霍尔开关驱动 */
| ├── tuya_key.h /* 按键驱动 */
| ├── tuya_led.h /* 指示灯驱动 */
| └── tuya_seg_lcd.h /* 段码液晶屏驱动 */
├── platform
| ├── tuya_gpio.h /* GPIO 驱动 */
| └── tuya_timer.h /* Timer 驱动 */
├── tuya_ble_app_demo.h /* 应用层入口文件 */
├── tuya_hula_hoop_ble_proc.h /* 呼啦圈联网相关处理 */
├── tuya_hula_hoop_evt_user.h /* 呼啦圈用户事件处理 */
├── tuya_hula_hoop_evt_timer.h /* 呼啦圈定时事件处理 */
├── tuya_hula_hoop_svc_basic.h /* 呼啦圈基础服务 */
├── tuya_hula_hoop_svc_data.h /* 呼啦圈数据服务 */
├── tuya_hula_hoop_svc_disp.h /* 呼啦圈显示服务 */
└── tuya_hula_hoop.h /* 呼啦圈 demo 入口 */
为方便后续程序扩展,可以先将各外设驱动部分的代码分别编写成组件。本教程所使用外设的基本情况如下:
外设 | 数量&规格 | 驱动方式 |
---|---|---|
按键 | 2 个 | 开关量检测 |
霍尔传感器 | 1 个;开关型 | 开关量检测 |
发光二极管 | 3 个;红色 | 电平或 PWM 驱动 |
段码液晶屏 | 1 个;3 位数字8 ;4 个 COM 口,6 个 SEG 口 |
COM 口和 SEG 口两端施加一定压差的交流电,压差大于阈值时对应笔段点亮。 |
按键
按键组件功能设置:
<1> 可在初期注册按键信息,包括引脚、有效电平、长按时间(2 种可选)、按键触发时响应的回调函数;
<2> 可检测按键触发事件,包括短按、长按(2 种可选),并在按键确认触发时,执行用户设置的回调函数;
<3> 可实现多个按键同时检测;
实现按键功能的初始化
/* 第一步:定义供用户注册按键信息使用的结构体类型 (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 检测一次每个按键的状态,并判断是否满足了按键触发事件的条件,标记事件的类型,然后执行对应的回调函数:
```c
/* 第一步:定义相关参数值 (tuya_key.c) */
#define KEY_SCAN_CYCLE_MS 10 /* 扫描周期 */
#define KEY_PRESS_SHORT_TIME 50 /* 短按确认时间 */
/* 第二步:编写用于更新单个按键状态的相关函数 (tuya_key.c) */
/* 获取按键实时状态,1-按压,0-释放 */
STATIC BOOL_T __get_key_stat(IN CONST TY_GPIO_PORT_E port, IN CONST UCHAR_T active_low)
{
BOOL_T key_stat;
if (active_low) {
key_stat = tuya_gpio_read(port) == 0 ? TRUE : FALSE;
} else {
key_stat = tuya_gpio_read(port) == 0 ? FALSE : TRUE;
}
return key_stat;
}
/* 更新按键状态 */
STATIC VOID_T __update_key_status(INOUT KEY_MANAGE_T *key_mag)
{
BOOL_T key_stat;
/* 保存前回状态 */
key_mag->key_status_s.prv_stat = key_mag->key_status_s.cur_stat;
key_mag->key_status_s.prv_time = key_mag->key_status_s.cur_time;
/* 获取实时状态 */
key_stat = __get_key_stat(key_mag->key_def_s->pin, key_mag->key_def_s->active_low);
/* 更新今回状态 */
if (key_stat != key_mag->key_status_s.cur_stat) {
key_mag->key_status_s.cur_stat = key_stat;
key_mag->key_status_s.cur_time = 0;
} else {
key_mag->key_status_s.cur_time += KEY_SCAN_CYCLE_MS;
}
}
/* 第三步:编写用于判断单个按键事件的相关函数 (tuya_key.c) */
/* 判断按键保持按压状态的时间是否达到了 over_time */
STATIC BOOL_T __is_key_press_over_time(IN CONST KEY_STATUS_T key_status_s, IN CONST UINT_T over_time)
{
if (key_status_s.cur_stat == TRUE) {
if ((key_status_s.cur_time >= over_time) &&
(key_status_s.prv_time < over_time)) {
return TRUE;
}
}
return FALSE;
}
/* 判断按键从开始被按压到被释放经过的时间是否达到了 over_time 并且少于 less_time */
STATIC BOOL_T __is_key_release_to_release_over_time_and_less_time(IN CONST KEY_STATUS_T key_status_s, IN CONST UINT_T over_time, IN CONST UINT_T less_time)
{
if ((key_status_s.prv_stat == TRUE) &&
(key_status_s.cur_stat == FALSE)) {
if ((key_status_s.prv_time >= over_time) &&
(key_status_s.prv_time < less_time)) {
return TRUE;
}
}
return FALSE;
}
/* 判断与处理按键事件 */
STATIC VOID_T __detect_and_handle_key_event(INOUT KEY_MANAGE_T *key_mag)
{
KEY_PRESS_TYPE_E type;
BOOL_T time_exchange;
UINT_T long_time1, long_time2;
/* 比较用户设置的长按时间 1 和长按时间 2 的大小,并标记是否交换 */
if (key_mag->key_def_s->long_press_time2 >= key_mag->key_def_s->long_press_time1) {
long_time1 = key_mag->key_def_s->long_press_time1;
long_time2 = key_mag->key_def_s->long_press_time2;
time_exchange = FALSE;
} else {
long_time1 = key_mag->key_def_s->long_press_time2;
long_time2 = key_mag->key_def_s->long_press_time1;
time_exchange = TRUE;
}
/* 判断按键状态,标记事件类型并跳转到 KEY_EVENT (根据长按时间设置情况使用对应的判断方式) */
if ((long_time2 != 0) && (long_time1 != 0)) {
if (__is_key_press_over_time(key_mag->key_status_s, long_time2)) {
type = LONG_PRESS_FOR_TIME2;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, long_time1, long_time2)) {
type = LONG_PRESS_FOR_TIME1;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time1)){
type = SHORT_PRESS;
goto KEY_EVENT;
}
} else if ((long_time2 != 0) && (long_time1 == 0)) {
if (__is_key_press_over_time(key_mag->key_status_s, long_time2)) {
type = LONG_PRESS_FOR_TIME2;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time2)){
type = SHORT_PRESS;
goto KEY_EVENT;
}
} else if ((long_time2 == 0) && (long_time1 != 0)) {
if (__is_key_press_over_time(key_mag->key_status_s, long_time1)) {
type = LONG_PRESS_FOR_TIME1;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time1)){
type = SHORT_PRESS;
goto KEY_EVENT;
}
} else {
if (__is_key_press_over_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME)) {
type = SHORT_PRESS;
goto KEY_EVENT;
}
}
return;
/* 处理按键事件 */
KEY_EVENT:
/* 如果在判断前进行了时间参数的交换,则将标记的事件类型进行交换 */
if (time_exchange) {
if (type == LONG_PRESS_FOR_TIME2) {
type = LONG_PRESS_FOR_TIME1;
} else if (type == LONG_PRESS_FOR_TIME1) {
type = LONG_PRESS_FOR_TIME2;
} else {
;
}
}
/* 执行用户设置的回调函数 */
key_mag->key_def_s->key_cb(type);
}
/* 第四步:编写 10ms 处理函数 (tuya_key.c) */
STATIC INT_T __key_timeout_handler(VOID_T)
{
/* 获取按键信息管理列表,无按键注册则返回 */
KEY_MANAGE_T *key_mag_tmp = sg_key_mag_list;
if (NULL == key_mag_tmp) {
return 0;
}
/* 循环处理每个按键 */
while (key_mag_tmp) {
__update_key_status(key_mag_tmp); /* 更新按键状态 */
__detect_and_handle_key_event(key_mag_tmp); /* 判断并处理按键事件 */
key_mag_tmp = key_mag_tmp->next; /* 加载下一个按键信息 */
}
return 0;
}
/* 第五步:在按键注册函数中创建定时器 (tuya_key.c) */
KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def)
{
...
if (sg_key_mag_list) {
key_mag->next = sg_key_mag_list;
} else { /* 注册第一个按键时创建 10ms 定时器,注册回调函数 */
tuya_software_timer_create(KEY_SCAN_CYCLE_MS*1000, __key_timeout_handler);
}
...
}
```
霍尔传感器
这次使用的霍尔传感器是开关型的,一般情况下也可视为按键处理。考虑到呼啦圈快速转动时霍尔传感器与磁铁的接触时间较短,并且上述按键驱动组件使用了 10ms 软件定时器进行处理,易被外部程序执行时间影响,可能会导致呼啦圈计数漏检的情况,所以这里我们采取外部中断的方式处理:
/* 【tuya_hall_sw.h】 */
/* 错误信息代码 */
typedef BYTE_T HSW_RET;
#define HSW_OK 0x00
#define HSW_ERR_MALLOC_FAILED 0x01
#define HSW_ERR_CB_UNDEFINED 0x02
/* 霍尔开关数据结构 */
typedef VOID_T (*HALL_SW_CALLBACK)();
typedef struct {
TY_GPIO_PORT_E port; /* 端口 */
BOOL_T active_low; /* 有效电平 */
HALL_SW_CALLBACK hall_sw_cb; /* 触发时回调函数 */
UINT_T invalid_intv; /* 两次触发间隔如果小于该时间则无效 */
} HALL_SW_DEF_T;
/* 【tuya_hall_sw.c】 */
/* 霍尔开关信息管理 */
typedef struct hall_sw_manage_s {
struct hall_sw_manage_s *next;
HALL_SW_DEF_T *def;
UINT_T wk_tm;
} HALL_SW_MANAGE_T;
STATIC HALL_SW_MANAGE_T *sg_hsw_mag_list = NULL;
/* 霍尔开关引脚初始化 */
STATIC VOID_T __hall_sw_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
{
tuya_gpio_init(port, TRUE, active_low);
if (active_low) {
tuya_gpio_irq_init(port, TY_GPIO_IRQ_FALLING, __hall_sw_irq_handler);
} else {
tuya_gpio_irq_init(port, TY_GPIO_IRQ_RISING, __hall_sw_irq_handler);
}
}
/* 霍尔开关注册 */
HSW_RET tuya_reg_hall_sw(IN HALL_SW_DEF_T *hsw_def)
{
/* 检查是否定义了回调函数,未定义则返回错误信息 */
if (hsw_def->hall_sw_cb == NULL) {
return HSW_ERR_CB_UNDEFINED;
}
/* 为 hall_sw_mag 分配空间并初始化,分配失败则返回错误信息 */
HALL_SW_MANAGE_T *hall_sw_mag = (HALL_SW_MANAGE_T *)tuya_ble_malloc(SIZEOF(HALL_SW_MANAGE_T));
if (NULL == hall_sw_mag) {
return HSW_ERR_MALLOC_FAILED;
}
hall_sw_mag->def = hsw_def;
/* 记录用户设置的霍尔开关信息,并存放至霍尔开关管理列表 */
if (sg_hsw_mag_list) {
hall_sw_mag->next = sg_hsw_mag_list;
}
sg_hsw_mag_list = hall_sw_mag;
/* 引脚初始化 */
__hall_sw_gpio_init(hsw_def->port, hsw_def->active_low);
return HSW_OK;
}
/* 霍尔开关触发时处理 */
STATIC VOID_T __hall_sw_trigger_handler(IN HALL_SW_MANAGE_T *hsw_mag)
{
/* 两次触发间隔检查 */
if (!tuya_is_clock_time_exceed(hsw_mag->wk_tm, hsw_mag->def->invalid_intv)) {
return;
}
hsw_mag->wk_tm = tuya_get_clock_time();
/* 执行用户设置的回调函数 */
hsw_mag->def->hall_sw_cb();
}
/* 霍尔开关外部中断回调 */
STATIC VOID_T __hall_sw_irq_handler(TY_GPIO_PORT_E port)
{
HALL_SW_MANAGE_T *hsw_mag_tmp = sg_hsw_mag_list;
while (hsw_mag_tmp) {
if (hsw_mag_tmp->def->port == port) {
__hall_sw_trigger_handler(hsw_mag_tmp);
break;
}
hsw_mag_tmp = hsw_mag_tmp->next;
}
}
发光二极管组件功能设置:
<1> 可在初期注册 LED 信息,包括引脚、有效电平;
<2> 可控制 LED 点亮或熄灭或闪烁;
<3> 可设置 LED 闪烁模式,包括闪烁方式(指定时长/指定次数/永远闪烁)、闪烁开始时和闪烁结束后的状态、点亮阶段的时间、熄灭阶段的时间、闪烁结束时的回调函数;
<4> 可实现多个 LED 同时控制;
这次没有设置亮度调节、呼吸灯控制等功能,因此只需要使用电平驱动方式。
实现初始化:
/* 第一步:定义 LED 句柄,用户通过该句柄来控制单个 LED (tuya_led.h) */
typedef VOID_T *LED_HANDLE;
/* 第二步:定义初期需注册的 LED 驱动相关信息 (tuya_led.c) */
typedef struct {
TY_GPIO_PORT_E pin; /* LED 引脚 */
BOOL_T active_low; /* LED 有效电平 (1-低电平点亮,0-高电平点亮) */
} LED_DRV_T;
/* 第三步:定义 LED 信息管理列表 (tuya_led.c) */
typedef struct led_manage_s {
struct led_manage_s *next;
LED_DRV_T drv_s;
} LED_MANAGE_T;
STATIC LED_MANAGE_T *sg_led_mag_list = NULL;/* 下一个 LED 信息存储的地址,实现多 LED 控制 */
/* 第四步:编写 LED 引脚初始化函数和 LED 注册函数 (tuya_led.c) */
STATIC VOID_T __led_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
{
tuya_gpio_init(port, FALSE, active_low);
}
LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle)
{
/* 检查句柄,未指定则返回错误参数 */
if (NULL == handle) {
return LED_ERR_INVALID_PARM;
}
/* 为 led_mag 分配空间并初始化,分配失败则返回错误信息 */
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)tuya_ble_malloc(SIZEOF(LED_MANAGE_T));
if (NULL == led_mag) {
return LED_ERR_MALLOC_FAILED;
}
/* 记录用户设置的 LED 信息,并存放至 LED 管理列表,同时将存储地址 */
led_mag->drv_s.pin = pin;
led_mag->drv_s.active_low = active_low;
*handle = (LED_HANDLE)led_mag;
if (sg_led_mag_list) {
led_mag->next = sg_led_mag_list;
}
sg_led_mag_list = led_mag;
/* 根据用户设置的有效电平对引脚进行初始化 (注册时默认不点亮) */
__led_gpio_init(pin, active_low);
/* 返回成功信息 */
return LED_OK;
}
/* 第五步:在头文件中定义 LED 注册接口 (tuya_led.h) */
LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle);
2.实现亮灭控制,根据设置的有效电平来控制 LED 引脚的输出状态即可:
/* 第一步:编写 LED 亮灭控制函数 (tuya_led.c) */
STATIC VOID_T __set_led_light(IN CONST LED_DRV_T drv_s, IN CONST BOOL_T on_off)
{
if (drv_s.active_low) {
tuya_gpio_write(drv_s.pin, !on_off);
} else {
tuya_gpio_write(drv_s.pin, on_off);
}
}
LED_RET tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off)
{
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
__set_led_light(led_mag->drv_s, on_off);
return LED_OK;
}
/* 第二步:在头文件中定义 LED 亮灭控制接口 (tuya_led.h) */
LED_RET tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off);
3.实现 LED 闪烁控制,闪烁功能将在用户配置闪烁参数后开启,每 100ms 处理一次:
/* 第一步:定义用于管理 LED 闪烁信息的结构体类型 (tuya_led.c) */
typedef struct {
LED_FLASH_MODE_E mode; /* 闪烁方式 */
LED_FLASH_TYPE_E type; /* 闪烁类型 */
USHORT_T on_time; /* 点亮阶段时间 */
USHORT_T off_time; /* 熄灭阶段时间 */
UINT_T total; /* 指定的时间或指定的次数 */
LED_CALLBACK end_cb; /* 闪烁结束时执行的回调函数 */
UINT_T work_timer; /* 闪烁工作用计时变量 */
} LED_FLASH_T;
/* 第二步:在头文件对闪烁方式和闪烁类型的可选项进行定义,用户配置时可直接使用这些宏 (tuya_led.h) */
/* 闪烁方式 */
typedef BYTE_T LED_FLASH_MODE_E;
#define LFM_SPEC_TIME 0x00 /* 闪烁指定时间 */
#define LFM_SPEC_COUNT 0x01 /* 闪烁指定次数 */
#define LFM_FOREVER 0x02 /* 永远闪烁 */
/* 闪烁类型 */
typedef BYTE_T LED_FLASH_TYPE_E;
#define LFT_STA_ON_END_ON 0x00 /* 开始时:亮;结束后:亮 */
#define LFT_STA_ON_END_OFF 0x01 /* 开始时:亮;结束后:灭 */
#define LFT_STA_OFF_END_ON 0x02 /* 开始时:灭;结束后:亮 */
#define LFT_STA_OFF_END_OFF 0x03 /* 开始时:灭;结束后:灭 */
/* 第三步:将 LED 闪烁信息加入 LED 管理,同时定义闪烁结束处理相关的变量 (tuya_led.c) */
typedef struct led_manage_s {
struct led_manage_s *next;
LED_DRV_T drv_s;
LED_FLASH_T *flash;
BOOL_T stop_flash_req; /* 停止闪烁请求 */
BOOL_T stop_flash_light; /* 停止闪烁后的亮灭状态 */
} LED_MANAGE_T;
/* 第四步:编写以下函数用于解析用户配置,方便闪烁处理时使用 (tuya_led.c) */
/* 获取闪烁开始时的亮灭状态,1-亮,0-灭 */
STATIC BOOL_T __get_led_flash_sta_light(IN CONST LED_FLASH_TYPE_E type)
{
BOOL_T ret = TRUE;
switch (type) {
case LFT_STA_ON_END_ON:
case LFT_STA_ON_END_OFF:
ret = TRUE;
break;
case LFT_STA_OFF_END_ON:
case LFT_STA_OFF_END_OFF:
ret = FALSE;
break;
default:
break;
}
return ret;
}
/* 获取闪烁结束后的亮灭状态,1-亮,0-灭 */
STATIC BOOL_T __get_led_flash_end_light(IN CONST LED_FLASH_TYPE_E type)
{
BOOL_T ret = TRUE;
switch (type) {
case LFT_STA_ON_END_ON:
case LFT_STA_OFF_END_ON:
ret = TRUE;
break;
case LFT_STA_ON_END_OFF:
case LFT_STA_OFF_END_OFF:
ret = FALSE;
break;
default:
break;
}
return ret;
}
/* 第五步:编写 LED 闪烁配置函数 (tuya_led.c) */
LED_RET tuya_set_led_flash(IN CONST LED_HANDLE handle, IN CONST LED_FLASH_MODE_E mode, IN CONST LED_FLASH_TYPE_E type, IN CONST USHORT_T on_time, IN CONST USHORT_T off_time, IN CONST UINT_T total, IN CONST LED_CALLBACK flash_end_cb)
{
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
led_mag->stop_flash_req = FALSE;
if (led_mag->flash == NULL) {
LED_FLASH_T *led_flash = (LED_FLASH_T *)tuya_ble_malloc(SIZEOF(LED_FLASH_T));
if (NULL == led_flash) {
return LED_ERR_MALLOC_FAILED;
}
led_mag->flash = led_flash;
}
led_mag->flash->mode = mode;
led_mag->flash->type = type;
led_mag->flash->on_time = on_time;
led_mag->flash->off_time = off_time;
led_mag->flash->total = total;
led_mag->flash->work_timer = 0;
led_mag->flash->end_cb = flash_end_cb;
__set_led_light(led_mag->drv_s, __get_led_flash_sta_light(type));
return LED_OK;
}
/* 第六步:编写 LED 闪烁处理函数 (tuya_led.c) */
STATIC VOID_T __led_flash_proc(INOUT LED_MANAGE_T *led_mag)
{
BOOL_T one_cycle_flag = FALSE;
UINT_T sum_time;
BOOL_T start_light;
USHORT_T start_time;
/* 解析闪烁配置 */
sum_time = led_mag->flash->on_time + led_mag->flash->off_time;
start_light = __get_led_flash_sta_light(led_mag->flash->type);
start_time = (start_light) ? led_mag->flash->on_time : led_mag->flash->off_time;
/* 闪烁周期性处理,实现按照指定时间点亮和熄灭 */
led_mag->flash->work_timer += LED_TIMER_VAL_MS;
if (led_mag->flash->work_timer >= sum_time) {
led_mag->flash->work_timer -= sum_time;
__set_led_light(led_mag->drv_s, start_light);
one_cycle_flag = TRUE;
} else if (led_mag->flash->work_timer >= start_time) {
__set_led_light(led_mag->drv_s, !start_light);
} else {
;
}
/* 闪烁倒计时/数处理,闪烁方式为“永远闪烁”时不处理 */
if (led_mag->flash->mode == LFM_FOREVER) {
return;
}
if (led_mag->flash->mode == LFM_SPEC_TIME) {
if (led_mag->flash->total > LED_TIMER_VAL_MS) {
led_mag->flash->total -= LED_TIMER_VAL_MS;
} else {
led_mag->flash->total = 0;
}
} else if (led_mag->flash->mode == LFM_SPEC_COUNT) {
if (one_cycle_flag) {
if (led_mag->flash->total > 0) {
led_mag->flash->total--;
}
}
} else {
;
}
/* 闪烁结束处理 */
if (led_mag->flash->total == 0) {
/* 如果设置了闪烁回调函数,则执行该函数 */
if (led_mag->flash->end_cb != NULL) {
led_mag->flash->end_cb();
}
/* 发起停止闪烁请求,并设置停止后的亮灭状态 */
led_mag->stop_flash_req = TRUE;
led_mag->stop_flash_light = __get_led_flash_end_light(led_mag->flash->type);
}
}
/* 第七步:编写 LED 超时处理函数 (tuya_led.c) */
STATIC INT_T __led_timeout_handler(VOID_T)
{
/* 获取 LED 信息管理列表,无 LED 注册则返回 */
LED_MANAGE_T *led_mag_tmp = sg_led_mag_list;
if (NULL == led_mag_tmp) {
return;
}
/* 循环处理每个 LED */
while (led_mag_tmp) {
/* 停止闪烁请求处理 */
if (led_mag_tmp->stop_flash_req) {
__set_led_light(led_mag_tmp->drv_s, led_mag_tmp->stop_flash_light);
tuya_ble_free((UCHAR_T *)led_mag_tmp->flash);
led_mag_tmp->flash = NULL;
led_mag_tmp->stop_flash_req = FALSE;
}
/* 如果闪烁功能未开启则不处理 */
if (NULL != led_mag_tmp->flash) {
__led_flash_proc(led_mag_tmp);
}
/* 加载下一个 LED 信息 */
led_mag_tmp = led_mag_tmp->next;
}
return 0;
}
/* 第八步:在 LED 注册函数中创建定时器 (tuya_led.c) */
#define LED_TIMER_VAL_MS 100
LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle)
{
...
if (sg_led_mag_list) {
led_mag->next = sg_led_mag_list;
} else { /* 注册第一个 LED 时创建 100ms 定时器,注册回调函数 */
tuya_software_timer_create(LED_TIMER_VAL_MS*1000, __led_timeout_handler);
}
...
}
/* 第九步:修改 LED 亮灭控制函数 (可能会存在闪烁未结束时调用了该函数的情况) (tuya_led.c) */
VOID_T tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off)
{
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
/* 闪烁未结束时,发起停止闪烁请求,并暂存用户设置的亮灭状态 */
if (led_mag->flash != NULL) {
led_mag->stop_flash_req = TRUE;
led_mag->stop_flash_light = on_off;
} else {
__set_led_light(led_mag->drv_s, on_off);
}
}
段码液晶屏主要有两种引脚,公共极 COM 和段电极 SEG,当 COM 口和 SEG 口的电压差大于液晶屏饱和电压时就能够点亮对应的笔段。
注意:压差必须交替变化。
举例:
本 Demo 使用的段码液晶屏是 3 位数字8
的样式,有 4 个 COM 口,6 个 SEG 口,其引脚对应笔段如下表所示:
No. | SEG1 | SEG2 | SEG3 | SEG4 | SEG5 | SEG6 |
---|---|---|---|---|---|---|
COM1 | 3D | 2D | 1D | |||
COM2 | 3C | 3E | 2C | 2E | 1C | 1E |
COM3 | 3B | 3G | 2B | 2G | 1B | 1G |
COM4 | 3A | 3F | 2A | 2F | 1A | 1F |
模拟出如下图所示波形对 COM1~COM4 进行动态扫描,再根据要显示的各个笔段对应的 SEG 口,在每个 COM 口的扫描周期内,控制 6 个 SEG 口的输出即可驱动段码液晶屏显示:
了解了如何驱动段码液晶屏之后即可开始编写驱动组件,结合呼啦圈 demo 的功能需求,组件功能设置如下:
<1> 可在初期设置 COM 口和 SEG 口的引脚;
<2> 可设置液晶屏显示内容的方式有数字、字符串、字符、自定义字符;
<3> 可控制液晶屏点亮或熄灭或闪烁;
<4> 可设置液晶屏闪烁模式,包括全屏闪烁或位闪烁、闪烁开始时和闪烁结束后的状态、闪烁间隔、闪烁指定次数或永远闪烁、闪烁结束时的回调函数;
由于段码液晶屏要控制的引脚较多,且每个引脚对应的笔段也有所不同,为了能简化代码,我们先来检讨 7 个笔段和 SEG 口输出值的存放方式。根据各数据位笔段的分布规律,我们可以使用 1 字节数据来存储 1 个数据位的段码:
1 byte | bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
---|---|---|---|---|---|---|---|---|
笔段 | f | a | g | b | e | c | d | - |
那么常用字符就可以定义为:
CONST UCHAR_T ch_seg_code[] = {
0xde, /* 0 */
0x14, /* 1 */
0x7a, /* 2 */
0x76, /* 3 */
0xb4, /* 4 */
0xe6, /* 5 */
0xee, /* 6 */
0x54, /* 7 */
0xfe, /* 8 */
0xf6, /* 9 */
0x20, /* - */
0x00 /* */
};
另外,6 个 SEG 口的输出值也可以使用 1 字节数据存储,驱动引脚时就可以通过 for 循环实现:
1 byte | bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
---|---|---|---|---|---|---|---|---|
SEG pin | - | - | SEG6 | SEG5 | SEG4 | SEG3 | SEG2 | SEG1 |
那么 4 个 COM 口对应的 SEG 口输出值就可以定义一个数组来存储:
#define COM_NUM 4 /* COM 口数量 */
UCHAR_T seg_pin_code[COM_NUM]; /* 4 组:6 个 SEG 口的输出值,1 字节 code */
在上述定义基础上,如果要让液晶屏显示123
,只需要从0x14
、0x7a
、0x76
中分别取出每个 COM 口对应的段,再存放至seg_pin_code[COM_NUM]
中,就可以得到每个 COM 口对应的 SEG 口输出 code:
bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 | SEG pin code | |
---|---|---|---|---|---|---|---|---|---|
- | - | SEG6 | SEG5 | SEG4 | SEG3 | SEG2 | SEG1 | (无效位默认为 0) | |
COM1 | - | - | - | 0 | - | 1 | - | 1 | 0x05 |
COM2 | - | - | 1 | 0 | 0 | 1 | 1 | 0 | 0x26 |
COM3 | - | - | 1 | 0 | 1 | 1 | 1 | 1 | 0x2F |
COM4 | - | - | 0 | 0 | 1 | 0 | 1 | 0 | 0x0A |
为了方便取出每个 COM 口对应的笔段,我们再定义一个数组来存储 4 个 COM 口对应的笔段在 1 字节数据中的位置:
CONST UCHAR_T ch_com_code[COM_NUM] = {
0x03, /* COM1: (bit0)-, (bit1)d */
0x0c, /* COM2: (bit2)c, (bit3)e */
0x30, /* COM3: (bit4)b, (bit5)g */
0xc0 /* COM4: (bit6)a, (bit7)f */
};
基于以上设定,SEG pin code 生成函数就可以写为:
/**
* @brief 按数据位更新 SEG 引脚输出 code
* @param[in] seg_code: 以"bit7~bit0: fagbecd-"顺序存储的段码
* @param[in] digit: 数据位,0 表示最低位
* @return none
*/
STATIC VOID_T __generate_seg_pin_output_code(IN CONST UCHAR_T seg_code, IN CONST UCHAR_T digit)
{
UCHAR_T i, tmp;
/* 循环处理 4 个 COM 口 */
for (i = 0; i < COM_NUM; i++) {
sg_seg_lcd_mag.seg_pin_code[i] &= ~(ch_com_code[digit]);
tmp = (seg_code & ch_com_code[i]) >> (i*2);
sg_seg_lcd_mag.seg_pin_code[i] |= tmp << (digit*2);
}
}
接下来编写将数字、字符串、字符和自定义字符转换为 SEG pin code 的函数:
显示数字
/* 可显示数字上限 */
#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);
}
}
}
显示字符串
/* 段码液晶屏数据位个数 */
#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));
}
}
显示字符
/**
* @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);
}
显示自定义字符
/* 自定义字符类型 */
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;
}
设备基础服务模块包括设备状态迁移、模式选择、模式切换、本地定时等功能。
定义设备状态:
设备状态 | 含义 | 处理 |
---|---|---|
未使用 | 用户未使用呼啦圈 | 保持熄屏,实时数据清除 |
使用中 | 用户未转动呼啦圈 | 保持亮屏 |
转动中 | 用户转动呼啦圈 | 开始转动时点亮屏幕,转动过程:亮屏 6 秒,熄屏 5 分钟 |
复位 | 设备复位 | 所有数据清除 |
设备状态迁移情况如下(第一列为迁移前状态,第一行为迁移后状态):
未使用 | 使用中 | 转动中 | 复位中 | |
---|---|---|---|---|
未使用 | / | 触发按键 | 呼啦圈转动 | / |
使用中 | 30 秒内无动作 | / | 呼啦圈转动 | 操作复位键 |
转动中 | / | 未检测到磁铁 2 秒后 | / | / |
复位 | / | 复位结束后 | / | / |
代码实现:
/* 第一步:定义基础服务数据类型 (tuya_hula_hoop_svc_basic.h) */
/* 工作模式 */
typedef BYTE_T MODE_E;
#define MODE_NORMAL 0x00 /* 普通模式 */
#define MODE_TARGET 0x01 /* 目标模式 */
/* 设备状态 */
typedef BYTE_T STAT_E;
#define STAT_USING 0x00 /* 使用中 */
#define STAT_ROTATING 0x01 /* 转动中 */
#define STAT_UNUSED 0x02 /* 未使用 */
#define STAT_RESET 0x03 /* 复位 */
/* 基础服务信息管理 */
typedef struct {
MODE_E mode; /* 工作模式 */
MODE_E mode_temp; /* 预选模式 */
STAT_E stat; /* 设备状态 */
} HULA_HOOP_T;
/* 第二步:定义基础服务信息管理结构体并初始化 (tuya_hula_hoop_svc_basic.c) */
HULA_HOOP_T g_hula_hoop;
/**
* @brief 基础服务模块初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_basic_service_init(VOID_T)
{
memset(&g_hula_hoop, 0, SIZEOF(HULA_HOOP_T));
__set_device_status(STAT_USING); /* 上电设备状态:使用中 */
__set_work_mode(MODE_NORMAL); /* 默认工作模式:普通模式 */
}
/* 第三步:编写基础服务相关处理函数 (tuya_hula_hoop_svc_basic.c) */
/**
* @brief 设置工作模式 (内部调用)
* @param[in] mode: 工作模式
* @return none
*/
STATIC VOID_T __set_work_mode(IN CONST MODE_E mode)
{
g_hula_hoop.mode = mode;
switch (mode) {
case MODE_NORMAL:
TUYA_APP_LOG_INFO("Work mode is normal mode now.");
hula_hoop_disp_switch_to_normal_mode();
break;
case MODE_TARGET:
TUYA_APP_LOG_INFO("Work mode is target mode now.");
hula_hoop_disp_switch_to_target_mode();
break;
default:
break;
}
}
/**
* @brief 切换预选模式,在模式键短按处理时调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_switch_temp_mode(VOID_T)
{
switch (g_hula_hoop.mode_temp) {
case MODE_NORMAL:
g_hula_hoop.mode_temp = MODE_TARGET;
break;
case MODE_TARGET:
g_hula_hoop.mode_temp = MODE_NORMAL;
break;
default:
break;
}
}
/**
* @brief 进入模式选择,在模式键短按处理时调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_enter_mode_select(VOID_T)
{
g_hula_hoop.mode_temp = g_hula_hoop.mode;
hula_hoop_disp_switch_to_mode_select();
}
/**
* @brief 退出模式选择,在复位键短按处理时调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_quit_mode_select(VOID_T)
{
__set_work_mode(g_hula_hoop.mode);
}
/**
* @brief 切换到预选模式,在模式键长按 2 秒处理时调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_switch_to_select_mode(VOID_T)
{
hula_hoop_clear_realtime_data();
__set_work_mode(g_hula_hoop.mode_temp);
}
/**
* @brief 设置工作模式,在云端下发工作模式时调用
* @param[in] mode: 工作模式
* @return none
*/
VOID_T hula_hoop_set_work_mode(IN CONST MODE_E mode)
{
if (mode != g_hula_hoop.mode) {
__set_work_mode(mode);
}
}
/**
* @brief 设置设备状态 (内部调用)
* @param[in] stat: 设备状态
* @return none
*/
STATIC VOID_T __set_device_status(IN CONST STAT_E stat)
{
hula_hoop_get_device_status() = stat;
switch (stat) {
case STAT_UNUSED:
TUYA_APP_LOG_INFO("Device status is 'unused'.");
hula_hoop_clear_realtime_data();
hula_hoop_disp_sleep();
break;
case STAT_USING:
TUYA_APP_LOG_INFO("Device status is 'using'.");
hula_hoop_disp_wakeup();
break;
case STAT_ROTATING:
TUYA_APP_LOG_INFO("Device status is 'rotating'.");
hula_hoop_disp_wakeup();
break;
case STAT_RESET:
start_reboot();
break;
default:
break;
}
}
/**
* @brief 设置设备状态,在状态更新时调用
* @param[in] stat: 设备状态
* @return none
*/
VOID_T hula_hoop_set_device_status(IN CONST STAT_E stat)
{
if (stat != hula_hoop_get_device_status()) {
__set_device_status(stat);
}
}
/**
* @brief 获取设备状态
* @param[in] none
* @return 设备状态
*/
STAT_E hula_hoop_get_device_status(VOID_T)
{
return hula_hoop_get_device_status();
}
/* 第四步:在头文件中定义基础服务处理接口 (tuya_hula_hoop_svc_basic.h) */
VOID_T hula_hoop_switch_temp_mode(VOID_T);
VOID_T hula_hoop_enter_mode_select(VOID_T);
VOID_T hula_hoop_quit_mode_select(VOID_T);
VOID_T hula_hoop_switch_to_select_mode(VOID_T);
VOID_T hula_hoop_set_work_mode(IN CONST MODE_E mode);
VOID_T hula_hoop_set_device_status(IN CONST STAT_E stat);
STAT_E hula_hoop_get_device_status(VOID_T);
VOID_T hula_hoop_basic_service_init(VOID_T);
除以上基础服务外,为了实现每日数据和每月数据的累计功能,需添加本地定时功能,以实现日期和月份变更的检查和处理。
本地定时模块的代码实现:
/* 第一步:定义本地时间数据类型 (tuya_local_time.h) */
typedef struct {
USHORT_T year;
UCHAR_T month;
UCHAR_T day;
UCHAR_T hour;
UCHAR_T minute;
UCHAR_T second;
} LOCAL_TIME_T;
/* 第二步:定义本地时间并初始化 (tuya_local_time.c) */
LOCAL_TIME_T g_local_time = {
.year = 1,
.month = 1,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
};
/* 第三步:定义每月天数表,用于判断月份变更 (tuya_local_time.c) */
STATIC UCHAR_T sg_day_tbl[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/* 第四步:编写本地时间更新相关处理函数 (tuya_local_time.c) */
/**
* @brief 闰年判断并更新 2 月份天数
* @param[in] year: 要判断的年份
* @return 2 月份天数
*/
STATIC UCHAR_T __local_time_leap_year_judgment(USHORT_T year)
{
if ((year % 400 == 0) ||
((year % 4 == 0) && (year / 100 != 0))) {
return 29;
} else {
return 28;
}
}
/**
* @brief 每年更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_year(VOID_T)
{
g_local_time.year++;
sg_day_tbl[1] = __local_time_leap_year_judgment(g_local_time.year);
}
/**
* @brief 每月更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_month(VOID_T)
{
g_local_time.month++;
if (g_local_time.month > 12) {
g_local_time.month = 1;
__local_time_update_per_year();
}
}
/**
* @brief 每天更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_day(VOID_T)
{
g_local_time.day++;
if (g_local_time.day > sg_day_tbl[g_local_time.month-1]) {
g_local_time.day = 1;
__local_time_update_per_month();
}
}
/**
* @brief 每小时更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_hour(VOID_T)
{
g_local_time.hour++;
if (g_local_time.hour >= 24) {
g_local_time.hour = 0;
__local_time_update_per_day();
}
}
/**
* @brief 每分钟更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_minute(VOID_T)
{
g_local_time.minute++;
if (g_local_time.minute >= 60) {
g_local_time.minute = 0;
__local_time_update_per_hour();
}
}
/**
* @brief 每秒钟更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_second(VOID_T)
{
g_local_time.second++;
if (g_local_time.second >= 60) {
g_local_time.second = 0;
__local_time_update_per_minute();
}
}
/**
* @brief 本地时间更新
* @param[in] none
* @return none
*/
VOID_T tuya_local_time_update(VOID_T)
{
__local_time_update_per_second();
}
/**
* @brief 获取本地时间
* @param[in] none
* @return 本地时间
*/
LOCAL_TIME_T tuya_get_local_time(VOID_T)
{
return g_local_time;
}
/* 第五步:在头文件中定义接口 (tuya_local_time.h) */
VOID_T tuya_local_time_update(VOID_T);
LOCAL_TIME_T tuya_get_local_time(VOID_T);
/* 第六步:编写本地时间变更检查处理函数 (tuya_hula_hoop_svc_basic.c) */
/**
* @brief 检查日期是否变更并处理
* @param[in] none
* @return none
*/
VOID_T hula_hoop_check_date_change(VOID_T)
{
STATIC UCHAR_T s_day = 0;
STATIC UCHAR_T s_month = 0;
LOCAL_TIME_T local_time;
/* 获取本地时间 */
local_time = tuya_get_local_time();
/* 日期变更检查 */
if (local_time.day != s_day) {
s_day = local_time.day;
hula_hoop_update_total_data_day();
}
/* 月份变更检查 */
if (local_time.month != s_month) {
s_month = local_time.month;
hula_hoop_update_total_data_month();
}
}
/* 第七步:在头文件中定义接口 (tuya_hula_hoop_svc_basic.h) */
VOID_T hula_hoop_check_date_change(VOID_T);
/* 第八步:在本地时间更新定时函数中调用相关接口进行处理 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 本地时间更新定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __upd_local_time_timer(IN CONST UINT_T time_inc)
{
g_timer.upd_local_time += time_inc;
if (g_timer.upd_local_time >= LOCAL_TIME_UPDATE_INTV_MS) {
g_timer.upd_local_time -= LOCAL_TIME_UPDATE_INTV_MS;
tuya_local_time_update(); /* 本地时间更新 */
hula_hoop_check_date_change(); /* 检查日期变更 */
}
}
显示处理模块包括显示屏内容的设定、显示屏状态控制和指示灯状态控制。
显示内容
显示模式 | 段码液晶屏显示 | LED 显示 |
---|---|---|
普通模式界面 | 显示实时数据,按”实时计时->实时圈数->实时卡路里“顺序切换 | 点亮数据对应指示灯 |
目标模式界面 | 当日目标剩余时长≠0 时,显示该数据,否则显示”—“ | 点亮计时指示灯 |
模式选择界面 | 普通模式显示”010“,目标模式显示”020“ | 全灭 |
复位提示界面 | 显示”000“ | 全灭 |
计时指示灯
指示灯功能 | 指示灯状态 | 优先级 |
---|---|---|
配网指示功能 | 配网指示灯快闪 | 高 |
数据指示功能 | 根据显示模式控制状态 | 低 |
液晶屏
段码液晶屏状态 | 处理 |
---|---|
熄灭 | 段码液晶屏熄灭 |
亮屏 | 段码液晶屏点亮 |
闪烁(目标完成) | 段码液晶屏闪烁 3 次,结束时点亮(保持“—”显示)(目标模式界面才执行) |
闪烁(复位提示) | 段码液晶屏闪烁 3 次,结束时执行“设备复位” |
代码实现
外设驱动的部分可以使用之前编写的 LED 驱动组件和 Segment LCD 驱动组件进行开发,本 demo 硬件方案如下:
外设 | 引脚 | 有效电平 |
---|---|---|
计时/配网指示灯 | PD4 | H |
圈数指示灯 | PB6 | H |
卡路里指示灯 | PD7 | H |
段码液晶屏 | COM1 - COM4 : PA1, PC2, PC3, PB4 SEG1 - SEG6 : PA0, PC0, PD3, PC1, PC4, PB5 |
/ |
实现显示处理模块的各项功能
/* 第一步:定义显示数据类型 (tuya_hula_hoop_svc_disp.h) */
/* 显示模式 */
typedef BYTE_T DISP_MODE_E;
#define DISP_NORMAL_MODE 0x00 /* 普通模式界面 */
#define DISP_TARGET_MODE 0x01 /* 目标模式界面 */
#define DISP_MODE_SELECT 0x02 /* 模式选择界面 */
#define DISP_RESET_REMIND 0x03 /* 复位提醒界面 */
/* 显示数据 */
typedef BYTE_T DISP_DATA_E;
#define DISP_DATA_TIME 0x00 /* 显示计时数据 */
#define DISP_DATA_COUNT 0x01 /* 显示圈数数据 */
#define DISP_DATA_CALORIES 0x02 /* 显示卡路里数据 */
#define DISP_DATA_NONE 0x03 /* 不显示数据 */
/* 指示灯功能 */
typedef BYTE_T LED_FUNC_E;
#define LED_FUNC_DATA 0x00 /* 数据指示功能 */
#define LED_FUNC_BIND 0x01 /* 配网指示功能 */
/* 屏幕状态 */
typedef BYTE_T SEG_LCD_STAT_E;
#define SEG_LCD_STAT_OFF 0x00 /* 屏幕点亮 */
#define SEG_LCD_STAT_ON 0x01 /* 屏幕熄灭 */
#define SEG_LCD_STAT_FLASH 0x02 /* 屏幕闪烁 */
/* 第二步:定义显示处理数据结构和外设引脚 (tuya_hula_hoop_svc_disp.c) */
/* 显示处理数据类型 */
typedef struct {
DISP_MODE_E mode;
DISP_DATA_E data;
LED_FUNC_E led_func;
SEG_LCD_STAT_E seg_lcd_stat;
} HULA_HOOP_DISP_T;
/* 指示灯引脚 */
STATIC TY_GPIO_PORT_E sg_user_led_pin[] = {
TY_GPIOD_4, /* 计时/配网指示灯 */
TY_GPIOB_6, /* 圈数指示灯 */
TY_GPIOD_7 /* 卡路里指示灯 */
};
/* 指示灯句柄 */
LED_HANDLE g_user_led_handle[(SIZEOF(sg_user_led_pin) / SIZEOF(sg_user_led_pin[0]))];
/* 段码液晶屏引脚 */
SEG_LCD_PIN_T seg_lcd_pin_s = {
.com = {TY_GPIOA_1, TY_GPIOC_2, TY_GPIOC_3, TY_GPIOB_4},
.seg = {TY_GPIOA_0, TY_GPIOC_0, TY_GPIOD_3, TY_GPIOC_1, TY_GPIOC_4, TY_GPIOB_5}
};
/* 显示信息管理 */
STATIC HULA_HOOP_DISP_T sg_disp;
/* 第三步:编写显示处理初始化函数 (tuya_hula_hoop_svc_disp.c) */
/**
* @brief 显示处理模块初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_proc_init(VOID_T)
{
UCHAR_T i;
LED_RET ret;
/* 指示灯初始化 */
for (i = 0; i < (SIZEOF(sg_user_led_pin) / SIZEOF(sg_user_led_pin[0])); i++) {
ret = tuya_create_led_handle(sg_user_led_pin[i], FALSE, &g_user_led_handle[i]);
if (ret != LED_OK) {
TUYA_APP_LOG_ERROR("led init err:%d", ret);
}
}
/* 段码液晶屏初始化 */
tuya_seg_lcd_init(seg_lcd_pin_s);
/* 变量初始化 */
memset(&sg_disp, 0, SIZEOF(HULA_HOOP_DISP_T));
}
/* 第四步:编写指示灯和段码液晶屏状态控制函数 (tuya_hula_hoop_svc_disp.c) */
/**
* @brief 设置指示灯状态(作为配网指示时)
* @param[in] none
* @return none
*/
STATIC VOID_T __set_net_led_status(VOID_T)
{
tuya_set_led_flash(g_user_led_handle[DISP_DATA_TIME], LFM_FOREVER, LFT_STA_ON_END_ON, LED_FLASH_INTV_MS, LED_FLASH_INTV_MS, 0, NULL);
tuya_set_led_light(g_user_led_handle[DISP_DATA_COUNT], FALSE);
tuya_set_led_light(g_user_led_handle[DISP_DATA_CALORIES], FALSE);
}
/**
* @brief 设置指示灯状态(作为数据指示时)
* @param[in] data: 显示数据
* @return none
*/
STATIC VOID_T __set_data_led_status(IN CONST DISP_DATA_E data)
{
UCHAR_T i;
if (sg_disp.led_func == LED_FUNC_BIND) {
return;
}
for (i = 0; i < (SIZEOF(sg_user_led_pin) / SIZEOF(sg_user_led_pin[0])); i++) {
if (i == data) {
tuya_set_led_light(g_user_led_handle[i], TRUE);
} else {
tuya_set_led_light(g_user_led_handle[i], FALSE);
}
}
}
/**
* @brief 目标完成提示结束时的回调函数
* @param[in] none
* @return none
*/
STATIC VOID_T __disp_target_remind_end_cb()
{
__set_seg_lcd_status(SEG_LCD_STAT_ON);
}
/**
* @brief 复位提示结束时的回调函数
* @param[in] none
* @return none
*/
STATIC VOID_T __disp_reset_remind_end_cb()
{
hula_hoop_set_work_stat(STAT_RESET);
}
/**
* @brief 设置段码液晶屏状态
* @param[in] stat: 段码液晶屏状态
* @return none
*/
STATIC VOID_T __set_seg_lcd_status(IN CONST SEG_LCD_STAT_E stat)
{
sg_disp.seg_lcd_stat = stat;
switch (stat) {
case SEG_LCD_STAT_OFF:
tuya_seg_lcd_set_light(FALSE);
break;
case SEG_LCD_STAT_ON:
tuya_seg_lcd_set_light(TRUE);
break;
case SEG_LCD_STAT_FLASH:
if (sg_disp.mode == DISP_TARGET_MODE) {
tuya_seg_lcd_set_flash(SEG_LCD_FLASH_DIGIT_ALL, SLFT_STA_ON_END_ON, SEG_LCD_FLASH_INTV_MS, SEG_LCD_FLASH_COUNT, __disp_target_remind_end_cb);
}
if (sg_disp.mode == DISP_RESET_REMIND) {
tuya_seg_lcd_set_flash(SEG_LCD_FLASH_DIGIT_ALL, SLFT_STA_ON_END_OFF, SEG_LCD_FLASH_INTV_MS, SEG_LCD_FLASH_COUNT, __disp_reset_remind_end_cb);
}
break;
default:
break;
}
}
/* 第四步:编写各界面的显示内容设定函数和显示处理循环 (tuya_hula_hoop_svc_disp.c) */
/**
* @brief 设置【普通模式界面】显示内容
* @param[in] data: 显示数据
* @return none
*/
STATIC VOID_T __set_seg_lcd_disp_normal_mode(IN CONST DISP_DATA_E data)
{
switch (data) {
case DISP_DATA_TIME: /* 显示计时数据 */
tuya_seg_lcd_disp_num(g_sport_data.time_realtime, 0);
break;
case DISP_DATA_COUNT: /* 显示圈数数据 */
tuya_seg_lcd_disp_num(g_sport_data.count_realtime, 0);
break;
case DISP_DATA_CALORIES: /* 显示卡路里数据 */
tuya_seg_lcd_disp_num(g_sport_data.calories_realtime, 0);
break;
default:
break;
}
}
/**
* @brief 设置【目标模式界面】显示内容
* @param[in] none
* @return none
*/
STATIC VOID_T __set_seg_lcd_disp_target_mode(VOID_T)
{
/* 当日目标剩余时长等于 0 时显示“---”,否则显示“当日目标剩余时长” */
if (g_sport_data.time_remain_today == 0) {
tuya_seg_lcd_disp_str("---");
} else {
tuya_seg_lcd_disp_num(g_sport_data.time_remain_today, 0);
}
}
/**
* @brief 设置【模式选择界面】显示内容
* @param[in] none
* @return none
*/
STATIC VOID_T __set_seg_lcd_disp_mode_select(VOID_T)
{
switch (g_hula_hoop.mode_temp) {
case MODE_NORMAL: /* 预选模式为普通模式时,显示“010” */
tuya_seg_lcd_disp_str("010");
break;
case MODE_TARGET: /* 预选模式为目标模式时,显示“020” */
tuya_seg_lcd_disp_str("020");
break;
default:
break;
}
}
/**
* @brief 设置【复位提醒界面】显示内容
* @param[in] none
* @return none
*/
STATIC VOID_T __set_seg_lcd_disp_reset_remind(VOID_T)
{
/* 显示“000” */
tuya_seg_lcd_disp_str("000");
}
/**
* @brief 显示处理循环
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_proc_loop(VOID_T)
{
/* 根据当前显示模式设置显示内容 */
switch (sg_disp.mode) {
case DISP_NORMAL_MODE:
__set_seg_lcd_disp_normal_mode(sg_disp.data);
break;
case DISP_TARGET_MODE:
__set_seg_lcd_disp_target_mode();
break;
case DISP_MODE_SELECT:
__set_seg_lcd_disp_mode_select();
break;
case DISP_RESET_REMIND:
__set_seg_lcd_disp_reset_remind();
break;
default:
break;
}
}
/* 第五步:编写显示模式迁移相关函数 (tuya_hula_hoop_svc_disp.c) */
/**
* @brief 切换到【普通模式界面】
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_switch_to_normal_mode(VOID_T)
{
sg_disp.mode = DISP_NORMAL_MODE;
sg_disp.data = DISP_DATA_TIME;
__set_led_status(sg_disp.data);
__set_seg_lcd_status(SEG_LCD_STAT_ON);
__set_seg_lcd_disp_normal_mode(DISP_DATA_TIME);
}
/**
* @brief 切换到【目标模式界面】
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_switch_to_target_mode(VOID_T)
{
sg_disp.mode = DISP_TARGET_MODE;
sg_disp.data = DISP_DATA_TIME;
__set_led_status(sg_disp.data);
__set_seg_lcd_status(SEG_LCD_STAT_ON);
__set_seg_lcd_disp_target_mode();
}
/**
* @brief 切换到【模式选择界面】
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_switch_to_mode_select(VOID_T)
{
sg_disp.mode = DISP_MODE_SELECT;
sg_disp.data = DISP_DATA_NONE;
__set_led_status(sg_disp.data);
__set_seg_lcd_status(SEG_LCD_STAT_ON);
__set_seg_lcd_disp_mode_select();
}
/**
* @brief 切换到【复位提醒界面】
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_switch_to_reset_remind(VOID_T)
{
sg_disp.mode = DISP_RESET_REMIND;
sg_disp.data = DISP_DATA_NONE;
__set_led_status(sg_disp.data);
__set_seg_lcd_status(SEG_LCD_STAT_FLASH);
__set_seg_lcd_disp_reset_remind();
}
/* 第六步:编写其他显示处理相关函数 (tuya_hula_hoop_svc_disp.c) */
/**
* @brief 设置指示灯功能,在联网处理模块中调用
* @param[in] func: 指示灯功能
* @return none
*/
VOID_T hula_hoop_disp_set_led_func(IN CONST LED_FUNC_E func)
{
if (func == sg_disp.led_func) {
return;
}
sg_disp.led_func = func;
if (func == LED_FUNC_BIND) {
__set_net_led_status();
} else {
__set_data_led_status(sg_disp.data);
}
}
/**
* @brief 显示唤醒,在需要点亮屏幕时调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_wakeup(VOID_T)
{
if (sg_disp.mode <= DISP_TARGET_MODE) {
sg_disp.data = DISP_DATA_TIME;
}
__set_data_led_status(sg_disp.data);
__set_seg_lcd_status(SEG_LCD_STAT_ON);
}
/**
* @brief 显示休眠,在需要熄灭屏幕时调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_disp_sleep(VOID_T)
{
__set_data_led_status(DISP_DATA_NONE);
__set_seg_lcd_status(SEG_LCD_STAT_OFF);
}
/**
* @brief 获取当前显示状态,在检查当前显示状态时调用
* @param[in] none
* @return 1-点亮,0-熄灭
*/
BOOL_T hula_hoop_disp_is_wakeup(VOID_T)
{
if (sg_disp.seg_lcd_stat == SEG_LCD_STAT_OFF) {
return FALSE;
} else {
return TRUE;
}
}
/**
* @brief 检查屏幕是否正在闪烁,在熄屏计时中调用,避免闪烁时被外部设置为熄屏
* @param[in] none
* @return TRUE - 闪烁, FALSE - 不闪烁
*/
BOOL_T hula_hoop_disp_is_flash(VOID_T)
{
if (sg_disp.seg_lcd_stat == SEG_LCD_STAT_FLASH) {
return TRUE;
} else {
return FALSE;
}
}
/**
* @brief 获取当前显示模式,在判断当前显示界面时调用
* @param[in] none
* @return 显示模式
*/
DISP_MODE_E hula_hoop_get_disp_mode(VOID_T)
{
return sg_disp.mode;
}
/**
* @brief 切换显示数据,在显示数据切换定时中调用
* @param[in] none
* @return none
*/
VOID_T hula_hoop_switch_disp_data(VOID_T)
{
if (sg_disp.data == DISP_DATA_CALORIES) {
if (hula_hoop_get_device_status() != STAT_ROTATING) {
sg_disp.data = DISP_DATA_TIME;
}
} else {
sg_disp.data++;
}
if (sg_disp.seg_lcd_stat != SEG_LCD_STAT_OFF) {
__set_led_status(sg_disp.data);
}
}
/* 目标完成时闪烁处理 */
VOID_T hula_hoop_disp_target_finish(VOID_T)
{
if (sg_disp.mode == DISP_TARGET_MODE) {
__set_seg_lcd_status(SEG_LCD_STAT_FLASH);
}
}
/* 第七步:在头文件中定义显示处理接口,供外部调用 (tuya_hula_hoop_svc_disp.h) */
VOID_T hula_hoop_disp_proc_init(VOID_T);
VOID_T hula_hoop_disp_proc_loop(VOID_T);
VOID_T hula_hoop_disp_switch_to_normal_mode(VOID_T);
VOID_T hula_hoop_disp_switch_to_target_mode(VOID_T);
VOID_T hula_hoop_disp_switch_to_mode_select(VOID_T);
VOID_T hula_hoop_disp_switch_to_reset_remind(VOID_T);
VOID_T hula_hoop_disp_set_led_func(IN CONST LED_FUNC_E func);
VOID_T hula_hoop_disp_wakeup(VOID_T);
VOID_T hula_hoop_disp_sleep(VOID_T);
BOOL_T hula_hoop_disp_is_wakeup(VOID_T);
BOOL_T hula_hoop_disp_is_flash(VOID_T);
DISP_MODE_E hula_hoop_get_disp_mode(VOID_T);
VOID_T hula_hoop_switch_disp_data(VOID_T);
VOID_T hula_hoop_disp_target_finish(VOID_T);
智能呼啦圈相关的运动数据设定如下:
No. | 数据 | 类别 | 数值范围 | 说明 |
---|---|---|---|---|
1 | 实时计时 | 实时 | 0~999 | 可上报 |
2 | 实时圈数 | 实时 | 0~999 | 可上报 |
3 | 实时卡路里 | 实时 | 0~999 | 可上报 |
4 | 当日累计计时 | 累计 | 0~1440 | 可上报 |
5 | 当日累计圈数 | 累计 | 0~99999 | 可上报 |
6 | 当日累计卡路里 | 累计 | 0~999 | 可上报 |
7 | 当月累计计时 | 累计 | 0~44640 | 仅用于计算当月剩余 |
8 | 30 天累计计时 | 累计 | 0~43200 | 可上报 |
9 | 30 天累计圈数 | 累计 | 0~9999999 | 可上报 |
10 | 30 天累计卡路里 | 累计 | 0~99999 | 可上报 |
11 | 当日目标时长 | 目标 | 0~45 | 云端下发 |
12 | 当月目标时长 | 目标 | 0~1350 | 云端下发 |
13 | 当日目标剩余 | 剩余 | 0~45 | 可上报 |
14 | 当月目标剩余 | 剩余 | 0~1350 | 可上报 |
15 | 每日累计时长 | 累计 | 0~1440 | 30 个数据,仅用于计算 30 天累计 |
16 | 每日累计圈数 | 累计 | 0~99999 | 30 个数据,仅用于计算 30 天累计 |
17 | 每日累计卡路里 | 累计 | 0~999 | 30 个数据,仅用于计算 30 天累计 |
数据更新处理的功能需求梳理如下:
更新节点 | 更新数据 | 数据类别 | 更新方式 |
---|---|---|---|
每分钟 | 时间 | 实时、累计 剩余 |
正向计数,实时超过 999 时从 0 开始计数 反向计数,等于 0 时停止 |
每圈 | 圈数/卡路里 | 实时、累计 | 圈数:正向计数;卡路里:按 1kcal=0.1 圈计算 |
目标设定时 | 时间 | 剩余 | 目标>累计时,目标剩余=目标-累计 |
日期变更时 | 计时/圈数/卡路里 | 30 天累计 当日累计 |
减去 30 天前的数据,每日累计数据迁移 清零 |
月份变更时 | 时间 | 当月累计 | 清零 |
模式更新时 | 计时/圈数/卡路里 | 实时 | 清零 |
停止使用时 | 计时/圈数/卡路里 | 实时 | 清零 |
代码实现
/* 第一步:定义数据类型 (tuya_hula_hoop_svc_data.h) */
typedef struct {
USHORT_T time_realtime;
USHORT_T count_realtime;
USHORT_T calories_realtime;
USHORT_T time_total_today;
UINT_T count_total_today;
USHORT_T calories_total_today;
USHORT_T time_total_month;
USHORT_T time_total_30days;
UINT_T count_total_30days;
UINT_T calories_total_30days;
UCHAR_T time_target_today;
USHORT_T time_target_month;
UCHAR_T time_remain_today;
USHORT_T time_remain_month;
USHORT_T time_total_days[SPORT_DATA_HISTORY_SIZE];
UINT_T count_total_days[SPORT_DATA_HISTORY_SIZE];
USHORT_T calories_total_days[SPORT_DATA_HISTORY_SIZE];
} HULA_HOOP_SPORT_DATA_T;
/* 第二步:定义运动数据并初始化 (tuya_hula_hoop_svc_data.c) */
HULA_HOOP_SPORT_DATA_T g_sport_data;
/**
* @brief 运动数据处理模块初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_data_proc_init(VOID_T)
{
memset(&g_sport_data, 0, SIZEOF(HULA_HOOP_SPORT_DATA_T));
}
/* 第三步:编写每分钟处理的运动数据更新函数 (tuya_hula_hoop_svc_data.c) */
/**
* @brief 更新运动数据 --时间
* @param[in] none
* @return 1-目标完成,0-目标未完成
*/
BOOL_T hula_hoop_update_sport_data_time(VOID_T)
{
/* 实时计时 */
if (g_sport_data.time_realtime < 999) {
g_sport_data.time_realtime++;
} else {
g_sport_data.time_realtime = 0;
}
/* 累计计时 */
g_sport_data.time_total_today++;
g_sport_data.time_total_month++;
g_sport_data.time_total_30days++;
/* 目标剩余时长 */
if (g_sport_data.time_remain_today > 0) {
g_sport_data.time_remain_today--;
if (g_sport_data.time_remain_today == 0) {
return TRUE;/* 目标剩余倒计时为 0 时,告知目标完成 */
}
}
if (g_sport_data.time_remain_month > 0) {
g_sport_data.time_remain_month--;
}
return FALSE;
}
/* 第四步:编写每圈处理的运动数据更新函数 (tuya_hula_hoop_svc_data.c) */
/**
* @brief 更新运动数据 --圈数
* @param[in] none
* @return none
*/
VOID_T hula_hoop_update_sport_data_count(VOID_T)
{
/* 实时圈数 */
g_sport_data.count_realtime++;
/* 累计圈数 */
g_sport_data.count_total_today++;
g_sport_data.count_total_30days++;
}
/**
* @brief 更新运动数据 --卡路里
* @param[in] none
* @return none
*/
VOID_T hula_hoop_update_sport_data_calories(VOID_T)
{
/* 实时卡路里 */
g_sport_data.calories_realtime = g_sport_data.count_realtime / 10;
/* 累计卡路里 */
g_sport_data.calories_total_today = g_sport_data.count_total_today / 10;
g_sport_data.calories_total_30days = g_sport_data.count_total_30days / 10;
}
/* 第五步:编写目标设定时的运动数据处理函数 (tuya_hula_hoop_svc_data.c) */
/**
* @brief 设置当日目标时间
* @param[in] tar_tm: 目标时间
* @return none
*/
VOID_T hula_hoop_set_time_target_today(IN CONST UCHAR_T tar_tm)
{
if (tar_tm == g_sport_data.time_target_today) {
return;
}
g_sport_data.time_target_today = tar_tm;
if (tar_tm > g_sport_data.time_total_today) {
g_sport_data.time_remain_today = tar_tm - g_sport_data.time_total_today;
} else {
g_sport_data.time_remain_today = 0;
}
}
/**
* @brief 设置当月目标时间
* @param[in] tar_tm: 目标时间
* @return none
*/
VOID_T hula_hoop_set_time_target_month(IN CONST USHORT_T tar_tm)
{
if (tar_tm == g_sport_data.time_target_month) {
return;
}
g_sport_data.time_target_month = tar_tm;
if (tar_tm > g_sport_data.time_total_month) {
g_sport_data.time_remain_month = tar_tm - g_sport_data.time_total_month;
} else {
g_sport_data.time_remain_month = 0;
}
}
/* 第六步:编写日期变更和月份变更时的运动数据处理函数 (tuya_hula_hoop_svc_data.c) */
/**
* @brief 更新当日累计数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_update_total_data_day(VOID_T)
{
UCHAR_T i;
g_sport_data.time_total_30days -= g_sport_data.time_total_days[0];
g_sport_data.count_total_30days -= g_sport_data.count_total_days[0];
g_sport_data.calories_total_30days -= g_sport_data.calories_total_days[0];
for (i = 0; i < (SPORT_DATA_HISTORY_SIZE-1); i++) {
g_sport_data.time_total_days[i] = g_sport_data.time_total_days[i+1];
g_sport_data.count_total_days[i] = g_sport_data.count_total_days[i+1];
g_sport_data.calories_total_days[i] = g_sport_data.calories_total_days[i+1];
}
g_sport_data.time_total_days[i] = 0;
g_sport_data.count_total_days[i] = 0;
g_sport_data.calories_total_days[i] = 0;
g_sport_data.time_total_today = 0;
g_sport_data.count_total_today = 0;
g_sport_data.calories_total_today = 0;
}
/**
* @brief 更新当月累计数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_update_total_data_month(VOID_T)
{
g_sport_data.time_total_month = 0;
}
/* 第七步:编写用于运动数据清零的相关函数 (tuya_hula_hoop_svc_data.c) */
/**
* @brief 清除实时数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_clear_realtime_data(VOID_T)
{
g_sport_data.time_realtime = 0;
g_sport_data.count_realtime = 0;
g_sport_data.calories_realtime = 0;
}
/**
* @brief 清除所有运动数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_clear_sport_data(VOID_T)
{
memset(&g_sport_data, 0, SIZEOF(g_sport_data));
}
/* 第八步:在头文件中定义外部调用接口 (tuya_hula_hoop_svc_data.h) */
VOID_T hula_hoop_data_proc_init(VOID_T);
BOOL_T hula_hoop_update_sport_data_time(VOID_T);
VOID_T hula_hoop_update_sport_data_count(VOID_T);
VOID_T hula_hoop_update_sport_data_calories(VOID_T);
VOID_T hula_hoop_set_time_target_today(IN CONST UCHAR_T tar_tm);
VOID_T hula_hoop_set_time_target_month(IN CONST USHORT_T tar_tm);
VOID_T hula_hoop_update_total_data_day(VOID_T);
VOID_T hula_hoop_update_total_data_month(VOID_T);
VOID_T hula_hoop_clear_realtime_data(VOID_T);
VOID_T hula_hoop_clear_sport_data(VOID_T);
用户可触发的部件有按键和霍尔传感器,前者用于模式切换、配网请求和设备复位,后者用于检测呼啦圈转动圈数。根据功能需求描述,用户事件处理模块的处理内容梳理如下:
事件 | 事件类型 | 限制条件 | 处理内容 |
---|---|---|---|
触发模式键 | 短按 | 模式选择界面 | 切换“预选模式” |
其他 | 进入“模式选择”处理 | ||
长按 2 秒 | 模式选择界面 | 模式更新为“预选模式”,数据上报 | |
长按 5 秒 | - | 允许配网 | |
触发复位键 | 短按 | 模式选择界面 | 退出“模式选择”处理 |
长按 2 秒 | 【使用中】 | 切换到复位提示界面 | |
触发任意键 | 任意类型 | 熄屏状态 | 点亮屏幕 |
亮屏状态 | 复位“熄屏计时”和“停用计时” | ||
磁铁接触霍尔 | 短触 | - | 数据更新,→【转动中】,“复位停转计时” |
代码实现:
外设驱动的部分可以使用之前编写的 KEY 驱动组件和 HALL_SW 驱动组件快速进行开发,本 demo 硬件方案如下:
外设 | 引脚 | 有效电平 |
---|---|---|
模式键 | PB7 | L |
复位键 | PB1 | L |
霍尔传感器 | PD2 | H |
下面开始编写用户事件处理模块:
/* 第一步:定义按键&霍尔注册信息,编写初始化函数 (tuya_hula_hoop_evt_user.c) */
/* 回调函数定义 */
STATIC VOID_T __mode_key_cb(KEY_PRESS_TYPE_E type);
STATIC VOID_T __reset_key_cb(KEY_PRESS_TYPE_E type);
STATIC VOID_T __hall_switch_cb(KEY_PRESS_TYPE_E type);
/* 模式键注册信息定义 */
KEY_DEF_T mode_key_def_s = {
.port = TY_GPIOB_7,
.active_low = TRUE,
.long_press_time1 = 2000,
.long_press_time2 = 5000,
.key_cb = __mode_key_cb
};
/* 复位键注册信息定义 */
KEY_DEF_T reset_key_def_s = {
.port = TY_GPIOB_1,
.active_low = TRUE,
.long_press_time1 = 2000,
.long_press_time2 = 0,
.key_cb = __reset_key_cb
};
/* 霍尔传感器注册信息定义 */
HALL_SW_DEF_T hall_sw_def_s = {
.port = TY_GPIOD_2,
.active_low = FALSE,
.hall_sw_cb = __hall_sw_cb,
.invalid_intv = 200000
};
/**
* @brief 按键&霍尔处理模块初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_key_hall_init(VOID_T)
{
UCHAR_T ret;
/* 注册模式键 */
ret = tuya_reg_key(&mode_key_def_s);
if (KEY_OK != ret) {
TUYA_APP_LOG_ERROR("mode key init error: %d", ret);
}
/* 注册复位键 */
ret = tuya_reg_key(&reset_key_def_s);
if (KEY_OK != ret) {
TUYA_APP_LOG_ERROR("reset key init error: %d", ret);
}
/* 注册霍尔传感器 */
ret = tuya_reg_hall_sw(&hall_sw_def_s);
if (HSW_OK != ret) {
TUYA_APP_LOG_ERROR("hall switch init error: %d", ret);
}
}
/* 第二步:在头文件中定义用户事件模块初始化接口 (tuya_hula_hoop_evt_user.h) */
VOID_T hula_hoop_key_hall_init(VOID_T);
/* 第三步:编写按键回调函数和各事件处理函数 (tuya_hula_hoop_evt_user.c) */
/**
* @brief 模式键短按处理
* @param[in] none
* @return none
*/
STATIC VOID_T __mode_key_short_press_handler(VOID_T)
{
if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {/* 当前显示界面不是[模式选择] */
hula_hoop_enter_mode_select(); /* 进入模式选择 */
} else {
hula_hoop_switch_temp_mode(); /* 退出模式选择 */
}
}
/**
* @brief 模式键长按 2s 处理
* @param[in] none
* @return none
*/
STATIC VOID_T __mode_key_long_press_handler(VOID_T)
{
/* 当前显示界面不是[模式选择]时不处理 */
if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
return;
}
/* 切换到预选模式并上报工作模式相关 DP 数据 */
hula_hoop_switch_to_select_mode();
hula_hoop_reset_upd_time_data_timer();
hula_hoop_report_mode();
}
/**
* @brief 模式键长按 5s 处理
* @param[in] none
* @return none
*/
STATIC VOID_T __mode_key_longer_press_handler(VOID_T)
{
/* 允许用户绑定 */
hula_hoop_allow_binding();
}
/**
* @brief 复位键短按处理
* @param[in] none
* @return none
*/
STATIC VOID_T __reset_key_short_press_handler(VOID_T)
{
/* 当前显示界面不是[模式选择]时不处理 */
if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
return;
}
/* 退出模式选择 */
hula_hoop_quit_mode_select();
}
/**
* @brief 复位键长按 2s 处理
* @param[in] none
* @return none
*/
STATIC VOID_T __reset_key_long_press_handler(VOID_T)
{
/* 设备当前状态不是使用中时不处理 */
if (hula_hoop_get_device_status() != STAT_USING) {
return;
}
/* 切换到复位提醒 */
hula_hoop_disp_switch_to_reset_remind();
}
/**
* @brief 霍尔传感器触发时处理
* @param[in] none
* @return none
*/
STATIC VOID_T __hall_switch_handler(VOID_T)
{
hula_hoop_update_sport_data_count(); /* 更新圈数数据 */
hula_hoop_update_sport_data_calories(); /* 更新卡路里数据 */
hula_hoop_set_work_stat(STAT_ROTATING); /* 设置设备状态为[旋转中] */
hula_hoop_reset_timer_for_hall_event(); /* 复位霍尔事件发生禁止的定时器计时 */
}
/**
* @brief 按键事件通用处理
* @param[in] none
* @return TRUE - 处理完成 FALSE - 处理未完成
*/
STATIC BOOL_T __key_event_handler(VOID_T)
{
/* 熄屏时,唤醒屏幕 */
if (FALSE == hula_hoop_disp_is_wakeup()) {
hula_hoop_disp_wakeup();
/* 未使用->使用中 */
if (hula_hoop_get_device_status() == STAT_UNUSED) {
hula_hoop_set_device_status(STAT_USING);
}
return TRUE;
/* 亮屏时,复位熄屏定时器计时 */
} else {
hula_hoop_reset_timer_for_key_event();
}
return FALSE;
}
/**
* @brief 模式键回调函数
* @param[in] type: 事件类型
* @return none
*/
STATIC VOID_T __mode_key_cb(KEY_PRESS_TYPE_E type)
{
/* 按键唤醒屏幕时不处理短按事件 */
BOOL_T ret = __key_event_handler();
if ((ret) &&
(type == SHORT_PRESS)) {
return;
}
/* 事件类型判断 */
switch (type) {
case SHORT_PRESS:
TUYA_APP_LOG_INFO("mode key pressed.");
__mode_key_short_press_handler();
break;
case LONG_PRESS_FOR_TIME1:
TUYA_APP_LOG_INFO("mode key long pressed for time1.");
__mode_key_long_press_handler();
break;
case LONG_PRESS_FOR_TIME2:
TUYA_APP_LOG_INFO("mode key long pressed for time2.");
__mode_key_longer_press_handler();
break;
default:
break;
}
}
/**
* @brief 复位键回调函数
* @param[in] type: 事件类型
* @return none
*/
STATIC VOID_T __reset_key_cb(KEY_PRESS_TYPE_E type)
{
/* 按键唤醒屏幕时不处理短按事件 */
BOOL_T ret = __key_event_handler();
if ((ret) &&
(type == SHORT_PRESS)) {
return;
}
/* 事件类型判断 */
switch (type) {
case SHORT_PRESS:
TUYA_APP_LOG_INFO("reset key short pressed.");
__reset_key_short_press_handler();
break;
case LONG_PRESS_FOR_TIME1:
TUYA_APP_LOG_INFO("reset key long pressed for time1.");
__reset_key_long_press_handler();
break;
case LONG_PRESS_FOR_TIME2:
break;
default:
break;
}
}
/**
* @brief 霍尔传感器回调函数
* @param[in] none
* @return none
*/
STATIC VOID_T __hall_sw_cb()
{
__hall_switch_handler();
}
本 demo 的功能设定中涉及较多的定时需求,具体情况整理如下:
No. | 定时器名称 | 执行条件 | 定时时间 | 超时处理内容 |
---|---|---|---|---|
1 | 屏幕熄灭确认 | 亮屏状态、【转动中】、无按键事件 | 6 秒 | 熄灭屏幕 |
2 | 屏幕点亮确认 | 熄屏状态、【转动中】 | 5 分钟 | 点亮屏幕 |
3 | 停止转动确认 | 【转动中】、无霍尔事件 | 3 秒 | 设置设备状态为【使用中】 |
4 | 停止使用确认 | 【使用中】、无按键事件 | 30 秒 | 设置设备状态为【未使用】 |
5 | 数据显示切换 | 普通模式界面 | 2 秒 | 切换显示数据 |
6 | 计时数据更新 | 【转动中】 | 1 分钟 | 更新计时数据并上报 |
7 | 本地时间更新 | - | 1 秒 | 更新本地时间、检查日期变更 |
8 | 数据上报更新 | - | 5 秒 | 上报圈数&卡路里数据 |
9 | 配网等待结束 | 设备未被用户绑定且正在等待配网 | 1 分钟 | 禁止配网 |
代码实现:
/* 第一步:定义定时器数据结构 (tuya_hula_hoop_evt_timer.c) */
typedef struct {
UINT_T disp_sleep;
UINT_T disp_wakeup;
UINT_T stop_rotating;
UINT_T stop_using;
UINT_T switch_disp_data;
UINT_T upd_time_data;
UINT_T upd_local_time;
UINT_T repo_dp_data;
UINT_T wait_bind;
} HULA_HOOP_TIMER_T;
/* 第二步:定义定时时间相关宏 (tuya_hula_hoop_evt_timer.c) */
#define TIMER_PERIOD_MS (100) /* 100ms */
#define DISP_SLEEP_CONFIRM_TIME_MS (6*1000) /* 6s */
#define DISP_WAKEUP_CONFIRM_TIME_MS (5*60*1000) /* 5min */
#define STOP_ROTATING_CONFIRM_TIME_MS (3000) /* 3s */
#define STOP_USING_CONFIRM_TIME_MS (30*1000) /* 30s */
#define DISP_DATA_SWITCH_INTV_MS (2000) /* 2s */
#define TIME_DATA_UPDATE_INTV_MS (1*60*1000) /* 1min */
#define LOCAL_TIME_UPDATE_INTV_MS (1000) /* 1s */
#define DP_DATA_REPO_INTV_MS (5*1000) /* 5s */
#define WAIT_BIND_END_TIME_MS (1*60*1000) /* 1min */
/* 第三步:定义定时器变量并编写初始化函数 (tuya_hula_hoop_evt_timer.c) */
STATIC HULA_HOOP_TIMER_T sg_timer;
/**
* @brief 定时处理模块初始化,创建 1 个 100ms 软件定时器
* @param[in] none
* @return none
*/
VOID_T hula_hoop_timer_init(VOID_T)
{
memset(&sg_timer, 0, SIZEOF(HULA_HOOP_TIMER_T));
tuya_software_timer_create(TIMER_PERIOD_MS*1000, __timer_timeout_handler);
}
/* 第四步:编写各个定时处理函数 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 屏幕休眠确认定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __disp_sleep_timer(IN CONST UINT_T time_inc)
{
if ((FALSE == hula_hoop_disp_is_wakeup()) ||
(hula_hoop_get_device_status() != STAT_ROTATING) ||
(F_WAIT_BINDING == SET)) {
sg_timer.disp_sleep = 0;
return;
}
sg_timer.disp_sleep += time_inc;
if (sg_timer.disp_sleep >= DISP_SLEEP_CONFIRM_TIME_MS) {
if (FALSE == hula_hoop_disp_is_flash()) {
hula_hoop_disp_sleep();
}
}
}
/**
* @brief 屏幕唤醒确认定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __disp_wakeup_timer(IN CONST UINT_T time_inc)
{
if ((TRUE == hula_hoop_disp_is_wakeup()) ||
(hula_hoop_get_device_status() != STAT_ROTATING)) {
sg_timer.disp_wakeup = 0;
return;
}
sg_timer.disp_wakeup += time_inc;
if (sg_timer.disp_wakeup >= DISP_WAKEUP_CONFIRM_TIME_MS) {
sg_timer.disp_wakeup = 0;
hula_hoop_disp_wakeup();
}
}
/**
* @brief 停止转动确认定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __stop_rotating_timer(IN CONST UINT_T time_inc)
{
if (hula_hoop_get_device_status() != STAT_ROTATING) {
sg_timer.stop_rotating = 0;
return;
}
sg_timer.stop_rotating += time_inc;
if (sg_timer.stop_rotating >= STOP_ROTATING_CONFIRM_TIME_MS) {
sg_timer.stop_rotating = 0;
hula_hoop_set_device_status(STAT_USING);
}
}
/**
* @brief 停止使用确认定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __stop_using_timer(IN CONST UINT_T time_inc)
{
if ((hula_hoop_get_device_status() != STAT_USING) ||
(F_WAIT_BINDING == SET)) {
sg_timer.stop_using = 0;
return;
}
sg_timer.stop_using += time_inc;
if (sg_timer.stop_using >= STOP_USING_CONFIRM_TIME_MS) {
sg_timer.stop_using = 0;
hula_hoop_set_device_status(STAT_UNUSED);
}
}
/**
* @brief 显示数据切换定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __switch_disp_data_timer(IN CONST UINT_T time_inc)
{
if ((hula_hoop_get_disp_mode() != DISP_NORMAL_MODE) ||
(FALSE == hula_hoop_disp_is_wakeup())) {
sg_timer.switch_disp_data = 0;
return;
}
sg_timer.switch_disp_data += time_inc;
if (sg_timer.switch_disp_data >= DISP_DATA_SWITCH_INTV_MS) {
sg_timer.switch_disp_data -= DISP_DATA_SWITCH_INTV_MS;
hula_hoop_switch_disp_data();
}
}
/**
* @brief 计时数据更新定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __upd_time_data_timer(IN CONST UINT_T time_inc)
{
if (hula_hoop_get_device_status() != STAT_ROTATING) {
return;
}
sg_timer.upd_time_data += time_inc;
if (sg_timer.upd_time_data >= TIME_DATA_UPDATE_INTV_MS) {
sg_timer.upd_time_data -= TIME_DATA_UPDATE_INTV_MS;
if (hula_hoop_update_sport_data_time()) {
hula_hoop_disp_target_finish();
}
hula_hoop_report_sport_data1();
}
}
/**
* @brief 本地时间更新定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __upd_local_time_timer(IN CONST UINT_T time_inc)
{
sg_timer.upd_local_time += time_inc;
if (sg_timer.upd_local_time >= LOCAL_TIME_UPDATE_INTV_MS) {
sg_timer.upd_local_time -= LOCAL_TIME_UPDATE_INTV_MS;
tuya_local_time_update();
hula_hoop_check_date_change();
}
}
/**
* @brief DP 数据上报定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __repo_dp_data_timer(IN CONST UINT_T time_inc)
{
sg_timer.repo_dp_data += time_inc;
if (sg_timer.repo_dp_data >= DP_DATA_REPO_INTV_MS) {
sg_timer.repo_dp_data -= DP_DATA_REPO_INTV_MS;
hula_hoop_report_sport_data2();
}
}
/**
* @brief 配网等待停止定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __wait_bind_timer(IN CONST UINT_T time_inc)
{
if ((F_BLE_BOUND == SET) || (F_WAIT_BINDING == CLR)) {
sg_timer.wait_bind = 0;
return;
}
sg_timer.wait_bind += time_inc;
if (sg_timer.wait_bind >= WAIT_BIND_END_TIME_MS) {
sg_timer.wait_bind = 0;
hula_hoop_prohibit_binding();
}
}
/* 第五步:编写 100ms 超时处理函数 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 定时处理
* @param[in] none
* @return none
*/
STATIC INT_T __timer_timeout_handler(VOID_T)
{
__disp_sleep_timer(TIMER_PERIOD_MS);
__disp_wakeup_timer(TIMER_PERIOD_MS);
__stop_rotating_timer(TIMER_PERIOD_MS);
__stop_using_timer(TIMER_PERIOD_MS);
__switch_disp_data_timer(TIMER_PERIOD_MS);
__upd_time_data_timer(TIMER_PERIOD_MS);
__upd_local_time_timer(TIMER_PERIOD_MS);
__repo_dp_data_timer(TIMER_PERIOD_MS);
__wait_bind_timer(TIMER_PERIOD_MS);
return 0;
}
/* 第六步:编写定时器计时复位函数,供外部复位 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 按键事件发生时相关计时复位
* @param[in] none
* @return none
*/
VOID_T hula_hoop_reset_timer_for_key_event(VOID_T)
{
sg_timer.disp_sleep = 0;
sg_timer.stop_using = 0;
}
/**
* @brief 霍尔事件发生时相关计时复位
* @param[in] none
* @return none
*/
VOID_T hula_hoop_reset_timer_for_hall_event(VOID_T)
{
sg_timer.stop_rotating = 0;
}
/**
* @brief 复位“计时更新”定时器
* @param[in] none
* @return none
*/
VOID_T hula_hoop_reset_upd_time_data_timer(VOID_T)
{
sg_timer.upd_time_data = 0;
}
/* 第七步:在头文件中定义相关接口 (tuya_hula_hoop_evt_timer.c) */
VOID_T hula_hoop_timer_init(VOID_T);
VOID_T hula_hoop_reset_timer_for_key_event(VOID_T);
VOID_T hula_hoop_reset_timer_for_hall_event(VOID_T);
VOID_T hula_hoop_reset_upd_time_data_timer(VOID_T);
配网等待处理
要实现和云端进行数据交互,首先要使设备配网。配网功能已经通过 Bluetooth LE SDK 实现,下面来介绍本 demo 的配网等待处理和配网提醒机制:
No. | 设备状态 | 执行动作 | 配网指示灯 |
---|---|---|---|
1 | 上电时,检测到已被用户绑定 | 不等待配网(保持蓝牙广播) | 不闪烁 |
2 | 上电时,检测到未被用户绑定 | 开始等待配网(保持蓝牙广播) | 开始闪烁 |
3 | 长按模式键 5 秒时 | 开始等待配网,打开蓝牙广播 | 开始闪烁 |
4 | 开始等待配网 1 分钟后,检测到仍未被用户绑定 | 停止等待配网,关闭蓝牙广播 | 停止闪烁 |
5 | 开始等待配网后 1 分钟内,检测到已被用户连接 | 停止等待配网(保持蓝牙广播) | 停止闪烁 |
6 | 检测到被用户解绑 | 不等待配网,关闭蓝牙广播 | 不闪烁 |
第 1 步:实现初期上电时的配网状态检测处理,即上表中的<1>和<2>:
/* 第一步:定义配网相关处理标志 (tuya_hula_hoop_ble_proc.c/.h) */
FLAG_BIT g_ble_proc_flag;
#define F_BLE_BOUND g_ble_proc_flag.bit0
#define F_WAIT_BINDING g_ble_proc_flag.bit1
/* 第二步:编写初期配网状态检测处理函数 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 蓝牙处理模块初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_ble_proc_init(VOID_T)
{
/* 使用 TUYA Bluetooth LE SDK 提供的 API 获取当前蓝牙连接状态 */
tuya_ble_connect_status_t ble_conn_sta;
ble_conn_sta = tuya_ble_connect_status_get();
TUYA_APP_LOG_DEBUG("ble connect status: %d", ble_conn_sta);
/* 判断与标记 */
if ((ble_conn_sta == BONDING_UNCONN) ||
(ble_conn_sta == BONDING_CONN) ||
(ble_conn_sta == BONDING_UNAUTH_CONN)) {
F_BLE_BOUND = SET; /* 标记为[已绑定] */
F_WAIT_BINDING = CLR; /* 标记为[禁止等待配网] */
} else {
F_BLE_BOUND = CLR; /* 标记为[未绑定] */
F_WAIT_BINDING = SET; /* 标记为[允许等待配网] */
hula_hoop_disp_set_led_func(LED_FUNC_BIND); /* 设置 LED 功能为[配网指示] */
}
}
/* 第三步:在头文件中定义初始化接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_proc_init(VOID_T);
/* 第四步:在设备初始化函数 tuya_hula_hoop_init()中调用 (tuya_hula_hoop.c) */
第 2 步:实现长按模式键打开配网功能。除上电时允许设备配网外,用户还可以通过长按模式键 5 秒来打开配网功能,即上表中的<3>:
/* 第一步:编写允许配网处理函数 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 允许用户绑定设备
* @param[in] none
* @return none
*/
VOID_T hula_hoop_allow_binding(VOID_T)
{
if (F_BLE_BOUND) { /* [已绑定]? */
tuya_ble_device_factory_reset(); /* 设备端解绑 */
}
F_WAIT_BINDING = SET; /* 标记为[允许等待配网] */
bls_ll_setAdvEnable(1); /* 打开蓝牙广播 */
hula_hoop_disp_set_led_func(LED_FUNC_BIND); /* 修改 LED 功能为[配网指示] */
}
/* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_allow_binding(VOID_T);
/* 第三步:在模式键处理函数中调用 (tuya_hula_hoop_evt_user.c) */
/**
* @brief 模式键长按 5 秒处理函数
* @param[in] none
* @return none
*/
STATIC VOID_T __mode_key_longer_press_handler(VOID_T)
{
hula_hoop_allow_binding(); /* 允许用户绑定设备 */
}
第 3 步:实现关闭配网功能。配网功能开启 1 分钟后,如果仍未被绑定则需要关闭配网功能,即上表中的<4>:
/* 第一步:编写禁止配网处理函数 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 禁止用户绑定设备
* @param[in] none
* @return none
*/
VOID_T hula_hoop_prohibit_binding(VOID_T)
{
F_WAIT_BINDING = CLR; /* 标记为[禁止等待配网] */
bls_ll_setAdvEnable(0); /* 关闭蓝牙广播 */
hula_hoop_disp_set_led_func(LED_FUNC_DATA); /* 修改 LED 功能为[数据指示] */
}
/* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_prohibit_binding(VOID_T);
/* 第三步:在定时模块中定义的 1 分钟配网等待定时处理函数中调用 (tuya_hula_hoop_tiemr.c) */
/**
* @brief 等待配网定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __ble_wait_bind_timer(IN CONST UINT_T time_inc)
{
/* [已绑定]或[禁止等待配网]时不处理 */
if ((F_BLE_BOUND == SET) || (F_WAIT_BINDING == CLR)) {
g_timer.ble_wait_bind = 0;
return;
}
/* 1 分钟配网等待定时 */
g_timer.ble_wait_bind += time_inc;
if (g_timer.ble_wait_bind >= BLE_WAIT_BIND_END_MS) {
g_timer.ble_wait_bind = 0;
hula_hoop_prohibit_binding(); /* 禁止用户绑定设备 */
}
}
第 4 步:实现蓝牙连接状态改变时和被解绑时的处理,即上表中的<5>和<6>:
/* 第一步:编写蓝牙连接状态改变时和设备被解绑时的处理函数 */
/**
* @brief 蓝牙连接状态更新处理
* @param[in] status: 蓝牙连接状态
* @return none
*/
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) { /* 蓝牙已连接? */
__report_all_dp_data(); /* 上报所有 DP 数据 */
tuya_ble_time_req(BLE_TIME_TYPE_NORMAL); /* 发送获取云端时间请求 */
if (F_WAIT_BINDING == SET) { /* 之前未绑定? */
F_BLE_BOUND = SET; /* 标记为[已绑定] */
F_WAIT_BINDING = CLR; /* 标记为[禁止等待配网] */
hula_hoop_disp_set_led_func(LED_FUNC_DATA); /* 修改 LED 功能为[数据指示] */
}
if (hula_hoop_get_device_status() == STAT_USING) { /* [使用中]? */
hula_hoop_reset_timer_for_key_event(); /* 复位相关计时 */
}
}
}
/**
* @brief 设备被解绑处理
* @param[in] none
* @return none
*/
VOID_T hula_hoop_ble_unbound_handler(VOID_T)
{
F_BLE_BOUND = CLR; /* 标记为[未绑定] */
bls_ll_setAdvEnable(0); /* 停止蓝牙广播 */
}
/* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status);
VOID_T hula_hoop_ble_unbound_handler(VOID_T);
/* 第三步:在 Bluetooth LE SDK 消息处理 callback 函数中调用 (tuya_ble_app_demo.c) */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_CONNECTE_STATUS: /* 蓝牙连接状态改变时 */
hula_hoop_ble_conn_stat_handler(event->connect_status); /* 蓝牙连接状态更新处理 */
TUYA_APP_LOG_INFO("received tuya ble conncet status update event, current connect status = %d", event->connect_status);
break;
...
case TUYA_BLE_CB_EVT_UNBOUND: /* App 端解绑设备时 */
hula_hoop_ble_unbound_handler(); /* 设备被解绑处理 */
TUYA_APP_LOG_INFO("received unbound req");
break;
case TUYA_BLE_CB_EVT_ANOMALY_UNBOUND: /* 异常解绑时 */
hula_hoop_ble_unbound_handler(); /* 设备被解绑处理 */
TUYA_APP_LOG_INFO("received anomaly unbound req");
break;
...
}
为了能让用户在 App 上实时查看设备状态,设备需在与 App 连接时,将可上报的功能点数据上报至云端。根据呼啦圈的功能特点,本 demo 的上报机制设定如下:
No. | 上报节点 | 上报功能点 | 备注 |
---|---|---|---|
1 | 设备连接到 App 时 | 所有功能点 | 初始化 App 显示 |
2 | 设备工作模式改变时 | 工作模式和实时类功能点 | 工作模式变更时会清除实时数据 |
3 | 设备状态为转动中时,每隔 1 分钟 | 计时类功能点 | 即计时数据更新时上报 |
4 | 设备状态为转动中时,每隔 5 秒 | 圈数&卡路里类功能点 | 数据变化较频繁,隔一段时间上报 |
代码实现:
/* 第一步:对功能定义时设定的 ID 进行宏定义 */
#define DP_ID_MODE 101
#define DP_ID_TIME_REALTIME 102
#define DP_ID_COUNT_REALTIME 103
#define DP_ID_CALORIES_REALTIME 104
#define DP_ID_TIME_TOTAL_TODAY 105
#define DP_ID_COUNT_TOTAL_TODAY 106
#define DP_ID_CALORIES_TOTAL_TODAY 107
#define DP_ID_TIME_TOTAL_30DAYS 108
#define DP_ID_COUNT_TOTAL_30DAYS 109
#define DP_ID_CALORIES_TOTAL_30DAYS 110
#define DP_ID_TIME_TARGET_TODAY 111
#define DP_ID_TIME_TARGET_MONTH 112
#define DP_ID_TIME_REMAIN_TODAY 113
#define DP_ID_TIME_REMAIN_MONTH 114
/* 第二步:对 DP 数据存放时各项内容的偏移量进行宏定义 */
#define DP_DATA_INDEX_OFFSET_ID 0
#define DP_DATA_INDEX_OFFSET_TYPE 1
#define DP_DATA_INDEX_OFFSET_LEN 2
#define DP_DATA_INDEX_OFFSET_DATA 3
/* 第三步:定义用于存放 DP 数据的数组 */
UCHAR_T dp_data_array[255+3];
/* 第四步:编写 DP 数据上报相关函数 */
/**
* @brief 上报 1 个 DP 数据
* @param[in] dp_id: DP ID
* @param[in] dp_type: DP 类型
* @param[in] dp_len: DP 数据长度
* @param[in] dp_data: DP 数据存放地址
* @return none
*/
STATIC VOID_T __report_one_dp_data(IN CONST UCHAR_T dp_id, IN CONST UCHAR_T dp_type, IN CONST UCHAR_T dp_len, IN CONST UCHAR_T *dp_data)
{
UCHAR_T i;
sg_repo_array[DP_DATA_INDEX_OFFSET_ID] = dp_id;
sg_repo_array[DP_DATA_INDEX_OFFSET_TYPE] = dp_type;
sg_repo_array[DP_DATA_INDEX_OFFSET_LEN] = dp_len;
for (i = 0; i < dp_len; i++) {
sg_repo_array[DP_DATA_INDEX_OFFSET_DATA + i] = *(dp_data + (dp_len-i-1));
}
tuya_ble_dp_data_report(sg_repo_array, dp_len + 3);
}
/**
* @brief 添加 1 个 DP 数据
* @param[in] dp_id: DP ID
* @param[in] dp_type: DP 类型
* @param[in] dp_len: DP 数据长度
* @param[in] dp_data: DP 数据存放地址
* @param[in] addr: DP 数据添加到上报数组中的位置
* @return 该 DP 数据总长度
*/
STATIC UCHAR_T __add_one_dp_data(IN CONST UCHAR_T dp_id, IN CONST UCHAR_T dp_type, IN CONST UCHAR_T dp_len, IN CONST UCHAR_T *dp_data, IN UCHAR_T *addr)
{
UCHAR_T i;
*(addr + DP_DATA_INDEX_OFFSET_ID) = dp_id;
*(addr + DP_DATA_INDEX_OFFSET_TYPE) = dp_type;
*(addr + DP_DATA_INDEX_OFFSET_LEN) = dp_len;
for (i = 0; i < dp_len; i++) {
*(addr + DP_DATA_INDEX_OFFSET_DATA + i) = *(dp_data + (dp_len-i-1));
}
return (dp_len + 3);
}
/**
* @brief 上报实时类 DP 数据
* @param[in] none
* @return none
*/
VOID_T __report_sport_data_realtime(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/**
* @brief 上报工作模式变更时的 DP 数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_report_mode(VOID_T)
{
__report_one_dp_data(DP_ID_MODE, DT_ENUM, SIZEOF(g_hula_hoop.mode), &g_hula_hoop.mode);
__report_sport_data_realtime();
}
/**
* @brief 上报计时类 DP 数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_report_sport_data1(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_total_today), (UCHAR_T *)&g_sport_data.time_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.time_total_30days), (UCHAR_T *)&g_sport_data.time_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/**
* @brief 上报圈数&卡路里类 DP 数据
* @param[in] none
* @return none
*/
VOID_T hula_hoop_report_sport_data2(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.count_total_today), (UCHAR_T *)&g_sport_data.count_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.calories_total_today), (UCHAR_T *)&g_sport_data.calories_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.count_total_30days), (UCHAR_T *)&g_sport_data.count_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.calories_total_30days), (UCHAR_T *)&g_sport_data.calories_total_30days, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/**
* @brief 上报所有 DP 数据
* @param[in] none
* @return none
*/
STATIC VOID_T __report_all_dp_data(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_MODE, DT_ENUM, SIZEOF(g_hula_hoop.mode), &g_hula_hoop.mode, sg_repo_array);
total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_total_today), (UCHAR_T *)&g_sport_data.time_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.count_total_today), (UCHAR_T *)&g_sport_data.count_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.calories_total_today), (UCHAR_T *)&g_sport_data.calories_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.time_total_30days), (UCHAR_T *)&g_sport_data.time_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.count_total_30days), (UCHAR_T *)&g_sport_data.count_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.calories_total_30days), (UCHAR_T *)&g_sport_data.calories_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/* 第五步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_report_mode(VOID_T);
VOID_T hula_hoop_report_sport_data1(VOID_T);
VOID_T hula_hoop_report_sport_data2(VOID_T);
/* 第六步:分别在各上报节点处调用 */
/* <1> 设备连接到 App 时 (tuya_hula_hoop_ble_proc.c) */
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) { /* 蓝牙已连接? */
__report_all_dp_data(); /* 上报所有 DP 数据 */
...
}
...
}
/* <2> 设备工作模式改变时 (tuya_hula_hoop_evt_user.c) */
STATIC VOID_T __mode_key_long_press_handler(VOID_T)
{
if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
return;
}
hula_hoop_switch_to_select_mode();
hula_hoop_report_mode(); /* 上报工作模式和实时数据 */
}
/* <3> 计时数据更新时 (tuya_hula_hoop_evt_timer.c) */
STATIC VOID_T __upd_time_data_timer(IN CONST UINT_T time_inc)
{
if (hula_hoop_get_device_status() != STAT_ROTATING) {
g_timer.upd_time_data = 0;
return;
}
g_timer.upd_time_data += time_inc;
if (g_timer.upd_time_data >= TIME_DATA_UPDATE_INTV_MS) {
g_timer.upd_time_data -= TIME_DATA_UPDATE_INTV_MS;
if (hula_hoop_update_sport_data_time()) {
hula_hoop_disp_target_finish();
}
hula_hoop_report_sport_data1(); /* 上报计时数据 */
}
}
/* <4> 上报定时处理中 (tuya_hula_hoop_evt_timer.c) */
STATIC VOID_T __repo_dp_data_timer(IN CONST UINT_T time_inc)
{
g_timer.repo_dp_data += time_inc;
if (g_timer.repo_dp_data >= DP_DATA_REPO_INTV_MS) {
g_timer.repo_dp_data -= DP_DATA_REPO_INTV_MS;
hula_hoop_report_sport_data2(); /* 上报圈数和卡路里数据 */
}
}
接收数据处理
当用户操作 App 设置可下发类型的功能点时,云端会将该功能点的 ID、类型、属性值、属性值长度下发给设备,此时设备只要在检测到云端下发数据事件后进行相应处理,即可实现云端任务。本 demo 可下发的功能点和接收到该功能点数据时需处理的内容如下:
No. | 下发功能点 | 接收到数据后需处理的内容 | 备注 |
---|---|---|---|
1 | 工作模式 | 更新设备工作模式,上报实时数据 | 工作模式变更时会清除实时数据 |
2 | 当日目标时长 | 更新设备当日目标时长,上报当日目标剩余时长 | 当日目标变更时会改变当日剩余 |
3 | 当月目标时长 | 更新设备当月目标时长,上报当月目标剩余时长 | 当月目标变更时会改变当月剩余 |
代码实现:
/* 第一步:编写 DP 数据接收处理函数 */
/**
* @brief 呼啦圈 DP 数据接收处理
* @param[in] dp_data: DP 数据存放数组
* @return none
*/
VOID_T hula_hoop_ble_dp_write_handler(IN UCHAR_T *dp_data)
{
if (hula_hoop_get_device_status() == STAT_USING) {
hula_hoop_reset_timer_for_key_event();
}
switch (dp_data[0]) {
case DP_ID_MODE:
{
hula_hoop_set_work_mode(dp_data[3]);
__report_sport_data_realtime();
TUYA_APP_LOG_INFO("App set the mode : %d.", dp_data[3]);
}
break;
case DP_ID_TIME_TARGET_TODAY:
{
hula_hoop_set_time_target_today(dp_data[6]);
__report_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today);
TUYA_APP_LOG_INFO("App set today's target : %dmin.", dp_data[6]);
}
break;
case DP_ID_TIME_TARGET_MONTH:
{
USHORT_T tar_tm = (((USHORT_T)dp_data[5]) << 8) | ((USHORT_T)dp_data[6]);
hula_hoop_set_time_target_month(tar_tm);
__report_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month);
TUYA_APP_LOG_INFO("App set this month's target : %dmin.", tar_tm);
}
break;
default:
break;
}
}
/* 第二步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_dp_write_handler(IN UCHAR_T *dp_data);
/* 第三步:在 Bluetooth LE SDK 消息处理 callback 函数中调用 (tuya_ble_app_demo.c) */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_DP_WRITE: /* 在接收到 DP 数据时执行 */
dp_data_len = event->dp_write_data.data_len;
memset(dp_data_array, 0, sizeof(dp_data_array));
memcpy(dp_data_array, event->dp_write_data.p_data, dp_data_len);
hula_hoop_ble_dp_write_handler(dp_data_array); /* 呼啦圈 DP 数据处理 */
TUYA_APP_LOG_HEXDUMP_DEBUG("received dp write data :", dp_data_array, dp_data_len);
sn = 0;
tuya_ble_dp_data_report(dp_data_array, dp_data_len);
break;
...
}
云端时间获取
Bluetooth LE SDK 提供获取云端时间的功能,当设备连接 App 时,可以通过获取云端时间来校准本地时间。涂鸦 Bluetooth LE SDK 提供两种格式的时间获取,分别为 13 字节 ms 级字符串格式和年月日时分秒星期格式时间,这里我们使用后者。
代码实现:
/* 第一步:在设备连接时发起获取云端时间请求 (tuya_hula_hoop_ble_proc.c) */
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) {
...
tuya_ble_time_req(BLE_TIME_TYPE_NORMAL);
...
}
...
}
/* 第二步:编写云端时间下发后的处理函数 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 云端时间处理
* @param[in] time_normal: 普通格式的云端时间
* @return none
*/
VOID_T hula_hoop_ble_time_normal_handler(IN CONST tuya_ble_time_noraml_data_t time_normal)
{
/* 类型转换并覆盖本地时间 */
LOCAL_TIME_T time_now = {
.year = time_normal.nYear + 2000,/* noraml 格式的年份为 2 位数形式,这里做简单处理 */
.month = time_normal.nMonth,
.day = time_normal.nDay,
.hour = time_normal.nHour,
.minute = time_normal.nMin,
.second = time_normal.nSec
};
tuya_set_local_time(time_now, (time_normal.time_zone / 100));
/* 日期变更检查处理 */
hula_hoop_check_date_change();
/* 本地时间更新日志打印 */
TUYA_APP_LOG_INFO("Local time has been updated to %04d.%02d.%02d %02d:%02d:%02d.",
g_local_time.year, g_local_time.month, g_local_time.day,
g_local_time.hour, g_local_time.minute, g_local_time.second);
}
/* 第三步:在头文件中定义接口 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_time_normal_handler(IN CONST tuya_ble_time_noraml_data_t time_normal);
/* 第四步:在 Bluetooth LE SDK 消息处理 callback 函数中调用 (tuya_ble_app_demo.c) */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_TIME_NORMAL: /* 云端下发时间时 */
hula_hoop_ble_time_normal_handler(event->time_normal_data); /* 云端时间处理 */
break;
...
}
需要注意的是,云端下发的时间为零时区时间,同时云端会下发表示实际时区的数据,如 800 表示 800/100=东八区,如果要取得本地时间,需对时区问题进行处理:(tuya_local_time.c/.h)
/**
* @brief 设置本地时间,需处理时区问题
* @param[in] time: 设置时间
* @param[in] time_zone: 时区
* @return none
*/
VOID_T tuya_set_local_time(IN CONST LOCAL_TIME_T time, IN CONST INT_T time_zone)
{
/* 更新本地时间 */
INT_T tmp_hour = time.hour;
tmp_hour += time_zone; /* 经过时区计算后的小时数 */
g_local_time = time;
/* 大于等于 24 时需做加一天处理 */
if (tmp_hour >= 24) {
g_local_time.hour = 0;
__local_time_update_per_day();
return;
}
/* 小于等于 0 时需做减一天处理 */
if (tmp_hour <= 0) {
g_local_time.hour = tmp_hour + 24;
if (g_local_time.day > 1) {
g_local_time.day--;
return;
}
if (g_local_time.month > 1) {
g_local_time.month--;
sg_day_tbl[1] = __local_time_leap_year_judgment(g_local_time.year);
g_local_time.day = sg_day_tbl[g_local_time.month-1];
} else {
g_local_time.year--;
g_local_time.month = 12;
g_local_time.day = sg_day_tbl[g_local_time.month-1];
}
return;
}
g_local_time.hour = tmp_hour;
}
/* 头文件中接口定义 */
VOID_T tuya_set_local_time(IN CONST LOCAL_TIME_T time, IN CONST INT_T time_zone);
由于 I/O 资源有限,在完成功能调试后,需关闭日志打印功能,可通过将tuya_ble_app_init()
函数中的elog_set_output_enabled(true);
语句修改为elog_set_output_enabled(false);
来实现禁止调试信息输出,同时还可以将custom_tuya_ble_config.h
中TUYA_APP_LOG_ENABLE
的值修改为 0 来关闭应用日志,以减少代码空间。
由于呼啦圈使用电池供电,为避免电量消耗过快,需对设备进行低功耗处理。
BTU 模组原厂芯片的电源管理模块提供了 3 种低功耗工作模式:
工作模式 | 特点 | 唤醒方式 |
---|---|---|
暂停模式 | MCU 暂停,PM 模块处于活动状态时,所有 SRAM 仍可访问,RF 收发器、音频和 USB 等模块断电,唤醒后从暂停处继续执行。 | PAD/32k Timer/RESET Pin |
SRAM 保留的深度睡眠模式 | PM 模块处于活动状态时,除两个 8KB 和一个 16KB 保留 SRAM 外,大多数模拟和所有数字模块都掉电。 | PAD/32k Timer/RESET Pin |
深度睡眠模式 | 只有 PM 模块处于活动状态时,包括保持 SRAM 在内的大多数模拟和所有数字模块都处于断电状态。 | PAD/32k Timer/RESET Pin |
对于 BTU 模组来说,这里的 SRAM 保留指的是 0x840000 - 0x847FFF 的存储空间,目前协议栈和应用都是默认优先使用这块空间,从编译生成的**.lst 文件**
中也可以看到相关数据都存储在该范围内。也就是说,如果使用 SRAM 保留的深度睡眠模式,可以保证数据在唤醒后能继续使用,而不需要频繁地进行闪存读写操作。
为了不影响呼啦圈的本地定时功能,这次选择了 SRAM 保留的深度睡眠模式作为低功耗模式,基本处理思路如下:
<1> 在设备[未使用]时,设置唤醒方式为引脚唤醒或定时器唤醒,然后进入 SRAM 保留的深度睡眠模式;
<2> 当设备被引脚唤醒时:更新本地时间后,设备状态切换到[使用中];
<3> 当设备被定时器唤醒时:更新本地时间后再次睡眠。
代码实现:
第 1 步:睡眠前处理
在进入睡眠状态之前,如果设备已连接 App,需在睡眠前主动断开连接,否则唤醒后可能会无法连接 APP。我们可以在设备切换为未使用状态后,通过调用tuya_ble_gap_disconnect()
接口实现断连;需要注意的是,调用该 API 后,断连状态不会立即生效,所以要在收到蓝牙状态变更事件回调后再执行睡眠动作。如果设备未连接 APP,则不需要进行上述处理,直接设置设备状态为睡眠状态即可。
/* tuya_hula_hoop_evt_timer.c */
/**
* @brief 停用确认定时
* @param[in] time_inc: 时间增量
* @return none
*/
STATIC VOID_T __stop_using_timer(IN CONST UINT_T time_inc)
{
if ((hula_hoop_get_device_status() != STAT_USING) ||
(F_WAIT_BINDING == SET)) {
g_timer.stop_using = 0;
return;
}
g_timer.stop_using += time_inc;
if (g_timer.stop_using >= STOP_USING_CONFIRM_TIME_MS) {
g_timer.stop_using = 0;
hula_hoop_set_device_status(STAT_UNUSED); /* 设置设备状态为[未使用] */
if (FALSE == hula_hoop_is_need_disconnect()) { /* 判断是否需要进行主动断连操作 */
hula_hoop_set_device_status(STAT_SLEEP); /* 设置设备状态为[睡眠] */
}
}
}
/* tuya_hula_hoop_ble_proc.c */
/**
* @brief 是否需要进行主动断连操作
* @param[in] none
* @return TRUE - 需要, FALSE - 不需要
*/
BOOL_T hula_hoop_is_need_disconnect(VOID_T)
{
bls_ll_setAdvEnable(0);
/* 已连接时处理 */
if (tuya_ble_connect_status_get() == BONDING_CONN) {
tuya_ble_gap_disconnect(); /* 断开蓝牙连接 */
return TRUE;
}
return FALSE;
}
/**
* @brief 蓝牙连接状态变更处理
* @param[in] status: 蓝牙连接状态
* @return none
*/
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (hula_hoop_get_device_status() == STAT_UNUSED) { /* 设备状态为[未使用]? */
if (status == BONDING_UNCONN){ /* 蓝牙状态为未连接 */
hula_hoop_set_device_status(STAT_SLEEP); /* 设置设备状态为[睡眠] */
}
return;
}
...
}
进入 SRAM 保留的深度睡眠模式
接下来编写让设备进入SRAM 保留的深度睡眠模式的相关代码。我们把复位键引脚作为唤醒引脚使用,SDK 的配置文件中已经定义了相关的宏,如有需要也可以同时设置多个引脚作为唤醒引脚。
/* app_config.h */
#define BLE_MODULE_PM_ENABLE 1
#define PM_DEEPSLEEP_RETENTION_ENABLE 1 /* SRAM 保留睡眠模式相关代码块,默认是打开的 */
#define GPIO_WAKEUP_MODULE GPIO_PB1/* 修改唤醒引脚,下面 4 行宏名对应修改 */
#define PB1_FUNC AS_GPIO
#define PB1_INPUT_ENABLE 1
#define PB1_OUTPUT_ENABLE 0
#define PB1_DATA_OUT 0
#define GPIO_WAKEUP_MODULE_HIGH gpio_setup_up_down_resistor(GPIO_WAKEUP_MODULE, PM_PIN_PULLUP_10K);
#define GPIO_WAKEUP_MODULE_LOW gpio_setup_up_down_resistor(GPIO_WAKEUP_MODULE, PM_PIN_PULLDOWN_100K);
/* App.c */
void main_loop(void)
{
...
if(1) { /* ← 将低功耗处理代码段打开 */
if((tuya_get_ota_status() == TUYA_OTA_STATUS_NONE)&&(ty_factory_flag==0)&&(ble_tx_is_busy()!=1)) {
app_power_management();
}
}
}
void app_power_management ()
{
...
if (app_module_busy()) {
return;
}
/* 这两句用于实现连接时的低功耗,需要循环调用,由于会影响液晶屏显示,这次不使用 */
//bls_pm_setSuspendMask (SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);
//bls_pm_setWakeupSource(PM_WAKEUP_PAD);
/* 进入睡眠模式的处理函数 */
hula_hoop_set_device_sleep();
}
/* tuya_hula_hoop_svc_basic.c */
VOID_T hula_hoop_set_device_sleep(VOID_T)
{
/* 由于该函数在循环中调用,要设置限制条件 */
if (hula_hoop_get_device_status() != STAT_SLEEP) {
return;
}
/* 保存当前时间 */
sg_sys_time = clock_time();
/* 设置唤醒引脚、唤醒定时时间并进入 SRAM 保留的低功耗模式 */
GPIO_WAKEUP_MODULE_HIGH;
cpu_set_gpio_wakeup(GPIO_WAKEUP_MODULE, Level_Low, 1);
cpu_sleep_wakeup(DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_PAD|PM_WAKEUP_TIMER, clock_time()+SLEEP_TIME_SEC*CLOCK_16M_SYS_TIMER_CLK_1S);
}
/* SDK 默认使用的唤醒引脚是高电平有效的,所以有关唤醒引脚极性的地方要稍做修改 */
int app_module_busy()
{
mcu_uart_working = gpio_read(GPIO_WAKEUP_MODULE);
module_uart_working = UART_TX_BUSY || UART_RX_BUSY;
//module_task_busy = mcu_uart_working || module_uart_working; /* 修改前 */
module_task_busy = (!mcu_uart_working) || module_uart_working; /* 修改后 */
return module_task_busy;
}
void user_init_normal(void)
{
...
/* 修改前 */
//cpu_set_gpio_wakeup (GPIO_WAKEUP_MODULE, Level_High, 1);
//GPIO_WAKEUP_MODULE_LOW;
/* 修改后 */
cpu_set_gpio_wakeup (GPIO_WAKEUP_MODULE, Level_Low, 1);
GPIO_WAKEUP_MODULE_HIGH;
...
}
唤醒后处理
设备唤醒后会从main()
函数开始执行,然后通过判断是否从 SRAM 保留的睡眠模式唤醒来进入对应的初始化函数user_init_deepRetn()
,我们在这里加入呼啦圈的处理函数:
/* main.c */
_attribute_ram_code_ int main(void)
{
...
int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();/* 是否从 SRAM 保留的睡眠模式唤醒 */
...
gpio_init( !deepRetWakeUp );/* 如果是从 SRAM 保留的睡眠模式唤醒,gpio 相关模拟寄存器无需初始化 */
...
if ( deepRetWakeUp ) {
user_init_deepRetn(); /* 从 SRAM 保留的睡眠模式唤醒时的初始化函数 */
} else {
user_init_normal(); /* 上电/复位后的初始化函数 */
}
...
}
/* App.c */
_attribute_ram_code_ void user_init_deepRetn(void)
{
tuya_ble_app_init_deepRetn();
...
}
/* tuya_ble_app_demo.c */
void tuya_ble_app_init_deepRetn(void)
{
tuya_software_timer_init();
tuya_hula_hoop_init_deepRetn();
}
/* tuya_hula_hoop.c */
VOID_T tuya_hula_hoop_init_deepRetn(VOID_T)
{
hula_hoop_device_wakeup_handler(); /* 唤醒后根据唤醒方式进行处理 */
if (hula_hoop_get_device_status() >= STAT_UNUSED) { /* 如果设备状态没有变化则不进行下面的初始化工作 */
return;
}
hula_hoop_key_hall_init_deepRetn(); /* 按键引脚、霍尔传感器引脚的初始化 */
hula_hoop_disp_proc_init_deepRetn(); /* 指示灯引脚、段码液晶屏引脚/定时器的初始化 */
hula_hoop_ble_proc_init_deepRetn(); /* 睡眠前如果是已绑定则重新打开蓝牙广播 */
hula_hoop_timer_reset(); /* 定时器复位 */
}
其中,hula_hoop_device_wakeup_handler()
是唤醒后的处理函数,用于判断设备是被何种方式唤醒并进行相应的处理。如果是被引脚唤醒,则说明呼啦圈被用户使用,所以需要切换到工作状态,切换前还要计算睡眠经过的时间,用来更新本地时间;如果是定时唤醒,则根据设定的睡眠时间更新本地时间即可,此时设备会继续保持STAT_SLEEP
状态,进入主循环后设备会再次睡眠。
/* tuya_hula_hoop_svc_basic.c */
/**
* @brief 更新本地时间,并检查日期是否变更
* @param[in] time_diff_sec: 时间差,单位“秒”
* @return none
*/
VOID_T __update_local_time(IN UCHAR_T time_diff_sec)
{
while(time_diff_sec--) {
tuya_local_time_update();
}
hula_hoop_check_date_change();
}
/**
* @brief 切换设备到工作状态
* @param[in] none
* @return none
*/
VOID_T __set_device_work(VOID_T)
{
/* 本地时间处理,计算时间差,四舍五入 */
UINT_T time_diff = clock_time() - sg_sys_time;
if ((time_diff % CLOCK_16M_SYS_TIMER_CLK_1S) >= (CLOCK_16M_SYS_TIMER_CLK_1S / 2)) {
time_diff = time_diff / CLOCK_16M_SYS_TIMER_CLK_1S + 1;
} else {
time_diff = time_diff / CLOCK_16M_SYS_TIMER_CLK_1S;
}
__update_local_time(time_diff);
TUYA_APP_LOG_INFO("Time difference: %ds.", time_diff);
TUYA_APP_LOG_INFO("Local time has been updated to %04d.%02d.%02d %02d:%02d:%02d.\n",
g_local_time.year, g_local_time.month, g_local_time.day,
g_local_time.hour, g_local_time.minute, g_local_time.second);
/* 设置状态为使用中,同时设置默认工作模式 */
__set_device_status(STAT_USING);
__set_work_mode(MODE_NORMAL);
}
/**
* @brief 唤醒后处理函数
* @param[in] none
* @return none
*/
VOID_T hula_hoop_device_wakeup_handler(VOID_T)
{
if (pm_get_wakeup_src() == (WAKEUP_STATUS_PAD | WAKEUP_STATUS_CORE)) {
/* 引脚唤醒时处理 */
__set_device_work();
} else {
/* 定时唤醒时处理 */
__update_local_time(SLEEP_TIME_SEC);
}
}
经上述处理后,呼啦圈正常运行时电流平均值约为 5.6uA,睡眠时约为 0.157uA。