物联网小项目——墨水屏时钟(STM32+ESP8266实现)

2023-12-31 17:50

本文主要是介绍物联网小项目——墨水屏时钟(STM32+ESP8266实现),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

无意间在网上看到开源的使用墨水屏打造的桌面时钟,当个桌面小摆件可谓是十分优雅,于是就萌生出了自己DIY一个的想法。这个墨水屏时钟具有以下特点

  • 时间日期的显示和自动校准
  • 自动获取实时天气
  • 半夜自动进入休眠
  • 支持微信智能配网

目前已经实现软件功能,但是硬件上没有画板做成一体化的,只是开发板和模块之间使用杜邦线连接的试验版本。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

硬件设计

硬件由STM32主控、ESP8266模块、墨水屏驱动电路和墨水屏主体组成。STM32通过串口给ESP8266发送AT指令控制其连接WIFI和获取信息等操作;通过SPI控制墨水屏。
为了节约成本墨水屏使用的是电子价签上拆下来的2.13寸汉朔墨水屏,驱动电路可以参照微雪电子官方的提供的驱动电路,单片机通过SPI与其通信即可。
墨水屏驱动电路

墨水屏驱动

驱动程序可以参考微雪电子官网的,只需把STM32版本的demo下载下来,找到RTS,CS,DC,BUSY,SCL,SDA,USART宏定义,并改成自己实际接的GPIO,最后将main函数里的EPD_2in13_test()注释打开,编译烧录就能看到墨水屏显示图片了。
能刷新墨水屏就说明硬件没有问题了,接下来就要精简出项目需要的墨水屏驱动代码即可。我用到了gpio,spi,DEV_Config,存储图片数据的imageData,存储字库数据的Font,负责控制的EPD_2in13,画图接口的GUI_Paint。

获取网络时间

墨水屏时钟的时钟来源是STM32内部的RTC时钟,因为晶振的差异,导致RTC时钟走时并不十分准确,所以使用网络校准每隔一个小时获取一次网络时间写入RTC寄存器中。如何配置RTC实时时钟和ESP8266连接WIFI可以参考我的另外两篇博客
STM32F1系列HAL库开发——RTC实时时钟
STM32使用ESP8266模块AT指令连接心知天气API获取天气信息
获取网络时间可以使用ESP8266的AT+CIPSNTPCFG设置时域和 SNTP 服务器AT+CIPSNTPTIME—查询 SNTP 时间

//设置SNTP服务器时域,参数1:使能,参数2:设置时域
AT+CIPSNTPCFG=1,8\r\n
//获取SNTP服务器时间
AT+CIPSNTPTIME?\r\n
//获取到的时间格式如下
+CIPSNTPTIME:Thu	Aug	04	14:48:05	2016
//解析获取到的数据
char result[16];
char *p = strstr((const char*)USART2_RX_BUF,"+CIPSNTPTIME:");
p += 17;		//跳到月份的字符串//设置月份for(i=0; (*p != ' ') && (*p != '\t'); i++){result[i] = *p++;}if(strcmp(result,"Jan") == 0)RTC_DataStruct.Month = RTC_MONTH_JANUARY;else if(strcmp(result,"Feb") == 0)RTC_DataStruct.Month = RTC_MONTH_FEBRUARY;else if(strcmp(result,"Mar") == 0)RTC_DataStruct.Month = RTC_MONTH_MARCH;else if(strcmp(result,"Apr") == 0)RTC_DataStruct.Month = RTC_MONTH_APRIL;else if(strcmp(result,"May") == 0)RTC_DataStruct.Month = RTC_MONTH_MAY;else if(strcmp(result,"Jun") == 0)RTC_DataStruct.Month = RTC_MONTH_JUNE;else if(strcmp(result,"Jul") == 0)RTC_DataStruct.Month = RTC_MONTH_JULY;else if(strcmp(result,"Aug") == 0)RTC_DataStruct.Month = RTC_MONTH_AUGUST;else if(strcmp(result,"Sept") == 0)RTC_DataStruct.Month = RTC_MONTH_SEPTEMBER;else if(strcmp(result,"Oct") == 0)RTC_DataStruct.Month = RTC_MONTH_OCTOBER;else if(strcmp(result,"Nov") == 0)RTC_DataStruct.Month = RTC_MONTH_NOVEMBER;else if(strcmp(result,"Dec") == 0)RTC_DataStruct.Month = RTC_MONTH_DECEMBER;p++;	//跳过空格//设置日for(i=0; *p != ' ' && *p != '\t'; i++){result[i] = *p++;}RTC_DataStruct.Date = (result[0]-'0')*16 + (result[1]-'0');p++;	//跳过空格//设置小时for(i=0; *p != ':'; i++){result[i] = *p++;}RTC_TimeStruct.Hours = (result[0]-'0')*10 + (result[1]-'0');p++;	//跳过://设置分钟for(i=0; *p != ':'; i++){result[i] = *p++;}RTC_TimeStruct.Minutes = (result[0]-'0')*10 + (result[1]-'0');p++;	//跳过://设置秒for(i=0; *p != ' ' && *p != '\t'; i++){result[i] = *p++;}RTC_TimeStruct.Seconds = (result[0]-'0')*10 + (result[1]-'0'); p++;	//跳过空格//设置年份for(i=0; i<4; i++){result[i] = *p++;}RTC_DataStruct.Year = (result[2]-'0')*16 + (result[3]-'0');RTC_DataStruct.WeekDay = RTC_WEEKDAY_TUESDAY;HAL_RTC_SetTime(&hrtc,&RTC_TimeStruct,RTC_FORMAT_BIN);	//设置时间HAL_RTC_SetDate(&hrtc,&RTC_DataStruct,RTC_FORMAT_BCD);	//设置日期

上电后获取完了网络时间并设置到RTC时钟后就设置RTC的闹钟时间为当前时间+1分钟,让STM32进入停止模式来降低功耗,设置的RTC闹钟中断就会让STM32从低功耗中唤醒,在闹钟中断里我们可以执行墨水屏刷新时间的操作后再重新设置闹钟时间和进入低功耗。这样就可以实现每隔一分钟墨水屏刷新时间。当走过一个小时后就再获取一次网络时间和天气信息。

//设置下一次闹钟时间if(RTC_TimeStruct.Minutes == 59){if(RTC_TimeStruct.Hours == 23)RTC_SetAlarm(0,0,0);elseRTC_SetAlarm(RTC_TimeStruct.Hours+1,0,0);			}else{RTC_SetAlarm(RTC_TimeStruct.Hours,RTC_TimeStruct.Minutes+1,0);}while(1){if(Alarm_Flag == 1){Alarm_Flag = 0;	//清除闹钟标志位RTC_Alarm();	//闹钟事件处理if(Updata_Flag == EPD_PART)	//局部刷新{Updata_Flag = 0;	//清除刷新标志位GUI_Display_Part();}else if(Updata_Flag == EPD_FULL)	//全局刷新{Updata_Flag = 0;	//清除刷新标志位GUI_Display_All();}else if(Updata_Flag == EPD_SLEEP)	//休眠页面{Updata_Flag = 0;GUI_Display_Sleep();}printf("%d-%02d-%02d %d\r\n",RTC_DataStruct.Year+2000,RTC_DataStruct.Month,RTC_DataStruct.Date,RTC_DataStruct.WeekDay);printf("%02d:%02d:%02d\r\n",RTC_TimeStruct.Hours,RTC_TimeStruct.Minutes,RTC_TimeStruct.Seconds);//设置下一次闹钟时间if(RTC_TimeStruct.Hours>=1 && RTC_TimeStruct.Hours<=6)RTC_SetAlarm(6,59,55);elseRTC_SetAlarm(RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds+55);Sys_Enter_Stop();	//系统进入停止模式}}//RTC闹钟事件处理
void RTC_Alarm(void)
{printf("WK_UP\r\n");do{HAL_RTC_GetTime(&hrtc,&RTC_TimeStruct,RTC_FORMAT_BIN);	//获取RTC时间HAL_Delay(250);}while(Paint_time.Min == RTC_TimeStruct.Minutes);	//等待分钟更新if(Paint_time.Hour != RTC_TimeStruct.Hours)	//每小时全局刷新一次{//凌晨时间,墨水屏休眠if(RTC_TimeStruct.Hours>=1 && RTC_TimeStruct.Hours<=6){Updata_Flag = EPD_SLEEP;}else{Updata_Flag = EPD_FULL;		//全局刷新标志位Get_Net_Time();				//每小时获取网络时间		Get_Weather();				//每小时更新天气信息}}else{Updata_Flag = EPD_PART;		//局部刷新标志位}
}//系统进入停止模式
void Sys_Enter_Stop(void)
{printf("go to sleep\r\n");__HAL_RCC_PWR_CLK_ENABLE();			//使能PWR时钟__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);	//清除唤醒标志HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON,PWR_STOPENTRY_WFI);		//进入待机模式
}//RTC闹钟中断服务函数
void RTC_Alarm_IRQHandler(void)
{HAL_RTC_AlarmIRQHandler(&hrtc);
}//RTC闹钟唤醒中断回调函数
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *RTC_Handle)
{SystemClock_Config();	//唤醒后要初始化时钟配置Alarm_Flag = 1;			//标志位置一__HAL_RTC_ALARM_EXTI_CLEAR_FLAG();
}

获取实时天气

利用ESP8266TCP连接来天气网站API获取天气信息,怎么连接天气API还是参考上面提到的另一篇博客。这里还是介绍怎么解析数据,并把解析到的天气代码,温度湿度放到一个结构体中管理,方便后续读出数据进行显示。

//存储天气信息的结构体
typedef struct{uint8_t icon;char Tempera[4];char Humidity[4];
} PAIN_WEATHER;//建立TCP连接,并开启透传,进行HTTP请求
uint8_t TCP_Connect(char *IP,char *URL)
{	uint8_t res = 0;USART2_RX_STA = 0;u2_printf("AT+CIPSTART=\"TCP\",\"%s\",80\r\n",IP);HAL_Delay(500);if(Send_Command("AT+CIPSTATUS","TCP",20))	//检查TCP是否连接{res = 1;Send_Command("AT+CIPMODE=1","OK",20);	//开启透传模式Send_Command("AT+CIPSEND",">",20);		//开始传输HAL_Delay(200);USART2_RX_STA = 0;u2_printf("GET %s\r\n",URL);HAL_Delay(200);strcpy(Rcv_Str,(const char*)USART2_RX_BUF);}return res;
}//关闭透传并断开TCP连接
void TCP_Disconnect(void)
{//退出发送模式while((USART2->SR&0X40)==0);	//等待发送空USART2->DR='+';      HAL_Delay(15);					//大于串口组帧时间(10ms)while((USART2->SR&0X40)==0);	//等待发送空USART2->DR='+';      HAL_Delay(15);					//大于串口组帧时间(10ms)while((USART2->SR&0X40)==0);	//等待发送空USART2->DR='+';      HAL_Delay(1000);					//等待1s//while(!Send_Command("AT","OK",20));//退出透传判断.//关闭透传模式Send_Command("AT+CIPMODE=0","OK",20);//断开TCP连接Send_Command("AT+CIPCLOSE","OK",20);
}//解析GET请求返回的数据
void Weather_DataParsing(char *reqRes, char *keywords, char *keyval)
{char *p1 = NULL;char *p2 = NULL;if(strstr(reqRes,"Sucess") != NULL){p1 = strstr(reqRes, keywords);  //查找关键词if(p1){p1 += strlen(keywords) + 3;p2 = strstr(p1, "\"");      //查找末端的 " strncpy(keyval, p1, p2 - p1); //拷贝数据}}
}void Get_Weather(void)
{char result[16] = "NULL";//连接天气API获取天气信息if(!TCP_Connect("api.yytianqi.com","http://api.yytianqi.com/observe?city=CH281101&key=72r8t4knnk32g8s9\r\n")){Paint_weather.Tempera[0] = '?';Paint_weather.Tempera[1] = '?';Paint_weather.Tempera[2] = 'C';Paint_weather.Humidity[0] = '?';Paint_weather.Humidity[1] = '?';Paint_weather.Humidity[2] = '%';}//获取气温Weather_DataParsing(Rcv_Str,"qw",result);if(result[1] == 'U')	//气温低于10度处理{Paint_weather.Tempera[0] = result[0];Paint_weather.Tempera[1] = 'C';Paint_weather.Tempera[2] = '\0';}else{Paint_weather.Tempera[0] = result[0];Paint_weather.Tempera[1] = result[1];Paint_weather.Tempera[2] = 'C';}//获取湿度Weather_DataParsing(Rcv_Str,"sd",result);Paint_weather.Humidity[0] = result[0];Paint_weather.Humidity[1] = result[1];Paint_weather.Humidity[2] = '%';//获取天气代码Weather_DataParsing(Rcv_Str,"numtq",result);Paint_weather.icon = (result[0]-'0')*10 + (result[1]-'0');printf("Rcv:%s\r\n",Rcv_Str);printf("%sC\t%s\r\n",Paint_weather.Tempera,Paint_weather.Humidity);HAL_Delay(1000);TCP_Disconnect();	//断开TCP连接
}

智能配网

ESP8266的智能配网指由外部设备(如手机)向 外部广播含有 SSID 和密码(PSW)的WiFi信息的报文,ESP8266获取到该报文就可以连接到WiFi了。使用AT+CWSTARTSMART开启 SmartConfig,使用AT+CWSTOPSMART停止 SmartConfig 。需要注意的是,在开启智能配网时不要执行其他的AT指令,无论配置成功与否都要及时停止智能配网。而且在实际使用中发现,如果上一次未配置成功即使重新上电也会影响下一次智能配网的开启,所以每次使用完都要调用停止智能配网。
具体操作流程是,系统上电后检测是否连接WiFi,未连接则显示智能配网的二维码,同时开启智能配网,监听报文。当用户输入WiFi的信息后,并且接收到报文就会连接WiFi,连接成功就会返回smartconfig connected wifi等一系列数据,就是通过判断是否接收到smartconfig connected wifi的应答来判断是否成功连接。
在这里插入图片描述

//检查WIFI是否连接
uint8_t Check_WIFI(void)
{if(Send_Command("AT+CWJAP_DEF?","No AP",20)){printf("WIFI未连接\r\n");return 0;}else{printf("WIFI已连接\r\n");return 1;}
}uint8_t WIFI_SmartConfig(uint32_t waittime)
{uint8_t res = 0;//开启station模式if(!Send_Command("AT+CWMODE?","1",20)){Send_Command("AT+CWMODE=1","OK",20);}//退出smartconfig,防止上一次未退出Send_Command("AT+CWSTOPSMART", "OK", 50);//开启smartconfigif(Send_Command("AT+CWSTARTSMART", "OK", 50)){//printf("%s\r\n",USART2_RX_BUF);while(waittime--){HAL_Delay(10);if(USART2_RX_STA & 0x8000){printf("%s\r\n",USART2_RX_BUF);if(Check_Command("smartconfig connected wifi")){res = 1;HAL_Delay(500);break;	}USART2_RX_STA=0;}if(waittime == 0)printf("Timeout\r\n");}if(Send_Command("AT+CWSTOPSMART", "OK", 50)){printf("Stop smartconfig\r\n");}else{printf("smartconfig stop error\r\n");}}else{printf("smartconfig open error\r\n");}return res;
}

墨水屏的显示

墨水屏的显示关键在于调用GUI_Paint里的各种画图接口来显示图片,数字,字母,中文等。这里用墨水屏的局部刷新为例子。
首先需要事先申请开辟一块空间用于存储画点的数据,调用Paint_NewImage函数创建一幅新的图像。

uint8_t	*BlackImage;
uint16_t Imagesize = ((EPD_WIDTH % 8 == 0)? (EPD_WIDTH / 8 ): (EPD_WIDTH / 8 + 1)) * EPD_HEIGHT;//申请空间if((BlackImage = (uint8_t *)malloc(Imagesize)) == NULL)return -1;Paint_NewImage(BlackImage, EPD_WIDTH, EPD_HEIGHT, 270, WHITE);	//创建新的图像

然后调用DEV_Module_Init初始化模块,EPD_Init确定刷新墨水屏的模式是局部刷新还是全局刷新,Paint_SelectImage选择刚刚申请的内存空间为刷新数据。然后就可以调用具体的画图函数去显示自己想要显示的内容即可,最后调用EPD_Display显示图片,调用EPD_SleepDEV_Module_Exit让墨水屏进入休眠即可。

//屏幕部分刷新
void GUI_Display_Part(void)
{DEV_Module_Init();EPD_Init(EPD_PART);Paint_SelectImage(BlackImage);RTC_GetTime();	//获取RTC时间//刷新时间Paint_ClearWindows(5, 50, 5 + FontNum.Width * 5-5, 50 + FontNum.Height, WHITE);Paint_DrawTime(5, 50, &Paint_time, &FontNum, WHITE, BLACK);//刷新WIFI连接状态Paint_ClearWindows(90,30,90 + 24,30 + 24,WHITE);if(Check_WIFI())Paint_DrawBitMap_Paste(gImage_WIFI_24,90,30,24,24,1);elsePaint_DrawBitMap_Paste(gImage_NoWIFI_24,90,30,24,24,1);EPD_Display(BlackImage);EPD_Sleep();DEV_Module_Exit();
}

这篇关于物联网小项目——墨水屏时钟(STM32+ESP8266实现)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/556749

相关文章

Flutter实现文字镂空效果的详细步骤

《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模... 目录引言实现原理开始实现步骤1:创建基础应用结构步骤2:创建主屏幕步骤3:实现自定义绘制器步骤4:构建U

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

Python开发文字版随机事件游戏的项目实例

《Python开发文字版随机事件游戏的项目实例》随机事件游戏是一种通过生成不可预测的事件来增强游戏体验的类型,在这篇博文中,我们将使用Python开发一款文字版随机事件游戏,通过这个项目,读者不仅能够... 目录项目概述2.1 游戏概念2.2 游戏特色2.3 目标玩家群体技术选择与环境准备3.1 开发环境3

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环