文章目录[隐藏]
基于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进行测试的问题,文章末尾有资料包可自行查看,此处只讲实际运用
附上代码和个人理解
//与AS608握手 PS_HandShake
//参数: PS_Addr地址指针
//说明: 模块返新地址(正确地址)
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; //默认
//初始化PA6为下拉输入
//读摸出感应状态(触摸感应时输出高电平信号)
void PS_StaGPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能GPIOA时钟
//初始化读状态引脚GPIOA
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;//输入下拉模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIO
}
// 对于官方的代码,建议不是很熟悉开发的不要改动配置
对于一个复杂模块的代码,其实不需要懂太多,看一下初始化,必要的功能函数,其他的函数只要知道它是干什么的,运用它即可。
贴上一两个重要的功能函数
//判断中断接收的数组有没有应答包
//waittime为等待中断接收数据的时间(单位1ms)
//返回值:数据包首地址
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;
}
/*
这里就是设置一些地址,接收数据包之后检查有无应答包
strstr函数常常用于一些需要指令的模块,函数的功能是判断参数str2是否包含在str1内,如果包涵就返回包涵数据的首地址,否则返回NULL。
用大白话说就是查包
查包是查应答包,包括指令的包头、模块地址、指令码。
*/
//录入图像 PS_GetImage
//功能:探测手指,探测到后录入指纹图像存于ImageBuffer。
//模块返回确认字
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卡发送电磁波,卡内的电路进行处理(此处有复杂的模电数电知识)然后由电子泵将产生的电荷进行储存。随后卡片就可以向读卡器发送或接收数据,实现通信。
流程为:放卡钥匙扣 -> 读卡器天线接收到信号 -> 读卡器内置电路处理信号并存储好(此处看代码命令是怎么操作的,我们的主要目标也在这里,你也可以读取到了信号但不计入卡内部) -> 验证或其他外围操作(指的是你读到了卡钥匙扣的信息后你想干嘛当然由你制定)
贴一些功能函数并适当讲解,完整工程文末会给出
/* 初始化函数 模块是spi的初始化 */
//RFID_RC522 GPIO SPI初始化
//RFID_NSS(CS) PB12
//RFID_RST PC0
//RFID_CLK PB13
//RFID_MOSI PB15
//RFID_MISO PB14
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; //RFID_CS
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_12);//拉高RFID_CS
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;//RFID_RST
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOC,&GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_0);//拉高
spi2_init(); //SPI2初始化
PcdReset(); //RC522初始化
PcdAntennaOff();//关天线
delay_ms(1);
PcdAntennaOn(); //开天线
M500PcdConfigISOType('A');//针对IOS14443A型卡进行初始化
}
//功 能:复位RC522
//返 回: 成功返回MI_OK
char PcdReset(void)
{
RST_1;
delay_us(10);
WriteRawRC(CommandReg,PCD_RESETPHASE);//启动命令的执行-执行复位指令
delay_us(1);
WriteRawRC(ModeReg,0x3d);//0x29?//0x3d//和Mifare卡通讯,CRC初始值0x6363
WriteRawRC(TReloadRegL,30);//timer=15ms//30
WriteRawRC(TReloadRegH,0);
WriteRawRC(TModeReg,0x8D);//Prescaler=3390
WriteRawRC(TPrescalerReg,0x3E);
WriteRawRC(TxAskReg,0x40); //forced to 100%ASK 天线驱动
PcdAntennaOn();
return MI_OK;
}
//初始化值块 查找的意思
void CZ(void)
{
Block=4;//将1扇区的数据块0初始化为值块 初始值为0
//寻卡、防冲突、选卡
if(Request_Anticoll_Select(PICC_REQALL,Card_Type,Card_Buffer,Card_ID)==MI_OK)
{
//用修改后的密码A验证卡片
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;
}
}
}
}
}
//这里的代码大概就是开发RC522功能的步骤了,其他的可以写比如钱包的加钱,减钱功能函数等
//减值(扣款)
void Decrement(void)
{
Block=4;
//寻卡、防冲突、选卡
if(Request_Anticoll_Select(PICC_REQALL,Card_Type,Card_Buffer,Card_ID)==MI_OK)
{
//用修改后的密码A验证卡片
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(); //LCD初始化
tp_dev.init(); //初始化触摸屏
以上三个就是其他花里胡哨的功能实现的前提下必须要写的。看起来低级,但往往有人功能写得花里胡哨却忘记初始化一个模块或者把它注释了之类的,那么就导致了不能使用LCD的功能。
其他形如一些指定的标准库函数就直接拿来用就好了
POINT_COLOR=BLUE; //字体颜色为蓝色
LCD_Clear(WHITE); //清屏
Show_Str_Mid(0,40,“LCD模块正常!”,16,240); //形如这样子的显示,前提是检测好字库
抽取一段实现触摸的代码进行说明
//得到触摸屏的输入
//x,y:键盘坐标
//返回值:(1~15,对应按键表)
u8 AS608_get_keynum(u16 x,u16 y)
{
u16 i,j;
static u8 key_x=0;//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)) //就是根据LCD屏幕的大小计算
{
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; //返回的key是对应LCD显示键盘上的数字
}
//触摸按键扫描
//tp:0,屏幕坐标;1,物理坐标(校准等特殊场合用)
//返回值:当前触屏状态.
//0,触屏无触摸;1,触屏有触摸
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)//触摸屏的1-9被按下,加载随机虚拟键盘
AS608_load_keyboard(0,170,(u8**)kbd_menu_random[temp_num]);//此处传入的menu可以更换
附上一个设置UI的代码
//加载按键界面(尺寸x,y为240*150)
//x,y:界面起始坐标(240*320分辨率的时候,x必须为0)
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);
/* 此处就是画一些条条框框来放置相应的UI值 */
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)//按键表第2个‘:’不需要中间显示
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; //UART4 TX;
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; //UART4 RX;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 9600; //波特率为9600
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //8 1 0初始化
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) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART4); //读取接收到的数据
//此处运用控制思想
/*如
if(Res==‘a’) LED0 = 0;
else if(Res=='b') LED0 = 1;
*/
if((USART4_RX_STA&0x8000)==0)//接收未完成
{
if(USART4_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART4_RX_STA=0;//接收错误,重新开始
else USART4_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
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); //配置目标TCP服务器
挑出最重要的一句来讲,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所指向的字符串。
//向ATK-ESP8266发送命令
//cmd:发送的命令字符串
//ack:期待的应答结果,如果为空,则表示不需要等待应答
//waittime:等待时间(单位:10ms)
//返回值:0,发送成功(得到了期待的应答结果)
// 1,发送失败
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;
}
/*
* 此函数专门用来测试AT命令,等待模块返回确认信息
* 如发送AT,模块回应OK,说明模块工作正常
*/
对于往后端发送数据格式的问题,可以是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连接到蓝牙设备。此调用将阻塞,直到成功或引发异常
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); //此模块用AT通信,并在此选用串口三
在项目的一开始,我的想法是,一个智能锁放在门口,那么出门或者回家的时候是不是要看一眼时间呢?而单机的时间难免会有误差,不久又得手动调整时间以保证时间的准确,那这还叫智能锁???智能从何而来呢?所以我的第一步就是先利用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); //申请50个字节的内存
p6=mymalloc(SRAMIN,20); //申请50个字节的内存
p7=mymalloc(SRAMIN,20); //申请50个字节的内存
p3=mymalloc(SRAMIN,20); //申请50个字节的内存
p4=mymalloc(SRAMIN,20); //申请50个字节的内存
p5=mymalloc(SRAMIN,20); //申请50个字节的内存
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);
//Show_Str(0,0,200,16,p,16,0);
USART3_RX_STA=0;
}
//其实这里的解包过程看不懂是正常的,你用串口助手看到SIM800C回传的时间包是什么样子的,即可看懂
//年
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中
RTC_Set(2020,z2,z3,z4,z5,30); //RTC设置时间
myfree(SRAMIN,p);
myfree(SRAMIN,p6);
myfree(SRAMIN,p7);
myfree(SRAMIN,p3);
myfree(SRAMIN,p4);
myfree(SRAMIN,p5);
}
拿到时间之后,就是显示到OLED上面了
//clendar就是RTC里面的结构体变量,因为之前已经用update_time();更新过了,所以直接显示即可
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(); //更新显示到OLED
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(); //更新显示到OLED
}
拿到时间之后,我想着这个模块不可能只有这一个功能吧?随后肯定就是电话和短信功能了,从实用性的想法出发,当你的智能锁忘记了密码之后,那不是很抓狂??破门而入?所以忘记密码的功能就交给它吧!首先要解决的如何发送中文短信的问题
//unicode gbk 转换函数
//src:输入字符串
//dst:输出(uni2gbk时为gbk内码,gbk2uni时,为unicode字符串)
//mode:0,unicode到gbk转换;
// 1,gbk到unicode转换;
void sim800c_unigbk_exchange(u8 *src,u8 *dst,u8 mode)
{
u16 temp;
u8 buf[2];
if(mode)//gbk 2 unicode
{
while(*src!=0)
{
if(*src<0X81) //非汉字
{
temp=(u16)ff_convert((WCHAR)*src,1);
src++;
}else //汉字,占2个字节
{
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 //unicode 2 gbk
{
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; //号码长度,最大15个数
u8 temp_code[4];
p=mymalloc(SRAMIN,100); //申请100个字节的内存,用于存放电话号码的unicode字符串
p1=mymalloc(SRAMIN,300);//申请300个字节的内存,用于存放短信的unicode字符串
p2=mymalloc(SRAMIN,100);//申请100个字节的内存 存放:AT+CMGS=p1
p3=mymalloc(SRAMIN,50);//申请50个字节的内存,用于存放密码
temp=mycode[0]*1000+mycode[1]*100+mycode[2]*10+mycode[3]; //整型密码转换为整型数字
sprintf((char *)temp_code,"%d",temp); //整型数字转换为字符串数字并保存进temp_code数组中
sim800c_unigbk_exchange(mycallbuf,p,1); //将电话号码转换为unicode字符串
sim800c_unigbk_exchange((u8*)sim900a_test_msg,p1,1);//将短信内容转换为unicode字符串
//temp_code就是我们当前锁体的密码
sim800c_unigbk_exchange(temp_code,p3,1); //将字符串密码转换为unicode字符串
sprintf((char*)p2,"AT+CMGS=\"%s\"",p);
if(sim800c_send_cmd(p2,">",200)==0) //发送短信命令+电话号码
{
u3_printf("%s%s",p1,p3); //发送短信内容到GSM模块
if(sim800c_send_cmd((u8*)0X1A,"+CMGS:",1000)==0)
{
LED1=!LED1;//发送结束符,等待发送完成(最长等待10秒钟,因为短信长了的话,等待时间会长一些)
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);//检查从GSM模块接收到的数据
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); //延迟等待AT指令发送完成
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) //error_num是全局变量
{
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做的,所以挺拉的,不过功能效果还是能看出来的
版权声明:本文为CSDN博主「白面师傅」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jiaqiddd/article/details/120386286
暂无评论