魏東山氏によるRTOS入門講座
コースリンク: Wei Dongshan ライブ公開クラス: RTOS 実践プロジェクト マルチタスク システムの実現 セクション 1: ベア メタル プログラムのフレームワークと欠陥_哔哩哔哩_bilibili
RTOS の概要
ベアメタル: 固定順序で実行。
中断: 中断がトリガーされるまで、ループ内の処理に集中できます。割り込みにフラグを設定してループ内の実行を検出し、割り込みのタイムアウトを防ぐこともできます。
タイマー:タスクが多すぎる場合、割り込みの使用には適していません。タイマーを使用して、さまざまなタスクの実行頻度を設定できます。たとえば、A は 1 ミリ秒に 1 回、B は 2 ミリ秒に 1 回実行します。ただし、これらは相互に影響します。たとえば、A がスタックすると、B にも影響します。 。
もう 1 つの解決策はステート マシンです。各関数は複数の状態を設定し、状態の一部が実行されるたびに、現在の状態が保持されて終了し、次に開始されたときに実行が継続されます。
ステート マシンには、状態、イベント、アクション、変換という 4 つの概念があります。
州は州だ。
イベントとは、操作を実行するためのトリガー条件で、たとえば、ドアが開いている場合はドアを閉じるイベントを開始し、ドアが閉まっている場合はドアを開くイベントを開始します。
アクションは、イベントによってトリガーされる特定の動作です。
遷移とは、状態間の切り替えです。
ステート マシンは扱いにくく、あまり最適ではありません。
たとえば、RTOS は、それほど複雑なステート マシンを記述することなく、特定のタイム スライスに従って実行される各プログラムを設定し、時間になると自動的に切り替えることができます。そして今では RTOS のエコロジー (特に rt-thread) が改善され、ほとんどの開発で RTOS が必要になります。実際、RTOS を使用する方が簡単です。
ARMベース
手順は何ですか?
プログラムを実行するときは、まずプログラムをフラッシュファイルに書き込み、データをRAM(変数)に置き、CPUが命令を読み取ってデータをフェッチし、データを書き込みます。RAM 上のデータは CPU のレジスタに取得されます。
ここでは 6 アームの命令に焦点を当てます。
- 読み取りコマンド。
LDR R0,[R1,#4]
、rd、rs、長さを指定します。LDR は 4B を取得し、R1+4 アドレスから取得するように固定されています。 - 書き込みコマンド。
STR R0,[R1,#4]
。 - 加減。
ADD R0,R1,R2
ADD R0,R0,#1
SUB R0,R1,R2
- 比較する。
CMP R0,R1
結果は PSR に保存されます。 - Jump
B BL
, BL はジャンプ後の戻りアドレスを保存します。
Cのアセンブリコードを解析してプログラムを理解する
非常に単純なプログラムを例に挙げると、Keil がデバッグ モードに入ると、対応するコードのアセンブリ コードが表示されます。
まず、PUSH 命令を通じてスタック r3 lr を自動的にプッシュし、sp ポインタを変更して r3 レジスタと関数の戻りアドレスを保存します。
2 番目の文では、r0 = a のアドレスを設定します。
3 番目の文は、アドレスに従って a の値を取り出し、r0 に格納します。
4 番目の文では、r0 の値がスタック 0 の位置に格納されます。これは、スタックがプッシュされた後、lr r3 が上位から下位に向かってスタックに格納されるためです。つまり、r0 は実際にデータを格納します。スタック内の r3 の位置、および r3 が入ります。スタックはスタック内のビットを占有します。
次に、スタックがポップされ、関数が返すように lr が pc に割り当てられ、r3 がスタックに書き込まれた値を取得します。
2 番目のプログラムを見てください。
int add_val(int *pa, int *pb)
{
volatile int tmp;
tmp = *pa;
tmp += *pb;
return tmp;
}
int mymain()
{
volatile int a = 1;
volatile int b = 2;
volatile int c;
c = add_val(&a, &b);
return 0;
}
コンパイルされたコード (.dis ファイル内):
i.mymain
mymain
0x08000372: b50e .. PUSH {r1-r3,lr}
0x08000374: 2001 . MOVS r0,#1
0x08000376: 9002 .. STR r0,[sp,#8]
0x08000378: 2002 . MOVS r0,#2
0x0800037a: 9001 .. STR r0,[sp,#4]
0x0800037c: a901 .. ADD r1,sp,#4
0x0800037e: a802 .. ADD r0,sp,#8 ;传参
0x08000380: f7ffffca .... BL add_val ; 0x8000318
0x08000384: 9000 .. STR r0,[sp,#0]
0x08000386: 2000 . MOVS r0,#0
0x08000388: bd0e .. POP {r1-r3,pc}
0x0800038a: 0000 .. MOVS r0,r0
i.add_val
add_val
0x08000318: b508 .. PUSH {r3,lr}
0x0800031a: 4602 .F MOV r2,r0
0x0800031c: 6810 .h LDR r0,[r2,#0]
0x0800031e: 9000 .. STR r0,[sp,#0]
0x08000320: 6808 .h LDR r0,[r1,#0]
0x08000322: 9b00 .. LDR r3,[sp,#0]
0x08000324: 4418 .D ADD r0,r0,r3
0x08000326: 9000 .. STR r0,[sp,#0]
0x08000328: 9800 .. LDR r0,[sp,#0]
0x0800032a: bd08 .. POP {r3,pc}
いくつかの関数パラメータ r0 r1... が渡され、r3 を超えるパラメータは通常スタックにプッシュされることがわかります。
パラメーターの問題。2 番目のコードを試してみましょう。4 つのパラメーターを追加します。
プログラム:
int add_val(int a, int b, int c, int d)
{
return a+b+c+d;
}
int mymain()
{
volatile int a = 1;
volatile int b = 2;
volatile int c = 3;
volatile int d = 4;
volatile int sum;
sum = add_val(a,b,c,d);
return 0;
}
i.mymain
mymain
0x0800036a: b500 .. PUSH {lr}
0x0800036c: b085 .. SUB sp,sp,#0x14
0x0800036e: 2001 . MOVS r0,#1
0x08000370: 9004 .. STR r0,[sp,#0x10]
0x08000372: 2002 . MOVS r0,#2
0x08000374: 9003 .. STR r0,[sp,#0xc]
0x08000376: 2003 . MOVS r0,#3
0x08000378: 9002 .. STR r0,[sp,#8]
0x0800037a: 2004 . MOVS r0,#4
0x0800037c: 9001 .. STR r0,[sp,#4]
0x0800037e: e9dd3201 ...2 LDRD r3,r2,[sp,#4]
0x08000382: e9dd1003 .... LDRD r1,r0,[sp,#0xc]
0x08000386: f7ffffc7 .... BL add_val ; 0x8000318
0x0800038a: 9000 .. STR r0,[sp,#0]
0x0800038c: 2000 . MOVS r0,#0
0x0800038e: b005 .. ADD sp,sp,#0x14
0x08000390: bd00 .. POP {pc}
0x08000392: 0000 .. MOVS r0,r0
lr r3 r2 r1 r0 を格納した後、r3 r2 r1 r0 を下位アドレスから上位アドレスにロードし (おそらく入力スタッキングと関数パラメーターの順序が逆になっているため)、その後ジャンプします。
i.add_val
add_val
0x08000318: b510 .. PUSH {r4,lr}
0x0800031a: 4604 .F MOV r4,r0
0x0800031c: 1860 `. ADDS r0,r4,r1
0x0800031e: 4410 .D ADD r0,r0,r2
0x08000320: 4418 .D ADD r0,r0,r3
0x08000322: bd10 .. POP {r4,pc}
これには、関数内のレジスタ保護が含まれます。最近、MIPS のアーキテクチャを調べました。MIPS はさまざまなレジスタ (t、s、a...) に分割されており、アームにもさまざまな機能があります。
r0 ~ r3 はパラメータを渡します。r13 sp、r14 lr、r15 pc。
パラメータを渡す 3 つの関数は保護なしで自由に使用でき、返されたときに値が異なっていても問題ありません。r4 ~ r11 も使用できますが、保存して復元する必要があります。上記の例の add 関数は r4 を使用します。
たとえば、コードが次のように変更されたとします。
int add_val(int a, int b, int c, int d)
{
// 故意使用R4
register int sum asm("r4");
sum = a+b+c+d;
return sum;
}
コンパイル:
i.add_val
add_val
0x08000318: b530 0. PUSH {r4,r5,lr}
0x0800031a: 4604 .F MOV r4,r0
0x0800031c: 1865 e. ADDS r5,r4,r1
0x0800031e: 4415 .D ADD r5,r5,r2
0x08000320: 18e8 .. ADDS r0,r5,r3
0x08000322: bd30 0. POP {r4,r5,pc}
r5 は計算の途中結果に相当し、r5 と r4 の両方が応答する必要があります。
割り込み処理
シーンの保存 - 中断の処理 - シーンの復元、そしてソース プログラムの実行を継続します。どのレジスタを保存するか?
-
まず、パラメータ レジスタを保存する必要があります。保存しないと、関数はまだパラメータを処理しておらず、割り込みパラメータが失われます。
-
r4 ~ r11 は保存する必要があります。保存しない場合、関数は保存するためにスタックにプッシュされていません。これらが失われると、取得することも復元することもできません。
-
lr は保存する必要があります。同様の理由で、lr がスタックにプッシュされていないときに lr が変更された場合、元の位置に戻ることはできません。
実際、割り込みが発生した瞬間にすべてのレジスタを保存する必要があります。
ここで呼び出す c 割り込み処理関数は、r4 ~ r11 が破壊されないことのみを保証できるため、すべてのレジスタを確実に保存できることを確認したい場合は、c 関数を呼び出す前にレジスタを保存する必要があります。ハードウェアは他のレジスタを自動的に保存します。
復元時、ハードウェアは他のレジスタを自動的に復元し、c 関数は r4 ~ r11 を復元することを保証します。
ハードウェアによって保存されるレジスタは、r0 ~ r3、r12、lr、および現在の割り込み戻り位置です。よくある誤解は、lr が現在の割り込みの戻り位置ではないのではないかということです。あまり。例えば、main関数がA関数を呼び出し、A関数の実行途中で割り込みが発生した場合、lrの値はA関数がmain関数に戻るために必要な位置のアドレスとなり、割り込みはA関数に戻りますが、アドレスは別途保存する必要があります。