リアルタイムOSの歴史(★)

[組込み製品、リアルタイムOS、組込みドライバーの開発依頼は、フィールドデザインまでお気軽にお問い合わせください。]

組込み製品でOSを頻繁に使うようになってきたのは、デジタルの携帯電話が出てきた1995年ころからです。アナログの携帯電話時代は、マイコンのメモリ量が少なく、OSを載せることができなかったことから、割り込みベースのハンドラで処理していく方式が一般的でした。

割り込みベースの時代

 現在ではあまりありませんが、メモリが少ないマイコンを使ってた時代は、OSを載せることができないため、割り込みハンドラだけでソフトウェアを作ることが主流でした。
 割り込みだけでソフトウェアを作成するときに問題になるのが、待ちがある複数の処理を非同期に行うことです。たとえば、データを送って、応答を受信することを3回繰り返す処理Aと、同じような処理Bがあるとします。
処理Aは、処理Bのことを考えないでよいのであれば、以下のようにwhileループで応答の受信フラグが立つのを待つようなプログラムで簡単に書けます。


main() {
    recv_flg = 0;
    send("1");
    while(!recv_flg);
    recv();

    recv_flg = 0;
    send("2");
    while(!recv_flg);
    recv();

    recv_flg = 0;
    send("3");
    while(!recv_flg);
    recv();
}
interruptA() {
    recv_flg = 1;
}

しかし、処理Bも同じように行わないとならない場合は、処理Bのプログラムの中で上記のような待ちを作ることができなくなります(どちらかのwhileループで止まってしまうため)。そのため処理A、処理Bは、割り込みハンドラ内に状態変数を使いながら待ちのない処理を書いていきます。(このプログラムはあまりいい例ではありません。stateを変更するまえに応答が来たら、どうなるのかなど。説明のために簡単にしています。)


main() {
    send("1");
    state = state1;
}
interruptA() {
    swithc(state) {
      state1:
        recv();
        send("2");
        state = state2;
      state2:
        recv();
        send("3");
        state = state3;
      state3:
        recv();
        state = state0;
    }
}

interruptB() {
    処理Bのプログラム
}

これくらいなら簡単なのでまだ許容できますが、処理Aと処理Bが互いに関係し合うなどになり、状態がさらに増えていくと、手に負えなくなってきます(状態が掛け算で効いてくるので、状態が爆発します)。
さらによくあるのが処理Bの方を優先的に1us以内に完了させなければならないというような条件がつくことがあります。そうなると、処理AのすべてのCPUサイクル数をカウントして、途中でも割り込みルーチンを抜けるように分割できる構造にしないとなりません。こうなると、状態がさらに増え、複雑なプログラムになります。

複雑な状態を管理しないといけないため、明らかに可読性が悪くなります。可読性が悪いということは、バグが出やすいということに繋がります。バグが出やすいだけでしたらまだいいのですが、バグの原因の範囲が全体に及んでいるので、原因の切り分けに時間がかかります。非常に開発効率の悪い方法です。

リアルタイムOSの時代

 メモリの価格が安くなってくると、組込み製品でもOSを載せることができるようになってきます。
 リアルタイムOSで上記の処理のプログラムを作ると、処理Aも処理Bも下記のように書けます。


taskA() {
    recv_flg = 0;
    send(1);
    while(!recv_flg) {sleep()};
    recv();

    recv_flg = 0;
    send(2);
    while(!recv_flg) {sleep()};
    recv();

    recv_flg = 0;
    send(3);
    while(!recv_flg) {sleep()};
    recv();
}
interruptA() {
    recv_flg = 1;
}
taskB() {
    処理Bのプログラム
}
interruptB() {
    処理Bのプログラム
}

非常に可読性のいいコードになります。つまり、バグの出にくいコードです。開発効率が非常に良くなります。
さらにセマフォなどを使えば、応答性のいいプログラムにできます。

ただし、OSを利用した場合でもバグの解析が困難なケースが一つだけ問題があります。それは、スタックがオーバーフローした場合です(バッファがオーバーフローしても同様なことが起きますが、普通は範囲チェックをしているので、そのようなバグはめったに起こりません。)。OSではプロセッサのレジスタ情報をスタックに積んでいきますが、スタックオーバーフローがあると、その部分が壊されてしまいます。
こうなると、他のタスクの処理まで影響をおよぼすので、全部の処理を見直さないといけないという最悪な状態になります。だいたい、スタックがオーバーフローすると、プログラムカウンタまで壊れてしまうので、何が原因か非常にわかりづらい状況になります。
そうならないために、プログラムの書き方として、スタックをできるだけ使わないようなコードにしておきます。
例えば、ローカル変数として256byte以上の配列を取らないようにとか、変数は極力グローバルに取るとかです。