提示:文章

前言

前期疑问:
本文目标:


一、背景

最近

二、

2.1

在有操作系统freeRTOS的框架下使用串口中断。
参考文章:接收中断

2.2

尝试在自己工程中实现freeRTOS串口中断接收。
写了第一版代码:SHA-1: 579c3e76615f3f34ff91905507f7f97092353946
上述代码报错:
在这里插入图片描述
将这个问题查资料,Error:…\FreeRTOS\portable\RVDS\ARM_CM3\port.c,244,查到上述文章。查看代码发现问题

static void KeyTask1(void* pvParam)
{
	if(GetKey1Value() == KEY_ON)
	{
		printf("按键1被按下\r\n");
	}
}

static void KeyTask2(void* pvParam)
{
	if(GetKey2Value() == KEY_ON)
	{
		printf("按键2被按下\r\n");
	}
}
// 任务函数定义不是死循环

实现现象
在这里插入图片描述
只有按键2按下按键2任务可以执行。按下按键1没有打印。led灯不闪烁。
分析原因,因为按键2优先级最高

序号 任务 优先级
1、 led1 2
2、 led2 3
3、 key1 4
4、 key2 5

所以按键2抢占任务,导致只有key2任务在一直占用CPU。如果在key任务加上延时,是的任务可以进行切换呢?效果会有不同吗,按键1可以得到执行吗?
实验现象如下:
在这里插入图片描述
这边按下按键2有可能不会打印按键2的信息。按下按键1有可能会打印。按键1打印的时候,优先级2和3的led任务会立即执行。
(我想了下这边好像出现了优先级翻转)。
下面对代码进行修改
即使用消息通知方法,并且消息通知在中断中产生。

先实现按键外部中断。
代码提交信息为:56b2ca303054e9786d0139789a3302aa617f292b
这过程中发现一个问题,就是如果配置了中断但是没有实现中断服务函数,并且触发了中断条件。程序会死机。

在基于按键外部中断可用的情况下,创建rtos任务,但是程序会死机。程序debug调试运行到

vTaskStartScheduler(); /* 启动任务,开启调度 */

会死机。
问了copilot,提到中断任务优先级应该大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY,试着搞了下也没有成功。
该版代码如下:SHA-1: 3693c52528518d109df40f62a0cce016af1e532e

索性按照copilot代码,只创建一个按键任务。修改代码如下,按下离屏幕最近的按键,能够正常进EXTI0_IRQHandler中断服务函数,但是程序会卡死在Error:freeRTOS\src\tasks.c,4579,卡在configASSERT( xTaskToNotify )。查了资料,解释大概如下:【xTaskToNotify: 这是一个任务句柄,用于标识要被通知的任务。在 FreeRTOS 中,使用 xTaskNotify() 或 xTaskNotifyGive() 函数可以向特定任务发送通知。】,意识到可能是通知的任务句柄不对。检查代码确实如此。
如图:
在这里插入图片描述
通知任务句柄为Key1_Task_Handle,但是Key1_Task_Handle函数被我注释了。

修改后发现还是有问题,configASSERT( xTaskToNotify )这行代码意思xTaskToNotify为空,实际应该不为空的。
仔细对比了代码,竟然发现了个大BUG。即任务创建时,这边应该写成取地址。
在这里插入图片描述
将这边改成取地址后,程序打印还是有点问题,就是按键2不会打印“按键2被按下”字符串。
在这里插入图片描述
上述代码提交到:SHA-1: 8a9161d9a97a8311026daed5acac717b09c6dd10可以实现按键中断,但是按键1正常,按键2不会打印预期文字

百思不得其解,还以为跟任务优先级有关。试了好多次,仔细看了代码,发现是因为按键2是下降沿触发。
改成上升沿触发就正常了。
放开打印任务,程序也正常运行。
SHA-1: 20a3f06543345afe55a70c21dff59be074ea8e1a程序正常运行

以做记录。

尝试将按键外部中断EXTI中断优先级改成1,按下按键后报错,
在这里插入图片描述
对应代码configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );,调试看到ucMaxSysCallPriority ucMaxSysCallPriority 值是5,所以EXTI中断优先级应该>=5才不会有问题。

三、

下一步就是配置串口中断,并且在串口中断中发送信号给任务,任务可以打印接收的数据。
2025年7月30日23:05:42
在这里插入图片描述这边中断优先级应该改成大于5。
实现freeRTOS中断接收数据
0d291559fcd4b87e8f82a4b161277811ca659741

3.1 关于freeRTOS中断配置

freeRTOS代码中这行代码什么意思?

这段 FreeRTOS 的代码是在做运行时的断言检查,来确保系统运行的安全性和一致性。来,咱们逐步拆解它:


代码解析:configASSERT( ucCurrentPriority >= ucMaxSysCallPriority )

这行代码的意思是:当前任务的优先级必须高于或等于允许调用系统函数的最低优先级。如果条件不满足,系统会触发断言(assert),通常是在调试时中断程序或者报告错误。

关键字段解释:

  • configASSERT(...)
    FreeRTOS 的宏定义,用于在调试时进行条件检查。条件为假时,触发断言(类似标准的 assert()),帮助开发者发现潜在错误。

  • ucCurrentPriority
    当前运行任务的优先级(通常是一个 uint8_t 类型的值)。

  • ucMaxSysCallPriority
    定义了能安全调用 FreeRTOS 系统调用(比如 vTaskDelay() 或者队列操作)的最大优先级阈值。优先级数字越小,优先级越高(这是 ARM Cortex-M 的优先级排序方式)。

⚠️ 为什么需要这个检查?

某些 FreeRTOS API 不支持在高优先级(特别是中断上下文)中调用,比如不能在某些中断里调用阻塞函数。这个断言是为了确保你的代码不会违反这些规则:

  • 高优先级任务或中断服务程序如果调用不允许的 API,可能导致系统不稳定或死锁。
  • 所以,系统在运行时做断言检查,提前防止这种危险情况发生。

为什么需要做这个检查,解释更详细些

当然可以,我来更深入地给你解析一下这个断言的必要性,甚至可以说它是系统稳定运行的“防火墙”。🔥


背后动机:为什么需要这个 configASSERT 检查?

FreeRTOS 是一个实时操作系统,它需要严格的优先级管理。为了防止“优先级混乱”引发系统崩溃或者行为异常,这个断言起到了“守门员”的作用。

防止错误调用系统 API

某些 FreeRTOS 的 API(比如队列发送/接收、任务调度)不能在中断优先级过高的情况下被调用。如果你在高优先级中断(比如不可屏蔽中断)里调用这些函数:

  • 系统可能无法正常关中断(因为中断优先级太高,不可被屏蔽)
  • 会造成调度器失效,引发 Hard Fault 或系统死锁

所以这里的 configASSERT( ucCurrentPriority >= ucMaxSysCallPriority ) 检查就是用来确保:

“当前优先级足够低,允许调用系统 API”


Cortex-M 的中断优先级机制补充

在 ARM Cortex-M 中断模型里:

  • 优先级值越小,优先级越高(比如 0 是最高优先级)
  • 高优先级中断不能被屏蔽(也就不能调用一些需要关闭中断的系统服务)

FreeRTOS 使用 configMAX_SYSCALL_INTERRUPT_PRIORITY 来标记“能安全调用系统 API 的最高优先级”,这个值通常设置为一个比较大的数字,比如 5 << (8 - __NVIC_PRIO_BITS)


举个例子更清楚:

假设你有一个中断服务函数(ISR),其优先级为 3(比 ucMaxSysCallPriority=5 高):

void MyISR(void)
{
    // 错误做法!在高优先级中断中调用系统API
    xQueueSendFromISR(...);
}

这个调用就是不安全的,因为它可能违反 FreeRTOS 的中断管理规则。

一旦你开启调试断言,就会触发:

configASSERT(3 >= 5); // 条件不满足,断言失败

这能立即提醒你:这里的 ISR 优先级设置不合理!


总结一下为什么有必要:

  • ⛔ 防止高优先级中断非法调用系统函数
  • 🧠 保护调度器运行和中断管理机制
  • 🧰 帮助开发者尽早发现配置错误,避免“潜在炸弹”
  • 📊 符合 ARM 中断模型和 FreeRTOS 的设计约束

。如果你在高优先级中断(比如不可屏蔽中断)里调用这些函数:

  • 系统可能无法正常关中断(因为中断优先级太高,不可被屏蔽)
    这是什么意思

这句话的核心意思是:中断优先级太高,导致系统无法暂时屏蔽它,从而无法安全地执行某些关键操作。我来给你深入拆解一下这个机制背后的逻辑👇


什么叫“关中断”?

在嵌入式系统中,为了保护共享资源或保证某些关键代码的原子性,FreeRTOS 在某些 API 中需要 临时关闭中断(也叫屏蔽中断),避免中断打断这些操作。

比如在访问一个队列或调度任务时:

taskENTER_CRITICAL();
  // 关键代码区域
taskEXIT_CRITICAL();

这段代码会禁止中断进入,从而避免数据竞争或死锁。


高优先级中断的问题

在 Cortex-M 架构中:

  • 优先级数字越小,优先级越高
  • 某些高优先级中断,不能被屏蔽,无论你怎么关中断

所以当你在这种“无法屏蔽”的中断里调用像 xQueueSendFromISR() 这样的函数:

  • FreeRTOS 会试图关闭中断来保护资源
  • 但高优先级中断无法关闭,就等于保护措施失效
  • 这可能造成资源破坏、调度器失控,甚至系统崩溃或 HardFault

举个具体例子:

假如你设定一个中断的优先级为 1(非常高),然后在里面调用:

xQueueSendFromISR(xQueue, &data, NULL);

此时 FreeRTOS 内部会尝试使用 portSET_INTERRUPT_MASK_FROM_ISR() 来屏蔽中断,但你的中断优先级太高,该调用无效。结果:

  • 调度器状态可能被破坏
  • 多任务访问共享资源时发生竞态条件
  • 轻则功能异常,重则系统崩溃

所以这就解释了为什么需要断言检查:

configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );

这是在提醒你:你正在一个优先级太高的中断里调用了不安全的 API!请调整中断优先级


下面是一个简洁清晰的FreeRTOS 与 Cortex-M 中断优先级配置图

优先级值(越小越高) 是否允许调用 FreeRTOS API 描述
0 不允许 极高优先级,通常为紧急中断,如故障处理
1 不允许 高优先级,不能被屏蔽,禁止调用系统函数
2 不允许 仍不可屏蔽,禁止调用调度器相关 API
3 不允许 超过最大系统调用优先级限制
4 不允许 接近阈值,系统断言会触发
5 允许 推荐:设置为configMAX_SYSCALL_INTERRUPT_PRIORITY
6 允许 可调用 xQueueSendFromISR() 等安全 API
7 允许 优先级较低,可用于一般外设中断
允许 越大越安全(越低优先级)
255 最安全 最低优先级(最大值具体取决于 NVIC 配置位数)

今天看串口通讯的代码,看到这行代码

void task_rs485(void *pvParameters)
{
    while (1)
    {
        vTaskSuspend(NULL);
        if (slave_rx_finish(&dat_rs485, FIRE_EVENT_SET_485) == FALSE)
        {
            bsp_rs485_send(NULL, 0);
        }
        else
        {
            dat_rs485.rxokcnt++;
            bsp_rs485_send(dat_rs485.ptxbuf, dat_rs485.txlen);
        }
    }
}

上述是一个串口任务,其中对于vTaskSuspend(NULL);这行代码不理解

查了下百度,

vTaskSuspend(NULL)用于挂起当前任务自身,即停止当前任务的执行,直到被唤醒。以下是具体说明:

行为描述

调用vTaskSuspend(NULL)时,当前运行的任务会被立即挂起,调度器会切换到其他就绪任务。挂起后,任务将不再参与调度,直到被vTaskResume()恢复。

就是任务挂起,但是任务什么时候被启动呢?程序中应该是调用了vTaskResume();函数来唤醒的啊,搜索程序发现这行代码

void hook_rs485(void)
{
    BaseType_t xHigherPriorityTaskWoken;

    if(eTaskGetState( app_task_t[ HANDLE_INDEX_TASK_RS485 ].handle ) == eSuspended)
    {
        xHigherPriorityTaskWoken = xTaskResumeFromISR(app_task_t[ HANDLE_INDEX_TASK_RS485 ].handle);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);/* 如果需要的话进行一次任务切换 */
    }else{
        usart_receive_config(USART1, USART_RECEIVE_ENABLE);
    }
}

所以使用的不是vTaskResume(),而是中断的唤醒函数xTaskResumeFromISR;

hook_rs485函数是在这里调用的,

/* 中断处理 */
void USART1_IRQHandler(void)
{
    /* 超时 */
    if(SET == usart_interrupt_flag_get(RS485_1_USART_NAME, USART_INT_FLAG_RT))
    {
        usart_receive_config(RS485_1_USART_NAME, USART_RECEIVE_DISABLE);
        usart_interrupt_flag_clear(RS485_1_USART_NAME, USART_INT_FLAG_RT);
        hook_rs485();
    }

    /* 接收中断处理 */
    if(SET == usart_interrupt_flag_get(RS485_1_USART_NAME, USART_INT_FLAG_RBNE))
    {
        if(dat_rs485.rxlen + 1 < UART_MSG_SIZE)
        {
            buf_rs485_rx[dat_rs485.rxlen++] = usart_data_receive(RS485_1_USART_NAME);
        }
    }
    /* 发送中断处?? */
    if(SET == usart_interrupt_flag_get(RS485_1_USART_NAME, USART_INT_FLAG_TC))
    {
         usart_interrupt_flag_clear(RS485_1_USART_NAME, USART_INT_FLAG_TC);
         if(dat_rs485.txlen == 0U)
         {
              dat_rs485.txcnt++;
              dat_rs485.rxlen = 0;
              usart_transmit_config(RS485_1_USART_NAME, USART_TRANSMIT_DISABLE);
              usart_receive_config(RS485_1_USART_NAME, USART_RECEIVE_ENABLE);
              dat_rs485.ptxbuf = buf_rs485_tx;
         }
         else
         {
              usart_data_transmit(RS485_1_USART_NAME, *dat_rs485.ptxbuf++);
              dat_rs485.txlen--;
         }
    }
}

至于为什么要在超时函数中调用就不知道了,但是除此其他的是理解了。另外上述是GD32的驱动函数。

总结

未完待续

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐