簡介:本文將從智能呼啦圈軟體整體方案,外設驅動以及功能實作幾個維度來帶大家一起了解如何實作呼啦圈智能計算、切換運動模式以及運動歷史曲線,
開發環境搭建
智能呼啦圈方案是基于涂鴉 BLE SDK 和 Telink 芯片平臺 TLSR825x 進行開發,BLE模組開發環境的搭建方案我們在前期的Demo有介紹過,大家可以參考BLE模組開發環境搭操作步驟
軟體方案介紹
完整Demo可在 tuya-iotos-embeded-demo-ble-smart-hula-hoop 中獲取,
一.總體設計
1.功能需求
智能呼啦圈demo的功能需求定義如下:
| 功能 | 需求描述 |
|---|---|
| 模式選擇 | 支持運動模式選擇(普通模式[默認]、目標模式), 【本地】模式鍵:短按,選擇模式;長按≥2s,確認模式, 段碼液晶屏:普通模式顯示“010”,目標模式顯示“020”, 【APP】下發“模式”資料, |
| 目標設定 | 支持運動目標時長設定, - 當日運動目標設定最長時間不超過45min; - 當月運動目標設定最長時間不超過1350min (45min*30天), 【APP】下發“目標”資料, |
| 目標完成提醒 | 支持目標模式下運動目標完成提醒, 【本地】段碼液晶屏顯示“—”并閃爍3次, |
| 智能計數 | 支持運動時間(min)、圈數(圈)、卡路里(kcal)累計, 【本地】轉動情況下計數,實時資料>999時重新從0開始計數, |
| 運動資料顯示 | 支持運動資料顯示, 【本地】段碼液晶屏:高位為0時不顯示; 1)普通模式:時長、圈數、卡路里資料輪流顯示,每間隔5分鐘顯示一輪; 2)目標模式:時長顯示6s,每間隔5分鐘顯示一次, 【APP】接收本地資料并顯示, |
| 運動資料記錄 | 支持運動資料記錄, 【本地】記錄30天內累計運動資料(時長、圈數、卡路里), |
| 資料指示 | 支持屏顯資料指示: 【本地】時長、圈數、卡路里指示燈根據當前屏顯資料依次點亮, |
| 螢屏狀態更新 | 支持螢屏狀態更新, 【本地】按鍵、開始轉動時螢屏點亮;停止轉動30s后螢屏熄滅, |
| 設備配網 | 支持設備配網, 【本地】上電時:允許設備配網,1分鐘后若未被用戶系結,禁止配網; 模式鍵:長按≥5秒,允許設備配網,1分鐘后若未被用戶系結,禁止配網; 配網提醒:配網(時長)指示燈快閃; |
| 資料上報 | 支持本地運動資料上報, 【本地】上報模式資料、運動資料至APP顯示, |
| 設備復位 | 支持設備復位, 【本地】復位鍵:長按≥2s,清空所有運動資料(恢復原始狀態); 段碼液晶屏顯示“000”并閃爍3次, |
2.模塊劃分
對以上功能需求進行分析梳理后,確定智能呼啦圈demo程式可劃分為以下七大模塊:
| No. | 模塊 | 處理內容 |
|---|---|---|
| 1 | 外設驅動組件 | 按鍵、霍爾傳感器、指示燈、段碼液晶屏的驅動程式 |
| 2 | 設備基礎服務 | 設備狀態遷移處理、模式切換、本地定時等 |
| 3 | 顯示處理服務 | 段碼液晶屏顯示內容更新和狀態控制、LED狀態控制 |
| 4 | 資料處理服務 | 運動資料(計時、圈數、卡路里)的更新和存盤 |
| 5 | 用戶事件處理 | 按鍵事件檢測和處理、霍爾傳感器事件檢測和處理 |
| 6 | 定時事件處理 | 各定時事件的判斷和處理 |
| 7 | 聯網相關處理 | 配網相關處理、資料上報與接收處理、云端時間獲取 |
3. 代碼結構
根據模塊層級關系,設定代碼檔案結構如下,后續將分別介紹外設驅動模塊和應用層六大功能模塊:
├── src /* 源檔案目錄 */
| ├── common
| | └── tuya_local_time.c /* 本地定時 */
| ├── sdk
| | └── tuya_uart_common_handler.c /* UART通用對接實作代碼 */
| ├── driver
| | ├── tuya_hall_sw.c /* 霍爾開關驅動 */
| | ├── tuya_key.c /* 按鍵驅動 */
| | ├── tuya_led.c /* 指示燈驅動 */
| | └── tuya_seg_lcd.c /* 段碼液晶屏驅動 */
| ├── platform
| | ├── tuya_gpio.c /* GPIO驅動 */
| | └── tuya_timer.c /* Timer驅動 */
| ├── tuya_ble_app_demo.c /* 應用層入口檔案 */
| ├── tuya_hula_hoop_ble_proc.c /* 呼啦圈聯網相關處理 */
| ├── tuya_hula_hoop_evt_user.c /* 呼啦圈用戶事件處理 */
| ├── tuya_hula_hoop_evt_timer.c /* 呼啦圈定時事件處理 */
| ├── tuya_hula_hoop_svc_basic.c /* 呼啦圈基礎服務 */
| ├── tuya_hula_hoop_svc_data.c /* 呼啦圈資料服務 */
| ├── tuya_hula_hoop_svc_disp.c /* 呼啦圈顯示服務 *
| └── tuya_hula_hoop.c /* 呼啦圈demo入口 */
|
└── include /* 頭檔案目錄 */
├── common
| ├── tuya_common.h /* 通用型別和宏定義 */
| └── tuya_local_time.h /* 本地定時 */
├── sdk
| ├── custom_app_uart_common_handler.h /* UART通用對接實作代碼 */
| ├── custom_app_product_test.h /* 自定義產測專案相關實作 */
| └── custom_tuya_ble_config.h /* 應用組態檔 */
├── driver
| ├── tuya_hall_sw.h /* 霍爾開關驅動 */
| ├── tuya_key.h /* 按鍵驅動 */
| ├── tuya_led.h /* 指示燈驅動 */
| └── tuya_seg_lcd.h /* 段碼液晶屏驅動 */
├── platform
| ├── tuya_gpio.h /* GPIO驅動 */
| └── tuya_timer.h /* Timer驅動 */
├── tuya_ble_app_demo.h /* 應用層入口檔案 */
├── tuya_hula_hoop_ble_proc.h /* 呼啦圈聯網相關處理 */
├── tuya_hula_hoop_evt_user.h /* 呼啦圈用戶事件處理 */
├── tuya_hula_hoop_evt_timer.h /* 呼啦圈定時事件處理 */
├── tuya_hula_hoop_svc_basic.h /* 呼啦圈基礎服務 */
├── tuya_hula_hoop_svc_data.h /* 呼啦圈資料服務 */
├── tuya_hula_hoop_svc_disp.h /* 呼啦圈顯示服務 */
└── tuya_hula_hoop.h /* 呼啦圈demo入口 */
4.軟體框圖

二.外設驅動
為方便后續程式擴展,可以先將各外設驅動部分的代碼分別撰寫成組件,
本demo所使用外設的基本情況如下:
| 外設 | 數量&規格 | 驅動方式 |
|---|---|---|
| 按鍵 | 2個 | 開關量檢測 |
| 霍爾傳感器 | 1個;開關型 | 開關量檢測 |
| 發光二極管 | 3個;紅色 | 電平或PWM驅動 |
| 段碼液晶屏 | 1個;3位數字“8”,4個COM口,6個SEG口 | COM口和SEG口兩端施加一定壓差的交流電,壓差大于閾值時對應筆段點亮 |
1.按鍵&霍爾驅動
- 按鍵
由于按鍵是嵌入式開發中的常用器件,這里不做過多介紹,結合呼啦圈demo的功能需求,組件功能設定如下:
<1> 可在初期注冊按鍵資訊,包括引腳、有效電平、長按時間(2種可選)、按鍵觸發時回應的回呼函式;
<2> 可檢測按鍵觸發事件,包括短按、長按(2種可選),并在按鍵確認觸發時,執行用戶設定的回呼函式;
<3> 可實作多個按鍵同時檢測;
首先我們先來實作按鍵的初始化:
/* 第一步:定義供用戶注冊按鍵資訊使用的結構體型別 (tuya_key.h) */
/* 按鍵事件型別 */
typedef BYTE_T KEY_PRESS_TYPE_E;
#define SHORT_PRESS 0x00 /* 短按 */
#define LONG_PRESS_FOR_TIME1 0x01 /* 長按超過時間1 */
#define LONG_PRESS_FOR_TIME2 0x02 /* 長按超過時間2 */
/* 按鍵回呼函式型別 */
typedef VOID_T (*KEY_CALLBACK)(KEY_PRESS_TYPE_E type);
/* 按鍵注冊資訊型別 */
typedef struct {
TY_GPIO_PORT_E port; /* 埠 */
BOOL_T active_low; /* 有效電平 (1-低電平有效,0-高電平有效) */
UINT_T long_press_time1; /* 長按時間1 (ms) */
UINT_T long_press_time2; /* 長按時間2 (ms) */
KEY_CALLBACK key_cb; /* 觸發按鍵回呼函式 */
} KEY_DEF_T;
/* 第二步:定義用來存盤按鍵狀態的結構體型別 (tuya_key.c) */
typedef struct {
BOOL_T cur_stat; /* 今回狀態 */
BOOL_T prv_stat; /* 前回狀態 */
UINT_T cur_time; /* 今回計時 */
UINT_T prv_time; /* 前回計時 */
} KEY_STATUS_T;
/* 第三步:定義用于按鍵資訊管理的結構體型別和該型別的指標 (tuya_key.c) */
typedef struct key_manage_s {
struct key_manage_s *next; /* 下一個按鍵資訊存盤的地址,實作多按鍵檢測 */
KEY_DEF_T *key_def_s;
KEY_STATUS_T key_status_s;
} KEY_MANAGE_T;
STATIC KEY_MANAGE_T *sg_key_mag_list = NULL;
/* 第四步:定義按鍵錯誤資訊代碼 (tuya_key.h) */
typedef BYTE_T KEY_RET;
#define KEY_OK 0x00
#define KEY_ERR_MALLOC_FAILED 0x01
#define KEY_ERR_CB_UNDEFINED 0x02
/* 第五步:撰寫按鍵注冊函式,包括對按鍵的初始化作業 (tuya_key.c) */
STATIC VOID_T __key_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
{
tuya_gpio_init(port, TRUE, active_low);
}
KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def)
{
/* 檢查是否定義了回呼函式,未定義則回傳錯誤資訊 */
if (key_def->key_cb == NULL) {
return KEY_ERR_CB_UNDEFINED;
}
/* 為key_mag分配空間并初始化,分配失敗則回傳錯誤資訊 */
KEY_MANAGE_T *key_mag = (KEY_MANAGE_T *)tuya_ble_malloc(SIZEOF(KEY_MANAGE_T));
if (NULL == key_mag) {
return KEY_ERR_MALLOC_FAILED;
}
/* 記錄用戶設定的按鍵資訊,并存放至按鍵管理串列 */
key_mag->key_def_s = key_def;
if (sg_key_mag_list) {
key_mag->next = sg_key_mag_list;
}
sg_key_mag_list = key_mag;
/* 根據用戶設定的有效電平對引腳進行初始化 */
__key_gpio_init(key_def->port, key_def->active_low);
/* 回傳成功資訊 */
return KEY_OK;
}
/* 第六步:在頭檔案中定義按鍵注冊介面 (tuya_key.h) */
KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def);
完成了按鍵初始化作業之后,我們來實作按鍵事件的檢測和處理,基本思路是每10ms檢測一次每個按鍵的狀態,并判斷是否滿足了按鍵觸發事件的條件,標記事件的型別,然后執行對應的回呼函式:
/* 第一步:定義相關引數值 (tuya_key.c) */
#define KEY_SCAN_CYCLE_MS 10 /* 掃描周期 */
#define KEY_PRESS_SHORT_TIME 50 /* 短按確認時間 */
/* 第二步:撰寫用于更新單個按鍵狀態的相關函式 (tuya_key.c) */
/* 獲取按鍵實時狀態,1-按壓,0-釋放 */
STATIC BOOL_T __get_key_stat(IN CONST TY_GPIO_PORT_E port, IN CONST UCHAR_T active_low)
{
BOOL_T key_stat;
if (active_low) {
key_stat = tuya_gpio_read(port) == 0 ? TRUE : FALSE;
} else {
key_stat = tuya_gpio_read(port) == 0 ? FALSE : TRUE;
}
return key_stat;
}
/* 更新按鍵狀態 */
STATIC VOID_T __update_key_status(INOUT KEY_MANAGE_T *key_mag)
{
BOOL_T key_stat;
/* 保存前回狀態 */
key_mag->key_status_s.prv_stat = key_mag->key_status_s.cur_stat;
key_mag->key_status_s.prv_time = key_mag->key_status_s.cur_time;
/* 獲取實時狀態 */
key_stat = __get_key_stat(key_mag->key_def_s->pin, key_mag->key_def_s->active_low);
/* 更新今回狀態 */
if (key_stat != key_mag->key_status_s.cur_stat) {
key_mag->key_status_s.cur_stat = key_stat;
key_mag->key_status_s.cur_time = 0;
} else {
key_mag->key_status_s.cur_time += KEY_SCAN_CYCLE_MS;
}
}
/* 第三步:撰寫用于判斷單個按鍵事件的相關函式 (tuya_key.c) */
/* 判斷按鍵保持按壓狀態的時間是否達到了over_time */
STATIC BOOL_T __is_key_press_over_time(IN CONST KEY_STATUS_T key_status_s, IN CONST UINT_T over_time)
{
if (key_status_s.cur_stat == TRUE) {
if ((key_status_s.cur_time >= over_time) &&
(key_status_s.prv_time < over_time)) {
return TRUE;
}
}
return FALSE;
}
/* 判斷按鍵從開始被按壓到被釋放經過的時間是否達到了over_time并且少于less_time */
STATIC BOOL_T __is_key_release_to_release_over_time_and_less_time(IN CONST KEY_STATUS_T key_status_s, IN CONST UINT_T over_time, IN CONST UINT_T less_time)
{
if ((key_status_s.prv_stat == TRUE) &&
(key_status_s.cur_stat == FALSE)) {
if ((key_status_s.prv_time >= over_time) &&
(key_status_s.prv_time < less_time)) {
return TRUE;
}
}
return FALSE;
}
/* 判斷與處理按鍵事件 */
STATIC VOID_T __detect_and_handle_key_event(INOUT KEY_MANAGE_T *key_mag)
{
KEY_PRESS_TYPE_E type;
BOOL_T time_exchange;
UINT_T long_time1, long_time2;
/* 比較用戶設定的長按時間1和長按時間2的大小,并標記是否交換 */
if (key_mag->key_def_s->long_press_time2 >= key_mag->key_def_s->long_press_time1) {
long_time1 = key_mag->key_def_s->long_press_time1;
long_time2 = key_mag->key_def_s->long_press_time2;
time_exchange = FALSE;
} else {
long_time1 = key_mag->key_def_s->long_press_time2;
long_time2 = key_mag->key_def_s->long_press_time1;
time_exchange = TRUE;
}
/* 判斷按鍵狀態,標記事件型別并跳轉到KEY_EVENT (根據長按時間設定情況使用對應的判斷方式) */
if ((long_time2 != 0) && (long_time1 != 0)) {
if (__is_key_press_over_time(key_mag->key_status_s, long_time2)) {
type = LONG_PRESS_FOR_TIME2;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, long_time1, long_time2)) {
type = LONG_PRESS_FOR_TIME1;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time1)){
type = SHORT_PRESS;
goto KEY_EVENT;
}
} else if ((long_time2 != 0) && (long_time1 == 0)) {
if (__is_key_press_over_time(key_mag->key_status_s, long_time2)) {
type = LONG_PRESS_FOR_TIME2;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time2)){
type = SHORT_PRESS;
goto KEY_EVENT;
}
} else if ((long_time2 == 0) && (long_time1 != 0)) {
if (__is_key_press_over_time(key_mag->key_status_s, long_time1)) {
type = LONG_PRESS_FOR_TIME1;
goto KEY_EVENT;
}
if (__is_key_release_to_release_over_time_and_less_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME, long_time1)){
type = SHORT_PRESS;
goto KEY_EVENT;
}
} else {
if (__is_key_press_over_time(key_mag->key_status_s, KEY_PRESS_SHORT_TIME)) {
type = SHORT_PRESS;
goto KEY_EVENT;
}
}
return;
/* 處理按鍵事件 */
KEY_EVENT:
/* 如果在判斷前進行了時間引數的交換,則將標記的事件型別進行交換 */
if (time_exchange) {
if (type == LONG_PRESS_FOR_TIME2) {
type = LONG_PRESS_FOR_TIME1;
} else if (type == LONG_PRESS_FOR_TIME1) {
type = LONG_PRESS_FOR_TIME2;
} else {
;
}
}
/* 執行用戶設定的回呼函式 */
key_mag->key_def_s->key_cb(type);
}
/* 第四步:撰寫10ms處理函式 (tuya_key.c) */
STATIC INT_T __key_timeout_handler(VOID_T)
{
/* 獲取按鍵資訊管理串列,無按鍵注冊則回傳 */
KEY_MANAGE_T *key_mag_tmp = sg_key_mag_list;
if (NULL == key_mag_tmp) {
return 0;
}
/* 回圈處理每個按鍵 */
while (key_mag_tmp) {
__update_key_status(key_mag_tmp); /* 更新按鍵狀態 */
__detect_and_handle_key_event(key_mag_tmp); /* 判斷并處理按鍵事件 */
key_mag_tmp = key_mag_tmp->next; /* 加載下一個按鍵資訊 */
}
return 0;
}
/* 第五步:在按鍵注冊函式中創建定時器 (tuya_key.c) */
KEY_RET tuya_reg_key(IN KEY_DEF_T *key_def)
{
...
if (sg_key_mag_list) {
key_mag->next = sg_key_mag_list;
} else { /* 注冊第一個按鍵時創建10ms定時器,注冊回呼函式 */
tuya_software_timer_create(KEY_SCAN_CYCLE_MS*1000, __key_timeout_handler);
}
...
}
- 霍爾傳感器
這次使用的霍爾傳感器是開關型的,一般情況下也可視為按鍵處理,考慮到呼啦圈快速轉動時霍爾傳感器與磁鐵的接觸時間較短,并且上述按鍵驅動組件使用了10ms軟體定時器進行處理,易被外部程式執行時間影響,可能會導致呼啦圈計數漏檢的情況,所以這里我們采取外部中斷的方式處理:
/* 【tuya_hall_sw.h】 */
/* 錯誤資訊代碼 */
typedef BYTE_T HSW_RET;
#define HSW_OK 0x00
#define HSW_ERR_MALLOC_FAILED 0x01
#define HSW_ERR_CB_UNDEFINED 0x02
/* 霍爾開關資料結構 */
typedef VOID_T (*HALL_SW_CALLBACK)();
typedef struct {
TY_GPIO_PORT_E port; /* 埠 */
BOOL_T active_low; /* 有效電平 */
HALL_SW_CALLBACK hall_sw_cb; /* 觸發時回呼函式 */
UINT_T invalid_intv; /* 兩次觸發間隔如果小于該時間則無效 */
} HALL_SW_DEF_T;
/* 【tuya_hall_sw.c】 */
/* 霍爾開關資訊管理 */
typedef struct hall_sw_manage_s {
struct hall_sw_manage_s *next;
HALL_SW_DEF_T *def;
UINT_T wk_tm;
} HALL_SW_MANAGE_T;
STATIC HALL_SW_MANAGE_T *sg_hsw_mag_list = NULL;
/* 霍爾開關引腳初始化 */
STATIC VOID_T __hall_sw_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
{
tuya_gpio_init(port, TRUE, active_low);
if (active_low) {
tuya_gpio_irq_init(port, TY_GPIO_IRQ_FALLING, __hall_sw_irq_handler);
} else {
tuya_gpio_irq_init(port, TY_GPIO_IRQ_RISING, __hall_sw_irq_handler);
}
}
/* 霍爾開關注冊 */
HSW_RET tuya_reg_hall_sw(IN HALL_SW_DEF_T *hsw_def)
{
/* 檢查是否定義了回呼函式,未定義則回傳錯誤資訊 */
if (hsw_def->hall_sw_cb == NULL) {
return HSW_ERR_CB_UNDEFINED;
}
/* 為hall_sw_mag分配空間并初始化,分配失敗則回傳錯誤資訊 */
HALL_SW_MANAGE_T *hall_sw_mag = (HALL_SW_MANAGE_T *)tuya_ble_malloc(SIZEOF(HALL_SW_MANAGE_T));
if (NULL == hall_sw_mag) {
return HSW_ERR_MALLOC_FAILED;
}
hall_sw_mag->def = hsw_def;
/* 記錄用戶設定的霍爾開關資訊,并存放至霍爾開關管理串列 */
if (sg_hsw_mag_list) {
hall_sw_mag->next = sg_hsw_mag_list;
}
sg_hsw_mag_list = hall_sw_mag;
/* 引腳初始化 */
__hall_sw_gpio_init(hsw_def->port, hsw_def->active_low);
return HSW_OK;
}
/* 霍爾開關觸發時處理 */
STATIC VOID_T __hall_sw_trigger_handler(IN HALL_SW_MANAGE_T *hsw_mag)
{
/* 兩次觸發間隔檢查 */
if (!tuya_is_clock_time_exceed(hsw_mag->wk_tm, hsw_mag->def->invalid_intv)) {
return;
}
hsw_mag->wk_tm = tuya_get_clock_time();
/* 執行用戶設定的回呼函式 */
hsw_mag->def->hall_sw_cb();
}
/* 霍爾開關外部中斷回呼 */
STATIC VOID_T __hall_sw_irq_handler(TY_GPIO_PORT_E port)
{
HALL_SW_MANAGE_T *hsw_mag_tmp = sg_hsw_mag_list;
while (hsw_mag_tmp) {
if (hsw_mag_tmp->def->port == port) {
__hall_sw_trigger_handler(hsw_mag_tmp);
break;
}
hsw_mag_tmp = hsw_mag_tmp->next;
}
}
2.發光二極管驅動
發光二極管同樣是常用器件,結合呼啦圈demo的功能需求,組件功能設定如下:
<1> 可在初期注冊LED資訊,包括引腳、有效電平;
<2> 可控制LED點亮或熄滅或閃爍;
<3> 可設定LED閃爍模式,包括閃爍方式(指定時長/指定次數/永遠閃爍)、閃爍開始時和閃爍結束后的狀態、點亮階段的時間、熄滅階段的時間、閃爍結束時的回呼函式;
<4> 可實作多個LED同時控制;
這次沒有設定亮度調節、呼吸燈控制等功能,因此只需要使用電平驅動方式,
首先還是先來實作初始化部分:
/* 第一步:定義LED句柄,用戶通過該句柄來控制單個LED (tuya_led.h) */
typedef VOID_T *LED_HANDLE;
/* 第二步:定義初期需注冊的LED驅動相關資訊 (tuya_led.c) */
typedef struct {
TY_GPIO_PORT_E pin; /* LED引腳 */
BOOL_T active_low; /* LED有效電平 (1-低電平點亮,0-高電平點亮) */
} LED_DRV_T;
/* 第三步:定義LED資訊管理串列 (tuya_led.c) */
typedef struct led_manage_s {
struct led_manage_s *next;
LED_DRV_T drv_s;
} LED_MANAGE_T;
STATIC LED_MANAGE_T *sg_led_mag_list = NULL;/* 下一個LED資訊存盤的地址,實作多LED控制 */
/* 第四步:撰寫LED引腳初始化函式和LED注冊函式 (tuya_led.c) */
STATIC VOID_T __led_gpio_init(IN CONST TY_GPIO_PORT_E port, IN CONST BOOL_T active_low)
{
tuya_gpio_init(port, FALSE, active_low);
}
LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle)
{
/* 檢查句柄,未指定則回傳錯誤引數 */
if (NULL == handle) {
return LED_ERR_INVALID_PARM;
}
/* 為led_mag分配空間并初始化,分配失敗則回傳錯誤資訊 */
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)tuya_ble_malloc(SIZEOF(LED_MANAGE_T));
if (NULL == led_mag) {
return LED_ERR_MALLOC_FAILED;
}
/* 記錄用戶設定的LED資訊,并存放至LED管理串列,同時將存盤地址 */
led_mag->drv_s.pin = pin;
led_mag->drv_s.active_low = active_low;
*handle = (LED_HANDLE)led_mag;
if (sg_led_mag_list) {
led_mag->next = sg_led_mag_list;
}
sg_led_mag_list = led_mag;
/* 根據用戶設定的有效電平對引腳進行初始化 (注冊時默認不點亮) */
__led_gpio_init(pin, active_low);
/* 回傳成功資訊 */
return LED_OK;
}
/* 第五步:在頭檔案中定義LED注冊介面 (tuya_led.h) */
LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle);
接下來實作簡單的亮滅控制,根據設定的有效電平來控制LED引腳的輸出狀態即可:
/* 第一步:撰寫LED亮滅控制函式 (tuya_led.c) */
STATIC VOID_T __set_led_light(IN CONST LED_DRV_T drv_s, IN CONST BOOL_T on_off)
{
if (drv_s.active_low) {
tuya_gpio_write(drv_s.pin, !on_off);
} else {
tuya_gpio_write(drv_s.pin, on_off);
}
}
LED_RET tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off)
{
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
__set_led_light(led_mag->drv_s, on_off);
return LED_OK;
}
/* 第二步:在頭檔案中定義LED亮滅控制介面 (tuya_led.h) */
LED_RET tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off);
最后是LED閃爍控制,閃爍功能將在用戶配置閃爍引數后開啟,每100ms處理一次:
/* 第一步:定義用于管理LED閃爍資訊的結構體型別 (tuya_led.c) */
typedef struct {
LED_FLASH_MODE_E mode; /* 閃爍方式 */
LED_FLASH_TYPE_E type; /* 閃爍型別 */
USHORT_T on_time; /* 點亮階段時間 */
USHORT_T off_time; /* 熄滅階段時間 */
UINT_T total; /* 指定的時間或指定的次數 */
LED_CALLBACK end_cb; /* 閃爍結束時執行的回呼函式 */
UINT_T work_timer; /* 閃爍作業用計時變數 */
} LED_FLASH_T;
/* 第二步:在頭檔案對閃爍方式和閃爍型別的可選項進行定義,用戶配置時可直接使用這些宏 (tuya_led.h) */
/* 閃爍方式 */
typedef BYTE_T LED_FLASH_MODE_E;
#define LFM_SPEC_TIME 0x00 /* 閃爍指定時間 */
#define LFM_SPEC_COUNT 0x01 /* 閃爍指定次數 */
#define LFM_FOREVER 0x02 /* 永遠閃爍 */
/* 閃爍型別 */
typedef BYTE_T LED_FLASH_TYPE_E;
#define LFT_STA_ON_END_ON 0x00 /* 開始時:亮;結束后:亮 */
#define LFT_STA_ON_END_OFF 0x01 /* 開始時:亮;結束后:滅 */
#define LFT_STA_OFF_END_ON 0x02 /* 開始時:滅;結束后:亮 */
#define LFT_STA_OFF_END_OFF 0x03 /* 開始時:滅;結束后:滅 */
/* 第三步:將LED閃爍資訊加入LED管理,同時定義閃爍結束處理相關的變數 (tuya_led.c) */
typedef struct led_manage_s {
struct led_manage_s *next;
LED_DRV_T drv_s;
LED_FLASH_T *flash;
BOOL_T stop_flash_req; /* 停止閃爍請求 */
BOOL_T stop_flash_light; /* 停止閃爍后的亮滅狀態 */
} LED_MANAGE_T;
/* 第四步:撰寫以下函式用于決議用戶配置,方便閃爍處理時使用 (tuya_led.c) */
/* 獲取閃爍開始時的亮滅狀態,1-亮,0-滅 */
STATIC BOOL_T __get_led_flash_sta_light(IN CONST LED_FLASH_TYPE_E type)
{
BOOL_T ret = TRUE;
switch (type) {
case LFT_STA_ON_END_ON:
case LFT_STA_ON_END_OFF:
ret = TRUE;
break;
case LFT_STA_OFF_END_ON:
case LFT_STA_OFF_END_OFF:
ret = FALSE;
break;
default:
break;
}
return ret;
}
/* 獲取閃爍結束后的亮滅狀態,1-亮,0-滅 */
STATIC BOOL_T __get_led_flash_end_light(IN CONST LED_FLASH_TYPE_E type)
{
BOOL_T ret = TRUE;
switch (type) {
case LFT_STA_ON_END_ON:
case LFT_STA_OFF_END_ON:
ret = TRUE;
break;
case LFT_STA_ON_END_OFF:
case LFT_STA_OFF_END_OFF:
ret = FALSE;
break;
default:
break;
}
return ret;
}
/* 第五步:撰寫LED閃爍配置函式 (tuya_led.c) */
LED_RET tuya_set_led_flash(IN CONST LED_HANDLE handle, IN CONST LED_FLASH_MODE_E mode, IN CONST LED_FLASH_TYPE_E type, IN CONST USHORT_T on_time, IN CONST USHORT_T off_time, IN CONST UINT_T total, IN CONST LED_CALLBACK flash_end_cb)
{
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
led_mag->stop_flash_req = FALSE;
if (led_mag->flash == NULL) {
LED_FLASH_T *led_flash = (LED_FLASH_T *)tuya_ble_malloc(SIZEOF(LED_FLASH_T));
if (NULL == led_flash) {
return LED_ERR_MALLOC_FAILED;
}
led_mag->flash = led_flash;
}
led_mag->flash->mode = mode;
led_mag->flash->type = type;
led_mag->flash->on_time = on_time;
led_mag->flash->off_time = off_time;
led_mag->flash->total = total;
led_mag->flash->work_timer = 0;
led_mag->flash->end_cb = flash_end_cb;
__set_led_light(led_mag->drv_s, __get_led_flash_sta_light(type));
return LED_OK;
}
/* 第六步:撰寫LED閃爍處理函式 (tuya_led.c) */
STATIC VOID_T __led_flash_proc(INOUT LED_MANAGE_T *led_mag)
{
BOOL_T one_cycle_flag = FALSE;
UINT_T sum_time;
BOOL_T start_light;
USHORT_T start_time;
/* 決議閃爍配置 */
sum_time = led_mag->flash->on_time + led_mag->flash->off_time;
start_light = __get_led_flash_sta_light(led_mag->flash->type);
start_time = (start_light) ? led_mag->flash->on_time : led_mag->flash->off_time;
/* 閃爍周期性處理,實作按照指定時間點亮和熄滅 */
led_mag->flash->work_timer += LED_TIMER_VAL_MS;
if (led_mag->flash->work_timer >= sum_time) {
led_mag->flash->work_timer -= sum_time;
__set_led_light(led_mag->drv_s, start_light);
one_cycle_flag = TRUE;
} else if (led_mag->flash->work_timer >= start_time) {
__set_led_light(led_mag->drv_s, !start_light);
} else {
;
}
/* 閃爍倒計時/數處理,閃爍方式為“永遠閃爍”時不處理 */
if (led_mag->flash->mode == LFM_FOREVER) {
return;
}
if (led_mag->flash->mode == LFM_SPEC_TIME) {
if (led_mag->flash->total > LED_TIMER_VAL_MS) {
led_mag->flash->total -= LED_TIMER_VAL_MS;
} else {
led_mag->flash->total = 0;
}
} else if (led_mag->flash->mode == LFM_SPEC_COUNT) {
if (one_cycle_flag) {
if (led_mag->flash->total > 0) {
led_mag->flash->total--;
}
}
} else {
;
}
/* 閃爍結束處理 */
if (led_mag->flash->total == 0) {
/* 如果設定了閃爍回呼函式,則執行該函式 */
if (led_mag->flash->end_cb != NULL) {
led_mag->flash->end_cb();
}
/* 發起停止閃爍請求,并設定停止后的亮滅狀態 */
led_mag->stop_flash_req = TRUE;
led_mag->stop_flash_light = __get_led_flash_end_light(led_mag->flash->type);
}
}
/* 第七步:撰寫LED超時處理函式 (tuya_led.c) */
STATIC INT_T __led_timeout_handler(VOID_T)
{
/* 獲取LED資訊管理串列,無LED注冊則回傳 */
LED_MANAGE_T *led_mag_tmp = sg_led_mag_list;
if (NULL == led_mag_tmp) {
return;
}
/* 回圈處理每個LED */
while (led_mag_tmp) {
/* 停止閃爍請求處理 */
if (led_mag_tmp->stop_flash_req) {
__set_led_light(led_mag_tmp->drv_s, led_mag_tmp->stop_flash_light);
tuya_ble_free((UCHAR_T *)led_mag_tmp->flash);
led_mag_tmp->flash = NULL;
led_mag_tmp->stop_flash_req = FALSE;
}
/* 如果閃爍功能未開啟則不處理 */
if (NULL != led_mag_tmp->flash) {
__led_flash_proc(led_mag_tmp);
}
/* 加載下一個LED資訊 */
led_mag_tmp = led_mag_tmp->next;
}
return 0;
}
/* 第五步:在LED注冊函式中創建定時器 (tuya_led.c) */
#define LED_TIMER_VAL_MS 100
LED_RET tuya_create_led_handle(IN CONST GPIO_PinTypeDef pin, IN CONST UCHAR_T active_low, OUT LED_HANDLE *handle)
{
...
if (sg_led_mag_list) {
led_mag->next = sg_led_mag_list;
} else { /* 注冊第一個LED時創建100ms定時器,注冊回呼函式 */
tuya_software_timer_create(LED_TIMER_VAL_MS*1000, __led_timeout_handler);
}
...
}
/* 第八步:修改LED亮滅控制函式 (可能會存在閃爍未結束時呼叫了該函式的情況) (tuya_led.c) */
VOID_T tuya_set_led_light(IN CONST LED_HANDLE handle, IN CONST BOOL_T on_off)
{
LED_MANAGE_T *led_mag = (LED_MANAGE_T *)handle;
/* 閃爍未結束時,發起停止閃爍請求,并暫存用戶設定的亮滅狀態 */
if (led_mag->flash != NULL) {
led_mag->stop_flash_req = TRUE;
led_mag->stop_flash_light = on_off;
} else {
__set_led_light(led_mag->drv_s, on_off);
}
}
3.段碼液晶屏驅動
段碼液晶屏主要有兩種引腳,公共極COM和段電極SEG,當COM口和SEG口的電壓差大于液晶屏飽和電壓時就能夠點亮對應的筆段,需要注意的是,壓差必須要是交替變化的,舉個例子,我們要點亮某個筆段時,只需要保證給其電極兩端加的電壓差為3.3V(如COM1=3.3V,SEG1=0V),并且間隔合適的時間,將這兩極的電壓反轉輸出(如COM1=0V,SEG1=3.3V);不點亮某個筆段時,只需要保證給其電極兩端加的電壓差為0V(如COM1=3.3V,SEG1=3.3V),并且間隔合適的時間,將這兩極的電壓反轉輸出(如COM1=0V,SEG1=0V),
本Demo使用的段碼液晶屏是3位數字“8”的樣式,有4個COM口,6個SEG口,其引腳對應筆段如下表所示:
| 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的函式:
<1> 顯示數字
/* 可顯示數字上限 */
#define SEG_LCD_DISP_MAX_NUM 999
/**
* @brief 顯示數字
* @param[in] num: 要顯示的十進制數字
* @param[in] high_zero: 高位為“0”時是否顯示0 (1-顯示,0-不顯示)
* @return none
*/
VOID_T tuya_seg_lcd_disp_num(IN CONST USHORT_T num, IN CONST BOOL_T high_zero)
{
UCHAR_T i, num_index[SEG_LCD_DISP_DIGIT];
UCHAR_T disp_digit = SEG_LCD_DISP_DIGIT;
USHORT_T tmp_num;
/* 檢查上限,超過則回傳 */
if (num > SEG_LCD_DISP_MAX_NUM) {
return;
}
/* 計算百位、十位、個位的值 */
tmp_num = num;
for (i = 0; i < SEG_LCD_DISP_DIGIT; i++) {
num_index[i] = tmp_num % 10;
tmp_num /= 10;
}
/* 高位為“0”時處理 */
if (!high_zero) {
for (i = (SEG_LCD_DISP_DIGIT-1); i > 0; i--) {
if (num_index[i] == 0) {
disp_digit--;
}
}
}
/* 生成SEG pin code */
for (i = 0; i < SEG_LCD_DISP_DIGIT; i++) {
if (i < disp_digit) {
__generate_seg_pin_output_code(ch_seg_code[num_index[i]], i);
} else {
__generate_seg_pin_output_code(0x00, i);
}
}
}
<2> 顯示字串
/* 段碼液晶屏資料位個數 */
#define SEG_LCD_DISP_DIGIT 3
/* 可用字符表,順序存放與ch_seg_code[]保持一致 */
CONST CHAR_T lcd_str_tbl[] = "0123456789- ";
/**
* @brief 顯示字串
* @param[in] str: 所有字串都已經在<lcd_str_tbl>中定義的字串
* @return none
*/
VOID_T tuya_seg_lcd_disp_str(IN CONST CHAR_T *str)
{
UINT_T i, len;
UCHAR_T ch_index[SEG_LCD_DISP_DIGIT];
/* 獲取字串長度 */
len = strlen(str);
if (len > SEG_LCD_DISP_DIGIT) {
len = SEG_LCD_DISP_DIGIT;
}
/* 查找每個字符在<lcd_str_tbl>中的位置以取出該字符的段碼,并生成SEG pin code */
for (i = 0; i < len; i++) {
ch_index[i] = strchr(lcd_str_tbl, *(str++)) - lcd_str_tbl;
__generate_seg_pin_output_code(ch_seg_code[ch_index[i]], (SEG_LCD_DISP_DIGIT-1-i));
}
}
<3> 顯示字符
/**
* @brief 在指定資料位顯示字符
* @param[in] ch: <lcd_str_tbl>中定義的字符
* @param[in] digit: 資料位,0表示最低位
* @return none
*/
VOID_T tuya_seg_lcd_disp_ch(IN CONST CHAR_T ch, IN CONST UCHAR_T digit)
{
UCHAR_T ch_index;
/* 查找字符在<lcd_str_tbl>中的位置以取出該字符的段碼,并生成SEG pin code */
ch_index = strchr(lcd_str_tbl, ch) - lcd_str_tbl;
__generate_seg_pin_output_code(ch_seg_code[ch_index], digit);
}
<4> 顯示自定義字符
/* 自定義字符型別 */
typedef struct {
UCHAR_T a : 1;
UCHAR_T b : 1;
UCHAR_T c : 1;
UCHAR_T d : 1;
UCHAR_T e : 1;
UCHAR_T f : 1;
UCHAR_T g : 1;
UCHAR_T dp : 1;
} SEG_LCD_CH_T;
/**
* @brief 在指定資料位顯示自定義字符
* @param[in] cus_ch: 自定字符
* @param[in] digit: 資料位,0表示最低位
* @return none
*/
VOID_T tuya_seg_lcd_disp_custom_ch(IN CONST SEG_LCD_CH_T cus_ch, IN CONST UCHAR_T digit)
{
UCHAR_T i, cus_seg_code, ch_index, tmp;
UCHAR_T seg_code = 0x00;
CHAR_T cus_seg_seq[8] = "abcdefg-";
CHAR_T *seg_seq = "-dcebgaf";
/* 段碼轉換 */
memcpy(&cus_seg_code, &cus_ch, 1);
for (i = 0; i < 8; i++) {
tmp = cus_seg_code & 0x01;
ch_index = strchr(seg_seq, cus_seg_seq[i]) - seg_seq;
seg_code |= (tmp << ch_index);
cus_seg_code >>= 1;
}
/* 生成SEG pin code */
__generate_seg_pin_output_code(seg_code, digit);
}
最后,在頭檔案中定義介面供用戶呼叫:
VOID_T tuya_seg_lcd_disp_num(IN CONST USHORT_T num, IN CONST BOOL_T high_zero);
VOID_T tuya_seg_lcd_disp_str(IN CONST CHAR_T *str);
VOID_T tuya_seg_lcd_disp_ch(IN CONST CHAR_T ch, IN CONST UCHAR_T digit);
VOID_T tuya_seg_lcd_disp_custom_ch(IN CONST SEG_LCD_CH_T cus_ch, IN CONST UCHAR_T digit);
完成了顯示內容的轉換,我們就可以在引腳驅動時使用seg_pin_code[COM_NUM]來控制SEG口的輸出了,
先來處理段碼液晶屏的初始化作業,即各引腳的初期配置,但具體使用哪些引腳由用戶定義后傳入組件:
/* 第一步:定義段碼液晶屏引腳型別 (tuya_seg_lcd.h) */
typedef struct {
TY_GPIO_PORT_E com[COM_NUM]; /* COM口: COM1-COM4 */
/* COM1: -, d */
/* COM2: c, e */
/* COM3: b, g */
/* COM4: a, f */
TY_GPIO_PORT_E seg[SEG_NUM]; /* SEG口: SEG1-SEG6 */
/* SEG1: --, 3c, 3b, 3a */
/* SEG2: 3d, 3e, 3g, 3f */
/* SEG3: --, 2c, 2b, 2a */
/* SEG4: 2d, 2e, 2g, 2f */
/* SEG5: --, 1c, 1b, 1a */
/* SEG6: 1d, 1e, 1g, 1f */
} SEG_LCD_PIN_T;
/* 第二步:定義用于段碼液晶屏資訊管理的結構體型別和該型別的指標 (tuya_seg_lcd.c) */
typedef struct {
SEG_LCD_PIN_T pin; /* 引腳管理 */
UCHAR_T seg_pin_code[COM_NUM]; /* SEG pin code */
BOOL_T light; /* 亮滅狀態 */
} SEG_LCD_MANAGE_T;
STATIC SEG_LCD_MANAGE_T sg_seg_lcd_mag;
/* 第三步:撰寫段碼液晶屏引腳控制相關函式,方便后續呼叫 (tuya_seg_lcd.c) */
/* 初始化段碼液晶屏引腳 */
STATIC VOID_T __seg_lcd_gpio_init(IN CONST TY_GPIO_PORT_E port)
{
tuya_gpio_input_init(port, TY_GPIO_FLOATING);
}
/* 設定段碼液晶屏引腳方向為輸出 */
STATIC VOID_T __seg_lcd_gpio_shutdown(IN CONST TY_GPIO_PORT_E port)
{
tuya_gpio_set_inout(port, TRUE);
}
/* 設定段碼液晶屏引腳狀輸出值 */
STATIC VOID_T __seg_lcd_gpio_write(IN CONST TY_GPIO_PORT_E port, IN CONST UINT_T level)
{
tuya_gpio_set_inout(port, FALSE);
tuya_gpio_write(port, level);
}
/* 第四步:撰寫段碼液晶屏驅動初始化函式 (tuya_seg_lcd.c) */
SEG_LCD_RET tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def)
{
UCHAR_T i;
/* 初始化管理資訊 */
memset(&sg_seg_lcd_mag, 0, sizeof(SEG_LCD_MANAGE_T));
sg_seg_lcd_mag.pin = pin_def;
/* COM引腳初始化 */
for (i = 0; i < COM_NUM; i++) {
__seg_lcd_gpio_init(pin_def.com[i]);
}
/* SEG引腳初始化 */
for (i = 0; i < SEG_NUM; i++) {
__seg_lcd_gpio_init(pin_def.seg[i]);
}
return SEG_LCD_OK;
}
/* 第五步:在頭檔案中定義初始化介面 (tuya_seg_lcd.h) */
VOID_T tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def);
接下來實作段碼液晶屏控制,由于段碼液晶屏的引腳控制需要3ms內處理一次,為避免液晶顯示被外部程式影響,所以設定一個硬體定時器來實作,并在定時器中斷中進行相關處理:
/* 第一步:定義COM口掃描相關宏 (tuya_seg_lcd.h) */
typedef BYTE_T SEG_LCD_STEP_E;
#define STEP_COM_HIGH 0x00
#define STEP_COM_LOW 0x01
#define STEP_COM_HI_Z 0x02
/* 第二步:添加COM口掃描相關變數 (tuya_seg_lcd.c) */
typedef struct {
UCHAR_T scan_com_num; /* 當前掃描COM口 */
SEG_LCD_STEP_E scan_step; /* 當前掃描階段 */
} SEG_LCD_MANAGE_T;
/* 第三步:撰寫引腳輸出控制函式 (tuya_seg_lcd.c) */
/**
* @brief 段碼液晶屏輸出控制,3ms處理一次
* @param[in] none
* @return none
*/
STATIC VOID_T __seg_lcd_output_ctrl(VOID_T)
{
UCHAR_T i, active_pin, actl_code;
BOOL_T seg_pin_level;
/* 獲取有效引腳 */
active_pin = (sg_seg_lcd_mag.scan_com_num == 0) ? 0x2a : 0x3f;
/* 獲取實際輸出code */
actl_code = __get_actual_output_code(sg_seg_lcd_mag.seg_pin_code[sg_seg_lcd_mag.scan_com_num]);
/* 每個COM口按輸出高-輸出低-高阻態順序進行控制,根據SEG口輸出code設定每個SEG口的輸出電平 */
switch (sg_seg_lcd_mag.scan_step) {
case STEP_COM_HIGH: /* COM輸出高 */
__seg_lcd_gpio_write(sg_seg_lcd_mag.pin.com[sg_seg_lcd_mag.scan_com_num], TRUE);
for (i = 0; i < SEG_NUM; i++) {
if (active_pin & (1 << i)) {
seg_pin_level = ((actl_code & (1 << i)) > 0) ? FALSE : TRUE;
__seg_lcd_gpio_write(sg_seg_lcd_mag.pin.seg[i], seg_pin_level);
}
}
sg_seg_lcd_mag.scan_step = STEP_COM_LOW;
break;
case STEP_COM_LOW: /* COM輸出低 */
__seg_lcd_gpio_write(sg_seg_lcd_mag.pin.com[sg_seg_lcd_mag.scan_com_num], FALSE);
for (i = 0; i < SEG_NUM; i++) {
if (active_pin & (1 << i)) {
seg_pin_level = ((actl_code & (1 << i)) > 0) ? TRUE : FALSE;
__seg_lcd_gpio_write(sg_seg_lcd_mag.pin.seg[i], seg_pin_level);
}
}
sg_seg_lcd_mag.scan_step = STEP_COM_HI_Z;
break;
case STEP_COM_HI_Z: /* COM高阻態 */
__seg_lcd_gpio_shutdown(sg_seg_lcd_mag.pin.com[sg_seg_lcd_mag.scan_com_num]);
for (i = 0; i < SEG_NUM; i++) {
if (active_pin & (1 << i)) {
__seg_lcd_gpio_shutdown(sg_seg_lcd_mag.pin.seg[i]);
}
}
sg_seg_lcd_mag.scan_com_num++;
if (sg_seg_lcd_mag.scan_com_num >= COM_NUM) {
sg_seg_lcd_mag.scan_com_num = 0;
}
sg_seg_lcd_mag.scan_step = STEP_COM_HIGH;
break;
default:
break;
}
}
/* 第四步:定義COM口掃描時間 (tuya_seg_lcd.c) */
#define SEG_LCD_COM_SCAN_CYCLE_MS 3
/* 第五步:添加硬體定時器初始化,注冊回呼函式 (tuya_seg_lcd.c) */
SEG_LCD_RET tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def)
{
...
/* 定時器初始化 */
tuya_hardware_timer_create(TY_TIMER_0, SEG_LCD_COM_SCAN_CYCLE_MS*1000, __seg_lcd_output_ctrl, TY_TIMER_REPEAT);
return SEG_LCD_OK;
}
最后,我們來實作液晶屏的點亮、熄滅和閃爍,基本參照LED的處理方式,但考慮到液晶屏在閃爍方面的需求沒有那么頻繁和復雜,所以這里對引數設定做了一些簡化,下面直接來看代碼:
【tuya_seg_lcd.h】
/* 閃爍方式相關宏定義 */
#define SEG_LCD_FLASH_DIGIT_ALL 0xFF
#define SEG_LCD_FLASH_FOREVER 0xFFFF
/* 閃爍型別定義 */
typedef BYTE_T SEG_LCD_FLASH_TYPE_E;
#define SLFT_STA_ON_END_ON 0x01 /* 開始時:亮;結束后:亮 */
#define SLFT_STA_ON_END_OFF 0x02 /* 開始時:亮;結束后:滅 */
#define SLFT_STA_OFF_END_ON 0x04 /* 開始時:滅;結束后:亮 */
#define SLFT_STA_OFF_END_OFF 0x05 /* 開始時:滅;結束后:滅 */
/* 回呼函式型別定義 */
typedef VOID_T (*SEG_LCD_CALLBACK)();
/* 相關介面定義 */
SEG_LCD_RET tuya_seg_lcd_set_light(BOOL_T on_off);
SEG_LCD_RET tuya_seg_lcd_set_flash(IN CONST UCHAR_T digit, IN CONST SEG_LCD_FLASH_TYPE_E type, IN CONST USHORT_T intv, IN CONST USHORT_T count, IN CONST SEG_LCD_CALLBACK end_cb);
【tuya_seg_lcd.c】
/* 段碼液晶屏閃爍資訊管理 */
typedef struct {
UCHAR_T digit; /* 閃爍的位,0xFF表示所有位同時閃爍 */
SEG_LCD_FLASH_TYPE_E type; /* 閃爍型別 */
USHORT_T intv; /* 閃爍間隔 */
USHORT_T count; /* 閃爍次數 */
SEG_LCD_CALLBACK end_cb; /* 閃爍結束時的回呼函式 */
UINT_T work_timer; /* 閃爍作業用計時變數 */
} SEG_LCD_FLASH_T;
/* 段碼液晶屏資訊管理 */
typedef struct {
SEG_LCD_PIN_T pin; /* 引腳管理 */
UCHAR_T seg_pin_code[COM_NUM]; /* SEG pin code */
BOOL_T light; /* 亮滅狀態 */
UCHAR_T scan_com_num; /* 當前掃描COM口 */
SEG_LCD_STEP_E scan_step; /* 當前掃描階段 */
SEG_LCD_FLASH_T *flash; /* 閃爍資訊管理 */
BOOL_T stop_flash_req; /* 停止閃爍請求 */
BOOL_T stop_flash_light; /* 停止閃爍后的亮滅狀態 */
} SEG_LCD_MANAGE_T;
STATIC SEG_LCD_MANAGE_T sg_seg_lcd_mag;
/**
* @brief 獲取實際輸出的SEG pin code,在__seg_lcd_output_ctrl()中呼叫
* @param[in] seg_pin_code: 用戶設定的顯示內容轉換得到的SEG pin code
* @return none
*/
STATIC UCHAR_T __get_actual_output_code(UCHAR_T seg_pin_code)
{
UCHAR_T code = seg_pin_code;
/* 當前亮滅狀態為熄滅時進行處理 */
if (!sg_seg_lcd_mag.light) {
if (sg_seg_lcd_mag.flash == NULL) {
code = 0x00;
} else {
if (sg_seg_lcd_mag.flash->digit == SEG_LCD_FLASH_DIGIT_ALL) {
code = 0x00;
} else {
code &= ~(ch_com_code[sg_seg_lcd_mag.flash->digit]);
}
}
}
return code;
}
/**
* @brief 亮滅控制 (內部呼叫)
* @param[in] on_off: 1-亮, 0-滅
* @return none
*/
VOID_T __set_seg_lcd_light(IN CONST BOOL_T on_off)
{
sg_seg_lcd_mag.light = on_off;
}
/**
* @brief 亮滅控制 (外部呼叫)
* @param[in] on_off: 1-亮, 0-滅
* @return SEG_LCD_RET
*/
SEG_LCD_RET tuya_seg_lcd_set_light(IN CONST BOOL_T on_off)
{
if (sg_seg_lcd_mag.flash != NULL) {
sg_seg_lcd_mag.stop_flash_req = TRUE;
sg_seg_lcd_mag.stop_flash_light = on_off;
} else {
__set_seg_lcd_light(on_off);
}
return SEG_LCD_OK;
}
/**
* @brief 獲取閃爍開始時的亮滅狀態
* @param[in] type: 閃爍型別
* @return 1-亮,0-滅
*/
STATIC BOOL_T __get_seg_lcd_flash_sta_light(IN CONST SEG_LCD_FLASH_TYPE_E type)
{
BOOL_T ret = TRUE;
switch (type) {
case SLFT_STA_ON_END_ON:
case SLFT_STA_ON_END_OFF:
ret = TRUE;
break;
case SLFT_STA_OFF_END_ON:
case SLFT_STA_OFF_END_OFF:
ret = FALSE;
break;
default:
break;
}
return ret;
}
/**
* @brief 獲取閃爍結束后的亮滅狀態
* @param[in] type: 閃爍型別
* @return 1-亮,0-滅
*/
STATIC BOOL_T __get_seg_lcd_flash_end_light(IN CONST SEG_LCD_FLASH_TYPE_E type)
{
BOOL_T ret = TRUE;
switch (type) {
case SLFT_STA_ON_END_ON:
case SLFT_STA_OFF_END_ON:
ret = TRUE;
break;
case SLFT_STA_ON_END_OFF:
case SLFT_STA_OFF_END_OFF:
ret = FALSE;
break;
default:
break;
}
return ret;
}
/**
* @brief 段碼液晶屏閃爍配置
* @param[in] digit: 閃爍資料位, "0xFF"表示所有位同時閃爍
* @param[in] type: 閃爍型別
* @param[in] intv: 閃爍間隔(ms)
* @param[in] count: 閃爍次數, "0xFFFF"表示永遠閃爍
* @param[in] end_cb: 閃爍結束時的回呼函式
* @return SEG_LCD_RET
*/
SEG_LCD_RET tuya_seg_lcd_set_flash(IN CONST UCHAR_T digit, IN CONST SEG_LCD_FLASH_TYPE_E type, IN CONST USHORT_T intv, IN CONST USHORT_T count, IN CONST SEG_LCD_CALLBACK end_cb)
{
sg_seg_lcd_mag.stop_flash_req = FALSE;
if (sg_seg_lcd_mag.flash == NULL) {
SEG_LCD_FLASH_T *seg_lcd_flash = (SEG_LCD_FLASH_T *)tuya_ble_malloc(SIZEOF(SEG_LCD_FLASH_T));
if (NULL == seg_lcd_flash) {
return SEG_LCD_ERR_MALLOC_FAILED;
}
sg_seg_lcd_mag.flash = seg_lcd_flash;
}
sg_seg_lcd_mag.flash->digit = digit;
sg_seg_lcd_mag.flash->type = type;
sg_seg_lcd_mag.flash->intv = intv;
sg_seg_lcd_mag.flash->count = count;
sg_seg_lcd_mag.flash->work_timer = 0;
sg_seg_lcd_mag.flash->end_cb = end_cb;
__set_seg_lcd_light(__get_seg_lcd_flash_sta_light(type));
return SEG_LCD_OK;
}
/**
* @brief 段碼液晶屏閃爍處理
* @param[inout] none
* @return none
*/
STATIC VOID_T __seg_lcd_flash_proc(VOID_T)
{
BOOL_T one_cycle_flag = FALSE;
BOOL_T start_light = __get_seg_lcd_flash_sta_light(sg_seg_lcd_mag.flash->type);
/* 閃爍周期性處理,實作按照指定時間點亮和熄滅 */
sg_seg_lcd_mag.flash->work_timer += SEG_LCD_FLASH_PROC_CYCLE_MS;
if (sg_seg_lcd_mag.flash->work_timer >= sg_seg_lcd_mag.flash->intv*2) {
sg_seg_lcd_mag.flash->work_timer -= sg_seg_lcd_mag.flash->intv*2;
__set_seg_lcd_light(start_light);
one_cycle_flag = TRUE;
} else if (sg_seg_lcd_mag.flash->work_timer >= sg_seg_lcd_mag.flash->intv) {
__set_seg_lcd_light(!start_light);
} else {
;
}
/* 閃爍倒計數處理,閃爍方式為“永遠閃爍”時不處理 */
if (sg_seg_lcd_mag.flash->count == SEG_LCD_FLASH_FOREVER) {
return;
}
if (one_cycle_flag) {
if (sg_seg_lcd_mag.flash->count > 0) {
sg_seg_lcd_mag.flash->count--;
}
}
/* 閃爍結束處理 */
if (sg_seg_lcd_mag.flash->count == 0) {
if (sg_seg_lcd_mag.flash->end_cb != NULL) {
sg_seg_lcd_mag.flash->end_cb();
}
sg_seg_lcd_mag.stop_flash_req = TRUE;
sg_seg_lcd_mag.stop_flash_light = __get_seg_lcd_flash_end_light(sg_seg_lcd_mag.flash->type);
}
}
/* 定義超時處理時間 */
#define SEG_LCD_FLASH_PROC_CYCLE_MS 10
/* 添加軟體定時器初始化,注冊回呼函式 */
SEG_LCD_RET tuya_seg_lcd_init(SEG_LCD_PIN_T pin_def)
{
...
/* 定時器初始化 */
tuya_software_timer_create(SEG_LCD_FLASH_PROC_CYCLE_MS*1000, __seg_lcd_timeout_handler);
...
}
/**
* @brief 段碼液晶屏超時處理
* @param[in] none
* @return 0
*/
STATIC INT_T __seg_lcd_timeout_handler(VOID_T)
{
/* 停止閃爍請求處理 */
if (sg_seg_lcd_mag.stop_flash_req) {
__set_seg_lcd_light(sg_seg_lcd_mag.stop_flash_light);
tuya_ble_free((UCHAR_T *)sg_seg_lcd_mag.flash);
sg_seg_lcd_mag.flash = NULL;
sg_seg_lcd_mag.stop_flash_req = FALSE;
}
/* 如果閃爍功能未開啟則不處理 */
if (sg_seg_lcd_mag.flash != NULL) {
__seg_lcd_flash_proc();
}
return 0;
}
三.功能實作
1.設備基礎服務
設備基礎服務模塊包括設備狀態遷移、模式選擇、模式切換、本地定時等功能處理,功能需求中已經對可選的運動模式即模式遷移方式進行了定義,這里再對設備狀態進行如下定義:
| 設備狀態 | 含義 | 處理 |
|---|---|---|
| 未使用 | 用戶未使用呼啦圈 | 保持熄屏,實時資料清除 |
| 使用中 | 用戶未轉動呼啦圈 | 保持亮屏 |
| 轉動中 | 用戶轉動呼啦圈 | 開始轉動時點亮螢屏,轉動程序:亮屏6秒,熄屏5分鐘 |
| 復位 | 設備復位 | 所有資料清除 |
設備狀態遷移情況如下(第一列為遷移前狀態,第一行為遷移后狀態):
| 未使用 | 使用中 | 轉動中 | 復位中 | |
|---|---|---|---|---|
| 未使用 | / | 觸發按鍵 | 呼啦圈轉動 | / |
| 使用中 | 30秒內無動作 | / | 呼啦圈轉動 | 操作復位鍵 |
| 轉動中 | / | 未檢測到磁鐵2秒后 | / | / |
| 復位 | / | 復位結束后 | / | / |
代碼實作:
/* 第一步:定義基礎服務資料型別 (tuya_hula_hoop_svc_basic.h) */
/* 作業模式 */
typedef BYTE_T MODE_E;
#define MODE_NORMAL 0x00 /* 普通模式 */
#define MODE_TARGET 0x01 /* 目標模式 */
/* 設備狀態 */
typedef BYTE_T STAT_E;
#define STAT_USING 0x00 /* 使用中 */
#define STAT_ROTATING 0x01 /* 轉動中 */
#define STAT_UNUSED 0x02 /* 未使用 */
#define STAT_RESET 0x03 /* 復位 */
/* 基礎服務資訊管理 */
typedef struct {
MODE_E mode; /* 作業模式 */
MODE_E mode_temp; /* 預選模式 */
STAT_E stat; /* 設備狀態 */
} HULA_HOOP_T;
/* 第二步:定義基礎服務資訊管理結構體并初始化 (tuya_hula_hoop_svc_basic.c) */
HULA_HOOP_T g_hula_hoop;
/**
* @brief 基礎服務模塊初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_basic_service_init(VOID_T)
{
memset(&g_hula_hoop, 0, SIZEOF(HULA_HOOP_T));
__set_device_status(STAT_USING); /* 上電設備狀態:使用中 */
__set_work_mode(MODE_NORMAL); /* 默認作業模式:普通模式 */
}
/* 第三步:撰寫基礎服務相關處理函式 (tuya_hula_hoop_svc_basic.c) */
/**
* @brief 設定作業模式 (內部呼叫)
* @param[in] mode: 作業模式
* @return none
*/
STATIC VOID_T __set_work_mode(IN CONST MODE_E mode)
{
g_hula_hoop.mode = mode;
switch (mode) {
case MODE_NORMAL:
TUYA_APP_LOG_INFO("Work mode is normal mode now.");
hula_hoop_disp_switch_to_normal_mode();
break;
case MODE_TARGET:
TUYA_APP_LOG_INFO("Work mode is target mode now.");
hula_hoop_disp_switch_to_target_mode();
break;
default:
break;
}
}
/**
* @brief 切換預選模式,在模式鍵短按處理時呼叫
* @param[in] none
* @return none
*/
VOID_T hula_hoop_switch_temp_mode(VOID_T)
{
switch (g_hula_hoop.mode_temp) {
case MODE_NORMAL:
g_hula_hoop.mode_temp = MODE_TARGET;
break;
case MODE_TARGET:
g_hula_hoop.mode_temp = MODE_NORMAL;
break;
default:
break;
}
}
/**
* @brief 進入模式選擇,在模式鍵短按處理時呼叫
* @param[in] none
* @return none
*/
VOID_T hula_hoop_enter_mode_select(VOID_T)
{
g_hula_hoop.mode_temp = g_hula_hoop.mode;
hula_hoop_disp_switch_to_mode_select();
}
/**
* @brief 退出模式選擇,在復位鍵短按處理時呼叫
* @param[in] none
* @return none
*/
VOID_T hula_hoop_quit_mode_select(VOID_T)
{
__set_work_mode(g_hula_hoop.mode);
}
/**
* @brief 切換到預選模式,在模式鍵長按2秒處理時呼叫
* @param[in] none
* @return none
*/
VOID_T hula_hoop_switch_to_select_mode(VOID_T)
{
hula_hoop_clear_realtime_data();
__set_work_mode(g_hula_hoop.mode_temp);
}
/**
* @brief 設定作業模式,在云端下發作業模式時呼叫
* @param[in] mode: 作業模式
* @return none
*/
VOID_T hula_hoop_set_work_mode(IN CONST MODE_E mode)
{
if (mode != g_hula_hoop.mode) {
__set_work_mode(mode);
}
}
/**
* @brief 設定設備狀態 (內部呼叫)
* @param[in] stat: 設備狀態
* @return none
*/
STATIC VOID_T __set_device_status(IN CONST STAT_E stat)
{
hula_hoop_get_device_status() = stat;
switch (stat) {
case STAT_UNUSED:
TUYA_APP_LOG_INFO("Device status is 'unused'.");
hula_hoop_clear_realtime_data();
hula_hoop_disp_sleep();
break;
case STAT_USING:
TUYA_APP_LOG_INFO("Device status is 'using'.");
hula_hoop_disp_wakeup();
break;
case STAT_ROTATING:
TUYA_APP_LOG_INFO("Device status is 'rotating'.");
hula_hoop_disp_wakeup();
break;
case STAT_RESET:
start_reboot();
break;
default:
break;
}
}
/**
* @brief 設定設備狀態,在狀態更新時呼叫
* @param[in] stat: 設備狀態
* @return none
*/
VOID_T hula_hoop_set_device_status(IN CONST STAT_E stat)
{
if (stat != hula_hoop_get_device_status()) {
__set_device_status(stat);
}
}
/**
* @brief 獲取設備狀態
* @param[in] none
* @return 設備狀態
*/
STAT_E hula_hoop_get_device_status(VOID_T)
{
return hula_hoop_get_device_status();
}
/* 第四步:在頭檔案中定義基礎服務處理介面 (tuya_hula_hoop_svc_basic.h) */
VOID_T hula_hoop_switch_temp_mode(VOID_T);
VOID_T hula_hoop_enter_mode_select(VOID_T);
VOID_T hula_hoop_quit_mode_select(VOID_T);
VOID_T hula_hoop_switch_to_select_mode(VOID_T);
VOID_T hula_hoop_set_work_mode(IN CONST MODE_E mode);
VOID_T hula_hoop_set_device_status(IN CONST STAT_E stat);
STAT_E hula_hoop_get_device_status(VOID_T);
VOID_T hula_hoop_basic_service_init(VOID_T);
除以上基礎服務外,為了實作每日資料和每月資料的累計功能,需添加本地定時功能,以實作日期和月份變更的檢查和處理,本地定時模塊的代碼實作如下:
/* 第一步:定義本地時間資料型別 (tuya_local_time.h) */
typedef struct {
USHORT_T year;
UCHAR_T month;
UCHAR_T day;
UCHAR_T hour;
UCHAR_T minute;
UCHAR_T second;
} LOCAL_TIME_T;
/* 第二步:定義本地時間并初始化 (tuya_local_time.c) */
LOCAL_TIME_T g_local_time = {
.year = 1,
.month = 1,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
};
/* 第三步:定義每月天數表,用于判斷月份變更 (tuya_local_time.c) */
STATIC UCHAR_T sg_day_tbl[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/* 第四步:撰寫本地時間更新相關處理函式 (tuya_local_time.c) */
/**
* @brief 閏年判斷并更新2月份天數
* @param[in] year: 要判斷的年份
* @return 2月份天數
*/
STATIC UCHAR_T __local_time_leap_year_judgment(USHORT_T year)
{
if ((year % 400 == 0) ||
((year % 4 == 0) && (year / 100 != 0))) {
return 29;
} else {
return 28;
}
}
/**
* @brief 每年更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_year(VOID_T)
{
g_local_time.year++;
sg_day_tbl[1] = __local_time_leap_year_judgment(g_local_time.year);
}
/**
* @brief 每月更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_month(VOID_T)
{
g_local_time.month++;
if (g_local_time.month > 12) {
g_local_time.month = 1;
__local_time_update_per_year();
}
}
/**
* @brief 每天更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_day(VOID_T)
{
g_local_time.day++;
if (g_local_time.day > sg_day_tbl[g_local_time.month-1]) {
g_local_time.day = 1;
__local_time_update_per_month();
}
}
/**
* @brief 每小時更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_hour(VOID_T)
{
g_local_time.hour++;
if (g_local_time.hour >= 24) {
g_local_time.hour = 0;
__local_time_update_per_day();
}
}
/**
* @brief 每分鐘更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_minute(VOID_T)
{
g_local_time.minute++;
if (g_local_time.minute >= 60) {
g_local_time.minute = 0;
__local_time_update_per_hour();
}
}
/**
* @brief 每秒鐘更新
* @param[in] none
* @return none
*/
STATIC VOID_T __local_time_update_per_second(VOID_T)
{
g_local_time.second++;
if (g_local_time.second >= 60) {
g_local_time.second = 0;
__local_time_update_per_minute();
}
}
/**
* @brief 本地時間更新
* @param[in] none
* @return none
*/
VOID_T tuya_local_time_update(VOID_T)
{
__local_time_update_per_second();
}
/**
* @brief 獲取本地時間
* @param[in] none
* @return 本地時間
*/
LOCAL_TIME_T tuya_get_local_time(VOID_T)
{
return g_local_time;
}
/* 第五步:在頭檔案中定義介面 (tuya_local_time.h) */
VOID_T tuya_local_time_update(VOID_T);
LOCAL_TIME_T tuya_get_local_time(VOID_T);
/* 第六步:撰寫本地時間變更檢查處理函式 (tuya_hula_hoop_svc_basic.c) */
/**
* @brief 檢查日期是否變更并處理
* @param[in] none
* @return none
*/
VOID_T hula_hoop_check_date_change(VOID_T)
{
STATIC UCHAR_T s_day = 0;
STATIC UCHAR_T s_month = 0;
LOCAL_TIME_T local_time;
/* 獲取本地時間 */
local_time = tuya_get_local_time();
/* 日期變更檢查 */
if (local_time.day != s_day) {
s_day = local_time.day;
hula_hoop_update_total_data_day();
}
/* 月份變更檢查 */
if (local_time.month != s_month) {
s_month = local_time.month;
hula_hoop_update_total_data_month();
}
}
/* 第七步:在頭檔案中定義介面 (tuya_hula_hoop_svc_basic.h) */
VOID_T hula_hoop_check_date_change(VOID_T);
/* 第八步:在本地時間更新定時函式中呼叫相關介面進行處理 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 本地時間更新定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __upd_local_time_timer(IN CONST UINT_T time_inc)
{
g_timer.upd_local_time += time_inc;
if (g_timer.upd_local_time >= LOCAL_TIME_UPDATE_INTV_MS) {
g_timer.upd_local_time -= LOCAL_TIME_UPDATE_INTV_MS;
tuya_local_time_update(); /* 本地時間更新 */
hula_hoop_check_date_change(); /* 檢查日期變更 */
}
}
2.顯示處理服務
顯示處理模塊包括顯示屏內容的設定、顯示屏狀態控制和指示燈狀態控制,
根據功能設定,顯示內容可以分為以下4個部分:
| 顯示模式 | 段碼液晶屏顯示 | LED顯示 |
|---|---|---|
| 普通模式界面 | 顯示實時資料,按”實時計時->實時圈數->實時卡路里“順序切換 | 點亮資料對應指示燈 |
| 目標模式界面 | 當日目標剩余時長≠0時,顯示該資料,否則顯示”—“ | 點亮計時指示燈 |
| 模式選擇界面 | 普通模式顯示”010“,目標模式顯示”020“ | 全滅 |
| 復位提示界面 | 顯示”000“ | 全滅 |
其中計時指示燈還作為配網指示燈使用:
| 指示燈功能 | 指示燈狀態 | 優先級 |
|---|---|---|
| 配網指示功能 | 配網指示燈快閃 | 高 |
| 資料指示功能 | 根據顯示模式控制狀態 | 低 |
還需實作液晶屏點亮、熄滅、閃爍的控制(當顯示屏熄滅時,如果指示燈作為資料指示,則隨之熄滅):
| 段碼液晶屏狀態 | 處理 |
|---|---|
| 熄滅 | 段碼液晶屏熄滅 |
| 亮屏 | 段碼液晶屏點亮 |
| 閃爍(目標完成) | 段碼液晶屏閃爍3次,結束時點亮(保持“—”顯示)(目標模式界面才執行) |
| 閃爍(復位提示) | 段碼液晶屏閃爍3次,結束時執行“設備復位” |
代碼實作:
外設驅動的部分可以使用之前撰寫的LED驅動組件和Segment LCD驅動組件進行開發,本demo硬體方案如下:
| 外設 | 引腳 | 有效電平 |
|---|---|---|
| 計時/配網指示燈 | 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);
3.資料處理服務
智能呼啦圈相關的運動資料設定如下:
| 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);
4.用戶事件處理
用戶可觸發的部件有按鍵和霍爾傳感器,前者用于模式切換、配網請求和設備復位,后者用于檢測呼啦圈轉動圈數,根據功能需求描述,用戶事件處理模塊的處理內容梳理如下:
| 事件 | 事件型別 | 限制條件 | 處理內容 |
|---|---|---|---|
| 觸發模式鍵 | 短按 | 模式選擇界面 | 切換“預選模式” |
| 其他 | 進入“模式選擇”處理 | ||
| 長按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();
}
5.定時事件處理
本demo的功能設定中涉及較多的定時需求,具體情況整理如下:
| No. | 定時器名稱 | 執行條件 | 定時時間 | 超時處理內容 |
|---|---|---|---|---|
| 1 | 螢屏熄滅確認 | 亮屏狀態、【轉動中】、無按鍵事件 | 6秒 | 熄滅螢屏 |
| 2 | 螢屏點亮確認 | 熄屏狀態、【轉動中】 | 5分鐘 | 點亮螢屏 |
| 3 | 停止轉動確認 | 【轉動中】、無霍爾事件 | 3秒 | 設定設備狀態為【使用中】 |
| 4 | 停止使用確認 | 【使用中】、無按鍵事件 | 30秒 | 設定設備狀態為【未使用】 |
| 5 | 資料顯示切換 | 普通模式界面 | 2秒 | 切換顯示資料 |
| 6 | 計時資料更新 | 【轉動中】 | 1分鐘 | 更新計時資料并上報 |
| 7 | 本地時間更新 | - | 1秒 | 更新本地時間、檢查日期變更 |
| 8 | 資料上報更新 | - | 5秒 | 上報圈數&卡路里資料 |
| 9 | 配網等待結束 | 設備未被用戶系結且正在等待配網 | 1分鐘 | 禁止配網 |
代碼實作:
/* 第一步:定義定時器資料結構 (tuya_hula_hoop_evt_timer.c) */
typedef struct {
UINT_T disp_sleep;
UINT_T disp_wakeup;
UINT_T stop_rotating;
UINT_T stop_using;
UINT_T switch_disp_data;
UINT_T upd_time_data;
UINT_T upd_local_time;
UINT_T repo_dp_data;
UINT_T wait_bind;
} HULA_HOOP_TIMER_T;
/* 第二步:定義定時時間相關宏 (tuya_hula_hoop_evt_timer.c) */
#define TIMER_PERIOD_MS (100) /* 100ms */
#define DISP_SLEEP_CONFIRM_TIME_MS (6*1000) /* 6s */
#define DISP_WAKEUP_CONFIRM_TIME_MS (5*60*1000) /* 5min */
#define STOP_ROTATING_CONFIRM_TIME_MS (3000) /* 3s */
#define STOP_USING_CONFIRM_TIME_MS (30*1000) /* 30s */
#define DISP_DATA_SWITCH_INTV_MS (2000) /* 2s */
#define TIME_DATA_UPDATE_INTV_MS (1*60*1000) /* 1min */
#define LOCAL_TIME_UPDATE_INTV_MS (1000) /* 1s */
#define DP_DATA_REPO_INTV_MS (5*1000) /* 5s */
#define WAIT_BIND_END_TIME_MS (1*60*1000) /* 1min */
/* 第三步:定義定時器變數并撰寫初始化函式 (tuya_hula_hoop_evt_timer.c) */
STATIC HULA_HOOP_TIMER_T sg_timer;
/**
* @brief 定時處理模塊初始化,創建1個100ms軟體定時器
* @param[in] none
* @return none
*/
VOID_T hula_hoop_timer_init(VOID_T)
{
memset(&sg_timer, 0, SIZEOF(HULA_HOOP_TIMER_T));
tuya_software_timer_create(TIMER_PERIOD_MS*1000, __timer_timeout_handler);
}
/* 第四步:撰寫各個定時處理函式 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 螢屏休眠確認定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __disp_sleep_timer(IN CONST UINT_T time_inc)
{
if ((FALSE == hula_hoop_disp_is_wakeup()) ||
(hula_hoop_get_device_status() != STAT_ROTATING) ||
(F_WAIT_BINDING == SET)) {
sg_timer.disp_sleep = 0;
return;
}
sg_timer.disp_sleep += time_inc;
if (sg_timer.disp_sleep >= DISP_SLEEP_CONFIRM_TIME_MS) {
if (FALSE == hula_hoop_disp_is_flash()) {
hula_hoop_disp_sleep();
}
}
}
/**
* @brief 螢屏喚醒確認定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __disp_wakeup_timer(IN CONST UINT_T time_inc)
{
if ((TRUE == hula_hoop_disp_is_wakeup()) ||
(hula_hoop_get_device_status() != STAT_ROTATING)) {
sg_timer.disp_wakeup = 0;
return;
}
sg_timer.disp_wakeup += time_inc;
if (sg_timer.disp_wakeup >= DISP_WAKEUP_CONFIRM_TIME_MS) {
sg_timer.disp_wakeup = 0;
hula_hoop_disp_wakeup();
}
}
/**
* @brief 停止轉動確認定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __stop_rotating_timer(IN CONST UINT_T time_inc)
{
if (hula_hoop_get_device_status() != STAT_ROTATING) {
sg_timer.stop_rotating = 0;
return;
}
sg_timer.stop_rotating += time_inc;
if (sg_timer.stop_rotating >= STOP_ROTATING_CONFIRM_TIME_MS) {
sg_timer.stop_rotating = 0;
hula_hoop_set_device_status(STAT_USING);
}
}
/**
* @brief 停止使用確認定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __stop_using_timer(IN CONST UINT_T time_inc)
{
if ((hula_hoop_get_device_status() != STAT_USING) ||
(F_WAIT_BINDING == SET)) {
sg_timer.stop_using = 0;
return;
}
sg_timer.stop_using += time_inc;
if (sg_timer.stop_using >= STOP_USING_CONFIRM_TIME_MS) {
sg_timer.stop_using = 0;
hula_hoop_set_device_status(STAT_UNUSED);
}
}
/**
* @brief 顯示資料切換定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __switch_disp_data_timer(IN CONST UINT_T time_inc)
{
if ((hula_hoop_get_disp_mode() != DISP_NORMAL_MODE) ||
(FALSE == hula_hoop_disp_is_wakeup())) {
sg_timer.switch_disp_data = 0;
return;
}
sg_timer.switch_disp_data += time_inc;
if (sg_timer.switch_disp_data >= DISP_DATA_SWITCH_INTV_MS) {
sg_timer.switch_disp_data -= DISP_DATA_SWITCH_INTV_MS;
hula_hoop_switch_disp_data();
}
}
/**
* @brief 計時資料更新定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __upd_time_data_timer(IN CONST UINT_T time_inc)
{
if (hula_hoop_get_device_status() != STAT_ROTATING) {
return;
}
sg_timer.upd_time_data += time_inc;
if (sg_timer.upd_time_data >= TIME_DATA_UPDATE_INTV_MS) {
sg_timer.upd_time_data -= TIME_DATA_UPDATE_INTV_MS;
if (hula_hoop_update_sport_data_time()) {
hula_hoop_disp_target_finish();
}
hula_hoop_report_sport_data1();
}
}
/**
* @brief 本地時間更新定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __upd_local_time_timer(IN CONST UINT_T time_inc)
{
sg_timer.upd_local_time += time_inc;
if (sg_timer.upd_local_time >= LOCAL_TIME_UPDATE_INTV_MS) {
sg_timer.upd_local_time -= LOCAL_TIME_UPDATE_INTV_MS;
tuya_local_time_update();
hula_hoop_check_date_change();
}
}
/**
* @brief DP資料上報定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __repo_dp_data_timer(IN CONST UINT_T time_inc)
{
sg_timer.repo_dp_data += time_inc;
if (sg_timer.repo_dp_data >= DP_DATA_REPO_INTV_MS) {
sg_timer.repo_dp_data -= DP_DATA_REPO_INTV_MS;
hula_hoop_report_sport_data2();
}
}
/**
* @brief 配網等待停止定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __wait_bind_timer(IN CONST UINT_T time_inc)
{
if ((F_BLE_BOUND == SET) || (F_WAIT_BINDING == CLR)) {
sg_timer.wait_bind = 0;
return;
}
sg_timer.wait_bind += time_inc;
if (sg_timer.wait_bind >= WAIT_BIND_END_TIME_MS) {
sg_timer.wait_bind = 0;
hula_hoop_prohibit_binding();
}
}
/* 第五步:撰寫100ms超時處理函式 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 定時處理
* @param[in] none
* @return none
*/
STATIC INT_T __timer_timeout_handler(VOID_T)
{
__disp_sleep_timer(TIMER_PERIOD_MS);
__disp_wakeup_timer(TIMER_PERIOD_MS);
__stop_rotating_timer(TIMER_PERIOD_MS);
__stop_using_timer(TIMER_PERIOD_MS);
__switch_disp_data_timer(TIMER_PERIOD_MS);
__upd_time_data_timer(TIMER_PERIOD_MS);
__upd_local_time_timer(TIMER_PERIOD_MS);
__repo_dp_data_timer(TIMER_PERIOD_MS);
__wait_bind_timer(TIMER_PERIOD_MS);
return 0;
}
/* 第六步:撰寫定時器計時復位函式,供外部復位 (tuya_hula_hoop_evt_timer.c) */
/**
* @brief 按鍵事件發生時相關計時復位
* @param[in] none
* @return none
*/
VOID_T hula_hoop_reset_timer_for_key_event(VOID_T)
{
sg_timer.disp_sleep = 0;
sg_timer.stop_using = 0;
}
/**
* @brief 霍爾事件發生時相關計時復位
* @param[in] none
* @return none
*/
VOID_T hula_hoop_reset_timer_for_hall_event(VOID_T)
{
sg_timer.stop_rotating = 0;
}
/**
* @brief 復位“計時更新”定時器
* @param[in] none
* @return none
*/
VOID_T hula_hoop_reset_upd_time_data_timer(VOID_T)
{
sg_timer.upd_time_data = 0;
}
/* 第七步:在頭檔案中定義相關介面 (tuya_hula_hoop_evt_timer.c) */
VOID_T hula_hoop_timer_init(VOID_T);
VOID_T hula_hoop_reset_timer_for_key_event(VOID_T);
VOID_T hula_hoop_reset_timer_for_hall_event(VOID_T);
VOID_T hula_hoop_reset_upd_time_data_timer(VOID_T);
6.聯網相關處理
-
配網等待處理
要實作和云端進行資料互動,首先要使設備配網,配網功能已經通過BLE SDK實作,下面來介紹本demo的配網等待處理和配網提醒機制:
| No. | 設備狀態 | 執行動作 | 配網指示燈 |
|---|---|---|---|
| 1 | 上電時,檢測到已被用戶系結 | 不等待配網(保持藍牙廣播) | 不閃爍 |
| 2 | 上電時,檢測到未被用戶系結 | 開始等待配網(保持藍牙廣播) | 開始閃爍 |
| 3 | 長按模式鍵5秒時 | 開始等待配網,打開藍牙廣播 | 開始閃爍 |
| 4 | 開始等待配網1分鐘后,檢測到仍未被用戶系結 | 停止等待配網,關閉藍牙廣播 | 停止閃爍 |
| 5 | 開始等待配網后1分鐘內,檢測到已被用戶連接 | 停止等待配網(保持藍牙廣播) | 停止閃爍 |
| 6 | 檢測到被用戶解綁 | 不等待配網,關閉藍牙廣播 | 不閃爍 |
首先來實作初期上電時的配網狀態檢測處理,即上表中的<1>和<2>:
/* 第一步:定義配網相關處理標志 (tuya_hula_hoop_ble_proc.c/.h) */
FLAG_BIT g_ble_proc_flag;
#define F_BLE_BOUND g_ble_proc_flag.bit0
#define F_WAIT_BINDING g_ble_proc_flag.bit1
/* 第二步:撰寫初期配網狀態檢測處理函式 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 藍牙處理模塊初始化
* @param[in] none
* @return none
*/
VOID_T hula_hoop_ble_proc_init(VOID_T)
{
/* 使用TUYA BLE SDK提供的API獲取當前藍牙連接狀態 */
tuya_ble_connect_status_t ble_conn_sta;
ble_conn_sta = tuya_ble_connect_status_get();
TUYA_APP_LOG_DEBUG("ble connect status: %d", ble_conn_sta);
/* 判斷與標記 */
if ((ble_conn_sta == BONDING_UNCONN) ||
(ble_conn_sta == BONDING_CONN) ||
(ble_conn_sta == BONDING_UNAUTH_CONN)) {
F_BLE_BOUND = SET; /* 標記為[已系結] */
F_WAIT_BINDING = CLR; /* 標記為[禁止等待配網] */
} else {
F_BLE_BOUND = CLR; /* 標記為[未系結] */
F_WAIT_BINDING = SET; /* 標記為[允許等待配網] */
hula_hoop_disp_set_led_func(LED_FUNC_BIND); /* 設定LED功能為[配網指示] */
}
}
/* 第三步:在頭檔案中定義初始化介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_proc_init(VOID_T);
/* 第四步:在設備初始化函式tuya_hula_hoop_init()中呼叫 (tuya_hula_hoop.c) */
除上電時允許設備配網外,用戶還可以通過長按模式鍵5秒來打開配網功能,即上表中的<3>:
/* 第一步:撰寫允許配網處理函式 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 允許用戶系結設備
* @param[in] none
* @return none
*/
VOID_T hula_hoop_allow_binding(VOID_T)
{
if (F_BLE_BOUND) { /* [已系結]? */
tuya_ble_device_factory_reset(); /* 設備端解綁 */
}
F_WAIT_BINDING = SET; /* 標記為[允許等待配網] */
bls_ll_setAdvEnable(1); /* 打開藍牙廣播 */
hula_hoop_disp_set_led_func(LED_FUNC_BIND); /* 修改LED功能為[配網指示] */
}
/* 第二步:在頭檔案中定義介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_allow_binding(VOID_T);
/* 第三步:在模式鍵處理函式中呼叫 (tuya_hula_hoop_evt_user.c) */
/**
* @brief 模式鍵長按5秒處理函式
* @param[in] none
* @return none
*/
STATIC VOID_T __mode_key_longer_press_handler(VOID_T)
{
hula_hoop_allow_binding(); /* 允許用戶系結設備 */
}
配網功能開啟1分鐘后,如果仍未被系結則需要關閉配網功能,即上表中的<4>:
/* 第一步:撰寫禁止配網處理函式 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 禁止用戶系結設備
* @param[in] none
* @return none
*/
VOID_T hula_hoop_prohibit_binding(VOID_T)
{
F_WAIT_BINDING = CLR; /* 標記為[禁止等待配網] */
bls_ll_setAdvEnable(0); /* 關閉藍牙廣播 */
hula_hoop_disp_set_led_func(LED_FUNC_DATA); /* 修改LED功能為[資料指示] */
}
/* 第二步:在頭檔案中定義介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_prohibit_binding(VOID_T);
/* 第三步:在定時模塊中定義的1分鐘配網等待定時處理函式中呼叫 (tuya_hula_hoop_tiemr.c) */
/**
* @brief 等待配網定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __ble_wait_bind_timer(IN CONST UINT_T time_inc)
{
/* [已系結]或[禁止等待配網]時不處理 */
if ((F_BLE_BOUND == SET) || (F_WAIT_BINDING == CLR)) {
g_timer.ble_wait_bind = 0;
return;
}
/* 1分鐘配網等待定時 */
g_timer.ble_wait_bind += time_inc;
if (g_timer.ble_wait_bind >= BLE_WAIT_BIND_END_MS) {
g_timer.ble_wait_bind = 0;
hula_hoop_prohibit_binding(); /* 禁止用戶系結設備 */
}
}
最后是藍牙連接狀態改變時和被解綁時的處理,即上表中的<5>和<6>:
/* 第一步:撰寫藍牙連接狀態改變時和設備被解綁時的處理函式 */
/**
* @brief 藍牙連接狀態更新處理
* @param[in] status: 藍牙連接狀態
* @return none
*/
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) { /* 藍牙已連接? */
__report_all_dp_data(); /* 上報所有DP資料 */
tuya_ble_time_req(BLE_TIME_TYPE_NORMAL); /* 發送獲取云端時間請求 */
if (F_WAIT_BINDING == SET) { /* 之前未系結? */
F_BLE_BOUND = SET; /* 標記為[已系結] */
F_WAIT_BINDING = CLR; /* 標記為[禁止等待配網] */
hula_hoop_disp_set_led_func(LED_FUNC_DATA); /* 修改LED功能為[資料指示] */
}
if (hula_hoop_get_device_status() == STAT_USING) { /* [使用中]? */
hula_hoop_reset_timer_for_key_event(); /* 復位相關計時 */
}
}
}
/**
* @brief 設備被解綁處理
* @param[in] none
* @return none
*/
VOID_T hula_hoop_ble_unbound_handler(VOID_T)
{
F_BLE_BOUND = CLR; /* 標記為[未系結] */
bls_ll_setAdvEnable(0); /* 停止藍牙廣播 */
}
/* 第二步:在頭檔案中定義介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status);
VOID_T hula_hoop_ble_unbound_handler(VOID_T);
/* 第三步:在BLE SDK訊息處理callback函式中呼叫 (tuya_ble_app_demo.c) */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_CONNECTE_STATUS: /* 藍牙連接狀態改變時 */
hula_hoop_ble_conn_stat_handler(event->connect_status); /* 藍牙連接狀態更新處理 */
TUYA_APP_LOG_INFO("received tuya ble conncet status update event, current connect status = %d", event->connect_status);
break;
...
case TUYA_BLE_CB_EVT_UNBOUND: /* APP端解綁設備時 */
hula_hoop_ble_unbound_handler(); /* 設備被解綁處理 */
TUYA_APP_LOG_INFO("received unbound req");
break;
case TUYA_BLE_CB_EVT_ANOMALY_UNBOUND: /* 例外解綁時 */
hula_hoop_ble_unbound_handler(); /* 設備被解綁處理 */
TUYA_APP_LOG_INFO("received anomaly unbound req");
break;
...
}
-
本地資料上報
為了能讓用戶在APP上實時查看設備狀態,設備需在與APP連接時,將可上報的功能點資料上報至云端,根據呼啦圈的功能特點,本demo的上報機制設定如下:
| No. | 上報節點 | 上報功能點 | 備注 |
|---|---|---|---|
| 1 | 設備連接到APP時 | 所有功能點 | 初始化APP顯示 |
| 2 | 設備作業模式改變時 | 作業模式和實時類功能點 | 作業模式變更時會清除實時資料 |
| 3 | 設備狀態為轉動中時,每隔1分鐘 | 計時類功能點 | 即計時資料更新時上報 |
| 4 | 設備狀態為轉動中時,每隔5秒 | 圈數&卡路里類功能點 | 資料變化較頻繁,隔一段時間上報 |
代碼實作:
/* 第一步:對功能定義時設定的ID進行宏定義 */
#define DP_ID_MODE 101
#define DP_ID_TIME_REALTIME 102
#define DP_ID_COUNT_REALTIME 103
#define DP_ID_CALORIES_REALTIME 104
#define DP_ID_TIME_TOTAL_TODAY 105
#define DP_ID_COUNT_TOTAL_TODAY 106
#define DP_ID_CALORIES_TOTAL_TODAY 107
#define DP_ID_TIME_TOTAL_30DAYS 108
#define DP_ID_COUNT_TOTAL_30DAYS 109
#define DP_ID_CALORIES_TOTAL_30DAYS 110
#define DP_ID_TIME_TARGET_TODAY 111
#define DP_ID_TIME_TARGET_MONTH 112
#define DP_ID_TIME_REMAIN_TODAY 113
#define DP_ID_TIME_REMAIN_MONTH 114
/* 第二步:對DP資料存放時各項內容的偏移量進行宏定義 */
#define DP_DATA_INDEX_OFFSET_ID 0
#define DP_DATA_INDEX_OFFSET_TYPE 1
#define DP_DATA_INDEX_OFFSET_LEN 2
#define DP_DATA_INDEX_OFFSET_DATA 3
/* 第三步:定義用于存放DP資料的陣列 */
UCHAR_T dp_data_array[255+3];
/* 第四步:撰寫DP資料上報相關函式 */
/**
* @brief 上報1個DP資料
* @param[in] dp_id: DP ID
* @param[in] dp_type: DP型別
* @param[in] dp_len: DP資料長度
* @param[in] dp_data: DP資料存放地址
* @return none
*/
STATIC VOID_T __report_one_dp_data(IN CONST UCHAR_T dp_id, IN CONST UCHAR_T dp_type, IN CONST UCHAR_T dp_len, IN CONST UCHAR_T *dp_data)
{
UCHAR_T i;
sg_repo_array[DP_DATA_INDEX_OFFSET_ID] = dp_id;
sg_repo_array[DP_DATA_INDEX_OFFSET_TYPE] = dp_type;
sg_repo_array[DP_DATA_INDEX_OFFSET_LEN] = dp_len;
for (i = 0; i < dp_len; i++) {
sg_repo_array[DP_DATA_INDEX_OFFSET_DATA + i] = *(dp_data + (dp_len-i-1));
}
tuya_ble_dp_data_report(sg_repo_array, dp_len + 3);
}
/**
* @brief 添加1個DP資料
* @param[in] dp_id: DP ID
* @param[in] dp_type: DP型別
* @param[in] dp_len: DP資料長度
* @param[in] dp_data: DP資料存放地址
* @param[in] addr: DP資料添加到上報陣列中的位置
* @return 該DP資料總長度
*/
STATIC UCHAR_T __add_one_dp_data(IN CONST UCHAR_T dp_id, IN CONST UCHAR_T dp_type, IN CONST UCHAR_T dp_len, IN CONST UCHAR_T *dp_data, IN UCHAR_T *addr)
{
UCHAR_T i;
*(addr + DP_DATA_INDEX_OFFSET_ID) = dp_id;
*(addr + DP_DATA_INDEX_OFFSET_TYPE) = dp_type;
*(addr + DP_DATA_INDEX_OFFSET_LEN) = dp_len;
for (i = 0; i < dp_len; i++) {
*(addr + DP_DATA_INDEX_OFFSET_DATA + i) = *(dp_data + (dp_len-i-1));
}
return (dp_len + 3);
}
/**
* @brief 上報實時類DP資料
* @param[in] none
* @return none
*/
VOID_T __report_sport_data_realtime(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/**
* @brief 上報作業模式變更時的DP資料
* @param[in] none
* @return none
*/
VOID_T hula_hoop_report_mode(VOID_T)
{
__report_one_dp_data(DP_ID_MODE, DT_ENUM, SIZEOF(g_hula_hoop.mode), &g_hula_hoop.mode);
__report_sport_data_realtime();
}
/**
* @brief 上報計時類DP資料
* @param[in] none
* @return none
*/
VOID_T hula_hoop_report_sport_data1(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_total_today), (UCHAR_T *)&g_sport_data.time_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.time_total_30days), (UCHAR_T *)&g_sport_data.time_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/**
* @brief 上報圈數&卡路里類DP資料
* @param[in] none
* @return none
*/
VOID_T hula_hoop_report_sport_data2(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.count_total_today), (UCHAR_T *)&g_sport_data.count_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.calories_total_today), (UCHAR_T *)&g_sport_data.calories_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.count_total_30days), (UCHAR_T *)&g_sport_data.count_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.calories_total_30days), (UCHAR_T *)&g_sport_data.calories_total_30days, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/**
* @brief 上報所有DP資料
* @param[in] none
* @return none
*/
STATIC VOID_T __report_all_dp_data(VOID_T)
{
UCHAR_T total_len = 0;
total_len += __add_one_dp_data(DP_ID_MODE, DT_ENUM, SIZEOF(g_hula_hoop.mode), &g_hula_hoop.mode, sg_repo_array);
total_len += __add_one_dp_data(DP_ID_TIME_REALTIME, DT_VALUE, SIZEOF(g_sport_data.time_realtime), (UCHAR_T *)&g_sport_data.time_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_REALTIME, DT_VALUE, SIZEOF(g_sport_data.count_realtime), (UCHAR_T *)&g_sport_data.count_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_REALTIME, DT_VALUE, SIZEOF(g_sport_data.calories_realtime), (UCHAR_T *)&g_sport_data.calories_realtime, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_total_today), (UCHAR_T *)&g_sport_data.time_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.count_total_today), (UCHAR_T *)&g_sport_data.count_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_TODAY, DT_VALUE, SIZEOF(g_sport_data.calories_total_today), (UCHAR_T *)&g_sport_data.calories_total_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.time_total_30days), (UCHAR_T *)&g_sport_data.time_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_COUNT_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.count_total_30days), (UCHAR_T *)&g_sport_data.count_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_CALORIES_TOTAL_30DAYS, DT_VALUE, SIZEOF(g_sport_data.calories_total_30days), (UCHAR_T *)&g_sport_data.calories_total_30days, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today, (sg_repo_array + total_len));
total_len += __add_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month, (sg_repo_array + total_len));
tuya_ble_dp_data_report(sg_repo_array, total_len);
}
/* 第五步:在頭檔案中定義介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_report_mode(VOID_T);
VOID_T hula_hoop_report_sport_data1(VOID_T);
VOID_T hula_hoop_report_sport_data2(VOID_T);
/* 第六步:分別在各上報節點處呼叫 */
/* <1> 設備連接到APP時 (tuya_hula_hoop_ble_proc.c) */
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) { /* 藍牙已連接? */
__report_all_dp_data(); /* 上報所有DP資料 */
...
}
...
}
/* <2> 設備作業模式改變時 (tuya_hula_hoop_evt_user.c) */
STATIC VOID_T __mode_key_long_press_handler(VOID_T)
{
if (hula_hoop_get_disp_mode() != DISP_MODE_SELECT) {
return;
}
hula_hoop_switch_to_select_mode();
hula_hoop_report_mode(); /* 上報作業模式和實時資料 */
}
/* <3> 計時資料更新時 (tuya_hula_hoop_evt_timer.c) */
STATIC VOID_T __upd_time_data_timer(IN CONST UINT_T time_inc)
{
if (hula_hoop_get_device_status() != STAT_ROTATING) {
g_timer.upd_time_data = 0;
return;
}
g_timer.upd_time_data += time_inc;
if (g_timer.upd_time_data >= TIME_DATA_UPDATE_INTV_MS) {
g_timer.upd_time_data -= TIME_DATA_UPDATE_INTV_MS;
if (hula_hoop_update_sport_data_time()) {
hula_hoop_disp_target_finish();
}
hula_hoop_report_sport_data1(); /* 上報計時資料 */
}
}
/* <4> 上報定時處理中 (tuya_hula_hoop_evt_timer.c) */
STATIC VOID_T __repo_dp_data_timer(IN CONST UINT_T time_inc)
{
g_timer.repo_dp_data += time_inc;
if (g_timer.repo_dp_data >= DP_DATA_REPO_INTV_MS) {
g_timer.repo_dp_data -= DP_DATA_REPO_INTV_MS;
hula_hoop_report_sport_data2(); /* 上報圈數和卡路里資料 */
}
}
-
接收資料處理
當用戶操作APP設定可下發型別的功能點時,云端會將該功能點的ID、型別、屬性值、屬性值長度下發給設備,此時設備只要在檢測到云端下發資料事件后進行相應處理,即可實作云端任務,本demo可下發的功能點和接收到該功能點資料時需處理的內容如下:
| No. | 下發功能點 | 接收到資料后需處理的內容 | 備注 |
|---|---|---|---|
| 1 | 作業模式 | 更新設備作業模式,上報實時資料 | 作業模式變更時會清除實時資料 |
| 2 | 當日目標時長 | 更新設備當日目標時長,上報當日目標剩余時長 | 當日目標變更時會改變當日剩余 |
| 3 | 當月目標時長 | 更新設備當月目標時長,上報當月目標剩余時長 | 當月目標變更時會改變當月剩余 |
代碼實作:
/* 第一步:撰寫DP資料接收處理函式 */
/**
* @brief 呼啦圈DP資料接收處理
* @param[in] dp_data: DP資料存放陣列
* @return none
*/
VOID_T hula_hoop_ble_dp_write_handler(IN UCHAR_T *dp_data)
{
if (hula_hoop_get_device_status() == STAT_USING) {
hula_hoop_reset_timer_for_key_event();
}
switch (dp_data[0]) {
case DP_ID_MODE:
{
hula_hoop_set_work_mode(dp_data[3]);
__report_sport_data_realtime();
TUYA_APP_LOG_INFO("APP set the mode : %d.", dp_data[3]);
}
break;
case DP_ID_TIME_TARGET_TODAY:
{
hula_hoop_set_time_target_today(dp_data[6]);
__report_one_dp_data(DP_ID_TIME_REMAIN_TODAY, DT_VALUE, SIZEOF(g_sport_data.time_remain_today), (UCHAR_T *)&g_sport_data.time_remain_today);
TUYA_APP_LOG_INFO("APP set today's target : %dmin.", dp_data[6]);
}
break;
case DP_ID_TIME_TARGET_MONTH:
{
USHORT_T tar_tm = (((USHORT_T)dp_data[5]) << 8) | ((USHORT_T)dp_data[6]);
hula_hoop_set_time_target_month(tar_tm);
__report_one_dp_data(DP_ID_TIME_REMAIN_MONTH, DT_VALUE, SIZEOF(g_sport_data.time_remain_month), (UCHAR_T *)&g_sport_data.time_remain_month);
TUYA_APP_LOG_INFO("APP set this month's target : %dmin.", tar_tm);
}
break;
default:
break;
}
}
/* 第二步:在頭檔案中定義介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_dp_write_handler(IN UCHAR_T *dp_data);
/* 第三步:在BLE SDK訊息處理callback函式中呼叫 (tuya_ble_app_demo.c) */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_DP_WRITE: /* 在接收到DP資料時執行 */
dp_data_len = event->dp_write_data.data_len;
memset(dp_data_array, 0, sizeof(dp_data_array));
memcpy(dp_data_array, event->dp_write_data.p_data, dp_data_len);
hula_hoop_ble_dp_write_handler(dp_data_array); /* 呼啦圈DP資料處理 */
TUYA_APP_LOG_HEXDUMP_DEBUG("received dp write data :", dp_data_array, dp_data_len);
sn = 0;
tuya_ble_dp_data_report(dp_data_array, dp_data_len);
break;
...
}
-
云端時間獲取
BLE SDK提供獲取云端時間的功能,當設備連接APP時,可以通過獲取云端時間來校準本地時間,涂鴉BLE SDK提供兩種格式的時間獲取,分別為13位元組ms級字串格式和年月日時分秒星期格式時間,這里我們使用后者,
代碼實作:
/* 第一步:在設備連接時發起獲取云端時間請求 (tuya_hula_hoop_ble_proc.c) */
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) {
...
tuya_ble_time_req(BLE_TIME_TYPE_NORMAL);
...
}
...
}
/* 第二步:撰寫云端時間下發后的處理函式 (tuya_hula_hoop_ble_proc.c) */
/**
* @brief 云端時間處理
* @param[in] time_normal: 普通格式的云端時間
* @return none
*/
VOID_T hula_hoop_ble_time_normal_handler(IN CONST tuya_ble_time_noraml_data_t time_normal)
{
/* 型別轉換并覆寫本地時間 */
LOCAL_TIME_T time_now = {
.year = time_normal.nYear + 2000,/* noraml格式的年份為2位數形式,這里做簡單處理 */
.month = time_normal.nMonth,
.day = time_normal.nDay,
.hour = time_normal.nHour,
.minute = time_normal.nMin,
.second = time_normal.nSec
};
tuya_set_local_time(time_now, (time_normal.time_zone / 100));
/* 日期變更檢查處理 */
hula_hoop_check_date_change();
/* 本地時間更新日志列印 */
TUYA_APP_LOG_INFO("Local time has been updated to %04d.%02d.%02d %02d:%02d:%02d.",
g_local_time.year, g_local_time.month, g_local_time.day,
g_local_time.hour, g_local_time.minute, g_local_time.second);
}
/* 第三步:在頭檔案中定義介面 (tuya_hula_hoop_ble_proc.h) */
VOID_T hula_hoop_ble_time_normal_handler(IN CONST tuya_ble_time_noraml_data_t time_normal);
/* 第四步:在BLE SDK訊息處理callback函式中呼叫 (tuya_ble_app_demo.c) */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_TIME_NORMAL: /* 云端下發時間時 */
hula_hoop_ble_time_normal_handler(event->time_normal_data); /* 云端時間處理 */
break;
...
}
需要注意的是,云端下發的時間為零時區時間,同時云端會下發表示實際時區的資料,如800表示800/100=東八區,如果要取得本地時間,需對時區問題進行處理:(tuya_local_time.c/.h)
/**
* @brief 設定本地時間,需處理時區問題
* @param[in] time: 設定時間
* @param[in] time_zone: 時區
* @return none
*/
VOID_T tuya_set_local_time(IN CONST LOCAL_TIME_T time, IN CONST INT_T time_zone)
{
/* 更新本地時間 */
INT_T tmp_hour = time.hour;
tmp_hour += time_zone; /* 經過時區計算后的小時數 */
g_local_time = time;
/* 大于等于24時需做加一天處理 */
if (tmp_hour >= 24) {
g_local_time.hour = 0;
__local_time_update_per_day();
return;
}
/* 小于等于0時需做減一天處理 */
if (tmp_hour <= 0) {
g_local_time.hour = tmp_hour + 24;
if (g_local_time.day > 1) {
g_local_time.day--;
return;
}
if (g_local_time.month > 1) {
g_local_time.month--;
sg_day_tbl[1] = __local_time_leap_year_judgment(g_local_time.year);
g_local_time.day = sg_day_tbl[g_local_time.month-1];
} else {
g_local_time.year--;
g_local_time.month = 12;
g_local_time.day = sg_day_tbl[g_local_time.month-1];
}
return;
}
g_local_time.hour = tmp_hour;
}
/* 頭檔案中介面定義 */
VOID_T tuya_set_local_time(IN CONST LOCAL_TIME_T time, IN CONST INT_T time_zone);
4.優化處理
(1)關閉日志
由于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來關閉應用日志,以減少代碼空間,
(2)低功耗處理
由于呼啦圈使用電池供電,為避免電量消耗過快,需對設備進行低功耗處理,
BTU模組原廠芯片的電源管理模塊提供了3種低功耗作業模式:
| 作業模式 | 特點 | 喚醒方式 |
|---|---|---|
| 暫停模式 | MCU暫停,PM 模塊處于活動狀態,所有 SRAM 仍可訪問,RF 收發器、音頻和 USB 等模塊斷電,喚醒后從暫停處繼續執行 | PAD/32k Timer/RESET Pin |
| SRAM保留的深度睡眠模式 | PM 模塊處于活動狀態,除兩個 8KB 和一個 16KB 保留 SRAM 外,大多數模擬和所有數字模塊都掉電 | PAD/32k Timer/RESET Pin |
| 深度睡眠模式 | 只有 PM 模塊處于活動狀態,而包括保持 SRAM 在內的大多數模擬和所有數字模塊都處于斷電狀態 | PAD/32k Timer/RESET Pin |
對于BTU模組來說,這里的SRAM保留指的是 0x840000 - 0x847FFF 的存盤空間,目前協議堆疊和應用都是默認優先使用這塊空間,從編譯生成的**.lst檔案**中也可以看到相關資料都存盤在該范圍內,也就是說,如果使用SRAM保留的深度睡眠模式,可以保證資料在喚醒后能繼續使用,而不需要頻繁地進行閃存讀寫操作,
為了不影響呼啦圈的本地定時功能,這次選擇了SRAM保留的深度睡眠模式作為低功耗模式,基本處理思路如下:
<1> 在設備[未使用]時,設定喚醒方式為引腳喚醒或定時器喚醒,然后進入SRAM保留的深度睡眠模式;
<2> 當設備被引腳喚醒時:更新本地時間后,設備狀態切換到[使用中];
<3> 當設備被定時器喚醒時:更新本地時間后再次睡眠,
代碼實作:
- 睡眠前處理
在進入睡眠狀態之前,如果設備已連接APP,需在睡眠前主動斷開連接,否則喚醒后可能會無法連接APP,我們可以在設備切換為未使用狀態后,通過呼叫tuya_ble_gap_disconnect()介面實作斷連;需要注意的是,呼叫該API后,斷連狀態不會立即生效,所以要在收到藍牙狀態變更事件回呼后再執行睡眠動作,如果設備未連接APP,則不需要進行上述處理,直接設定設備狀態為睡眠狀態即可,
/* tuya_hula_hoop_evt_timer.c */
/**
* @brief 停用確認定時
* @param[in] time_inc: 時間增量
* @return none
*/
STATIC VOID_T __stop_using_timer(IN CONST UINT_T time_inc)
{
if ((hula_hoop_get_device_status() != STAT_USING) ||
(F_WAIT_BINDING == SET)) {
g_timer.stop_using = 0;
return;
}
g_timer.stop_using += time_inc;
if (g_timer.stop_using >= STOP_USING_CONFIRM_TIME_MS) {
g_timer.stop_using = 0;
hula_hoop_set_device_status(STAT_UNUSED); /* 設定設備狀態為[未使用] */
if (FALSE == hula_hoop_is_need_disconnect()) { /* 判斷是否需要進行主動斷連操作 */
hula_hoop_set_device_status(STAT_SLEEP); /* 設定設備狀態為[睡眠] */
}
}
}
/* tuya_hula_hoop_ble_proc.c */
/**
* @brief 是否需要進行主動斷連操作
* @param[in] none
* @return TRUE - 需要, FALSE - 不需要
*/
BOOL_T hula_hoop_is_need_disconnect(VOID_T)
{
bls_ll_setAdvEnable(0);
/* 已連接時處理 */
if (tuya_ble_connect_status_get() == BONDING_CONN) {
tuya_ble_gap_disconnect(); /* 斷開藍牙連接 */
return TRUE;
}
return FALSE;
}
/**
* @brief 藍牙連接狀態變更處理
* @param[in] status: 藍牙連接狀態
* @return none
*/
VOID_T hula_hoop_ble_conn_stat_handler(IN CONST tuya_ble_connect_status_t status)
{
if (hula_hoop_get_device_status() == STAT_UNUSED) { /* 設備狀態為[未使用]? */
if (status == BONDING_UNCONN){ /* 藍牙狀態為未連接 */
hula_hoop_set_device_status(STAT_SLEEP); /* 設定設備狀態為[睡眠] */
}
return;
}
...
}
- 進入SRAM保留的深度睡眠模式
接下來撰寫讓設備進入SRAM保留的深度睡眠模式的相關代碼,我們把復位鍵引腳作為喚醒引腳使用,SDK的組態檔中已經定義了相關的宏,如有需要也可以同時設定多個引腳作為喚醒引腳,
/* app_config.h */
#define BLE_MODULE_PM_ENABLE 1
#define PM_DEEPSLEEP_RETENTION_ENABLE 1 /* SRAM保留睡眠模式相關代碼塊,默認是打開的 */
#define GPIO_WAKEUP_MODULE GPIO_PB1/* 修改喚醒引腳,下面4行宏名對應修改 */
#define PB1_FUNC AS_GPIO
#define PB1_INPUT_ENABLE 1
#define PB1_OUTPUT_ENABLE 0
#define PB1_DATA_OUT 0
#define GPIO_WAKEUP_MODULE_HIGH gpio_setup_up_down_resistor(GPIO_WAKEUP_MODULE, PM_PIN_PULLUP_10K);
#define GPIO_WAKEUP_MODULE_LOW gpio_setup_up_down_resistor(GPIO_WAKEUP_MODULE, PM_PIN_PULLDOWN_100K);
/* app.c */
void main_loop(void)
{
...
if(1) { /* ← 將低功耗處理代碼段打開 */
if((tuya_get_ota_status() == TUYA_OTA_STATUS_NONE)&&(ty_factory_flag==0)&&(ble_tx_is_busy()!=1)) {
app_power_management();
}
}
}
void app_power_management ()
{
...
if (app_module_busy()) {
return;
}
/* 這兩句用于實作連接時的低功耗,需要回圈呼叫,由于會影響液晶屏顯示,這次不使用 */
//bls_pm_setSuspendMask (SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);
//bls_pm_setWakeupSource(PM_WAKEUP_PAD);
/* 進入睡眠模式的處理函式 */
hula_hoop_set_device_sleep();
}
/* tuya_hula_hoop_svc_basic.c */
VOID_T hula_hoop_set_device_sleep(VOID_T)
{
/* 由于該函式在回圈中呼叫,要設定限制條件 */
if (hula_hoop_get_device_status() != STAT_SLEEP) {
return;
}
/* 保存當前時間 */
sg_sys_time = clock_time();
/* 設定喚醒引腳、喚醒定時時間并進入SRAM保留的低功耗模式 */
GPIO_WAKEUP_MODULE_HIGH;
cpu_set_gpio_wakeup(GPIO_WAKEUP_MODULE, Level_Low, 1);
cpu_sleep_wakeup(DEEPSLEEP_MODE_RET_SRAM_LOW32K, PM_WAKEUP_PAD|PM_WAKEUP_TIMER, clock_time()+SLEEP_TIME_SEC*CLOCK_16M_SYS_TIMER_CLK_1S);
}
/* SDK默認使用的喚醒引腳是高電平有效的,所以有關喚醒引腳極性的地方要稍做修改 */
int app_module_busy()
{
mcu_uart_working = gpio_read(GPIO_WAKEUP_MODULE);
module_uart_working = UART_TX_BUSY || UART_RX_BUSY;
//module_task_busy = mcu_uart_working || module_uart_working; /* 修改前 */
module_task_busy = (!mcu_uart_working) || module_uart_working; /* 修改后 */
return module_task_busy;
}
void user_init_normal(void)
{
...
/* 修改前 */
//cpu_set_gpio_wakeup (GPIO_WAKEUP_MODULE, Level_High, 1);
//GPIO_WAKEUP_MODULE_LOW;
/* 修改后 */
cpu_set_gpio_wakeup (GPIO_WAKEUP_MODULE, Level_Low, 1);
GPIO_WAKEUP_MODULE_HIGH;
...
}
- 喚醒后處理
設備喚醒后會從main()函式開始執行,然后通過判斷是否從SRAM保留的睡眠模式喚醒來進入對應的初始化函式user_init_deepRetn(),我們在這里加入呼啦圈的處理函式:
/* main.c */
_attribute_ram_code_ int main(void)
{
...
int deepRetWakeUp = pm_is_MCU_deepRetentionWakeup();/* 是否從SRAM保留的睡眠模式喚醒 */
...
gpio_init( !deepRetWakeUp );/* 如果是從SRAM保留的睡眠模式喚醒,gpio相關模擬暫存器無需初始化 */
...
if ( deepRetWakeUp ) {
user_init_deepRetn(); /* 從SRAM保留的睡眠模式喚醒時的初始化函式 */
} else {
user_init_normal(); /* 上電/復位后的初始化函式 */
}
...
}
/* app.c */
_attribute_ram_code_ void user_init_deepRetn(void)
{
tuya_ble_app_init_deepRetn();
...
}
/* tuya_ble_app_demo.c */
void tuya_ble_app_init_deepRetn(void)
{
tuya_software_timer_init();
tuya_hula_hoop_init_deepRetn();
}
/* tuya_hula_hoop.c */
VOID_T tuya_hula_hoop_init_deepRetn(VOID_T)
{
hula_hoop_device_wakeup_handler(); /* 喚醒后根據喚醒方式進行處理 */
if (hula_hoop_get_device_status() >= STAT_UNUSED) { /* 如果設備狀態沒有變化則不進行下面的初始化作業 */
return;
}
hula_hoop_key_hall_init_deepRetn(); /* 按鍵引腳、霍爾傳感器引腳的初始化 */
hula_hoop_disp_proc_init_deepRetn(); /* 指示燈引腳、段碼液晶屏引腳/定時器的初始化 */
hula_hoop_ble_proc_init_deepRetn(); /* 睡眠前如果是已系結則重新打開藍牙廣播 */
hula_hoop_timer_reset(); /* 定時器復位 */
}
其中,hula_hoop_device_wakeup_handler()是喚醒后的處理函式,用于判斷設備是被何種方式喚醒并進行相應的處理,如果是被引腳喚醒,則說明呼啦圈被用戶使用,所以需要切換到作業狀態,切換前還要計算睡眠經過的時間,用來更新本地時間;如果是定時喚醒,則根據設定的睡眠時間更新本地時間即可,此時設備會繼續保持STAT_SLEEP狀態,進入主回圈后設備會再次睡眠,
/* tuya_hula_hoop_svc_basic.c */
/**
* @brief 更新本地時間,并檢查日期是否變更
* @param[in] time_diff_sec: 時間差,單位“秒”
* @return none
*/
VOID_T __update_local_time(IN UCHAR_T time_diff_sec)
{
while(time_diff_sec--) {
tuya_local_time_update();
}
hula_hoop_check_date_change();
}
/**
* @brief 切換設備到作業狀態
* @param[in] none
* @return none
*/
VOID_T __set_device_work(VOID_T)
{
/* 本地時間處理,計算時間差,四舍五入 */
UINT_T time_diff = clock_time() - sg_sys_time;
if ((time_diff % CLOCK_16M_SYS_TIMER_CLK_1S) >= (CLOCK_16M_SYS_TIMER_CLK_1S / 2)) {
time_diff = time_diff / CLOCK_16M_SYS_TIMER_CLK_1S + 1;
} else {
time_diff = time_diff / CLOCK_16M_SYS_TIMER_CLK_1S;
}
__update_local_time(time_diff);
TUYA_APP_LOG_INFO("Time difference: %ds.", time_diff);
TUYA_APP_LOG_INFO("Local time has been updated to %04d.%02d.%02d %02d:%02d:%02d.\n",
g_local_time.year, g_local_time.month, g_local_time.day,
g_local_time.hour, g_local_time.minute, g_local_time.second);
/* 設定狀態為使用中,同時設定默認作業模式 */
__set_device_status(STAT_USING);
__set_work_mode(MODE_NORMAL);
}
/**
* @brief 喚醒后處理函式
* @param[in] none
* @return none
*/
VOID_T hula_hoop_device_wakeup_handler(VOID_T)
{
if (pm_get_wakeup_src() == (WAKEUP_STATUS_PAD | WAKEUP_STATUS_CORE)) {
/* 引腳喚醒時處理 */
__set_device_work();
} else {
/* 定時喚醒時處理 */
__update_local_time(SLEEP_TIME_SEC);
}
}
經上述處理后,呼啦圈正常運行時電流平均值約為5.6uA,睡眠時約為0.157uA,
以上主要是對智能計數、運動模式切換以及運動歷史資料記錄開發方案介紹,方案不僅僅針對呼啦圈,如果同學們感興趣可以試試開發智能跳繩,智能家用跑步機等小型運動健康類產品,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/332167.html
標籤:其他
