使用配件:
STM32F103C8T6
DSB8120
DS1302
ESP8266
W25Q64
实现功能:
OLED屏幕显示多级菜单
实现联网功能,获取天气,时间
OLED显示时间,通过联网功能实现更新时间
监测当前室内温度
获取天气
ESP8266-01S
获取天气是使用ESP8266-01S进行联网获取数据。ESP8266-01S使用AT指令进行配置,通过AT指令控制ESP8266_at指令esp8266 可以通过这个文章进行学习,下方我列出此次使用的AT指令。
AT+UART=115200,8,1,0,0 //配置波特率,数据位,停止位,校验位,流控
AT //复位
AT+CWMODE=1 // AP+Station模式
AT+CWJAP="WIFI_Name,"WIFI_Password" //连接wifi
AT+CIPSNTPCFG=1,8 //设置时间地域
AT+CIPSTART="TCP","api.seniverse.com",80 //连接心知服务器
AT+CIPMODE=1 //使能esp8266透传模式
AT+CIPSEND //开启数据传输
GET https://api.seniverse.com/v3/weather/now.json?key=S9eaZ9XPwCwgr8NVc&location=ip&language=zh-Hans&unit=c //获取天气信息(私钥,城市,语言)
其中的心知服务器是国内比较知名的天气服务器,首次使用需进入下方网址进行注册,开通免费版即可。网址为:心知天气
上方的GET 指令为GET https://api.seniverse.com/v3/weather/now.json?key=S9eaZ9XPwCwgr8NVc&location=ip&language=en&unit=c
需要进行自己的客制化,key在免费版中可以查看到,location可以输入自己城市的拼音或直接输入ip(使用电脑调试若开了VPN会导致跳到别的城市)而language可以调整回传的数据的格式是中文或英文,详情可见心知天气参考文档心知天气数据目录 HyperData · 心知科技
首次调试可以使用TTL配置串口调试工具进行调试。接线分别为
TTL ESP
vcc -------------3.3
gnd -------------gnd
tx ------------- rx
rx ------------- tx
TTL在接线时请注意拔掉跳线帽,确保供电部分连接的是VCC而不是3v3,否则给ESP发送指令时可能会回传busyp...。
下方是api返回的数据,在EDGE浏览器中出现了大量的空格,而在其他的浏览器中却没有,因此为了节省空间,可以在IRQHandler中忽略空格和冒号。
{
"results":
[{
"location":{
"id":"WS0E9D8WN298",
"name":"广州",
"country":"CN",
"path":"广州,广州,广东,中国",
"timezone":"Asia/Shanghai",
"timezone_offset":"+08:00"},
"now":{
"text":"晴",
"code":"0",
"temperature":"9"
},
"last_update":"2024-03-02T11:52:26+08:00"
}]
}
上方是错误的说法,也算是一个坑。真正的json字符串是:
{"results":[{"location":{"id":"WS0E9D8WN298","name":"广州","country":"CN","path":"广州,广州,广东,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"中雨","code":"14","temperature":"13"},"last_update":"2024-03-09T23:35:16+08:00"}]}
而api返回的是json格式,但是在回传给单片机的时候已经变成了String格式,所以需要用字符串的方式来进行解析。
我另外踩到的一个坑是发送AT指令时返回的数据都是比如 OK\r\n 会带有换行符的,而返回的json数据却没有\r\n,所以IRQHandle在检测时不能检测\r\n,需要以时间来检测一帧数据的完成与否。
//IRQ
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
RxData[RxDataCnt++] = USART1->DR;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
char USART1_WaitRecive(void) //不定长串口接受接口
{
Recive_Flag = 0;
if(RxDataCnt == 0){ //如果接收计数为0 则说明没有处于接收数据中,所以直接跳出,结束函数
Recive_Flag = 0;
return Recive_Flag;
}
if(RxDataCnt == RxDataCntPre) //如果上一次的值和这次相同,则说明接收完毕
{
RxDataCnt = 0; //清0接收计数
Recive_Flag = 1;
return Recive_Flag; //返回接收完成标志
}
RxDataCntPre = RxDataCnt; //置为相同
return Recive_Flag; //返回接收未完成标志
}
可以在while里循环调用USART1_WaitRecive进行判断,外加一个延时函数,如延时5ms,意思就是如果延时超过5m数据,则判定为第二帧数据。
在接收数据后可以使用第三方库cJSON来进行解析,可以在GitHub上进行下载相关文件。
json相关知识可以查看JSON 基础结构,在返回的json中先包含着数组结构的results,然后包含着对象结构的Location和Now,最后才是最里层的键值对。
void ESP_parse(char *RxData_String){
cJSON *json,*arrayItem,*object,*subobject,*name,*text,*temperature;
json = cJSON_Parse(RxData_String); //解析JSON数据包
if(json == NULL) //检测JSON数据包是否存在语法上的错误,返回NULL表示数据包无效
{
//数据包语法错误
Serial_SendString(USART1,"ERROR");
}
else
{
arrayItem = cJSON_GetObjectItem(json,"results");
object = cJSON_GetArrayItem(arrayItem,0);
subobject = cJSON_GetObjectItem(object,"location");
name = cJSON_GetObjectItem(subobject,"name");
subobject = cJSON_GetObjectItem(object,"now");
text = cJSON_GetObjectItem(subobject,"text");
temperature = cJSON_GetObjectItem(subobject,"temperature");
text_value = text->valuestring,
temperature_value = temperature->valuestring;
name_value = name->valuestring;
Weather(0,0,text_value);
OLED_ShowString(0,16,temperature_value,OLED_8X16);
city(0,32,name_value);
OLED_Update();
}
cJSON_Delete(json); //释放cJSON_Parse()分配出来的内存空间
}
在接收json指令的时候其实是可以接收中文城市和气候的,但是返回的是UTF-8的编码,所以会显示?,如果不打算存入字库的话,也可以把项目改成UTF-8的编码,也不需要再去解析天气和气候了。
项目在这里卡住了很久,因为在串口调试中显示通过ESP得到的天气数据没有问题,而解析函数也确定没有问题,但是解析出来的结果却是空的,通过查询,发现是内存空间分配较小,使用C8T6的需要Heap_Size,需要改大一些。参考这个关于STM32+cJSON所遇到的一些问题_cjson_parse返回null
DS1302
DS1302是一个实时时钟芯片,可以提供秒、分、时、日、月、年等信息,可以决定是12小时制或是24小时制。
秒:读命令地址:0x81,写命令0x80,范围 00-59;
分:读命令地址:0x83,写命令0x82,范围 00-59;(BIT4-BIT7的值)*10+(BIT0-BIT3的值)=分钟
时:读命令地址:0x85,写命令0x84,范围 1-12/0-23;
日:读命令地址:0x87,写命令0x80,范围 1-31;
月:读命令地址:0x89,写命令0x88,范围 1-12;
周:读命令地址:0x8B,写命令0x8A,范围 1-7;
年:读命令地址:0x8D,写命令0x8H,范围 00-99;
void ds1302_init()
{
ds1302_wirte_rig(0x8e,0x00);//关闭写保护
ds1302_wirte_rig(0x80,0x55);//seconds37秒
ds1302_wirte_rig(0x82,0x59);//minutes58分
ds1302_wirte_rig(0x84,0x23);//hours23时
ds1302_wirte_rig(0x86,0x30);//date30日
ds1302_wirte_rig(0x88,0x09);//months9月
ds1302_wirte_rig(0x8a,0x07);//days星期日
ds1302_wirte_rig(0x8c,0x24);//year2024年
//ds1302_wirte_rig(0x80,0x19);
ds1302_wirte_rig(0x8e,0x80);//关闭写保护
}
void ds1302_read_time(void)
{
read_time[0]=ds1302_read_rig(0x81);//读秒
read_time[1]=ds1302_read_rig(0x83);//读分
read_time[2]=ds1302_read_rig(0x85);//读时
read_time[3]=ds1302_read_rig(0x87);//读日
read_time[4]=ds1302_read_rig(0x89);//读月
read_time[5]=ds1302_read_rig(0x8B);//读星期
read_time[6]=ds1302_read_rig(0x8D);//读年
}
DS1302中的数据都是以BCD码存储的,因此读出来或写入的数据都应该是BCD码。所以需要转换成十进制。
ds1302_read_time(); //BCD码转换为10进制
TimeData.second=(read_time[0]>>4)*10+(read_time[0]&0x0f);
TimeData.minute=((read_time[1]>>4)&(0x07))*10+(read_time[1]&0x0f);
TimeData.hour=(read_time[2]>>4)*10+(read_time[2]&0x0f);
TimeData.day=(read_time[3]>>4)*10+(read_time[3]&0x0f);
TimeData.month=(read_time[4]>>4)*10+(read_time[4]&0x0f);
TimeData.week=read_time[5];
TimeData.year=(read_time[6]>>4)*10+(read_time[6]&0x0f)+2000;