制作智能空气净化器原型

更新时间Invalid date

概况

随着空气污染的日益严重,以及人们对生活质量的要求越来越高,越来越多的人开始关注到空气质量的问题。一台智能化空气净化器在保证操作方便、支持智能化控制的同时,如果还能根据空间空气质量调整净化强度,那么不仅能帮助保障家人呼吸健康,还能最大效率节约资源。

本文所介绍的空气净化器解决方案不仅拥有基础功能,还增加了其他功能。此款智能空气净化器可以自动检测空气中有害物质的浓度,根据污染浓度实时自动调节净化强度,还能检测空气净化器滤芯使用情况、计算滤芯使用寿命,并提醒更换滤芯。

步骤

  • 第 1 步:硬件设计方案

    硬件特性

    • 主控模块采用涂鸦自研的 WBRU 模组,用于控制整机运行,包括按键检测、LED 指示、负载控制、数据通讯等工作。
    • 采用四合一粉尘传感器实时测量输出温湿度、VOC 浓度、PM1.0/PM2.5/PM10 质量浓度。
    • 蜂鸣器采用压电式无源蜂鸣器,通过模组输出相应频率 PWM 信号驱动。
    • 微动开关可以防止误打开滤芯所在后盖。
    • 光敏传感器可以检测环境光线强度,自动调节显示亮度。
    • LED 可直观显示定时、异常警报等信息。
    • NFC 读卡器可以有效检测滤芯的匹配情况与使用寿命。

    硬件方案框架图

    主控单元

    本方案选用涂鸦智能的一款低功耗 WBRU 模组作为主控板,并基于该模组进行 SoC 开发,实现电机控制和传感器数据采集。

    电机控制

    空气净化器的风机选用的型号是稻津的 MI-JK48HM710,电机的控制信号有调速信号 PWM、反馈信号 FG 和刹车信号 BRK,控制信号与模组之间的通讯需要进行电平转化。

    按键与感光传感器

    ​系统有两个按键。

    • 主按键:长按开关机,短按切换不同的工作模式(自动、睡眠、最爱)。
    • 屏幕按键:短按切换屏幕和指示灯的状态(PM2.5、PM10、TVOC、温度、湿度),长按进入配网模式。系统中有一个光敏二极管,用于控制夜间光线较暗时显示的亮度。

    显示电路

    方案使用一个 TM1650 驱动 IC 来驱动一个 3 位 8 段数码管和 5 个模式指示灯。数码管用于显示空气指标,通过屏幕按键控制,分别为 PM2.5 数据、PM10 数据、TVOC 数据、温度、湿度。

    NFC 驱动

    方案使用 MFRC522,它是高度集成的非接触式读写卡芯片,可根据不同主机接口实现 SPI、串行 UART、I2C 协议通讯。方案中使用 SPI 通讯来实现滤芯 NFC 检测功能(通过 NFC 判断滤芯工作时间,以及是否更换新滤芯)。

    四合一传感器驱动

    AM1002 是一款高性价比的多参数空气质量传感器集成模组,能够实时测量输出温湿度、VOC 浓度、PM1.0/PM2.5 /PM10 质量浓度。

    电源

    方案使用 LowPower Semiconductor (微源半导体)的 LP6498AB6F 输出 5V 给一些传感器供电,ASM1117 由 5V 转 3.3V 给模组供电,供电电流不低于 150mA。

    LP6498AB6F 的规格如下:

    • 最大输入电压: 4.5~30V
    • 输出电压范围: 0.8~12V
    • 开关频率: 600KHZ
    • 最大持续电流可达: 1.2A

    其他驱动

    此外,还有蜂鸣器、UV 灯、以及机械微动开关用于仓门检测。

    原理图与 PCB

  • 第 2 步:整机搭建

    整机搭建如下图所示:

  • 第 3 步:软件设计方案

    功能需求

    产品功能简述: 有自动、手动、睡眠三种模式。可以通过按键和 App 选择切换。其中自动模式会根据环境自动控制风扇转速、UV 灯、屏幕灯光等,还可以通过 NFC 检测滤芯是否安装,并读取滤芯生产信息,同时能够智能计算滤芯寿命。

    功能 说明
    设备功能
    • 主按键: 长按开关机。短按切换不同的工作模式(自动、手动、睡眠)。
    • 屏幕按键: 短按切换屏幕和指示灯的状态(PM2.5、PM10、TVOC、温度、湿度), 长按进入配网模式。
    • 显示屏:三位断码屏,显示空气指标,通过屏幕按键控制,分别为 PM2.5 数据、PM10 数据、TVOC 数据、温度、湿度。
    • 蜂鸣器:按键音及报警提醒。如果滤芯仓门是打开的,则设备断电不工作。
    • NFC 芯片:每次开机,检测滤芯 NFC 是否存在,读取滤芯信息并上报。
    App 功能
    • 总开关。
    • 童锁:App 上开启后,设备端按键无法使用。
    • 静音:App 上开启后,设备端按键静音。
    • 定时:App 上设置后,启动定时开关功能。
    • 智能杀菌:需要 App 手动开启。
    • 空气指标显示:分别为 PM2.5 数据、PM10 数据、TVOC 数据、温度、湿度。
    • 滤芯寿命显示及重置:支持 App 手动重置,也支持 NFC 识别后自动重置。
    • 工作模式:自动模式、手动模式、睡眠模式三种。
    • 自动模式:运行自动模式功能。
    • 手动模式:可以手动设置此模式下的风速档位。
    • 睡眠模式:进入睡眠档位 20% 风速,屏幕 LED 灯关闭。
    • 风速档位:睡眠(20%)、低(40%)、中(70%)、高(100%)。
    智能功能
    • 自动模式——风扇自动调节功能。
    • 自动模式——环保功能:当室内空气质量优(0-35ug/m³)且保持一段时间(2h)时,关闭风扇,以节省能源,如果 PM2.5 再次超过 35,则启动风扇自动调节。
    • 自动模式—睡眠功能:当光线传感器检测到房间已经变暗,设备自动关闭灯光和屏幕。此时如果空气质量优保持一段时间时(30min),则风扇降为最低档位(20%风速)。
    • 智能杀菌功能:温度 20℃ 以上,湿度 65% 以上,且近 4h 无触发过杀菌功能(开启 UV 灯),则开启 UV 杀菌持续 1h。
    • 滤芯 NFC 检测功能:检测滤芯是否存在,并读取滤芯类别和生产信息,可防盗版。
    • 滤芯寿命智能计算功能:智能计算滤芯的粉尘累计吸附量,得到滤芯剩余寿命信息,并根据粉尘吸附速度和设备使用频率估算滤芯剩余可用时间。
    配网功能 通用 Wi-Fi+Bluetooth Low Energy 配网,长按配网按键重置配网。

    环境搭建

    开发环境搭建

    功能 DP 点

    DP ID 功能点 标识符 数据传输类型 数据类型 功能点属性
    1 开关 switch 可下发可上报 bool
    2 PM2.5 pm25 只上报 value 数值范围: 0~700;间距: 1;倍数: 0;单位: ug/m³
    3 模式 mode 可下发可上报 enum 枚举值: manual; auto; sleep
    4 风速 fan_speed_enum 可下发可上报 enum 枚举值: sleep;low; mid;high
    5 滤芯寿命 filter_life 只上报 value 数值范围: 0~100;间距: 1;倍数: 0;单位: %
    7 童锁 child_lock 可下发可上报 bool
    8 灯光 light 可下发可上报 bool
    9 UV 杀菌 uv 可下发可上报 bool
    11 滤芯复位 filter_reset 可下发可上报 bool
    12 室内温度 temp_indoor 只上报 value 数值范围: -20~50;间距: 1; 倍数: 0;单位: ℃
    13 室内湿度 humidity 只上报 value 数值范围: 0~100;间距: 1;倍数: 0;单位: %
    14 TVOC tvoc 只上报 value 数值范围: 0~999;间距: 1;倍数: 0;单位: ug/m3
    16 滤芯剩余天数 filter_days 只上报 value 数值范围: 0~1000;间距: 1; 倍数: 0;单位: day
    17 累计工作时间 runtime_total 只上报 value 数值范围: 0~5,256,000;间距: 1;倍数: 0;单位: min
    18 倒计时 countdown_set 可下发可上报 enum 枚举值: cancel、1h、 2h、3h、 4h、 5h
    19 倒计时剩余时间 countdown_left 只上报 value 数值范围: 0~360;间距: 1;倍数: 0;单位: min
    20 累计吸收颗粒 pm_total 只上报 value 数值范围: 0~10000000,间距: 1, 倍数: 0,单位: mg
    22 故障告警 fault 只上报 fault 故障值: e1, e2
    23 温标切换 temp_unit_convert 可下发可上报 enum 枚举值: c, f
    101 PM10 pm10 只上报 value 数值范围: 0~999,间距: 1, 倍数: 0,单位: ug/m3
    102 滤芯种类 filter_type 只上报 enum 枚举值: standard, Antibacterial, Aldehyde_removal, Professional
    103 静音 sound_switch 可下发可上报 bool

    总体设计

    模块划分

    对功能需求进行分析梳理后,可将空气净化器 demo 程序划分为以下八大模块:

    No. 模块 处理内容
    1 外设驱动组件 按键、电机、段码显示屏、NFC 读卡器、空气质量传感器等驱动程序
    2 设备基础服务 设备开关、状态处理、模式切换、本地定时等
    3 显示处理服务 段码液晶屏显示空气指标数据、LED 灯指示段码屏显示的空气指标内容
    4 环境检测服务 检测空气质量环境、光线亮暗情况
    5 数据计算处理 自动风扇转速算法、滤芯寿命算法、粉尘吸附量的计算和存储、滤芯 NFC 数据解析
    6 用户事件处理 按键事件检测和处理、仓门开关事件检测和处理
    7 定时事件处理 各定时事件的判断和处理
    8 联网相关处理 配网相关处理、数据上报与接收处理、云端时间获取

    代码结构

    tuya_air_cleaner_demo
    ├── platform             /* 涂鸦通用 Tuya IoTOS SDK 的开发编译环境和工具链 */
    ├── sdk                  /* 存放涂鸦通用 Tuya IoTOS SDK 的头文件和库文件 */
    └──  app                 /* 存放涂鸦通用 Tuya IoTOS SDK 的 demo */
        ├── src              /* 源文件目录 */
        │   ├── common
        │   │   ├── tuya_device.c                     /* 应用层入口 */
        │   │   ├── tuya_dp_process.c                 /* DP 上下发处理 */
        │   │   ├── tuya_iot_funtion.c                /* 连接 IOT 云 */
        │   │   └── tuya_key_funtion.c                /* 按键处理 */
        │   ├── driver
        │   │   ├── mfrc522
        │   │   │   ├── tuya_mfrc522_app.c            /* NFC 芯片 mfrc522 中间层驱动 */
        │   │   │   └── tuya_mfrc522.c                /* NFC 芯片 mfrc522 驱动 */
        │   │   ├── tm1650
        │   │   │   ├── soc_i2c.c                     /* 软件 I2C 模拟驱动 */
        │   │   │   ├── tm1650_app.c                  /* 液晶显示屏芯片 tm1650 中间层驱动 */
        │   │   │   └── tm1650.c                      /* 液晶显示屏芯片 tm1650 驱动 */
        │   │   ├── tuya_buz_driver.c                 /* 蜂鸣器驱动 */
        │   │   ├── tuya_hardware_driver.c            /* 硬件 GPIO 驱动 */
        │   │   ├── tuya_lcd_display.c                /* 液晶屏应用层驱动 */
        │   │   └── tuya_motor_driver.c               /* 风扇电机驱动 */
        │   └── function
        │       ├── tuya_air_quality_funtion.c        /* 空气质量获取和显示功能 */
        │       ├── tuya_automatic_mode_funtion.c     /* 自动模式相关功能 */
        │       ├── tuya_countdown_funtion.c          /* 倒计时功能 */
        │       ├── tuya_filter_funtion.c             /* 滤芯寿命相关功能 */
        │       ├── tuya_mode_funtion.c               /* 模式切换相关功能 */
        │       ├── tuya_nfc_funtion.c                /* 滤芯 NFC 检测相关功能 */
        │       └── tuya_timer_funtion.c              /* Timer 相关功能 */
        └── include
            ├── common
            │   ├── tuya_device.h
            │   ├── tuya_dp_process.h
            │   ├── tuya_iot_funtion.h
            │   └── tuya_key_funtion.h
            ├── driver
            │   ├── mfrc522
            │   │   ├── tuya_mfrc522_app.h
            │   │   └── tuya_mfrc522.h
            │   ├── tm1650
            │   │   ├── soc_i2c.h
            │   │   ├── tm1650_app.h
            │   │   └── tm1650.h
            │   ├── tuya_buz_driver.h
            │   ├── tuya_hardware_driver.h
            │   ├── tuya_lcd_display.h
            │   └── tuya_motor_driver.h
            └── function
                ├── tuya_air_quality_funtion.h
                ├── tuya_automatic_mode_funtion.h
                ├── tuya_countdown_funtion.h
                ├── tuya_filter_funtion.h
                ├── tuya_mode_funtion.h
                ├── tuya_nfc_funtion.h
                └── tuya_timer_funtion.h
    

    应用框架

    下图所示为基于 Tuya Wi-Fi SDK 的应用框架:

    • Platform:所使用的芯片平台。芯片 + 协议栈由芯片公司维护。

    • Port:Tuya Wi-Fi SDK 所需要的抽象接口,需要用户根据具体的芯片平台移植实现。

    • Tuya Wi-Fi SDK : 封装了涂鸦 Wi-Fi 通信协议,提供构建涂鸦 Wi-Fi 应用所需的服务接口。

    • Application:基于 Tuya Wi-Fi SDK 构建的应用。

    • Tuya SDK API:API 用于设备实现 Wi-Fi 相关的管理、通信等,API 的调用将采用基于消息的异步机制,API 的执行结果将会以 Message 或者 Call back 的方式通知给设备的 Application。

    • SDK Config:Tuya Wi-Fi SDK 可裁剪可配置,通过配置文件中的宏定义可将 Tuya Wi-Fi SDK 设置成不同模式,例如配置成适用于多协议设备的通用配网模式、单模配网模式、是否使用 OS 等。

    • Main Process: Tuya SDK API 的主引擎。Application 需要一直调用,如果 Platform 架构是带 OS 的,Tuya Wi-Fi SDK 会基于 Port 层提供的 OS 相关接口自动创建一个任务用于执行 Main Process,如果是非 OS 平台,需要设备 Application 循环调用。

    • Message or Call back:SDK 通过 Message 或者设备 Application 注册的 Call back 函数向设备 Application 发送数据(状态、数据等)。

    驱动软件模块

    方案流程图

    • 模式选择功能流程图:

    • 按键功能流程图

    • 滤芯检测流程图

    功能实现

    外设驱动

    段码液晶屏

    段码液晶屏由 3*8 断码屏和 5 个指示灯构成,分别对应 PM2.5、PM10、TVOC、温度、湿度。

    其中外挂驱动芯片 TM1650,I2C 通信,本文 demo 中使用 2*GPIO 模拟 I2C 。

    段码液晶屏部分代码:

    /**
    * @brief: tuya_lcd_show_num
    * @desc: show num on lcd
    * @param[in] number:-99<number<999
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    OPERATE_RET tuya_lcd_show_num(IN INT_T number)
    {
        OPERATE_RET op_ret = OPRT_OK;
        MINUS_FLAG_E minus_flag = MINUS_NULL;
        UCHAR_T i = 0;
        UCHAR_T bit[3];
    
        if (number > MAX_NUM) {
            number = MAX_NUM;
        }
        if (number < MIN_NUM) {
            number = MIN_NUM;
        }
        if (number < 0) {
            if (number > -10) {  //-10<number<0
                minus_flag = MINUS_TEN;
            } else {             //-100<number<-10
                minus_flag = MINUS_HUNDRED;
            }
            number = number * (-1);
        }
    
        bit[0] = number / 100 % 10;  //hundred
        bit[1] = number / 10 % 10;   //ten
        bit[2] = number % 10;        //one
    
        /* hundred == 0 */
        if (bit[0] == 0) {
            if (minus_flag == MINUS_HUNDRED) {
                bit[0] = SEG_SHOW_MINUS;  //hundred bit show minus
            } else {
                bit[0] = SEG_SHOW_NULL;   //hundred bit show null
            }
        /* ten == 0 */
            if (bit[1] == 0) {
                if (minus_flag == MINUS_TEN) {
                    bit[1] = SEG_SHOW_MINUS;  //ten bit show minus
                } else {
                    bit[1] = SEG_SHOW_NULL;  //ten bit show null
                }
            }
        }
    
        /* i2c send command and data */
        for (i = 0; i < 3; i++) {
            op_ret = tuya_tm1650_app_write(dig[i], seg_num[bit[i]]); 
            if (op_ret != OPRT_OK) {
                PR_ERR("tuya_tm1650_app_write error:%d", op_ret);
                return op_ret;
            }
        }
        
        return op_ret;
    }
    
    /**
    * @brief: tuya_led_show
    * @desc: Choose the led that led show
    * @param[in] show_led: 0,1,2,3,4,5
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    OPERATE_RET tuya_led_show(IN CONST UCHAR_T show_led)
    {
        OPERATE_RET op_ret = OPRT_OK;
    
        //Check the input parameter, show_led: 0~5
        if (show_led > 5) {
            PR_ERR("show_led input parameter error!");
            return OPRT_INVALID_PARM;
        }
        
        /* i2c send command and data */
        op_ret = tuya_tm1650_app_write(dig[3], seg_led[show_led]); 
        if (op_ret != OPRT_OK) {
            PR_ERR("tuya_tm1650_app_write error:%d", op_ret);
            return op_ret;
        }
        return op_ret;
    }
    
    风扇电机

    采用空气净化器专用的直流无刷电机,模块主要引脚如下表。

    引脚 I/O 直流电压值 说明
    CLK IN VIH:4.5~5.0
    VIL:0.5V max
    CLK = 50~425 Hz
    Duty 50%
    FG OUT VIH:4.5~5.0
    VIL:0.5V max
    FG(Hz)= SPEED[r/min] * 15/60
    BRK IN VIH:4.5~5.0
    VIL:0.5V max
    Hi:BREAK ON (Motor Stop)
    Low:BREAK OFF (Motor Start)

    电源和 I/O 引脚输入步骤:

    开机:5V on > 24V on > CLK

    关机:BRK 输入 Hi > CLK 信号输入 Low > 24V off > 5V off

    风扇电机驱动部分代码

    /**
    * @brief: _tuya_motor_start
    * @desc: start motor to control fan speed
    * @param[in] percent:set fan speed percent, 0.2~1
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    STATIC OPERATE_RET _tuya_motor_start(IN CONST FLOAT_T percent)
    {
        OPERATE_RET op_ret = OPRT_OK;
        FLOAT_T speed_percent = 0;
    
        speed_percent = percent;
    
        //Check the input parameter, percent: 0.2~1
        if (speed_percent < 0.2) {
            speed_percent = 0.2;
        }
        if (speed_percent > 1) {
            speed_percent = 1;
        }
    
        /* Break pin off */
        tuya_gpio_write(MOROR_BRK_PIN, TRUE);
    
        /* Start motor pwm */
        op_ret = tuya_motor_pwm_start(430 * speed_percent); //430hz is max frequency
        if (op_ret != OPRT_OK) {
            PR_ERR("tuya_motor_pwm_start error:%d", op_ret);
            return op_ret;
        }
    
        return op_ret;
    }
    
    /**
    * @brief: tuya_motor_stop
    * @desc: stop motor
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    OPERATE_RET tuya_motor_stop(VOID_T)
    {
        OPERATE_RET op_ret = OPRT_OK;
    
        /* Break pin on */
        tuya_gpio_write(MOROR_BRK_PIN, FALSE);
    
        /* stop motor pwm */
        op_ret = tuya_pwm_stop(p_pwm_motor);
        if (op_ret != OPRT_OK) {
            PR_ERR("stop motor pwm error:%d", op_ret);
            return op_ret;
        }
    
        /* set g_fan_speed_percent value*/
        g_fan_speed_percent = 0;
    
        PR_DEBUG("ali, tuya_motor_stop suc");
        return op_ret;
    }
    

    应用功能

    空气指标获取

    向空气质量传感器发送命令,然后读取 UART 数据缓存,解析得到空气质量指标。

    空气指标获取部分代码:

    CONST UCHAR_T uart_buf_tx[4] = {0x11, 0x01, 0x16, 0xD8};
    
    /**
    * @brief: tuya_get_air_quality_index
    * @desc: get air quality index
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    STATIC OPERATE_RET tuya_get_air_quality_index(VOID_T)
    {
        OPERATE_RET op_ret = OPRT_OK;
        UINT_T tmp_len = 0, read_len = 0, read_time = 0;
        UCHAR_T uart_buf_rx[UART_BUFSZ];
        UINT_T cs_check_num = 0;
    
        /* write command 11 01 16 D8 */
        tuya_uart_write(p_uart0, uart_buf_tx, sizeof(uart_buf_tx));
    
        /* read the return data */
        do {
            tmp_len = tuya_uart_read(p_uart0, &uart_buf_rx[read_len], UART_BUFSZ - read_len);
            read_len += tmp_len;
            if (read_time++ > 20) {
                PR_ERR("uart no return data");
                return OPRT_COM_ERROR;
            }
            tuya_hal_system_sleep(10);
        } while (read_len < UART_BUFSZ);
    
        /* check frame header */
        if ((uart_buf_rx[0] != 0x16) || (uart_buf_rx[1] != 0x13) || (uart_buf_rx[2] != 0x16)) {
            PR_ERR("uart receive data error, frame header");
            return OPRT_COM_ERROR;
        }
    
        /* check cs check num */
        cs_check_num = 256 - tuya_get_check_sum(uart_buf_rx, UART_BUFSZ-1);
        if (uart_buf_rx[UART_BUFSZ-1] != cs_check_num) {
            PR_ERR("uart receive data error, cs check num");
            return OPRT_COM_ERROR;
        }
    
        /* set air quality index value */
        g_air_quality_index.pm25 = uart_buf_rx[9]*256 + uart_buf_rx[10];
        g_air_quality_index.pm10 = uart_buf_rx[11]*256 + uart_buf_rx[12];
        g_air_quality_index.tvoc = (uart_buf_rx[3]*256 + uart_buf_rx[4]);  //ppb,alitest
        g_air_quality_index.temperature = (uart_buf_rx[13]*256 + uart_buf_rx[14] - 500)/10;
        g_air_quality_index.humidity = (uart_buf_rx[15]*256 + uart_buf_rx[16])/10;
    
        return op_ret;
    }
    
    风扇自动调节

    风扇自动调节规则:

    PM2.5(ug/m3) 空气质量 风扇转速
    115+ 污染 保持最大风速运行,对应最大占空比,同时主控监测 PM2.5 的变化,如果一直在 115+,则一直最高档运行。
    75 - 115 微污染 80% 风速运行,同时检测 PM2.5 的变化,每隔 30s 检测一次 PM2.5 的变化数值,如果下降小于 1,则切换最大风速运行,在 PM2.5 到达 75 下之前,保持最大风速。
    35 - 75 60% 风速运行,同时检测 PM2.5 的变化,30s 内数值下降小于 1,则切换为 80% 风速,下个 30s 数值下降仍小于 1,则切换 100% 风速(如果大于 1,则保持 80% ),之后保持 100% 风速,直到 pm2.5 降到 35 以内。
    0 - 35 40% 风速运行,同时检测 PM2.5 变化,30s 数值如果下降超过 1,则维持风速运行;如果数值不降反升(超过 35 ),则切换为上述工作模式。

    滤芯寿命降低导致净化效果衰减,通过风速提升进行补偿:

    • 80~100%, 按上述规则

    • 50~80%, 风速提升 10%

    • 20~50%,风速提升 20%

    • 0-~20%,风速提升 30%

    风扇自动调节部分代码

    /**
    * @brief: tuya_auto_fan_speed_start
    * @desc: start auto fan speed
    * @param[in] start:
    *             TRUE: start
    *             FLASE: stop
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    OPERATE_RET tuya_auto_fan_speed_start(IN CONST BOOL_T start)
    {
        OPERATE_RET op_ret = OPRT_OK;
    
        if (FALSE == start) {
            tuya_pm25_scan_timer_stop();
            PR_DEBUG("stop auto fan speed");
            /* release flag */
            compare_flag.pm25_75_115 = FALSE;
            compare_flag.pm25_35_75 = FALSE;
            compare_flag.pm25_35_75_again = FALSE;
            motor_already_stop = FALSE;
            return op_ret;
        }
    
        /* release time */
        g_pm25_good_time = 0;
    
        /* scan pm2.5 value once */
        PR_DEBUG("start auto fan speed");
        tuya_pm25_scan();
    
        /* cycle scan pm2.5, 30s */
        tuya_pm25_scan_timer_start();
    
        return op_ret;
    }
    
    /**
    * @brief: tuya_pm25_scan
    * @desc: scan pm2.5 value once
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    OPERATE_RET tuya_pm25_scan(VOID_T)
    {
        OPERATE_RET op_ret = OPRT_OK;
        PM25_VALUE_RANGE_E pm25_value_range;
        FLOAT_T speed_percent = 0;
        UINT_T pm25_new = 0;
    
        pm25_new = g_air_quality_index.pm25;
        g_pm25_reduction = g_pm25_last - pm25_new;
        g_pm25_last = pm25_new;
    
        /* Check whether to into the dark sleep mode */
        if (TRUE == stop_auto_speed_flag) {
            return op_ret;
        }
    
        /* Check whether to into the environmental mode */
        if (pm25_new < 35) {
            //hold for 2 hours, 240*30 s
            if (g_pm25_good_time++ >= 240) {
                if (g_pm25_good_time > 1000) {
                    g_pm25_good_time = 1000;
                }
                if (FALSE == motor_already_stop) {
                    tuya_motor_stop();
                    motor_already_stop = TRUE;
                }
                return op_ret;
            }
        } else {
            g_pm25_good_time = 0;
            motor_already_stop = FALSE;
        }
    
        /* Check pm2.5 value range */
        if (pm25_new >= 115) {
            pm25_value_range = PM25_RANGE_115;
        } else if (pm25_new >= 75) {
            pm25_value_range = PM25_RANGE_75_115;
        } else if (pm25_new >= 35) {
            pm25_value_range = PM25_RANGE_35_75;
        } else {
            pm25_value_range = PM25_RANGE_35;
        }
    
        /* control fan speed */
        switch (pm25_value_range)
        {
            /* pm2.5: 115+ */
            case PM25_RANGE_115:
                speed_percent = 1;
                break;
            /* pm2.5: 75-115 */
            case PM25_RANGE_75_115:
                if (TRUE == compare_flag.pm25_35_75_again) {
                    speed_percent = 1;
                } else {
                    speed_percent = 0.8;
                }
    
                if (TRUE == compare_flag.pm25_75_115) {
                    speed_percent = tuya_pm25_compare_75_115();
                } else {
                    compare_flag.pm25_75_115 = TRUE;
                }
                break;
            /* pm2.5: 35-75 */
            case PM25_RANGE_35_75:
                compare_flag.pm25_75_115 = FALSE;
                speed_percent = 0.6;
                if (TRUE == compare_flag.pm25_35_75) {
                    speed_percent = tuya_pm25_compare_35_75();
                    compare_flag.pm25_35_75 = FALSE;
                } else if ((TRUE == compare_flag.pm25_35_75_again)) {
                    speed_percent = tuya_pm25_compare_35_75_again();
                } else {
                    compare_flag.pm25_35_75 = TRUE;
                }
                break;
            /* pm2.5: 0-35 */
            case PM25_RANGE_35:
                compare_flag.pm25_75_115 = FALSE;
                compare_flag.pm25_35_75_again = FALSE;
                speed_percent = 0.4;
                break;  
            default:
                break;
        }
    
        /* start fan motor */
        op_ret = tuya_motor_start(speed_percent);
        if (OPRT_OK != op_ret) {
            PR_ERR("tuya_motor_start error:%d", op_ret);
            return op_ret;
        }  
    
        return op_ret;
    }
    
    滤芯 NFC 检测

    NFC 读卡器芯片为 MFRC522,滤芯上的 NFC 卡片类型为 NXP MIFARE Ultralight,内存 180 bytes,45 页(4 bytes 每页)。

    NFC 读卡器的步骤:寻卡 > 防冲撞 > 选卡 > 校验密码 > 读写卡片。Mifare Ultralight 类型卡片没有密码认证(Mifare One 类型卡片支持密码认证),所以使用 Mifare Ultralight 类型卡片的话就可以跳过校验密码步骤。

    滤芯 NFC 卡片信息数据协议定义如下表:

    内存读写地址 长度(byte) 说明
    12 页 4 Data[0]:标识,0x74,“t“表示 tuya。
    Data[1]:Type,滤芯的类型。0x00 表示标准。0x01 表示抗菌。0x02 表示除醛。 0x03 表示专业。
    Data[2]:滤芯 ID,高位
    Data[3]:滤芯 ID,低位
    13 页 4 Data[0]:生产年份,0x00 表示 2000 年
    Data[1]:生产月份,从 1 开始到 12 结束
    Data[2]:生产日份,从 1 开始到 31 结束
    Data[3]:保留

    滤芯 NFC 检测部分代码

    /**
    * @brief: tuya_nfc_detect_filter
    * @desc: nfc detect to read filter message
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    STATIC OPERATE_RET tuya_nfc_detect_filter(VOID_T)
    {
        OPERATE_RET op_ret = OPRT_OK;
        UCHAR_T i = 0;
        UCHAR_T page = 0;
        UCHAR_T read_buf[16] = {0};
        UCHAR_T TagType[2];
        UCHAR_T SelectedSnr[4];
    
        /* mfrc522 reset, and open antenna */
        PcdReset();
    	PcdAntennaOff(); 
    	PcdAntennaOn(); 
    
        /* look for card */
        op_ret = PcdRequest(0x52, TagType);
        if (OPRT_OK != op_ret) {
            PR_INFO("PcdRequest err:%d", op_ret);
            PR_INFO("Do not find NFC Card, filter do not install");
            return op_ret;
        }
    
        /* printf the type of card */
        PR_DEBUG_RAW("TagType: 0x");
        for (i = 0; i < 2; i++) {
            PR_DEBUG_RAW("%02X", TagType[i]);
        }
        PR_DEBUG_RAW("\n");
    
        /* card collision protection */
        op_ret = PcdAnticoll(SelectedSnr);
        if (OPRT_OK != op_ret) {
            PR_INFO("PcdAnticoll err:%d", op_ret);
            PR_INFO("Do not find NFC Card, filter do not install");
            return op_ret;
        }
    
        /* select card */
        op_ret = PcdSelect(SelectedSnr);
        if (OPRT_OK != op_ret) {
            PR_INFO("PcdSelect err:%d", op_ret);
            PR_INFO("Do not find NFC Card, filter do not install");
            return op_ret;
        }
    
        /* read card */
        PR_DEBUG("Find NFC Card, the filter is install ok");
        PR_DEBUG_RAW("Read NFC Card data:\n");
        for (page = 0; page < 45; page +=4) {
            //read
            op_ret = PcdRead(page, read_buf);
            if (OPRT_OK != op_ret) {
                PR_ERR("PcdRead err:%d, page:%d~%d", op_ret, page, (page + 3));
            }
            //get filter message
            if (CARD_READ_PAGE_ADDR == page) {
                memset(filter_buf, 0x00, CARD_READ_BYTE_LEN);
                for (i = 0; i < CARD_READ_BYTE_LEN; i++) {
                    filter_buf[i] = *(read_buf + i);
                }
            }
            //printf read buf
            PR_DEBUG_RAW("  ");
            for (i = 0; i < 16; i++) {
                PR_DEBUG_RAW("%02X ", read_buf[i]);
            }
            PR_DEBUG_RAW("----- page: %d~%d\n", page, (page + 3));     
        }
        PR_DEBUG_RAW("\n");
    
        op_ret = tuya_analyze_CardData_to_get_filter_msg();
        if (OPRT_OK != op_ret) {
            return op_ret;
        }
    
        return op_ret;
    }
    
    /**
    * @brief: tuya_analyze_CardData_to_get_filter_msg
    * @desc: analyze CardData to get filter msg
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    STATIC OPERATE_RET tuya_analyze_CardData_to_get_filter_msg(VOID_T)
    {
        OPERATE_RET op_ret = OPRT_OK;
        UCHAR_T i = 0;
    
        /* check filter_buf */
        if (NULL == filter_buf) {
            PR_ERR("filter_buf is error");
            return OPRT_INVALID_PARM;
        }
    
        /* printf filter buf */
        PR_DEBUG_RAW("filter buf data: ");
        for (i = 0; i < CARD_READ_BYTE_LEN; i++) {
            PR_DEBUG_RAW("%02X ", filter_buf[i]);
        }
        PR_DEBUG_RAW("\n");
    
        /* check if filter is piratic */
        if (CARD_READ_HEAD_TAG != filter_buf[0]) {
            PR_INFO("Do not have tuya tag, filter is piratic");
            return OPRT_COM_ERROR;
        }
    
        /* get filter type */
        switch (filter_buf[1]) {
        case FILTER_STANDARD:
            g_filter_card_msg.type = FILTER_STANDARD;
            PR_DEBUG_RAW("filter type: standard\n");
            break;
        case FILTER_ANTIBACTERIAL:
            g_filter_card_msg.type = FILTER_ANTIBACTERIAL;
            PR_DEBUG_RAW("filter type: antibacterial\n");
            break;
        case FILTER_ALDEHYDE_REMOVAL:
            g_filter_card_msg.type = FILTER_ALDEHYDE_REMOVAL;
            PR_DEBUG_RAW("filter type: aldehyde removal\n");
            break;
        case FILTER_PROFESSIONAL:
            g_filter_card_msg.type = FILTER_PROFESSIONAL;
            PR_DEBUG_RAW("filter type: professional\n");
            break;
        default:
            PR_INFO("filter type buf data is erro, maybe filter is piratic");
            return OPRT_COM_ERROR;
            break;
        }
        
        /* get filter ID */
        g_filter_card_msg.ID = filter_buf[2] * 256 + filter_buf[3];
        PR_DEBUG_RAW("filter ID: %d\n", g_filter_card_msg.ID);
    
        /* get filter product date */
        g_filter_card_msg.product_date.year  = filter_buf[4] + 2000;
        g_filter_card_msg.product_date.month = filter_buf[5];
        g_filter_card_msg.product_date.day   = filter_buf[6];
        PR_DEBUG_RAW("filter product date: %d-%d-%d\n", filter_buf[4]+2000, filter_buf[5], filter_buf[6]);
    
        /* DP report */
        tuya_update_single_dp(FILTER_TYPE_DPID, PROP_ENUM, g_filter_card_msg.type);
        /* check whether the filter is new */
        tuya_check_whether_the_filter_is_new();
    
        return op_ret;
    }
    
    滤芯寿命计算

    滤芯剩余寿命算法

    说明:

    • 根据公式,只要知道当前滤芯粉尘的吸附量和 pm2.5 颗粒浓度,就可以算出当前粉尘吸附量。

    • 每 1 分钟计算一次粉尘吸附量并累加。

    • 当空气净化器收到关机命令时或者每过 24h,系统会将此前累加的粉尘吸附量存入 FLASH,下次开机可以继续累加。

    • 滤芯寿命根据每周粉尘累加的吸附量上报一次。

    注: 若设备发生断电情况,可能会产生 0~1 天的误差。

    滤芯剩余可用天数算法

    粉尘吸附量累加一周后,就可以得到粉尘吸附速度的信息。根据粉尘吸附的速度和滤芯剩余的寿命,就可以估算出滤芯剩余可用天数。

    滤芯寿命计算部分代码

    /**
    * @brief: tuya_filter_1minute_task
    * @desc: filter 1minute task
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    STATIC VOID_T tuya_filter_1minute_task(VOID_T)
    {
        STATIC UINT_T uv_close_time = 240;
        STATIC UINT_T s_tick;
    
        while(1) 
        {
            UINT_T fan_speed_percent = 0;
            
            /* check whether air cleaner power on */
            if (POWER_ON == tuya_get_power_status()) {
                //get current fan_speed
                fan_speed_percent = tuya_get_fan_speed_percent();
                //figure pm_total
                g_filter_pm_total = g_filter_pm_total + (FLOAT_T)fan_speed_percent/100 * FILTER_FIGURE_CONST;
                g_filter_msg.pm_total = g_filter_msg.pm_total + (FLOAT_T)fan_speed_percent/100 * FILTER_FIGURE_CONST;
    
                //DP report, pm_total
                tuya_update_single_dp(PM_TOTAL_DPID, PROP_VALUE, g_filter_msg.pm_total/1000);  //ug -> mg
                //DP report, runtime_total
                tuya_update_single_dp(RUNTIME_TOTAL_DPID, PROP_VALUE, ++g_runtime_total);  //runtime_total add
            }
    
            /* enter every 60 minutes, 1hour */
            if (s_tick++ >= 60){
                s_tick = 0;
                tuya_filter_1hour_task();
            }
    
            /* if uv close time more than 240min, scan for uv funtion */
            if (FALSE == tuya_get_uv_status) {
                if (uv_close_time++ >= 240) {
                    tuya_uv_funtion_scan();
                    if (uv_close_time > 1000) {
                        uv_close_time = 1000;
                    }
                }
            } else {
                uv_close_time = 0; 
            }
    
            tuya_hal_system_sleep(FILTER_TASK_DELAY_TIME); //1min   
        }
    }
    
    /**
    * @brief: tuya_filter_1hour_task
    * @desc: filter 1hour task
    * 
    * @return OPERATE_RET  OPRT_OK is sucess, other is fail
    */
    STATIC VOID_T tuya_filter_1hour_task(VOID_T)
    {
        POSIX_TM_S local_time;
        UINT_T now_days = 0;
    
        uni_local_time_get(&local_time);
        now_days = local_time.tm_year*365 + local_time.tm_mon*30 + local_time.tm_mday;
    
        if (g_now_days == 0) {
            g_now_days = now_days;
        }
    
        /* if days pass */
        if (g_now_days < now_days) {
            /* figure filter_use_days */
            g_filter_use_days = g_filter_use_days + (now_days - g_now_days);
            /* figure air cleaner run_days */
            g_run_days = g_run_days + (now_days - g_now_days);
    
            /* if air cleaner run_days more than 7 days */
            if (g_run_days >= 7) {
                //figure filter_life
                g_filter_msg.filter_life = (1 - (FLOAT_T)g_filter_pm_total / FILTER_MAX_PM_CAN_ABSORB) * 100;
                //figure filter_days
                if (g_filter_pm_total == 0) {
                    g_filter_msg.filter_days = 180;
                } else {
                    g_filter_msg.filter_days = ((FILTER_MAX_PM_CAN_ABSORB * g_filter_use_days) / g_filter_pm_total) - g_filter_use_days;
                }
                
                //DP report
                if (g_filter_msg.filter_days > 1000) {
                    g_filter_msg.filter_days = 1000;
                }
                tuya_update_single_dp(FILTER_DAYS_DPID, PROP_VALUE, g_filter_msg.filter_days);  //filter_days
                tuya_update_single_dp(FILTER_LIFE_DPID, PROP_VALUE, g_filter_msg.filter_life);  //filter_life
    
                g_run_days = 0;
            }
    
            g_now_days = now_days;
    
            /* write save data in flash */
            tuya_write_save_data_in_flash();
        }
    }