多功能智能感应台灯设计(嵌入式)

Posted 三明治开发社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多功能智能感应台灯设计(嵌入式)相关的知识,希望对你有一定的参考价值。

一、产品创建

  • 进入涂鸦智能IoT平台,点击创建产品,选择照明->氛围照明->台灯。
    创建产品

  • 选择自定义方案,输入产品名称,选择通讯协议为WIFI+蓝牙,点击创建产品。

  • 根据要实现的设备功能,创建好DP功能点。
    创建功能点

  • 设定完功能点后,下一步点击设备面板,选择app的面板样式。推荐选择开发调试面板,比较直观,且可以开到dp数据包的接收和发送,方便开发阶段调试使用。

至此,产品的创建基本完成,可以正式开始嵌入式软件部分的开发。

二、软件方案介绍

嵌入式代码基于BK7231n平台,使用涂鸦通用Wi-Fi SDK进行SOC开发,具体代码可下载查看demo例程。

1.应用层入口

打开demo例程,其中的apps文件夹内就是demo的应用代码。应用代码结构如下:

├── src	
|    ├── app_driver
|    |    ├── lamp_pwm.c           //台灯PWM驱动相关文件
|    |    ├── sh1106.c             //OLED屏驱动相关文件
|    |    ├── bh1750.c             //光照强度传感器驱动相关文件
|    |    └── app_key.c            //触摸按键相关代码文件
|    ├── app_soc                   //tuya SDK soc层接口相关文件
|    ├── tuya_device.c             //应用层入口文件
|    ├── app_lamp.c            //主要应用层
|    └── lamp_control.c             //按键相关逻辑
|
├── include				//头文件目录
|    ├── app_driver
|    |    ├── lamp_pwm.h       
|    |    ├── sh1106.h
|    |    ├── bh1750.h           
|    |    └── app_key.h           
|    ├── app_soc
|    ├── tuya_device.h
|    ├── app_lamp.h
|    └── lamp_control.h
|
└── output              //编译产物

打开tuya_device.c文件,找到device_init函数:

OPERATE_RET device_init(VOID_T) 
{
  OPERATE_RET op_ret = OPRT_OK;

  TY_IOT_CBS_S wf_cbs = {
      status_changed_cb,\\
      gw_ug_inform_cb,\\
      gw_reset_cb,\\
      dev_obj_dp_cb,\\
      dev_raw_dp_cb,\\
      dev_dp_query_cb,\\
      NULL,
  };

  op_ret = tuya_iot_wf_soc_dev_init_param(WIFI_WORK_MODE_SEL, WF_START_SMART_FIRST, &wf_cbs, NULL, PRODECT_ID, DEV_SW_VERSION);
  if(OPRT_OK != op_ret) {
      PR_ERR("tuya_iot_wf_soc_dev_init_param error,err_num:%d",op_ret);
      return op_ret;
  }

  op_ret = tuya_iot_reg_get_wf_nw_stat_cb(wf_nw_status_cb);
  if(OPRT_OK != op_ret) {
      PR_ERR("tuya_iot_reg_get_wf_nw_stat_cb is error,err_num:%d",op_ret);
      return op_ret;
  }

  op_ret = app_lamp_init(APP_LAMP_NORMAL);
  if(OPRT_OK != op_ret) {
      PR_ERR("app init err!");
      return op_ret;
  }
  
  return op_ret;
}

在BK7231平台的SDK环境中,该函数为重要的应用代码入口,设备上电后平台适配层运行完一系列初始化代码后就会调用该函数来进行应用层的初始化操作。

该函数做了三件事:

  • 调用tuya_iot_wf_soc_dev_init_param()接口进行SDK初始化,配置了工作模式、配网模式,同时注册了各种回调函数并存入了PID(代码中宏定义为PRODECT_KEY)。
   TY_IOT_CBS_S wf_cbs = {
       status_changed_cb,\\
       gw_ug_inform_cb,\\
       gw_reset_cb,\\
       dev_obj_dp_cb,\\
       dev_raw_dp_cb,\\
       dev_dp_query_cb,\\
       NULL,
   };

   op_ret = tuya_iot_wf_soc_dev_init_param(WIFI_WORK_MODE_SEL, WF_START_SMART_FIRST, &wf_cbs, NULL, PRODECT_ID, DEV_SW_VERSION);
   if(OPRT_OK != op_ret) {
       PR_ERR("tuya_iot_wf_soc_dev_init_param error,err_num:%d",op_ret);
       return op_ret;
   }
  • 调用tuya_iot_reg_get_wf_nw_stat_cb()接口注册设备网络状态回调函数。
   op_ret = tuya_iot_reg_get_wf_nw_stat_cb(wf_nw_status_cb);
   if(OPRT_OK != op_ret) {
       PR_ERR("tuya_iot_reg_get_wf_nw_stat_cb is error,err_num:%d",op_ret);
       return op_ret;
   }
  • 调用应用层初始化函数
   op_ret = app_lamp_init(APP_LAMP_NORMAL);
   if(OPRT_OK != op_ret) {
       PR_ERR("app init err!");
       return op_ret;
   }

2.应用结构

本demo应用代码主要分三层来实现:

  • 最底层为一些外设、传感器的驱动代码,例如光照传感器、OLED屏幕、微波雷达、触摸按键、灯板等,封装出常用接口;
  • 第二层为控制逻辑部分的代码,调用驱动层的各类接口,实现各个组件的控制逻辑,封装出数据处理轮询接口;
  • 第一层为主要应用层,创建应用任务调用第二层的接口,同时处理DP点数据的上报和接受解析。

第一层就是在app_lamp.c文件中实现的,大致内容如下:

  • app_lamp_init() 调用第二层封装出的设备初始化接口,创建应用任务;
OPERATE_RET app_lamp_init(IN APP_LAMP_MODE mode)
{
    OPERATE_RET op_ret = OPRT_OK;

    if(APP_LAMP_NORMAL == mode) {
        
        lamp_device_init();

        //create ADC sensor data collection thread
        tuya_hal_thread_create(NULL, "thread_data_get", 512*4, TRD_PRIO_4, sensor_data_get_thread, NULL);

        tuya_hal_thread_create(NULL, "thread_data_deal", 512*4, TRD_PRIO_4, sensor_data_deal_thread, NULL);

        tuya_hal_thread_create(NULL, "key_scan_thread", 512*4, TRD_PRIO_4, key_scan_thread, NULL);

        tuya_hal_thread_create(NULL, "thread_data_report", 512*4, TRD_PRIO_4, sensor_data_report_thread, NULL);
    }else {
        //not factory test mode
    }

    return op_ret;
}
  • app_report_all_dp_status()用于上报所有DP数据:
VOID app_report_all_dp_status(VOID)
{
   OPERATE_RET op_ret = OPRT_OK;

   INT_T dp_cnt = 0;
   dp_cnt = 5;

   TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(dp_cnt*SIZEOF(TY_OBJ_DP_S));
   if(NULL == dp_arr) {
       PR_ERR("malloc failed");
       return;
   }

   memset(dp_arr, 0, dp_cnt*SIZEOF(TY_OBJ_DP_S));

   dp_arr[0].dpid = DPID_DELAY_OFF;
   dp_arr[0].type = PROP_BOOL;
   dp_arr[0].time_stamp = 0;
   dp_arr[0].value.dp_value = lamp_ctrl_data.Lamp_delay_off;

   dp_arr[1].dpid = DPID_LIGHT_MODE;
   dp_arr[1].type = PROP_ENUM;
   dp_arr[1].time_stamp = 0;
   dp_arr[1].value.dp_value = lamp_ctrl_data.Light_mode;

   dp_arr[2].dpid = DPID_SIT_REMIND;
   dp_arr[2].type = PROP_BOOL;
   dp_arr[2].time_stamp = 0;
   dp_arr[2].value.dp_value = lamp_ctrl_data.Sit_remind;

   dp_arr[3].dpid = DPID_AUTO_LIGHT;
   dp_arr[3].type = PROP_BOOL;
   dp_arr[3].time_stamp = 0;
   dp_arr[3].value.dp_value = lamp_ctrl_data.Auto_light;

   dp_arr[4].dpid = DPID_LOW_POW_ALARM;
   dp_arr[4].type = PROP_BOOL;
   dp_arr[4].time_stamp = 0;
   dp_arr[4].value.dp_value = lamp_ctrl_data.Low_pow_alarm;

   op_ret = dev_report_dp_json_async(NULL,dp_arr,dp_cnt);
   Free(dp_arr);
   if(OPRT_OK != op_ret) {
       PR_ERR("dev_report_dp_json_async relay_config data error,err_num",op_ret);
   }

   PR_DEBUG("dp_query report_all_dp_data");
   return;
}
  • 任务函数,任务内循环调用的lamp_get_sensor_data()lamp_key_poll()lamp_ctrl_handle()都是第二层的接口,实现在lamp_control.c文件中,分别负责传感器数据的采集,触摸按键扫描轮询及数据处理和功能逻辑实现轮询:
STATIC VOID sensor_data_get_thread(PVOID_T pArg)
{   
    while(1) {
        PR_DEBUG("sensor_data_get_thread");
        lamp_get_sensor_data();
        tuya_hal_system_sleep(TASKDELAY_SEC/2);
        
    }
}

STATIC VOID key_scan_thread(PVOID_T pArg)
{   


    while(1) {
        lamp_key_poll();
       
        tuya_hal_system_sleep(25);       
    }

}

STATIC VOID sensor_data_deal_thread(PVOID_T pArg)
{   
    while(1) {
        lamp_ctrl_handle();
        tuya_hal_system_sleep(TASKDELAY_SEC);
        
    }
}

STATIC VOID sensor_data_report_thread(PVOID_T pArg)
{   
    while(1) {

        tuya_hal_system_sleep(TASKDELAY_SEC*10);

        app_report_all_dp_status();
    }

}
  • deal_dp_proc()处理接受到的DP数据,通过识别DP id来进行相应的数据接收处理:
VOID deal_dp_proc(IN CONST TY_OBJ_DP_S *root)
{
    UCHAR_T dpid;

    dpid = root->dpid;
    PR_DEBUG("dpid:%d",dpid);
    
    switch (dpid) {
    
    case DPID_DELAY_OFF:
        PR_DEBUG("set led switch:%d",root->value.dp_bool);
        lamp_ctrl_data.Lamp_delay_off = root->value.dp_bool;
        break;
        
    case DPID_LIGHT_MODE:
        PR_DEBUG("set light mode:%d",root->value.dp_enum);
        lamp_ctrl_data.Light_mode = root->value.dp_enum;
        break;
    
    case DPID_SIT_REMIND:
        PR_DEBUG("set sit remind switch:%d",root->value.dp_bool);
        lamp_ctrl_data.Sit_remind = root->value.dp_bool;
        break;

    case DPID_AUTO_LIGHT:
        PR_DEBUG("set auto switch:%d",root->value.dp_bool);
        lamp_ctrl_data.Auto_light = root->value.dp_bool;
        break;
    
    case DPID_LOW_POW_ALARM:
        PR_DEBUG("set low power alarm switch:%d",root->value.dp_bool);
        lamp_ctrl_data.Low_pow_alarm = root->value.dp_bool;
        break;

    default:
        break;
    }
    
    app_report_all_dp_status();

    return;

}

实现了上述的几个函数后,应用层代码的大概结构就已经确定下来了,接下来就需要实现上面提到的被调用的第二层接口,这些接口都放在本demo的lamp_control.c文件中。在下面的内容里,本篇文档将根据实现的具体功能解说demo例程。

3.触摸按键

​ 本demo硬件设计上留出了四个触摸按键,而需要实现灯的开关、灯光颜色切换、档位调光和无极调光、静音模式开启关闭、延时关灯等等按键功能,仅靠四个按键单一的一次触发对应一种功能是远远不够的。因此,需要实现长按、短按和组合键三种不同的触发方式来应对多种按键功能。

​ 在app_key.c文件中,封装了app_key_init()app_key_scan()两个函数。app_key_init()用于初始化按键IO,app_key_scan()用于扫描按键按下情况获取键值。

void app_key_scan(unsigned char *trg,unsigned char *cont)
{
    unsigned char read_data = 0x00;
    read_data = (tuya_gpio_read(KEY_SWITCH_PIN)<<3)|(tuya_gpio_read(KEY_SET_PIN)<<2)|(tuya_gpio_read(KEY_UP_PIN)<<1)|(tuya_gpio_read(KEY_DOWN_PIN));
    *trg = (read_data & (read_data ^ (*cont)));
    *cont = read_data;

}

该函数会检测四个按键的按下情况,然后将键值赋值给传参,其中trg是在整个按键按下动作中只出现一次的键值,而cont是只在按键松手后才会复位的键值。通过这两种不同特效的键值,就可以实现长短按和组合键的功能。

按键触发后对应的具体功能实现在lamp_control.c中,分两个函数:

STATIC VOID lamp_key_event(UINT8_T key_event)
{
    if(key_event == KEY_CODE_SWITCH) {
        PR_NOTICE("--------------POWER ON!!!!!!!-------------");
        if(lamp_ctrl_data.Lamp_switch == FALSE) {
            lamp_ctrl_data.Lamp_switch = TRUE;
            lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty); 
        }else{
            lamp_ctrl_data.Lamp_switch = FALSE;
            lamp_pwm_off();    
        }
    }else if(key_event == KEY_CODE_SET_LIGHT_COLOR) {
        lamp_ctrl_data.Light_mode++;
        if(lamp_ctrl_data.Light_mode > 2){
            lamp_ctrl_data.Light_mode = 0;
        }
        lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
        PR_NOTICE("-----------change light mode to %d-------------",lamp_ctrl_data.Light_mode);
    }
    else if(key_event == KEY_CODE_UP) {
        if(user_pwm_duty != 600) {
            if(user_pwm_duty > 400){
                user_pwm_duty = 600;
            }else{
                user_pwm_duty += 200;
            }
            lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
            PR_NOTICE("-----------PWM_VALUE UP ONCE-------------");
        }
    }
    else if(key_event == KEY_CODE_DOWN) {
        if(user_pwm_duty != 0) {
            if(user_pwm_duty < 200){
                user_pwm_duty = 0;
            }else{
                user_pwm_duty -= 200;
            }
            lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
            PR_NOTICE("-----------PWM_VALUE DOWN ONCE-------------");
        }
    }
    else if(key_event == KEY_CODE_SET_BEEP) {
        lamp_ctrl_data.Silent_mode = !lamp_ctrl_data.Silent_mode;
        PR_NOTICE("-----------SET BEEP-------------");
    }
        __ctrl_beep(100);
}

VOID lamp_key_poll(VOID)
{
    app_key_scan(&key_trg,&key_cont);

    switch (key_cont)
    {
    case KEY_CODE_RELEASE:
        if(key_buf != 0) {
            lamp_key_event(key_buf);
        }
        key_buf = 0;
        key_old = KEY_CODE_RELEASE;
        break;
    case KEY_CODE_SWITCH:
        vTaskDelay(10);
        app_key_scan(&key_trg,&key_cont);
        if(key_cont == KEY_CODE_SWITCH) {
            key_buf = KEY_CODE_SWITCH;
        }
        key_old = KEY_CODE_SWITCH;
        break;
    case KEY_CODE_SET_LIGHT_COLOR:
        if(lamp_ctrl_data.Lamp_switch == FALSE) {
            key_buf = 0;
            return ;
        }
        vTaskDelay(10);
        app_key_scan(&key_trg,&key_cont);
        if(key_cont == KEY_CODE_SET_LIGHT_COLOR) {
            key_buf = KEY_CODE_SET_LIGHT_COLOR;
        }
        key_old = KEY_CODE_SET_LIGHT_COLOR;
        break;
    case KEY_CODE_UP:
        if(lamp_ctrl_data.Lamp_switch == FALSE) {
            key_buf = 0;
            return ;
        }
        if(key_old == KEY_CODE_UP) {
            key_delay_cont++;
        }else{
            key_delay_cont = 0;
        }

        if(key_delay_cont >= 2) {
            key_buf = KEY_CODE_UP;
        }

        if(key_delay_cont >= 40) {
            key_buf = 0;
            if(user_pwm_duty <= 590) {
                user_pwm_duty += 10;
                lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
            }
        }
        
        key_old = KEY_CODE_UP;
        break;
    case KEY_CODE_DOWN:
        if(lamp_ctrl_data.Lamp_switch == FALSE) {
            key_buf = 0;
            return ;
        }
        if(key_old == KEY_CODE_DOWN) {
            key_delay_cont++;
        }else{
            key_delay_cont = 0;
        }

        if(key_delay_cont >= 2) {
            key_buf = KEY_CODE_DOWN;
        }

        if(key_delay_cont >= 40) {
            key_buf = 0;
            if(user_pwm_duty>=10) {
                user_pwm_duty -= 10;
                lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
            } 
        }
        
        key_old = KEY_CODE_DOWN;        
        break;
    case KEY_CODE_SET_BEEP:
        vTaskDelay(10);
        app_key_scan(&key_trg,&key_cont);
        if(key_cont == KEY_CODE_SET_BEEP) {
            key_buf = KEY_CODE_SET_BEEP;
        }
        break;
    case KEY_CODE_DELAY_OFF:
        
        break;           
    default:
        break;
    }
 
}

4.时间显示

​ 本demo通过tuya SDK的接口可以在联网后获取本地时间,并显示在OLED屏幕上。屏幕SH1106的驱动和封装的接口都在sh1106.c文件中,以软件实现的iic来驱动屏幕。封装的接口有以下几个:

  • tuya_sh1106_init()屏幕驱动初始化,传参为指定做为SDA、SCL脚的IO口:
UCHAR_T tuya_sh1106_init(sh1106_init_t* param)
{
    UCHAR_T error = 0;

    int opRet = -1;

    i2c_pin_t i2c_config = {
        .ucSDA_IO = param ->SDA_PIN,
        .ucSCL_IO = param ->SCL_PIN,
    };
    opRet = opSocI2CInit(&i2c_config);          /* SDA&SCL GPIO INIT */
    PR_NOTICE("SocI2CInit = %d",opRet);

    UCHAR_T i;
    for(i = 0; i  < 25; i++) {
        sh1106_send_cmd(oled_init_cmd[i]);
    }
}
  • tuya_sh1106_full()tuya_sh1106_clear()显示屏幕全亮或清空显示内容:
VOID tuya_sh1106_full(VOID)
{    
    UCHAR_T i,j,k;
    UCHAR_T *p;
    for(i = 0; i < 4; i++) {
        for(j = 0; j < 16; j++) {
            OLED_GRAM[i][j] = full_buff;
        }
    }
    for(i = 0; i < OLED_PAGE_NUMBER; i++) {
        sh1106_page_set(i);
        sh1106_column_set(0);	  
        for(j = 0; j < (OLED_COLUMN_NUMBER/8); j++) {
            p = OLED_GRAM[i][j];
            for(k = 0; k < 8; k++) {
                sh1106_send_data(*p);
                p++;
            }
        }
    }
}

VOID tuya_sh1106_clear(VOID)
{    
    UCHAR_T i,j,k;
    UCHAR_T *p;
    for(i = 0; i < 4; i++) {
        for(j = 0; j < 16; j++) {
            OLED_GRAM[i][j] = clear_buff;
        }
    }
    for(i = 0; i < OLED_PAGE_NUMBER; i++) {
        sh1106_page_set(i);
        sh1106_column_set(0);	  
        for(j = 0; j < (OLED_COLUMN_NUMBER/8); j++) {
            p = OLED_GRAM[i][j];
            for(k = 0; k < 8; k++) {
                sh1106_send_data(*p);
                p++;
            }
        }
    }
}
  • tuya_sh1106_gram_point_set()按坐标点更改显存内容,改变将要显示的图案,传参分别为页数、行数及字模缓存数组的首地址:
VOID tuya_sh1106_gram_point_set(UCHAR_T x, UCHAR_T y, CONST UCHAR_T *ptr_pic)
{   
    UCHAR_T i;
    UCHAR_T *p;

    if((x < 4)&&(y < 16)) {
        OLED_GRAM[x][y] = ptr_pic;
    }
    p = OLED_GRAM[x][y];
    
    sh1106_page_set(x);
    sh1106_column_set(y*8);

    for(i = 0; i < 8; i++) {
        sh1106_send_data(*p);
        p++;
    }
}
  • tuya_sh1106_display()根据显存内容显示图像:
VOID tuya_sh1106_gram_point_set(UCHAR_T x, UCHAR_T y, CONST UCHAR_T *ptr_pic)
{   
    UCHAR_T i;
    UCHAR_T *p;

    if((x < 4)&&(y < 16)) {
        OLED_GRAM[x][y] = ptr_pic;
    }
    p = OLED_GRAM[x][y];
    
    sh1106_page_set(x);
    sh1106_column_set(y*8);

    for(i = 0; i < 8; i++) {
        sh1106_send_data(*p);
        p++;
    }
}
  • tuya_sh1106_on()tuya_sh1106_off()点亮屏幕和熄灭屏幕,由于屏幕的点亮和熄灭需要时间,所以调用前后需要至少150ms的延时:
VOID tuya_sh1106_on(VOID)
{
    sh1106_send_cmd(0x8D);
    sh1106_send_cmd(0x14);
    sh1106_send_cmd(0xAF);
}

VOID tuya_sh1106_off(VOID)
{
    sh1106_send_cmd(0x8D);
    sh1106_send_cmd(0x10);
    sh1106_send_cmd(0xAE);
}

完成屏幕驱动后,就可以使用取模软件将时间转换出字模来显示。

  • 要获取本地时间,首先需包含头文件uni_time.h
  • 定义一个本地时间结构体变量,然后作为传参调用uni_local_time_get()接口获取时间:
    POSIX_TM_S cur_time; 
	
    if( uni_local_time_get(&cur_time) != OPRT_OK ) {
        PR_NOTICE("cant get local time");
    }
    lamp_ctrl_data.time_hour = cur_time.tm_hour;
    lamp_ctrl_data.time_min = cur_time.tm_min;
  • 按位解析时间,将对应字模缓存传入显存,然后开启显示:
   
    for(i = 4; i < 8; i++) {
        tuya_sh1106_gram_point_set(0,i,&diplay_buffer_time[(i+14)*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(1,i,&diplay_buffer_time[(i+14)*OLED_PIX_HEIGHT+8]);  
    }

    if(lamp_ctrl_data.time_hour < 10) {
        tuya_sh1106_gram_point_set(0,9,&diplay_buffer_time[0]);
        tuya_sh1106_gram_point_set(1,9,&diplay_buffer_time[8]);
    }else {
        tuya_sh1106_gram_point_set(0,9,&diplay_buffer_time[(lamp_ctrl_data.time_hour/10)*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(1,9,&diplay_buffer_time[(lamp_ctrl_data.time_hour/10)*OLED_PIX_HEIGHT+8]);
    }
    tuya_sh1106_gram_point_set(0,10,&diplay_buffer_time[(lamp_ctrl_data.time_hour%10)*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(1,10,&diplay_buffer_time[(lamp_ctrl_data.time_hour%10)*OLED_PIX_HEIGHT+8]);

    //flicker effect of ':'
    tuya_sh1106_gram_point_set(0,11,&diplay_buffer_time[10*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(1,11,&diplay_buffer_time[10*OLED_PIX_HEIGHT+8]);

    if(lamp_ctrl_data.time_min < 10) {
        tuya_sh1106_gram_point_set(0,12,&diplay_buffer_time[0]);
        tuya_sh1106_gram_point_set(1,12,&diplay_buffer_time[8]);
    }else {
        tuya_sh1106_gram_point_set(0,12,&diplay_buffer_time[(lamp_ctrl_data.time_min/10)*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(1,12,&diplay_buffer_time[(lamp_ctrl_data.time_min/10)*OLED_PIX_HEIGHT+8]);
    }
    
    tuya_sh1106_gram_point_set(0,13,&diplay_buffer_time[(lamp_ctrl_data.time_min%10)*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(1,13,&diplay_buffer_time[(lamp_ctrl_data.time_min%10)*OLED_PIX_HEIGHT+8]);
        
    tuya_sh1106_display();

5.电量显示和低电告警

​ 本 demo 通过ADC读取电池电压的1/2分压,根据读到的ADC值换算回电压值并将剩余电压用百分比的方式显示在OLED屏幕上,同时当电压值低于一定水平时驱动蜂鸣器实现低电量告警。

  • 调用tuya hal接口,初始化adc,获取adc值:
    USHORT_T adc_value = 0;
    float adc_voltage = 0.0;
    tuya_hal_adc_init(&tuya_adc);
    tuya_hal_adc_value_get(TEMP_ADC_DATA_LEN, &adc_value);
    PR_NOTICE("------------------adc_value = %d----------------",adc_value);
    adc_voltage = 2.4*((float)adc_value/2048);

    PR_NOTICE("------------------adc_voltage = %f----------------",adc_voltage);
    tuya_hal_adc_finalize(&tuya_adc);
  • 根据计算出的电压值估算电池大概的剩余电量,并在低电量时驱动蜂鸣器:
    if(adc_voltage > 1.95) {
        lamp_ctrl_data.Battery_remain = 100;
        return ;
    }
    if(adc_voltage > 1.92) {
        lamp_ctrl_data.Battery_remain = 80;
        return ;
    }
    if(adc_voltage > 1.89) {
        lamp_ctrl_data.Battery_remain = 60;
        return ;
    }
    if(adc_voltage > 1.86) {
        lamp_ctrl_data.Battery_remain = 40;
        return ;
    }
    if(adc_voltage > 1.8) {
        lamp_ctrl_data.Battery_remain = 20;
        if(lamp_ctrl_data.Low_pow_alarm) {
            __ctrl_beep(300);
        }
        return ;
    }
  • 根据剩余电量,解析出对应字模,显示在屏幕上:
STATIC VOID lamp_display_power(VOID)
{   
    if(lamp_ctrl_data.Lamp_switch != TRUE) {
        return ;
    }

    UCHAR_T i = 0;
    for(i = 4; i < 9; i++) {
        tuya_sh1106_gram_point_set(2,i,&diplay_buffer_time[(i+8)*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(3,i,&diplay_buffer_time[(i+8)*OLED_PIX_HEIGHT+8]);
    }

    //flicker effect of ':'
    tuya_sh1106_gram_point_set(2,9,&diplay_buffer_time[17*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(3,9,&diplay_buffer_time[17*OLED_PIX_HEIGHT+8]);

    if(lamp_ctrl_data.Battery_remain == 100) {
        tuya_sh1106_gram_point_set(2,10,&diplay_buffer_time[OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(3,10,&diplay_buffer_time[OLED_PIX_HEIGHT+8]);

        tuya_sh1106_gram_point_set(2,11,&diplay_buffer_time[0]);
        tuya_sh1106_gram_point_set(3,11,&diplay_buffer_time[8]);
        tuya_sh1106_gram_point_set(2,12,&diplay_buffer_time[0]);
        tuya_sh1106_gram_point_set(3,12,&diplay_buffer_time[8]);
    }else {
        tuya_sh1106_gram_point_set(2,10,&diplay_buffer_time[17*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(3,10,&diplay_buffer_time[17*OLED_PIX_HEIGHT+8]);

        tuya_sh1106_gram_point_set(2,11,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain/10)*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(3,11,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain/10)*OLED_PIX_HEIGHT+8]);
        tuya_sh1106_gram_point_set(2,12,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain%10)*OLED_PIX_HEIGHT]);
        tuya_sh1106_gram_point_set(3,12,&diplay_buffer_time[(lamp_ctrl_data.Battery_remain%10)*OLED_PIX_HEIGHT+8]);
    }

    tuya_sh1106_gram_point_set(2,13,&diplay_buffer_time[11*OLED_PIX_HEIGHT]);
    tuya_sh1106_gram_point_set(3,13,&diplay_buffer_time[11*OLED_PIX_HEIGHT+8]);

}

6.光照传感器

​ 为了实现后续的自动开关灯功能,硬件上还需借助光照传感器来检测当前的亮暗程度,从而判断当前是否需要允许自动开灯。选用的传感器型号为BH1750,通过I2C协议与SOC进行通信,相关接口封装都在bh1750.c文件中。模块具体使用流程如下:

  • 调用tuya_bh1750_init初始化模组:
VOID lamp_device_init(VOID)
{
   ......
   tuya_bh1750_init(&bh1750_int_param);
   ......
}
  • 调用tuya_bh1750_get_bright_value获取光照强度值:
VOID lamp_light_detect(VOID)
{
   lamp_ctrl_data.Light_intensity = tuya_bh1750_get_bright_value();
   PR_NOTICE("light_intensity_value = %d",lamp_ctrl_data.Light_intensity);
}

7.坐姿检测和自动开关灯

​ 本 demo 采用的微波雷达通过串口不间断的向soc发送包含运动状态、距离和能量的不定长字符串,而soc则根据这些参数来实现简易的坐姿检测,并配合环境光照强度实现自动开关灯。

  • 通过检索特定的符号字符来读取需要的参数:
VOID lamp_get_sensor_data(VOID)
{   

    UCHAR_T data[50];
    memset(data, 0, sizeof(data));

    CHAR_T opt;
    opt = get_radar_data(data,50);
    if(opt == 0){
        UCHAR_T i;
        if((data[0] == 'S')&&(data[6] == ':')) {
            if(data[8] == '[') {
                lamp_ctrl_data.Radar_sensor = TRUE;
            }else {
                lamp_ctrl_data.Radar_sensor = FALSE;
                lamp_ctrl_data.Human_distance = 0;
                PR_NOTICE("--------NO MAN AROUND-----------");
            }  
        }
        if(lamp_ctrl_data.Radar_sensor == FALSE) {
            return ;
        }
        for(i=0;i<50;i++) {
            if(data[i]=='R') {
                if((data[i+8] >= '0')&&(data[i+8] <= '9')) {
                    lamp_ctrl_data.Human_distance = ((data[i+7] - '0') * 10) + (data[i+8] - '0');
                }else {
                    lamp_ctrl_data.Human_distance = (data[i+7] - '0');
                }
                PR_NOTICE("--------Human_distance = %d-----------",lamp_ctrl_data.Human_distance);
                return ;        
            }    
        }
    }
}
  • 在app开启或按键开启坐姿提醒功能的前提下,当距离小于一定数值时,触发坐姿告警:
VOID STATIC lamp_sit_remind(VOID)
{
    if(lamp_ctrl_data.Sit_remind != TRUE) {
        alert_count = 0;
        return ;
    }

    if((lamp_ctrl_data.Human_distance <= 5)&&(lamp_ctrl_data.Radar_sensor == TRUE)) {
        PR_NOTICE("------------enter sit remind-------------");
        alert_count++;
        if(alert_count >= 3) {
            __ctrl_beep(300);
        }
    }else{
        alert_count = 0; 
    }
}
  • 在app开启自动开关灯功能的前提下,当雷达检测到有人靠近时,若当前环境光照强度很弱则自动打开灯光,同时当检测到无人在附近一定时间后,自动关闭灯光:
VOID STATIC lamp_light_control(VOID)
{   
    if((light_mode_old != lamp_ctrl_data.Light_mode)&&(lamp_ctrl_data.Lamp_switch == TRUE)) {
        lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
    }
    light_mode_old = lamp_ctrl_data.Light_mode;
    

    if(lamp_ctrl_data.Auto_light != TRUE) {
        return ;
    }

    if(lamp_ctrl_data.Radar_sensor == FALSE) {
        lamp_ctrl_data.Lamp_switch = FALSE;
        lamp_pwm_off();
    }else if((lamp_ctrl_data.Human_distance <= DISTANCE_THRESHOLD)&&\\\\
	        (lamp_ctrl_data.Light_intensity <= LIGHT_INTENSITY_THRESHOLD)) {
        if(lamp_ctrl_data.Lamp_switch == FALSE) {
            lamp_ctrl_data.Lamp_switch = TRUE;
            lamp_pwm_set(lamp_ctrl_data.Light_mode,user_pwm_duty);
        }
    }
}

8.编译和烧录

在linux终端输入指令运行SDK环境目录下的build_app.sh脚本来编译代码生成固件,指令格式为 sh build_app.sh APP_PATH APP_NAME APP_VERSION

若出现下图所示提示,则表示编译成功,固件已经生成:

固件生成路径为:apps->APP_PATH->output

将固件烧录至模组即可开始功能调试阶段,有关烧录和授权方式请参照文档: WB系列模组烧录授权

以上是关于多功能智能感应台灯设计(嵌入式)的主要内容,如果未能解决你的问题,请参考以下文章

多功能智能感应台灯设计(FAQ)

多功能智能感应台灯实物(展示)

基于单片机工业生产现场的光照强度控制系统设计-基于单片机多功能智能台灯设计-基于单片机多功能智能台灯控制系统设计-资料转发分享

[资料分享]基于单片机智能多功能自动窗帘定时光照强度检测系统设计基于单片机多功能台灯自动调节光亮强度系统设计

基于单片机多功能智能台灯设计(带时钟姿态矫正温度亮度)

基于单片机多功能智能台灯控制系统设计(时钟姿态矫正温度采集)