FreeRTOS 小项目--基于STM32F103智能桌面小闹钟(附完整代码)

一、项目概述

        此项目是一款结合 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的部分,导致代码质量不高,也存在很多冗余的部分。如果大家发现了我的代码存在问题,请随时提出改进的意见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值