HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

RTOS实时系统

RTOS基础概念

什么是实时操作系统

实时操作系统(RTOS)保证任务在确定时间内响应,分为两类:

  • 硬实时: 必须在规定时间内完成(如安全气囊,飞控系统)
  • 软实时: 尽量在规定时间内完成(如多媒体播放)

RTOS vs 裸机开发:

特性裸机RTOS
开发难度低中
任务管理手动轮询自动调度
响应时间不确定可预测
代码复用低高
内存占用小(0KB)大(5-20KB)
适用场景简单控制复杂系统

任务调度算法

抢占式调度(Preemptive): 高优先级任务可随时打断低优先级任务,适合实时性要求高的场景。

协作式调度(Cooperative): 任务主动让出CPU,简单但响应慢。

// FreeRTOS任务状态转换
// Running -> Ready: 更高优先级任务就绪
// Running -> Blocked: 等待资源(延时/信号量)
// Running -> Suspended: 被挂起
// Blocked -> Ready: 资源可用/延时到期

FreeRTOS任务管理

任务创建

#include "FreeRTOS.h"
#include "task.h"

// 任务句柄
TaskHandle_t led_task_handle;
TaskHandle_t key_task_handle;

// LED任务
void LED_Task(void *pvParameters) {
    while (1) {
        LED_TOGGLE();
        vTaskDelay(pdMS_TO_TICKS(500));  // 延时500ms
    }
}

// 按键任务
void Key_Task(void *pvParameters) {
    while (1) {
        if (key_scan()) {
            printf("Key pressed!\n");
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // 10ms轮询
    }
}

int main(void) {
    // 创建任务
    xTaskCreate(
        LED_Task,           // 任务函数
        "LED",              // 任务名称
        128,                // 栈大小(字)
        NULL,               // 参数
        2,                  // 优先级
        &led_task_handle    // 任务句柄
    );

    xTaskCreate(Key_Task, "KEY", 128, NULL, 3, &key_task_handle);

    // 启动调度器
    vTaskStartScheduler();

    // 正常不会执行到这里
    while (1);
}

任务优先级分配原则

// FreeRTOS优先级: 0(最低) - configMAX_PRIORITIES-1(最高)

#define PRIORITY_IDLE        0  // 空闲任务(系统自动创建)
#define PRIORITY_LOW         1  // 后台任务(日志记录)
#define PRIORITY_NORMAL      2  // 普通任务(LED显示)
#define PRIORITY_HIGH        3  // 重要任务(数据处理)
#define PRIORITY_REALTIME    4  // 实时任务(中断后处理)

// 原则:
// 1. 实时性要求高的任务优先级高
// 2. 执行时间短的任务优先级可适当提高
// 3. 避免优先级反转(使用互斥锁)

任务控制函数

// 挂起任务
void suspend_led(void) {
    vTaskSuspend(led_task_handle);  // LED停止闪烁
}

// 恢复任务
void resume_led(void) {
    vTaskResume(led_task_handle);
}

// 删除任务
void delete_task(void) {
    vTaskDelete(key_task_handle);  // 删除按键任务
    key_task_handle = NULL;
}

// 任务通知(轻量级信号量)
void notify_task(void) {
    xTaskNotifyGive(led_task_handle);  // 通知LED任务
}

void LED_Task(void *pvParameters) {
    while (1) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // 等待通知
        LED_TOGGLE();
    }
}

信号量(Semaphore)

二值信号量

用于任务同步,类似标志位:

SemaphoreHandle_t xBinarySem;

void setup(void) {
    // 创建二值信号量
    xBinarySem = xSemaphoreCreateBinary();
}

// 中断中释放信号量
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    if (USART1->SR & USART_SR_RXNE) {
        rx_buffer[rx_index++] = USART1->DR;

        if (rx_index >= RX_SIZE) {
            xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
            portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
        }
    }
}

// 任务中获取信号量
void UART_Task(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdTRUE) {
            process_data(rx_buffer, rx_index);
            rx_index = 0;
        }
    }
}

计数信号量

用于资源计数:

#define MAX_BUFFERS 5
SemaphoreHandle_t xCountingSem;

void setup(void) {
    // 创建计数信号量,最大值5,初始值5
    xCountingSem = xSemaphoreCreateCounting(MAX_BUFFERS, MAX_BUFFERS);
}

// 生产者任务
void Producer_Task(void *pvParameters) {
    while (1) {
        uint8_t *buffer = malloc(256);
        // 生产数据
        produce_data(buffer);

        // 放入队列
        xQueueSend(data_queue, &buffer, portMAX_DELAY);
        xSemaphoreGive(xCountingSem);  // 信号量+1

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 消费者任务
void Consumer_Task(void *pvParameters) {
    while (1) {
        xSemaphoreTake(xCountingSem, portMAX_DELAY);  // 信号量-1

        uint8_t *buffer;
        xQueueReceive(data_queue, &buffer, portMAX_DELAY);
        consume_data(buffer);
        free(buffer);
    }
}

互斥锁(Mutex)

防止多任务同时访问共享资源,支持优先级继承:

SemaphoreHandle_t xMutex;

void setup(void) {
    xMutex = xSemaphoreCreateMutex();
}

// 安全访问共享资源
void access_shared_resource(void) {
    if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        // 临界区代码
        shared_counter++;
        printf("Counter: %d\n", shared_counter);

        xSemaphoreGive(xMutex);  // 释放互斥锁
    } else {
        printf("Failed to acquire mutex\n");
    }
}

死锁预防

// 错误示例:循环等待导致死锁
void Task_A(void) {
    xSemaphoreTake(mutex_1, portMAX_DELAY);
    vTaskDelay(10);
    xSemaphoreTake(mutex_2, portMAX_DELAY);  // 等待Task_B释放
    // ...
    xSemaphoreGive(mutex_2);
    xSemaphoreGive(mutex_1);
}

void Task_B(void) {
    xSemaphoreTake(mutex_2, portMAX_DELAY);
    vTaskDelay(10);
    xSemaphoreTake(mutex_1, portMAX_DELAY);  // 等待Task_A释放
    // ...
    xSemaphoreGive(mutex_1);
    xSemaphoreGive(mutex_2);
}

// 解决方案1:统一获取顺序
void Task_A_Fixed(void) {
    xSemaphoreTake(mutex_1, portMAX_DELAY);
    xSemaphoreTake(mutex_2, portMAX_DELAY);
    // ...
    xSemaphoreGive(mutex_2);
    xSemaphoreGive(mutex_1);
}

void Task_B_Fixed(void) {
    xSemaphoreTake(mutex_1, portMAX_DELAY);  // 同样先获取mutex_1
    xSemaphoreTake(mutex_2, portMAX_DELAY);
    // ...
    xSemaphoreGive(mutex_2);
    xSemaphoreGive(mutex_1);
}

// 解决方案2:超时机制
void Task_C(void) {
    if (xSemaphoreTake(mutex_1, pdMS_TO_TICKS(100)) == pdTRUE) {
        if (xSemaphoreTake(mutex_2, pdMS_TO_TICKS(100)) == pdTRUE) {
            // 成功获取两个锁
            xSemaphoreGive(mutex_2);
        }
        xSemaphoreGive(mutex_1);
    }
}

消息队列(Queue)

队列创建和使用

QueueHandle_t xQueue;

typedef struct {
    uint8_t sensor_id;
    float temperature;
    float humidity;
} SensorData_t;

void setup(void) {
    // 创建队列:10个元素,每个元素大小为SensorData_t
    xQueue = xQueueCreate(10, sizeof(SensorData_t));
}

// 发送任务
void Sensor_Task(void *pvParameters) {
    SensorData_t data;

    while (1) {
        data.sensor_id = 1;
        data.temperature = read_temperature();
        data.humidity = read_humidity();

        // 发送到队列(阻塞100ms)
        if (xQueueSend(xQueue, &data, pdMS_TO_TICKS(100)) != pdTRUE) {
            printf("Queue full!\n");
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// 接收任务
void Display_Task(void *pvParameters) {
    SensorData_t data;

    while (1) {
        // 从队列接收(永久阻塞)
        if (xQueueReceive(xQueue, &data, portMAX_DELAY) == pdTRUE) {
            printf("Sensor %d: %.1f°C, %.1f%%\n",
                   data.sensor_id, data.temperature, data.humidity);
        }
    }
}

// 查询队列状态
void check_queue(void) {
    UBaseType_t items = uxQueueMessagesWaiting(xQueue);
    UBaseType_t spaces = uxQueueSpacesAvailable(xQueue);
    printf("Queue: %d items, %d spaces\n", items, spaces);
}

队列在中断中使用

void ADC_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    uint16_t adc_value = ADC1->DR;

    // 中断中使用FromISR版本
    xQueueSendFromISR(xQueue, &adc_value, &xHigherPriorityTaskWoken);

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

事件标志组(Event Group)

用于多个事件的同步:

EventGroupHandle_t xEventGroup;

#define EVENT_WIFI_CONNECTED   (1 << 0)
#define EVENT_DATA_READY       (1 << 1)
#define EVENT_SEND_COMPLETE    (1 << 2)

void setup(void) {
    xEventGroup = xEventGroupCreate();
}

// WiFi任务设置事件
void WiFi_Task(void *pvParameters) {
    while (1) {
        if (wifi_connect()) {
            xEventGroupSetBits(xEventGroup, EVENT_WIFI_CONNECTED);
        }
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

// 数据发送任务等待事件
void Send_Task(void *pvParameters) {
    while (1) {
        // 等待WiFi连接和数据就绪
        EventBits_t bits = xEventGroupWaitBits(
            xEventGroup,
            EVENT_WIFI_CONNECTED | EVENT_DATA_READY,  // 等待的位
            pdTRUE,     // 返回后清除位
            pdTRUE,     // 等待所有位都设置
            portMAX_DELAY
        );

        if (bits == (EVENT_WIFI_CONNECTED | EVENT_DATA_READY)) {
            send_data();
            xEventGroupSetBits(xEventGroup, EVENT_SEND_COMPLETE);
        }
    }
}

内存管理

FreeRTOS提供5种内存管理方案:

方案特点使用场景
heap_1只分配不释放任务创建后不删除
heap_2允许释放,不合并碎片已弃用
heap_3封装malloc/free需要线程安全的malloc
heap_4合并相邻空闲块通用(推荐)
heap_5支持多内存区域内部RAM+外部RAM
// FreeRTOSConfig.h配置
#define configTOTAL_HEAP_SIZE  (20 * 1024)  // 20KB堆

// 动态分配内存
void *pvPortMalloc(size_t xWantedSize);
void vPortFree(void *pv);

// 查询剩余堆空间
size_t xPortGetFreeHeapSize(void);

// 查询历史最小剩余空间
size_t xPortGetMinimumEverFreeHeapSize(void);

// 使用示例
void task_example(void) {
    uint8_t *buffer = pvPortMalloc(256);
    if (buffer != NULL) {
        // 使用buffer
        vPortFree(buffer);
    } else {
        printf("Memory allocation failed!\n");
    }

    printf("Free heap: %d bytes\n", xPortGetFreeHeapSize());
}

内存碎片处理

// 使用内存池减少碎片
#define POOL_SIZE 10
typedef struct {
    uint8_t buffer[256];
    bool in_use;
} BufferPool_t;

BufferPool_t buffer_pool[POOL_SIZE];

uint8_t* alloc_from_pool(void) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!buffer_pool[i].in_use) {
            buffer_pool[i].in_use = true;
            return buffer_pool[i].buffer;
        }
    }
    return NULL;
}

void free_to_pool(uint8_t *buffer) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (buffer_pool[i].buffer == buffer) {
            buffer_pool[i].in_use = false;
            break;
        }
    }
}

FreeRTOS配置

// FreeRTOSConfig.h关键配置
#define configUSE_PREEMPTION            1      // 抢占式调度
#define configUSE_IDLE_HOOK             0      // 空闲钩子
#define configUSE_TICK_HOOK             0      // 时钟钩子
#define configCPU_CLOCK_HZ              72000000
#define configTICK_RATE_HZ              1000   // 1ms一个tick
#define configMAX_PRIORITIES            5
#define configMINIMAL_STACK_SIZE        128
#define configTOTAL_HEAP_SIZE           20480
#define configMAX_TASK_NAME_LEN         16
#define configUSE_TRACE_FACILITY        1
#define configUSE_16_BIT_TICKS          0
#define configIDLE_SHOULD_YIELD         1
#define configUSE_MUTEXES               1
#define configUSE_COUNTING_SEMAPHORES   1
#define configUSE_QUEUE_SETS            0
#define configUSE_RECURSIVE_MUTEXES     1

// 中断优先级配置
#define configKERNEL_INTERRUPT_PRIORITY         (15 << 4)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    (5 << 4)

高频面试题

1. RTOS中任务切换的过程是什么?

答案:

任务切换(Context Switch)分为两步:

  1. 保存当前任务上下文: 将CPU寄存器(R0-R12, LR, PC, PSR)压栈
  2. 恢复新任务上下文: 从新任务的栈中弹出寄存器
// Cortex-M3任务切换(汇编)
__asm void PendSV_Handler(void) {
    // 保存当前任务
    MRS     R0, PSP         // 获取任务栈指针
    STMDB   R0!, {R4-R11}   // 保存R4-R11
    // 保存栈指针到TCB
    LDR     R1, =pxCurrentTCB
    LDR     R1, [R1]
    STR     R0, [R1]

    // 切换到新任务
    // ...

    // 恢复新任务
    LDR     R0, [R1]
    LDMIA   R0!, {R4-R11}   // 恢复R4-R11
    MSR     PSP, R0
    BX      LR              // 返回,硬件自动恢复R0-R3,R12,LR,PC,PSR
}

2. 什么是优先级反转?如何解决?

答案:

优先级反转: 高优先级任务被低优先级任务阻塞。

场景:

  • 低优先级任务L获取互斥锁
  • 高优先级任务H等待该锁,被阻塞
  • 中优先级任务M抢占L,导致H长时间等待

解决方案:

  1. 优先级继承: L获取锁后,临时提升到H的优先级
// FreeRTOS的Mutex自动实现优先级继承
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
  1. 优先级天花板: 获取锁时提升到所有可能使用该锁的任务的最高优先级

3. 信号量和互斥锁的区别?

答案:

特性信号量互斥锁
用途同步互斥访问
所有权无有(只能由获取者释放)
优先级继承不支持支持
递归获取不支持支持(递归互斥锁)
初始值可为任意值1
// 信号量:任何任务都可以Give
xSemaphoreGive(sem);

// 互斥锁:必须由Take的任务Give
xSemaphoreTake(mutex, portMAX_DELAY);
// ...
xSemaphoreGive(mutex);

4. vTaskDelay和vTaskDelayUntil的区别?

答案:

// vTaskDelay:相对延时
void Task1(void) {
    while (1) {
        do_work();  // 假设耗时10ms
        vTaskDelay(pdMS_TO_TICKS(100));  // 实际周期:110ms
    }
}

// vTaskDelayUntil:绝对延时,保证固定周期
void Task2(void) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    while (1) {
        do_work();  // 假设耗时10ms
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));  // 周期:100ms
    }
}

vTaskDelayUntil用于需要固定执行周期的任务(如采样、控制算法)。

5. 如何检测任务栈溢出?

答案:

方法1:配置检测

// FreeRTOSConfig.h
#define configCHECK_FOR_STACK_OVERFLOW  2

// 实现钩子函数
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    printf("Stack overflow in task: %s\n", pcTaskName);
    while (1);  // 停止系统
}

方法2:查询高水位

// 查询任务栈的剩余空间
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
printf("Stack free: %d words\n", uxHighWaterMark);

方法3:手动检查栈填充

// FreeRTOS会用0xA5填充栈,检查栈底是否仍为0xA5