基于STM32F103的智能门锁系统
直接说明实现了什么效果 1 指纹解锁(基于AS608) 2 RFID解锁(基于RC522) 3 密码解锁 (基于LCD电容屏触摸控制) 4 蓝牙解锁 (基于HC-06) 5 后台服务器管理开锁信息(基于ESP8266) 6 APP集成蓝牙功能、门锁开锁信息 7 管理员密码修改开锁密码 8 管理员添加指纹删除指纹 9 实时同步网络时间(基于SIM800C) 10 监控开锁时间并手机发送短信或打电话预警开锁异常(基于SIM800C)
其他用到的模块或硬件:28BYJ-48电机、IIC通信OLED屏幕
实现的效果如视频展示 效果视频
注:这是大一利用暑假写的小demo,剪视频的时候比较乱,可以将就食用 哈哈哈
前言
此demo运用的多种模块,文中会对各个模块进行较为详细 的讲解,个人讲解有不到位的地方文末还将整理各个模块的官方或其他资料分享出来。此demo的主要难度就是各个模块之间的融合和信息上传,前后端数据整合
下面依次按照每个模块进行分析
接下来,本文会很长很长很长,挑自己需要的看即可,主要是要看整个系统的思路是怎么实现的。
一、AS608
看一眼长啥样子~
AS608 指纹识别模块主要是指采用了杭州晟元芯片技术有限公司(Synochip)的 AS608 指纹识别芯片 而做成的指纹模块,模块厂商只是基于该芯片设计外围电路,集成一个可供2次开发的指纹模块;所以,只要是基于AS608芯片的指纹模块,其控制电路及控制协议几乎是一样的,只是不同厂家的性能不同而已。(其实玩嵌入式的都知道,同种类型的模块代码都大差不差啦 ) 玩一个模块最先的必须先看各种技术指标,如上电电压,默认通讯设置,如波特率等。这一步必须要做,不然你就很有可能会烧了它(骚了它~) 呃…想表达的意思就是,看完指标我们才能更快的入手这个模块啊,对后续莫名其妙 的bug也能更快的排查。(其实根本没有那么多所谓莫名其妙的bug啦,肯定是你哪里的指标没看,或接错或代码逻辑错,科学都是很严谨的!!) 技术指标看完是不是就想上电啦?别急,先看看接线方式
- 采用8pin方式,其中有两个脚需要上电3.3V,分别是Vi和Vt(记得别上5V!!!
骚了它骚了它~ ) Vi上电是给整个模块供电,而Vt上电是给采集指纹的那块触摸感应屏幕上电,缺一不可。 - Tx和Rx看你用的哪个串口对应接。Rx、Tx反接记得检查。我用的是STM32F103ZET6,开发板为正点原子精英板,接的串口3,即Rx接PA3,Tx接PA2。波特率可以自己设置,默认波特率57600
- WAK脚接上一个可用于输入输出的ADC脚,此处接的PA6。接上此脚的原因是为了能让AS608模块收到相关的指纹命令后,做对应的输出。(所以要用输入输出捕获呀)
- 正常接GND即可
对于WAK脚,接不接看你要什么效果,一般脱机(PC)做项目要接上。
关于用PC端串口助手直接连接AS608进行测试的问题,文章末尾有资料包可自行查看,此处只讲实际运用
附上代码和个人理解
u8 PS_HandShake(u32 *PS_Addr)
{
SendHead();
SendAddr();
MYUSART_SendData(0X01);
MYUSART_SendData(0X00);
MYUSART_SendData(0X00);
delay_ms(200);
if(USART2_RX_STA&0X8000)
{
if(
USART2_RX_BUF[0]==0XEF
&&USART2_RX_BUF[1]==0X01
&&USART2_RX_BUF[6]==0X07
)
{
*PS_Addr=(USART2_RX_BUF[2]<<24) + (USART2_RX_BUF[3]<<16)
+(USART2_RX_BUF[4]<<8) + (USART2_RX_BUF[5]);
USART2_RX_STA=0;
return 0;
}
USART2_RX_STA=0;
}
return 1;
}
模块初始化代码
u32 AS608Addr = 0XFFFFFFFF;
void PS_StaGPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
对于一个复杂模块的代码,其实不需要懂太多,看一下初始化,必要的功能函数,其他的函数只要知道它是干什么的,运用它即可。
贴上一两个重要的功能函数
static u8 *JudgeStr(u16 waittime)
{
char *data;
u8 str[8];
str[0]=0xef;str[1]=0x01;str[2]=AS608Addr>>24;
str[3]=AS608Addr>>16;str[4]=AS608Addr>>8;
str[5]=AS608Addr;str[6]=0x07;str[7]='\0';
USART2_RX_STA=0;
while(--waittime)
{
delay_ms(1);
if(USART2_RX_STA&0X8000)
{
USART2_RX_STA=0;
data=strstr((const char*)USART2_RX_BUF,(const char*)str);
if(data)
return (u8*)data;
}
}
return 0;
}
u8 PS_GetImage(void)
{
u16 temp;
u8 ensure;
u8 *data;
SendHead();
SendAddr();
SendFlag(0x01);
SendLength(0x03);
Sendcmd(0x01);
temp = 0x01+0x03+0x01;
SendCheck(temp);
data=JudgeStr(2000);
if(data)
ensure=data[9];
else
ensure=0xff;
return ensure;
}
整个开发流程大概遵循以下步骤:
初始化硬件 →检查字库 →是否触摸校准(电阻屏) →与 AS608模块通讯 →通讯成功读取模块参数 →显示模块参数 →加载虚拟键盘 →while while 循环获取触摸键值 →判断键值进入录指纹或删流程 →判断触摸感应状态 →进入刷指纹流程。
串口调试AS608模块,注意波特率,57600和115200看实际情况 验证结果
二、RFID-RC522模块
看一眼长啥样子~ 该图片是读取卡 该图片是卡钥匙扣
RFID-RC522模块是众多射频模块中最常用的一种,由于模块涉及的东西还是较为复杂的,本文就简明扼要的挑选一些重要的特性进行介绍。 首先,卡本身是分扇区的,所谓分扇区,就是一片片的储存信息区域,且每个区域负责的信息不同,若需要查看请去找模块手册,我们本文的目标是教会大家怎么使用。 其次,要想对数据块进行操作,首先要看该数据块的控制位是否允许对数据块的操作,如果允许操作,再看需要验证什么密码,只有验证密码正确后才可以对该数据块执行相应操作,看到这里就会明白为什么代码里会有0XFF…的校验了。 卡的工作原理就是卡本身内置天线且绕线为线圈的形式封装在卡片内,工作的时候读卡器会向M1卡发送电磁波,卡内的电路进行处理(此处有复杂的模电数电知识)然后由电子泵将产生的电荷进行储存。随后卡片就可以向读卡器发送或接收数据,实现通信。
流程为:放卡钥匙扣 -> 读卡器天线接收到信号 -> 读卡器内置电路处理信号并存储好(此处看代码命令是怎么操作的,我们的主要目标也在这里,你也可以读取到了信号但不计入卡内部) -> 验证或其他外围操作(指的是你读到了卡钥匙扣的信息后你想干嘛当然由你制定)
贴一些功能函数并适当讲解,完整工程文末会给出
extern void open_close(void);
void RFIDGPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOC,&GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_0);
spi2_init();
PcdReset();
PcdAntennaOff();
delay_ms(1);
PcdAntennaOn();
M500PcdConfigISOType('A');
}
char PcdReset(void)
{
RST_1;
delay_us(10);
WriteRawRC(CommandReg,PCD_RESETPHASE);
delay_us(1);
WriteRawRC(ModeReg,0x3d);
WriteRawRC(TReloadRegL,30);
WriteRawRC(TReloadRegH,0);
WriteRawRC(TModeReg,0x8D);
WriteRawRC(TPrescalerReg,0x3E);
WriteRawRC(TxAskReg,0x40);
PcdAntennaOn();
return MI_OK;
}
void CZ(void)
{
Block=4;
if(Request_Anticoll_Select(PICC_REQALL,Card_Type,Card_Buffer,Card_ID)==MI_OK)
{
if(PcdAuthState(PICC_AUTHENT1A,Block,Modify_Key,Card_ID)==MI_OK)
{
USART_SendData(USART1,table[Block/10]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);
USART_SendData(USART1,table[Block%10]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);
USART_SendData(USART1,' ');
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);
usart1_sendstring("PcdAuthState_OK\r\n");
if(PcdWrite(Block,DefaultValue)==MI_OK)
{
if(PcdRead(Block,Data_Buffer)==MI_OK)
{
Printing(Data_Buffer,16);
usart1_sendstring("Init_OK\r\n");
usart1_sendstring("\r\n");
BEEP_SET;
delay_ms(500);
BEEP_CLR;
}
}
}
}
}
void Decrement(void)
{
Block=4;
if(Request_Anticoll_Select(PICC_REQALL,Card_Type,Card_Buffer,Card_ID)==MI_OK)
{
if(PcdAuthState(PICC_AUTHENT1A,Block,Modify_Key,Card_ID)==MI_OK)
{
USART_SendData(USART1,table[Block/10]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);
USART_SendData(USART1,table[Block%10]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);
USART_SendData(USART1,' ');
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)!=SET);
usart1_sendstring("PcdAuthState_OK\r\n");
if(PcdRead(Block,Data_Buffer)==MI_OK)
{
Printing(Data_Buffer,16);
if(Data_Buffer[0]<=0)
{
BEEP_SET;delay_ms(50);BEEP_CLR;delay_ms(50);
BEEP_SET;delay_ms(50);BEEP_CLR;delay_ms(50);
BEEP_SET;delay_ms(50);BEEP_CLR;
return;
}
if(PcdValue(PICC_DECREMENT,Block,value_Buf)==MI_OK)
{
if(PcdRead(Block,Data_Buffer)==MI_OK)
{
Printing(Data_Buffer,16);
usart1_sendstring("-_OK\r\n");
usart1_sendstring("\r\n");
BEEP_SET;
delay_ms(50);
BEEP_CLR;
}
}
}
}
}
}
整个工程下来,最重要的是怎么读取到你的当前卡号问题,代码中有个数组尾Card_ID,里面存储的就是读取到的卡号。读卡的操作也可以用电脑串口助手调试。如图所示(这不是卖广告,测试自带的)
三、LCD模块以及触摸功能讲解
因为LCD模块的代码多且复杂,故这里也就将它分开讲解并以实用为主。 首先因为LCD的型号也是多且杂的,先放上本文讲解的版本 它的功能用大白话来说就是,显示+输入。分辨率为240*320,此分辨率也就是能加载一些较为模糊的图片和弄好的UI布局,我了解过的一个是emwin图形界面,可以设计很多的“美丽”布局,见仁见智的美丽。 LCD屏幕另一个重要的用途就是用作触摸输入了。市面上又会分电阻屏和电容屏的区别,TFT只是一种液晶屏幕的材料,电容屏是靠电流热感应工作,只能用手指操作, 电阻屏靠压力工作,任何东西压在上面都有反应·所以用指甲或手写笔都可以。图中的是电阻屏 上代码先看看是怎么驱动的
#include "lcd.h"
LCD_Init();
tp_dev.init();
以上三个就是其他花里胡哨的功能实现的前提下必须要写的。看起来低级,但往往有人功能写得花里胡哨却忘记初始化一个模块或者把它注释了之类的,那么就导致了不能使用LCD的功能。 其他形如一些指定的标准库函数就直接拿来用就好了
POINT_COLOR=BLUE; //字体颜色为蓝色 LCD_Clear(WHITE); //清屏 Show_Str_Mid(0,40,“LCD模块正常!”,16,240); //形如这样子的显示,前提是检测好字库
抽取一段实现触摸的代码进行说明
u8 AS608_get_keynum(u16 x,u16 y)
{
u16 i,j;
static u8 key_x=0;
u8 key=0;
tp_dev.scan(0);
if(tp_dev.sta&TP_PRES_DOWN)
{
for(i=0;i<5;i++)
{
for(j=0;j<3;j++)
{
if(tp_dev.x[0]<(x+j*80+80)&&tp_dev.x[0]>(x+j*80)&&tp_dev.y[0]<(y+i*30+30)&&tp_dev.y[0]>(y+i*30))
{
key=i*3+j+1;
break;
}
}
if(key)
{
if(key_x==key)key=0;
else
{
AS608_key_staset(x,y,key_x-1,0);
key_x=key;
AS608_key_staset(x,y,key_x-1,1);
}
break;
}
}
}else if(key_x)
{
AS608_key_staset(x,y,key_x-1,0);
key_x=0;
}
return key;
}
u8 TP_Scan(u8 tp)
{
if(PEN==0)
{
if(tp)TP_Read_XY2(&tp_dev.x[0],&tp_dev.y[0]);
else if(TP_Read_XY2(&tp_dev.x[0],&tp_dev.y[0]))
{
tp_dev.x[0]=tp_dev.xfac*tp_dev.x[0]+tp_dev.xoff;
tp_dev.y[0]=tp_dev.yfac*tp_dev.y[0]+tp_dev.yoff;
}
if((tp_dev.sta&TP_PRES_DOWN)==0)
{
tp_dev.sta=TP_PRES_DOWN|TP_CATH_PRES;
tp_dev.x[4]=tp_dev.x[0];
tp_dev.y[4]=tp_dev.y[0];
}
}else
{
if(tp_dev.sta&TP_PRES_DOWN)
{
tp_dev.sta&=~(1<<7);
}else
{
tp_dev.x[4]=0;
tp_dev.y[4]=0;
tp_dev.x[0]=0xffff;
tp_dev.y[0]=0xffff;
}
}
return tp_dev.sta&TP_PRES_DOWN;
}
小总结:对于LCD的使用,普通显示直接调用库函数即可,注意根据屏幕大小实现显示;对于触摸使用,触摸不准确的时候可以进行触摸屏校准 TP_Adjust(); //屏幕校准 TP_Save_Adjdata();//保存校准参数 触摸的时候一是设置UI,二就是根据点击的物理地址返回UI的对应值 提供一个思路:输入密码的时候,可以根据点击事件的发生,更换UI界面,实现如同中国农业银行app输入密码的效果 思路代码如下:
if(key_num>=4&&key_num<=13)
AS608_load_keyboard(0,170,(u8**)kbd_menu_random[temp_num]);
附上一个设置UI的代码
void AS608_load_keyboard(u16 x,u16 y,u8 **kbtbl)
{
u16 i;
POINT_COLOR=RED;
kbd_tb2=kbtbl;
LCD_Fill(x,y,x+240,y+150,WHITE);
LCD_DrawRectangle(x,y,x+240,y+150);
LCD_DrawRectangle(x+80,y,x+160,y+150);
LCD_DrawRectangle(x,y+30,x+240,y+60);
LCD_DrawRectangle(x,y+90,x+240,y+120);
POINT_COLOR=BLUE;
for(i=0;i<15;i++)
{
if(i==1)
Show_Str(x+(i%3)*80+2,y+7+30*(i/3),80,30,(u8*)kbd_tb2[i],16,0);
else
Show_Str_Mid(x+(i%3)*80,y+7+30*(i/3),(u8*)kbd_tb2[i],16,80);
}
}
效果如下:
四、基于HC-06的蓝牙解锁
此处放上HC-05和HC-06的图片,第一张带有小按键的是05,第二张不带按键的是06 关于HC-05和HC-06的区别,在这里简单描述,HC-05和HC-06都是主从一体的蓝牙模块,意思就是都能配置主从模式,不同的地方是HC-05的指令集比HC-06的指令集丰富,HC-05可以查询模块的主从状态、PIN码等等。HC-06的优势就是更加的简洁便用,其实两个模块用哪个都无所谓。因为这个项目只是使用的从机模式,简单的收取一个字节的命令即可。此项目用的是HC-06 指令集给出如下: 关于驱动蓝牙模块的问题,其实就是串口通信,初始化一个串口,与蓝牙模块的波特率保持一致即可,此处用的是9600。简单的理解,波特率越高,传输的速率就越快。但是丢包率不一定,丢包率有时候并不关波特率大小的事,是看具体两个通信体之间的稳定性问题。此处不多作赘述。 STM32驱动蓝牙模块一般用串口1、2、3,但因为此项目工程量较大,用了过多的串口,故此处用的串口四 初始化代码如下
void UART4_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE );
RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(UART4, &USART_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = UART4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);
USART_Cmd(UART4, ENABLE);
}
蓝牙处理部分,运用最简单的处理一个buff位的思想
if(USART_GetITStatus(USART4, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART4);
if((USART4_RX_STA&0x8000)==0)
{
if(USART4_RX_STA&0x4000)
{
if(Res!=0x0a)USART4_RX_STA=0;
else USART4_RX_STA|=0x8000;
}
else
{
if(Res==0x0d)USART4_RX_STA|=0x4000;
else
{
USART4_RX_BUF[USART4_RX_STA&0X3FFF]=Res ;
USART4_RX_STA++;
if(USART4_RX_STA>(USART4_REC_LEN-1))USART4_RX_STA=0;
}
}
}
五、后台服务器管理开锁信息(基于ESP8266)
嵌入式最常用的WIFI通信模块莫过于ESP8266了。ESP8266是上海乐鑫信息科技设计的低功耗WiFi芯片,集成完整的TCP/IP协议栈和MCU。而ESP8266模块是深圳安信可公司基于ESP8266芯片研发(增加必要外围电路、串口flash、板载天线等)的串口WiFi模块,成本低、使用简便、功能强大。
玩单片机的应该都听说过单片机就是一台微型计算机?微型计算机?那不就是电脑嘛?电脑?那电脑岂能没网?
其实单片机联网也有以太网的形式连接,但是那也太笨重了吧。而小巧的ESP8266WiFi模块通过串口AT指令与单片机通讯,实现串口透传,非常好上手。
说了这么多,其实市面上有非常多种类的8266,本项目使用的是ATK-esp8266,可以直接插在开发板上使用。使用的是哪个类的8266无所谓,因为整个esp8266的协议都是一模一样的,只要会了一个种类的,其他的自然就会了。
其实就是插上去了就直接能跑例程了,我就是因为方便 才选用的这个。 首先介绍ESP8266的三种模式,分别为
- WIFI STA
STA站点,每一个连接到无线网络中的终端(如笔记本电脑、PDA及其它可以联网的用户设备)都可称为一个站点。简单的说就是手机热点,想要连接一个路由器而不消耗流量 - WIFI AP
AP,也就是无线接入点,是一个无线网络的创建者,是网络的中心节点。一般家庭或办公室使用的无线路由器就一个AP。简单的说就是路由器,等你的手机开启WiFi并连接上去。 - WIFI STA+AP
两种模式的共存模式,(STA 模式)即可以通过路由器连接到 互联网,并通过互联网控制设备; (AP模式)也可作为wifi热点,其他wifi 设备连接到模块。这样实现局域网和广域网的无缝切换,方便操作。
因为ESP8266的模式也是AT指令的方式,也就是通过串口的方式进行设置。接下来简单的介绍一些常用的AT指令和调试的经验。 基础AT指令:
执行指令 | 响应 | 功能 |
---|
AT | OK | 测试通信 | AT+RST | OK | 重启模块 | AT+RESTORE | OK | 恢复出厂设置 |
WIFI功能AT指令
执行指令 | 响应 | 功能 |
---|
AT+CWMODE? | +CWMODE:<mode> OK | 返回当前模块的模式 | AT+CWJAP? | +CWJAP:<ssid> OK | 返回当前选择的AP | AT+CIPSTA? | +CIPSTA:<ip> OK | 设置模块STA的IP地址 | AT+CIPAP? | +CIPAP:<ip> OK | 设置模块AP的IP地址 |
TCP/IP相关AT指令
由于篇幅原因,只介绍大致的指令集,详细的指令集文章末尾会给出用户手册
接下来梳理本项目实际应用ESP8266的过程,并分析怎么传数据到后端
sprintf((char*)p,"AT+CIPSTART=\"TCP\",\"%s\",%s",ipbuf,(u8*)portnum);
挑出最重要的一句来讲,ipbuf就是你后端服务器的ip地址,portnum就是后端服务器的端口号,只要这两个写对了,按照例程中的流程,走TCP三次握手连接之后,开启透传模式,就可以发送数据到服务器。
sprintf((char*)p,"ATK-8266%s测试%02d\r\n",ATK_ESP8266_WORKMODE_TBL[netpro],t/10);
连接完毕之后就可以发送数据进行测试了。如果没有服务器的话,测试的时候是有一个叫网络助手的软件的,软件会有ip地址和端口号,这个软件是专门供测试硬件网络模块使用的。
由于ESP8266使用的是串口调试,所以难免会用到sprintf的函数,关于这个函数,就是将后面的参数p2和p3发送格式化输出到str所指向的字符串。
u8 atk_8266_send_cmd(u8 *cmd,u8 *ack,u16 waittime)
{
u8 res=0;
USART3_RX_STA=0;
u3_printf("%s\r\n",cmd);
if(ack&&waittime)
{
while(--waittime)
{
delay_ms(10);
if(USART3_RX_STA&0X8000)
{
if(atk_8266_check_cmd(ack))
{
printf("ack:%s\r\n",(u8*)ack);
break;
}
USART3_RX_STA=0;
}
}
if(waittime==0)res=1;
}
return res;
}
对于往后端发送数据格式的问题,可以是json格式,也可以是其他格式,后端自己确定格式之后对数据包进行解包就行了,可以加一些帧头帧尾作为校验,包的格式可以参考网上的。如帧头为0x55 0x55,作为检测数据正确的标准。
六、APP集成蓝牙功能、门锁开锁信息
由于部分原因,导致APP连接后端部分的代码遗失,但之前项目是能够实现此功能的。此代码部分是由另一个项目的安卓方向的同学帮助开发。如今只剩下搜索蓝牙并连接蓝牙的部分。 给出一部分的代码
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bluetooth_control_panel);
SetSystemBar.setStatusBarColor(this, R.color.blue);
SetSystemBar.setAndroidNativeLightStatusBar(this, false);
Toolbar toolbar = findViewById(R.id.toolbar_BluetoothControlPanelActivity);
viewPager = findViewById(R.id.viewPager_BluetoothControlPanelActivity);
tabLayout = findViewById(R.id.tabLayout_BluetoothControlPanelActivity);
toolbar.setTitle("控制面板");
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
assert actionBar != null;
actionBar.setDisplayHomeAsUpEnabled(true);
loadAnimationDialog = LoadAnimationDialog.showDialog(this,"连接设备中,请稍后...");
bluetoothDevice = getIntent().getParcelableExtra("bluetoothDevice");
loadAnimationDialog.show();
new Thread(new Runnable() {
@Override
public void run() {
bluetoothAdapter.cancelDiscovery();
try {
socket = bluetoothDevice.createRfcommSocketToServiceRecord(bluetoothDevice.getUuids()[0].getUuid());
socket.connect();
} catch (Exception e) {
try {
socket.close();
loadAnimationDialog.cancel();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(BluetoothControlPanelActivity.this,"连接失败,请重试!",Toast.LENGTH_SHORT).show();
finish();
}
});
} catch (Exception ee) {
ee.printStackTrace();
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
loadAnimationDialog.cancel();
Toast.makeText(BluetoothControlPanelActivity.this,"连接成功!",Toast.LENGTH_SHORT).show();
List<Fragment> fragments = new ArrayList<>();
fragments.add(new ChatFragment());
fragments.add(new KeyboardFragment());
tabLayout.setTabTextColors(getResources().getColor(R.color.dark_grey),getResources().getColor(R.color.blue));
tabLayout.setupWithViewPager(viewPager);
BluetoothControlPanelActivity_ViewPagerAdapter viewPagerAdapter = new BluetoothControlPanelActivity_ViewPagerAdapter(getSupportFragmentManager(), fragments);
viewPager.setAdapter(viewPagerAdapter);
}
});
}
}).start();
}
完整代码文章末尾给出
七、其他综合功能(基于SIM800C)
SIM800C模块是一个GSM/GPRS模块,简单来说就是电话卡模块,可以实现的功能:低功耗语言通话、短信发送与接收、获取网络时间、其他如http、tcp协议的网络通信等。 不要以为这是一个网络模块,准确的来说这是一个基于通信基站(就是平时我们手机没有信号都会怪的那个基站),是基于GSM全球移动通讯系统(Global System for Mobile Communications)升级过来的,GPRS是通用分组无线业务(General Packet Radio Service)的简称。 本项目使用此项目实现的功能包括:
- 短信通信
- 电话通知
- 获取网络时间并显示
因为本项目用了ESP8266,所以就没用上网络的功能,不要问为什么不整合到一起,问就是之前并不知道SIM模块有网络功能!!!(理不直气也壮)
接下来先介绍SIM800C大致是怎么使用的: SIM800C其实也是一种使用串口命令的模块,设置它可以直接用一些AT指令的形式,但由于SIM800C的AT指令集实在是太多太杂了,所以文中只能罗列一些我自以为常用的AT指令,全部的指令集文章末尾给出。
- AT+CPIN?
该指令用于查询SIM卡的状态,主要是PIN码,如果该指令返回: +CPIN:READY,则 表明SIM卡状态正常,返回其他值,则有可能是没有SIM卡。在模块出现问题的时候,-一 定要先发送: AT+CPIN?, 查询一下,看看是不是SIM卡和SIM卡座没有接触好?如果返 回ERROR,则说明可能是SIM卡没接触好,用纱布擦一下SIM卡座和SIM卡的接触焊 盘,然后重装SIM卡,重启,一般就可以解决。 - AT+COPS?
该指令用于查询当前运营商,该指令只有在连上网络后,才返回运营商,否则返回空, 如返回: +COPS:0,0,“CHINA MOBILE”,表示当前选择的运营商是中国移动。 - ATD+<号码>
我们将利用ATK-SIM800C模块来拨打10086,并进行话费查询。 首先发送: ATE1,设置回显,再发送: AT+COLP=1,设置被叫号码显示。 然后,我们发送: ATD10086;(注意完整命令是有;的,且要加回车换行) 拨打10086, 在接通后,SIM800C模块返回: +COLP:1006129."”,此时,我们就可以听到中国移动那熟悉的声音了… - 短信命令
SIM800C是可以直接发送中英文短信的,作为China man,直接说明怎么发送中文的短信。 后续我会用代码来更直观的说明是如何使用的此功能。 - AT+CCLK?
指令功能是获取网络时间,返回的是一串字符,包括状态和实时的网络时间 想要的时间数据需要自己写解包的代码。 同样的,后续我会用代码来更直观的说明是如何使用的此功能。
在此工程中,我是这样来使用这个模块的: 首先,初始化是
usart3_init(115200);
在项目的一开始,我的想法是,一个智能锁放在门口,那么出门或者回家的时候是不是要看一眼时间呢?而单机的时间难免会有误差,不久又得手动调整时间以保证时间的准确,那这还叫智能锁???智能从何而来呢?所以我的第一步就是先利用SIM800C拿到实时网络时间。
update_time();
void update_time(void)
{
u8 *p,*p1,*p2;
u16 z1=0,z2=0,z3=0,z4=0,z5=0;
u16 t,i;
u16 *p6,*p7,*p3,*p4,*p5;
u16 s=0;
p=mymalloc(SRAMIN,50);
p6=mymalloc(SRAMIN,20);
p7=mymalloc(SRAMIN,20);
p3=mymalloc(SRAMIN,20);
p4=mymalloc(SRAMIN,20);
p5=mymalloc(SRAMIN,20);
POINT_COLOR=BLUE;
USART3_RX_STA=0;
if(sim800c_send_cmd("AT+CCLK?","+CCLK:",200)==0)
{
p1=(u8*)strstr((const char*)(USART3_RX_BUF),"\"");
p2=(u8*)strstr((const char*)(p1+1),":");
p2[3]=0;
sprintf((char*)p,"日期时间:%s",p1+1);
USART3_RX_STA=0;
}
for(i=9;i<11;i++)
{
if(p[i]>='0'&&p[i]<='9')
{
*(p6+s)=p[i]-48;
s++;
}
}
s=0;
for(i=12;i<14;i++)
{
if(p[i]>='0'&&p[i]<='9')
{
*(p7+s)=p[i]-48;
s++;
}
}
s=0;
for(i=15;i<17;i++)
{
if(p[i]>='0'&&p[i]<='9')
{
*(p3+s)=p[i]-48;
s++;
}
}
s=0;
for(i=18;i<20;i++)
{
if(p[i]>='0'&&p[i]<='9')
{
*(p4+s)=p[i]-48;
s++;
}
}
s=0;
for(i=21;i<23;i++)
{
if(p[i]>='0'&&p[i]<='9')
{
*(p5+s)=p[i]-48;
s++;
}
}
for(t=0;t<2;t++)
{
z1+=(*(p6+t))*pow(10,2-(t+1));
z2+=(*(p7+t))*pow(10,2-(t+1));
z3+=(*(p3+t))*pow(10,2-(t+1));
z4+=(*(p4+t))*pow(10,2-(t+1));
z5+=(*(p5+t))*pow(10,2-(t+1));
}
RTC_Set(2020,z2,z3,z4,z5,30);
myfree(SRAMIN,p);
myfree(SRAMIN,p6);
myfree(SRAMIN,p7);
myfree(SRAMIN,p3);
myfree(SRAMIN,p4);
myfree(SRAMIN,p5);
}
拿到时间之后,就是显示到OLED上面了
if(t!=calendar.sec)
{
t=calendar.sec;
OLED_ShowNum(0,16,calendar.w_year,4,16);
OLED_ShowString(36,16,"-",16);
OLED_ShowNum(44,16,calendar.w_month,2,16);
OLED_ShowString(62,16,"-",16);
OLED_ShowNum(70,16,calendar.w_date,2,16);
OLED_ShowString(0,0,"Time:",16);
OLED_Refresh_Gram();
switch(calendar.week)
{
case 0:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,8,1);
break;
case 1:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,2,1);
break;
case 2:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,3,1);
break;
case 3:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,4,1);
break;
case 4:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,5,1);
break;
case 5:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,6,1);
break;
case 6:
Show_MyChinese(48,0,0,1);
Show_MyChinese(64,0,1,1);
Show_MyChinese(80,0,7,1);
break;
}
OLED_ShowNum(32,32,calendar.hour,2,16);
OLED_ShowNum(56,32,calendar.min,2,16);
OLED_ShowNum(80,32,calendar.sec,2,16);
OLED_Refresh_Gram();
}
拿到时间之后,我想着这个模块不可能只有这一个功能吧?随后肯定就是电话和短信功能了,从实用性的想法出发,当你的智能锁忘记了密码之后,那不是很抓狂??破门而入?所以忘记密码的功能就交给它吧!首先要解决的如何发送中文短信的问题
void sim800c_unigbk_exchange(u8 *src,u8 *dst,u8 mode)
{
u16 temp;
u8 buf[2];
if(mode)
{
while(*src!=0)
{
if(*src<0X81)
{
temp=(u16)ff_convert((WCHAR)*src,1);
src++;
}else
{
buf[1]=*src++;
buf[0]=*src++;
temp=(u16)ff_convert((WCHAR)*(u16*)buf,1);
}
*dst++=sim800c_hex2chr((temp>>12)&0X0F);
*dst++=sim800c_hex2chr((temp>>8)&0X0F);
*dst++=sim800c_hex2chr((temp>>4)&0X0F);
*dst++=sim800c_hex2chr(temp&0X0F);
}
}else
{
while(*src!=0)
{
buf[1]=sim800c_chr2hex(*src++)*16;
buf[1]+=sim800c_chr2hex(*src++);
buf[0]=sim800c_chr2hex(*src++)*16;
buf[0]+=sim800c_chr2hex(*src++);
temp=(u16)ff_convert((WCHAR)*(u16*)buf,0);
if(temp<0X80){*dst=temp;dst++;}
else {*(u16*)dst=swap16(temp);dst+=2;}
}
}
*dst=0;
}
完整发送当前锁体密码的代码如下:
void send_messege(void)
{
u8 *p,*p1,*p2,*p3;
u16 temp;
u8 pohnenumlen=11;
u8 temp_code[4];
p=mymalloc(SRAMIN,100);
p1=mymalloc(SRAMIN,300);
p2=mymalloc(SRAMIN,100);
p3=mymalloc(SRAMIN,50);
temp=mycode[0]*1000+mycode[1]*100+mycode[2]*10+mycode[3];
sprintf((char *)temp_code,"%d",temp);
sim800c_unigbk_exchange(mycallbuf,p,1);
sim800c_unigbk_exchange((u8*)sim900a_test_msg,p1,1);
sim800c_unigbk_exchange(temp_code,p3,1);
sprintf((char*)p2,"AT+CMGS=\"%s\"",p);
if(sim800c_send_cmd(p2,">",200)==0)
{
u3_printf("%s%s",p1,p3);
if(sim800c_send_cmd((u8*)0X1A,"+CMGS:",1000)==0)
{
LED1=!LED1;
Show_Str_Mid(0,120,"发送成功!",16,240);
delay_ms(1000);
LCD_Fill(0,120,lcddev.width,160,WHITE);
}
}
USART3_RX_STA=0;
mycallbuf[pohnenumlen]=0;
if(USART3_RX_STA&0X8000)sim_at_response(1);
myfree(SRAMIN,p);
myfree(SRAMIN,p1);
myfree(SRAMIN,p2);
myfree(SRAMIN,p3);
}
完成了短信功能之后,我想着,那这个智能锁,没有防盗或者防范的体现吗?于是乎,我想到了,盗贼都是深夜无人或者观察居住者不在家的时候进行偷盗,那么,我是不是可以设定在某个我需要的特定时间点,有开锁的行为进行电话预警呢?当然了,如果恰好是我开的,直接挂了就行,益大大的大于弊!好,那就开始!
if(calendar.hour>=19&&calendar.hour<=22)
{
Show_Str_Mid(0,140,"开锁时间异常",16,240);
u3_printf("ATD%s;\r\n",mycallbuf);
delay_ms(600);
LCD_Fill(0,140,lcddev.width,160,WHITE);
}
哈哈,其实核心的代码就这几句,但你要知道,其实有时候好的效果是关乎你的想法而不是你复杂的代码
对于这句话,将此想法和恶意尝试解锁的防范写出完整代码如下:
u16 Check_code(void)
{
u16 i;
u16 lock_num;
u16 temp_num=0;
if(error_num>=3)
{
do
{
Show_Str_Mid(0,100,"密码输入错误5次,请30秒后重试",16,240);
delay_ms(1000);
temp_num++;
}while(temp_num<error_num*10);
LCD_Fill(0,100,lcddev.width,160,WHITE);
}
update_time();
Show_Str_Mid(0,100,"请输入解锁密码...",16,240);
lock_num=LOCK_GET_NUM();
for( i = 4 ; i > 0 ; i-- )
{
testcode[i-1]=lock_num%10;
lock_num/=10;
}
delay_ms(500);
LCD_Fill(0,100,lcddev.width,160,WHITE);
Show_Str_Mid(0,100,"正在检索密码...",16,240);
if(testcode[0]==mycode[0]&&testcode[1]==mycode[1]&&testcode[2]==mycode[2]&&testcode[3]==mycode[3])
{
LCD_Fill(0,100,lcddev.width,160,WHITE);
Show_Str_Mid(0,120,"密码正确!正在解锁...",16,240);
if(calendar.hour>=19&&calendar.hour<=22)
{
Show_Str_Mid(0,140,"开锁时间异常",16,240);
u3_printf("ATD%s;\r\n",mycallbuf);
delay_ms(600);
LCD_Fill(0,140,lcddev.width,160,WHITE);
}
AS608_load_keyboard(0,170,(u8**)kbd_menu);
open_close();
LCD_Fill(0,120,lcddev.width,160,WHITE);
return 1;
}
else
{
error_num++;
LCD_Fill(0,100,lcddev.width,160,WHITE);
LCD_ShowxNum(10,120,error_num,1,16,0);
Show_Str_Mid(0,120,"次密码错误!请重新输入...",16,240);
AS608_load_keyboard(0,170,(u8**)kbd_menu);
BEEP=1;
delay_ms(1000);
BEEP=0;
LCD_Fill(0,120,lcddev.width,160,WHITE);
return 0;
}
}
总结
整个项目的实现是大一暑假的时候自己学习STM32并完成的,有一些地方肯定是存在思考不周到之处,即所谓的BUG,当然这是思维bug,当然还存在大量的代码bug(项目用到的东西太多,这是肯定的,只是整体系统测试还并未完全到位)写下这个博客也是为了记录自己的项目和学习状态,慢慢进步即可。
这里整个博客是为了大致的展示项目的架构与设计,每个模块并未更加详细的介绍。后续会将每个模块拿出来并将实际应用该模块的过程或方法给出,模块涉及的协议或算法,也将详细给出,详情可关注博客后续。
整篇博文很长很长很长,课余时间花费了差不多一个月整理出来的,也有其他事情在忙。
因为演示视频是大一自学pr做的,所以挺拉的,不过功能效果还是能看出来的
演示视频
ESP8266用户手册
蓝牙串口APP源码
SIM800CAT指令集
TJDZ-RC522射频卡用户使用手册资料Ver_1.0
RFRD可用工程
HC-05AT指令集
|