基于FreeRTOS的STM32多功能手表

前言

项目背景

项目演示

使用到的硬件

项目原理图

目前版本实现的功能

设计到的freertos知识

实现思路

代码讲解

初始化GPIO引脚、配置时钟

蜂鸣器初始化以及软件定时器创建

系统默认创建的defaultTaskHandle

创建七个Task,代表七个功能

ShowTimeTask

ShowMenuTask

ShowCalendarTask

ShowFlashLightTask

ShowDHT11Task

ShowClockTimeTask

ShowSetting_Task

总结

按键中断回调函数

蜂鸣器实现按键音效

buzzer_init

[SystemSoundTimer_Func( TimerHandle_t xTimer )](#SystemSoundTimer_Func( TimerHandle_t xTimer ))

[void buzzer_buzz(int freq, int time_ms)](#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,并且裸机开发过类似的功能,那不需要一天,你就能完美复刻我这个小项目。

相关推荐
嵌入式大圣1 小时前
STM32 单片机最小系统全解析
stm32·单片机·嵌入式硬件
LN花开富贵6 小时前
stm32g431rbt6芯片中VREF+是什么?在电路中怎么设计?
笔记·stm32·单片机·嵌入式硬件·学习
qq21084629536 小时前
【stm32笔记】使用rtt-studio与stm32CubeMx联合创建项目
笔记·stm32·嵌入式硬件
CV金科6 小时前
蓝桥杯—STM32G431RBT6按键的多方式使用(包含软件消抖方法精讲)从原理层面到实际应用(一)
stm32·单片机·嵌入式硬件·蓝桥杯
2021.096 小时前
五、CAN总线
嵌入式硬件
luckyluckypolar6 小时前
STM32——输入捕获
stm32·单片机·嵌入式硬件·物联网
hong1616886 小时前
嵌入式硬件基础知识
嵌入式硬件
hai405876 小时前
单片机(Microcontroller)原理及应用
单片机·嵌入式硬件
jun7788957 小时前
嵌入式硬件基础知识
嵌入式硬件
Projectsauron8 小时前
STM32 芯片启动过程
stm32·单片机·芯片启动过程