目录
前言
Hey,我是Moyiji,一个嵌入式小白,也是一名大三在读生,这是我的第一篇博客,在这里我将向你介绍过去一段时间我开发这个项目的经历和感受。并且通过这篇博客来记录自己的开发过程。
程序现象
my_watch_video
项目背景
在四月份我使用标准库在裸机上复刻了谷歌小恐龙游戏和一个类似于天天酷跑炫飞模式的游戏,并且写了一个多级菜单,在那之后我就决定写一个游戏机项目来集成这些游戏。同时我也在学习韦东山的FREERTOS教程,学习完FREERTOS之后我就感觉这玩意儿用来做这个项目实在太合适了!于是我便决定使用FREERTOS来开发这个项目。不过在五一前夕到五月中旬这段时间里我有足足四门考试要准备, 因此进度便搁置了一段时间。在考完试之后,我便着手开发这个项目(考试中也有缓慢推进)。在写了无数个bug之后,我终于完成了这个项目的软件开发(不过我转换了方向,从做游戏机换成了做多功能手表)。
项目介绍
基于FREERTOS的STM32多功能手表:正如标题所言,这个项目所使用的硬件平台是STM32(stm32f103c8t6),然后使用freerrtos来管理和协同各个任务。
目前版本实现的功能
- 时间显示
- 多级菜单显示
- 万年历(显示2024年份的日历)
- 模拟手电
- 温湿度显示
- 电子闹钟
- 设置(开关系统声音)
这里补充一下,本来我是准备加上血氧监测模块的,因为之前花了十几块大洋买了一个MAX30102一直在搁置,但是当我移植完之后发现这个驱动运行需要占用10K左右的RAM!这你受得了,因为c8t6只有20k RAM, 再加上freertos的占用,就,就没内存了,,,因此只能放弃这个功能了。。。
设计到的freertos知识
- 任务管理(创建,删除,状态转换)
- 软件定时器(创建,启动,停止)
- 队列(创建,写队列,读队列)
- 二值信号量(创建,give,take)
- 中断管理(中断与任务通信)
- 资源管理(主要是堆栈大小的处理)
使用到的硬件
- stm32f103c8t6(20k RAM, 64KROM)
- 0.96寸oled显示屏(黄蓝双色, u8g2库)
- 四个独立按键(中断中写入队列与任务协调)
- 无源蜂鸣器(按键或者电子时钟触发)
- DHT11(显示温湿度信息)
硬件连线图
OLED | KEY | 蜂鸣器 | DHT11 |
SCL(PB6) | key.rdata(PB11) | I/O(PA8) | DAT(PA1) |
SDA(PB7) | key.ldata(PB10) | ||
key.updata(PB1) | |||
key.exdata(PB0) |
实现思路
在freertos初始化时创建除默认任务之外的七个任务,分别是显示时间task,菜单task以及五个功能task,在默认任务中创建两个软件定时器,分别是时间显示Timer和电子闹钟Timer,在中断中向队列写入数据与各个任务完成通信并响应操作,在显示时间task中挂起其它六个任务,在按键触发任务切换时恢复下一个任务,并且挂起自己。
任务调度流程图
任务具体操作导图
代码讲解
这部分时不太想写的,因为在我写完前文所提到的两个游戏和多级菜单的代码后,回看时真的感觉自己写的代码跟屎一样,毫无命名规范,可阅读性极差,真的就是所谓的屎山代码,虽然在这个项目中有去努力优化代码但还是感觉自己的编程水平很低,毕竟我可是大一C语言考试全班倒数第一的男人(狗头)。好了接下来我向各位讲解部分代码,由于是个人开发,程序前后也没有进行严格的测试和优化,可能部分代码是没有作用的,但是不影响整体程序的运行就没有优化,如果你遇到看不懂的就当作无效跳过就行。
".........................."为省略部分,详情请查看源码
freertos初始化
创建显示时间定时器,和电子闹钟定时器,注意显示时间的周期是1000tick,而电子闹钟的周期是100tick,另外按键音效也是通过软件定时器的。补充:定时器默认优先级很低,你需要到FreeRTOSConfig.h文件中手动提高软件定时器的优先级(高于所以创建的任务)
/* USER CODE BEGIN RTOS_TIMERS */ /* start timers, add new ones, ... */ /* time and clock's Timer */ g_Timer = xTimerCreate("Timer1", 1000, pdTRUE, NULL, (TimerCallbackFunction_t)TimerCallBackFun); g_Clock_Timer = xTimerCreate("Timer2", 100, pdTRUE, NULL, (TimerCallbackFunction_t)ClockTimerCallBackFun); /* USER CODE END RTOS_TIMERS */
创建默认task和其它七个task,优先级均为默认优先级,栈大小为128/256,代码中的 Task_default_size也是128,调试的时候设置的就没改。在第三个任务下面有一个ShowWoodenFishTask, 这是一个电子木鱼,按下按键就可以加功德,因为后面又写了日历功能,就把这个给替换掉了,但是文件没有删,你可以将这个功能替换来体验一下
/* Create the thread(s) */ /* creation of defaultTask */ defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ /* create some tasks */ xTaskCreate(ShowTimeTask, "ShowTimeTask", 128, NULL, osPriorityNormal, &xShowTimeTaskHandle); xTaskCreate(ShowMenuTask, "ShowMenuTask", 256, NULL, osPriorityNormal, &xShowMenuTaskHandle); /******** 5 apps ********/ /*1*/ xTaskCreate(ShowCalendarTask, "ShowCalendarTask", 256, NULL, osPriorityNormal, &xShowCalendarTaskHandle); /*2*/ xTaskCreate(ShowFlashLightTask, "ShowFlashLightTask", Task_default_size, NULL, osPriorityNormal, &xShowFlashLightTaskHandle); /*3*/ xTaskCreate(ShowDHT11Task, "ShowDHT11Task", Task_default_size, NULL, osPriorityNormal, &xShowDHT11TaskHandle); //xTaskCreate(ShowWoodenFishTask, "ShowWoodenFishTask", Task_default_size, NULL, osPriorityNormal, &xShowWoodenFishTaskHandle); /*4*/ xTaskCreate(ShowClockTimeTask, "ShowClockTimeTask", Task_default_size, NULL, osPriorityNormal, &xShowClockTaskHandle); /*5*/ xTaskCreate(ShowSetting_Task, "ShowSetting_Task", 256, NULL, osPriorityNormal, &xShowSettingTaskHandle); /* USER CODE END RTOS_THREADS */
按键中断回调函数
在中断回调函数中将数据写入队列,这里的for循环本意是想进行按键消抖,因为我在开发板上测试的时候,会遇到多次响应的问题就很烦,然后加了这个for循环,发现没啥用,后来自己用小面包板上换了另一种按键搭建了一个按键模块,发现效果好多了,呜呜呜,网上买的按键模块实在是垃圾,电路板明明设计的有电容的焊点但是却没有焊电容,哎浪费我很多时间,也没办法,几块钱还要啥自行车
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { /* key interrupt : send data to queue */ ........................... if(GPIO_Pin == GPIO_PIN_11) { for(int i = 0; i<5000; i++){} if(end_flag == 1&&seclect_end == 0) { RM_Flag = 1; key_data.rdata = RM_Flag; xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); RM_Flag = 0; } } ............................ }
显示时间任务
初始驱动,并且挂起其它任务,这里时间需要不停刷新显示,因此在读队列函数中就没有portMAX_DELAY, 而是手动vTaskDelay了一段时间来让出CPU资源
void ShowTimeTask(void *params) { buzzer_init(); //xSemaphoreTake(g_xSemTicks, portMAX_DELAY); /* suspend_other_task */ vTaskSuspend(xShowMenuTaskHandle); //vTaskSuspend(xShowWoodenFishTaskHandle); vTaskSuspend(xShowFlashLightTaskHandle); vTaskSuspend(xShowSettingTaskHandle); vTaskSuspend(xShowClockTaskHandle); vTaskSuspend(xShowCalendarTaskHandle); vTaskSuspend(xShowDHT11TaskHandle); ...................... while(1) { ...................... vTaskDelay(250); /* handle queue data */ if(time_flag == 0) { pdPASS == xQueueReceive(g_xQueueMenu, &key_data, 0); } /* task scheduling */ if(key_data.updata == 1) { buzzer_buzz(2500, 100); vTaskResume(xShowMenuTaskHandle); vTaskSuspend(NULL); key_data.updata = 0; } } }
显示菜单任务
创建队列显示图像,读队列,图标移动,状态机里面来进行任务的切换
void ShowMenuTask(void *params) { /* system sound */ buzzer_init(); /* create queue */ g_xQueueMenu = xQueueCreate(4, 4); if(NULL != g_xQueueMenu)HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); ..................................... while(1) { u8g2_ClearBuffer(&u8g2); ShowUI(); u8g2_SendBuffer(&u8g2); /* receive queue data and keep waitting */ if(queue_flag == 0) { pdPASS == xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY); } /* handle data */ if(key_data.rdata == 1) { ........................... /********* move **********/ ........................... } /* ststus machine : task scheduling */ else if(key_data.exdata == 1) { buzzer_buzz(2000, 100); switch(dock_pos) { case 0: vTaskResume(xShowCalendarTaskHandle);vTaskSuspend(NULL);key_data.exdata = 0;break; case 1: vTaskResume(xShowFlashLightTaskHandle);vTaskSuspend(NULL);key_data.exdata = 0;break; case 2: vTaskResume(xShowDHT11TaskHandle);vTaskSuspend(NULL);key_data.exdata = 0;break; case 3: vTaskResume(xShowClockTaskHandle);vTaskSuspend(NULL);key_data.exdata = 0;break; case 4: vTaskResume(xShowSettingTaskHandle);vTaskSuspend(NULL);key_data.exdata = 0;break; } } else if(key_data.updata == 1) { /* SysSound */ buzzer_buzz(2000, 100); vTaskResume(xShowTimeTaskHandle); vTaskSuspend(NULL); key_data.updata = 0; } } }
其它任务(ShowCalendarTask)
这里我就只介绍显示日历这个功能了,其它功能大家可以自行了解一下。这里是参考博主@
齊 天 大 聖的教程,原址:C语言实现万年历(附代码)_万年历c语言编程代码-CSDN博客
/* 判断是否是闰年 */ int judge_year(int year) { ..................... } /* 判断需要输出的年份的一月一日是星期几 */ int judge_week(int year) { ..................... } /* 输出闰年各个月的天数 */ int month_run(int n) { ..................... } void ShowCalendarTask(void *params) { buzzer_init(); /* 创建队列 */ g_xQueueMenu = xQueueCreate(1, 4); if(NULL != g_xQueueMenu)HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); ................................. struct Key_data key_data; /* some data */ ................................. while(1) { u8g2_ClearBuffer(&u8g2); /* 绘制星期标头 */ for(int i=0; i<=6; i++){ u8g2_DrawStr(&u8g2, usWeekX[i], 8, ucWeekHeader[i]); } /* month: 当前月份 */ month_temp = month_run(month);//获取月份对应的天数 week_temp = judge_week(2024);//获取2024年1月1日是星期几 wee = week_temp; for(int m=1; m<month; m++){ wee = (wee+month_run(m))%7; //记录当前月的第一天是星期几 } week_temp = wee; week_temp_temp = week_temp; /* 绘制当前月的日历 */ for(int k=1; k<=month_temp; k++){ enter_temp = week_temp%7; week_temp++; if(k<=(7-week_temp_temp)){ week_pos=0; }else if(enter_temp == 0){ week_pos = week_pos+1; } u8g2_DrawStr(&u8g2, usWeekX[enter_temp], usWeekY[week_pos], ucMonthDay[k]); } .................................... u8g2_SendBuffer(&u8g2); /* 接受队列数据 */ xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY); /* 切换下一个月 */ if(key_data.rdata == 1) { buzzer_buzz(2500, 100);//响应蜂鸣器(这里也是使用软件定时器实现的前面忘记说了) ................................ } /* 任务切换 */ if(key_data.exdata == 1) { ................................. } //u8g2_SendBuffer(&u8g2); } }
2024/6/5更新
原版本存在按键抖动致使向队列连续多次写入多个数据而导致“单次操作多次响应”的问题,更新使用二值信号量来解决上述问题,代码思路如下,源码中未使用,大家可以自己尝试去解决这一问题
/* task中 */ SemaphoreHandle_t xBinarySemaphore; ................................ xSemaphoreCreateBinary(); ................................ if(......)xSemaphoreGive(xBinarySemaphore); ................................ /* key interrupt */ if(pdTRUE == xSemaphoreTakeFromISR(xBinarySemaphore, 0)) { xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL); }
总结
OJBK!感谢你能看到这里,这是我的第一篇博客,大概写了两三个小时左右,可能写的很垃圾,见谅见谅,后续我也会录制一个更为详细的视频发布到小破站,想要学习的可以了解一下。后续的话我可能会尝试为这个项目打个板子,maybe。
这是我第一次尝试使用freertos去做项目开发,freertos确实很强大,在我看来,freertos的基本原理就跟我们专业(通信工程)里所将的时分复用类似:把时间分为周期性的帧,每一帧又分为若干个时隙,就构成通信的最小单元——时隙,然后占用时隙进行通信。而freertos就是的最小单元就是tick,每个任务就是占用一个又一个tick进行运行的,freertos利用这一原理巧妙的在单核CPU上进行多任务操作,不得不佩服前人的智慧!
开源链接
链接:开源让世界更美好