在成功使用上一章节的RCT时钟后,基于FreeRTOS来实现简单的时钟功能,我将功能划分如下:主界面、选择菜单、时间、闹钟、地区各设置选项。
在做好任务的构思后,先来理清各个任务之间的触发关系。在菜单中的操作命令:上移、下移、进入、返回由按键通过外部中断触发,在触发并接收命令后,执行各任务流程,刷新OLED,修改时钟、闹钟设置,修改地区设置并通过WiFi通知服务器修改要获取天气位置。
因为菜单选项较多,因此引入Free RTOS中的队列来用于记录当前菜单位置,需引入"queue.h"。菜单的结构分为两个部分来定义更清楚,第一部分是菜单中的某个选项与他对应的下一级菜单之间的联系,比如闹钟选项与其子菜单的关系。第二部分是当前菜单中相关的设置,menu_index用于说明当前菜单页中,被选中的菜单项下标(0 开始),menu_item_count则是说明当前菜单页包含的菜单项总数。
typedef struct menu_item_s { const char *next_menu_name; void (*action)(void); // 可选:确认时执行的动作 const struct menu_item_s *next_items; // 子菜单数组 uint8_t next_count; // 子菜单项数 }menu_item_t; typedef struct { const menu_item_t *menu_items; uint8_t menu_index; uint8_t menu_item_count; }menu_page_t; extern menu_page_t menu_stack[MENU_STACK_MAX]; extern xQueueHandle xQueueKey; //示例 ,这里主菜单中选择闹钟菜单-》对应的闹钟子菜单中有4个选项 const menu_item_t main_menu[] = { {"Time", NULL, time_menu, 2}, {"Alarm", NULL, alarm_menu, 4}, {"Area", NULL, area_menu, 1}, }; const menu_item_t alarm_menu[] = { {"Alarm Add", alarm_add_page, NULL, 0}, {"Alarm Delete", alarm_delete_page, NULL, 0}, {"Alarm Enable", alarm_enable_page, NULL, 0}, {"Alarm Disable", alarm_disable_page, NULL, 0}, };这里因为涉及到按键和OLED屏两个硬件设备,因此我将任务划分为两部分,按键任务用于检测是否有按键中断,显示任务负责接收按键的时间信息并通知执行任务。这里还是使用的轮询扫描,我明天再看看能不能优化一下直接用中断发送信号量来获取。
void key_task(void *pvParameters) { key_event_t evt; while (1) { evt = KEY_Scan(); if (evt != NO_PRESS) { xQueueSend(xQueueKey, &evt, 0); } vTaskDelay(pdMS_TO_TICKS(20)); // 去抖 } } void oledmenu_task(void *pvParameters) { key_event_t evt; main_window_render(); // 开机先显示一次 while (1) { if (xQueueReceive(xQueueKey, &evt, pdMS_TO_TICKS(500)) == pdTRUE) { if(menu_top == -1) { // 如果栈为空,按下任意键进入主菜单 menu_push(main_menu, sizeof(main_menu)/sizeof(main_menu[0])); menu_render(); continue; } menu_key_handler(evt); menu_render(); } else{ // 没有按键事件,栈为空,刷新主界面时间显示 if (menu_top == -1) { main_window_render(); } } } }和菜单的栈相关函数如下,主要需要考虑的就是出栈、入栈以及相关函数使用时存在的边界问题,发生越界会导致程序崩溃。
void menu_push(const menu_item_t *items, uint8_t count) { if (menu_top < MENU_STACK_MAX - 1) { menu_top++; menu_stack[menu_top].menu_items = items; menu_stack[menu_top].menu_index = 0; menu_stack[menu_top].menu_item_count = count; } } void menu_pop(void) { if (menu_top >= 0) { menu_top--; } } menu_page_t *menu_current(void)//当前菜单 { if (menu_top >= 0) { return &menu_stack[menu_top]; } return NULL; } void menu_render(void)//渲染选择菜单 { menu_page_t *p = menu_current(); if (p == NULL) return; // 栈为空,直接返回 OLED_Clear(); for (int i = 0; i < p->menu_item_count; i++) { char line[24]; sprintf(line, "%c %s", (i == p->menu_index) ? '>' : ' ', p->menu_items[i].next_menu_name); OLED_ShowString(0, i*24, (u8*)line, 16); } OLED_Refresh(); } void menu_key_handler(key_event_t key) { menu_page_t *p = menu_current(); if (p == NULL) return; switch (key) { case KEY_UP: if (p->menu_index > 0) {p->menu_index--;} break; case KEY_DOWN: if (p->menu_index < p->menu_item_count - 1) p->menu_index++; break; case KEY_OK: if (p->menu_items[p->menu_index].action) p->menu_items[p->menu_index].action(); if (p->menu_items[p->menu_index].next_items) menu_push(p->menu_items[p->menu_index].next_items, p->menu_items[p->menu_index].next_count); break; case KEY_BACK: menu_pop(); if (menu_top == -1) main_window_render(); break; case NO_PRESS: break; } }实现效果如图所示。
总结:栈的结构确实很适合来做菜单的任务。
改进:只做了还没完全实现功能,可以通过加入WiFi来获取在线的准确时间以及天气。