STM32でコンテキストを切り替える方法

こんにちは



ストリーム...コンテキストの切り替え... OSの基本的な本質。 そしてもちろん、ライブラリとアプリケーションを開発するとき、スレッドの実装にエラーがないという事実に常に依存しています。 そのため、ネットワーク、ファイルシステム、および多くのサードパーティライブラリが長時間機能していたときに、STM32のスレッドをEmbox RTOSに切り替える際に大きなエラーを見つけることは予想外でした。 そして、 Habréでの成果について自慢することさえできました。



Cortex-Mのスレッド切り替えをどのように行い、STM32でテストしたかについてお話したいと思います。 さらに、他のOS(NuttXおよびFreeRTOS)でこれがどのように行われるかについても説明します。



さて、最初に、問題がどのように発見されたかについてのいくつかの言葉。 その瞬間、私は別の作品を集めていました-異なるセンサーを備えたロボットです。 ある時点で、2つのステッピングモーターを制御したいのですが、それぞれが別々のストリームから制御されていました(フローはまったく同じです)。 結果-1つのモーターが回転を終了するまで、2番目のモーターは始動さえしません。



デバッグのために座った。 すべての割り込みがスレッドで単に無効にされていることが判明しました! あなたは、どうして何かがうまくいくのでしょうか? すべてが単純ですmutex_lock()



mutex_lock()



、およびその他の「待機」が存在する場所が多くあり、それらによってフローが自然に切り替わります。 問題は明らかにSTM32F4のコンテキストスイッチングに関連しており、その上で発見しました。



問題をより詳細に分析しましょう。 フローのコンテキストの切り替えは、タイマーによるもの、つまり割り込みによるものを含みます。 概略的に、Emboxの割り込み処理は次のように表すことができます。



 void irq_handler(pt_regs_t *regs) { ... int irq = get_irq_number(regs); { ipl_enable(); irq_dispatch(irq); ipl_disable(); } irqctrl_eoi(irq); ... critical_dispatch_pending(); }
      
      





全体のポイントは、 irq_dispatch



割り込みハンドラーがirq_dispatch



irq_dispatch



、その後割り込みハンドラーが「終了」し、スケジューラーがcritical_dispatch_pending



内で必要とする場合、コンテキストは別のスレッドに切り替わります。 ここで、このスレッドのプロセッサの状態は、割り込みの許可または禁止を含め、割り込み前と同じであることが非常に重要です。 xPSR



のビットは、割り込みを解決する役割を果たします。割り込みxPSR



、プロセッサ自体が割り込みを開始するとスタックにxPSR



され、割り込みを終了するとスタックから取得します。 問題は、プリエンプティブマルチタスクを使用しているため、あるスレッドで割り込みが発生した場合、 xPSR



保存されていない別のスレッドのスタックで終了したい場合があることxPSR



。 さらに、ほとんどのOSのように、たとえばpthread_mutex_lock()



などの同期プリミティブがあります。これにより、割り込みからではなくコンテキストの切り替えが可能になります。 一般的に、このアーキテクチャは小さなタスク向けに最適化されているため、cortex-mでプリエンプティブマルチタスクを編成できるかどうかを疑い始めました。 しかし、やめて! しかし、他のOSはどのように機能しますか?



Cortex-Mでの割り込み処理



まず、Cortex-Mでの割り込み処理の仕組みを理解しましょう。









図は、2つのモードでスタックを示しています-浮動小数点ありとなし。 割り込みが発生すると、プロセッサは対応するレジスタをスタックに保存し、次の表の次の値のいずれかをLR



レジスタに入れます。 つまり、割り込みがネストされている場合、0xFFFFFFF1が存在します。







次に、OS割り込みハンドラーが呼び出され、その最後で通常「bx lr」が実行されます(0xFFFFFFXXがLRにあることを思い出してください)。 その後、自動的に保存されたレジスタが復元され、プログラムの実行が続行されます。



次に、異なるOSでコンテキストの切り替えがどのように発生するかを見てみましょう。



FreeRTOS



FreeRTOSから始めましょう。 これを行うには、 portable/GCC/ARM_CM4F/port.c



以下は、 xPortSysTickHandler



関数のコードです。



xPortSysTickHandler
 void xPortSysTickHandler( void ) { /* The SysTick runs at the lowest interrupt priority, so when this interrupt executes all interrupts must be unmasked. There is therefore no need to save and then restore the interrupt mask value as its value is already known. */ portDISABLE_INTERRUPTS(); { /* Increment the RTOS tick. */ if( xTaskIncrementTick() != pdFALSE ) { /* A context switch is required. Context switching is performed in the PendSV interrupt. Pend the PendSV interrupt. */ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } } portENABLE_INTERRUPTS(); }
      
      







これはハードウェアタイマーハンドラーです。 ここで、コンテキスト切り替えを行う必要がある場合、特定のPendSV割り込みがトリガーされることがわかります。 ドキュメントにあるように、「PendSVはシステムレベルのサービスに対する割り込み駆動型の要求です。 OS環境では、他の例外がアクティブでない場合、コンテキスト切り替えにPendSVを使用します。” xPortPendSVHandler



割り込みハンドラー内で、コンテキストは直接切り替えられます。



xPortPendSVHandler
 void xPortPendSVHandler( void ) { /* This is a naked function. */ __asm volatile ( " mrs r0, psp \n" " isb \n" " \n" " ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */ " ldr r2, [r3] \n" " \n" " tst r14, #0x10 \n" /* Is the task using the FPU context? If so, push high vfp registers. */ " it eq \n" " vstmdbeq r0!, {s16-s31} \n" " \n" " stmdb r0!, {r4-r11, r14} \n" /* Save the core registers. */ " \n" " str r0, [r2] \n" /* Save the new top of stack into the first member of the TCB. */ " \n" " stmdb sp!, {r3} \n" " mov r0, %0 \n" " msr basepri, r0 \n" " dsb \n" " isb \n" " bl vTaskSwitchContext \n" " mov r0, #0 \n" " msr basepri, r0 \n" " ldmia sp!, {r3} \n" " \n" " ldr r1, [r3] \n" /* The first item in pxCurrentTCB is the task top of stack. */ " ldr r0, [r1] \n" " \n" " ldmia r0!, {r4-r11, r14} \n" /* Pop the core registers. */ " \n" " tst r14, #0x10 \n" /* Is the task using the FPU context? If so, pop the high vfp registers too. */ " it eq \n" " vstmdbeq r0!, {s16-s31} \n" " \n" " stmdb r0!, {r4-r11, r14} \n" /* Save the core registers. */ " \n" " str r0, [r2] \n" /* Save the new top of stack into the first member of the TCB. */ " \n" " stmdb sp!, {r3} \n" " mov r0, %0 \n" " msr basepri, r0 \n" " dsb \n" " isb \n" " bl vTaskSwitchContext \n" " mov r0, #0 \n" " msr basepri, r0 \n" " ldmia sp!, {r3} \n" " \n" " ldr r1, [r3] \n" /* The first item in pxCurrentTCB is the task top of stack. */ " ldr r0, [r1] \n" " \n" " ldmia r0!, {r4-r11, r14} \n" /* Pop the core registers. */ " \n" " tst r14, #0x10 \n" /* Is the task using the FPU context? If so, pop the high vfp registers too. */ " it eq \n" " vldmiaeq r0!, {s16-s31} \n" " \n" " msr psp, r0 \n" " isb \n" " \n" #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */ #if WORKAROUND_PMU_CM001 == 1 " push { r14 } \n" " pop { pc } \n" #endif #endif " \n" " bx r14 \n" " \n" " .align 4 \n" "pxCurrentTCBConst: .word pxCurrentTCB \n" ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) ); }
      
      







しかし、ここで、たとえば特定の関数fn



を実行する新しいスレッドに切り替えると想像してみましょう。 つまり、関数fn



アドレスをPC



に単純に配置すると、すぐに正しい場所に到達しますが、コンテキストが間違っています-割り込みを終了しませんでした! FreeRTOSは次のソリューションを提供します。 割り込みを終了するかのように、作成されたストリームを初期化しましょう- /* Simulate the stack frame as it would be created by a context switch interrupt. */



/* Simulate the stack frame as it would be created by a context switch interrupt. */



。 この場合、最初にxPortPendSVHandler



ハンドラーを「 xPortPendSVHandler



。つまり、正しいコンテキストになり、準備されたスタックに従ってfn



進みます。 以下は、そのようなスレッド準備のコードです。



pxPortInitialiseStack
 StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters ) { /* Simulate the stack frame as it would be created by a context switch interrupt. */ /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts, and to ensure alignment. */ pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; /* xPSR */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */ /* Save code space by skipping register initialisation. */ pxTopOfStack -= 5; /* R12, R3, R2 and R1. */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ /* A save method is being used that requires each task to maintain its own exec return value. */ pxTopOfStack--; *pxTopOfStack = portINITIAL_EXEC_RETURN; pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */ return pxTopOfStack; }
      
      







それが、FreeRTOSが提案した方法の1つでした。



ナタックス



NuttXによって提案された別のメソッドを見てみましょう。 これは、鉄のさまざまな小片に対するもう1つの相対的な既知のOSです。



割り込み処理の主要部分は、アセンブラコードから呼び出される本質的に第2レベルの割り込みハンドラであるup_doirq



関数内で発生します。 別のスレッドに切り替えるかどうかを決定します。 この関数は、新しいスレッドの必要なコンテキストを返します。



up_doirq
 uint32_t *up_doirq(int irq, uint32_t *regs) { board_autoled_on(LED_INIRQ); #ifdef CONFIG_SUPPRESS_INTERRUPTS PANIC(); #else uint32_t *savestate; /* Nested interrupts are not supported in this implementation. If you want * to implement nested interrupts, you would have to (1) change the way that * CURRENT_REGS is handled and (2) the design associated with * CONFIG_ARCH_INTERRUPTSTACK. The savestate variable will not work for * that purpose as implemented here because only the outermost nested * interrupt can result in a context switch. */ /* Current regs non-zero indicates that we are processing an interrupt; * CURRENT_REGS is also used to manage interrupt level context switches. */ savestate = (uint32_t *)CURRENT_REGS; CURRENT_REGS = regs; /* Acknowledge the interrupt */ up_ack_irq(irq); /* Deliver the IRQ */ irq_dispatch(irq, regs); /* If a context switch occurred while processing the interrupt then * CURRENT_REGS may have change value. If we return any value different * from the input regs, then the lower level will know that a context * switch occurred during interrupt processing. */ regs = (uint32_t *)CURRENT_REGS; /* Restore the previous value of CURRENT_REGS. NULL would indicate that * we are no longer in an interrupt handler. It will be non-NULL if we * are returning from a nested interrupt. */ CURRENT_REGS = savestate; #endif board_autoled_off(LED_INIRQ); return regs; }
      
      







関数から戻った後、再び第1レベルのハンドラーに戻ります。 また、新しいスレッドに切り替える必要がある場合は、割り込み処理の完了時に目的のストリームに入るように、スタックの割り込みに入るときに自動的に保存されるレジスタを変更します。 以下はコードのスニペットです。



  bl up_doirq /* R0=IRQ, R1=register save (msp) */ mov r1, r4 /* Recover R1=main stack pointer */ /* On return from up_doirq, R0 will hold a pointer to register context * array to use for the interrupt return. If that return value is the same * as current stack pointer, then things are relatively easy. */ cmp r0, r1 /* Context switch? */ beq l2 /* Branch if no context switch */ //   … /* We are returning with a pending context switch. This case is different * because in this case, the register save structure does not lie in the * stack but, rather, within a TCB structure. We'll have to copy some * values to the stack. */ add r1, r0, #SW_XCPT_SIZE /* R1=Address of HW save area in reg array */ ldmia r1, {r4-r11} /* Fetch eight registers in HW save area */ ldr r1, [r0, #(4*REG_SP)] /* R1=Value of SP before interrupt */ stmdb r1!, {r4-r11} /* Store eight registers in HW save area */ #ifdef CONFIG_BUILD_PROTECTED ldmia r0, {r2-r11,r14} /* Recover R4-R11, r14 + 2 temp values */ #else ldmia r0, {r2-r11} /* Recover R4-R11 + 2 temp values */ #endif …
      
      





つまり、Nuttxでは(FreeRTOSとは異なり)スタックに自動的に保存されるレジスタ値はすでに変更されています。 これがおそらく主な違いです。 さらに、PendSVなしでも非常にうまく機能することがわかります(ただし、ARMでは:)を推奨しています)。 さて、最後のもの-コンテキストの切り替え自体は遅延し、原則ではなく、割り込みスタックを介して行われます-「古い値を保持し、すぐに新しい値をレジスタにロードしました」。



Embox



最後に、これがEmboxでどのように行われるかについて。 主なアイデアは、追加の関数を追加することです( __irq_trampoline



と呼びましょう)。このモードでは、割り込み処理モードではなく、既に「通常モード」でコンテキスト切り替えを行い、その後、実際に割り込みハンドラを終了します。 つまり、言い換えると、記事の冒頭で説明したロジックを完全に保存しようとしたということです。



 void irq_handler(pt_regs_t *regs) { ... int irq = get_irq_number(regs); { ipl_enable(); irq_dispatch(irq); ipl_disable(); } irqctrl_eoi(irq); //      ,     ... }
      
      





まず、全体像を示す写真を提供します。 そして、何が何であるかを部分的に説明します。







これはどのように行われますか? その考え方は次のとおりです。 割り込みハンドラは、他のプラットフォームと同様に、通常の方法で最初に実行されます。 しかし、ハンドラーを終了するとき、実際にスタックを変更し、 __pending_handle



まったく異なる場所に__pending_handle



ます! これは、 __pending_handle



関数の入力で実際に割り込みが発生したかのように発生します。 以下は、スタックを変更して__pending_handle



終了する__pending_handle



です。 私はロシア語の特に重要な場所にコメントを書き込もうとしました。



 //        struct cpu_saved_ctx { uint32_t r[5]; uint32_t lr; uint32_t pc; uint32_t psr; }; void interrupt_handle(struct context *regs) { uint32_t source; struct irq_saved_state state; struct cpu_saved_ctx *ctx; ... //    ,  state.sp = regs->sp; state.lr = regs->lr; assert(!interrupted_from_fpu_mode(state.lr)); ctx = (struct cpu_saved_ctx*) state.sp; memcpy(&state.ctx, ctx, sizeof *ctx); //        /* It does not matter what value of psr is, just set up sime correct value. * This value is only used to go further, after return from interrupt_handle. * 0x01000000 is a default value of psr and (ctx->psr & 0xFF) is irq number if any. */ ctx->psr = 0x01000000 | (ctx->psr & 0xFF); ctx->r[0] = (uint32_t) &state; // we want pass the state to __pending_handle() ctx->r[1] = (uint32_t) regs; // we want pass the registers to __pending_handle() ctx->lr = (uint32_t) __pending_handle; ctx->pc = ctx->lr; /* Now return from interrupt context into __pending_handle */ __irq_trampoline(state.sp, state.lr); }
      
      





関数コード__irq_trampoline



も提供します。 関数へのコメントには、SPからの読み取りが示されていますが、記事を過負荷にしないために、これをスキップします。 主なものは、関数の最後にある「bx r1」です。 __irq_trampoline



関数の2番目の引数はr1レジスタにあることを思い出してください。 上記のコードを見ると、「 __irq_trampoline(state.sp, state.lr)



」という呼び出しが表示されます。これは、レジスターr1がstate.lrの値であり、値0xFFFFFXXに等しいことを意味します(最初のセクションを参照)



__irq_trampoline
 .global __irq_trampoline __irq_trampoline: cpsid i # r0 contains SP stored on interrupt handler entry. So we keep some data # behind SP for a while, but interrupts are disabled by 'cpsid i' mov sp, r0 # Return from interrupt handling to usual mode bx r1
      
      







要するに、 __irq_trampoline



関数を終了した後、スタックを解き、割り込みを終了し、 __pending_handle



ます。 この関数では、残りのすべての操作(コンテキストスイッチなど)を実行します。 同時に、この関数を終了するとき、元々保存されていたレジスタの値をスタックに戻す必要があります。その後、再び割り込みに入り、元の場所に戻ります! このために、次のことが行われます。 最初にスタックを準備し、次にPendSV割り込みを開始__pendsv_handle



てから、 __pendsv_handle



ハンドラーで__pendsv_handle



を見つけます。 そして、正直なところ、通常の方法でハンドラーを終了しますが、すでに元の古いスタックに沿っています。 __pending_handle



および__pendsv_handle



のコードを以下に示します。



__pending_handleおよび__pendsv_handle
 .global __pending_handle __pending_handle: //     “” ,     //  -,       . # Push initial saved context (state.ctx) on top of the stack add r0, #32 ldmdb r0, {r4 - r11} push {r4 - r11} //    .      , //      , . ... cpsie i //    ,   bl critical_dispatch_pending cpsid i # Generate PendSV interrupt //    PendSV,    bl nvic_set_pendsv cpsie i # DO NOT RETURN 1: b 1 .global __pendsv_handle __pendsv_handle: # 32 == sizeof (struct cpu_saved_ctx) add sp, #32 # Return to the place we were interrupted at, # ie before interrupt_handle_enter bx r14
      
      







結論として、context_switchの実装の考慮されたバージョンに関するいくつかのフレーズを言います。 考慮された各方法は機能しており、独自の利点と欠点があります。 FreeRTOSオプションは、特定のチップに特定の「ハードコード化された」context_switchを必要とするマイクロコントローラーを主に対象としているため、私たちにはあまり適していません。 そして、私たちのOSでは、「大」OSの原理を使用するマイクロコントローラーさえも提供しようとしています... NuttXにはほぼ同じアプローチがあり、スタックを変更するというアイデアを使用して、同様のアプローチを実装するか、改善することができます。 しかし、現時点では、このバージョンはタスクに非常に対応しています 。これは、 リポジトリからコードを取得した場合に確認できます



All Articles