基于FreeRTOS的STM32智能手表开发详解

前言

        在嵌入式系统开发中,实时操作系统的引入为多任务协调、资源管理提供了高效且可靠的解决方案。出于对实时系统运行逻辑的好奇,以及系统性掌握嵌入式开发核心技能的目标并整合自身所学的知识,我以stm32系列微控制器为硬件平台,结合FreeRTOS的核心功能(如任务调度、队列通信、中断管理及信号量同步),设计并实现了一款多功能智能手表。

一、项目介绍

        基于FreeRTOS的stm32智能手表,支持时间显示、实时天气(显示所在地区以及相应的天气情况)、模拟闹钟、模拟手电、万年历、挡球板游戏。使用FreeRTOS统一管理各个任务。支持方便地扩展功能(仅需添加自己想要实现的功能对应的任务函数即可)。

二、FreeRTOS概述

        FreeRTOS是一款专为嵌入式系统设计的完全免费的轻量级实时操作系统,其源代码公开、可移植可裁剪,调度策略灵活。内核采用抢占式调度策略,支持优先级划分与时间片轮转动态任务切换提升CPU利用率。

        主要功能包括:任务管理、时间管理、中断管理、内存管理等。任务管理方面,任务间通信与同步依赖队列、信号量、互斥量等机制,其中队列实现数据安全传递,二值信号量常用于保护临界资源,防止多任务竞争导致的状态混乱。中断管理方面,FreeRTOS允许在中断中通过FreeRTOS提供的中断安全函数快速唤醒任务,将非实时操作延迟至任务上下文执行,从而平衡实时性与复杂逻辑处理。内存管理方面支持静态与动态分配模式,动态内存分配在运行时通过pvPortMalloc等函数按需分配内存,使用方便但可能产生碎片,静态内存分配在编译时预先确定内存位置和大小,避免运行时碎片问题,但需手动管理内存布局。此外,FreeRTOS提供软件定时器、事件组等扩展功能,进一步简化周期性任务与多条件状态机的实现。

三、硬件部分

硬件准备

  • STM32最小系统板

  • USB转TTL模块(用于串口收发信息)

  • WIFI模块(用于获取数据,例如天气信息)

  • OLED显示屏

  • MPU6050

  • 按钮(可选,可通过串口发送数据替代按钮事件)

  • 硬件连线

    MCU USB转TTL
    5V VCC
    GND GND
    USART1_RX TX
    USART2_TX RX
    MCU ESP8266
    3.3V VCC
    GND GND
    USART2_RX TX
    USART2_TX RX
    3.3V IO
    3.3V RST
    MCU OLED显示屏
    3.3V VCC
    GND GND
    I2C1_SCL SCL
    I2C1_SDA SDA
    MCU MPU6050
    3.3V VCC
    GND GND
    I2C2_SCL SCL
    I2C2_SDA SDA

    四、项目所涉及的知识

  • 任务管理:任务的创建、删除以及状态转换。

  • 队列:用于各个任务之间数据的传递。

  • 二值信号量:用于保护OLED外设等各种临界资源。

  • 中断管理:实时响应与任务解耦,用于接收串口数据。

  • 定时器非阻塞式扫描按键。

  • 任务管理

            FreeRTOS的任务管理通过动态或静态方式创建任务,每个任务拥有独立栈空间与优先级。任务状态包括就绪、运行、阻塞、挂起等,调度器根据优先级与时间片轮转切换任务。例如,调用xTaskCreate()动态分配内存创建任务,而vTaskDelete()可显式删除任务以释放资源。使用任务挂起函数(vTaskSuspend())与任务恢复(vTaskResume())灵活控制执行流程。

     队列

            队列是FreeRTOS中任务间通信的核心机制,基于FIFO原则传递数据,支持结构体、指针等复杂类型。通过xQueueSend()将数据写入队列,通过xQueueReceive()读取数据。队列长度与数据项大小在创建时定义,阻塞等待机制(如超时参数)可避免资源竞争。

    二值信号量

            二值信号量用于任务同步或临界资源保护,本质是仅含一个消息的队列,状态为“空”或“满”。例如,创建信号量后,中断服务例程(ISR)通过xSemaphoreGiveFromISR()释放信号量,触发任务执行临界区代码(如OLED刷新)。任务需通过xSemaphoreTake()获取信号量,用于确保同一时刻仅一个任务访问共享资源。

    中断管理

            FreeRTOS通过中断服务例程(ISR)与任务同步实现硬件事件的实时响应。例如,串口接收中断触发后,ISR通过xQueueSendFromISR()将数据存入队列并唤醒处理任务,避免在ISR中执行耗时操作。这种设计既保证中断的快速响应,又通过任务解耦复杂逻辑(如数据解析),提升系统实时性。此外,FreeRTOS提供中断安全API(如xSemaphoreGiveFromISR()),确保在ISR中安全操作信号量或队列,防止资源竞争。

    软件定时器

            FreeRTOS的软件定时器通过守护任务(Daemon Task) 和队列 实现周期性或单次任务触发,无需占用硬件定时器资源。例如,通过xTimerCreate()创建定时器后,可设置周期模式(如pdTRUE)实现每秒触发一次回调函数,用于屏幕刷新或数据采集。回调函数在守护任务上下文中执行,需避免阻塞操作(如vTaskDelay()),否则会影响其他定时器。此外,定时器的启动(xTimerStart())、停止(xTimerStop())等操作通过队列传递至守护任务统一处理,确保线程安全。

    五、代码实现思路

            在FreeRTOS初始化时分别创建显示时间任务、菜单任务以及剩余五个功能任务,然后还需要创建两个软件定时器任务,第一个定时器任务用于更新时间,第二个定时器任务用于实现非阻塞是扫描按键,当按下按键时会向队列中写入按键被按下的消息,也可以通过向串口发送特定数据可向队列中写入按键消息,各个任务在读取到按键消息之后做出特定响应,例如在按键触发任务切换时,当前任务唤醒指定任务并挂起自身。

    代码讲解

    FreeRTOS初始化

            创建显示时间任务、菜单任务以及剩余五个功能任务。优先级均为普通优先级,栈大小均为128*4字节。需要特别注意的是CMSIS-FreeRTOS里的栈大小(.stack_size)的单位为字节,CMSIS内部会将栈大小除以4,因为STM32的栈宽度为4字节,以得到底层的FreeRTOS真正所需的栈深度。所以最终每个任务分配到了128个字(即128*4字节)的栈空间。

    osThreadId_t clock_task_handle;
    const osThreadAttr_t clock_task_attr = {
        .name = FUNC_NAME_TO_STR(Watch_ClockTask),
        .priority = (osPriority_t)osPriorityNormal,
        .stack_size = 128 * 4};
    
    osThreadId_t count_down_timer_task_handle;
    const osThreadAttr_t count_down_timer_task_attr = {
        .name = FUNC_NAME_TO_STR(Watch_CountDownTimerTask),
        .priority = (osPriority_t)osPriorityNormal,
        .stack_size = 128 * 4};
    
    osThreadId_t torch_task_handle;
    const osThreadAttr_t torch_task_attr = {
        .name = FUNC_NAME_TO_STR(Watch_TorchTask),
        .priority = (osPriority_t)osPriorityNormal,
        .stack_size = 128 * 4};
    
    osThreadId_t cal_task_handle;
    const osThreadAttr_t cal_task_attr = {
        .name = FUNC_NAME_TO_STR(Watch_WeatherTask),
        .priority = (osPriority_t)osPriorityNormal,
        .stack_size = 128 * 4};
    
    osThreadId_t weather_task_handle;
    const osThreadAttr_t weather_task_attr = {
        .name = FUNC_NAME_TO_STR(Watch_CalendarTask),
        .priority = (osPriority_t)osPriorityNormal,
        .stack_size = 128 * 4};
    
    osThreadId_t game_page_task_handle;
    const osThreadAttr_t game_task_attr = {.name = FUNC_NAME_TO_STR(Watch_GamePage),
                                           .priority =
                                               (osPriority_t)osPriorityNormal,
                                           .stack_size = 128 * 4};
    
    osThreadId_t game1_task_handle;
    const osThreadAttr_t game1_task_attr = {.name = FUNC_NAME_TO_STR(Game1Task),
                                            .priority =
                                                (osPriority_t)osPriorityNormal,
                                            .stack_size = 128 * 4};
    
    osThreadId_t main_page_task_handle;
    const osThreadAttr_t main_task_attr = {.name = FUNC_NAME_TO_STR(Watch_MainPage),
                                           .priority = osPriorityNormal,
                                           .stack_size = 128 * 4};
    
    osMessageQueueId_t key_event_que;
    const osMessageQueueAttr_t key_event_que_attr = {
        .name = "KeyEventQueue",
        .attr_bits = 0,
        .cb_mem = NULL,
        .cb_size = 0,
        .mq_mem = NULL,
        .mq_size = 0,
    };
    
    
    clock_task_handle = osThreadNew(Watch_ClockTask, NULL, &clock_task_attr);
    count_down_timer_task_handle = osThreadNew(Watch_CountDownTimerTask, NULL,
                                               &count_down_timer_task_attr);
    torch_task_handle = osThreadNew(Watch_TorchTask, NULL, &torch_task_attr);
    cal_task_handle = osThreadNew(Watch_CalendarTask, NULL, &cal_task_attr);
    weather_task_handle =
         osThreadNew(Watch_WeatherTask, NULL, &weather_task_attr);
    game1_task_handle = osThreadNew(Game1Task, NULL, &game1_task_attr);
    game_page_task_handle = osThreadNew(Watch_GamePage, NULL, &game_task_attr);
    main_page_task_handle = osThreadNew(Watch_MainPage, NULL, &main_task_attr);
    
    osThreadSuspend(count_down_timer_task_handle);
    osThreadSuspend(torch_task_handle);
    osThreadSuspend(cal_task_handle);
    osThreadSuspend(weather_task_handle);
    osThreadSuspend(game1_task_handle);
    osThreadSuspend(game_page_task_handle);
    osThreadSuspend(main_page_task_handle);

            需要定时器非阻塞式扫描案件的话还需要在创建一个定时器用于扫描按键。需要特别注意的是定时器默认优先级很低,你需要到FreeRTOSConfig.h文件中通过修改configTIMER_TASK_PRIORITY宏定义手动提高软件定时器优先级,高于所有创建的任务。

    osTimerId_t keyscan_task_handle;
    const osTimerAttr_t keyscan_task_attr = {
        .name = FUNC_NAME_TO_STR(KeyEvent_Handler),
        .attr_bits = 0,
        .cb_mem = NULL,
        .cb_size = 0,
    };
    
    keyscan_task_handle =
        osTimerNew(Watch_ClockTask, osTimerPeriodic, NULL, &keyscan_task_attr);
    
    
    void KeyEvent_Handler(void) {
        KeyEvent_t event;
        KeyHandle_t *hkey = NULL;
        const KeyConfig_t *const_kc = NULL;
    
        for (size_t i = 0; i < key_nums; i++) {
            hkey = (keys + i);
            const_kc = &hkey->key;
    
            /**
             * If the key is pressed, update prev_state to activation_level,
             * and determine whether it's a press or long press event.
             */
            if (KEY_READSTATE(const_kc->key_port, const_kc->key_pin)
                == const_kc->activation_level) {
                /**
                 * Before debouncing starts, initialize filter_timer with a
                 * threshold value, as it has already been initialized to 0.
                 */
                if (hkey->filter_timer < const_kc->filter_threshold) {
                    hkey->filter_timer = const_kc->filter_threshold;
                }
                else if (hkey->filter_timer < 2 * const_kc->filter_threshold) {
                    hkey->filter_timer++;
                }
                else {
                    // Transition from inactive to active state indicates a key
                    // press.
                    if (hkey->prev_state != const_kc->activation_level) {
                        event.key_id = const_kc->key_id;
                        event.key_status = KEY_PRESSED;
                        Key_PushEvent(event);
                    }
    
                    if (const_kc->long_press_threshold > 0) {
                        if (hkey->long_press_timer
                            < const_kc->long_press_threshold) {
                            // If the key remains active for longer than the
                            // specified time, it's considered a long press.
                            if (++hkey->long_press_timer
                                    >= const_kc->long_press_threshold
                                && hkey->prev_state == const_kc->activation_level) {
                                event.key_id = const_kc->key_id;
                                event.key_status = KEY_LONG_PRESSED;
                                Key_PushEvent(event);
                            }
                        }
                    }
                    hkey->prev_state = const_kc->activation_level;
                }
            }
            else {
                if (hkey->filter_timer > const_kc->filter_threshold) {
                    hkey->filter_timer = const_kc->filter_threshold;
                }
                else if (hkey->filter_timer > 0) {
                    hkey->filter_timer--;
                }
                else {
                    if (hkey->prev_state == hkey->key.activation_level) {
                        hkey->long_press_timer = 0;
    
                        event.key_id = hkey->key.key_id;
                        event.key_status = KEY_RELEASED;
                        Key_PushEvent(event);
                    }
                    hkey->prev_state = !(hkey->key.activation_level);
                }
            }
        }
    }

    串口中断回调函数

            用于使用串口发送数据替代按键操作。

    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
        if (huart->Instance == USART1) {
            KeyEvent_t key_event;
            if ((KeyId_t)uart_rx_buf[0] == KEY_ENTER) {
                key_event.key_id = KEY_ENTER;
                key_event.key_status = KEY_PRESSED;
                osMessageQueuePut(key_event_que, (void *)&key_event, 0, 0);
            }
            else if ((KeyId_t)uart_rx_buf[0] == KEY_EXIT) {
                key_event.key_id = KEY_EXIT;
                key_event.key_status = KEY_PRESSED;
                osMessageQueuePut(key_event_que, (void *)&key_event, 0, 0);
            }
            else if ((KeyId_t)uart_rx_buf[0] == KEY_UP) {
                key_event.key_id = KEY_UP;
                key_event.key_status = KEY_PRESSED;
                osMessageQueuePut(key_event_que, (void *)&key_event, 0, 0);
            }
            else if ((KeyId_t)uart_rx_buf[0] == KEY_DOWN) {
                key_event.key_id = KEY_DOWN;
                key_event.key_status = KEY_PRESSED;
                osMessageQueuePut(key_event_que, (void *)&key_event, 0, 0);
            }
            else if ((KeyId_t)uart_rx_buf[0] == KEY_LEFT) {
                key_event.key_id = KEY_LEFT;
                key_event.key_status = KEY_PRESSED;
                osMessageQueuePut(key_event_que, (void *)&key_event, 0, 0);
            }
            else if ((KeyId_t)uart_rx_buf[0] == KEY_RIGHT) {
                key_event.key_id = KEY_RIGHT;
                key_event.key_status = KEY_PRESSED;
                osMessageQueuePut(key_event_que, (void *)&key_event, 0, 0);
            }
            HAL_UART_Receive_IT(&huart1, uart_rx_buf, 1);
        }
        else if (huart->Instance == USART2) {
            /*接收esp8266返回的数据,此处省略。*/
        }
    }

    显示菜单任务

    MenuItem_t items[] = {{.name = "weather",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = weather_task_handle},
                          {.name = "calendar",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = cal_task_handle},
                          {.name = "torch",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = torch_task_handle},
                          {.name = "count down timer",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = count_down_timer_task_handle},
                          {.name = "game",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = game_page_task_handle},
                          {.name = "music",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = NULL},
                          {.name = "setting",
                           .cb_func = Watch_SwitchToSelectedItemTask,
                           .cb_args = NULL}};
    MenuPageHandle_t hpage = {
        .page = {.title = "Main Page",
                 .items = items,
                 .num_items = sizeof(items) / sizeof(items[0])},
        .curr_selected_item_index = 0,
        .wheel_dis = 0,
        .is_running = 1};
    {
        const uint8_t num_items = hpage.page.num_items;
        hpage.page.__title_len = strlen(hpage.page.title);
        for (uint8_t i = 0; i < num_items; i++) {
            hpage.page.items[i].__name_len = strlen(hpage.page.items[i].name);
        }
    }
    while (1) {
        Watch_UpdatePage(&hpage);
        osMessageQueueGet(key_event_que, (void *)&key_event, NULL,
                          osWaitForever);
        if (key_event.key_id == KEY_UP && key_event.key_status == KEY_PRESSED) {
            hpage.wheel_dis -= MENU_ITEM_TOTAL_HEIGHT;
        }
        else if (key_event.key_id == KEY_DOWN
                 && key_event.key_status == KEY_PRESSED) {
            hpage.wheel_dis += MENU_ITEM_TOTAL_HEIGHT;
        }
        else if (key_event.key_id == KEY_ENTER
                 && key_event.key_status == KEY_PRESSED) {
            MenuItemCallbackFunc_t cb_func =
                hpage.page.items[hpage.curr_selected_item_index].cb_func;
            if (cb_func != NULL) {
                cb_func(
                    hpage.page.items[hpage.curr_selected_item_index].cb_args);
            }
        }

    其他功能函数

            其他功能函数的由于较多,在这里仅举两个例子,分别是日历功能函数和获取天气情况的函数。日历功能函数因为和大部分功能函数逻辑相近,仅讲一个大家也可以举一反三,而获取天气功能函数由于是通过esp8266获取网络天气数据,比较有意思,故值得单独拉出来讲解。

    日历功能函数
    static uint8_t Watch_CalendarGetDaysInMonth(uint16_t year, uint8_t month);
    static uint8_t Watch_CalendarGetFirstDayOfWeek(uint16_t year, uint8_t month);
    
    void Watch_CalendarTask() {
        static const char *const month_day_to_str[] = {
            /* 0 is a place holder.*/
            "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"};
        static const char *const cal_header[] = {"Su", "Mo", "Tu", "We",
                                                 "Th", "Fr", "Sa"};
        static const uint8_t cal_elem_posx[] = {0, 17, 34, 51, 68, 85, 102};
        static const uint8_t cal_elem_posy[] = {17, 26, 35, 44, 53};
    
        uint16_t curr_year = /*在这里获取当前的年份,可通过读取网络时间或者实时时钟*/;
        uint8_t curr_month = /*在这里获取当前的月份*/;
        uint8_t curr_day = /*在这里获取当前的天数*/;
    
        uint16_t show_year = curr_year;
        uint8_t show_month = curr_month;
        while (1) {
            OLED_Clear();
    
            /*绘制日历头*/
            for (uint8_t i = 0; i < 7; i++) {
                OLED_ShowString(cal_elem_posx[i], 8, (char *)cal_header[i],
                                OLED_6X8);
            }
    
            /*绘制当前月的月历*/
            uint8_t day_in_month =
                Watch_CalendarGetDaysInMonth(show_year, show_month);
            uint8_t day_of_week =
                Watch_CalendarGetFirstDayOfWeek(show_year, show_month);
            uint8_t day_elem_posy_index = 0;
            for (uint8_t i = 1; i <= day_in_month; i++) {
                OLED_ShowString(cal_elem_posx[day_of_week],
                                cal_elem_posy[day_elem_posy_index],
                                (char *)month_day_to_str[i], OLED_6X8);
                if (curr_year == show_year && curr_month == show_month
                    && curr_day == i) {
                    OLED_ReverseArea(cal_elem_posx[day_of_week],
                                     cal_elem_posy[day_elem_posy_index], 8, 6);
                }
                day_of_week++;
                if (day_of_week >= 7) {
                    day_of_week = 0;
                    day_elem_posy_index++;
                }
            }
    
            OLED_Update();
    
            osMessageQueueGet(key_event_que, (void *)&key_event, NULL,
                              osWaitForever);
            if ((key_event.key_id == KEY_RIGHT || key_event.key_id == KEY_DOWN)
                && key_event.key_status == KEY_PRESSED) {
                show_month++;
                if (show_month > 12) {
                    show_year++;
                    show_month = 1;
                }
            }
            else if ((key_event.key_id == KEY_LEFT || key_event.key_id == KEY_UP)
                     && key_event.key_status == KEY_PRESSED) {
                show_month--;
                if (show_month == 0) {
                    show_year--;
                    show_month = 12;
                }
            }
            else if (key_event.key_id == KEY_EXIT
                     && key_event.key_status == KEY_PRESSED) {
                osThreadResume(main_page_task_handle);
                osThreadSuspend(osThreadGetId());
            }
        }
    }
    
    static uint8_t Watch_CalendarGetDaysInMonth(uint16_t year, uint8_t month) {
        switch (month) {
        case 1:  // January
        case 3:  // March
        case 5:  // May
        case 7:  // July
        case 8:  // August
        case 10: // October
        case 12: // December
            return 31;
        case 4:  // April
        case 6:  // June
        case 9:  // September
        case 11: // November
            return 30;
        case 2: // February
            if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
                return 29; // Leap year
            }
            else {
                return 28; // Non-leap year
            }
        default:
            return 0; // Invalid month
        }
    }
    
    static uint8_t Watch_CalendarGetFirstDayOfWeek(uint16_t year, uint8_t month) {
        if (month < 3) {
            month += 12;
            year -= 1;
        }
    
        uint16_t k = year % 100;
        uint16_t j = year / 100;
    
        uint16_t f = (1 + 13 * (month + 1) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
    
        // Zeller's Congruence 返回 0 表示星期六,6 表示星期五
        // 这里需要调整为 0 表示星期日,6 表示星期六
        return (f + 6) % 7;
    }
    获取天气思路

            心知天气提供免费的获取天气数据的api,注册完帐号之后可以在控制台获取免费版产品对应的公钥和私钥,然后将私钥填写在api接口(API接口说明)中的your_api_key处,当你想查询某地天气时,将城市对应的拼音填写在location处即可返回对应城市的天气情况。

            API接口返回json格式数据,我们只需要使用cJSON或者其他的json数据解析库解析即可获取天气数据。

            这里我使用ESP8266的AT指令完成获取天气数据的功能,使用cJSON解析api返回的数据。

    /**
     * @brief 发送AT指令给ESP8266
     *
     * @param cmd AT指令
     * @param expected_ack
     * @param wait_ticks 等待esp8266返回数据的时间
     * @return int8_t 当esp8266返回的数据中包含expected_ack时,返回0,否则返回-1。expected_ack为NULL时始终返回0。
     */
    int8_t ESP8266_SendCmd(const char *const cmd, const char *const expected_ack,
                           uint32_t wait_ticks) {
        HAL_UART_AbortReceive_IT(&huart2);
        memset(esp_rx_buf, 0, esp_rx_write_pos);
        esp_rx_write_pos = 0;
        HAL_UART_Receive_IT(&huart2, esp_rx_buf, 1);
        osDelay(wait_ticks);
        HAL_UART_AbortReceive_IT(&huart2);
        esp_rx_buf[esp_rx_write_pos] = '\0';
    
        if (expected_ack == NULL) {
            return 0;
        }
        else {
            if (strstr((char *)esp_rx_buf, expected_ack) == NULL) {
                return -1;
            }
            else {
                return 0;
            }
        }
        return 0;
    }
    
    static void Watch_WeatherTaskInitESP8266();
    static WeatherRes_t Watch_GetCurrentWeather();
    static WeatherRes_t Watch_ParseWeatherData(char *json);
    
    void Watch_WeatherTask() {
        OLED_Clear();
        OLED_ShowString(40, 40, "Loading...", OLED_8X16);
        OLED_Update();
        Watch_WeatherTaskInitESP8266();
        WeatherRes_t curr_weather = Watch_GetCurrentWeather();
        while (1) {
            OLED_Clear();
            OLED_ShowImage(40, 0, 48, 48, weather_img[(int)curr_weather.code]);
            OLED_Printf(14, 54, OLED_6X8, "%s %s", curr_weather.country,
                        curr_weather.weather_now);
            OLED_Update();
            osMessageQueueGet(key_event_que, (void *)&key_event, NULL,
                              osWaitForever);
            if (key_event.key_id == KEY_EXIT
                && key_event.key_status == KEY_PRESSED) {
                osThreadResume(main_page_task_handle);
                osThreadSuspend(osThreadGetId());
            }
        }
    }
    
    static void Watch_WeatherTaskInitESP8266() {
        while (ESP8266_SendCmd("AT\r\n", "OK", 500)) {
        }
        ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK", 500);
        ESP8266_SendCmd("AT+RST\r\n", "ready", 3000);
        ESP8266_SendCmd("AT+CIPMUX=0\r\n", "OK", 500);
        ESP8266_SendCmd("AT+CWJAP=\"ssid\",\"passwd\"\r\n", "WIFI GOT IP",
                        3000);
    }
    
    static WeatherRes_t Watch_GetCurrentWeather() {
        while (ESP8266_SendCmd("AT+CIPSTART=\"TCP\",\"api.seniverse.com\",80\r\n",
                               "OK", 2000)) {
        }
        ESP8266_SendCmd("AT+CIPMODE=1\r\n", "OK", 500);
        while (ESP8266_SendCmd("AT+CIPSEND\r\n", ">", 500)) {
        }
        ESP8266_SendCmd("GET https://api.seniverse.com/v3/weather/"
                        "now.json?key=your_api_keylocation=hangzhou&"
                        "language=en&unit=c\r\n",
                        NULL, 1000);
    
        return Watch_ParseWeatherData((char *)esp_rx_buf);
    }
    
    static WeatherRes_t Watch_ParseWeatherData(char *json) {
        WeatherRes_t res = {.country = "UNKNOWN", .code = UNKNOWN};
    
        cJSON *root = cJSON_Parse(json);
        if (root == NULL) {
            goto Parse_Err;/*goto慎用不是不能用嗷。*/
        }
        else {
            cJSON *result = cJSON_GetObjectItem(root, "results");
            if (result == NULL) {
                goto Parse_Err;
            }
            cJSON *arr_item = cJSON_GetArrayItem(result, 0);
            if (arr_item == NULL) {
                goto Parse_Err;
            }
    
            cJSON *location = cJSON_GetObjectItem(arr_item, "location");
            if (location == NULL) {
                goto Parse_Err;
            }
    
            cJSON *name = cJSON_GetObjectItem(location, "name");
            if (name == NULL) {
                goto Parse_Err;
            }
            else {
                strncpy(res.country, name->valuestring, 19);
            }
    
            cJSON *now = cJSON_GetObjectItem(arr_item, "now");
            if (now == NULL) {
                goto Parse_Err;
            }
    
            cJSON *text = cJSON_GetObjectItem(now, "text");
            if (text == NULL) {
                goto Parse_Err;
            }
            else {
                strncpy(res.weather_now, text->valuestring,
                        sizeof(res.weather_now));
            }
    
            cJSON *code = cJSON_GetObjectItem(now, "code");
            if (code == NULL) {
                goto Parse_Err;
            }
            else {
                res.code = (WeatherCode_t)(atoi(code->valuestring));
            }
        }
    Parse_Err:
        cJSON_Delete(root);
        return res;
    }

    六、程序现象

            由于当初学习重点在于学习FreeRTOS并整合自己的代码,所以图方便用了自己之前写的一个简陋的菜单驱动,程序的UI界面做的比较简陋。后续会不断学习,并使用其他图形驱动库(例如u8g2)。

    时钟界面

    时钟界面

    程序菜单

    程序菜单

    天气界面

    天气界面

    日历界面

    模拟手电界面

    倒计时界面

    打砖块游戏界面

            可使用按键操控挡板,也可用mpu6050操控挡板。


    总结

            感谢您看到最后,这是我的第一篇blog。可能有些地方我写得不是特别好,有些地方可能表达得不够清楚不够仔细,也希望您能留下一些小建议。

            最后本次的小项目对我FreeRTOS的学习有举足轻重的帮助,通过任务调度、队列通信、中断同步等FreeRTOS的机制,我实现了智能手表的中各个任务的协同工作,也让我看到了RTOS在复杂嵌入式场景中的优势,并且它的移植也非常方便,以需要修改部分接口函数,配置好FreeRTOS的各个参数就可以非常方便地移植好。

    开源地址:待上传。

    作者:2501_91184823

    物联沃分享整理
    物联沃-IOTWORD物联网 » 基于FreeRTOS的STM32智能手表开发详解

    发表回复