
こんにちは
ストリーム...コンテキストの切り替え... 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 ) { portDISABLE_INTERRUPTS(); { if( xTaskIncrementTick() != pdFALSE ) { portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } } portENABLE_INTERRUPTS(); }
これはハードウェアタイマーハンドラーです。 ここで、コンテキスト切り替えを行う必要がある場合、特定のPendSV割り込みがトリガーされることがわかります。
ドキュメントにあるように、「PendSVはシステムレベルのサービスに対する割り込み駆動型の要求です。 OS環境では、他の例外がアクティブでない場合、コンテキスト切り替えにPendSVを使用します。”
xPortPendSVHandler
割り込みハンドラー内で、コンテキストは直接切り替えられます。
xPortPendSVHandler void xPortPendSVHandler( void ) { __asm volatile ( " mrs r0, psp \n" " isb \n" " \n" " ldr r3, pxCurrentTCBConst \n" " ldr r2, [r3] \n" " \n" " tst r14, #0x10 \n" " it eq \n" " vstmdbeq r0!, {s16-s31} \n" " \n" " stmdb r0!, {r4-r11, r14} \n" " \n" " str r0, [r2] \n" " \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" " ldr r0, [r1] \n" " \n" " ldmia r0!, {r4-r11, r14} \n" " \n" " tst r14, #0x10 \n" " it eq \n" " vstmdbeq r0!, {s16-s31} \n" " \n" " stmdb r0!, {r4-r11, r14} \n" " \n" " str r0, [r2] \n" " \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" " ldr r0, [r1] \n" " \n" " ldmia r0!, {r4-r11, r14} \n" " \n" " tst r14, #0x10 \n" " it eq \n" " vldmiaeq r0!, {s16-s31} \n" " \n" " msr psp, r0 \n" " isb \n" " \n" #ifdef WORKAROUND_PMU_CM001 #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 ) { pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; pxTopOfStack--; *pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; pxTopOfStack -= 5; *pxTopOfStack = ( StackType_t ) pvParameters; pxTopOfStack--; *pxTopOfStack = portINITIAL_EXEC_RETURN; pxTopOfStack -= 8; 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; savestate = (uint32_t *)CURRENT_REGS; CURRENT_REGS = regs; up_ack_irq(irq); irq_dispatch(irq, regs); regs = (uint32_t *)CURRENT_REGS; CURRENT_REGS = savestate; #endif board_autoled_off(LED_INIRQ); return regs; }
関数から戻った後、再び第1レベルのハンドラーに戻ります。 また、新しいスレッドに切り替える必要がある場合は、割り込み処理の完了時に目的のストリームに入るように、スタックの割り込みに入るときに自動的に保存されるレジスタを変更します。 以下はコードのスニペットです。
bl up_doirq mov r1, r4 cmp r0, r1 beq l2 // … add r1, r0, #SW_XCPT_SIZE ldmia r1, {r4-r11} ldr r1, [r0, #(4*REG_SP)] stmdb r1!, {r4-r11} #ifdef CONFIG_BUILD_PROTECTED ldmia r0, {r2-r11,r14} #else ldmia r0, {r2-r11} #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
です。 私はロシア語の特に重要な場所にコメントを書き込もうとしました。
関数コード
__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にはほぼ同じアプローチがあり、スタックを変更するというアイデアを使用して、同様のアプローチを実装するか、改善することができます。 しかし、現時点では、このバージョンはタスクに非常に対応してい
ます 。これは、
リポジトリからコードを取得した場合に確認でき
ます 。