暖风机设备是居住环境中十分常用的冬季电器设备,它可以快速提高室内温度,实现室内恒温。
典型的暖风机多为设备按键控制和红外遥控器控制,在某些情况下已经不能满足当下的居住需求。例如,遥控器容易丢失且需要更换电池、用户在外出行时无法确认设备是否关闭、用户回家前不能提前开启暖风机使室内升到舒适温度等。
针对这些场景的需求,我们选了一款常见的暖风机,在此基础上进行改装,使用涂鸦 IoT 对其进行赋能,使其能够实现智能化控制。
通过查询资料,在不消耗太高成本情况下,智能暖风机可实现以下功能:
功能 | 说明 |
---|---|
开关 |
|
模式 |
|
定时 |
|
灯光 |
|
设温 |
|
温度显示 |
|
摇头 |
|
待机记忆 | 按键和 App 主动操作开关键关机为待机状态。再开启后恢复上一次设置:
|
断电记忆 | 断电后为断电状态,再上电恢复上一次设置: |
基于涂鸦智能的一款低功耗嵌入式Wi-Fi+BLE 双协议CBU 模组开发的一款主控板。您可通过此主控板,搭配其他功能模块,实现对应的功能。在设计暖风机过程中,我们对此开发板进行了部分沿用。查看详情
您可以选购一款市面上的传统暖风机,借鉴其已有的硬件结构,然后在其基础上根据教程步骤完成暖风机的改造。
基于涂鸦三明治 Wi-Fi SoC 主控板(CBU)设计的定制 PCB。
用于编程、烧录、测试等一系列操作,选用了一个 AC-DC 的隔离电源。
用于设计一个 DC12-DC3.3电平转换方案。
TM1650 是一种带键盘扫描接口的 LED(发光二极管显示器)驱动控制专用电路。内部集成有 MCU 输入输出控制数字接口、数据锁存器、LED 驱动、键盘扫描、亮度调节等电路。TM1650 性能稳定、质量可靠、抗干扰能力强,适用于 24 小时长期连续工作的应用场合。
让 CBU 模组能够使用较少的 I/O 口驱动六路触摸按键。
温度采集使用 CBU 模组的 ADC 管脚结合 NTC3950 实现。
12V RGB 灯带驱动。
用于实现两档加热功能。
电机驱动,提供电机动能。
提醒用户是否操作成功,以及是否有误触到。
为实现 涂鸦智能 或 智能生活 App 远程控制,我们选用涂鸦 CBU 云模组 作为主控。涂鸦三明治 Wi-Fi SoC 主控板(CBU)是方便开发者快速实现各种智能硬件产品原型的一款开发板。
PCB 接线原理图(点击下载资料文件)
PCB设计图
PCB 3D 图:
因为后续要进行编程、烧录、测试等一系列操作,电源上还是选用了一个 AC-DC 的隔离电源 HLK-20M12(点击下载资料文件),HLK-20M12 具有全球输入电压范围、低温升、低功耗、高效率、高可靠性、高安全隔
在主控板系统上有三种电源网络,即 AC220V、12V、3.3V。
隔离电源实现了AC220V-DC12V电源转换,而CBU模组和逻辑电平还需要3.3V的电源网络,需要再设计一个 DC12-DC3.3电平转换方案。
通过海选我们选择了一款成本较低的 FR9885S6CTR(点击下载资料文件)芯片来实现。
FR9885S6CTR 是一款同步降压 DC/DC 转换器,提供宽 4.5V 至 18V 输入电压范围和 2A 负载电流能力。在轻载条件下,FR9885S6CTR 可以在节电模式下运行,支持高效率和减少功耗。
同时我们又加上了一个 DC 5.5-2.0 的电源插座,以便于在调试时,使用 12V 2A 的电源适配器为其供电。
面板显示是我们此次改装的核心问题之一。
我们要达到使用 CBU 模组控制全部部件的目的,但是由于 I/O 口数量和电气特性限制,CBU 模组无法直接控制面板,我们必须在 CBU 和面板之间加一个驱动芯片,既要节省 CBU 模组 I/O 口,还要负责带动面板负载。
面板是由一个两位 8 段共阴数码管和 7 个 4MM 直插白色 LED 构成,8段共阴数码管我们使用拆下来的配件,7个4mm直插白色LED我们使用7个5mm白色LED代替。
通过海选,我们选择了 TM1650(点击下载资料文件)作为面板驱动。
TM1650 是一种带键盘扫描接口的 LED(发光二极管显示器)驱动控制专用电路。内部集成有 MCU 输入输出控制数字接口、数据锁存器、LED 驱动、键盘扫描、亮度调节等电路。TM1650 性能稳定、质量可靠、抗干扰能力强,适用于 24 小时长期连续工作的应用场合。
面板方案同样的问题,为节省 I/O 口我们必须寻找一块驱动芯片,让 CBU 模组能够使用较少的 I/O 口驱动六路触摸按键。
通过选型对比,我们选择了一款 IIC 通信的 TC309(点击下载资料文件) 来实现此功能。TC309主要有以下特点:
可以控制 9 个按键
自动灵敏度校正
系统低成本
I2C 输出方式
降低系统复杂度提高稳定性
嵌入的共模干扰去除电路
空闲状态可以节省功耗
RoHS 兼容的 SOP-16 封装
温度采集使用 CBU 模组的 ADC 管脚结合 NTC3950 实现。
温度关系转换为 T1=1/(log(Rt/R)/B+1/T2)
,其中:
电路方案为:
12V RGB 灯带驱动我们使用 D882(点击下载资料文件) 三极管来驱动。
相比于用驱动芯片用三极管驱动功率比较大的12V RGB灯带成本会低很多,电路设计也比较简单。
加热方案我们为配合拆机方案的结构和原理,继续使用类似方案,利用二极管的单向导通性。
另外使用 ULN2001D(点击下载资料文件) 继电器驱动和两个继电器,实现两档加热功能,电路图如下:
电机控制使用一个三路达林顿管继电器驱动 ULN2001D(点击下载资料文件),结合两个5A继电器实现。电路图如下:
一般情况下,带触摸按键的产品都需要有用户交互反馈,提醒用户是否操作成功,以及是否有误触到。
此时我们选择了一个有源蜂鸣器(点击下载资料文件),并通过一个三极管来驱动它,来达到用户交互反馈的听觉效果。
本小节简单介绍产品创建流程,详细的操作和介绍请参考 选品类创建产品。
本案例是基于 BK7231N 平台进行的 SoC 开发,开发所用的涂鸦标准模组 SDK 编译需要使用 Linux 系统。您首先要安装linux开发环境,然后从涂鸦仓库拉取包含 SDK 环境的 demo 例程。
SDK GitHub 地址:Tuya IoTOS Embeded WiFi & BLE sdk
Shell 命令:
$ cd "your directory"
$ git clone https://github.com/tuya/tuya-iotos-embeded-sdk-wifi-ble-bk7231n
注意事项:
apps
目录中也有几个应用案例,您可以使用 apps/tuya_demo_template
这个 Demo 为开发模板,在此基础上增减代码,实现一个嵌入式系统框架。将 Git 库克隆至本地后,打开文件目录,找到 apps
文件夹。apps
文件夹用于存放应用层代码,也就是 Demo 的代码。在这里创建一个文件夹,命名为 bk7231n_calorifier_demo
,与 Demo 有关的所有源文件、头文件以及编译产物都将放到该文件夹中。
如果您是第一次在该平台上进行嵌入式开发,建议将 apps
文件下的 tuya_demo_template
中的tuya_device.c
和 tuya_device.h
文件都复制过来。
打开 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;
}
calorifier_init();
return op_ret;
}
在 BK7231N 平台的 SDK 环境中,device_init
函数为重要的应用代码入口。设备上电后,BK7231T 平台适配层运行完一系列初始化代码后就会调用该函数来初始化应用层。该函数主要做三件事:
调用 tuya_iot_wf_soc_dev_init_param()
接口初始化 SDK,配置了工作模式、配网模式,同时注册了各种回调函数并存入了固件 Key 和PID。
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;
}
调用应用层初始化函数:
calorifier_init();
初始化函数里面主要是进行 GPIO 配置、定时器中断配置、PWM 等的配置。
本 Demo 应用代码主要分三层来实现:
- 最底层为各个传感器的驱动代码,封装出传感器的初始化、采集等接口。
- 第二层为控制逻辑部分的代码,调用驱动层的传感器接口,实现各个组件的控制逻辑,封装出数据处理轮询接口。
- 第一层为主要应用层,创建应用任务调用第二层的接口,同时处理DP点数据的上报和接受解析。
第一层主要在tuya_thread.c
文件和tuya_dp_process.c
中实现,大致内容如下:
thread_init()
创建四个处理线程,用作温度采集线程、按键处理线程、数据保存线程以及空闲轮询线程的创建,内容如下:
void thread_init(void)
{
int rt = OPRT_OK;
rt = tuya_hal_thread_create(NULL, "update_dp_thread", 512*4, TRD_PRIO_4, update_temperature_thread, NULL);
if (rt != OPRT_OK) {
PR_ERR("Create update_dp_thread error!: %d", rt);
return;
}
rt = tuya_hal_thread_create(NULL, "flash_write_thread", 512*4, TRD_PRIO_4, flash_write_thread, NULL);
if (rt != OPRT_OK) {
PR_ERR("Create flash_write_thread error!: %d", rt);
return;
}
rt = tuya_hal_thread_create(NULL, "tc309_keyscan_task", 512*4, TRD_PRIO_5, tc309_keyscan_task, NULL);
if (rt != OPRT_OK) {
PR_ERR("Create tc309_keyscan_task error!: %d", rt);
return;
}
rt = tuya_hal_thread_create(NULL, "idle_task1", 512*4, TRD_PRIO_6, idle_task, NULL);
if (rt != OPRT_OK) {
PR_ERR("Create idle_task error!: %d", rt);
return;
}
}
update_temperature_thread()
是温度采集线程,采集温度数据以及上报温度数据,内容如下:
void update_temperature_thread(void)
{
static int last_temper = 0;
while(1)
{
tuya_hal_semaphore_wait(g_temper_binsemap);
last_temper = cur_temper_get();
if(last_temper > 50)
{
last_temper = 50;
}
else if(last_temper < -20)
{
last_temper = -20;
}
temper_s.value = last_temper;
dp_memory_s.temperature_value = last_temper;
PR_DEBUG("CURRENT TIME : %d",timercount);
if((switch_s.power == 1) && (dev_key_s.display_lock == 0)){
display_num(dp_memory_s.temperature_value);
display_status(dp_memory_s.status);
report_one_dp_status(DP_TEMPER);
report_one_dp_status(DP_SHUTDOWN_TIME);
}
}
}
flash_write_thread()
是数据保存线程,将DP点的数据写入闪存,实现断电记忆功能,内容如下:
void flash_write_thread(void)
{
while(1)
{
tuya_hal_semaphore_wait(g_flash_binsemap);
opUserFlashWriteAppData(&dp_memory_s);
}
}
tc309_keyscan_task()
是按键处理线程,采集哪个按键按下并执行按键触发的功能,内容如下:
void tc309_keyscan_task(void)
{
static uint16_t key_value = 0x1ff;
UserTc309Init();
UserTc309Set_Sensitivity(7);
while(1)
{
//GET KEY_VALUE
tuya_hal_semaphore_wait(g_key_trigger_binsemap);
uint8_t *buffer = (uint8_t *)Malloc(2*sizeof(uint8_t));
//tc309 read 0x08 0x09
i2c_read_direct(0x40,buffer,2);
key_value = (buffer[0] << 4) & 0x1ff;
key_value |= (buffer[1] >> 4) & 0x0f;
PR_DEBUG("BUF0:%x",buffer[0]);
PR_DEBUG("BUF1:%x",buffer[1]);
PR_DEBUG("key_value:%x",key_value);
//tc309
dev_key_s.key_value = key_value;
Free(buffer);
//Actions are performed according to key values
switch (key_value){
case KEY0: {
//shake key
if(dp_memory_s.switch_bool) {
timer1_init();
PR_DEBUG("key_value0");
if(dp_memory_s.shake_bool == 1) {
shake_handle(0);
}else {
shake_handle(1);
}
}
}
break;
case KEY1: {
//relay key
if(dp_memory_s.switch_bool) {
timer1_init();
PR_DEBUG("key_value1");
static uint8_t cur_mode = 0;
cur_mode = dp_memory_s.relay_mode;
cur_mode = (cur_mode + 1 < 3) ? (cur_mode + 1) : 0;
relay_handle(cur_mode);
PR_DEBUG("relay mode:%d",dp_memory_s.relay_mode);
}
}
break;
case KEY2: {
//timer close key
if(dp_memory_s.switch_bool) {
timer1_init();
PR_DEBUG("key_value2");
if(dev_key_s.last_time_count && ((timercount - dev_key_s.last_time_count) < 2)) {
dev_key_s.timer_hour++;
}
dev_key_s.last_time_count = timercount;
if(dev_key_s.timer_hour > 12) {
dev_key_s.timer_hour = 0;
}
//Avoid triggering the shutdown directly by pressing the timing button for the first time
if(dev_key_s.timer_hour > 0) {
dev_key_s.key_notice = 1;
}else {
dev_key_s.key_notice = 0;
}
dev_key_s.temp_time_count = timercount + dev_key_s.timer_hour*360;
timer2_init();
display_num(dev_key_s.timer_hour);
display_status(0x04);
}
}
break;
case KEY3: {
//Set target temperature
if(dp_memory_s.switch_bool) {
timer1_init();
PR_DEBUG("key_value3");
static uint8_t cur_set_temp = 0;
cur_set_temp = dp_memory_s.set_temper_value;
cur_set_temp = (cur_set_temp + 1 < 41) ? (cur_set_temp + 1) : 15;
set_termper_handle(cur_set_temp);
timer2_init();
display_num(dp_memory_s.set_temper_value);
display_status(0x02);
}
}
break;
case KEY4: {
//Short press:led mode key long press: Reconfigure the network mode
if(dp_memory_s.switch_bool) {
timer1_init();
static uint32_t count = 0;
while(tuya_gpio_read(9) == 0){
count++;
if(count > 700000){
break;
}
}
if(count > 700000){
count = 0;
PR_DEBUG("wf_unc");
tuya_iot_wf_gw_unactive();
}else{
PR_DEBUG("key_value4");
static uint8_t cur_ledmode = 0;
cur_ledmode = dp_memory_s.led_mode;
cur_ledmode = (cur_ledmode + 1 < 5) ? (cur_ledmode + 1) : 0;
led_handle(cur_ledmode);
}
PR_DEBUG("led mode:%d",dp_memory_s.led_mode);
}
}
break;
case KEY5:{
//switch key
timer1_init();
PR_DEBUG("key_value5");
if(dp_memory_s.switch_bool == 1){
user_switch_handle(0);
tm1650_close();
}else{
user_switch_handle(1);
}
}
break;
case KEY6:{
PR_DEBUG("key_value6");
}
break;
case KEY7:{
PR_DEBUG("key_value7");
}
break;
case KEY8:{
PR_DEBUG("key_value8");
}
break;
default:
break;
}
//set timing time
PR_DEBUG("KEY_HOUR:%d",dev_key_s.timer_hour);
}
}
idle_task()
是空闲处理线程,当温度到达设温时会关闭加热,开启倒计时功能时计算所剩的倒计时时间,配网模式下开启指示灯效果,内容如下:
void idle_task(void)
{
static uint8_t wf_nw_led = 1;
while(1)
{
shutdown_time_s.value = ((dev_key_s.temp_time_count > timercount) && (0 < (dev_key_s.temp_time_count - timercount)/6 < 720)) ? (dev_key_s.temp_time_count - timercount)/6 : 0;
dev_key_s.timer_hour = shutdown_time_s.value/60;
if((timercount > dev_key_s.temp_time_count) && dev_key_s.temp_time_count && dev_key_s.key_notice){
PR_DEBUG("SHUTDOWN TIME");
time_off_handle(1);
dev_key_s.temp_time_count = 0;
dev_key_s.timer_hour = 0;
dev_key_s.key_notice = 0;
tuya_hal_system_sleep(1000);
}
if((dp_memory_s.temperature_value >= dp_memory_s.set_temper_value) && dp_memory_s.relay_mode){
relay_handle(0);
}
if(wf_nw_led && dp_memory_s.switch_bool){
switch (wf_nw_status_temp)
{
case STAT_LOW_POWER: //WiFi connection network timeout, enter low power mode
{
color_handle(0);
wf_nw_led = 0;
}
break;
case STAT_UNPROVISION: //SamrtConfig connected network mode, waiting for connection
{
color_handle(5);
}
break;
case STAT_AP_STA_UNCFG: //ap connected network mode, waiting for connection
{
color_handle(6);
}
break;
case STAT_AP_STA_DISC:
case STAT_STA_DISC: //SamrtConfig/ap connecting...
{
color_handle(7);
}
break;
case STAT_CLOUD_CONN:
case STAT_AP_CLOUD_CONN: //Already connected to Tuya Cloud
{
color_handle(0);
wf_nw_led = 0;
}
break;
default:
break;
}
tuya_hal_system_sleep(200);
}
}
}
report_one_dp_status()
是上传单个 DP 数据的上报函数,内容如下:
VOID_T report_one_dp_status(int dp_id)
{
OPERATE_RET op_ret = OPRT_OK;
GW_WIFI_NW_STAT_E wifi_state = 0xFF;
op_ret = get_wf_gw_nw_status(&wifi_state);
if (OPRT_OK != op_ret) {
PR_ERR("get wifi state err");
return;
}
if (wifi_state <= STAT_AP_STA_DISC || wifi_state == STAT_STA_DISC) {
return;
}
TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(SIZEOF(TY_OBJ_DP_S));
if(NULL == dp_arr) {
PR_ERR("malloc failed");
return;
}
memset(dp_arr, 0, SIZEOF(TY_OBJ_DP_S));
switch (dp_id){
case DP_SWITCH: {
dp_arr[0].dpid = switch_s.dp_id;
dp_arr[0].type = PROP_BOOL;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_bool = switch_s.power;
}
break;
case DP_TEMPER: {
dp_arr[0].dpid = temper_s.dp_id;
dp_arr[0].type = PROP_VALUE;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_value = temper_s.value;
}
break;
case DP_SHAKE: {
dp_arr[0].dpid = shake_s.dp_id;
dp_arr[0].type = PROP_BOOL;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_bool = shake_s.power;
}
break;
case DP_MODE: {
dp_arr[0].dpid = mode_s.dp_id;
dp_arr[0].type = PROP_ENUM;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_enum = mode_s.value;
}
break;
case DP_LED: {
dp_arr[0].dpid = led_s.dp_id;
dp_arr[0].type = PROP_ENUM;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_enum = led_s.value;
}
break;
case DP_SET_TEMP: {
dp_arr[0].dpid = set_temper_s.dp_id;
dp_arr[0].type = PROP_VALUE;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_value = set_temper_s.value;
}
break;
case DP_TIME_ON: {
dp_arr[0].dpid = time_to_open_s.dp_id;
dp_arr[0].type = PROP_BOOL;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_bool = time_to_open_s.power;
}
break;
case DP_TIME_OFF: {
dp_arr[0].dpid = time_to_close_s.dp_id;
dp_arr[0].type = PROP_BOOL;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_bool = time_to_close_s.power;
}
break;
case DP_SHUTDOWN_TIME: {
dp_arr[0].dpid = shutdown_time_s.dp_id;
dp_arr[0].type = PROP_VALUE;
dp_arr[0].time_stamp = 0;
dp_arr[0].value.dp_value = shutdown_time_s.value;
}
break;
default:
break;
}
op_ret = dev_report_dp_json_async(NULL , dp_arr, 1);
Free(dp_arr);
dp_arr = NULL;
if(OPRT_OK != op_ret) {
PR_ERR("dev_report_dp_json_async relay_config data error,err_num",op_ret);
}
}
本案例中温度采集方案是使用热敏电阻,热敏电阻在不同温度下有不同的阻值,根据此特性,通过电路设计和软件程序配合采集到热敏电阻的阻值,从而计算出当前的温度值。
采样电路图如下:R1 为 10k 的定值电阻,CN1 为热敏电阻(B3950),ADC是电压采样点,采集电压后,根据欧姆定律即可算出热敏电阻的阻值。
得到热敏电阻阻值Rt,根据B3950的热敏曲线即可算出当前温度值。
上图是该系列热敏电阻的热敏曲线,图示参数含义:
测温原理是通过采样电路采样热敏电阻两端的电压,从而计算出R的阻值,再根据R的阻值计算当前的温度:
R=R0 expB (1/T-1/T0)
||
\/
T=1/(ln(R/R0)/B+1/T0)
近似为T=1/(log(R/R0)/B+1/T0)
,得到的T为开尔文温度,换算成摄氏度即temp = T - 273.15
。
接下来是软件实现阶段。在b3950.c
中实现:
//注册ADC设备用于温度采集电路的电压采集
void b3950_init(void)
{
/*create adc device,get handle*/
temper_adc = (tuya_adc_t *)tuya_driver_find(TUYA_DRV_ADC, TUYA_ADC2);
/*adc_dev cfg*/
TUYA_ADC_CFG(temper_adc, TUYA_ADC2, 0);
/*adc_dev init*/
tuya_adc_init(temper_adc);
}
//获取当前的温度值
int cur_temper_get()
{
float Rt = 0;
float Rp = 10000;
float T2 = 273.15 + 25;
float Bx = 3950;
float Ka = 273.15;
int temp = 0;
/*Collect AD data and store it in adc_buffer*/
tuya_adc_convert(temper_adc, &adc_buf, 1);
/*req_val(0-4096) - V(0-2.4)*/
volt = (float)adc_buf *2.4/ 4096;
//volt = adc_buf;
Rt = volt*10000/(3.3 - volt);
temp = (int)(1/(1/T2+log(Rt/Rp)/Bx)-Ka+0.5);
PR_DEBUG("volt:%f", volt);
return temp;
}
本案例中面板由两个数码管和 7 个 LED 状态灯组成,驱动芯片选择 TM1650。采用 IIC 通信,具体控制时序如下:
对显存地址的写时序:
控制流程是以IIC时序发送要写的寄存器地址,再发送要写的数据,例:
address : 0x48 //控制寄存器地址
data: 0x0a //8级亮度 开显示 7段显示方式
address : 0x68 //显存地址DIG1
data: 0x7f //7段全亮
完整的写时序:
代码实现在tm1650.c
中进行实现:
//显示初始化
void tm1650_led_init(void)
{
/*SCL_PIN:PA24 SDA_PIN:PA26*/
tuya_i2c_sw_cfg_t sw_cfg;
TUYA_I2C_SW_CFG_INIT(&sw_cfg, TUYA_PA24, TUYA_PA26, 1);
tuya_sw_i2c_register(TUYA_I2C0, &sw_cfg);
tm1650_init(&tm1650_dev);
}
//芯片初始化,设定亮度
void tm1650_init(tm1650_dev_t *dev)
{
uint8_t buffer[2];
//! looking for IIC handle
dev->i2c = tuya_driver_find(TUYA_DRV_I2C, TUYA_I2C0);
//! no device addr
TUYA_I2C_MASTER_CFG(dev->i2c, 0x00);
tuya_i2c_init(dev->i2c);
buffer[0] = 0x48; //! register addr
buffer[1] = 0x71; //! power set
tuya_i2c_master_send(dev->i2c, 0x00, TUYA_I2C_NO_START | TUYA_I2C_WR, buffer, 2);
}
//关闭显示
void tm1650_close(void)
{
TM1650_Write(0x68, 0x00, &tm1650_dev);
TM1650_Write(0x6a, 0x00, &tm1650_dev);
TM1650_Write(0x6e, 0x00, &tm1650_dev);
}
//向tm1650发送数据
void TM1650_Write(uint8_t addr, uint8_t data, tm1650_dev_t *dev)
{
uint8_t buffer[2];
buffer[0] = addr; //! register addr
buffer[1] = data; //! Transmitted data
tuya_i2c_master_send(dev->i2c, 0x00, TUYA_I2C_NO_START | TUYA_I2C_WR, buffer, 2);
}
//设置数码管显示的亮度
void TM1650_SetDisplay(uint8_t brightness, uint8_t mode, uint8_t state)
{
if(state)
{
if(mode == 7) // 7段显示方式
{
TM1650_Write(0x48, brightness*16 + 1*4 + 1, &tm1650_dev);
}
else if(mode == 8) // 8段显示方式
{
TM1650_Write(0x48, brightness*16 + 1, &tm1650_dev);
}
}
else
{
TM1650_Write(0x48, 0x00,&tm1650_dev); // 关闭显示
}
}
//设置数码管显示的值
void TM1650_SetNumber(uint8_t index, uint8_t mode, uint8_t num)
{
uint8_t indexAddr = 0;
uint8_t numValue = 0;
if(index == 1)
{
indexAddr = 0x68;
}
else if(index == 2)
{
indexAddr = 0x6A;
}
else if(index == 3)
{
indexAddr = 0x6C;
}
else if(index == 4)
{
indexAddr = 0x6E;
}
if(mode == 7) // 7段显示方式
{
numValue = s_7number[num];
}
else if(mode == 8) // 8段显示方式
{
numValue = s_8number[num];
}
TM1650_Write(indexAddr, numValue, &tm1650_dev);
}
//显示温度所调的接口
void display_num(uint8_t num)
{
uint8_t num_1 = 0;
uint8_t num_10 = 0;
num_1 = num%10;
num_10 = (num%100)/10;
TM1650_SetNumber(1, 7, num_10);
TM1650_SetNumber(2, 7, num_1);
}
//暖风机状态指示灯显示
void display_status(uint8_t status)
{
TM1650_Write(0x6E, status, &tm1650_dev);
}
本案例中触摸按键采集芯片选择 TC309。
TC309 是一个 9 按键电容传感装置,本案例中只使用了六个按键。电路方案已在硬件设计里列出。
器件地址:
地址 (A[6:0]) | 40H |
---|---|
读命令 (A[6:0]+RWB) | 81H |
写命令 (A[6:0]+RWB) | 80H |
读操作:
TC309 的默认读寄存器地址为 08H,如果操作过其他寄存器,再要读取 08H 地址数据需要重新访问08H,即通过 IIC 向 TC309 发送 0x80(0x40地址写操作) 和 0x08,之后再进行读操作。
TC309 控制寄存器:
按下按键后芯片的 INT 引脚会拉低,所以我们采用外部中断去采集 INT 引脚的变化,一次下降沿说明按键按下一次,中断发生后发送信号量,按键读取键值进程将会运行,采集到是哪个按键按下,从而对暖风机进行控制。
代码实现,在 tc309.c
进行实现:
//设置按键灵敏度
int UserTc309Set_Sensitivity(UCHAR_T level)
{
int opRet = 0;
UCHAR_T IIC_Sendbuf[5] = {0};
IIC_Sendbuf[0] = 0x80 ;
IIC_Sendbuf[1] = 0x00;
IIC_Sendbuf[2] = senset[level];
IIC_Sendbuf[3] = senset[level];
opRet = vTc309WritePage(IIC_Sendbuf, 4);
IIC_Sendbuf[0] = 0x80 ;
IIC_Sendbuf[1] = 0x08;
opRet = vTc309WritePage(IIC_Sendbuf, 2);
return 0;
}
//tc309芯片IIC通信初始化
int UserTc309Init()
{
int opRet = -1;
if(gTc309InitFlag != FALSE) {
PR_NOTICE("Tc309 already init!");
return 0;
}
I2C_PIN_T tI2CConfig = {
.ucSdaPin = 15,
.ucSclPin = 17,
};
opRet = opSocI2CInit(&tI2CConfig); /* SDA&SCL GPIO INIT */
if(opRet != 0){
PR_ERR("Tc309 I2C init error!");
return -1;
}
gTc309InitFlag = TRUE;
return 0;
}
//读取tc309的寄存器值,获取按键值
void i2c_read_direct(UCHAR_T dev_addr,UCHAR_T * dest_buf,UCHAR_T len)
{
char i;
vTc309Start();//启动
vTc309SendOneByte((dev_addr<<1)+1);//发送发送从设备地址 读操作
vTc309WaitAck();//等待从设备的响应
dest_buf[0]=i2c_read_byte();//获取数据
for(i=1;i<len;i++)
{
i2c_sendack();
dest_buf[i]=i2c_read_byte();
}
i2c_sendnack();
vTc309Stop();//停止
}
驱动方案设定:
HIN : P20 |P20引脚为高加热档控制引脚
LIN : P22 |P22引脚为低加热档控制引脚
本方案中 HIN 和 LIN 引脚不能同时拉高
将控制引脚配置为推挽输出模式,初始化为拉低状态,可以根据传入的函数传参执行引脚拉高和拉低,实现控制继电器功能。
以下为控制程序:
//控制继电器端口IO初始化
void relay_init()
{
tuya_pin_init(relay_leve1, TUYA_PIN_MODE_OUT_PP_LOW);
tuya_pin_init(relay_leve2, TUYA_PIN_MODE_OUT_PP_LOW);
}
//Heating mode setting
void relay_set(int level)
{
switch (level){
case mode0:
{
tuya_pin_write(relay_leve1, TUYA_PIN_LOW);
tuya_pin_write(relay_leve2, TUYA_PIN_LOW);
}
break;
case mode1:
{
tuya_pin_write(relay_leve1, TUYA_PIN_HIGH);
tuya_pin_write(relay_leve2, TUYA_PIN_LOW);
}
break;
case mode2:
{
tuya_pin_write(relay_leve1, TUYA_PIN_LOW);
tuya_pin_write(relay_leve2, TUYA_PIN_HIGH);
}
break;
default:
break;
}
}
驱动方案设定:
将控制引脚配置为推挽输出模式,初始化为拉低状态,可以根据传入的函数传参执行引脚拉高和拉低,实现电机控制。
以下为控制程序:
//控制电机的IO口初始化
void motor_init()
{
tuya_pin_init(fan_motor, TUYA_PIN_MODE_OUT_PP_LOW);
tuya_pin_init(shake_motor, TUYA_PIN_MODE_OUT_PP_LOW);
}
//设置风扇电机工作状态
void fan_motor_set(IN BOOL_T bONOFF)
{
if(bONOFF == TRUE){
tuya_pin_write(fan_motor, TUYA_PIN_HIGH);
}else{
tuya_pin_write(fan_motor, TUYA_PIN_LOW);
}
}
//设计摇头电机工作状态
void shake_motor_set(IN BOOL_T bONOFF)
{
if(bONOFF == TRUE){
tuya_pin_write(shake_motor, TUYA_PIN_HIGH);
}else{
tuya_pin_write(shake_motor, TUYA_PIN_LOW);
}
}
驱动引脚设定:
将控制引脚配置为推挽输出模式,初始化为拉低状态,可以根据传入的函数传参执行引脚拉高和拉低,实现蜂鸣器控制。
以下为控制程序:
//控制蜂鸣器的IO初始化
void buzzer_init()
{
tuya_pin_init(buzzer_pin, TUYA_PIN_MODE_OUT_PP_LOW);
}
//蜂鸣器开启关断设置
void buzzer_set(IN BOOL_T bONOFF)
{
if(bONOFF == TRUE){
tuya_pin_write(buzzer_pin, TUYA_PIN_HIGH);
}else{
tuya_pin_write(buzzer_pin, TUYA_PIN_LOW);
}
}
使用三个PWM输出引脚,配置输出的模式以及输出占空比。
PWM引脚设置:
在led_color_set.c
中实现,代码如下:
//初始化驱动led的三路PWM
void led_init()
{
/*color Red init*/
rgb_r_pwm = (tuya_pwm_t *)tuya_driver_find(TUYA_DRV_PWM, TUYA_PWM0);
TUYA_PWM_CFG(rgb_r_pwm, rgb_r_pwm_pin, 10 * 1000, 0);
tuya_pwm_init(rgb_r_pwm);
tuya_pwm_start(rgb_r_pwm);
/*color Green init*/
rgb_g_pwm = (tuya_pwm_t *)tuya_driver_find(TUYA_DRV_PWM, TUYA_PWM1);
TUYA_PWM_CFG(rgb_g_pwm, rgb_g_pwm_pin, 10 * 1000, 0);
tuya_pwm_init(rgb_g_pwm);
tuya_pwm_start(rgb_g_pwm);
/*color Bule init*/
rgb_b_pwm = (tuya_pwm_t *)tuya_driver_find(TUYA_DRV_PWM, TUYA_PWM5);
TUYA_PWM_CFG(rgb_b_pwm, rgb_b_pwm_pin, 10 * 1000, 0);
tuya_pwm_init(rgb_b_pwm);
tuya_pwm_start(rgb_b_pwm);
}
//设置RGB灯带的工作模式
VOID color_handle(IN int mode)
{
switch (mode){
case LED_CLOSE:
{
led_close();
}
break;
case COLOR1:
{
color1_set();
}
break;
case COLOR2:
{
color2_set();
}
break;
case COLOR3:
{
color3_set();
}
break;
case COLOR4:
{
color4_set();
}
break;
case STAT_UNPROVISION_LED:
{
//配网模式指示灯
stat_unprovision_led();
}
break;
case STAT_AP_STA_UNCFG_LED:
{
//配网模式指示灯
stat_unprovision_led();
}
break;
case STAT_AP_STA_DISC_LED:
{
//配网模式指示灯
stat_unprovision_led();
}
break;
default:
break;
}
}
void color1_set()
{
tuya_pwm_duty_set(rgb_r_pwm, 1.0);
tuya_pwm_duty_set(rgb_g_pwm, 0);
tuya_pwm_duty_set(rgb_b_pwm, 0);
}
void color2_set()
{
tuya_pwm_duty_set(rgb_r_pwm, 0);
tuya_pwm_duty_set(rgb_g_pwm, 1.0);
tuya_pwm_duty_set(rgb_b_pwm, 0);
}
void color3_set()
{
tuya_pwm_duty_set(rgb_r_pwm, 0);
tuya_pwm_duty_set(rgb_g_pwm, 0);
tuya_pwm_duty_set(rgb_b_pwm, 1.0);
}
void color4_set()
{
tuya_pwm_duty_set(rgb_r_pwm, 0.15);
tuya_pwm_duty_set(rgb_g_pwm, 0.8);
tuya_pwm_duty_set(rgb_b_pwm, 0.8);
}
void led_close()
{
tuya_pwm_duty_set(rgb_r_pwm, 0);
tuya_pwm_duty_set(rgb_g_pwm, 0);
tuya_pwm_duty_set(rgb_b_pwm, 0);
}
void stat_unprovision_led()
{
static float pwm_duty = 0;
tuya_pwm_duty_set(rgb_r_pwm, pwm_duty);
tuya_pwm_duty_set(rgb_g_pwm, 0);
tuya_pwm_duty_set(rgb_b_pwm, 0);
pwm_duty = pwm_duty + 0.1;
if(pwm_duty > 1)
{
pwm_duty = 0;
}
}
void stat_ap_sta_uncfg_led()
{
static float pwm_duty = 0;
tuya_pwm_duty_set(rgb_r_pwm, 0);
tuya_pwm_duty_set(rgb_g_pwm, 0);
tuya_pwm_duty_set(rgb_b_pwm, pwm_duty);
pwm_duty = pwm_duty + 0.1;
if(pwm_duty > 1)
{
pwm_duty = 0;
}
}
void stat_ap_sta_disc_led()
{
static float pwm_duty = 0;
tuya_pwm_duty_set(rgb_r_pwm, 0);
tuya_pwm_duty_set(rgb_g_pwm, pwm_duty);
tuya_pwm_duty_set(rgb_b_pwm, 0);
pwm_duty = pwm_duty + 0.1;
if(pwm_duty > 1)
{
pwm_duty = 0;
}
}
我们可以将要保存的数据写入到 FLASH 闪存中,在需要时再读取出来,从而实现记忆功能。
以下为控制程序:
//创建了一个dp数据保存的结构体
Data_Memory_T dp_memory_s = {
.switch_bool = 0,
.led_mode = 0,
.relay_mode = 0,
.temperature_value = 0,
.shake_bool = 0,
.set_temper_value = 0,
.time_on_bool = 0,
.time_off_bool = 0,
.status = 0,
};
//创建了一个flash写入线程,接受到信号量后会将dp_memory_s结构写入flash
void flash_write_thread(void)
{
while(1)
{
tuya_hal_semaphore_wait(g_flash_binsemap);
//写入数据到flash
opUserFlashWriteAppData(&dp_memory_s);
}
}
/*上电后会执行记忆恢复函数,将dp_memory_s数据读出,根据读出的数据恢复断电前使用者的
暖风机控制*/
VOID_T Power_data_recovery(void)
{
Data_Memory_T *power_down_memory = (Data_Memory_T *)Malloc(SIZEOF(Data_Memory_T));
memset(power_down_memory,0,sizeof(Data_Memory_T));
uiSocFlashRead(SAVE_TYP1, APP_DP_DATA_OFFSET, sizeof(Data_Memory_T), (UCHAR_T *)(power_down_memory));
dp_memory_s.led_mode = power_down_memory->led_mode;
dp_memory_s.switch_bool = 0;
dp_memory_s.set_temper_value = power_down_memory->set_temper_value;
//device timer close clear
dev_key_s.temp_time_count = 0;
dev_key_s.timer_hour = 0;
Free(power_down_memory);
}
前面我们已经实现了暖风机的基本功能,现在需要实现 App远程控制设备这最后一步,实现设备的智能化。
以下为控制程序:
//app下发命令后将会触发回调函数从而执行该函数,根据下发的DP_ID以及控制值执行相关函数
VOID_T deal_dp_proc(IN CONST TY_OBJ_DP_S *root)
{
UCHAR_T dpid;
dpid = root->dpid;
switch (dpid){
case DP_SWITCH:
{
user_switch_handle(root->value.dp_bool);
}
break;
case DP_TEMPER:
{
//只上报dp,无需处理
}
break;
case DP_SHAKE:
{
shake_handle(root->value.dp_bool);
}
break;
case DP_MODE:
{
relay_handle(root->value.dp_enum);
}
break;
case DP_LED:
{
led_handle(root->value.dp_value);
}
break;
case DP_SET_TEMP:
{
set_termper_handle(root->value.dp_value);
}
break;
case DP_TIME_ON:
{
time_open_handle(root->value.dp_value);
}
break;
case DP_TIME_OFF:
{
time_off_handle(root->value.dp_value);
}
break;
case DP_SHUTDOWN_TIME:
{
//只上报dp,无需处理
}
break;
default:
break;
}
}
在 Linux 终端输入命令运行 SDK 环境目录下的 build_app.sh
脚本来编译代码生成固件。固件生成路径为 apps
> APP_PATH
> output
命令格式:
build_app.sh <APP_PATH> <APP_NAME> <APP_VERSION>
命令示例:
sh build_app.sh apps/bk7231n_calorifier_demo bk7231n_calorifier_demo 1.0.0
编译成功后将固件烧录至模组,即可进行功能调试阶段。固件烧录授权相关信息请参考 Wi-Fi + BLE 系列模组烧录授权。
将机器组装起来逐步验证以下功能:
倾倒断电
触摸按键
配网测试
手机连接
功能逻辑
全部功能调试完成后,一款包含自动手机 App 远程遥控、数据监测(温度,开关状态、档位信息)的智能暖风机就完成制作了。 同时您可以基于 涂鸦 IoT 平台 丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。