前言
项目背景
项目演示
使用到的硬件
项目原理图
目前版本实现的功能
设计到的freertos知识
实现思路
代码讲解
初始化GPIO引脚、配置时钟
蜂鸣器初始化以及软件定时器创建
系统默认创建的defaultTaskHandle
创建七个Task,代表七个功能
ShowTimeTask
ShowMenuTask
ShowCalendarTask
ShowFlashLightTask
ShowDHT11Task
ShowClockTimeTask
ShowSetting_Task
总结
按键中断回调函数
蜂鸣器实现按键音效
buzzer_init
SystemSoundTimer_Func( TimerHandle_t xTimer )
void buzzer_buzz(int freq, int time_ms)
总结
前言
这是我第一篇关于FreeRTOS项目的博客,在这之前,我写了十几篇关于FreeRTOS的博客,从应用到底层原理都有讲解,如果复刻我这个项目的同学,遇到不会的话,可以跳转到我之前写的关于FreeRTOS博客。深入了解FreeRTOS:实时操作系统的核心概念和应用
项目背景
在更新完FreeRTOS的系列教程之后,迟迟没有通过一些项目来使用FreeRTOS,之前用裸机做过万年历,包括温湿度模块,以及做过一个多级菜单,正好可以拿来练手FreeRTOS,于是打算做一个多功能的手环,包括多级菜单、温湿度显示、闹钟设置、手电筒功能、日历显示、设置菜单。花费了几天,改了不少的BUG,终于把这几个功能集成到一起,做成了基于FREERTOS的STM32多功能手表。
项目演示
基于freertos的智能手表
使用到的硬件
- Stm32f103c8t6最小系统板(20k RAM, 64KROM)
- 0.96寸oled显示屏(u8g2库)
- 四个独立按键(中断中写入队列与任务协调)
- 无源蜂鸣器(按键或者电子时钟触发)
- DHT11(显示温湿度信息)
项目原理图
目前版本实现的功能
- 时间显示
- 多级菜单显示
- 万年历(显示2024年份的日历)
- 模拟手电
- 温湿度显示
- 电子闹钟
- 设置(开关系统声音)
- 补充:如果你有多的传感器当然可以继续扩展手环的功能,不过你要注意的是,c8t6只有20K的RAM,各个驱动运行的内存以及FreeRTOS的,如果超过了20K,那就无法继续扩展了。
设计到的freertos知识
- 任务管理(创建,删除,状态转换)
- 软件定时器(创建,启动,停止)
- 队列(创建,写队列,读队列)
- 二值信号量(创建,give,take)
- 中断管理(中断与任务通信)
- 资源管理(主要是堆栈大小的处理)
- 补充:这些知识点我都有写博客,不会的可以跳转去仔细看看。
- https://blog.csdn.net/m0_74676415/article/details/139202620
实现思路
在freertos初始化时创建除默认任务之外的七个任务,分别是时间显示task,舵机菜单显示task以及五个功能(温湿度、手电筒、设置、日历、闹钟)task,在默认任务中创建两个软件定时器,分别是时间显示Timer和电子闹钟Timer,其中,时间显示的定时时间为1s,保证手表时钟显示时间的正确。电子闹钟Timer的定时时间为0.1s,因为我们闹钟定时是0.1s刷新一次,这样子可以实现一秒之内显示0-9这十个数字,在观感上有很大提升。另外在中断中向队列写入数据与各个任务完成通信并响应操作。
1、创建两个定时器。
2、创建七个任务。
3、按键中断触发任务切换。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* key interrupt : send data to queue */
/* some data maybe useless */
extern BaseType_t end_flag;
extern BaseType_t seclect_end;
BaseType_t RM_Flag, LM_Flag, EN_Flag, EX_Flag;
Key_data key_data;
if(GPIO_Pin == GPIO_PIN_11)
{
mdelay(15);
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;
}
}
if(GPIO_Pin == GPIO_PIN_10)
{
mdelay(15);
if(end_flag == 1&&seclect_end == 0)
{
LM_Flag = 1;
key_data.ldata = LM_Flag;
xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
LM_Flag = 0;
}
}
if(GPIO_Pin == GPIO_PIN_1)
{
mdelay(15);
if(end_flag == 1&&seclect_end == 0)
{
EN_Flag = 1;
key_data.updata = EN_Flag;
xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
EN_Flag = 0;
}
}
if(GPIO_Pin == GPIO_PIN_0)
{
mdelay(15);
if(end_flag == 1&&seclect_end == 0)
{
EX_Flag = 1;
key_data.exdata = EX_Flag;
if(end_flag == 1&&seclect_end == 0)xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
EX_Flag = 0;
}
}
}
代码讲解
我这里会介绍一下各个任务,让你有大体的认识。由于是个人开发,程序前后也没有进行严格的测试和优化,可能部分代码是没有作用的,但是不影响整体程序的运行就没有优化,如果你有某些不理解的地方,可以删掉该部分代码,如果现象依旧正常,说明我之前写的那堆就确实没用,但是,也不要随便去质疑,可能我这么做有我的考虑。
初始化GPIO引脚、配置时钟
我们使用CubeMX完成GPIO引脚的初始化,包括四个按键中断引脚,DHT11温湿度的Data引脚以及Oled屏幕的I2C引脚和无源蜂鸣器的电平触发引脚。
蜂鸣器初始化以及软件定时器创建
这里创建了两个定时器,g_Timer和g_Clock_Timer,显示时间定时器,和电子闹钟定时器,注意显示时间的周期是1000tick,而电子闹钟的周期是100tick,另外按键音效也是通过软件定时器的。补充:定时器默认优先级很低,你需要到FreeRTOSConfig.h文件中手动提高软件定时器的优先级(高于所以创建的任务),我们设置为34,比我们其他任务的优先级都要高,不然定时器定时的时间会不准确。
系统默认创建的defaultTaskHandle
我们直接在这里开启时间定时器,这样子,我们就可以每隔1s对时间加一次,保证复位上电第一时间闹钟可以正确显示时钟。我们还在这里创建了一个队列,这个队列是来处理按键的数据的,可以发现,这个队列的参数是1和4,1表示的是,这个队列只可以放一个数据,4表示的是,这个数据的大小是四个字节,正好对应我们按键结构体的四个字节,这样设置队列,可以有效防止按键抖动带来的影响,因为队列满的话,你中断里面写队列也会失败,但不会阻塞中断,不要太好用。
创建七个Task,代表七个功能
这七个任务,分别代表了时间显示、多级菜单显示、日历显示、手电功能显示、温湿度显示、闹钟显示、设置界面显示,这是这个小项目的核心,看懂这七个任务,了解好如何调度这七个任务,那这个项目你就拿捏了。
ShowTimeTask
void ShowTimeTask(void *params)
{/* suspend_other_task */
vTaskSuspend(xShowMenuTaskHandle);
//vTaskSuspend(xShowWoodenFishTaskHandle);
vTaskSuspend(xShowFlashLightTaskHandle);
vTaskSuspend(xShowSettingTaskHandle);
vTaskSuspend(xShowClockTaskHandle);
vTaskSuspend(xShowCalendarTaskHandle);
vTaskSuspend(xShowDHT11TaskHandle);/* u8g2 Start */
u8g2_t u8g2;
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0, u8x8_byte_hw_i2c, u8g2_stm32_delay);
u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
u8g2_SetPowerSave(&u8g2, 0); // wake up display
u8g2_ClearDisplay(&u8g2);
// u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese1);
u8g2_SetFont(&u8g2, u8g2_font_fur35_tf);
struct Key_data key_data;while(1)
{
u8g2_ClearBuffer(&u8g2);
/* draw */
u8g2_DrawXBMP(&u8g2, 0, 0, 23, 10, ShowPower);
u8g2_DrawXBMP(&u8g2, 105, 0, 23, 10, ShowGame);
/* draw time */
u8g2_DrawXBMP(&u8g2, time.x[3], time.y, time.w, time.h, BigNum[sec_unit]);
u8g2_DrawXBMP(&u8g2, time.x[2], time.y, time.w, time.h, BigNum[sec_decade]);
u8g2_DrawRBox(&u8g2, Box1.x, Box1.y, Box1.w, Box1.h, BOX_R);
u8g2_DrawRBox(&u8g2, Box2.x, Box2.y, Box2.w, Box2.h, BOX_R);
u8g2_DrawXBMP(&u8g2, time.x[1], time.y, time.w, time.h, BigNum[min_unit]);
u8g2_DrawXBMP(&u8g2, time.x[0], time.y, time.w, time.h, BigNum[min_decade]);u8g2_DrawXBMP(&u8g2, 56, 2, 6, 8, Num_6x8[hour_decade]);
u8g2_DrawXBMP(&u8g2, 66, 2, 6, 8, Num_6x8[hour_unit]);
u8g2_SendBuffer(&u8g2);vTaskDelay(250);
/* handle queue data */
if(time_flag == 0)
{
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;
}
}
}
1、挂起其他的任务,因为默认上电之后,是显示时间的。
2、 初始化OLED屏幕,调用 u8g2 库。
3、不断的循环显示时间。
4、如果队列读到有效按键数据,就跳转到菜单界面。
ShowMenuTask
void ShowMenuTask(void *params)
{/*u8g2_config*/
u8g2_config();
/* ShowUI */
// u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese1);
u8g2_FirstPage(&u8g2);
do {
u8g2_SendBuffer(&u8g2);
} while (u8g2_NextPage(&u8g2));
struct Key_data key_data;
while(1)
{
u8g2_ClearBuffer(&u8g2);
ShowUI();
u8g2_SendBuffer(&u8g2);
/* receive queue data and keep waitting */
if(queue_flag == 0)
{
xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY);
}
/* handle data */
if(key_data.rdata == 1)
{
end_flag = 0;
if(dock_pos != 0)
{
ui_right(&cleder.x, 2);ui_right(&torch.x, 2);ui_right(&hum.x, 2);ui_right(&clock.x, 2);ui_right(&setting.x, 2);
/* state_machine */
if(dock_status==0)dock_status=1;
dock_status--;
switch(dock_pos)
{
case 2:if(dock_status!=0){ui_up(&cleder.y, 1);ui_up(&torch.y, 1);ui_down(&hum.y, 1);ui_down(&clock.y, 1);}break;
case 1:if(dock_status!=0){ui_up(&cleder.y, 1);ui_up(&setting.y, 1);ui_down(&torch.y, 1);ui_down(&hum.y, 1);}break;
case 4:if(dock_status!=0){ui_up(&clock.y, 1);ui_up(&hum.y, 1);ui_down(&setting.y, 1);ui_down(&cleder.y, 1);}break;
case 3:if(dock_status!=0){ui_up(&hum.y, 1);ui_up(&torch.y, 1);ui_down(&setting.y, 1);ui_down(&clock.y, 1);}break;
}
}
queue_flag++;
if(queue_flag == 20)
{
dock_status=10;
end_flag = 1;
if(dock_pos != 0){dock_pos--;str_flag--;}
queue_flag = 0;
key_data.rdata = 0;
key_data.ldata = 0;
}
if(end_flag == 1)buzzer_buzz(2000, 100);
}
else if(key_data.ldata == 1)
{
end_flag = 0;
if(dock_pos < 4)
{
ui_left(&cleder.x, 2);ui_left(&torch.x, 2);ui_left(&hum.x, 2);ui_left(&clock.x, 2);ui_left(&setting.x, 2);
/* state_machine */
if(dock_status==0)dock_status=1;
dock_status--;
switch(dock_pos)
{
case 0:if(dock_status!=0){ui_up(&hum.y, 1);ui_up(&torch.y, 1);ui_down(&cleder.y, 1);ui_down(&setting.y, 1);}break;
case 1:if(dock_status!=0){ui_up(&hum.y, 1);ui_up(&clock.y, 1);ui_down(&torch.y, 1);ui_down(&cleder.y, 1);}break;
case 2:if(dock_status!=0){ui_up(&clock.y, 1);ui_up(&setting.y, 1);ui_down(&hum.y, 1);ui_down(&torch.y, 1);}break;
case 3:if(dock_status!=0){ui_up(&setting.y, 1);ui_up(&cleder.y, 1);ui_down(&clock.y, 1);ui_down(&hum.y, 1);}break;
}
}
queue_flag++;
if(queue_flag == 20)
{
dock_status = 10;
end_flag = 1;
if(dock_pos < 4){dock_pos++;str_flag++;}
queue_flag = 0;
key_data.ldata = 0;
key_data.rdata = 0;
}
if(end_flag == 1)buzzer_buzz(2000, 100);
}
/* 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;
}
}
}
1、初始化OLED屏幕,调用 u8g2 库。
2、显示UI界面。
3、读取队列,如果读不到就阻塞,直到中断中写入队列之后唤醒任务。
4、根据不同的按键处理不同的事件。
ShowCalendarTask
void ShowCalendarTask(void *params)
{
u8g2_t u8g2;
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0, u8x8_byte_hw_i2c, u8g2_stm32_delay);
u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
u8g2_SetPowerSave(&u8g2, 0); // wake up display
u8g2_ClearDisplay(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_spleen5x8_mf);u8g2_SendBuffer(&u8g2);
struct Key_data key_data;const char ucMonthDay[32][3] = {"0","1","2","3","4","5","6","7","8","9","10","11","12","13","14",
"15","16","17","18","19","20","21","22","23","24","25","26",
"27","28","29","30","31"};
const char ucWeekHeader[7][3] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
uint16_t usWeekX[7] = {0, 17, 34 , 51, 68, 85, 102};
uint16_t usWeekY[6] = {17, 26, 35 ,44, 53, 62};
uint16_t usLineY[12] = {0, 11, 22, 33, 44, 55, 66, 77, 88, 99, 110, 121};
uint8_t line_pos = 0;uint8_t week_pos = 0;
uint32_t week_temp, week_temp_temp, month_temp, enter_temp;
uint32_t month = 1;
uint16_t wee = 0; //记录当前月的第一天是星期几
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);//获取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_DrawLine(&u8g2, 115, 0, 115, 62);
u8g2_DrawStr(&u8g2, 117, 32, ucMonthDay[line_pos+1]);
u8g2_DrawHLine(&u8g2, usLineY[line_pos], 63, 11);
u8g2_SendBuffer(&u8g2);/* 读按键中断队列 */
xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY);if(key_data.rdata == 1)
{
buzzer_buzz(2500, 100);
month++;
line_pos++;
if(line_pos>11)line_pos=0;
if(month>12)month=1;
key_data.rdata = 0;
}
else if(key_data.exdata == 1)
{
buzzer_buzz(2500, 100);
vTaskResume(xShowMenuTaskHandle);
key_data.exdata = 0;
vTaskSuspend(NULL);}
else if(key_data.ldata==1)
{
buzzer_buzz(2500, 100);
month--;
line_pos--;
if(line_pos>=11)line_pos=11;
if(month==0)month=12;
key_data.ldata = 0;
}
else if(key_data.updata==1)
{
memset(&key_data, 0, sizeof(Key_data));
}
}
}
1、初始化OLED屏幕,调用 u8g2 库。
2、根据全局变量month以及line_pos,显示出这个月份的特定日历。
3、读取按键中断队列,进入阻塞状态,直到按键中断写入队列,唤醒任务。
4、根据读取到的键值去进行不同事件的处理。
ShowFlashLightTask
void ShowFlashLightTask(void *params)
{
/* u8g2 Start */
u8g2_t u8g2;
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0, u8x8_byte_hw_i2c, u8g2_stm32_delay);
u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
u8g2_SetPowerSave(&u8g2, 0); // wake up display
u8g2_ClearDisplay(&u8g2);
// u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese1);
// u8g2_SetFont(&u8g2, u8g2_font_spleen32x64_mf);
u8g2_SetFont(&u8g2, u8g2_font_fur35_tf);
//u8g2_ClearBuffer(&u8g2);
u8g2_DrawXBMP(&u8g2, 48, 16, 30, 30, light);
u8g2_SendBuffer(&u8g2);uint8_t light_flag = 0;
struct Key_data key_data;
while(1)
{
u8g2_ClearBuffer(&u8g2);
/* 读按键中断队列 */
xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY);
if(key_data.updata == 1)
{
buzzer_buzz(2500, 100);
switch(light_flag)
{
case 0: u8g2_DrawBox(&u8g2, 0, 0, 128, 64);light_flag++;break;
case 1: u8g2_DrawXBMP(&u8g2, 48, 16, 30, 30, light);light_flag--;break;
}
}
if(key_data.exdata == 1)
{
buzzer_buzz(2500, 100);
vTaskResume(xShowMenuTaskHandle);
vTaskSuspend(NULL);
u8g2_ClearBuffer(&u8g2);
u8g2_DrawXBMP(&u8g2, 48, 16, 30, 30, light);
}
else if(key_data.ldata==1||key_data.rdata==1)
{
switch(light_flag)
{
case 1: u8g2_DrawBox(&u8g2, 0, 0, 128, 64);break;
case 0: u8g2_DrawXBMP(&u8g2, 48, 16, 30, 30, light);break;
}
}
u8g2_SendBuffer(&u8g2);
}
}
1、 初始化OLED屏幕,调用 u8g2 库。
2、显示手电筒图标。
3、读取按键中断队列,进入阻塞状态,直到按键中断写入队列之后,唤醒任务。
4、根据不同的键值进行不同事件的处理。
ShowDHT11Task
void ShowDHT11Task(void *params)
{
DHT11_Init();
u8g2_t u8g2;
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0, u8x8_byte_hw_i2c, u8g2_stm32_delay);
u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
u8g2_SetPowerSave(&u8g2, 0); // wake up display
u8g2_ClearDisplay(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese1);u8g2_SendBuffer(&u8g2);
struct Key_data key_data;
int hum, temp;
int hum1, hum2,temp1, temp2;
while(1)
{
u8g2_ClearBuffer(&u8g2);
if (DHT11_Read(&hum, &temp) !=0 ){
//printf("\n\rdht11 read err!\n\r");
DHT11_Init();
}
else{
temp1 = temp%10; //low bit
temp2 = temp/10; //high bit
hum1 = hum%10; //low bit
hum2 = hum/10; //high bit
u8g2_DrawXBMP(&u8g2, 10, 20, 20, 40, BigNum[temp2]);
u8g2_DrawXBMP(&u8g2, 35, 20, 20, 40, BigNum[temp1]);
u8g2_DrawXBMP(&u8g2, 75, 20, 20, 40, BigNum[hum2]);
u8g2_DrawXBMP(&u8g2, 100, 20, 20, 40, BigNum[hum1]);
}
u8g2_DrawStr(&u8g2, 15, 15, "temp");
u8g2_DrawStr(&u8g2, 85, 15, "Hum");
u8g2_SendBuffer(&u8g2);
vTaskDelay(500);//0.5s刷新一次
/* 读按键中断队列 */
xQueueReceive(g_xQueueMenu, &key_data, 0);
if(key_data.exdata == 1)
{
buzzer_buzz(2500, 100);
vTaskResume(xShowMenuTaskHandle);
vTaskSuspend(NULL);
key_data.exdata = 0;
}
else if(key_data.ldata==1||key_data.rdata==1||key_data.updata==1)
{
memset(&key_data, 0, sizeof(Key_data));
}
}
}
1、温湿度模块初始化。
2、初始化OLED屏幕,调用 u8g2 库。
3、不断循环,读取温湿度,并把数据刷新到OLED屏幕上面。
4、直到从队列中读取到有用的数据,切换回去多级菜单界面。
ShowClockTimeTask
void ShowClockTimeTask(void *params)
{/* u8g2 Start */
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0, u8x8_byte_hw_i2c, u8g2_stm32_delay);
u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
u8g2_SetPowerSave(&u8g2, 0); // wake up display
u8g2_ClearDisplay(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese1);
// u8g2_SetFont(&u8g2, u8g2_font_spleen32x64_mf);
// u8g2_SetFont(&u8g2, u8g2_font_fur35_tf);
struct Key_data key_data;
while(1)
{
u8g2_ClearBuffer(&u8g2);
ShowClock();
//u8g2_DrawXBMP(&u8g2, 0, 0, 20, 40, BigNum[temp]);
u8g2_SendBuffer(&u8g2);
/* 读按键中断队列 */
if(clock_flag == 0)
{
xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY);
}
/* seclect */
if(key_data.rdata == 1)
{
buzzer_buzz(2500, 100);
seclect_flag++;
if(seclect_flag>4)seclect_flag=0;
}
if(key_data.ldata == 1)
{
buzzer_buzz(2500, 100);
seclect_flag--;
if(seclect_flag<0)seclect_flag=4;
}
/* handle_data */
if(key_data.updata == 1)
{
buzzer_buzz(2500, 100);
if(seclect_flag == 4)
{
/*启动定时器*/
if(g_Clock_Timer != NULL)
{
xTimerStart(g_Clock_Timer, 0);
clock_flag = 1;
key_data.updata = 0;
}
}
else{
g_clock_num[seclect_flag]++;
if(g_clock_num[seclect_flag]>9)g_clock_num[seclect_flag]=0;
}
}
if(key_data.exdata == 1)
{
buzzer_buzz(2500, 100);
clock_flag = 0;
xTimerStop(g_Clock_Timer, 0);
vTaskResume(xShowMenuTaskHandle);
vTaskSuspend(NULL);
}
/* circle_run */
if(clock_flag == 1)
{
/* time_stop */
if(g_clock_num[0]==g_real_time[0]&&g_clock_num[1]==g_real_time[1]&&g_clock_num[2]==g_real_time[2]&&g_clock_num[3]==g_real_time[3])
{
clock_flag = 0;
xTimerStop(g_Clock_Timer, 0);
memset(g_real_time, 0, sizeof(g_real_time));
/* music */
buzzer_buzz(2500, 1000);
}
}
}
}
1、初始化OLED屏幕,调用 u8g2 库。
2、显示当前的闹钟界面。
3、读取按键中断队列,等待按键中断唤醒。
4、根据不同的键值处理不同的事件。(左移、右移、确认、退出)
5、如果确认了开始计时,会启动g_Clock_Timer定时器,时间到了之后,蜂鸣器发生播报,并且回归最开始的循环。
ShowSetting_Task
void ShowSetting_Task(void)
{
u8g2_config();
u8g2_SetFont(&u8g2, u8g2_font_7x13_mf);
u8g2_FirstPage(&u8g2);
do {
ShowSetiing();
u8g2_SendBuffer(&u8g2);
} while (u8g2_NextPage(&u8g2));
for(int i = 0; i<5; i++)
{
width[i] = u8g2_GetStrWidth(&u8g2, &strs[i][10]);
}
struct Key_data key_data;while(1)
{
u8g2_ClearBuffer(&u8g2);
switch(seclect)
{
case 0: u8g2_DrawStr(&u8g2, 64, 25,"@moyiji");u8g2_DrawStr(&u8g2, 61, 50,"2021/05/27");break;
case 1: ShowSwitch(power_button);break;
case 2: ShowSwitch(power_button);break;
case 3: ShowSwitch(power_button);break;
case 4: ShowAbout();break;
}
ShowSetiing();
u8g2_SendBuffer(&u8g2);
if(seclect_end == 0)
{
pdPASS == xQueueReceive(g_xQueueMenu, &key_data, portMAX_DELAY);
}
/* move_down */
if(key_data.rdata == 1)
{
seclect_end++;
if(seclect!=4)
{
/* status_machine */
switch(seclect)
{
case 0: ui_run(&seclect_w[0], &seclect_w[1], 1);ui_run(&seclect_y[0], &seclect_y[1], 1);break;
case 1: ui_run(&seclect_w[0], &seclect_w[2], 1);ui_run(&seclect_y[0], &seclect_y[2], 1);break;
case 2: ui_run(&seclect_w[0], &seclect_w[3], 1);ui_run(&seclect_y[0], &seclect_y[3], 1);break;
case 3: ui_run(&seclect_w[0], &seclect_w[4], 1);ui_run(&seclect_y[0], &seclect_y[4], 1);break;
case 4: ui_run(&seclect_w[0], &seclect_w[5], 1);ui_run(&seclect_y[0], &seclect_y[5], 1);break;
}
}
if(seclect_end == 20)
{
if(seclect!=4)seclect++;
seclect_end = 0;
key_data.rdata = 0;
}
if(seclect_end == 0)buzzer_buzz(2500, 100);
}
/* move_up */
else if(key_data.ldata == 1)
{
seclect_end++;
if(seclect!=0)
{
switch(seclect)
{
case 0: break;
case 1: ui_run(&seclect_w[0], &seclect_w[5], 1);ui_run(&seclect_y[0], &seclect_y[5], 1);break;
case 2: ui_run(&seclect_w[0], &seclect_w[1], 1);ui_run(&seclect_y[0], &seclect_y[1], 1);break;
case 3: ui_run(&seclect_w[0], &seclect_w[2], 1);ui_run(&seclect_y[0], &seclect_y[2], 1);break;
case 4: ui_run(&seclect_w[0], &seclect_w[3], 1);ui_run(&seclect_y[0], &seclect_y[3], 1);break;
}
}
if(seclect_end == 20)
{
if(seclect!=0)seclect--;
seclect_end = 0;
key_data.ldata = 0;
}
if(seclect_end == 0)buzzer_buzz(2500, 100);
}
if(key_data.updata == 1)
{
buzzer_buzz(2500, 100);
if(seclect!=0&&seclect!=4)
{
power_button++;
power_button = power_button%2;
}
}
/* task scheduling */
if(key_data.exdata == 1)
{
buzzer_buzz(2500, 100);
vTaskResume(xShowMenuTaskHandle);
vTaskSuspend(NULL);
}
}
}
1、初始化OLED屏幕,调用 u8g2 库。
2、显示设置界面,如何读取按键中断队列,进入阻塞状态,等待唤醒。
3、根据读取到的键值进行不同的事件处理。
总结
这就是这七个任务的具体代码,我讲每个任务的主要流程给读者列举了出来,具体如何实现,需要读者细细揣摩,多看几遍就读懂了。
按键中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* key interrupt : send data to queue */
/* some data maybe useless */
extern BaseType_t end_flag;
extern BaseType_t seclect_end;
BaseType_t RM_Flag, LM_Flag, EN_Flag, EX_Flag;
Key_data key_data;
if(GPIO_Pin == GPIO_PIN_11)
{
mdelay(15);
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;
}
}
if(GPIO_Pin == GPIO_PIN_10)
{
mdelay(15);
if(end_flag == 1&&seclect_end == 0)
{
LM_Flag = 1;
key_data.ldata = LM_Flag;
xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
LM_Flag = 0;
}
}
if(GPIO_Pin == GPIO_PIN_1)
{
mdelay(15);
if(end_flag == 1&&seclect_end == 0)
{
EN_Flag = 1;
key_data.updata = EN_Flag;
xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
EN_Flag = 0;
}
}
if(GPIO_Pin == GPIO_PIN_0)
{
mdelay(15);
if(end_flag == 1&&seclect_end == 0)
{
EX_Flag = 1;
key_data.exdata = EX_Flag;
if(end_flag == 1&&seclect_end == 0)xQueueSendToBackFromISR(g_xQueueMenu, &key_data, NULL);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
EX_Flag = 0;
}
}
}
1、我们给按键中断回调函数里面加了一点点延迟,可以减小按键抖动带来的影响。
2、 往g_xQueueMenu队列里面,不断的写入按键按下的数据,这里由于队列是一位数据的,面对按键抖动的情况,在任务读取队列的时候,中断虽然会进行写队列,但是都是不成功的,有效的减少了按键抖动。
蜂鸣器实现按键音效
buzzer_init
void buzzer_init(void)
{
/* 初始化蜂鸣器 */
PassiveBuzzer_Init();
/* 创建定时器 */g_TimerSound = xTimerCreate( "SystemSound",
200,
pdFALSE,
NULL,
SystemSoundTimer_Func);
}
1、初始化蜂鸣器,将蜂鸣器引脚配置为定时器pwm输出模式,因为无源蜂鸣器的发声,需要给特定的频率的pwm。
2、创建定时器 SystemSound,实现如何只b一声,实现按键的音效。
SystemSoundTimer_Func( TimerHandle_t xTimer )
static void SystemSoundTimer_Func( TimerHandle_t xTimer )
{
PassiveBuzzer_Control(0);
}
1、这个定时器的任务就是关闭pwm生成,从而导致蜂鸣器停止发生。
void buzzer_buzz(int freq, int time_ms)
void buzzer_buzz(int freq, int time_ms)
{
if(power_button == 0)
{
PassiveBuzzer_Set_Freq_Duty(freq, 50);
/* 启动定时器 */
xTimerChangePeriod(g_TimerSound, time_ms, 0);
}}
1、如果 power_button == 0,我们就可以触发按键音效,这个值可以在设置菜单里面设置。
2、我们先开启pwm生成,这个时候蜂鸣器会叫,同时开启了g_TimerSound定时器,0.2s之后就会去关闭掉蜂鸣器,从而实现了按键音效的功能。
总结
这就是我这篇基于FreeRTOS的STM32多功能手表的博客,这是个十分简单的小项目,如果你很了解FreeRTOS,并且裸机开发过类似的功能,那不需要一天,你就能完美复刻我这个小项目。