找回密码
 立即注册
查看: 30|回复: 0

在没有 PendSV 的单片机上硬上 FreeRTOS:关总中断切任务的安全隐患清单

[复制链接]

188

主题

1406

回帖

5508

积分

荣誉版主

积分
5508
发表于 11 小时前 | 显示全部楼层 |阅读模式
(1)在本论坛,TZZ1***网友发表了自己的FreeRTOS在STC32G12单片机上的移植版
FreeRTOS  让 FreeRTOS 在 C251 上从能用到好用, 2026/9/14更新
https://www.stcaimcu.com/thread-7614-1-1.html
(出处: 国芯人工智能技术交流网站)

(2)他认为他的移植最大的看点是:简介:
用于STC32G12K128, 32位8051内核上的 FreeTROS_V10.5.1  移植版本.
移植版本弥补了 portYIELD_FROM_ISR() 缺失的问题.
Keil C251 编译器      

这里portYIELD_FROM_ISR() 缺失的问题是指STC官方移植版本。


(3)他采用的方法是
简介: 用于STC MUC 251内核上的 FreeTROS_V10.5.1  移植版本.
         用一个硬件中断来模拟PendSv行为, 用于切换任务. PendSv占用一个中断源.
         此版本弥补了 portYIELD_FROM_ISR() 缺失的问题.



下面我以题目:在没有 PendSV 的单片机上硬上 FreeRTOS:关总中断切任务的安全隐患清单
具体根据他的“FreeRTOS_STC32G12K - 26.rar”版本里的具体移植程序分析。为了方便大家阅读,我请AI进行了润色。

===========================================
在没有 PendSV 的单片机上硬上 FreeRTOS:关总中断切任务的安全隐患清单


一、先说结论有,而且不止一个。
如果你打算把 FreeRTOS 从 ARM Cortex-M 单片机搬到 STC32G12K128 这类没有 PendSV 异常硬件、也没有 BASEPRI 中断屏蔽寄存器的 8051 增强型单片机上,并且采用 "在中断里关总中断强行切换任务" 的做法,
那么功能上大概率能跑通,但实时性和可靠性会比 ARM 原版差一截
具体有哪些隐患、严重到什么程度,下面结合一段真实可跑的移植代码逐条说。

二、ARM 原版是怎么切任务的
很多人写 FreeRTOS 移植分析时,一上来就讲栈帧、寄存器保存,其实没必要。
你只要先理解一件事:ARM Cortex-M 切任务,靠的是三样硬件 ——NVIC 中断控制器、按优先级屏蔽的 BASEPRI 寄存器、最低优先级的 PendSV 异常。
系统节拍定时器 Systick 中断来了,里面只做一件事:把 tick 加一,看看有没有任务该唤醒了。
如果确实需要切任务,Systick 并不会自己去切,它只是 "挂起" 一个 PendSV 异常。
PendSV 被故意配成系统里最低的优先级。
这意味着真正干活的上下文切换代码,可以在一个 "谁都能打断它" 的优先级上跑。
高优先级的急停中断、掉电保护中断、看门狗喂狗中断,随时可以插队。
等这些急事处理完,再回到 PendSV 把切换做完。
整段切换过程,普通中断是开着的,硬件靠优先级自动保证不会出问题。
这就是 ARM 上 FreeRTOS 可以做到 "关中断时间只有几微秒" 的根本原因。


三、搬到 STC32G 之后变成什么样
STC32G12K128 是 8051 内核,跑 Keil C251。
它没有 NVIC,没有 BASEPRI,也没有 PendSV。
作者想了个办法:挑一个硬件中断,用软件标志位去触发它,让它扮演 PendSV 的角色。
节拍中断是 Timer0,代码长这样:
void Timer0_ISR_Handler(void) interrupt 1{    EA = 0;    portYIELD_FROM_ISR( xTaskIncrementTick() );    EA = 1;}真正干切换活的那个模拟 PendSV 中断,进来第一件事就是关总中断:
PendSvIsr_Entrance:    CLR     EA    portSAVE_CONTEXT()    ...    vTaskSwitchContext()    ...    portRESTORE_CONTEXT()    SETB    EA    RETI注意 CLR EA 和 SETB EA 之间夹着什么。
夹着寄存器保存、夹着一个叫 vTaskSwitchContext 的 C 函数、夹着寄存器恢复。
这段时间里,整个芯片的中断开关是彻底关上的。

四、那到底有哪些隐患
隐患一:切换期间所有中断都进不来
在 ARM 上,PendSV 优先级最低,急停中断可以随时抢进来。
在这个移植里,CLR EA 一执行,就没有任何中断能进来。
问题是 vTaskSwitchContext 不是个耗时固定的函数。
它要遍历就绪任务列表,挑下一个该跑的任务。
系统里任务越多、刚好同一时刻醒的任务越多,它跑的时间就越长。
短的时候几微秒,长的时候几十微秒甚至上百微秒都不奇怪。
这段时间里,急停按钮按了没反应。
看门狗喂狗中断进不来。
串口一帧数据来了,接收中断排队等着。
如果你的产品里有任何一个 "必须立刻响应" 的信号,这个设计就是埋雷。

隐患二:节拍中断里也关了总中断
再看 Timer0 那段:EA=0 之后才调 xTaskIncrementTick。
xTaskIncrementTick 这个函数本身也不是恒定耗时。
多个任务同时延时到期时,它要挨个把任务从延时列表摘出来、挂到就绪列表上。
tick 计数要溢出了,它还得做一次链表交换。
万一开了 tick hook,钩子里再做点事,时间就更长。
ARM 上这段操作用的是 BASEPRI,只屏蔽同优先级和更低的中断,高优先级照样响应。
这里是 EA=0,一刀切把所有中断源都屏蔽了。
连你用来做精确测量的外部中断、连捕获引脚都被挡住。
最坏情况下的中断响应延迟,不是某一个 ISR 的执行时间,而是所有这种关中断窗口加起来。

隐患三:ISR 里保护内核数据靠 "自觉",不靠框架
这一点特别容易被忽略。
看这个移植里的两个宏:
#define portSET_INTERRUPT_MASK_FROM_ISR()  ((!_testbit_(EA))?0:0x80)#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) { IE |= (uint8_t)x; }第一行看起来是在 "设置中断屏蔽",其实它只是读了一下当前 EA 是开是关,根本没有执行关中断
也就是说,任何一个调用了 xQueueSendFromISR、xSemaphoreGiveFromISR 的中断服务函数,如果程序员在开头没自己写一句 EA=0,那它操作内核队列的时候就是毫无保护的。
新人新来加一个串口接收中断,照着别人的代码抄,忘了写 EA=0。
编译能过,烧进去能跑,偶尔就崩一次。
这种 bug 在现场抓起来非常痛苦。
FreeRTOS 原版的契约是:调了 portSET_INTERRUPT_MASK_FROM_ISR,内核数据就受保护。
这个移植把
契约偷偷换成了:"请你自己记得在 ISR 开头关总中断"。
契约的弱化,就是隐患的来源。


隐患四:临界区 API 本身就漏了一步
这一条是代码里实打实的 bug,不是设计权衡。
看 vPortEnterCritical:
void vPortEnterCritical( void ){    if (!_testbit_(EA)) {        if (uxCriticalNesting == 0) _bEA = 0;    } else {        if (uxCriticalNesting == 0) _bEA = 1;    }    uxCriticalNesting++;}它记录了进入前的 EA 状态,也数了嵌套层数。
但是 —— 它没有执行 EA=0。
标准 ARM 移植里,这一步会调 portDISABLE_INTERRUPTS (),也就是真正关中断。
这里漏了。

后果是任务里调 taskENTER_CRITICAL () 包裹的那段代码,中断实际上一直开着。
如果同时有个 ISR 在碰同一个队列,两边就会踩同一个数据结构。
这个 bug 平时不咬人,因为大多数任务临界区和 ISR 操作的不是同一份数据。
一旦踩到,就是偶发的内存损坏,非常难查。


隐患五:不是作者写得差,是架构先天决定的
可能有人会问,那为什么不学着 ARM 那样,PendSV 里不关中断?
答案是:做不到。
ARM 的 PendSV handler 能不关中断,靠的是 NVIC 硬件自动压栈。
进异常时,硬件自动把 xPSR、PC、LR、R0 到 R3、R12 压进当前栈,整个过程是原子的。
退出时硬件自动弹栈。
中间被高优先级中断打断,硬件帮你保存现场,回来继续。
8051 内核没这一套。
进中断时硬件只压一个 PC。
寄存器全靠软件 PUSH。
vTaskSwitchContext 跑到一半,pxCurrentTCB 可能已经指到新任务了,但寄存器还没切过来。
这时候如果允许一个 ISR 闯进来,那个 ISR 再调 FromISR API 读 pxCurrentTCB,读到的就是一个半新半旧的指针。
所以作者只能 CLR EA,把整段切换包成原子操作。
这不是设计品味问题,是架构没给硬件帮忙的余地。
代价就是前面说的那个关中断窗口。


隐患六:关中断窗口加在一起,会吃掉 tick 余量
STC32G 虽然号称 1T,但 C251 编译器生成的代码效率摆在那儿。
保存十几个寄存器、跑一遍就绪列表查找、再恢复回去,几十微秒是正常的。
假设你用 1kHz 的 tick,也就是 1 毫秒一次。
Timer0 自己 EA=0 几十微秒。
PendSV 再 EA=0 几十微秒。
两个窗口串起来,最坏情况一个 tick 周期里有 5% 到 10% 的时间全机关中断。
如果你为了任务响应更跟手,把 tick 拉到 10kHz,周期变成 100 微秒,那光切换一次就把整个 tick 吃掉了。
节拍中断自己都进不来。

隐患七:中断嵌套能力被主动放弃
8051 其实是支持中断优先级嵌套的,STC32G 还扩到了 4 级。
但 EA=0 一上来,这个能力就等于没用。
两个中断同时到了,只能排队。
最坏情况响应延迟不再是 "最高优先级中断自己的执行时间",而是 "所有关中断窗口加起来"。
对实时性要求高的产品,这个账很难算。


隐患八:软件模拟 PendSV 标志,原子性要自己管
ARM 上 PendSV 的挂起标志在 NVIC 寄存器里,硬件写一次就是原子的。
这个移植里 PendSv_SetFlag 是个软件标志位。
任务里调一次,ISR 里也调一次。
两个写入之间没有统一的临界区包着。
虽然 "置位" 这个动作本身不会把标志写坏,但如果逻辑里有 "进 ISR 先清标志" 这种操作,就可能出现置位和清位的交错。
偶尔一次该切的任务没切过去,任务调度看起来就 "卡住了一下"。


五、那到底能不能用
直接给结论。
做演示、做玩具、做对实时性不敏感的小东西 —— 能用。
做产品、做有安全要求的设备、做工业现场的控制器 —— 这套移植方案的安全余量不够。
具体判断标准有三条,你可以对着自己的项目量一量。
第一条,用 GPIO 翻转实测 CLR EA 到 SETB EA 的最长时间,包括 PendSV 和 Timer0 两个地方。
第二条,找出你系统里最不能等的那个中断 —— 可能是急停、可能是看门狗、可能是总线错误。
第三条,把上面两个数加起来,看是不是远小于那个最紧急中断能容忍的延迟窗口。
如果加起来已经接近甚至超过容忍窗口,那就别在这种架构上硬撑 FreeRTOS 了。
要么老老实实前后轮询加状态机,不一定比被一堆隐患拖住的 RTOS 差。

最后说一句
FreeRTOS 官方这么多年一直没出 8051 的官方移植,不是没人想做。
是因为在没有 NVIC、没有 BASEPRI、没有 PendSV 这种硬件原语的单片机上,你要么用关总中断换正确性,要么用关总中断换原子性。
怎么选都是同一个结果:关中断时间比 ARM 长,最坏情况响应延迟比 ARM 大。
移植者能做的,只是把这个窗口尽量压短、把契约尽量做硬。
知道自己在做什么,比能不能跑起来重要得多。









回复

使用道具 举报 送花

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2026-9-19 23:42 , Processed in 0.063338 second(s), 39 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表