一、项目概述
此项目是一款结合 FreeRTOS,使用 STM32F103C8T6 制作的智能桌面小闹钟。功能主要有显示时间,设置时间,设定闹钟,查看天气,秒表计时五个功能。用到的模块有 stm32f103c8t6,DS3231 时钟模块,esp8266 WIFI模块,4脚 0.96寸OLED 屏,5v 有源蜂鸣器,按键。
二、模块介绍
stm32f103c8t6:
笔者使用的是市面上最常见的这款板子做主控。
DS3231 时钟模块:
DS3231与单片机通过I2C双向串行总线传输地址与数据。它的寄存器能保存秒、分、时、星期、日期、月、年和闹钟设置等信息。少于31天的月份,可自动调整月末日期,包括闰年补偿。并且提供两个可编程日历闹钟。
esp8266 WIFI模块:
ESP8266是一个功能强大的Wi-Fi模块,它能够轻松与其他设备进行通信,为物联网和智能家居等领域的应用提供了灵活而高效的解决方案。我们使用 esp8266 模块连接服务器来获取天气信息。
0.96寸OLED 屏:
我使用的是 4 线的 IIC 通信的 OLED 屏幕来显示时间。
5v 有源蜂鸣器:
使用是是有源蜂鸣器,操作简单,有电势差就响。
三、引脚连接
引脚连接:
整体框图:
四、功能介绍
1、整体思路
创建不同的任务分别执行不同的功能。使用按键控制进入不同的任务中。四个按键分别表示:
1——前进;
2——后退;
3——向上;
4——向下;
主界面显示时间,然后按 1 进入菜单界面。按 3、4 进行选择。按 1 进入,2 退出。
显示时间,设置闹钟,设置时间都可以配合 DS3231 时钟模块完成。秒表我使用的是 c8t6 中的定时器。天气功能使用 esp8266 连接服务器后,在心知天气中获取天气,温度,风力等等。
void start_task( void * pvParameters )
{
taskENTER_CRITICAL(); //进入临界区
weather_event_group = xEventGroupCreate(); //创建事件标志组
xTaskCreate( (TaskFunction_t ) showtime, //主界面,显示时间
(char * ) "showtime",
(uint16_t ) SHOWTIME_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) SHOWTIME_PRIO,
(TaskHandle_t * ) &showtime_handler );
xTaskCreate( (TaskFunction_t ) menus, //菜单界面,显示功能
(char * ) "menus",
(uint16_t ) MENUS_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) MENUS_PRIO,
(TaskHandle_t * ) &menus_handler );
xTaskCreate( (TaskFunction_t ) settime, //设置时间
(char * ) "settime",
(uint16_t ) SETTIME_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) SETTIME_PRIO,
(TaskHandle_t * ) &settime_handler );
xTaskCreate( (TaskFunction_t ) setclock, //闹钟功能
(char * ) "setclock",
(uint16_t ) SETCLOCK_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) SETCLOCK_PRIO,
(TaskHandle_t * ) &setclock_handler );
xTaskCreate( (TaskFunction_t ) stopwatch, //秒表功能
(char * ) "stopwatch",
(uint16_t ) STOPWATCH_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) STOPWATCH_PRIO,
(TaskHandle_t * ) &stopwatch_handler );
xTaskCreate( (TaskFunction_t ) weather_forecast, //天气功能
(char * ) "weather_forecast",
(uint16_t ) WEATHER_FORECAST_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) WEATHER_FORECAST_PRIO,
(TaskHandle_t * ) &weather_forecast_handler );
xTaskCreate( (TaskFunction_t ) beep, //声音功能
(char * ) "beep",
(uint16_t ) BEEP_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) BEEP_PRIO,
(TaskHandle_t * ) &beep_handler );
xTaskCreate(key_task, "Key Task", 128, NULL, 2, NULL);// 创建按键任务
vTaskDelete(NULL); //vTaskDelete(start_task_handler);//清除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
2、显示时间
显示时间任务中主要是获取时间这个函数。在这个函数中,通过 I2C_DS3231_getTime(); 和I2C_DS3231_getTemperature(); 来分别获取时间和温度。这两个函数通过 IIC 通信直接读取 DS3231 时钟模块中的寄存器来获取数据并返回到一个 calendar 的结构体中。下面将结构体中的数据赋值到全局变量中并在OLED屏上显示。
void get_time()
{
I2C_DS3231_getTime(); //获取时间
I2C_DS3231_getTemperature(); //获取温度
years = calendar.year;
mons = calendar.month;
day = calendar.date;
week = calendar.week-1;
hrs = calendar.hour;
mins = calendar.min;
sec = calendar.sec;
tem = calendar.temperature;
// u2_printf("时%d 分%d 秒%d 温度%d\r\n",hrs,mins,sec,tem); //调试参数
}
3、设置时间
设置时间主要用到了 I2C_DS3231_SetTime(years,mons,day,week+1,hrs,mins,0); 这个函数。这个函数也是通过 IIC 通信直接将数据写入 DS3231 时钟模块中的寄存器中,达到修改时间的目的。
void I2C_DS3231_SetTime(u8 yea,u8 mon,u8 da,u8 we,u8 hou,u8 min,u8 sec)
{
u8 temp=0;
temp=DEC_BCD(yea);
I2C_DS3231_ByteWrite(0x06,temp);
temp=DEC_BCD(mon);
I2C_DS3231_ByteWrite(0x05,temp);
temp=DEC_BCD(da);
I2C_DS3231_ByteWrite(0x04,temp);
temp=DEC_BCD(we);
I2C_DS3231_ByteWrite(0x03,temp);
temp=DEC_BCD(hou);
I2C_DS3231_ByteWrite(0x02,temp);
temp=DEC_BCD(min);
I2C_DS3231_ByteWrite(0x01,temp);
temp=DEC_BCD(sec);
I2C_DS3231_ByteWrite(0x00,temp);
}
4、设置闹钟
闹钟的设置也同理。闹钟触发会置标志位,Alarmclock1state() 可以查看标志位,返回 1 则证明触发了。
void SetAlarmclock(u8 ahour,u8 amin,u8 asec)//设置闹钟
{
u8 d;
d = DEC_BCD(ahour);
I2C_DS3231_ByteWrite(DS3231_ALARM1HOUR,d);
d = DEC_BCD(amin);
I2C_DS3231_ByteWrite(DS3231_ALARM1MINUTE,d);
d = DEC_BCD(asec);
I2C_DS3231_ByteWrite(DS3231_SALARM1ECOND,d);
I2C_DS3231_ByteWrite(DS3231_ALARM1WEEK,0x80);
}
void beep( void * pvParameters ){
while(1){
if(Alarmclock1state()==1)//检测闹钟标志位
{
beep0 = 1; //蜂鸣器打开
vTaskDelay(5000);
beep0 = 0; //蜂鸣器关闭
Alarmclock1_cmd(0); //闹钟失能
Alarmclock1_close(); //关闭闹钟并清理标志位
}
vTaskDelay(100);
}
}
5、按键功能
按键使用消息队列。在中断中检测具体哪个按键触发并将按键数据添加到消息队列中,在按键任务中如果检测到消息队列有数据就将它赋值给 key_value,然后在其他任务中通过获取 key_value 的值来读取按键信息。
按键任务:
void key_task(void *pvParameters)
{
uint8_t key;
while (1)
{
if (xQueueReceive(keyQueue, &key, portMAX_DELAY) == pdPASS)
{
beep0 = 1; //蜂鸣器打开
key_value=key;
vTaskDelay(200);
key_pressed=0;
}
beep0 = 0; //蜂鸣器关闭
vTaskDelay(200);
}
}
u8 KEY_Scan()
{
uint8_t i=key_value;
key_value=0;
return i;
}
中断服务函数:
void EXTI15_10_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t key = 0;
if (EXTI_GetITStatus(EXTI_Line12) != RESET)
{
if (!key_pressed)
{
key = KEY1_PRES; // 第一行第一列按下
xQueueSendFromISR(keyQueue, &key, &xHigherPriorityTaskWoken);
key_pressed = 1;
}
EXTI_ClearITPendingBit(EXTI_Line12);
}
if (EXTI_GetITStatus(EXTI_Line13) != RESET)
{
if (!key_pressed)
{
key = KEY2_PRES; // 第一行第二列按下
xQueueSendFromISR(keyQueue, &key, &xHigherPriorityTaskWoken);
key_pressed = 1;
}
EXTI_ClearITPendingBit(EXTI_Line13);
}
if (EXTI_GetITStatus(EXTI_Line14) != RESET)
{
if (!key_pressed)
{
key = KEY3_PRES; // 第二行第一列按下
xQueueSendFromISR(keyQueue, &key, &xHigherPriorityTaskWoken);
key_pressed = 1;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
if (EXTI_GetITStatus(EXTI_Line15) != RESET)
{
if (!key_pressed)
{
key = KEY4_PRES; // 第二行第二列按下
xQueueSendFromISR(keyQueue, &key, &xHigherPriorityTaskWoken);
key_pressed = 1;
}
EXTI_ClearITPendingBit(EXTI_Line15);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
6、天气功能
获取天气的基本思路是创建一个任务去连接服务器获取天气,如果获取成功就置一个事件标志位。在需要查看天气的时候去判断事件标志位是否置位,如果没有就显示正在获取天气,如果成功置位就显示获取到的天气。
获取天气
获取天气使用的是心知天气,它可以免费获取天气,获取方法也很简单,网上教程比较多。在你拿到如下图的私钥后,将它复制到下面的私钥位置。城市名是你要获取的城市名称,语言就是数据返回的格式。比如我所在城市是西安,语言选择英语:城市名称:xian;语言:en。
printf("GET https://api.seniverse.com/v3/weather/now.json?key=私钥&location=城市名称&language=语言&unit=c\r\n");
在获取到天气后使用 cJSON 来解析天气数据。
JSON 全称 JavaScript Object Notation,即 JS对象简谱,是一种轻量级的数据格式。它采用完全独立于编程语言的文本格式来存储和表示数据,语法简洁、层次结构清晰,易于人阅读和编写,同时也易于机器解析和生成,有效的提升了网络传输效率。
u8 get_current_weather(void)
{
u8 res;
p=mymalloc(40); //申请40字节内存
//配置目标TCP服务器
sprintf((char*)p,"AT+CIPSTART=\"TCP\",\"%s\",%s",WEATHER_SERVERIP,WEATHER_PORTNUM);
u2_printf("send:%s\r\n",p);
res = esp8266_send_cmd(p,"OK",100);//连接到目标TCP服务器
if(res==1)
{
myfree(p);
return 1;
}
delay_ms(300);
//传输模式为:透传
u2_printf("send:AT+CIPMODE=1\r\n");
esp8266_send_cmd("AT+CIPMODE=1","OK",100);
//开始透传
USART_RX_STA=0;
u2_printf("send:AT+CIPSEND\r\n");
esp8266_send_cmd("AT+CIPSEND","OK",100);
u2_printf("GET https://api.seniverse.com/v3/weather/now.json?key=xxxxxxxx(自己的私钥)&location=xian&language=en&unit=c\r\n");
printf("GET https://api.seniverse.com/v3/weather/now.json?key=xxxxxxxx(自己的私钥)&location=xian&language=en&unit=c\r\n");
delay_ms(20);//延时20ms返回的是指令发送成功的状态
USART_RX_STA=0; //清零串口数据
delay_ms(1000);
if(USART_RX_STA&0X8000) //此时再次接到一次数据,为天气的数据
{
USART_RX_BUF[USART_RX_STA&0X7FFF]=0;//添加结束符
}
//解析天气数据
cJSON_WeatherParse(USART_RX_BUF, results);
strcpy(city,results[0].location.name);
strcpy(code,results[0].now.code);
strcpy(temp,results[0].now.temperature);
strcpy(time,results[0].last_update);
strcpy(weat,results[0].now.text);
if(strcmp(weat,"Sunny")==0||strcmp(weat,"Clear")==0||strcmp(weat,"Fair")==0){
weather_nums=1;
}else if(strcmp(weat,"Cloudy")==0||strcmp(weat,"Partly cloudy")==0||strcmp(weat,"Mostly cloudy")==0){
weather_nums=2;
}else if(strcmp(weat,"Overcast")==0){
weather_nums=3;
}else if(strcmp(weat,"Shower")==0||strcmp(weat,"Thundershower")==0||strcmp(weat,"Thundershower with hail")==0){
weather_nums=4;
}else if(strcmp(weat,"Light rain")==0||strcmp(weat,"Light rainain")==0||strcmp(weat,"Moderate rain")==0||strcmp(weat,"Heavy rain")==0){
weather_nums=4;
}else if(strcmp(weat,"Storm")==0||strcmp(weat,"Heavy storm")==0||strcmp(weat,"Severe storm")==0){
weather_nums=4;
}else if(strcmp(weat,"Light snow")==0||strcmp(weat,"Moderate snow")==0||strcmp(weat,"Heavy snow")==0||strcmp(weat,"Snowstorm")==0){
weather_nums=5;
}else{
weather_nums=0;
}
//退出透传;
atk_8266_quit_trans();
//关闭TCP连接
u2_printf("send:AT+CIPCLOSE\r\n");
esp8266_send_cmd("AT+CIPCLOSE","OK",50);
myfree(p);
return 0;
}
这段代码是因为天气实在太多了,OLED那边如果要显示所有类型的天气,字库太大而且天气图标难做。所以我就将天气分了 5 个大类。
if(strcmp(weat,"Sunny")==0||strcmp(weat,"Clear")==0||strcmp(weat,"Fair")==0){
weather_nums=1;
}else if(strcmp(weat,"Cloudy")==0||strcmp(weat,"Partly cloudy")==0||strcmp(weat,"Mostly cloudy")==0){
weather_nums=2;
}else if(strcmp(weat,"Overcast")==0){
weather_nums=3;
}else if(strcmp(weat,"Shower")==0||strcmp(weat,"Thundershower")==0||strcmp(weat,"Thundershower with hail")==0){
weather_nums=4;
}else if(strcmp(weat,"Light rain")==0||strcmp(weat,"Light rainain")==0||strcmp(weat,"Moderate rain")==0||strcmp(weat,"Heavy rain")==0){
weather_nums=4;
}else if(strcmp(weat,"Storm")==0||strcmp(weat,"Heavy storm")==0||strcmp(weat,"Severe storm")==0){
weather_nums=4;
}else if(strcmp(weat,"Light snow")==0||strcmp(weat,"Moderate snow")==0||strcmp(weat,"Heavy snow")==0||strcmp(weat,"Snowstorm")==0){
weather_nums=5;
}else{
weather_nums=0;
}
连接WIFI
此处分别填写需要连接的 WIFI 名称和密码。
五、写在最后
完整项目我放在主页的资源中了,大家可以随时下载!!!
我在串口2中添加了调试信息,大家可以使用串口调试助手检查。
第一次做FreeRTOS项目,目的是为了练习。因此,在代码中努力添加了FreeRTOS的部分,导致代码质量不高,也存在很多冗余的部分。如果大家发现了我的代码存在问题,请随时提出改进的意见。