直流变频智能风扇原型开发

更新时间Invalid date

概况

风扇由于便捷、易安装、能耗低的特性,在消费品市场非但没有受空调的影响,反而新增了远程控制、自然风、暖风、清凉风、睡眠风、负离子功能等新型产品,甚至还有驱蚊、低噪风扇,具有非常高的实用价值。

随着消费者对健康的日益关注,产品设计还可以在提高空气质量、便携移动、低噪声等方向进行新品研发。

本教程主要介绍目前市场上流行 直流变频风扇 的设计思路,通过方案设计、硬件设计、涂鸦 IoT 平台 DP 设置、嵌入式开发,来讲解普通风扇智能化的研发流程。如果你也感兴趣,可以一起动手操作,制作出一款原型产品,了解物联网硬件产品的设备端开发。

功能需求

功能设计 详细说明
工作模式
  • 正常风;
  • 自然风:忽大忽小间隔 5 秒;
  • 睡眠风:每隔一小时自动降档,最后降到最低档。
风速控制 编码器旋转控制风速,顺时针旋转风速+,逆时针旋转风速- 。
模式切换 编码器按钮短按切换模式,长按设备复位。
本地定时 定时结束后自动关机,定时按键:无定时 -> 1 小时 -> 2 小时 -> 3 小时 -> 4 小时 -> 无定时。
指示灯显示 1         4 颗指示灯显示风速,4 颗指示灯指示 8 档风速,闪烁代表 1 档,常亮代表 2 档。4 颗指示灯还复用本地定时指示。
指示灯显示 2 除 4 颗风速指示灯外,还有 4 颗指示灯。一颗 Wi-Fi 指示灯,指示 Wi-Fi 状态;其他三颗指示当前风扇工作模式。
电源 电源按键,控制风扇的启动和关闭。
设备配网 长按 Wi-Fi 按键,设备进入配网模式。
指示灯调节 指示灯亮度可通过 App 调整,正常亮度或较暗亮度。
断电记忆 设备通电自动恢复。

硬件框图

步骤

  • 第 1 步:硬件方案设计

    方案选型

    直流电机

    相比传统交流电机风扇,无刷直流电机(BLDC,Brushless DC electric motor)具有中低速转矩性能好等多种优势,且使用寿命更长。因此直流电机选用的是 MINEBEA 12V 直流三相无刷电机,作为风量控制电机。

    三相无刷直流电机的电流流向

    • 红色流向:电流从 AH 高位 MOS 流入电机的 MOT A,再从 MOT B 流出,经过 BL 低位 MOS 回到电源负端。
    • 蓝色流向:电流从 BH 高位 MOS 流入电机的 MOT B,再从 MOT C 流出,经过 CL 低位 MOS 回到电源负端。
    • 绿色流向:电流从 CH 高位 MOS 流入电机的 MOT C,再从 MOT A 流出,经过 AL 低位 MOS 回到电源负端。

    电机驱动器

    驱动器主要控制直流无刷电动机(Brushless DC electric motor,BLDC)。本案例采用峰岹的 BLDC 解决方案,BLDC 的控制 MCU 使用了 FU6832—双核:8051 内核+ME,16KB flash,256 bytes IRAM, 768 bytes XRAM SSOP24 封装,集成 BLDC 控制算法。

    FU6832S 芯片有 6 个指定引脚 H_PU、H_PV、H_PW、L_U、L_V、L_W 用于控制三相直流无刷电机,内置上拉电阻和下拉电阻,高电平最大值是 VCC 电压。

    只要将芯片供电的输入电压和电机工作电压保持相同,就不需要再在这些引脚的外围增加电平转换电路,可直接用 3 颗驱动芯片 AO4606 驱动电机的 U、V、W 三个引脚,该芯片由一个 Nmos 和 Pmos 组成,通过通过引脚走线可设计成半桥驱动电路。

    FU6832S 芯片内置 1 个可配置增益的独立运算放大器,外围增加取样电阻,便可用于实现电机过流检测保护。

    电机正反转监测电路,通过电阻分压后,进入 FU6832S 芯片 14、15、16 脚。

    因为 FU6832S 芯片数字电路引脚的电平是 5V,Wi-Fi 模块的引脚电平是 3.3V,因此两个通信间需要增加电平转换电路。

    通信模组

    Wi-Fi 模组选择涂鸦 SoC 方案模组——WBRU;它由一个高集成度的无线射频芯片 W701-VA2-CG 构成,内置了 Wi-Fi 网络协议栈和丰富的库函数。WBRU 还包含低功耗的 KM4 MCU,WLAN MAC,1T1R WLAN,最高主频 100MHz,内置 256K SRAM ,芯片内置 2Mbyte flash 和丰富的外设资源。WBRU 是一个 RTOS 平台,集成了所有 Wi-Fi MAC 以及 TCP/IP 协议的函数库。用户可以基于这些开发满足自己需求的嵌入式 Wi-Fi 产品。更多详情,请参考 WBRU 模块规格书

    旋钮编码器

    风量调节方案采用旋钮编码器作为风量调节旋钮,顺时针调大,逆时针调小,该编码器可根据要求,设置需要的档位数。该器件手柄支持 360°选择,结构上无限位点。

    • 编码器的脉冲波形:
    • 编码器的外围电路:

    按键设计

    方案设计 4 个按键,按键均设置为低电平有效,按键功能:

    • 电源按键:开启关闭风扇。
    • 定时按键:设置风扇定时开/关的时间段。
    • Wi-Fi 配网重置按键:清除已联网的 Wi-Fi 的网络信息,恢复到出厂设置状态。
    • 模式按键:设置风扇工作模式。模式按键在旋钮编码器上。

    指示灯设计

    方案设计 8 个指示灯,指示灯由 CBU Wi-Fi 模组的 I/O 口控制,设置均为低电平点亮,指示灯功能:

    • 4 个档位指示灯
    • 3 个模式指示灯
    • 1 个网络状态指示灯
    • 增加一个指示灯亮度调节控制 I/O 口,可设置调节 LED 指示灯亮度

    img

    交互信号

    采用 PWM 信号作为 Wi-Fi 模组和 FU6832S 电机驱动控制芯片的交互信号。

    电源方案

    • 电源插头:选用绿源电源插头,输出 12V DC/2A,用于满足风扇整体供电要求。

    • 转换电路:

      此次教程需要一个 12V DC 转 3.3V DC 的转换电路,用于 Wi-Fi 模组及外围供电。一个 12V DC 转 5V DC 的转换电路,用于 FU6832S 电机驱动控制芯片及外围供电。

      转换电路采用基于矽力杰的 SY8121B 芯片的降压(Buck)电路。SY8121B 是一款高效,响应速度快的同步整流方案的 DC-DC 转换芯片。

      • 输入电压范围:4.5V~18V
      • 输出电流:2A(max)

    原理图(点击下载

    • Wi-Fi 模组及外围电路
    • DC-DC 转换电路
    • 电机控制芯片(FU6832S)及外围电路

    img

    整机实物图

    • 电机驱动板和电压转换板:

    • Wi-Fi 模组控制板:

    • 操作界面:

  • 第 2 步:在涂鸦 IoT 平台上创建产品

    产品创建

    1. 首先进入 涂鸦 IoT 平台,创建一个风扇产品。通过点击**“创建产品”->“小家电”->“风扇”进行创建,填入“产品名称”,选择通讯协议为“WiFi+蓝牙”,然后点击下方的“创建产品”**即可完成产品创建。

    2. 产品创建后,进入到开发界面,此时会弹出功能选择界面,可以直接选择需要用到的的风扇标准功能。

      如果标准功能中没有合适的选项,也可以选择自定义功能。本案例选择了风扇标准功能中的 4 项功能:“开关”“工作模式”“风速””倒计时“,并对功能点属性进行相应修改,另外还自定义了 1 项功能:”灯光亮度“,最终 DP 点定义如下图所示。

    3. 完成功能定义后,我们还需要设置设备面板,可以根据喜好选择一种面板进行编辑,编辑完成后可以按照提示用“涂鸦智能 App”扫码体验手机控制。

    4. 最后进入硬件开发阶段。我们选择 “涂鸦标准模组 SDK 开发”“WBR3 Wi-Fi & Bluetooth 模组”

      说明:后续烧录授权时可以点击下图中的 “免费领取 10 个激活码” 进行烧录凭证的申请。

    开发环境搭建

    本案例使用涂鸦 WiFi&BLE SDK 和 WBRU 云模组进行开发。

    • SDK 获取

      在涂鸦 IoT 平台的产品创建流程中,进入 “硬件开发” 界面左下角,找到 “开发资料” 后,下载 SDK。

    更多详情和步骤说明,请参考 通用 Wi-Fi SDK DemoWBR 系列模组烧录授权

  • 第 3 步:软件方案设计

    前言

    传统的电风扇一般有风速挡位调节、定时关机等功能,本案例在此基础上,提供了三种工作模式可选,并使用涂鸦 WiFi&BLE SDK 和 WiFi&BLE 模组实现了对电风扇的远程控制,包括开/关控制、工作模式设定、风速设定、定时关机设定和灯光亮度调节功能,另外还增加了断电记忆功能,使电风扇更加智能化、人性化。

    完整 Demo 可在 tuya-iotos-embeded-demo-wifi-ble-smart-fan-wbru 中获取。

    外设驱动

    按键检测

    本案例提供 4 个按键,其中包括了旋钮编码器上的按键,低电平有效,其对应的引脚和功能设定如下:

    名称 网络标号 引脚 功能
    电源键 power PA7 轻触,切换设备开/关
    定时键 timer PA8 轻触,设置定时时间
    配网键 wifi PA19 长按 10s,重置设备配网状态
    模式键(旋钮编码器-N) mode PA17 轻触,切换工作模式;长按 3s,重置设备状态

    涂鸦 WiFi&BLE SDK 提供了按键检测组件,因此我们只需要调用 tuya_key.h 中提供的接口进行注册即可:

    /* 按键端口 */
    #define P_KEY_POWER                 TY_GPIOA_7
    #define P_KEY_TIMER                 TY_GPIOA_8
    #define P_KEY_WIFI                  TY_GPIOA_19
    #define P_KEY_ROTARY_N              TY_GPIOA_17
    /* 按键配置 */
    #define KEY_SEQ_PRESS_TIME_MS       400     /* 连按时间: 400ms */
    #define KEY_MODE_LONG_PRESS_TIME_MS 10000   /* 模式键(复位)长按时间: 10s */
    #define KEY_WIFI_LONG_PRESS_TIME_MS 3000    /* 配网键长按时间: 3s */
    #define KEY_LONG_PRESS_TIME_MS      10000   /* 默认长按时间: 10s */
    
    /**
     * @brief   按键事件回调函数
     * @param   port: 按键端口
     * @param   type: 按键触发类型
     * @param   cnt: 连击次数
     * @return  none
     */
    STATIC VOID_T fan_key_press_cb(IN CONST TY_GPIO_PORT_E port, IN CONST PUSH_KEY_TYPE_E type, IN CONST INT_T cnt)
    {
        PR_DEBUG("port: %d, type: %d, cnt: %d", port, type, cnt);
    
        switch (port) {
        case P_KEY_POWER:
            fan_key_power_handler(type, cnt);
            g_fan_mag.timing_display_flag = FALSE;
            break;
        case P_KEY_TIMER:
            fan_key_timer_handler(type, cnt);
            break;
        case P_KEY_WIFI:
            fan_key_wifi_handler(type, cnt);
            g_fan_mag.timing_display_flag = FALSE;
            break;
        case P_KEY_ROTARY_N:
            fan_key_mode_handler(type, cnt);
            g_fan_mag.timing_display_flag = FALSE;
            break;
        default:
            break;
        }
    }
    
    /**
     * @brief   按键初始化
     * @param   none
     * @return  none
     */
    VOID_T fan_key_init(VOID_T)
    {
        OPERATE_RET opRet;
        /* 初始化 */
        opRet = key_init(NULL, 0, 0);
        if (opRet != OPRT_OK) {
            PR_ERR("key_init err:%d", opRet);
            return;
        }
        /* 用户定义初始化 */
        memset(&g_key_def, 0, SIZEOF(KEY_USER_DEF_S));
        g_key_def.low_level_detect = TRUE;
        g_key_def.lp_tp = LP_ONCE_TRIG;
        g_key_def.seq_key_detect_time = KEY_SEQ_PRESS_TIME_MS;
        g_key_def.call_back = fan_key_press_cb;
        /* 开关键 */
        g_key_def.port = P_KEY_POWER;
        g_key_def.long_key_time = KEY_LONG_PRESS_TIME_MS;
        opRet = reg_proc_key(&g_key_def);
        if (opRet != OPRT_OK) {
            PR_ERR("reg_proc_key err:%d", opRet);
            return;
        }
        /* 定时键 */
        g_key_def.port = P_KEY_TIMER;
        g_key_def.long_key_time = KEY_LONG_PRESS_TIME_MS;
        opRet = reg_proc_key(&g_key_def);
        if (opRet != OPRT_OK) {
            PR_ERR("reg_proc_key err:%d", opRet);
            return;
        }
        /* 配网键 */
        g_key_def.port = P_KEY_WIFI;
        g_key_def.long_key_time = KEY_WIFI_LONG_PRESS_TIME_MS;
        opRet = reg_proc_key(&g_key_def);
        if (opRet != OPRT_OK) {
            PR_ERR("reg_proc_key err:%d", opRet);
            return;
        }
        /* 模式键/复位键 */
        g_key_def.port = P_KEY_ROTARY_N;
        g_key_def.long_key_time = KEY_MODE_LONG_PRESS_TIME_MS;
        opRet = reg_proc_key(&g_key_def);
        if (opRet != OPRT_OK) {
            PR_ERR("reg_proc_key err:%d", opRet);
            return;
        }
    }
    

    旋钮检测

    本案例提供 1 个旋钮编码器用于调节风速挡位,其对应的引脚和功能设定如下:

    名称 网络标号 引脚 功能
    旋钮编码器-A(A-C 端子间) wind_down PA18 用于旋钮转向检测
    旋钮编码器-B(B-C 端子间) wind_up PA0 用于旋钮转向检测

    为方便后续程序扩展,编写了专用于旋钮编码器检测的组件,具体可参考driver\tuya_rotary.c

    旋钮编码器注册和回调函数实现如下:

    /* Pin */
    #define P_ROTARY_A  TY_GPIOA_18   /* 旋钮编码器-A */
    #define P_ROTARY_B  TY_GPIOA_0    /* 旋钮编码器-B */
    
    VOID_T fan_rotary_cb(BOOL_T dir);
    
    ROTARY_USER_DEF_S g_rotary_def = {
        .port_a = P_ROTARY_A,
        .port_b = P_ROTARY_B,
        .call_back = fan_rotary_cb
    };
    
    /**
     * @brief   旋钮编码器回调函数
     * @param   dir: 旋转方向 TRUE-顺时针 FALSE-逆时针
     * @return  none
     */
    VOID_T fan_rotary_cb(BOOL_T dir)
    {
        if (dir) {
            PR_NOTICE("The direction of rotation is CW.");
        } else {
            PR_NOTICE("The direction of rotation is CCW.");
        }
    }
    
    /**
     * @brief   旋钮编码器初始化
     * @param   none
     * @return  none
     */
    VOID_T fan_rotary_init(VOID_T)
    {
        OPERATE_RET opRet;
        /* 初始化 */
        opRet = rotary_init(NULL, 0);
        if (opRet != OPRT_OK) {
            PR_ERR("rotary_init err:%d", opRet);
            return;
        }
        opRet = reg_proc_rotary(&g_rotary_def);
        if (opRet != OPRT_OK) {
            PR_ERR("reg_proc_rotary err:%d", opRet);
        }
    }
    

    指示灯控制

    本案例提供 8 个指示灯用于设备状态指示,低电平有效,其对应引脚和功能设定如下:

    名称 网络标号 引脚 功能
    挡位指示灯-1 D1 PA9 挡位≥1,常亮;挡位≥2,闪烁;
    定时≥1h,常亮;定时=无定时,常灭;
    挡位指示灯-2 D2 PA2 挡位≥3,常亮;挡位≥4,闪烁;
    定时≥2h,常亮;定时=无定时,常灭;
    挡位指示灯-3 D3 PA3 挡位≥5,常亮;挡位≥6,闪烁;
    定时≥3h,常亮;定时=无定时,常灭;
    挡位指示灯-4 D4 PA4 挡位≥7,常亮;挡位≥8,闪烁;
    定时=4h,常亮;定时=无定时,常灭;
    模式指示灯-直吹风 mode_w TX 直吹风模式常亮;其他模式常灭;
    模式指示灯-自然风 mode_n PA10 自然风模式常亮;其他模式常灭;
    模式指示灯-睡眠风 mode_s PA20 睡眠风模式常亮;其他模式常灭;
    配网状态指示灯 net_led RX 指示配网状态

    本案例还可以对指示灯亮度进行调节,这里采用 PWM 控制方式,对应引脚和功能设定如下:

    名称 网络标号 引脚 功能
    指示灯亮度调节 dimmer PA12 PWM 方式调节亮度,可设置正常亮度和较暗亮度

    下面开始编码实现指示灯相关功能,首先初始化指示灯和指示灯亮度调节端口,这里指示灯注册调用的是 SDK 中tuya_led.h提供的接口:

    /* LED 端口 */
    STATIC UCHAR_T sg_fan_led_port[] = {
        TY_GPIOA_9,     /* D1 */
        TY_GPIOA_2,     /* D2 */
        TY_GPIOA_3,     /* D3 */
        TY_GPIOA_4,     /* D4 */
        TY_GPIOA_14,    /* MODE_WIND */
        TY_GPIOA_10,    /* MODE_NATURE */
        TY_GPIOA_20,    /* MODE_SLEEP */
        TY_GPIOA_13     /* WIFI */
    };
    
    /* LED 句柄 */
    #define LED_HANDLE_D1               0
    #define LED_HANDLE_D2               1
    #define LED_HANDLE_D3               2
    #define LED_HANDLE_D4               3
    #define LED_HANDLE_MODE_WIND        4
    #define LED_HANDLE_MODE_NATURE      5
    #define LED_HANDLE_MODE_SLEEP       6
    #define LED_HANDLE_WIFI             7
    LED_HANDLE fan_led_handle[(SIZEOF(sg_fan_led_port) / SIZEOF(sg_fan_led_port[0]))];
    
    /* LED 亮度调节 PWM */
    #define P_LED_DIMMER                TY_GPIOA_12
    #define LED_DIMMER_PWM_PERIOD       1000    /* PWM 周期: 1ms(1kHz) */
    #define LED_DIMMER_PWM_DUTY_NORMAL  900     /* PWM 占空比: 90%(正常亮度) */
    #define LED_DIMMER_PWM_DUTY_DARKER  100     /* PWM 占空比: 10%(较暗亮度) */
    pwmout_t g_led_dimmer_pwmout;
    
    /**
     * @brief   指示灯亮度调节 PWM 初始化
     * @param   none
     * @return  none
     */
    STATIC VOID_T fan_led_dimmer_init(VOID_T)
    {
        pwmout_init(&g_led_dimmer_pwmout, P_LED_DIMMER);
        pwmout_period_us(&g_led_dimmer_pwmout, LED_DIMMER_PWM_PERIOD);
        pwmout_pulsewidth_us(&g_led_dimmer_pwmout, (LED_DIMMER_PWM_PERIOD - LED_DIMMER_PWM_DUTY_NORMAL));
    }
    
    /**
     * @brief   指示灯初始化
     * @param   none
     * @return  none
     */
    VOID_T fan_led_init(VOID_T)
    {
        INT_T i;
        OPERATE_RET op_ret = OPRT_OK;
        /* 指示灯初始化 */
        for (i = 0; i < (SIZEOF(sg_fan_led_port) / SIZEOF(sg_fan_led_port[0])); i++) {
            op_ret = tuya_create_led_handle(sg_fan_led_port[i], FALSE, &fan_led_handle[i]);
            if (op_ret != OPRT_OK) {
                PR_ERR("led init err:%d", op_ret);
                return;
            }
            tuya_set_led_light_type(fan_led_handle[i], OL_HIGH, 0, 0xffff); /* 关闭 LED */
        }
        /* 指示灯亮度初始化 */
        fan_led_dimmer_init();
    }
    

    接下来进行各指示灯指示功能的实现,首先是模式指示灯,根据当前工作模式点亮对应的指示灯:

    /**
     * @brief   设置模式指示灯
     * @param   cur_mode: 当前工作模式
     * @return  none
     */
    VOID_T fan_set_mode_led(IN CONST UCHAR_T cur_mode)
    {
        /* 关闭所有模式指示灯 */
        tuya_set_led_light_type(fan_led_handle[LED_HANDLE_MODE_WIND], OL_HIGH, 0, 0xffff);
        tuya_set_led_light_type(fan_led_handle[LED_HANDLE_MODE_NATURE], OL_HIGH, 0, 0xffff);
        tuya_set_led_light_type(fan_led_handle[LED_HANDLE_MODE_SLEEP], OL_HIGH, 0, 0xffff);
        /* 根据当前模式打开对应的指示灯 */
        if (cur_mode == FAN_MODE_NORMAL) {          /* 直吹风 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_MODE_WIND], OL_LOW, 0, 0xffff);
        } else if (cur_mode == FAN_MODE_NATURE) {   /* 自然风 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_MODE_NATURE], OL_LOW, 0, 0xffff);
        } else {                                    /* 睡眠风 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_MODE_SLEEP], OL_LOW, 0, 0xffff);
        }
    }
    

    挡位指示灯同时作为挡位指示和定时指示,所以增加一个标志来区分当前指示的内容;挡位指示根据当前挡位确定点亮指示灯的个数和点亮模式,定时指示根据定时时间确定指示灯点亮个数:

    #define LED_GEAR_NUM                4       /* 挡位指示灯数量 */ 
    
    /**
     * @brief   设置挡位指示灯
     * @param   cur_gear: 当前挡位
     * @return  none
     */
    VOID_T fan_set_gear_led(IN CONST UCHAR_T cur_gear)
    {
        UCHAR_T i;
    
        if (g_fan_mag.timing_display_flag) {
            return;
        }
    
        for (i = 0; i < LED_GEAR_NUM; i++) {
            if (i == (cur_gear / 2)) {
                if (cur_gear % 2) {
                    /* LED 闪烁 */
                    tuya_set_led_light_type(fan_led_handle[i], OL_FLASH_HIGH, 0, 0xffff);
                } else {
                    /* LED 常亮 */
                    tuya_set_led_light_type(fan_led_handle[i], OL_LOW, 0, 0xffff);
                }
            } else if (i < (cur_gear/2)) {
                /* LED 常亮 */
                tuya_set_led_light_type(fan_led_handle[i], OL_LOW, 0, 0xffff);
            } else {
                /* LED 常灭 */
                tuya_set_led_light_type(fan_led_handle[i], OL_HIGH, 0, 0xffff);
            }
        }
    }
    
    /**
     * @brief   设置定时指示灯(与挡位指示灯共用)
     * @param   hour: 定时时间
     * @return  none
     */
    VOID_T fan_set_timing_led(IN CONST UCHAR_T hour)
    {
        UCHAR_T i;
    
        if (!g_fan_mag.timing_display_flag) {
            return;
        }
    
        /* 无定时,关闭所有指示灯 */
        if (hour > LED_GEAR_NUM) {
            for (i = 0; i < LED_GEAR_NUM; i++) {
                tuya_set_led_light_type(fan_led_handle[i], OL_HIGH, 0, 0xffff);
            }
            return;
        }
        /* 点亮对应指示灯 */
        for (i = 0; i < LED_GEAR_NUM; i++) {
            if (i < hour) {
                tuya_set_led_light_type(fan_led_handle[i], OL_LOW, 0, 0xffff);
            } else {
                tuya_set_led_light_type(fan_led_handle[i], OL_HIGH, 0, 0xffff);
            }
        }
    }
    

    最后是配网指示灯,根据当前配网状态确定指示灯状态:

    #define LED_WIFI_FAST_FLASH_MS      300     /* 快闪 300ms */ 
    #define LED_WIFI_SLOW_FLASH_MS      500     /* 慢闪 500ms */ 
    
    /**
     * @brief   设置配网指示灯
     * @param   cur_stat: 当前 wifi 状态
     * @return  none
     */
    VOID_T fan_set_wifi_led(IN CONST GW_WIFI_NW_STAT_E cur_stat)
    {
        switch (cur_stat) {
        case STAT_LOW_POWER:    /* wifi 连接超时,进入低功耗模式 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_WIFI], OL_HIGH, 0, 0);/* 关闭指示灯 */
            break;
        case STAT_UNPROVISION:  /* SamrtConfig 配网模式,等待连接 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_WIFI], OL_FLASH_HIGH, LED_WIFI_FAST_FLASH_MS, 0xFFFF);/* 指示灯快闪 */
            break;
        case STAT_AP_STA_UNCFG: /* ap 配网模式,等待连接 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_WIFI], OL_FLASH_HIGH, LED_WIFI_SLOW_FLASH_MS, 0xFFFF);/* 指示灯慢闪 */
            break;
        case STAT_AP_STA_DISC:
        case STAT_STA_DISC:     /* SamrtConfig/ap 正在连接中 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_WIFI], OL_HIGH, 0, 0);/* 关闭指示灯 */
            break;
        case STAT_CLOUD_CONN:
        case STAT_AP_CLOUD_CONN:/* 连接到涂鸦云 */
            tuya_set_led_light_type(fan_led_handle[LED_HANDLE_WIFI], OL_LOW, 0, 0); /* 指示灯常亮 */
            break;
        default:
            break;
        }
    }
    

    由于关机时,挡位指示灯和模式指示灯需要熄灭,这里再添加一个关闭所有指示灯的函数:

    /**
     * @brief   关闭所有指示灯(除 Wi-Fi 指示灯)
     * @param   none
     * @return  none
     */
    VOID_T fan_close_all_led(VOID_T)
    {
        INT_T i;
        for (i = 0; i < (SIZEOF(sg_fan_led_port) / SIZEOF(sg_fan_led_port[0]) - 1); i++) {
            tuya_set_led_light_type(fan_led_handle[i], OL_HIGH, 0, 0xffff); /* 关闭 LED */
        }
    }
    

    指示灯亮度调节通过改变 PWM 占空比实现:

    /**
     * @brief   设置指示灯亮度
     * @param   brightness: 10-1000, 越大灯越亮
     * @return  none
     */
    VOID_T fan_set_led_dimmer(IN CONST USHORT_T brightness)
    {
        pwmout_pulsewidth_us(&g_led_dimmer_pwmout, (LED_DIMMER_PWM_PERIOD - brightness));
    }
    

    BLDC 电机控制

    本案例使用无刷直流电机来实现风扇扇叶的转动,采用 PWM 信号作为 Wi-Fi 模组和电机驱动器的交互信号,其对应引脚和功能设定如下:

    名称 网络标号 引脚 功能
    无刷直流电机 PWM2 PA11 PWM 方式控制电机转速从而调节风速

    初始化电机驱动 PWM,再根据风速设定调节 PWM 占空比:

    /* Pin */
    #define P_BLDC_MOTOR            TY_GPIOA_11
    /* PWM 周期 */
    #define BLDC_MOTOR_PWM_PERIOD   1000
    /* PWM 占空比 */
    #define BLDC_MOTOR_PWM_DUTY_OFF 5
    #define BLDC_MOTOR_PWM_DUTY_MIN 30
    #define BLDC_MOTOR_PWM_DUTY_MAX 99
    
    pwmout_t g_bldc_motor_pwmout;
    
    /**
     * @brief   电机初始化
     * @param   none
     * @return  none
     */
    VOID_T fan_motor_init(VOID_T)
    {
        pwmout_init(&g_bldc_motor_pwmout, P_BLDC_MOTOR);
        pwmout_period_us(&g_bldc_motor_pwmout, BLDC_MOTOR_PWM_PERIOD);
        pwmout_pulsewidth_us(&g_bldc_motor_pwmout, (BLDC_MOTOR_PWM_PERIOD * BLDC_MOTOR_PWM_DUTY_OFF / 100));
    }
     
    /**
     * @brief   设置风速
     * @param   speed: 风速
     * @return  none
     */
    VOID_T fan_set_speed(IN CONST UCHAR_T speed)
    {
        UCHAR_T pwm_duty;
    
        if (speed <= 0) {
            pwmout_pulsewidth_us(&g_bldc_motor_pwmout, (BLDC_MOTOR_PWM_PERIOD * BLDC_MOTOR_PWM_DUTY_OFF / 100));
            return;
        }
    
        /* 由于电机在 30%以下工作时间过长会出现异常,这里对 PWM 输出进行一些处理,使输出的 PWM 在 30%-99% 之间 */
        pwm_duty = (UCHAR_T)(BLDC_MOTOR_PWM_DUTY_MIN + ((BLDC_MOTOR_PWM_DUTY_MAX - BLDC_MOTOR_PWM_DUTY_MIN) * (speed / 100.0)));
        pwmout_pulsewidth_us(&g_bldc_motor_pwmout, (BLDC_MOTOR_PWM_PERIOD * pwm_duty / 100));
    }
    

    离线控制

    设备开关控制

    本案例通过操作电源键控制设备的开/关机:

    a. 编写电源键处理函数

    /**
     * @brief   开关键处理函数
     * @param   type: 触发类型
     * @param   cnt: 连击次数
     * @return  none
     */
    STATIC VOID_T fan_key_power_handler(IN CONST PUSH_KEY_TYPE_E type, IN CONST INT_T cnt)
    {
        switch (type) {
        case NORMAL_KEY:
            PR_DEBUG("power press.");
            fan_set_power(fan_switch_power());  /* 设置开/关机 */
            break;
        case LONG_KEY:
            PR_DEBUG("power long press.");
            break;
        case SEQ_KEY:
            PR_DEBUG("power SEQ press, the count is %d.", cnt);
            break;
        default:
            break;
        }
    }
    

    b. 设备开/关时需要处理的内容如下:

    /**
     * @brief   风扇开关切换
     * @param   none
     * @return  TRUE-开, FALSE-关
     */
    BOOL_T fan_switch_power(VOID_T)
    {
        if (g_fan_mag.power == FALSE) {
            g_fan_mag.power = TRUE;
        } else {
            g_fan_mag.power = FALSE;
        }
        return g_fan_mag.power;
    }
    
    /**
     * @brief   开/关机设置
     * @param   power: 开/关
     * @return  none
     */
    VOID_T fan_set_power(IN CONST BOOL_T power)
    {
        if (power) {
            PR_NOTICE("Power on.");
            /* 有定时设置时,启动定时关机定时器 */
            if (g_fan_mag.timing > FAN_TIMING_NONE) {
                PR_NOTICE("The fan will shutdown after: %dh", g_fan_mag.timing);
                fan_shutdown_timer_start();
            }
            /* 从当前模式启动 */
            fan_set_mode(g_fan_mag.mode);
        } else {
            PR_NOTICE("Power off.");
            /* 停止所有定时器 */
            fan_nature_mode_timer_stop();
            fan_sleep_mode_timer_stop();
            fan_timing_confirm_timer_stop();
            fan_shutdown_timer_stop();
            /* 关闭电机和指示灯 */
            fan_set_speed(0);
            fan_close_all_led();
            g_fan_mag.timing_display_flag = FALSE;
        }
        /* 上报/存储数据更新 */
        fan_report_all_dp_status();
        fan_write_data_to_flash();
    }
    

    工作模式选择

    本案例设置了直吹风、自然风和睡眠风 3 种工作模式,可通过轻触模式键进行切换,其工作内容如下:

    工作模式 工作内容
    直吹风 8 档风速可调
    自然风 忽大忽小间隔 5 秒
    睡眠风 每隔 1 小时自动降档,最后降到最低档,初始档位可自由设定

    下面我们来实现这些功能,首先要在模式键处理函数中加入工作模式的切替处理:

    /**
     * @brief   模式键处理函数
     * @param   type: 触发类型
     * @param   cnt: 连击次数
     * @return  none
     */
    STATIC VOID_T fan_key_mode_handler(IN CONST PUSH_KEY_TYPE_E type, IN CONST INT_T cnt)
    {
        switch (type) {
        case NORMAL_KEY:
            PR_DEBUG("mode press.");
            if (g_fan_mag.power == TRUE) {          /* 开机时 */
                fan_set_mode(fan_switch_mode());    /* 设置工作模式 */
            }
            break;
        case LONG_KEY:
            PR_DEBUG("mode long press.");
            break;
        case SEQ_KEY:
            PR_DEBUG("mode SEQ press, the count is %d.", cnt);
            break;
        default:
            break;
        }
    }
    

    3 种模式切替顺序为直吹风->自然风->睡眠风->直吹风,模式切替后进入各模式执行相关操作:

    /* 工作模式 */
    typedef BYTE_T FAN_MODE_E;
    #define FAN_MODE_NORMAL     0x00    /* 直吹风 */
    #define FAN_MODE_NATURE     0x01    /* 自然风 */
    #define FAN_MODE_SLEEP      0x02    /* 睡眠风 */
    
    /**
     * @brief   切换模式
     * @param   none
     * @return  工作模式
     */
    UCHAR_T fan_switch_mode(VOID_T)
    {
        /* 直吹风->自然风->睡眠风->直吹风 */
        if (g_fan_mag.mode == FAN_MODE_SLEEP) {
            g_fan_mag.mode = FAN_MODE_NORMAL;
        } else {
            g_fan_mag.mode++;
        }
        PR_NOTICE("fan mode changed to: %d", g_fan_mag.mode);
        return g_fan_mag.mode;
    }
    
    /**
     * @brief   设置工作模式
     * @param   mode: 工作模式
     * @return  none
     */
    VOID_T fan_set_mode(IN CONST FAN_MODE_E mode)
    {
    	/* 各模式入口 */
        switch (mode) {
        case FAN_MODE_NORMAL:
            fan_mode_normal();
            break;
        case FAN_MODE_NATURE:
            fan_mode_nature();
            break;
        case FAN_MODE_SLEEP:
            fan_mode_sleep();
            break;
        default:
            break;
        }
        /* 指示灯状态更新 */
        fan_set_mode_led(mode);
        fan_set_gear_led(g_fan_mag.gear);
        /* 上报/存储数据更新 */
        fan_report_all_dp_status();
        fan_write_data_to_flash();
    }
    

    各模式的具体流程实现如下:

    a. 直吹风模式

    /**
     * @brief   直吹风模式
     * @param   none
     * @return  none
     */
    STATIC VOID_T fan_mode_normal(VOID_T)
    {
    	/* 停止其他模式使用的定时器 */
        fan_nature_mode_timer_stop();
        fan_sleep_mode_timer_stop();
    	/* 根据当前挡位设置风速并输出 */
        g_fan_mag.speed = g_fan_speed_gear[g_fan_mag.gear];
        fan_set_speed(g_fan_mag.speed);
        PR_NOTICE("fan current speed is : %d", g_fan_mag.speed);
    }
    

    b. 自然风模式

    /* TIMER ID */
    #define TIMER_ID_NATURE_MODE    0           /* 自然模式 */
    /* 定时时间设置 */
    #define CYCLE_MS_NATURE_MODE    (5*1000)    /* 5s */
    
    /**
     * @brief   自然模式定时器启动
     * @param   none
     * @return  none
     */
    VOID_T fan_nature_mode_timer_start(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_start(TIMER_ID_NATURE_MODE, CYCLE_MS_NATURE_MODE, fan_nature_mode_timer_cb);
        if (op_ret != OPRT_OK) {
            PR_ERR("Start nature mode timer fails");
        }
    }
    
    /**
     * @brief   自然模式定时器关闭
     * @param   none
     * @return  none
     */
    VOID_T fan_nature_mode_timer_stop(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_stop(TIMER_ID_NATURE_MODE);
        if (op_ret != OPRT_OK) {
            PR_ERR("Stop nature mode timer fails");
        }
    }
    
    /* 自然风模式挡位设置 */
    #define NATURE_MODE_SPEED_SLOW_GEAR 1
    #define NATURE_MODE_SPEED_FAST_GEAR 6
    
    /**
     * @brief   自然风模式风速设置
     * @param   start_flag: 初始化标志
     * @return  none
     */
    STATIC VOID_T fan_set_nature_mode_speed(IN CONST BOOL_T start_flag)
    {
        if (start_flag) {
            g_fan_mag.gear = NATURE_MODE_SPEED_FAST_GEAR;
            PR_NOTICE("natural mode fast speed");
        } else {
            if (g_fan_mag.gear == NATURE_MODE_SPEED_SLOW_GEAR) {
                g_fan_mag.gear = NATURE_MODE_SPEED_FAST_GEAR;
                PR_NOTICE("natural mode fast speed");
            } else {
                g_fan_mag.gear = NATURE_MODE_SPEED_SLOW_GEAR;
                PR_NOTICE("natural mode low speed");
            }
        }
        g_fan_mag.speed = g_fan_speed_gear[g_fan_mag.gear];
        fan_set_speed(g_fan_mag.speed);
        PR_NOTICE("fan current speed is : %d", g_fan_mag.speed);
        fan_set_gear_led(g_fan_mag.gear);
    }
    
    /**
     * @brief   自然风模式回调函数
     * @param   none
     * @return  none
     */
    VOID_T fan_nature_mode_timer_cb(VOID_T)
    {
        /* 风速更新,再次启动定时器 */
        fan_set_nature_mode_speed(FALSE);
        fan_nature_mode_timer_start();
        /* 上报/存储数据更新 */
        fan_report_all_dp_status();
        fan_write_data_to_flash();
    }
    
    /**
     * @brief   自然风模式
     * @param   none
     * @return  none
     */
    STATIC VOID_T fan_mode_nature(VOID_T)
    {
    	/* 停止其他模式使用的定时器 */
        fan_sleep_mode_timer_stop();
        /* 风速设置并启动定时器 */
        fan_set_nature_mode_speed(TRUE);
        fan_nature_mode_timer_start();
    }
    

    c. 睡眠风模式

    /* TIMER ID */
    #define TIMER_ID_SLEEP_MODE     1           /* 睡眠模式 */
    /* 定时时间设置 */
    #define CYCLE_MS_SLEEP_MODE     (60*60*1000)/* 1h */
    
    /**
     * @brief   睡眠模式定时器启动
     * @param   none
     * @return  none
     */
    VOID_T fan_sleep_mode_timer_start(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_start(TIMER_ID_SLEEP_MODE, CYCLE_MS_SLEEP_MODE, fan_sleep_mode_timer_cb);
        if (op_ret != OPRT_OK) {
            PR_ERR("Start sleep mode timer fails");
        }
    }
    
    /**
     * @brief   睡眠模式定时器关闭
     * @param   none
     * @return  none
     */
    VOID_T fan_sleep_mode_timer_stop(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_stop(TIMER_ID_SLEEP_MODE);
        if (op_ret != OPRT_OK) {
            PR_ERR("Stop sleep mode timer fails");
        }
    }
    
    /**
     * @brief   睡眠风模式风速设置
     * @param   start_flag: 初始化标志
     * @return  none
     */
    STATIC VOID_T fan_set_sleep_mode_speed(IN CONST BOOL_T start_flag)
    {
        if (!start_flag) {
            if (g_fan_mag.gear > 0) {
                g_fan_mag.gear--;
            }
        }
        PR_NOTICE("current gear is: %d", g_fan_mag.gear);
        g_fan_mag.speed = g_fan_speed_gear[g_fan_mag.gear];
        fan_set_speed(g_fan_mag.speed);
        PR_NOTICE("fan current speed is : %d", g_fan_mag.speed);
        fan_set_gear_led(g_fan_mag.gear);
    }
    
    /**
     * @brief   睡眠风模式回调函数
     * @param   none
     * @return  none
     */
    VOID_T fan_sleep_mode_timer_cb(VOID_T)
    {
        /* 风速更新,再次启动定时器 */
        fan_set_sleep_mode_speed(FALSE);
        if (g_fan_mag.gear != 0) {
            fan_sleep_mode_timer_start();
        }
        /* 上报/存储数据更新 */
        fan_report_all_dp_status();
        fan_write_data_to_flash();
    }
    
    /**
     * @brief   睡眠风模式
     * @param   none
     * @return  none
     */
    STATIC VOID_T fan_mode_sleep(VOID_T)
    {
        /* 停止其他模式使用的定时器 */
        fan_nature_mode_timer_stop();
        /* 风速设置并启动定时器 */
        fan_set_sleep_mode_speed(TRUE);
        if (g_fan_mag.gear != 0) {
            fan_sleep_mode_timer_start();
        }
    }
    

    风速挡位调节

    本案例设置 8 挡风速可调,通过旋钮转向加大或减小风速。

    下面来实现挡位调节功能,只需在旋钮编码器回调函数中加入以下处理:

    /**
     * @brief   旋钮编码器回调函数
     * @param   dir: 旋转方向 TRUE-顺时针 FALSE-逆时针
     * @return  none
     */
    VOID_T fan_rotary_cb(BOOL_T dir)
    {
        /* 关机时旋钮无效 */
        if (g_fan_mag.power == FALSE) {
            return;
        }
        /* 旋转旋钮时可退出定时设置 */
        if (g_fan_mag.timing_display_flag) {
            fan_timing_confirm_timer_stop();
            fan_quit_timing_setting();
        }
        /* 根据转向加大或减小挡位 */
        if (dir) {
            PR_NOTICE("The direction of rotation is CW.");
            g_fan_mag.gear = (g_fan_mag.gear >= (8-1)) ? (8-1) : (g_fan_mag.gear+1);
        } else {
            PR_NOTICE("The direction of rotation is CCW.");
            g_fan_mag.gear = (g_fan_mag.gear <= 0) ? (0) : (g_fan_mag.gear-1);
        }
        PR_NOTICE("fan current gear is : %d", g_fan_mag.gear);
        /* 更新风速输出 (自然风模式下挡位变化时默认切换到直吹风模式) */
        if (g_fan_mag.mode == FAN_MODE_SLEEP) {
            fan_set_mode(FAN_MODE_SLEEP);
        } else {
            fan_set_mode(FAN_MODE_NORMAL);
        }
    }
    

    定时关机设定

    本案例通过操作定时键可以设置定时关机时间,由于挡位指示和定时指示共用 4 个指示灯,为方便用户确认当前设备状态,定时关机功能的具体设置方式如下:

    状态 操作 响应
    未进入定时设置 轻触定时键 进入定时设置,指示灯显示当前剩余定时时间;
    已进入定时设置 轻触定时键 按照”无定时->1 小时->2 小时->3 小时->4 小时->无定时“切换定时时间,指示灯显示当前设置定时时间;开启定时确认的 2 秒计时;
    已进入定时设置,设置定时 2 秒后 / 确认定时时间并退出定时设置,指示灯显示当前挡位;跟据定时设置结果进行定时关机处理;
    已进入定时设置,设置定时 2 秒内 轻触定时键以外任意键或旋转旋钮 定时设置无效,退出定时设置,指示灯显示当前挡位;

    a. 编写定时键处理函数

    /**
     * @brief   定时键处理函数
     * @param   type: 触发类型
     * @param   cnt: 连击次数
     * @return  none
     */
    STATIC VOID_T fan_key_timer_handler(IN CONST PUSH_KEY_TYPE_E type, IN CONST INT_T cnt)
    {
        switch (type) {
        case NORMAL_KEY:
            PR_DEBUG("timer press.");
            if (g_fan_mag.power == TRUE) {  /* 开机时 */
                fan_set_local_timing();     /* 设置本地定时 */
            }
            break;
        case LONG_KEY:
            PR_DEBUG("timer long press.");
            break;
        case SEQ_KEY:
            PR_DEBUG("timer SEQ press, the count is %d.", cnt);
            break;
        default:
            break;
        }
    }
    

    b. 本地定时设置及确认:

    /* TIMER ID */
    #define TIMER_ID_TIMING_CONFIRM 2           /* 定时功能确认 */
    /* 定时时间设置 */
    #define CYCLE_MS_TIMING_CONFIRM (2*1000)    /* 2s */
    
    /**
     * @brief   定时设置确认定时器启动
     * @param   none
     * @return  none
     */
    VOID_T fan_timing_confirm_timer_start(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_start(TIMER_ID_TIMING_CONFIRM, CYCLE_MS_TIMING_CONFIRM, fan_confirm_timer_cb);
        if (op_ret != OPRT_OK) {
            PR_ERR("Start confirm timer fails");
        }
    }
    
    /**
     * @brief   定时确认定时器关闭
     * @param   none
     * @return  none
     */
    VOID_T fan_timing_confirm_timer_stop(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_stop(TIMER_ID_TIMING_CONFIRM);
        if (op_ret != OPRT_OK) {
            PR_ERR("Stop confirm timer fails");
        }
    }
    
    /* 定时设置 */
    #define FAN_TIMING_NONE             0
    #define FAN_TIMING_MAX              4
    
    /**
     * @brief   本地定时设置
     * @param   none
     * @return  none
     */
    VOID_T fan_set_local_timing(VOID_T)
    {
        if (g_fan_mag.timing_display_flag == FALSE) {
            /* 进入定时设置,获取当前剩余定时时间 */
            g_fan_mag.timing_set = g_fan_mag.timing;
            g_fan_mag.timing_display_flag = TRUE;
        } else {
            /* 定时时间切换:无定时->1h->2h->3h->4h->无定时 */
            if (g_fan_mag.timing_set >= FAN_TIMING_MAX) {
                g_fan_mag.timing_set = FAN_TIMING_NONE;
            } else {
                g_fan_mag.timing_set++;
            }
        }
        /* 定时设置时间显示并启动计时 */
        fan_set_timing_led(g_fan_mag.timing_set);
        fan_timing_confirm_timer_start();
    }
    
    /**
     * @brief   退出定时设置
     * @param   none 
     * @return  none
     */
    VOID_T fan_quit_timing_setting(VOID_T)
    {
        /* 回到挡位显示 */
        g_fan_mag.timing_display_flag = FALSE;
        fan_set_gear_led(g_fan_mag.gear);
    }
    
    /**
     * @brief   定时确认回调函数
     * @param   none
     * @return  none
     */
    VOID_T fan_confirm_timer_cb(VOID_T)
    {
        if (g_fan_mag.timing != g_fan_mag.timing_set) {
            /* 确认定时设置 */
            g_fan_mag.timing = g_fan_mag.timing_set;
            fan_report_all_dp_status();
            /* 关闭定时 */
            fan_shutdown_timer_stop();
            /* 定时设置不是“无定时”时, 开启定时关机 */
            if (g_fan_mag.timing > FAN_TIMING_NONE) {
                fan_shutdown_timer_start();
                PR_NOTICE("The fan will shutdown after: %dh", g_fan_mag.timing);
            }
        }
        /* 退出定时设置 */
        fan_quit_timing_setting();
    }
    

    c. 定时关机计时启动,计时结束时关机:

    /* TIMER ID */
    #define TIMER_ID_SHUTDOWN       3           /* 定时关机 */
    /* 定时时间设置 */
    #define CYCLE_MS_SHUTDOWN       (60*60*1000)/* 1h */
    
    /**
     * @brief   定时关机定时器关闭
     * @param   none
     * @return  none
     */
    VOID_T fan_shutdown_timer_stop(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_stop(TIMER_ID_SHUTDOWN);
        if (op_ret != OPRT_OK) {
            PR_ERR("Stop shutdown timer fails");
        }
    }
    
    /**
     * @brief   定时设置确认定时器启动
     * @param   none
     * @return  none
     */
    VOID_T fan_timing_confirm_timer_start(VOID_T)
    {
        OPERATE_RET op_ret;
        op_ret = tuya_soc_software_timer_start(TIMER_ID_TIMING_CONFIRM, CYCLE_MS_TIMING_CONFIRM, fan_confirm_timer_cb);
        if (op_ret != OPRT_OK) {
            PR_ERR("Start confirm timer fails");
        }
    }
    
    /**
     * @brief   定时关机回调函数
     * @param   none
     * @return  none
     */
    VOID_T fan_shutdown_timer_cb(VOID_T)
    {
        g_fan_mag.timing--;
        g_fan_mag.timing_set = g_fan_mag.timing;
        if (g_fan_mag.timing == 0) {
            fan_shutdown_timer_stop();
            fan_set_power(FALSE);
        } else {
            PR_NOTICE("The fan will shutdown after: %dh", g_fan_mag.timing);
            fan_report_all_dp_status();
            fan_write_data_to_flash();
            fan_shutdown_timer_start();
        }
    }
    

    设备状态重置

    本案例提供一键复位功能,按下模式键 3 秒,设备状态即可恢复到默认状态。

    a. 添加模式键的长按处理:

    /**
     * @brief   模式键处理函数
     * @param   type: 触发类型
     * @param   cnt: 连击次数
     * @return  none
     */
    STATIC VOID_T fan_key_mode_handler(IN CONST PUSH_KEY_TYPE_E type, IN CONST INT_T cnt)
    {
        switch (type) {
        case NORMAL_KEY:
            PR_DEBUG("mode press.");
            if (g_fan_mag.power == TRUE) {          /* 开机时 */
                fan_set_mode(fan_switch_mode());    /* 设置工作模式 */
            }
            break;
        case LONG_KEY:
            PR_DEBUG("mode long press.");
            fan_reset();                            /* 复位 */
            break;
        case SEQ_KEY:
            PR_DEBUG("mode SEQ press, the count is %d.", cnt);
            break;
        default:
            break;
        }
    }
    

    b. 编写设备复位函数,重置设备状态:

    /**
     * @brief   复位
     * @param   none 
     * @return  none
     */
    VOID_T fan_reset(VOID_T)
    {
        g_fan_mag = g_fan_mag_default;
        fan_set_bright(g_fan_mag.bright);
        fan_set_power(g_fan_mag.power);
        PR_INFO("fan reset.");
    }
    

    通电自动恢复

    本案例设置了断电记忆功能,可在通电时恢复到上一次断电前的状态,主要通过读写 Flash 来实现此功能。

    a. 确定需要记忆的状态属性,编写 Flash 读写函数:

    /* 风扇数据存储偏移量 */
    #define FAN_DATA_STORAGE_OFFSET 0x00
    /* 风扇数据存储长度:数据头-风扇开关-风扇模式-风扇转速-定时时间-CRC_8 校验 */
    #define FAN_DATA_STORAGE_LEN    6   
    /* 数据帧头 */
    #define FAN_DATA_HEADER         0xFF
    /* 风扇状态数据存放位置 */
    #define FAN_STATE_POWER         1
    #define FAN_STATE_MODE          2
    #define FAN_STATE_SPEED         3
    #define FAN_STATE_TIMING        4
    
    /**
     * @brief   读取 flash 存储的风扇数据
     * @param   none
     * @return  none
     */
    VOID_T fan_read_data_from_flash(VOID_T)
    {
        INT_T read_cnt;
        UCHAR_T fan_state_data_crc;
        UCHAR_T fan_state_buffer[FAN_DATA_STORAGE_LEN];
    
        /* 读取 flash 数据 */
        read_cnt = tuya_soc_flash_read(SAVE_TYP1, FAN_DATA_STORAGE_OFFSET, FAN_DATA_STORAGE_LEN, (UCHAR_T *)fan_state_buffer);
        if (read_cnt <= 0) {
            PR_ERR("Reset cnt read error!");
            return;
        }
    
        /* 检查数据头 */ 
        if (fan_state_buffer[0] != FAN_DATA_HEADER) {
            PR_ERR("data head error");
            return;
        }
        /* 检查校验数据 */
        fan_state_data_crc = get_crc_8(fan_state_buffer, (FAN_DATA_STORAGE_LEN - 1) * SIZEOF(UCHAR_T));
        if (fan_state_data_crc != fan_state_buffer[FAN_DATA_STORAGE_LEN - 1]) { 
            PR_ERR("crc error, before_fan_power_off_state[%d] = %02x, crc data = %02x.", FAN_DATA_STORAGE_LEN - 1, fan_state_buffer[FAN_DATA_STORAGE_LEN - 1], fan_state_data_crc);
            return;
        }
    
        /* 更新风扇数据 */ 
        g_fan_mag.power = fan_state_buffer[FAN_STATE_POWER];
        g_fan_mag.mode = fan_state_buffer[FAN_STATE_MODE];
        g_fan_mag.speed = fan_state_buffer[FAN_STATE_SPEED];
        g_fan_mag.timing = fan_state_buffer[FAN_STATE_TIMING];
    }
    
    /**
     * @brief   将风扇数据写入 flash
     * @param   none
     * @return  none
     */
    VOID_T fan_write_data_to_flash(VOID_T)
    {
        INT_T i;
        OPERATE_RET opRet;
        UCHAR_T fan_data_buffer[FAN_DATA_STORAGE_LEN];
    
        /* 更新风扇数据到 buffer */
        fan_data_buffer[0] = FAN_DATA_HEADER;
        fan_data_buffer[FAN_STATE_POWER] = g_fan_mag.power;
        fan_data_buffer[FAN_STATE_MODE] = g_fan_mag.mode;
        fan_data_buffer[FAN_STATE_SPEED] = g_fan_mag.speed;
        fan_data_buffer[FAN_STATE_TIMING] = g_fan_mag.timing;
        fan_data_buffer[FAN_DATA_STORAGE_LEN-1] = get_crc_8(fan_data_buffer, (FAN_DATA_STORAGE_LEN - 1) * SIZEOF(UCHAR_T));
        /* 打印 buffer */
        for (i = 0; i < FAN_DATA_STORAGE_LEN; i++) {
            PR_NOTICE(" +++ fan_data_buffer is [%d] : %02x", i, fan_data_buffer[i]);
        }
    
        /* 写入 flash 数据 */
        opRet = tuya_soc_flash_write(SAVE_TYP1, FAN_DATA_STORAGE_OFFSET, fan_data_buffer, FAN_DATA_STORAGE_LEN); 
        if (opRet != OPRT_OK) {
            PR_ERR("write flash error");
        }
        
        return;
    }
    

    b. 由于断电随时可能发生,所以在每次状态属性变化时就调用写 Flash 函数存储一次数据,具体调用位置已在上述各项功能代码中体现;

    c. 在上电时调用读 Flash 函数加载数据:

    /* 风扇数据管理 */
    FAN_MANAGE_T g_fan_mag;
    FAN_MANAGE_T g_fan_mag_default = {
        .power = FALSE,
        .mode = FAN_MODE_NORMAL,
        .speed = FAN_SPEED_DEFAULT,
        .bright = FAN_BRIGHT_NORMAL,
        .timing = FAN_TIMING_NONE,
        .timing_set = FAN_TIMING_NONE,
        .timing_display_flag = FALSE,
        .gear = FAN_GEAR_DEFAULT
    };
    
    /**
     * @brief   BLDC 风扇应用初始化程序, 被 app_init 调用
     * @param   none
     * @return  none
     */
    VOID_T fan_app_init(VOID_T)
    {
        /* 初始化 g_fan_mag */
        g_fan_mag = g_fan_mag_default;
        /* 读取 flash 数据 */
        fan_read_data_from_flash();
        /* 首次上电时,更新默认值 */
        if (g_fan_mag.power == 0xFF) {
            g_fan_mag = g_fan_mag_default;
            fan_write_data_to_flash();
        } else {
            g_fan_mag.power = FALSE;
        }
    }
    

    云端控制

    配网状态重置

    本案例可通过长按 10 秒配网键重置设备配网状态。添加配网键处理,调用 SDK 提供的接口进行解绑:

    /**
     * @brief   配网键处理函数
     * @param   type: 触发类型
     * @param   cnt: 连击次数
     * @return  none
     */
    STATIC VOID_T fan_key_wifi_handler(IN CONST PUSH_KEY_TYPE_E type, IN CONST INT_T cnt)
    {
        switch (type) {
        case NORMAL_KEY:
            PR_DEBUG("wifi press.");
            break;
        case LONG_KEY:
            PR_DEBUG("wifi long press.");
            tuya_iot_wf_gw_unactive();  /* 手动移除设备 */
            break;
        case SEQ_KEY:
            PR_DEBUG("wifi SEQ press, the count is %d.", cnt);
            break;
        default:
            break;
        }
    }
    

    本地数据上报

    在设备配网后,就可以使用 App 来控制设备和查看设备上报的数据,下面是数据上报的实现过程。

    /* DP ID */
    #define DP_ID_SWITCH            1
    #define DP_ID_MODE              2
    #define DP_ID_FAN_SPEED         3
    #define DP_ID_BRIGHT_MODE       101
    #define DP_ID_COUNTDOWN_SET     22
    /* DP amount */
    #define FAN_DP_AMOUNT           5
    
    /**
     * @brief   上报所有 dp 点
     * @param   none
     * @return  none
     */
    VOID_T fan_report_all_dp_status(VOID_T)
    {
        OPERATE_RET op_ret = OPRT_OK;
    
        /* 没有连接到路由器,退出 */
        GW_WIFI_NW_STAT_E wifi_state = STAT_LOW_POWER;
        get_wf_gw_nw_status(&wifi_state);
        if (wifi_state <= STAT_AP_STA_DISC || wifi_state == STAT_STA_DISC) {
            return;
        }
        TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(FAN_DP_AMOUNT*SIZEOF(TY_OBJ_DP_S));
    
        if(NULL == dp_arr) {
            PR_ERR("malloc failed");
            return;
        }
    
        memset(dp_arr, 0, FAN_DP_AMOUNT*SIZEOF(TY_OBJ_DP_S));
    
        dp_arr[0].dpid = DP_ID_SWITCH;
        dp_arr[0].type = PROP_BOOL;
        dp_arr[0].time_stamp = 0;
        dp_arr[0].value.dp_bool = g_fan_mag.power;
    
        dp_arr[1].dpid = DP_ID_MODE;
        dp_arr[1].type = PROP_ENUM;
        dp_arr[1].time_stamp = 0;
        dp_arr[1].value.dp_enum = g_fan_mag.mode;
    
        dp_arr[2].dpid = DP_ID_FAN_SPEED;
        dp_arr[2].type = PROP_VALUE;
        dp_arr[2].time_stamp = 0;
        dp_arr[2].value.dp_value = g_fan_mag.speed;
    
        dp_arr[3].dpid = DP_ID_BRIGHT_MODE;
        dp_arr[3].type = PROP_ENUM;
        dp_arr[3].time_stamp = 0;
        dp_arr[3].value.dp_enum = g_fan_mag.bright;
    
        dp_arr[4].dpid = DP_ID_COUNTDOWN_SET;
        dp_arr[4].type = PROP_ENUM;
        dp_arr[4].time_stamp = 0;
        dp_arr[4].value.dp_enum = g_fan_mag.timing;
    
        op_ret = dev_report_dp_json_async(NULL, dp_arr, FAN_DP_AMOUNT);
        Free(dp_arr);
        if (OPRT_OK != op_ret) {
            PR_ERR("dev_report_dp_json_async relay_config data error, err_num: %d", op_ret);
        }
    
        PR_DEBUG("dp_query report_all_dp_data");
    }
    

    接收数据处理

    在 App 上改变设备状态时,会从云端下发控制数据,设备在接收到数据后进行如下处理,即可实现云端任务。

    /**
     * @brief   处理 dp 信息
     * @param   root: dp 信息
     * @return  none
     */
    VOID_T fan_deal_dp_proc(IN CONST TY_OBJ_DP_S *root)
    {
        UCHAR_T dpid;
    
        dpid = root->dpid;
        PR_NOTICE("dpid:%d", dpid);
    
        switch(dpid) {
        case DP_ID_SWITCH:
            PR_NOTICE("root->value.dp_bool:%d", root->value.dp_bool);
            fan_set_cloud_power(root->value.dp_bool);
            break;
        case DP_ID_MODE:
            PR_NOTICE("root->value.dp_enum:%d", root->value.dp_enum);
            fan_set_cloud_mode((FAN_MODE_E)root->value.dp_enum);
            break;
        case DP_ID_FAN_SPEED:
            PR_NOTICE("root->value.dp_value:%d", root->value.dp_value);
            fan_set_cloud_speed((UCHAR_T)root->value.dp_value);
            break;
        case DP_ID_BRIGHT_MODE:
            PR_NOTICE("root->value.dp_enum:%d", root->value.dp_enum);
            fan_set_cloud_bright((FAN_BRIGHT_E)root->value.dp_enum);
            break;
        case DP_ID_COUNTDOWN_SET:
            PR_NOTICE("root->value.dp_enum:%d", root->value.dp_enum);
            fan_set_cloud_timing((UCHAR_T)root->value.dp_enum);
            break;
        default:
            break;
        }
    }
    

    各属性的处理函数如下:

    /**
     * @brief   云端开关设置
     * @param   power: 开关
     * @return  none
     */
    VOID_T fan_set_cloud_power(IN CONST BOOL_T power)
    {
        if (power != g_fan_mag.power) {
            g_fan_mag.power = power;
            fan_set_power(g_fan_mag.power);
        }
    }
    
    /**
     * @brief   云端模式设置
     * @param   mode: 模式
     * @return  none
     */
    VOID_T fan_set_cloud_mode(IN CONST FAN_MODE_E mode)
    {
        if (!g_fan_mag.power) {
            return;
        }
        if (mode != g_fan_mag.mode) {
            g_fan_mag.mode = mode;
            fan_set_mode(g_fan_mag.mode);
        }
    }
    
    /**
     * @brief   云端速度设置
     * @param   speed: 速度值
     * @return  none
     */
    VOID_T fan_set_cloud_speed(IN CONST UCHAR_T speed)
    {
        /* 关机时设置无效 */
        if (!g_fan_mag.power) {
            return;
        }
        /* 速度和挡位更新 */
        fan_set_speed(speed);
        g_fan_mag.speed = speed;
        g_fan_mag.gear = fan_get_cur_gear_according_to_speed(speed);
        fan_set_gear_led(g_fan_mag.gear);
        PR_NOTICE("fan current speed is : %d", g_fan_mag.speed);
        PR_NOTICE("fan current gear is : %d", g_fan_mag.gear);
        /* 模式如果是自然风则更新为直吹风 */
        if (g_fan_mag.mode == FAN_MODE_NATURE) {
            g_fan_mag.mode = FAN_MODE_NORMAL;
            fan_set_mode_led(g_fan_mag.mode);
            fan_nature_mode_timer_stop();
        }
        /* 模式如果是睡眠风则更新重新计时 */
        if (g_fan_mag.mode == FAN_MODE_SLEEP) {
            fan_sleep_mode_timer_stop();
            fan_sleep_mode_timer_start();
        }
        /* 更新记忆值 */
        fan_write_data_to_flash();
    }
    
    /**
     * @brief   亮度设置
     * @param   bright: 亮度
     * @return  none
     */
    STATIC VOID_T fan_set_bright(IN CONST FAN_BRIGHT_E bright)
    {
        switch (bright) {
        case FAN_BRIGHT_NORMAL:
            fan_set_led_dimmer(LED_DIMMER_PWM_DUTY_NORMAL);
            break;
        case FAN_BRIGHT_DARKER:
            fan_set_led_dimmer(LED_DIMMER_PWM_DUTY_DARKER);
            break;
        default:
            break;
        }
    }
    
    /**
     * @brief   云端亮度设置
     * @param   bright: 亮度
     * @return  none
     */
    VOID_T fan_set_cloud_bright(IN CONST FAN_BRIGHT_E bright)
    {
        if (bright != g_fan_mag.bright) {
            g_fan_mag.bright = bright;
            fan_set_bright(bright);
        }
    }
    
    /**
     * @brief   云端定时设置
     * @param   timing: 定时值
     * @return  none
     */
    VOID_T fan_set_cloud_timing(IN CONST UCHAR_T timing)
    {
        if ((!g_fan_mag.power) || (timing == g_fan_mag.timing)) {
            return;
        }
        if (timing > FAN_TIMING_MAX) {
            g_fan_mag.timing = FAN_TIMING_MAX;
        } else {
            g_fan_mag.timing = timing;
        }
        fan_shutdown_timer_stop();
        if (g_fan_mag.timing > 0) {
            fan_shutdown_timer_start();
        }
    }
    

小结

至此,智能电风扇就完成了,它可以通过 App 或者按键控制,具有 3 种模式风、风速调节、定时关机、断电记忆、灯光亮度调节等功能。在这款智能电风扇的基础上还有很多功能可以深入开发,使体验更加人性化,智能化。同时您可以基于 涂鸦 IoT 平台 丰富它的功能,也可以更加方便地搭建更多智能产品原型,加速智能产品的开发流程。

更进一步