STM32+ESP8266+TFTLCD实现天气预报显示
- 前言
- 项目思路
- 效果演示
- 心知天气API
- 硬件部分
- 材料
- 硬件连接
- 软件部分
- ESP8266AT固件指令
- 页面布局
- JSON数据解析
- 时间数据解析与计时
- 遇到的问题
前言
最近发现自己学的东西都太杂了,真正开始找工作,才意识到自己应该精通某样技能。认真开始使用STM32做小项目后才发现自己有好多东西不知道,感觉自己是真的菜。虽然这也不是新的技术,但是真正要做到精通,其中道理要研究的东西还是蛮多的。并且好久没有更新文章了,最近在知乎上看到别人使用STM32做了一个实时疫情的显示屏,想着自己也动手做一个显示天气预报的装置。工程源码见文末。
项目思路
STM32与ESP8266通过串口连接,STM32通过串口向ESP8266发送指令:连接AP,创建TCP连接,创建SSL连接,发送GET请求获取天气数据,STM32解析JSON数据,将天气数据显示在TFTLCD屏幕上。屏幕可显示最近三天的天气情况和显示实时24小时天气(最多显示至第12小时),使用按键来切换,显示不同的天气页面。
效果演示
系统启动默认显示当前时间之后三个小时的天气信息
按下切换按钮显示下三个小时的天气信息
按下切换按钮显示逐日天气信息
心知天气API
我使用心知天气获取天气信息,由于STM32使用cJSON需要消耗大量内存,如果获取的数据量过大会导致JSON数据解析失败的结果,而这个API可以根据自己的要求获取一定数量的数据,不仅可以获取24小时逐小时天气数据,还可以获取逐日天气数据。
逐日天气预报参数表
24小时逐小时天气预报
硬件部分
材料
STM32F103ZET6开发板,ESP8266-01,2.8寸TFTLCD屏幕。
硬件连接
ESP8266-RX->PB10
ESP8266-TX->PB11
ESP8266-CH_PD->3V3
ESP8266-3V3->3V3
ESP8266-GND->GND
软件部分
ESP8266AT固件指令
- 连接ap指令:AT+CWJAP=“SSID”,“password”
- 创建TCP连接:AT+CIPSTART=“TCP”,“www.domain.com”,80
- 创建SSL连接:AT+CIPSTART=“SSL”,“www.domain.com”,443
- 设置为透传模式:AT+CIPMODE=1
- 发送数据:AT+CIPSEND
- 直接向串口发送GET请求
- 退出透传模式:
+++
AT+CIPMODE=0 - 断开TCP连接:AT+CIPCLOSE
- 断开SSL连接:AT+CIPCLOSE
页面布局
/*
显示实时天气
*/
void showRTweather(){
char path[15];
u8 i,x0=20,x1=50;
LCD_Fill(0,22,320,240,WHITE);
for(i=0;i<3;i++){
LCD_ShowString(20+i*110,22,64,16,16,(u8*)weather.time[i]);
sprintf(path,"0:/ICONS/%s.bmp",weather.code[i]);
ai_load_picfile((const unsigned char*)path,x0+i*110,40,60,60,1);
Show_Str(10+i*110,130,32,16,"天气",16,0);
LCD_ShowChar(42+i*110,130,':',16,0);
Show_Str(x1+i*110,130,32,16,(u8*)WeatherCode[u8Tou16(weather.code[i])],16,0);
Show_Str(10+i*110,150,32,16,"温度",16,0);
LCD_ShowChar(42+i*110,150,':',16,0);
LCD_ShowString(x1+i*110,150,32,16,16,(u8*)weather.temperature[i]);
Show_Str(10+i*110,170,32,16,"湿度",16,0);
LCD_ShowChar(42+i*110,170,':',16,0);
LCD_ShowString(x1+i*110,170,32,16,16,(u8*)weather.humidity[i]);
}
}
/*
显示逐日天气
*/
void showRDweather(){
char path[15];
u8 i,x0=20,x1=50;
LCD_Fill(0,22,320,240,WHITE);
for(i=0;i<3;i++){
Show_Str(30+i*110,22,32,16,(u8*)date[i],16,0);
sprintf(path,"0:/ICONS/%s.bmp",RDweather.code_day[i]);
ai_load_picfile((const unsigned char*)path,x0+i*110,40,60,60,1);
Show_Str(10+i*110,130,32,16,"白天",16,0);
LCD_ShowChar(42+i*110,130,':',16,0);
Show_Str(x1+i*110,130,32,16,(u8*)WeatherCode[u8Tou16(RDweather.code_day[i])],16,0);
Show_Str(10+i*110,150,32,16,"夜晚",16,0);
LCD_ShowChar(42+i*110,150,':',16,0);
Show_Str(x1+i*110,150,32,16,(u8*)WeatherCode[u8Tou16(RDweather.code_night[i])],16,0);
Show_Str(10+i*110,170,32,16,"最高",16,0);
LCD_ShowChar(42+i*110,170,':',16,0);
LCD_ShowString(x1+i*110,170,32,16,16,(u8*)RDweather.high[i]);
Show_Str(10+i*110,190,32,16,"最低",16,0);
LCD_ShowChar(42+i*110,190,':',16,0);
LCD_ShowString(x1+i*110,190,32,16,16,(u8*)RDweather.low[i]);
Show_Str(10+i*110,210,32,16,"湿度",16,0);
LCD_ShowChar(42+i*110,210,':',16,0);
LCD_ShowString(x1+i*110,210,32,16,16,(u8*)RDweather.humidity[i]);
}
}
/*
刷新当前页面
*/
void refresh(){
if(thispage==-1){
getRDWeather();
showRDweather();
}else{
getRTWeather(thispage);
showRTweather();
}
}
/*
thispage=-1,0,3,6,9
*/
void switchWeather(){
u8 key=0;
key=KEY_Scan(0); //扫描按键按下
if(key==1){
if(thispage<0){ //-1代表逐日天气页面,按下KEY1跳转0页面
getRTWeather(0);
showRTweather();
thispage=0;
}else{ //跳转至-1页面
getRDWeather(); //获取逐日时间
showRDweather(); //显示逐日时间
thispage=-1;
}
}
if(key==2){
if(thispage==-1){
getRTWeather(0);
showRTweather();
thispage=0;
}else{
if(thispage==9){ //最多显示到第9+3小时的天气
thispage=0;
getRTWeather(thispage);
showRTweather();
}else{ //每个页面显示三个小时的天气
thispage=thispage+3;
getRTWeather(thispage);
showRTweather();
}
}
}
}
/*
日期时间显示
*/
void gui_load(){
LCD_DrawLine(0,20,320,20); //
LCD_ShowNum(0,0,Localtime.year,4,16);
LCD_ShowStr(32,0,3); //年
LCD_ShowNum(48,0,Localtime.month,2,16);
LCD_ShowStr(64,0,2); //月
LCD_ShowNum(80,0,Localtime.day,2,16);
LCD_ShowStr(96,0,4); //日
LCD_ShowNum(130,0,Localtime.hour,2,16);
LCD_ShowStr(146,0,5); //时
LCD_ShowNum(162,0,Localtime.min,2,16);
LCD_ShowStr(178,0,6); //分
LCD_ShowNum(194,0,Localtime.sec,2,16);
LCD_ShowStr(210,0,7); //秒
}
JSON数据解析
时间JSON数据格式:
{
"code": 200,
"msg": "success",
"newslist": [
{
"country": "中国",
"city": "上海",
"timeZone": "GMT +8",
"strtime": "2020-09-26 15:35:07",
"timestamp": 1601105707,
"weeknum": "6"
}
]
}
天气JSON数据格式:
{
"results": [{
"location": {
"id": "WTW3SJ5ZBJUY",
"name": "上海",
"country": "CN",
"path": "上海,上海,中国",
"timezone": "Asia/Shanghai",
"timezone_offset": "+08:00"
},
"hourly": [{
"time": "2020-09-30T17:00:00+08:00",
"text": "晴",
"code": "0",
"temperature": "25",
"humidity": "55",
"wind_direction": "北",
"wind_speed": "19.80"
}, {
"time": "2020-09-30T18:00:00+08:00",
"text": "晴",
"code": "1",
"temperature": "23",
"humidity": "57",
"wind_direction": "北",
"wind_speed": "16.56"
}]
}]
}
解析数据并储存在结构体中
u8 getRDWeather(){
cJSON *root,*results,*result_arr,*result0,*results_arr,*item;
u8 error=0;
int i=0;
ConSSL();
delay_ms(500);
SetPassThrough();
memset(USART_RX_BUF3,0,strlen((const char*)USART_RX_BUF3));
USART_RX_STA3 = 0;
Usart_SendString(USART3,"GET https://api.seniverse.com/v3/weather/daily.json?key=SRpZAIwb07j1twRHA&location=shanghai&language=zh-Hans&unit=c&start=0&days=3\r\n\r\n"); //获取当地天气
delay_ms(1000);
root=cJSON_Parse((char*)USART_RX_BUF3); //解析收到的JSON数据
RstPassThrough(); //关闭透传
if(root!=0){
results=cJSON_GetObjectItem(root,"results"); //获取JSON对象的results属性
results_arr=cJSON_GetArrayItem(results,0);
result_arr=cJSON_GetObjectItem(results_arr,"daily");
if(result_arr->type==cJSON_Array){
for(i=0;i<3;i++){
result0=cJSON_GetArrayItem(result_arr,i);
item=cJSON_GetObjectItem(result0,"code_day");
memcpy(RDweather.code_day[i],item->valuestring,strlen(item->valuestring)); //属性数据复制到结构体中
item=cJSON_GetObjectItem(result0,"code_night");
memcpy(RDweather.code_night[i],item->valuestring,strlen(item->valuestring));
printf("temperature:%s",weather.temperature[i]);
item=cJSON_GetObjectItem(result0,"high");
memcpy(RDweather.high[i],item->valuestring,strlen(item->valuestring));
item=cJSON_GetObjectItem(result0,"low");
memcpy(RDweather.low[i],item->valuestring,strlen(item->valuestring));
item=cJSON_GetObjectItem(result0,"humidity");
memcpy(RDweather.humidity[i],item->valuestring,strlen(item->valuestring));
printf("humidity:%s",weather.humidity[i]);
item=cJSON_GetObjectItem(result0,"wind_speed");
printf("windspeed:%s\r\n",item->valuestring); //
memcpy(RDweather.wind_speed[i],item->valuestring,strlen(item->valuestring));
printf("wind_speed:%s\r\n",weather.wind_speed[i]); //wind_speed:傀?
}
}
}else{
error=1;
printf("Error before: [%s]\n",cJSON_GetErrorPtr());
}
cJSON_Delete(root); //一定要释放JSON对象
USART_RX_STA3 = 0;
Usart_SendString(USART3,"AT+CIPCLOSE\r\n"); //关闭连接
return error;
}
时间数据解析与计时
/*
将u8字符串数字转换成u16类型数字
*/
u16 u8Tou16(char *str){
u8 i;
u16 num=0;
u8 len=strlen(str);
for(i=len;i>0;i--){
num+=(*str-'0')*pow(10,i-1); //pow次方函数
str++;
}
return num;
}
/*
转换成字符串时间
*/
void Parse_Time(char *str){
u8 i=0,j=0,k=0;
char time[6][5]={"","","","","",""};
//time=malloc();
u8 len=strlen(str);
for(i=0;i<len+1;i++){ //拆分字符串
if(*str=='-'||*str==0x20||*str==':'||*str=='\0'){
time[j][k]='\0';
j++;
str++;
k=0;
}
time[j][k]=*str;
k++;
str++;
}
Localtime.year=u8Tou16(time[0]);
Localtime.month=u8Tou16(time[1]);
Localtime.day=u8Tou16(time[2]);
Localtime.hour=u8Tou16(time[3]);
Localtime.min=u8Tou16(time[4]);
Localtime.sec=u8Tou16(time[5]);
}
/*
获取串口数据转换可用时间常数
0:获取成功
1:接口请求错误
2:非JSON格式
*/
u8 getTime(){
cJSON *root,*result_arr,*result;
u8 code=0,error=0;
const char *TimeStr;
char *time = (char *)malloc(sizeof(char) * 20);
ConTCP();
delay_ms(100);
SetPassThrough(); //透传模式
memset(USART_RX_BUF3,0,strlen((const char*)USART_RX_BUF3));
USART_RX_STA3 = 0;
Usart_SendString(USART3,"GET http://api.tianapi.com/txapi/worldtime/index?key=652e7ab06ae02d789f04e2b08dbfd5f6&city=上海\r\n\r\n"); //发送GET请求获取当前时间
delay_ms(1000);
if(USART_RX_BUF3[0]=='{'){
root=cJSON_Parse((const char*)USART_RX_BUF3); //"{\"code\": 200,\"newslist\": [{\"strtime\": \"2020-09-26 19:02:16\",\"weeknum\": \"6\"}]}"
RstPassThrough();
if(root!=0){
code=cJSON_GetObjectItem(root,"code")->valueint;
if(code==200){
result_arr=cJSON_GetObjectItem(root,"newslist");
if(result_arr->type==cJSON_Array){
result=cJSON_GetObjectItem(result_arr,0);
TimeStr=(const char *)cJSON_GetObjectItem(result,"strtime")->valuestring;
Localtime.weeknum=(u8 *)cJSON_GetObjectItem(result,"weeknum")->valuestring;
}
}else{ error=1;}
}else{
error=3;
printf("JSON format error:%s\r\n", cJSON_GetErrorPtr()); //堆尺寸太小,导致转换失败
}
strcpy(time,TimeStr);
cJSON_Delete(root);
Parse_Time(time);
TIM_Cmd(TIM2,ENABLE); //获取到时间后,启动TIM2
}else{ error=2;}
USART_RX_STA3 = 0;
Usart_SendString(USART3,"AT+CIPCLOSEMODE=0\r\n"); //关闭连接
delay_ms(500);
Usart_SendString(USART3,"AT+CIPCLOSE=0\r\n");
return error;
}
void TimeRun(){
if(Localtime.sec<60){
Localtime.sec++;
}
if(Localtime.sec==60){ //分进位
Localtime.sec=0;
if(Localtime.min<60){
Localtime.min++;
}
}
if(Localtime.min==60){ //时进位
Localtime.min=0;
if(Localtime.hour<24){
Localtime.hour++;
}
}
if(Localtime.hour==24){
Localtime.hour=0;
}
}
遇到的问题
1、关于字符串操作内存溢出问题
一开始对指针的理解不深,调试时总是造成内存溢出的情况,还以为是分配的栈空间太小,最后查资料慢慢找到问题所在。字符串复制函数:memcpy函数,strcpy函数,两者都可以实现字符串的复制,由于C语言中字符串是以字符数组的形式储存,strcpy(time,TimeStr);不需要定义字符串长度,字符串长度函数中可以根据“\0”位置计算出来,如果time是一个没有定义的字符串指针,容易导致内存冲突,因为没有给time指向的数据分配空间。正确使用应该先给变量分配一定空间,要么使用动态内存分配函数分配一定长度的内存,要么定义一定长度的数组,如“char time[20];”或者“char *time = (char *)malloc(sizeof(char) * 20);”,在调用复制字符串函数就不会内存冲突。
2、sprintf函数内存溢出问题
使用sprintf函数也要注意先申请足够空间的内存来储存拼接后的字符串,因为sprintf函数不会考虑申请储存空间是否充足,不断地向后占用空间,如果超出了申请的内存空间就会导致内存冲突。
3、cJSON 解析时打印的串口JSON数据明明是对的,但是解析之后却是空的
因为是引用cJSON需要根据json数据动态申请大量的内存空间,动态申请的内存空间都是储存在堆区,如果分配的堆区尺寸过小就会导致转换结果为空。
4、使用指针总结
指针的本质就是地址,而字符串指针本质是字符串首字符地址,字符串指针和字符串数组差不多,也就是说,定义的字符串数组地址也是数组第一位的地址。至于像print函数传入一个字符串指针,就可以打印出整个字符串而不是打印出地址,因为字符串一般以’\0’结束,所以函数中会首先计算出字符串长度,根据长度一个字符一个字符的打印出来。
5、中文解码问题
LCD中文显示的字库采用的是GBK编码,GBK是兼容GB2312的,有时如果解码有问题就会导致屏幕上显示出与自己定义的文字不一致。发送HTTP请求其中采用的TTF-8编码,如果使用GB2312的编码发送GET请求就会请求不到数据。