你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】AliOS 任务上下文切换原理分析

[复制链接]
STMCU小助手 发布时间:2022-3-3 23:58
前言$ M6 v1 V% i  |% i- W- v
一般来讲,MCU 相关的操作系统都具有最基本的多任务管理功能。多任务管理功能包含任务的上下文切换,因其涉及到特定寄存器的操作,都是使用汇编代码开发,相应代码由软件厂商提供。
- p! W6 a; m* L' a! i0 y在 STM32 技术人员的实际支持工作中,例如 IDE 移植,可能需要读懂和修改这些汇编代码。本文就是从这一需求出发,描述AliOS 操作系统里的任务上下文切换的基本原理。读者在明白了上下文切换原理后,去阅读和修改任何 MCU 操作系统的关于任务上下文切换的汇编代码就不会是个难题。本文包含的代码引用基于 STM32F4(ARM Cortex-M4 内核)芯片上的实现。
. D( [3 G$ c+ Q+ U" a' a" d) L' ]4 E3 _: z8 Q) d: F/ _/ C9 R
上下文切换的源动力
: ]# L) {4 ~- _/ @7 k带操作系统的应用程序,上下文切换的逻辑在 PendSV 中断处理函数中完成。 为什么是 PendSV 中断,而不是其它的中断?ARM 技术文档里提到,PendSV 设计为上下文切换服务的中断请求,优先级最低。任务切换的策略可以根据时间片和优先级来制定。切换时机可由系统滴答时钟(SysTick)触发,或者由操作系统根据已有策略来触发 PendSV 中断。
5 I6 }8 `2 I' c% \+ `$ [0 b以 AliOS 的系统时钟中断处理函数为例,则可以看到有如下代码:/ Z% Q; G$ ^$ r, _2 x2 |  [# y
  1. void SysTick_Handler(void)0 H" C! _+ U  ]) x- w
  2. {
    0 ?( {3 ]) u/ G* f
  3. HAL_IncTick();& h! b% n, q5 T& a/ x8 }& S: |1 r
  4. if (0 == HAL_GetTick() % 10) {
    0 o. q& N  w* q: `$ W: N
  5. krhino_intrpt_enter();
    $ v5 k9 l' v6 b; l/ Q+ `9 W
  6. krhino_tick_proc();4 k0 L' }& @6 c9 z3 k& V" R
  7. krhino_intrpt_exit();3 p8 G) O' C! U$ _% e7 `% q& V$ T( F6 \
  8. }
    / C( C5 c! j+ E& g1 o
  9. //HAL_SYSTICK_IRQHandler();7 p- `. w/ n/ C% e
  10. }
复制代码
在 AliOS 中有很多地方,根据系统的需要,还可直接调用 cpu_task_switch 来触发 PendSV 中断。这一部分是纯软件的设计,不再列举。
* B5 N" V, A! t, w! d9 s8 h
  1. cpu_task_switch:3 J! ]. @! {4 ^/ w4 Q
  2. LDR R0, =SCB_ICSR7 C' r& n6 H0 R8 z$ {1 D
  3. LDR R1, =ICSR_PENDSVSET
    2 a2 P2 ?5 P$ z. D* S  n. ^
  4. STR R1, [R0]
    8 ^, r+ h6 {- q, K0 u0 ]/ \$ h
  5. BX LR
复制代码

% k* {9 U9 x& c2 x' R  V7 u上下文切换
' g3 g  O1 b2 q6 ^+ r  y" |上下文切换的汇编代码实现的功能就是把前一个与当前任务相关的寄存器信息进行压栈保存,比如当前栈指针、TCB 信息等,把将要运行的新任务的栈内信息恢复到对应寄存器中为新任务的执行做好准备。这里的上下文就是指各个寄存器信息。
" k' }7 q. b0 @$ E9 L( M
+ p# ?9 W2 u6 x/ ]. N4 x3 k" ~
( W$ N: r! I/ k! k$ {$ _寄存器分类" P/ [3 r8 u5 _" @1 C9 ~6 a
 通用寄存器/ r  D. I3 \' E1 K8 }5 r6 b6 F! j
      R0-R12 是 32 位通用寄存器。
: Q+ C( o0 \! m/ y8 S 堆栈指针 SP- H$ D4 }! Z: [
      R13 是堆栈指针寄存器。堆栈分为复位后缺省使用的主堆栈(由 MSP 指向)以及用户应用程序使用的任务堆栈(由PSP 指向)。可以通过控制寄存器控制当前正在使用的堆栈。
( i8 G5 q# b; M& }+ w) Z2 K% m 链接寄存器 LR) X& R. n7 v- U! ^, c2 C! j
      R14 是链接寄存器,用在函数或者异常/中断返回。9 c7 I* F# y' `3 r
 程序计数器 PC
- g" B& C" O9 }$ @8 P0 U       R15 是程序计数器。它指向当前运行的指令地址。% m" Q5 b' Q/ s1 Q. O6 x: a
 程序状态寄存器 xPSR
# P9 {/ l1 G1 ]2 k( Y9 o6 X程序状态寄存器包含三个寄存器:应用程序状态寄存器 APSR, 中断程序状态寄存器 IPSR 和执行程序状态寄存器EPSR。APSR 包含上一条指令执行后的条件标志,IPSR 包含有中断号,EPSR 包含有被中断指令的待执行寄存器信息,诸如 LDM、STM、PUSH、POP,或 IF-THEN 条件指令的信息以及当前是否运行在 Thumb 状态。
. Y8 V* x' w2 T1 O: r! Q 优先级掩码寄存器 PRIMASK(1 位). I* N' A* J# [( o* J5 X
         PRIMASK 置 1 后,系统阻止除 NMI 和 hard fault 外的所有异常/中断。
; k- S+ L6 m* o; [! M5 H 故障掩码寄存器 FAULTMASK(1 位)- T' @' i! G8 U
         FAULTMASK 置 1 后,系统阻止除 NMI 外的 所有异常/中断。
' n- h9 Q( q8 d* J 基线掩码寄存器 BASEPRI(9 位)
. A7 ~2 R6 i/ q0 _9 s% x" ^; m  G         根据在 BASEPRI 中的优先级设置,系统阻止优先级小于等于它的所有异常/中断。2 a- t3 |" p, N: f
 控制寄存器 CONTROL
3 h" G4 E! S9 `8 h# i; V( X        CONTROL 寄存器控制在线程(THREAD)模式下所处的特权级别(特权级还是用户级),以及当前系统使用的堆栈。中断处理(Handler)模式只能处于特权级,总是使用主堆栈(由 MSP 指向)。可以通过读出 SPSEL 位来进行验证。线程(THREAD)模式可以通过 CONTROL 寄存器切换是使用主堆栈(由 MSP 指向),还是任务堆栈(由 PSP 指向)。4 E! J: v4 B% c" z' ^' y) i2 G0 X
! }7 `/ I- B* D: B$ j
硬件管理的寄存器
' I, q! h: B9 z5 y# Y在上下文切换的中断处理中,一部分寄存器由硬件管理。也就是说,在进入 PendSV 中断处理函数时一部分寄存器信息被自动压栈保存,在退出 PendSV 中断时这些寄存器自动被弹出。  }3 z6 J1 Y8 R0 s# O  F% y
根据 ARM 文档,在上下文切换中,不考虑浮点和考虑 32 个单精度或者 16 个双精度浮点寄存器两种情况下,由硬件管理的寄存器列表分别如下:
. [! ~7 {( q% x( E" ?- ?: _( z0 j: c$ z- g5 ^
~PY5F8C)QOEO51M9]2LWH@R.png 2 c! d0 }$ r. ]" s- P

% X  b  w' s  B3 J9 S! ^0 n我们要特别注意主堆栈 MSP 以及任务堆栈 PSP 在这里的用法。5 r# x& X1 y# y3 [( R
进入 PendSV 中断处理函数,与任务有关的硬件管理的寄存器信息被自动压栈进 PSP 中(因为任务是用户应用程序的一部分,因此使用的是任务堆栈);在退出 PendSV 时,是 PSP 里的相应内容被自动弹出。读者可以在中断之外查看CONTROL 寄存器的 SPSEL 位,值为 1 表示的是当前任务使用任务堆栈(由 PSP 所指向)。读者可在 PendSV 的汇编代码里看到以下代码,目的是将 PSP 里的任务堆栈恢复:2 ~5 r+ t1 L1 X, n
  1. /* return stack = PSP */
    / v4 @5 g3 N  n% |
  2. MSR PSP, R05 h7 j" y# {8 M' z  U
  3. ORR LR, LR, #0x04
复制代码
" _* z9 i; U6 @& ?
但是,PendSV 本身和所有其他的中断处理函数一样,使用的是主堆栈(由 MSP 指向)。这使我们不用担心中断处理函数是否弄脏了任务的堆栈,比如给任务堆栈添加了不必要的内容。这里值得一提的是,从逻辑上来说,使用一个堆栈指针就已经足够了。$ m" H- n/ k3 N8 D* s
, A6 R  {( f4 Q" f
软件管理的寄存器6 h2 M% B; ~% p! [& @# q
不考虑浮点,则 R4~R11 和 LR 寄存器需要软件在中断处理函数里进行处理。软件管理的寄存器在压栈时,紧随在硬件管理的寄存器之后。栈是向地址减小的方向生长的。从这个角度来看,软件管理的寄存器在栈里处于更低的地址。若考虑浮点,则需要额外压栈浮点寄存器:D8~D15
/ [' W7 u0 @" s2 V在 PendSV 相应的压栈代码和弹栈代码如下:: |* O% {8 T6 M0 K( i
  1. ;save context& W  N! @( b* ]8 s1 h, R7 @; R* M
  2. IF {FPU} != "SoftVFP"
    6 [, e  K2 R1 }6 E% h+ D1 T
  3. VSTMFD R0!, {D8 - D15}
    # Z0 V/ Y( a% W* n0 p- q
  4. ENDIF1 T( Y) Z/ M+ i* m5 F, A2 P: C
  5. SUBS R0, R0, #0x24
    3 j7 K5 Z5 b& K3 `$ f* ^
  6. STM R0, {R4-R11, LR}& M  P7 @5 n" z$ X6 Y7 N
  7. ;restore context
    : K  T) H% W/ s" z
  8. LDM R0, {R4-R11, LR}
    & z7 ]6 d: V8 c4 @  |) p5 [
  9. ADDS R0, R0, #0x24
    / a+ P/ o/ O, E  u/ m8 }$ }
  10. IF {FPU} != "SoftVFP"
    ; o2 [( Z: L: I) o- X
  11. VLDMFD R0!, {D8 - D15}1 C- b* L4 B, i6 j5 N
  12. ENDIF
复制代码
+ _: {5 ]" Z  Q* Z# S; m! Z
锐利的读者可能已经注意到,软件和硬件都同时对 LR 进行了压栈。事实上,软件压栈的 LR 包含的是异常返回值EXC_RETURN,该值可指定异常返回时的堆栈(主堆栈还是用户堆栈)和处理器工作模式(用户模式还是特权模式)。
* t( p" \5 k: h$ z; n7 {$ f& {& W$ j# [( t- c
PendSV 的处理流程
" h/ a. n' o+ o+ Y& _硬件管理的寄存器,不需要在 PendSV 函数里手动进行处理。另外,切换到第一个任务(没有更早的任务),也不需要保存当前任务的的寄存器信息到堆栈。已经保存有寄存器信息的堆栈栈顶指针,一般存在任务的控制结构里;以 AliOS 为例,在进入 PendSV 中断处理前已经将下一个任务的控制结构准备就绪,可以通过 g_preferred_ready_task 进行访问。
) [/ y1 t* _: R3 z6 w, _) b) ?
* k2 ^, q2 ^0 a+ c/ E! ^) h %XOS9`$BN31T[~1UWA2U[[T.png 3 o: u7 o# q( k' P- ?4 {

+ L7 A* {0 t7 h0 w6 v小结一下:对于 AliOS 操作系统,每个 task 在创建时用户都会为它指定一段存储区域作为物理上该任务的 task_stack;PSP永远指向当前任务的 task_stack;在任务切换时,执行 PendSV:切换 PSP,并且把内核的当前状态(寄存器组的值)保存到当前这个马上要被切走的老任务的 task_stack —— 保存上下文@入栈,再把马上要被调度的新任务上下文恢复到内核的寄存器中  恢复上下文 @ 出栈。
9 n6 {9 U( L7 d1 s+ r; v
% ~5 X/ j$ y+ |+ Q3 S1 w7 f9 ~+ Q6 \% J' L
总结
. h& V, a7 r" w: B, ~% j3 T本文简单介绍了 AliOS 的任务上下文切换原理,包括哪些是硬件负责入栈和出栈的寄存器,哪些是软件负责入栈和出栈的寄存器,以及上下文切换的流程。理解 MCU 操作系统的上下文切换原理,有助于阅读操作系统厂商的汇编源代码,进行移植或者其它高级应用。, c( ]% t. f' f) s! P/ p# Q9 T4 N: f
关中断1 g4 S3 Z6 R' a

: B7 w# H6 C6 b# s6 o2 a附录' J# m2 F+ ^( D
  1. PendSV_Handler& ?" o1 S3 f+ \
  2. CPSID I
    ! `- s' ]5 }, A  W4 C) k+ I5 t
  3. MRS R0, PSP // RO 指向当前的任务堆栈栈顶
    & H8 `2 Q, _9 k; U
  4. ;branch if cpu_first_task_start5 f! X7 W, _- z1 K
  5. CBZ R0, _pendsv_handler_nosave
    / t% X" x6 i$ G1 X  `( Q; T
  6. ;hardware saved R0~R3,R12,LR,PC,xPSR
    # n& G- i$ L3 M# u" y
  7. ;save context% p2 T+ X3 {+ F, q- n7 D' P
  8. IF {FPU} != "SoftVFP"
    ( R  f% o8 z: _, A3 U
  9. VSTMFD R0!, {D8 - D15}
    . K2 O- I' L% U+ r; M
  10. ENDIF
    3 `7 T6 L6 b: X) S' ^6 Y
  11. SUBS R0, R0, #0x24. D) j0 [( ?% W' V& w2 a
  12. STM R0, {R4-R11, LR} // 把当前任务的上下文保存到任务堆栈(栈顶也随之调整)' {( \0 k7 A; a7 W; k9 `- h
  13. ;g_active_task->task_stack = context region, L. ~' L7 b8 ~9 y
  14. LDR R1, =g_active_task
    6 C2 G2 }: U- L. D
  15. LDR R1, [R1]) Q8 o, C& U  ^# G
  16. STR R0, [R1]
    + p0 E0 k! t: d' q8 R' J8 G0 W) C
  17. bl krhino_stack_ovf_check8 s$ L' v9 t. r& z: R
  18. _pendsv_handler_nosave
    + Q( u4 A* I9 o% ]9 _& G7 q! O4 N
  19. LDR R0, =g_active_task
    % }8 B; `) C4 S- [
  20. LDR R1, =g_preferred_ready_task; H0 F3 ?6 A6 d; G5 U0 F
  21. LDR R2, [R1]
    3 a" q8 D/ u- u9 W
  22. STR R2, [R0]
    * a" {( s5 V, d  s' `) ?- _7 n
  23. ;R0 = g_active_task->task_stack = context region
    $ {' l+ L6 H5 ]( q5 n; `4 x
  24. LDR R0, [R2] // L0 指向新任务控制块中设置的任务堆栈+ ^) B$ v% m* o1 u5 w
  25. ;restore context$ f$ P' f  e! M" @& C4 k
  26. LDM R0, {R4-R11, LR}
    " C  s7 V8 L: ~. d
  27. ADDS R0, R0, #0x24 // 恢复新任务的上下文(从其任务堆栈中读到内核寄存器组)0 x7 h* K& _; K2 `9 g
  28. IF {FPU} != "SoftVFP"/ ?1 }$ e6 `9 n- Y2 D
  29. VLDMFD R0!, {D8 - D15}
    ' U5 Z( d7 Z' q
  30. ENDIF
    + H5 ]& L* s5 x0 v* j5 C( s' H
  31. ;return stack = PSP
    & h5 {1 p+ B) y
  32. MSR PSP, R0 // PSP 指向新任务的堆栈
    1 {6 ^0 S5 @+ {# S2 Z+ L( S8 w0 P$ }
  33. ;after exception return: stack = PSP4 j3 }/ `/ n' ]- i0 [5 L
  34. ORR LR, LR, #0x04$ o4 T7 j9 W- c3 ~+ f5 O% E9 h

  35. 2 d- \, a7 k2 {6 u% O+ o3 q
  36. CPSIE I: E8 f3 b, ?; y' j4 e
  37. ;hardware restore R0~R3,R12,LR,PC,xPSR  u6 b& q$ a3 E; k% ^) A  A( U! X
  38. BX LR; U$ y& |/ ?7 }' R0 I
  39. ALIGN! X  s5 W; Z) L. [, ^
  40. END
复制代码

; _  C. w: o) {# _5 LLDR R0, [R1]:把以 R1 为地址的存储区内容读到 R0 中 【Memory - R】右到左
5 O7 G4 L5 [+ ]6 qSTR R0, [R1]:把 R0 存储到以 R1 为地址的存储区 【R - Memory】左到右* v. d/ B! s! h. k* N
LDM R0, {R4-R11, LR}:把以 R0 所指的存储区内容读到寄存器组中 【Memory - Ri】左到右4 c! M- [8 [0 P! ^/ _' f
STM R0, {R4-R11, LR}:把寄存器组的内容保存到 R0 所指的存储区 【Ri - Memory】右到左9 p; r! x, T6 `7 ]( V: o
MRS R0, PSP:把状态寄存器(PSP)的值读到 R0 右到左! _6 I  {5 p  a! b0 T5 T8 Y
MSR PSP, R0:把 R0 写到状态寄存器(PSP)中 右到左! c. A4 b$ K1 \4 \3 u* [
3 E1 U: J; m: e9 H
赞 收藏 评论0 发布时间:2022-3-3 23:58

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版