先简单说一下实验目的吧。平时做项目或做一些小作品的时候需要用到时间,时间用的是STM32内部的RTC,在精度要求不是特别高时这样省去接外设时钟模块,省时省力。但我们都知道,RTC在断电后数据是不保存的,也就是说如果没有电源如电池之类一直给后备寄存器供电的话数据是会丢失的,下次开机时时间就会恢复初始化时设置的那个时间,想要时间正确就要重新设置时间,这就很不实用。所以就想通过网络获取时间的方式来自动校正时间。又恰好用到SIM900A这个模块,所以查了下资料,发现已经有前辈做过了。看了https://blog.csdn.net/ludaoyi88/article/details/51757664这位博主的文章,获取时间部分用了这位前辈提供的代码,在他提供的代码上进行测试和改进(直接用不修改的话是不行的。如果大家仔细对比的话会发现其实我改进后的代码跟原版的还是有蛮多小细节不同的),最终得以达到目的,即可以通过服务器获取到网络的时间并自动校正到STM32内部RTC中,这里再次感谢 ludaoyi123这位前辈。大家可以先去看看这位前辈的博客,也就是上面那个链接,获取时间和处理时间数据都是源于他的那篇文章。好了,接下来就说一下怎么获取时间的吧。
此次试验用的单片机是STM32F103C8T6核心板,串口2控制SIM900A模块数据的收发,串口1用于在串口调试助手打印相关信息。下图是我的硬件平台:STM32F103C8T6核心板和SIM900A模块。
获取网络时间的第一种方法是连接到国外的授时服务器,IP为:time.nist.gov,端口为:13,我用的连接方式是TCP连接。当客户端连接到此服务器后,服务器会立刻发送一串格式为“58646 19-06-12 16:05:36 50 0 0 668.3 UTC(NIST) * ”这样的字符串返回给客户端并主动断开连接。这一串字符串中就包含有日期和时间,我们所要做的,就是把相关的日期和时间提取出来转换成数字就好了,这也是最最重要的部分。这里贴出我修改后的代码:`
_nowtime_obj NowTime; //现在时间日期结构体
///*******************************************************************************
//* 函数名 : Get_Sever_Time
//* 描述 : 获取Time信息(连接服务器成功情况下)
//* 输入 :
//* 输出 :
//* 返回 :
//* 注意 :服务器返回的数据形式如下:58646 19-06-12 16:05:36 50 0 0 668.3 UTC(NIST) *
//*******************************************************************************/
void Get_Sever_Time(void)
{
u8 i =0;
char timestr1[200]={0};
char *timestr = timestr1;//指向timestr1地址
printf("\r\n获取时间日期中...\r\n");
while(1)
{
if(strstr((const char*)USART2_RX_BUF , "-") != NULL || strstr((const char*)USART2_RX_BUF , "5") != NULL )
{
timestr = strstr((const char*)USART2_RX_BUF,"5");//58646//USART2_RX_BUF 为接收缓存数组
break;
}
}
printf("\r\n时间数组timestr的数据为");
printf((char *)timestr);
printf("\r\n");
delay_ms(5);
//提取UTC世界时间
for(i = 0 ; i <50 ; i++)
{
if(timestr[i] == '-')
{
NowTime.year = (timestr[i-2]-'0')*10 + (timestr[i-1]-'0') + 2000;
NowTime.moon = (timestr[i+1]-'0')*10 + (timestr[i+2]-'0');
NowTime.day = (timestr[i+4]-'0')*10 + (timestr[i+5]-'0');
NowTime.hour = (timestr[i+7]-'0')*10 + (timestr[i+8]-'0');//时差相差8
if(NowTime.hour >= 16)//北京时间新的一天
{
NowTime.hour = (timestr[i+7]-'0')*10 + (timestr[i+8]-'0') + 8 - 24;
NowTime.day = NowTime.day + 1;
if(NowTime.hour == 24)
NowTime.hour = 0;
}
else
NowTime.hour = (timestr[i+7]-'0')*10 + (timestr[i+8]-'0') + 8 ;//时差相差8
NowTime.minu = (timestr[i+10]-'0')*10 + (timestr[i+11]-'0');
NowTime.sec = (timestr[i+13]-'0')*10 + (timestr[i+14]-'0') + 2;//2是返回数据到处理处结果的误差
sprintf((char*)TimeRTC,"AT+CCLK=\"%d/%02d/%02d,%02d:%02d:%02d+08\"\r\n",NowTime.year,NowTime.moon,NowTime.day,NowTime.hour,NowTime.minu,NowTime.sec);
//转换成RTC时间格式
break;
}
}
printf("\r\n当前时间:%d年%02d月%02d日%02d时%02d分%02d秒\r\n",NowTime.year,NowTime.moon,NowTime.day,NowTime.hour,NowTime.minu,NowTime.sec);
printf("\r\n写入SIM900A设置时间的字符串为:");
printf((char *)TimeRTC);
printf("\r\n");
if(NowTime.year!=0&&NowTime.moon!=0&&NowTime.day!=0&&NowTime.hour!=0&&NowTime.minu!=0)//获取到正确数据
{
RTC_Set(NowTime.year,NowTime.moon,NowTime.day,NowTime.hour,NowTime.minu,NowTime.sec); //设置STM32单片机内部RTC时间
Set_SIM900A_RTCtime();//给GSM模块设置从网络获取来的时间
}
else
printf("\r\n没能在服务器获取到正确时间,复位重试一下\r\n");
delay_ms(500);
Get_GSM_RTCtime();//用串口查看一下GSM模块时间正常了没有
sim900a_send_cmd("AT+CIPSHUT\r\n","SHUT OK",2); //关闭连接
delay_ms(300);
AT_DataInit();//清除接收数组
CLR_Buf2();
delay_ms(200);
}
代码内容很简单,就是连接上服务器后用strstr函数和字符“-”或者“5”比较串口2接收数组中的“58646 19-06-12 16:05:36 50 0 0 668.3 UTC(NIST) * ”这串字符串,如果串口二接收数组中接收到了这一串字符串,那个就将这串字符串保存到数组timestr中并跳出死循环。实验过程中发现最容易出现问题的就是这一块,也就是有时候串口2没能收到完整的字符串导致程序死在死循环里面,应该是串口2中断服务函数没写好。接到完整的字符串后,接下来接着把年月日时分秒提取出来,把字符变成数字并做判断转换成北京时间,当服务器中的小时大于或等于16时当地小时+8-24,且天数+1,否则小时只是单纯地+8,这个8就是时差。数据处理好后就给STM32内部的RTC设置时间和给GSM模块的RTC设置时间,后续如果要重新校正时间的话只需要在SIM900A内部获取RTC的时间就可以了。实验过程中相关信息在串口调试助手显示,结果如下图所示:
一次成功不代表什么,所以又在不同时间段多次测试:
测试结果表明,实验是成功的,也就是在SIM900A连接到服务器后可以获取到正确时间并校正时间。
但测试的过程中发现上面连接服务器的方法并不是最佳的,原因是连接服务器不是很好用,有时候需要六七秒就能连上,有时候要半分多钟才能连上,感觉不可靠,就像刷脸一样,而且,有时候辛辛苦苦连上了,串口2还不能完完整整接收到那串字符串。。。所以,提供了第二个获取时间的方法,也就是直接从SIM900A内部获取时间日期,下面贴上代码:
_rtctime_obj RTCTime; //SIM900A时间日期结构体
/*******************************************************************************
* 函数名 : Get_GSM_RTCtime(void)
* 描述 : 获取SIM900A模块中RTC的时间
* 输入 :
* 输出 :
* 返回 :RTCTime的时间结构体--年月日时分秒
* 注意: 如:"19/06/18,13:11:52+08";
*******************************************************************************/
void Get_GSM_RTCtime(void)//直接从GSM模块内部获取时间,初次上电时需要手动复位
{
u8 i = 0;
char timestr1[50]={0};
char *timestr = timestr1;//指向timestr1地址
AT_DataInit();//清除接收数组
sim900a_send_cmd("AT+CCLK?\r\n","OK",5);
delay_ms(1000);
printf("\r\n获取SIM900A内部时间日期中...\r\n");
while(1)
{
if(strstr((const char*)USART2_RX_BUF , "+CCLK") != NULL )
{
timestr = strstr((const char*)USART2_RX_BUF , "+CCLK");
break;
}
}
delay_ms(500);
for(i = 0 ; i <50 ; i++)
{
if(timestr[i] == '/')
{
RTCTime.year = (timestr[i-2]-'0')*10 + (timestr[i-1]-'0') + 2000;
RTCTime.moon = (timestr[i+1]-'0')*10 + (timestr[i+2]-'0');
RTCTime.day= (timestr[i+4]-'0')*10 + (timestr[i+5]-'0');
RTCTime.hour = (timestr[i+7]-'0')*10 + (timestr[i+8]-'0');
RTCTime.minu = (timestr[i+10]-'0')*10 + (timestr[i+11]-'0');
RTCTime.sec = (timestr[i+13]-'0')*10 + (timestr[i+14]-'0');
break;
}
}
printf("\r\nGSM内部时间:%d年%02d月%02d日%02d时%02d分%02d秒\r\n",RTCTime.year,RTCTime.moon,RTCTime.day,RTCTime.hour,RTCTime.minu,RTCTime.sec);
RTC_Set(RTCTime.year,RTCTime.moon,RTCTime.day,RTCTime.hour,RTCTime.minu,RTCTime.sec); //设置STM32单片机内部RTC时间
AT_DataInit();//清除接收数组
memset(USART2_RX_BUF,0,USART2_MAX_RECV_LEN);
}
这代码跟上面的差不多,这里就不重复解释了,也很容易看懂。用法是直接在主函数调用就行了。需要注意的是,板子初次上电时需要手动复位一下单片机,不然串口发送AT+CCLK?命令给SIM900A模块时它不会返回字符串。这里的代码也是上面提到的那位博主提供我再进行小小修改的,我只是代码搬运工。然后发现用AT+CCLK?指令查询时间并没有网上说的那样不能用,反而觉得更好用,个人比较推荐这种方法。其效果图如下所示:
下面给出main.c和串口2配置及中断服务函数代码,请各路大神批评指正,提出宝贵意见。
/**
******************************************************************************
* @file main.c
* @author GXNU_LPK
* @version V1.0
* @date 2019-06-17
* @brief 用3.5.0版本库建的工程模板
******************************************************************************
* @attention
*
* 实验平台: STM32F103CT6核心板
* 实验内容:GSM(SIM900A)模块通过服务器实现网络授时
* 实验作者:广西师范大学电子工程学院2015LPK
* 备 注:Get_net_time.c这部分的代码来源于https://blog.csdn.net/ludaoyi88/article/details/51757664 ,
* 根据此ludaoyi123博主提供的思路和代码进行测试和修改而来,经实验测试和改善后目前已初步达到实验目的,但
* 通过服务器获取时间那种方法 稳定性方面略微存在一些欠缺,请学习者自行改善,仅供学习,不得用于其他用途
******************************************************************************
*/
#include "stm32f10x.h"
#include "GSM.h"
#include "AT_Cmd.h"
#include "usart2.h"
#include "usart1.h"
#include "delay.h"
#include "string.h"
#include "rtc.h"
#include "Get_net_time.h"
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
u8 res;
delay_init();
Usart2_Init(115200); //初始化串口2
delay_ms(3);
Usart1_Init(115200); //初始化串口1
if(RTC_Init()==0)
printf("RTC初始化成功\r\n");
else
printf("RTC初始化失败\r\n");
printf("初始化SIM900A中...\r\n");
res=1;
while(res)
{
res=GSM_Dect();
delay_ms(2000);
}
res=1;
while(res)
{
res=SIM900A_CONNECT_SERVER_SEND_INFOR((u8*)"time.nist.gov",(u8*)"13");//连接授时服务器(国外)
}
Get_Sever_Time();//提取获取到的时间并存入STM32和GSM模块内部RTC中
// Get_GSM_RTCtime();//直接从GSM模块内部获取时间,初次上电时需要手动复位
printf("\r\n系统初始化完成\r\n");
while(1)
{
display_time();//显示STM32内部RTC的时间
delay_ms(1000);
}
}
/*********************************************END OF FILE**********************/
串口2配置及中断服务函数
#include "usart2.h"
#include "stdio.h"
#include "string.h"
#include "stdarg.h"
//串口接收缓存区
u8 USART2_RX_BUF[USART2_MAX_RECV_LEN]; //接收缓冲,最大USART2_MAX_RECV_LEN个字节.
u8 USART2_TX_BUF[USART2_MAX_SEND_LEN]; //发送缓冲,最大USART2_MAX_SEND_LEN字节
u16 USART2_RX_STA=0;
/*
功能描述: 发送一个字节
函数参数: byte —— 要发送的字节
返回说明: 无
*/
void UART2_SendByte(unsigned char byte)
{
USART_SendData(USART2,byte);//向串口2发送数据
while(USART_GetFlagStatus(USART2,USART_FLAG_TC)!=SET);//等待发送结束
}
/*
功能描述: 串口发送字符串
函数参数: s —— 指向字符串的指针(字符串以'\0'结尾)
返回说明: 无
注:如果在字符串结尾有'\n',则会发送一个回车换行
*/
void UART2_SendStr(char *s)
{
while( *s != '\0')
{
UART2_SendByte( *s );
s ++;
}
}
void Usart2_Init(unsigned int bps)
{
USART_InitTypeDef USART_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
USART_DeInit(USART2);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2; //TX PA2
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_10MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure );
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3; //RX PA3
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure );
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannel=USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0x01 ;
NVIC_InitStructure.NVIC_IRQChannelSubPriority= 1 ;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_InitStructure.USART_BaudRate=bps;
USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
USART_InitStructure.USART_Parity=USART_Parity_No;
// USART_InitStructure.USART_StopBits=USART_StopBits_1;
USART_InitStructure.USART_StopBits=USART_StopBits_2;
USART_InitStructure.USART_WordLength=USART_WordLength_8b;
USART_Init(USART2,&USART_InitStructure );
// USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
USART_Cmd(USART2,ENABLE);
}
//串口2,printf 函数
//确保一次发送数据不超过USART2_MAX_SEND_LEN字节
void u2_printf(char* fmt,...)
{
u16 i,j;
va_list ap;
va_start(ap,fmt);
vsprintf((char*)USART2_TX_BUF,fmt,ap);
va_end(ap);
i=strlen((const char*)USART2_TX_BUF); //此次发送数据的长度
for(j=0;jDR);
USART2_RX_BUF[USART2_RX_STA++]=USART2->DR;
USART_ClearFlag(USART2,USART_FLAG_ORE);//读SR
}
else if(USART_GetFlagStatus(USART2,USART_FLAG_IDLE)!=Bit_RESET)
{
clear=USART2->SR;
clear=USART2->DR;
USART2_RX_STA=0;
}
}
完整工程下载链接:https://download.csdn.net/download/qq_36112455/11247378
版权所有,转载请注明出处,谢谢!代码仅供学习,不得用于其他用途。
时隔一年多,现在回头看了下,上文提到的问题是串口2中断接收没处理好造成的。如果不想改中断服务函数的话就在void Get_Sever_Time(void)函数下把所有USART2_RX_BUF改成AT_RecvBuffer可以解决此问题,而且再也不会出现上文提到的那些问题。其实用服务器那种方式还是很稳的,只需要小小改动一下,在void Get_Sever_Time(void)函数下把所有USART2_RX_BUF改成AT_RecvBuffer就好了,克服了上文说的那些缺点,今晚测试过好多次了,没有失败过,每次都成功。而且连接服务器的速度其实是跟信号有关的,之前那个地方信号太弱了,导致连接速度比较慢,在信号好的地方一下子就连上了。还有,AT+CCLK?只是获取模块的内部时间,断电重新上电后还是要从网络获取时间同步进去的,不然也是不准的。此外,获取网络时间和日期也可以用GPRS基站定位,从返回的字符串中把时间数据解析出来就行了。这个方法我也测过了,是能用的,但对信号强度要求更高,不然网络没配置好的话也是定位不到进而获取不了数据的。
此外,我又找到了一种方法,NTP应用手册上提到的。
封装成函数就是这样的,亲测可用,函数如下:
u8 Synchro_NTP_Time(void)//同步时间
{
printf("\r\n正在同步网络\r\n");
if(sim900a_send_cmd("AT+SAPBR=3,1,\"CONTYPE\",\"GPRS\"\r\n","OK",100)) //设置网络参数
{
printf("设置网络参数失败\r\n");
return 1;
}
if(sim900a_send_cmd("AT+SAPBR=3,1,\"APN\",\"UNIWAP\"\r\n","OK",500)) //设置APN
{
printf("设置APN失败\r\n");
return 2;
}
if( sim900a_send_cmd("AT+SAPBR=1,1\r\n","OK",200))//激活网络场景
{
printf("激活网络场景失败\r\n");
// sim900a_send_cmd("AT+CGATT=1\r\n","OK",200);
return 3;
}
if(sim900a_send_cmd("AT+SAPBR=2,1\r\n","OK",600)) //获取分配的ip地址
{
printf("获取分配的IP地址失败\r\n");
return 4;
}
if(sim900a_send_cmd("AT+CNTP=\"ntp1.aliyun.com\",32\r\n","OK",200)) //设置NTP服务地址和时区(阿里云的NTP)
{
printf("设置NTP服务地址和时区失败\r\n");
return 5;
}
if(sim900a_send_cmd("AT+CNTP\r\n","CNTP: 1",500)) //开启网络同步
{
printf("开启网络同步失败\r\n");
return 6;
}
printf("\r\n网络同步成功!\r\n");
Get_GSM_RTCtime();
return 0;
}
只要模块注册到了网络,一下子就同步到网络了,GSM模块内部时间也自动对齐网络时间了。模块有信号能注册到网络的话一秒钟就搞定了,还是很快的。调用的时候可以让它循环执行,若是不成功,设置失败次数达到10次就跳出就好了。若是失败的话估计就是在关闭网络场景那一步,其他的没啥问题。下面是我在串口调试助手显示的内容:
AT
OK
AT+CPIN?
READY
GSM模块自检成功
AT+CCLK?
CCLK: "04/01/01,00:00:05+0
获取SIM900A内部时间日期中...
AT+CCLK?
CCLK?
+CCLK: "04/01/01,00:00:05+0
模块返回时间数据:CCLK?
+CCLK: "04/01/01,00:00:05+08"
OK
SMS Ready
AT+CCLK?
+CCLK: "04/01/01,00:00:05+08"
OK
数据转换成功
GSM内部时间:2004年01月01日00时00分05秒
系统初始化完毕!
进入同步网络模式
正在同步网络
AT+SAPBR=3,1,"CONTYPE","GPRS"
OK
AT+SAPBR=3,1,"APN","UNIWAP"
OK
AT+SAPBR=1,1
OK
AT+SAPBR=2,1
OK
AT+CNTP="ntp1.aliyun.com",32
OK
AT+CNTP
CNTP: 1
网络同步成功!
AT+CCLK?
CCLK: "20/02/05,20
获取SIM900A内部时间日期中...
AT+CCLK?
CCLK?
+CCLK: "20/02/05,20
模块返回时间数据:CCLK?
+CCLK: "20/02/05,20:33:05+08"
OK
AT+CCLK?
+CCLK: "20/02/05,20:33:05+08"
OK
数据转换成功
GSM内部时间:2020年02月05日20时33分05秒
可以看到模块刚开机初始化完成时内部时间是2004年01月01日00时00分05秒,同步网络后时间自动更新到当前时间:2020年02月05日20时33分05秒 了。有兴趣的各位不妨试试。
附图: