8051マイクロコントローラでブロッキング遅延関数をプログラミングする方法

APDahlen Applications Engineer

読者の皆さんには、Arduino IDEに搭載されているdelay( ) 関数に親しみがあるかもしれません。これは、Arduinoマイクロコントローラのすべてのシリーズに共通して使用できるブロッキング遅延を提供するシンプルな関数です。ベアメタル(OSを介さず直接命令を実行する)マイクロコントローラのプログラミングに移行すると、同じような遅延関数を探すことになるかもしれません。残念ながら、8051マイクロコントローラの「標準ライブラリ」に、このような遅延関数が含まれていることはほとんどありません。

この記事では、ハードウェアの遅延方法について簡潔に説明した後、厳密に定義された前提条件を使用した8051 Busy Beeソリューションを紹介します。

おそらく、delay()の省略の最大の理由は柔軟性です。Arduinoのコードは、基盤となるマイクロコントローラの大部分を制御していることを理解してください。クロック速度や専用タイマはすべて事前に定義され、バックグラウンドに隠されています。隠されているからこそ、delay()関数がすべてのプラットフォームで同じように動作し、シンプルなプログラミングを提供します。そうでなければ、Arduinoプログラマはペリフェラルのアーキテクチャや動作について多くの知識を持つ必要があります。このようなことは、初心者や趣味でArduinoを使う人たちに分かりやすさを提供するというArduinoの目的に反しています。

一般的な8051の環境では状況は大きく異なります。隠されたコードはありませんし、通常、必要なぺリフェラルをすべて設定することが求められます。クロックについてもユーザーが関与し、高速な外部クロック、内部クロック、32.768kHzから自由に選択できます。また、ディープスリープ状態のマイクロコントローラを定期的に起動するウォッチドッグタイマも自由に選択できます。これらのオプションのどれを選んでも、delay()のような関数が期待通りに動作しなかったり、停止したりする可能性があります。

Busy Bee reference manualのコピーを見つけて、始めましょう。

技術的なヒント: ブロッキングという用語は、マイクロコントローラのメインプログラムが遅延時間中ずっとブロックされる(何もしない)ことを意味します。これは一般的に、小さな遅延や単純な問題であれば許容されますが、許容できない動作につながる可能性があります。例えば、ブロッキング遅延が進行している間、マイクロコントローラはボタンを押しても反応しません。この問題を解決するには、割り込みやノンブロッキング遅延を使用する方法があります。

利用可能な多くの遅延オプション

マイクロコントローラで遅延関数を構成する方法は数多くあることを認識することから始めましょう。例を挙げると、以下のようなものがあります。

  • NOP(何もしない命令)を含む、慎重に構築されたアセンブラコードです。ここでプログラマは、アセンブルされた各命令の特質に基づいてマイクロコントローラのクロックサイクルを計算します。

  • ベクタ割り込みで使用するハードウェアタイマです。遅延に関連する動作は、割り込みサービスルーチン(ISR:Interrupt Service Routine)に組み込むか、割り込みがArduinoの millis( ) functionに似たシステム時間を保持することができます。

  • 割り込みなしで動作するフリーランニングのハードウェアタイマです。このハードウェアタイマは常に動作しています。これは、モジュロ60演算の壁掛け時計を見るのと似ています。現在時刻が50秒とし、20秒遅らせたいとします。その場合、秒針が10秒になるまで待つことになります。8051の、このようなソリューションは、どのタイマのタイプを選択するかによって、モジュロ256または25536となります。動作の「速さ」は、システムクロックとタイマのプリスケール設定に依存します。

  • 割り込みを使用しない制御されたハードウェアタイマです。この方法では、ユーザーのプログラムはタイマを停止させ、タイマを既知の値にあらかじめ設定し、タイマをイネーブルにし、オーバーフローが発生するのを待ちます。

各オプションには長所と短所があります。その選択は、個々のプロジェクトのニーズ、プログラマのスキル、将来の保守性、利用可能なハードウェアリソースに大きく依存します。例えば、プロジェクトが一貫したハートビート(システムの正常性を示す一定間隔の信号)を必要とする場合、ベクタ割り込み付きのハードウェアタイマは非常に優れたソリューションです。この精密な周期的タイミングは、比例積分微分コントローラやDACを使った波形生成に適しています。

ブロッキング遅延

この記事では、割り込みを使用しないハードウェアタイマを使った解決策を紹介します。マイクロコントローラのハードウェアタイマは、通常、時計の秒針のようにカウントアップすることを思い出してください。このコードの開発に使用したSilicon LabsのEFM8BB1 8051は、標準の8051と後方互換性を持つ4つの16ビット汎用ハードウェアタイマを含むタイマ群を備えています。

16ビットタイマはモジュロ 2^{16} の演算を行います。モジュロ60のクロックのように、16ビットタイマは0から65535までカウントしてから0に戻ります。このオーバーフローイベントは、ハードウェアが自動的に割り込みフラグをセットする特別なものです。この動作は、非常に単純化された図1のブロック図で示されています。

図1: Busy Beeタイマ#2のブロック図の概要

技術的なヒント: ペリフェラルの割り込みフラグは自動的に初期化されるわけではありません。関連する割り込みイネーブルビットが割り込みまたは拡張割り込みイネーブルレジスタに設定されている場合にのみ、割り込みフラグが初期化されます。

この割り込みフラグは、文末のコードで説明されているブロッキング遅延のキーとなります。遅延の動作は次のように記述されます。

  1. 適切なプリスケールとモードをそれぞれ1:1と16ビット自動リロードとしてタイマを構成します。また、sbit演算子を使用して、実行制御と割り込みフラグに別名を付けます。これらの操作はwhile(1)のスーパーループの前に一度だけ実行さ れます。

  2. タイマをオフにし、割り込みフラグをクリアします。

  3. タイマに所定の遅延値を設定します。

  4. タイマの実行ビットを有効にします。

  5. 割り込みフラグがセットされるまで、whileループ(何もしない)を回ります。この操作は、フラグがセットされるまで、他のすべてのmain()コードをブロックします。

技術的なヒント: オリジナルの8051はビットレベルでレジスタを操作できるユニークなアーキテクチャです。このため、レジスタ内の特定のビットを選択するためにマスクを使用する必要がなく、高速なコードを実現できます。これはKeil C51 sbit assembler statementを使用することで実現できます。

この後方機能はEFM8BB1のような次世代の製品にも受け継がれています。しかし、新しい製品には、オリジナルの8051よりもかなり多くのペリフェラルと関連するSFR(Special Function Register:特殊機能レジスタ)が含まれています。残念ながら、すべてのレジスタがビットアドレス指定可能というわけではありません。0x00または0x08で終わるレジスタだけが、この便利で高速なsbit演算子を使用できます。リファレンスマニュアルを注意深く読むと、0xC8のアドレスのTMR2CN0が、ビット操作可能であることがわかります。

関数の呼び出しと演算に伴うオーバーヘッド

この記事を締めくくる前に、関数の呼び出しとリロード値の計算に伴うオーバーヘッドを考慮する必要があります。例えば、この小さなコードが重大な問題を引き起こします。

    tmr_load = -((n_us * 49) >> 1);

これは、1マイクロ秒あたり24.5クロックティックを考慮した巧妙なものです。ハーフビットを考慮するために浮動小数点演算を使用する代わりに、49を乗算し、右シフト演算を使用して2で除算します。

最後のステップは、 2^{16} からリロード値を引くことです。省略形は結果をマイナスにすることです。別の言い方をすれば、モジュロ65536の環境では、65536 = 0(モジュロ65536)なので、65536 - xは0 - xと同じ答えになります。これは60 = 0(モジュロ60)となる時計と同じです。

次の行では、tmr_load変数に100を足しています。これは、関数呼び出しのオーバーヘッドとtmr_loadの計算にかかる時間を考慮するための粗い方法です。Busy Beeにはハードウェア乗算器がないことを思い出してください。その結果、8ビットALUを介して8ビット x 16ビットの乗算を実行するのに時間がかかります。

この関数のオーバーヘッドによる課題の1つは、小さな遅延を受け入れられる能力がないことです。およそ4マイクロ秒のオーバーヘッドがあるため、それ以下の遅延は不可能です。このような小さな遅延が必要な場合は、前述のNOP命令を使った手書きのアセンブラコードを使う必要があります。

実際の結果を図2に示します。ピンが5マイクロ秒ハイになり、5マイクロ秒ローになり、そして再びハイになります。同様の結果は関連するblk_ms_delayでも得られます。blk_ms_delay(2000)の呼び出しは、Digilent Analog Discoveryのオシロスコープ機能により測定された結果は0.01ms以内でした。ブロッキング関数が妥当な性能を提供することに同意しますか?

図2: 5マイクロ秒ハイ、5マイクロ秒ローにプログラムした実信号のオシロスコープ波形

まとめ

前述のように、文末のコードはBusy Beeのクロック、プリスケール、タイマ設定に大きく依存します。このコードの開発で想定した条件から外れる場合は、コードを修正する必要があります。

おそらく消費電力を節約するために、より低速のクロックを使用するようにコードを修正することになるでしょう。マイクロコントローラをディープスリープさせる方法もあります。そののち再び、マイクロコントローラを可能な限り高速に起動させ、スリープに戻る前に素早く演算を実行したいと思うかもしれません。

その柔軟性こそが、マイクロコントローラのベアメタルプログラミングは、これまで使ってきたハイレベルプログラミングよりも難しくする原因です。それはまた、性能を引き出すキーでもあります。

ご意見、ご提案をお寄せください。

幸運を祈ります。

APDahlen

/***************************************************************************************************
 *
 * This code was developed on the EFM8BB1 featuring a 24.5 MHz internal clock.
 *
 * Note that the TMR2CN0 SFR is located at 0xC8. Since it ends in 0x08, it is one of the
 * bit-addressable memory locations. This is convenient as it allows the Keil C51
 * compiler to use the sbit construct.
 *
 * Be sure to include these statements:
 *
 *     sbit TR2 = TMR2CN0^2;              // Timer 2 run control
 *     sbit TF2 = TMR2CN0^7;              // TMR2 16-bit overflow on the 0xFFFF to 0x0000 transition.
 *
 * Also, don't forget to configure timer 2:
 *
 *     TMR2CN0 = 0x00;                    // default for timer 2: clear overload flag, 16-bit, timer off
 *     CKCON0 |= CKCON0_T2ML__SYSCLK;     // use system clock
 *
 */

/***************************************************************************************************
 *
 * @brief Microsecond blocking delay based on T2
 *
 * Given a 24.5 MHz system clock, this function provides a delay between 5 and 1000 us.
 * The actual max value without an overflow error is 1337. However, it's easier and
 * less error prone to remember 5 to 1000.
 *
 * Note that this function cannot be used for single us delays as it takes longer than that to
 * perform the function calls and math to calculate the appropriate timer delay.
 *
 * An empirical correction is added to account for the calling overhead and delay calculations.
 *
 * This function assumes a 24.5 MHz system clock. It also assumes a 1:1 pre-scale or Timer 2.
 * For fast computation:
 *     1) multiply n_us by 49
 *     2) divide by 2 using a shift right
 *
 * @param n_us Identifies the number of microseconds to delay.
 *
 * @warning There are no guard rails for overflow.
 *
 * @warning Delays less than 5 us will be extended to 5 us.
 *
 * @warning For improved performance replace the reload calculation with a lookup table.
 *
 */

void blk_us_delay (uint16_t n_us){

    uint16_t  tmr_load;

    if (n_us < 5){                // Extend small delays to 5 us
        n_us = 5;
    }

    tmr_load = -((n_us * 49) >> 1);

    tmr_load += 100;              // Estimate accounting or the calling overhead
                                  // and the machine cycles to perform the previous
                                  // 8 by 16-bit multiplication.

    TR2 = 0;                      // Stop timer
    TF2 = 0;                      // Clear timer overflow flag

    TMR2L = tmr_load;
    TMR2H = tmr_load >> 8;

    TR2 = 1;                      // Start timer
    while (!TF2);                 // Wait for the overflow
    TR2 = 0;                      // Stop timer to save power

  }


/***************************************************************************************************
 *
 * @brief Millisecond blocking delay based on blk_us_delay which in turn is based on T2.
 *
 * Provides a delay between 1 and 65,534 ms.
 *
 * @param n_ms Identifies the number of milliseconds to delay.
 *
 * @warning For improved accuracy be sure to use a hardware timer with a large pre-scale value.
 *
 */
void blk_ms_delay (uint16_t n_ms){

  uint16_t i;

  for(i = 0; i < n_ms; i++){
      blk_us_delay (1000);
  }
}





オリジナル・ソース(English)