組み込みプログラミングTips

投稿者: | 2018年12月17日

※この記事はあくあたん工房AdventCalendar 17日目の記事です。

はいどうも、社会人1年目のれいです。
中学3年の夏に中学にロボット研究部に入部、組み込みの世界に入ってから9年経過した私ですが、まだまだ半人前であることを痛感しつつ日々業務に取り組んでいます。


さて、組み込み開発をやったことのある人なら、
割り込み内の処理は極力短くしろ
と何度も言われてきたことでしょう。

今回はその理由と初歩的(というか原始的)な対策2つを紹介したいと思います。

Contents

割り込みとは

割り込みとは,コンピュータが周辺機器から受け取る要求の一種です。
割り込み要求を受けたコンピュータは実行中の処理を中断して,優先度の高い別の処理を行います。

割り込みが発生した際に処理する内容は割り込みサービスルーチン(Interrupt Service Routine; ISR)に記述します。

組み込み分野における割り込み処理には以下のような例があります。

  • ボタンが押された場合(外部割り込み)
  • データの送受信が完了した場合(送受信割り込み)
  • 一定時間が経過した場合(タイマ割り込み)

また、割り込みにはそれぞれ優先度が指定できます。
データ受信割り込みの割り込み優先度を高くする等、適切な割り込み優先度を設定しないと、受信データの取りこぼしが発生する可能性があるため注意する必要があります。

割り込みにかかる時間

現在の処理を中断して割り込みの処理を行うとき、復帰後に元の処理を継続できるようにレジスタの内容などを一時的に対比する必要があります。
このCPU状態のを一時保存・復元の処理をコンテキストスイッチといいます。

割り込み開始時と終了時、このコンテキストスイッチによって、それぞれ20クロック程度時間を要します。
(アセンブラで書くと関数の呼び出しという形を取らずに済むため、10~15クロックくらいまで短くすることも出来ます。が、やりたくない…)

割り込み内で長時間処理をしていけない理由

以下のような問題が発生するから。

  • メインルーチンや下位の割り込みが待ち状態となり、なかなか処理されない。
  • 一定周期で行うべき処理が割り込み完了待ちによって実行されず、タイミングがずれる。
  • 通信データの送信周期よりも長い時間割り込み処理を行うと、データ取りこぼしや返信データ生成の失敗が起こる。

なので、割り込み内の処理は少しでも早くする必要があります。

遅い処理の例

関数呼び出し

関数の呼び出しを行うと、以下の順番で処理が行われます。

  1. 引数・関数実行後の戻り先のアドレスをスタックに格納(push)する
  2. プログラムカウンタの値を関数の先頭に書き換える
  3. 引数の内容をスタックから取り出す(popする)。
  4. 関数内の処理を実行する
  5. 戻り先のアドレスをスタックからpopし、プログラムカウンタに書き込む

つまり、割り込み内に処理をベタ書きした際と比較すると、1,2,4の処理が追加されることになります。

関数呼び出しは用法・用量を守って正しくお使いください。

除算・剰余演算

除算は並列化出来ないため、他の四則演算と比較して時間を要します。

高速化の例

inline関数を使用する

inline関数を使用すると、コンパイル時にその関数の呼び出し元に処理が展開され、
ベタ書きと同じ速度で実行することが出来ます。

inline int is_positive(int a){
    if(a > 0)
        return 1;
    else
        return 0;
}

ただし、注意する点が2つ。

1つ目。処理内容が各呼び出し元でそれぞれ展開されるため、コードサイズが大きくなります。
何十、百回と呼び出されるような大きな処理にはinline関数を用いないほうが良いでしょう。

2つ目に、inline関数をソースファイル(拡張子がcのファイル)で定義した場合、inline展開されるのは、同一ファイル内だけです。

例えば、

  • app.cでinline関数inline void func()を定義
  • app.hでinline関数funcをプロトタイプ宣言
  • interrupt.cでinline関数func()を呼び出し

とした場合、interrupt.cではfuncはinline展開されず、関数呼び出しとして処理されます。
コンパイルとリンクの関係を考えれば、なんとなく分かりますよね。

複数のソースファイルで特定の処理をinline展開したい場合、そのinline関数はヘッダーファイルに記述する必要があります。

ビットシフト・論理積を使用する

2の累乗の除算、剰余演算の場合、それぞれ以下のように書き換える。

  • 除算の場合(8で割る場合)
 result = value / 8;
  • 変更後(2の3乗なので、3bit右にシフト)
 result = value >> 3;
  • 剰余演算の場合(16で割ったときの余りを求める場合)
 result = value % 16;
  • 変更後( 16-1の15で論理積 )
 result = value & 0x0f;

これだけで10倍程度高速化されます。

UARTのリングバッファにおけるインデックスの計算など、高頻度で受信割り込みが発生するものは、この手法を用いて必ず高速化するべきでしょう。
(そもそもデータの送受信はDMAで行ったほうが良いとは思いますが…)

除算ではなく乗算にする(浮動小数点演算の場合)

除算する値が定数の場合、乗算に変えましょう。

  • 10で割る場合
 result = value / 10.0f;
  • 変更後( 0.1を掛ける )
 result = value * 0.1f;

ただし、演算の精度が落ちる可能性がありますので、その点には注意する必要があります。
(浮動小数点を使っている時点であまり精度は気にしていない気はしますが…)

この高速化手法は、コンパイラ最適化を有効にしていると、コンパイラが勝手にやってくれることもあります。

おわりに

我々の世代での学生ロボコンでは、モータの制御器としては速度制御・位置制御までで終わっていました。これらの制御器の制御周波数は1kHzでも十分です。
180MHzのマイコンを使っていた場合、割り込みの間隔までに18万CLKもあります。
足回り4輪を1つのマイコンで処理するとしても、プログラムの実行速度なんて気にしないで済んでいました。

しかし今では学生ロボコンでもトルク制御が行われ、10kHzオーダーでの制御周波数が要求される様になってきました。
この時、足回り4輪分の電流・速度・位置制御を1つのマイコンでまかなえるでしょうか…?

必要な処理能力の見積もりやパフォーマンスチューニングについて、
学生ロボコンでも必要なレベルになってきたのかもしれません。

参考

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です