FreeRTOS:中断(ISR)与 RTOS 安全 API

FreeRTOS:中断(ISR)与 RTOS 安全 API

前言

在嵌入式系统中,中断是处理实时事件的核心机制。然而,当我们引入 RTOS 后,中断服务程序(ISR)与操作系统的交互就成了一个需要格外小心的领域。使用不当的 API 可能导致系统崩溃、任务调度失败,甚至数据损坏。

本节课程将深入探讨如何在 FreeRTOS 中正确处理中断,包括专用的 FromISR 后缀 API、上下文切换机制、中断优先级配置,以及一些常见的陷阱和解决方案。

一、核心概念

1.1 为什么需要 FromISR API?

FreeRTOS 的常规 API(如 xQueueSendxSemaphoreGive)是为任务上下文设计的,它们可能会引发任务切换、进入阻塞状态。但在 ISR 中:

  • 不能阻塞:ISR 必须快速执行并返回
  • 上下文切换时机特殊:需要延迟到 ISR 结束后才能切换任务
  • 临界区处理不同:ISR 通过禁用中断实现互斥,而非调度器锁

因此,FreeRTOS 提供了一套专门的 FromISR API,它们:

  • 永不阻塞
  • 通过 pxHigherPriorityTaskWoken 参数指示是否需要上下文切换
  • 使用中断安全的临界区保护

1.2 Cortex-M 中断优先级与 FreeRTOS

在 ARM Cortex-M 架构中,中断优先级数字越小优先级越高 (0 是最高优先级)。FreeRTOS 通过 configMAX_SYSCALL_INTERRUPT_PRIORITY 定义了一个临界值:

复制代码
优先级 0-4:不受 FreeRTOS 管理(不能调用任何 FreeRTOS API)
优先级 5-15:受 FreeRTOS 管理(可以调用 FromISR API)

BASEPRI 寄存器 :FreeRTOS 使用 BASEPRI 寄存器来实现临界区。设置 BASEPRI 后,所有优先级数值大于等于该值的中断会被屏蔽。

PRIMASK 寄存器:全局中断使能位,设置后屏蔽所有可屏蔽中断(不推荐在 RTOS 中使用)。

二、核心 FromISR API 详解

2.1 队列操作

xQueueSendFromISR / xQueueSendToBackFromISR
c 复制代码
BaseType_t xQueueSendFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
);

参数说明

  • xQueue:队列句柄
  • pvItemToQueue:要发送的数据指针
  • pxHigherPriorityTaskWoken:输出参数,指示是否有更高优先级任务被唤醒

返回值

  • pdTRUE:成功发送
  • errQUEUE_FULL:队列已满

使用示例

c 复制代码
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    char receivedChar;
    
    if (UART_GetITStatus(UART1, UART_IT_RXNE)) {
        receivedChar = UART_ReceiveData(UART1);
        
        // 发送到队列
        xQueueSendFromISR(uartQueue, &receivedChar, &xHigherPriorityTaskWoken);
        
        UART_ClearITPendingBit(UART1, UART_IT_RXNE);
    }
    
    // ISR 结束时检查是否需要上下文切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
xQueueReceiveFromISR
c 复制代码
BaseType_t xQueueReceiveFromISR(
    QueueHandle_t xQueue,
    void *pvBuffer,
    BaseType_t *pxHigherPriorityTaskWoken
);

注意:这个 API 较少使用,因为通常是 ISR 向队列发送数据,任务从队列接收。

2.2 信号量操作

xSemaphoreGiveFromISR
c 复制代码
BaseType_t xSemaphoreGiveFromISR(
    SemaphoreHandle_t xSemaphore,
    BaseType_t *pxHigherPriorityTaskWoken
);

典型用例:中断触发后,通知任务进行处理

c 复制代码
void TIM2_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if (TIM_GetITStatus(TIM2, TIM_IT_Update)) {
        // 释放二值信号量,唤醒处理任务
        xSemaphoreGiveFromISR(timeSemaphore, &xHigherPriorityTaskWoken);
        
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
    
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

2.3 任务通知

vTaskNotifyGiveFromISR
c 复制代码
void vTaskNotifyGiveFromISR(
    TaskHandle_t xTaskToNotify,
    BaseType_t *pxHigherPriorityTaskWoken
);

优势:比信号量更快,开销更小

c 复制代码
TaskHandle_t dataProcessTask;

void ADC_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)) {
        // 直接通知任务
        vTaskNotifyGiveFromISR(dataProcessTask, &xHigherPriorityTaskWoken);
        
        ADC_ClearFlag(ADC1, ADC_FLAG_EOC);
    }
    
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
xTaskNotifyFromISR
c 复制代码
BaseType_t xTaskNotifyFromISR(
    TaskHandle_t xTaskToNotify,
    uint32_t ulValue,
    eNotifyAction eAction,
    BaseType_t *pxHigherPriorityTaskWoken
);

eAction 参数

  • eNoAction:仅设置通知状态
  • eSetBits:将 ulValue 与任务通知值按位或
  • eIncrement:递增通知值(忽略 ulValue)
  • eSetValueWithOverwrite:强制设置为 ulValue
  • eSetValueWithoutOverwrite:仅在没有待处理通知时设置

2.4 上下文切换宏

portYIELD_FROM_ISR
c 复制代码
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

功能:在 ISR 退出前,检查是否需要立即进行上下文切换

等价形式(Cortex-M):

c 复制代码
if (xHigherPriorityTaskWoken) {
    portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
}

三、实验:中断驱动的串口接收

3.1 实验目标

实现一个完整的中断驱动串口接收系统:

  1. UART 中断接收字符
  2. 通过队列传递数据到任务
  3. 任务处理接收到的数据(回显 + 统计)

3.2 完整代码

c 复制代码
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "stm32f4xx.h"
#include <stdio.h>

/* 队列句柄 */
QueueHandle_t xUartRxQueue;

/* 统计信息 */
typedef struct {
    uint32_t totalReceived;
    uint32_t overrunErrors;
    uint32_t queueFullCount;
} UartStats_t;

UartStats_t uartStats = {0};

/* UART 初始化 */
void UART_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    
    // 使能时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    
    // GPIO 配置:PA9(TX), PA10(RX)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
    
    // UART 配置
    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART1, &USART_InitStructure);
    
    // 使能接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
    
    // 配置 NVIC(重要:优先级必须高于 configMAX_SYSCALL_INTERRUPT_PRIORITY)
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;  // 假设 configMAX_SYSCALL = 5
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    USART_Cmd(USART1, ENABLE);
}

/* UART 中断服务函数 */
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    char receivedChar;
    
    // 检查接收中断标志
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        receivedChar = (char)USART_ReceiveData(USART1);
        
        // 尝试发送到队列
        if (xQueueSendFromISR(xUartRxQueue, &receivedChar, &xHigherPriorityTaskWoken) != pdTRUE) {
            // 队列满,记录错误
            uartStats.queueFullCount++;
        } else {
            uartStats.totalReceived++;
        }
        
        // 清除中断标志(STM32 读取数据后自动清除)
    }
    
    // 检查溢出错误
    if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET) {
        USART_ReceiveData(USART1);  // 读取数据寄存器清除 ORE
        uartStats.overrunErrors++;
    }
    
    // 执行上下文切换(如果需要)
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/* UART 发送函数(阻塞式,供任务使用) */
void UART_SendChar(char ch) {
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, ch);
}

void UART_SendString(const char* str) {
    while (*str) {
        UART_SendChar(*str++);
    }
}

/* 数据处理任务 */
void vUartProcessTask(void *pvParameters) {
    char receivedChar;
    char buffer[64];
    
    UART_SendString("UART Processing Task Started\r\n");
    UART_SendString("Type characters to echo back...\r\n");
    
    while (1) {
        // 从队列接收数据(阻塞等待)
        if (xQueueReceive(xUartRxQueue, &receivedChar, portMAX_DELAY) == pdTRUE) {
            // 回显字符
            UART_SendChar(receivedChar);
            
            // 特殊命令处理
            if (receivedChar == '\r') {
                UART_SendChar('\n');  // 换行
            } else if (receivedChar == 's') {
                // 显示统计信息
                snprintf(buffer, sizeof(buffer), 
                    "\r\n[Stats] Total: %lu, Queue Full: %lu, Overrun: %lu\r\n",
                    uartStats.totalReceived, 
                    uartStats.queueFullCount,
                    uartStats.overrunErrors);
                UART_SendString(buffer);
            }
        }
    }
}

/* 监控任务(可选) */
void vMonitorTask(void *pvParameters) {
    char buffer[64];
    uint32_t lastTotal = 0;
    
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(5000));  // 每 5 秒报告一次
        
        uint32_t newChars = uartStats.totalReceived - lastTotal;
        lastTotal = uartStats.totalReceived;
        
        snprintf(buffer, sizeof(buffer), 
            "[Monitor] New: %lu chars, Queue waiting: %lu\r\n",
            newChars, uxQueueMessagesWaiting(xUartRxQueue));
        UART_SendString(buffer);
    }
}

/* 主函数 */
int main(void) {
    // 初始化硬件
    UART_Init();
    
    // 创建队列(32 个字符缓冲)
    xUartRxQueue = xQueueCreate(32, sizeof(char));
    if (xUartRxQueue == NULL) {
        // 队列创建失败
        while (1);
    }
    
    // 创建数据处理任务(高优先级)
    xTaskCreate(vUartProcessTask, "UartProcess", 
                configMINIMAL_STACK_SIZE * 2, NULL, 3, NULL);
    
    // 创建监控任务(低优先级)
    xTaskCreate(vMonitorTask, "Monitor", 
                configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    
    // 启动调度器
    vTaskStartScheduler();
    
    // 不应该运行到这里
    while (1);
}

3.3 实验运行效果

  1. 正常接收:输入字符会立即回显
  2. 统计命令 :输入 s 显示接收统计
  3. 监控输出:每 5 秒自动报告接收速率
  4. 错误处理:队列满时记录丢失计数

3.4 关键设计点

  1. 最小化 ISR 工作量:ISR 只负责读取数据、发送队列,不做复杂处理
  2. 队列缓冲:32 字符缓冲足以应对突发流量
  3. 错误统计:记录队列满和溢出错误,便于调试
  4. 优先级配置:UART 中断优先级设置为 6(高于 configMAX_SYSCALL)

四、作业:中断优先级错误问题分析

4.1 作业题目

场景描述: 某嵌入式系统使用 FreeRTOS,配置如下:

  • configMAX_SYSCALL_INTERRUPT_PRIORITY = 5(在 STM32 上,实际值为 5 << 4 = 0x50)
  • UART 中断优先级设置为 3
  • 定时器中断优先级设置为 7

系统运行后出现以下问题:

  1. UART 中断处理后偶尔系统崩溃
  2. 定时器中断调用 xSemaphoreGiveFromISR 正常工作
  3. 调试发现 HardFault 发生在任务切换代码中

问题

  1. 分析导致系统崩溃的根本原因
  2. 给出详细的解决方案
  3. 编写验证代码,展示正确和错误的配置

4.2 问题分析

根本原因

UART 中断优先级配置错误

复制代码
UART 优先级 = 3 < configMAX_SYSCALL (5)
                 ↑
            不受 FreeRTOS 管理

当 UART 中断优先级低于 (数值小于)configMAX_SYSCALL_INTERRUPT_PRIORITY 时,该中断处于 FreeRTOS 无法控制的范围。此时:

  1. 临界区失效 :FreeRTOS 的临界区通过设置 BASEPRI = configMAX_SYSCALL 来禁用受管理的中断。但 UART 中断(优先级 3)仍然可以抢占,导致临界区被破坏。
  2. 数据结构损坏:如果 UART 中断在任务切换过程中触发,可能会修改正在更新的内核数据结构(如就绪列表、延迟列表)。
  3. 调度器状态不一致:中断可能在调度器挂起期间运行,导致调度器状态机混乱。
为什么定时器中断正常?

定时器中断优先级 = 7 > configMAX_SYSCALL (5),处于受管理范围,FreeRTOS 可以正确保护临界区。

4.3 解决方案

方案 1:调整 UART 中断优先级(推荐)

将 UART 中断优先级设置为 5 或更高

c 复制代码
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;  // 正确:6 > 5
方案 2:调整 configMAX_SYSCALL_INTERRUPT_PRIORITY

如果 UART 中断确实需要极高优先级,可以降低 configMAX_SYSCALL:

c 复制代码
// FreeRTOSConfig.h
#define configMAX_SYSCALL_INTERRUPT_PRIORITY  (2 << 4)  // 降低到 2

但这会导致更多中断不受 FreeRTOS 管理,需谨慎评估。

方案 3:高优先级中断不调用 FreeRTOS API

如果必须使用高优先级(< configMAX_SYSCALL),则完全不能调用任何 FreeRTOS API

c 复制代码
void USART1_IRQHandler(void) {
    char data = USART_ReceiveData(USART1);
    
    // 直接写入全局缓冲区(使用硬件同步或原子操作)
    if (rxBufferHead != rxBufferTail) {
        rxBuffer[rxBufferHead++] = data;
    }
    
    // 不调用任何 FreeRTOS API!
}

4.4 验证代码

c 复制代码
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "stm32f4xx.h"

SemaphoreHandle_t xTestSemaphore;
volatile uint32_t errorCount = 0;
volatile uint32_t successCount = 0;

/* 错误配置示例 */
void ConfigureUART_Wrong(void) {
    NVIC_InitTypeDef NVIC_InitStructure;
    
    // 错误:优先级 3 < configMAX_SYSCALL (5)
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;  // ❌ 错误
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    printf("UART Priority: 3 (WRONG - below configMAX_SYSCALL)\r\n");
}

/* 正确配置示例 */
void ConfigureUART_Correct(void) {
    NVIC_InitTypeDef NVIC_InitStructure;
    
    // 正确:优先级 6 > configMAX_SYSCALL (5)
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;  // ✓ 正确
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    printf("UART Priority: 6 (CORRECT - above configMAX_SYSCALL)\r\n");
}

/* UART 中断处理 */
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        USART_ReceiveData(USART1);  // 读取数据
        
        // 尝试释放信号量
        if (xSemaphoreGiveFromISR(xTestSemaphore, &xHigherPriorityTaskWoken) == pdTRUE) {
            successCount++;
        } else {
            errorCount++;
        }
        
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

/* 测试任务:触发大量中断 */
void vStressTestTask(void *pvParameters) {
    uint32_t count = 0;
    
    while (1) {
        // 模拟发送数据触发 UART 中断
        USART_SendData(USART1, 'A');
        
        // 获取信号量
        if (xSemaphoreTake(xTestSemaphore, pdMS_TO_TICKS(100)) == pdTRUE) {
            count++;
            
            if (count % 1000 == 0) {
                printf("Test cycles: %lu, Success: %lu, Errors: %lu\r\n",
                       count, successCount, errorCount);
            }
        }
        
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

/* 优先级检查任务 */
void vPriorityCheckTask(void *pvParameters) {
    uint32_t priority = NVIC_GetPriority(USART1_IRQn);
    uint32_t maxSyscall = configMAX_SYSCALL_INTERRUPT_PRIORITY >> 4;
    
    printf("\r\n=== Interrupt Priority Analysis ===\r\n");
    printf("configMAX_SYSCALL_INTERRUPT_PRIORITY: %lu\r\n", maxSyscall);
    printf("UART IRQ Priority: %lu\r\n", priority);
    
    if (priority < maxSyscall) {
        printf("❌ ERROR: UART priority too high (not managed by FreeRTOS)\r\n");
        printf("   This will cause system instability!\r\n");
        printf("   Solution: Set priority >= %lu\r\n", maxSyscall);
    } else {
        printf("✓ CORRECT: UART priority properly configured\r\n");
    }
    printf("===================================\r\n\r\n");
    
    vTaskDelete(NULL);
}

/* 主函数 */
int main(void) {
    // 创建二值信号量
    xTestSemaphore = xSemaphoreCreateBinary();
    
    // 选择配置方式
    #ifdef USE_WRONG_CONFIG
        ConfigureUART_Wrong();   // 测试错误配置
    #else
        ConfigureUART_Correct(); // 使用正确配置
    #endif
    
    // 创建测试任务
    xTaskCreate(vPriorityCheckTask, "PriorityCheck", 
                configMINIMAL_STACK_SIZE * 2, NULL, 1, NULL);
    xTaskCreate(vStressTestTask, "StressTest", 
                configMINIMAL_STACK_SIZE * 2, NULL, 2, NULL);
    
    // 启动调度器
    vTaskStartScheduler();
    
    while (1);
}

4.5 调试技巧

1. 使用 configASSERT 检查

在 FreeRTOSConfig.h 中启用断言:

c 复制代码
#define configASSERT(x) \
    if ((x) == 0) { \
        taskDISABLE_INTERRUPTS(); \
        printf("ASSERT FAILED: %s:%d\r\n", __FILE__, __LINE__); \
        for(;;); \
    }
2. 编译时检查宏
c 复制代码
// 在 FromISR API 前添加
#define CHECK_ISR_PRIORITY() \
    do { \
        if (__get_IPSR() != 0) { \
            uint32_t irqn = __get_IPSR() - 16; \
            uint32_t priority = NVIC_GetPriority(irqn); \
            configASSERT(priority >= (configMAX_SYSCALL_INTERRUPT_PRIORITY >> 4)); \
        } \
    } while(0)
3. HardFault 分析

当发生 HardFault 时,检查:

  • HFSR 寄存器:确定是否为 FORCED 标志
  • CFSR 寄存器:查看具体错误类型(UFSR/BFSR/MMFSR)
  • 栈回溯:查看崩溃时的调用栈
c 复制代码
void HardFault_Handler(void) {
    __asm volatile (
        "TST lr, #4 \n"
        "ITE EQ \n"
        "MRSEQ r0, MSP \n"
        "MRSNE r0, PSP \n"
        "B HardFault_Handler_C \n"
    );
}

void HardFault_Handler_C(uint32_t *hardfault_args) {
    printf("HardFault!\r\n");
    printf("R0 = 0x%08X\r\n", hardfault_args[0]);
    printf("R1 = 0x%08X\r\n", hardfault_args[1]);
    printf("PC = 0x%08X\r\n", hardfault_args[6]);
    printf("HFSR = 0x%08X\r\n", SCB->HFSR);
    printf("CFSR = 0x%08X\r\n", SCB->CFSR);
    
    while(1);
}

五、ISR 使用最佳实践

5.1 黄金法则

  1. 快速进出:ISR 执行时间应 < 几十微秒
  2. 延迟处理:复杂逻辑交给任务完成
  3. 正确优先级:受管理中断 ≥ configMAX_SYSCALL
  4. 使用 FromISR API:永远不在 ISR 中调用常规 API
  5. 检查返回值:处理队列满等错误情况

5.2 禁止在 ISR 中使用的 API

禁止使用(会阻塞) 使用替代 API
xQueueSend xQueueSendFromISR
xSemaphoreTake 不应在 ISR 获取信号量
vTaskDelay 不应在 ISR 延时
xTaskCreate 在任务中创建任务
vTaskSuspend vTaskNotifyGiveFromISR 配合使用

5.3 性能优化

示例:高频中断优化

c 复制代码
#define BUFFER_SIZE 128
static char dmaBuffer[BUFFER_SIZE];
static volatile uint16_t dmaIndex = 0;

void DMA_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) {
        // 传输完成,通知任务处理整个缓冲区
        xTaskNotifyFromISR(dmaProcessTask, 
                          dmaIndex, 
                          eSetValueWithOverwrite,
                          &xHigherPriorityTaskWoken);
        
        // 切换缓冲区
        dmaIndex = (dmaIndex == 0) ? BUFFER_SIZE : 0;
        
        DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5);
    }
    
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
相关推荐
一路往蓝-Anbo2 小时前
STM32单线串口通讯实战(三):协议层设计 —— 帧结构、多机寻址与硬件唤醒
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网
Love Song残响2 小时前
高效自动化清理临时文件方案
java·开发语言·spring
古城小栈2 小时前
Rust 中符号语法 一文全晓
开发语言·后端·rust
沃斯堡&蓝鸟2 小时前
DAY34 文件的规范拆分和写法
开发语言·python
ss2732 小时前
final关键字如何创造线程安全的对象
开发语言·python
flysh052 小时前
深度解析 C# 核心:类(Class)的设计精髓与高级特性
开发语言·c#
Feibo20112 小时前
R-3east
开发语言·r语言
_OP_CHEN2 小时前
【从零开始的Qt开发指南】(十四)Qt 窗口之“三剑客”:工具栏、状态栏、浮动窗口进阶实战指南
开发语言·c++·qt·前端开发·gui开发·qt窗口
古城小栈2 小时前
Rust 模块管理与文件联动
开发语言·后端·rust