此设计是基于STM32F407单片机的智能家居系统,具体完成要求如下:
设计一个基于蓝牙/Wifi的智能家居控制系统,实现手机端蓝牙发射和接收控制,并能实现控制各种家用电器设备,设计并完成特定的控制对象。
1、遥控距离大于6米,
2、采用单片机与HC05蓝牙模块实现信号接收与控制端输出。
3、能实现多种负载控制,如灯光、窗帘控制、门铃等智能家居设备的控制
4、画出的系统原理图并焊接调试。
5、单片机可选用stm32系列,手机端app控制家居的开关与灯光亮暗。
6、自行设计其他功能如语音控制等。
设计流程如下图:
值得注意的是我们选择的蓝牙模块是必须要3.3V供电的,否则发送/接收的信号会乱码!!!
(如果选择Wifi模块的话,我选择的是正点原子的Esp8266模块,相关资料比较完善)
通过查阅STM32F4的数据手册,查找到对应复用功能的引脚,进行引脚的选择(按键和拨码开关本来是想做一些复杂一点的用户界面以及状态机进行切换,但是时间原因以及课程设计本来没有要求,个人也比较懒,就没有用到这些外设。。。)
硬件方面最后插一句嘴,就是舵机的可控电压范围是4.5~8V,基于成本的考虑,设计了两种不同的供电方式,分别引出接口给两舵机5V/6V供电,实测5V(1117-5.0进行稳压)效果也不错,所以可以不对6V(AS1015稳压电路)供电部分电路进行焊接。
由于项目本身是“智能家居”嘛,所以DIY了一个小房子,并且利用3D打印技术将小房子模型打印了出来,拼接起来。(没有3D打印机可以用的同学可以用纸板粘一个出来)。
// 硬件初始化
void BoardInit()
{
// GPIO_InitTypeDef GPIO_InitStructure; //GPIO的初始化
BEEP_Init(); //蜂鸣器初始化
TIM4_LightPWM_Init(10000,168); //照明装置初始化--频率50HZ(由于照明与舵机在同一个定时器下初始化,故定时器频率只能按照舵机的50Hz进行初始化)
TIM4_SERVOPWM_Init(10000,168); //舵机初始化--频率50HZ
uart_init(115200); //串口1初始化,波特率为115200
delay_init(168); //delay函数的初始化
OLED_Init(); //OLED的初始化
LED_GPIO_Config(); //LED初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级组别为2(抢占优先级和响应优先级均为4位)
EXTIX_Init(); //外部中断初始化
}
int main()
{
BoardInit();
TIM_SetCompare2(TIM4,550); //设定两舵机的初始位置
TIM_SetCompare4(TIM4,300);
// TIM_SetCompare1(TIM4,10000); //测试照明灯亮度是否可控
// TIM_SetCompare3(TIM4,10000);
while(1)
{
if(oled_flag != 0)
{
OLED_P6x8Str(3, 2,"SMART HOME!");
}
else
{
OLED_Cls();
}
}
}
大部分的动作代码放在串口中断服务函数中,主函数主要是对各模块进行初始化,具体的初始化函数分别在各外设的.c文件中。这里就不一一展示了。
该部分的代码主要是对接收到的指令进行对应操作(存在一些不足,具体会在后面章节中详细说明)
void USART1_IRQHandler(void) //串口1中断函数
{
u8 Res;
#if SYSTEM_SUPPORT_OS
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res = USART_ReceiveData(USART1);//(USART1->DR); 读取串口1的存储数据的寄存器
switch(Res)
{
//
case('1'):
{
//发送1激活
printf("Welcome to smart home system!!\r\n");
printf("Please send order to choose the mode:\r\n");
oled_flag = 1; //屏幕标志位置1,开始显示
printf("2--DoorBell\r\n3--Open Door\r\n4--Lighting\r\n5--Draw the Curtain\r\n6--Close thr Curtain\r\no--Close All\r\n");
LED_Bling(2); //指示灯闪2次,表示智能家居系统已被激活
USART1_ClearBUF(); //清除串口1缓冲区数据(自己编写)以保证串口1设定的100个数据不会溢出
break;
}
case('2'):
{
Beep_Bling();
USART1_ClearBUF();
break;
}
case('3'):
{
TIM_SetCompare2(TIM4,900);
delay_ms(2000);
TIM_SetCompare2(TIM4,550);
USART1_ClearBUF();
break;
}
case('4'):
{
printf("Please send l,m or h to choose Brightness!");
break;
}
case('l'):
{
TIM_SetCompare1(TIM4,2000);
TIM_SetCompare3(TIM4,2000);
break;
}
case('m'):
{
TIM_SetCompare1(TIM4,6000);
TIM_SetCompare3(TIM4,6000);
break;
}
case('h'):
{
TIM_SetCompare1(TIM4,10000);
TIM_SetCompare3(TIM4,10000);
break;
}
case('5'):
{
TIM_SetCompare4(TIM4,1100);
// delay_ms(3000);
// TIM_SetCompare4(TIM4,800);
USART1_ClearBUF();
break;
}
case('6'):
{
TIM_SetCompare4(TIM4,300);
USART1_ClearBUF();
break;
}
case('o'):
{
printf("GoodBye!");
oled_flag = 0;
TIM_SetCompare4(TIM4,300);
TIM_SetCompare1(TIM4,0);
TIM_SetCompare3(TIM4,0);
USART1_ClearBUF();
LED_Bling(1);
break;
}
}
if((USART_RX_STA&0x8000)==0)
{
if(USART_RX_STA&0x4000)
{
if(Res!=0x0a)USART_RX_STA=0;
else USART_RX_STA|=0x8000;
}
else //۹û˕ս0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
}
}
}
}
#if SYSTEM_SUPPORT_OS //ɧڻSYSTEM_SUPPORT_OSΪ֦ìղѨҪ֧ԖOS.
OSIntExit();
#endif
}
本项目中我选择的是BT08b蓝牙模块,购买之后,商家会给出具体的AT指令集以及配置指南,该模块只需要配置一次,故可以通过USB-TTL模块连接PC端先把模块配置好之后再连接单片机使用,这样该模块与普通的无线串口模块使用方式没有区别。
相比蓝牙模块,Wifi模块的配置相对比较复杂,需要在每次上电之后对我们需要连接的上位机端的热点,以及进行模式选择进入TCP/UDP透传模式,但是由于正点原子给出的例程较为复杂(实现“闭环”进行状态确定,接收指令反馈),本人接触时间也并没有很久,所以没有使用官方给出的例程,只是像esp8266发送指令,不对其反馈进行操作。具体实现代码如下:
#include "wifi.h"
#include "usart.h"
#include "delay.h"
void WIFI_Init(void)
{
printf("AT+CWMODE=1\r\n");
delay_ms(3000);
printf("AT+CWAUTOCONN=0\r\n");
delay_ms(3000);
printf("+++");
delay_ms(3000);
printf("AE0\r\n");
delay_ms(3000);
printf("AT+CWMODE=1\r\n");
delay_ms(3000);
printf("AT+RST\r\n");
delay_ms(5000);
printf("AT+CWAUTOCONN=0\r\n");
delay_ms(3000);
printf("AT+CWJAP=\"Name\",\"xxxxxxxx\"\r\n"); //热点名称(英文),后面为热点密码
delay_ms(8000);
printf("AT+CIPMUX=0\r\n");
delay_ms(3000);
printf("AT+CIPSTART=\"TCP\",\"10.41.75.244\",8080\r\n"); //地址及端口好
delay_ms(3000);
printf("AT+CIPMODE=1\r\n");
delay_ms(3000);
printf("AT+CIPSEND\r\n");
delay_ms(3000);
}
由于模块接收指令需要一定时间对指令进行处理并做出对应的操作,所以每输出一条指令之后,都进行一段时间的演示,所以Wifi模块在每次上电之后都需要一段较长的时间进行初始化。
此次的更新内容是在原先工程的基础上,完善了相关的用户界面,实现了在不同界面下调节时间、时制转换、秒表的开始/暂停、闹钟的设定、设定温湿度报警的阈值的功能。
(1)时制转换的功能是由拨码开关实现的----通过读取单片机IO口的高低电平。检测到电平不一致的时候就会进行时制转换;
(2)秒表和普通时钟&闹钟没有放在同一个定时器中,所以定时其采用了不同频率;
具体如何实现见如下代码(工程中的 tim.c 文件):
//时钟变量初始化
ClockNode CLK_interface =
{
.year = 2021, //闰年被四整除不被100整除
.month = 11, //月份数组
.date = 17,
.weekday = 3,
.h = 10,
.min = 0,
.s = 0,
.tomorrow_flag = 0
};
int month_days;
int month_1[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; //闰年/非闰年每月天数
int month_2[12] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int timer_flag = 0; //计时器开始与结束
//计时器初始化(定时器2---0.01s)
ClockNode Millisecond =
{
.h = 0,
.min = 0,
.s = 0,
.ms_10 = 0
};
//闹钟初始化
ClockNode Alarm =
{
.h = 0,
.min = 0,
.s = 0
};
void clock_24(void)
{
CLK_interface.s += 1;
if(CLK_interface.s > 59)
{
CLK_interface.min += 1;
CLK_interface.s = 0;
if(CLK_interface.min > 59)
{
CLK_interface.h += 1;
CLK_interface.min = 0;
Beep_Bling_once();
if(CLK_interface.h > 23)
{
CLK_interface.date += 1;
CLK_interface.h = 0;
if(CLK_interface.year % 4 == 0 && CLK_interface.year % 100 != 0) //判断是否为闰年
{
month_days = month_2[CLK_interface.month - 1];
}
else month_days = month_1[CLK_interface.month - 1];
if(CLK_interface.date > month_days)
{
CLK_interface.month += 1;
CLK_interface.date = 1;
if(CLK_interface.month > 12)
{
CLK_interface.year += 1;
CLK_interface.month = 1;
}
}
}
}
}
}
void clock_12(void)
{
CLK_interface.s += 1;
if(CLK_interface.s > 59)
{
CLK_interface.min += 1;
CLK_interface.s = 0;
if(CLK_interface.min > 59)
{
CLK_interface.h += 1;
CLK_interface.min = 0;
Beep_Bling_once();
if(CLK_interface.h > 11)
{
CLK_interface.h = 0;
if(CLK_interface.tomorrow_flag % 2 == 1)
{
CLK_interface.date += 1;
CLK_interface.tomorrow_flag = 0;
}
if(CLK_interface.year % 4 == 0 && CLK_interface.year % 100 != 0) //判断是否为闰年
{
month_days = month_2[CLK_interface.month - 1];
}
else month_days = month_1[CLK_interface.month - 1];
if(CLK_interface.date > month_days)
{
CLK_interface.month += 1;
CLK_interface.date = 1;
if(CLK_interface.month > 12)
{
CLK_interface.year += 1;
CLK_interface.month = 1;
}
}
}
}
}
}
void Millisecond_clock(void)
{
if(timer_flag % 2 ==1 ) Millisecond.ms_10 += 1;
if(Millisecond.ms_10 > 99)
{
Millisecond.s += 1;
Millisecond.ms_10 = 0;
if(Millisecond.s > 59)
{
Millisecond.min += 1;
Millisecond.s = 0;
if(Millisecond.min > 59)
{
Millisecond.h += 1;
Millisecond.min = 0;
}
}
}
}
int time_conv_flag = 1;
void TIM3_IRQHandler(void)
{
month_days = month_1[CLK_interface.month - 1];
if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
//time_conv_flag = SWITCH6_READ; //标记拨码开关状态反转
if(CLK_interface.h >= 12) CLK_interface.tomorrow_flag = 1;
if(CLK_interface.tomorrow_flag == 0 && CLK_interface.h == 11 && CLK_interface.min == 59 && CLK_interface.s == 59)
{
CLK_interface.tomorrow_flag = 1;
}
if(SWITCH6_READ == 0)
{
if(CLK_interface.tomorrow_flag%2 == 1 && SWITCH6_READ != time_conv_flag && CLK_interface.h > 12)
{
CLK_interface.h -= 12;
time_conv_flag = SWITCH6_READ; //记忆上一次拨码开关的状态
}
clock_12();
}
else
{
if(CLK_interface.tomorrow_flag%2 == 1 && SWITCH6_READ != time_conv_flag && CLK_interface.h < 12)
{
CLK_interface.h += 12;
time_conv_flag = SWITCH6_READ; //记忆上一次拨码开关的状态
}
clock_24();
if(CLK_interface.h == Alarm.h && CLK_interface.min == Alarm.min && CLK_interface.s == Alarm.s) Beep_Bling();
}
int alarm_flag = 0;
getTem_Humi();
if(DHT11.tempereture >= tempereture || DHT11.humidity >= humidity)
{
alarm_flag = 1;
if(DHT11.tempereture >= tempereture && DHT11.humidity >= humidity)
{
alarm_flag = 2;
}
}
if(alarm_flag == 1) LED_Bling(1);
if(alarm_flag == 2) Beep_Bling_once();
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIM3更新中断标志
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
if(Millisecond.h > 12)
{
Millisecond.h = 0;
}
Millisecond_clock();
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新中断标志
}
#include "User-interface.h"
#include "oled.h"
#include "tim.h"
#include "key.h"
#include "delay.h"
#include "DHT11.h"
UserinterfaceNode UserInterface =
{
.PageNum = 4,
.paraNum = 5
};
int Para = 0;
//时钟界面
void Clock_Interface(void)
{
OLED_P6x8Str(1, 3, "Custom_clock!");
OLED_P6x8Str(3, 1, "Date:");
OLED_P6x8Int(4, 2, CLK_interface.year, 4);
OLED_P6x8Str(4, 8, "-");
OLED_P6x8Int(4, 9, CLK_interface.month , 2);
OLED_P6x8Str(4, 13, "-");
OLED_P6x8Int(4, 14, CLK_interface.date, 2);
OLED_P6x8Str(5, 1, "Time:");
OLED_P6x8Int(6, 2, CLK_interface.h, 2);
OLED_P6x8Str(6, 5, ":");
OLED_P6x8Int(6, 6, CLK_interface.min, 2);
OLED_P6x8Str(6, 9, ":");
OLED_P6x8Int(6, 10, CLK_interface.s, 2);
OLED_P6x8Int(7, 15, Para % UserInterface.paraNum, 1);
}
void timer_clock(void)
{
OLED_P6x8Str(1, 5, "TIMER!!");
OLED_P6x8Str(3, 0, "Hour:");
OLED_P6x8Int(3, 5, Millisecond.h, 2);
OLED_P6x8Int(5, 2, Millisecond.min, 2);
OLED_P6x8Str(5, 5, ":");
OLED_P6x8Int(5, 6, Millisecond.s, 2);
OLED_P6x8Str(5, 9, ":");
OLED_P6x8Int(5, 10, Millisecond.ms_10, 2);
OLED_P6x8Int(7, 15, Para % UserInterface.paraNum, 1);
}
void Alarm_clock(void)
{
OLED_P6x8Str(1, 5, "Alarm clock!!");
OLED_P6x8Int(5, 2, Alarm.h, 2);
OLED_P6x8Str(5, 5, ":");
OLED_P6x8Int(5, 6, Alarm.min, 2);
OLED_P6x8Str(5, 9, ":");
OLED_P6x8Int(5, 10, Alarm.s, 2);
OLED_P6x8Int(7, 15, Para % UserInterface.paraNum, 1);
}
void Tem_Hum_DBG(void)
{
OLED_P6x8Str(1, 3, "TEM&HUMI-Debug");
OLED_P6x8Str(3, 0, "Temp:");
OLED_P6x8Int(3, 4, (int)DHT11.tempereture, 2);
OLED_P6x8Str(4, 0, "Humi:");
OLED_P6x8Int(4, 4, (int)DHT11.humidity, 2);
OLED_P6x8Str(5, 0, "Threshold:");
OLED_P6x8Str(6, 0, "Tem:");
OLED_P6x8Int(6, 4, tempereture, 2);
OLED_P6x8Str(6, 9, "Humi:");
OLED_P6x8Int(6, 14, humidity, 2);
OLED_P6x8Int(7, 15, Para % UserInterface.paraNum, 1);
}
void Key_Hub(void)
{
delay_ms(168);
if((KeyMessage.KeyValue = Key_Scan()) == KEY_PAGE)
{
++UserInterface.PageStatus;
UserInterface.PageStatus = (PAGE_STATUS)(UserInterface.PageStatus % UserInterface.PageNum);
OLED_Cls();
}
if(KeyMessage.KeyValue == KEY_PARA) Para += 1;
switch (UserInterface.PageStatus)
{
case Custom_clock:
{
UserInterface.paraNum = 6;
Clock_Interface();
switch(Para % UserInterface.paraNum)
{
case(0):
{
DebugI(&CLK_interface.year, 1);
break;
}
case(1):
{
if(CLK_interface.month <= 12 && CLK_interface.month > 0) DebugI(&CLK_interface.month, 1);
else CLK_interface.month = 1;
break;
}
case(2):
{
if(CLK_interface.year % 4 == 0 && CLK_interface.year % 100 != 0) //判断是否为闰年
{
month_days = month_2[CLK_interface.month - 1];
}
else month_days = month_1[CLK_interface.month - 1];
if(CLK_interface.date <= month_days && CLK_interface.date > 0) DebugI(&CLK_interface.date, 1);
else CLK_interface.date = 1;
break;
}
case(3):
{
if(CLK_interface.h < 24 && CLK_interface.h >= 0) DebugI(&CLK_interface.h, 1);
else CLK_interface.h = 0;
break;
}
case(4):
{
if(CLK_interface.min < 60 && CLK_interface.min >= 0) DebugI(&CLK_interface.min, 1);
else CLK_interface.min = 0;
break;
}
case(5):
{
if(CLK_interface.s < 60 && CLK_interface.s > 0) DebugI(&CLK_interface.s, 1);
else CLK_interface.s = 0;
break;
}
}
break;
}
case Timer_clock:
{
timer_clock();
if(KeyMessage.KeyValue == KEY_START_STOP)
{
timer_flag += 1; //决定定时器的使能与失能
}
if(timer_flag > 8)
{
Millisecond.h = 0;
Millisecond.min = 0;
Millisecond.s = 0;
Millisecond.ms_10 = 0;
timer_flag = 0;
}
break;
}
case Alarm_Clock:
{
UserInterface.paraNum = 3;
Alarm_clock();
switch(Para % UserInterface.paraNum)
{
case(0):
{
if(Alarm.h < 24 && Alarm.h >= 0) DebugI(&Alarm.h, 1);
else Alarm.h = 0;
break;
}
case(1):
{
if(Alarm.min < 60 && Alarm.min >= 0) DebugI(&Alarm.min, 1);
else Alarm.min = 0;
break;
}
case(2):
{
if(Alarm.s < 60 && Alarm.s >= 0) DebugI(&Alarm.s, 1);
else Alarm.s = 0;
break;
}
}
break;
}
case Tem_Hum:
{
UserInterface.paraNum = 2;
Tem_Hum_DBG();
switch(Para % UserInterface.paraNum)
{
case(0):
{
DebugI(&tempereture, 1);
break;
}
case(1):
{
DebugI(&humidity, 2);
break;
}
}
break;
}
}
}
此部分代码较为简单,相关的配置函数该博主的分享, 单总线通信的配置函数很好理解,看过之后复制之后很快就可以读取到温湿度数值了。
变化不大,基本上的功能都封装成函数了,主函数中非常干净整洁:
#include "headfile.h"
#include "User-interface.h"
// 硬件初始化
void BoardInit()
{
// GPIO_InitTypeDef GPIO_InitStructure; //GPIO的初始化
BEEP_Init(); //蜂鸣器初始化
TIM4_LightPWM_Init(10000,168); //照明装置初始化--频率50HZ
TIM4_SERVOPWM_Init(10000,168); //舵机初始化--频率50HZ
uart_init(115200); //串口1初始化,波特率为115200
delay_init(168); //delay函数的初始化
OLED_Init(); //OLED的初始化
LED_GPIO_Config(); //LED初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级组别为2(抢占优先级和响应优先级均为4位)
//EXTIX_Init(); //外部中断初始化
TIM3_Init(10000, 8400); //定时器3初始化-----1s
TIM2_Init(10000, 84); //定时器2初始化----0.01s
Key_Init(); //按键初始化
switch_Init(); //拨码开关初始化
DHT11_Init(); //温湿度模块初始化
}
int main()
{
BoardInit();
TIM_SetCompare2(TIM4,300);
TIM_SetCompare4(TIM4,300);
// TIM_SetCompare1(TIM4,10000);
// TIM_SetCompare3(TIM4,10000);
while(1)
{
//oled_flag = 1; //调试的时候方便看屏幕
if(oled_flag != 0)
{
//OLED_P6x8Str(3, 2,"SMART HOME!");
Key_Hub();
}
else
{
OLED_Cls();
}
}
}
由于当时对于串口接收的缓冲区数据了解不足,所以只能读取到一位数据,所以每个指令都设置为一位字符,并且没有区分指令的优先级(e.g.:没有设置开机指令执行之后才能执行其他指令、也没有设置控制灯亮度的指令必须在执行控灯指令执行之后才能执行),可以通过设置对应的标志位进行状态判别,从而实现指令的不同优先级设定。
由于没学过上位机的设计,所以导致用到的上位机都是网上界面比较简陋的上位机,之后如果有接触到的话,可以对应设计对应的上位机,使UI更加美观。
完整资料已上传百度网盘(有需要可以下载)
链接:https://pan.baidu.com/s/1RE_kGghCOCSc6G-6EwNKOA
提取码:0727
PS:希望该文对有需要的同学们能有一定的帮助,项目本身还有很多缺陷以及可以优化提升的方面,希望大佬们轻喷,不足的地方可以指正出来!!!