組み込みシステムで割り込みを使用する際のグローバル変数の回避

グローバル変数を回避する組み込みシステムのためにISRとプログラムの残りの部分との間の通信を実装するための良い方法はありますか?

一般的なパターンは、ISRとプログラムの他の部分との間で共有され、フラグとして使用されるグローバル変数を使用することですが、このグローバル変数の使用は、私にとっては厄介です。 avr-libcスタイルのISRを使用した簡単な例を含めました。

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

私は本質的にスコープの問題が何であるかを見逃すことはできません。 ISRとプログラムの他の部分の両方からアクセス可能な変数は、本質的にグローバルである必要があります。それにもかかわらず、私は人々が「グローバル変数はISRとプログラムの残りの部分との間の通信を実装するための一方向」であると強調しています。他の方法があります。他の方法がある場合、それらは何ですか?

9
追加された 著者 Nick Alexeev,
その上、flagは通常のプログラムフローの外側で使用/変更するので、flagはvolatileとして宣言する必要があります。これにより、コンパイラはフラグへの読み書きを最適化せず、実際の読み書き操作を実行します。
追加された 著者 agfa555,
プログラムの残りの部分すべてがアクセスできるとは限らない。変数を静的変数として宣言した場合は、その変数が宣言されているファイルだけに表示されます。 1つのファイル全体の中に見える変数を持つことはまったく難しいことではありませんが、プログラムの残りの部分では変わらず、それが役立ちます。
追加された 著者 Silicomancer,
@ next-hackはい、それは絶対に正しいのです。申し訳ありませんが、すぐに例を挙げようとしていました。
追加された 著者 user109324,

6 答え

これを行うためのデファクトスタンダードな方法があります(Cプログラミングを仮定)。

  • 割り込み/ ISRは低レベルなので、割り込みを生成するハードウェアに関連するドライバ内にのみ実装する必要があります。それらは他の場所ではなく、そのドライバの中にあるべきではありません。
  • ISRとの通信はすべてドライバーとドライバーのみが行います。プログラムの他の部分がその情報にアクセスする必要がある場合は、セッター/ゲッター関数などを介してドライバーからそれを要求する必要があります。
  • 「グローバル」変数を宣言しないでください。外部リンケージを持つ、グローバルな意味を持つファイルスコープ変数。つまり、 extern キーワードで呼び出されたり、単に誤って呼び出されたりする可能性のある変数です。
  • 代わりに、ドライバ内でプライベートなカプセル化を強制するために、ドライバとISRの間で共有されるそのような変数はすべて static として宣言されるものとします。このような変数はグローバルではありませんが、宣言されているファイルに制限されます。
  • コンパイラの最適化の問題を防ぐために、そのような変数は volatile として宣言する必要があります。注意:これはアトミックなアクセスを与えたり、再入場を解決したりするものではありません。
  • ISRが変数に書き込む場合に備えて、ドライバには何らかの形の再入メカニズムが必要な場合があります。例:割り込みの無効化、グローバル割り込みマスク、セマフォ/ミューテックス、アトミック読み取りの保証。
12
追加された
注:ISR関数のプロトタイプを別のファイルにあるベクトルテーブルに配置するには、ヘッダーを介してそれを公開する必要があります。しかし、それが割り込みであり、プログラムから呼び出されるべきではないと文書化している限り、それは問題ではありません。
追加された 著者 Neil Foley,
@ Leroy105 C言語は現在までにインライン関数を永遠にサポートしています。 inline の使用さえ時代遅れになっていますが、コンパイラはコードの最適化においてますます賢くなっています。私は、オーバーヘッドについて心配することは「時期尚早の最適化」であると言います - ほとんどの場合、オーバーヘッドは、たとえそれがマシンコードに存在していても問題ありません。
追加された 著者 Neil Foley,
とはいえ、ISRドライバを書く場合、すべてのプログラマの80〜90%(ここでは誇張していません)は常に何か間違いを犯しています。結果は微妙なバグです。誤ってクリアされたフラグ、欠けている揮発性からの誤ったコンパイラの最適化、競合するリアルタイムパフォーマンス、スタックオーバーフローなど。ISRがドライバ内に適切にカプセル化されていない場合。さらに増えました。 setter/getterがちょっとしたオーバーヘッドを招くなど、周辺の関心事について心配する前に、バグフリードライバを書くことに集中してください。
追加された 著者 Neil Foley,
対引数が、setterを使用すること/関数を取得することの増加したオーバーヘッド(および余分なコード)であったとしたら、どうしますか?私は8ビット組み込みデバイスのためのコード標準について考えながら、これを自分で調べてきました。
追加された 著者 Leroy105,
このグローバル変数の使用は私にとって穀物に反する

これが本当の問題です。それを乗り越えなさい。

さて、これがどのようにして汚れているのかについて、膝ジャーカーがすぐに気付く前に、それを少し限定しましょう。グローバル変数を過剰に使用することは確かに危険です。しかし、それらはまた効率を向上させることができ、それは時に小さなリソースが限られたシステムでは問題になります。

重要なのは、あなたがそれらを合理的に使用することができ、トラブルに遭遇する可能性が低いのに対して、ちょうど起こるのを待っているバグに対して考えることです。常にトレードオフがあります。割り込みコードとフォアグラウンドコードの間の通信にグローバル変数を使用しないことは一般的に避けられないガイドラインですが、他のほとんどのガイドラインと同様に、極端な宗教を採用することは逆効果です。

私が割り込みとフォアグラウンドコードの間で情報を渡すためにグローバル変数を使用することがあるいくつかの例は次のとおりです。

  1. Clock tick counters managed by the system clock interrupt. I usually have a periodic clock interrupt that runs every 1 ms. That is often useful for various timing in the system. One way to get this information out of the interrupt routine to where the rest of the system can use it is to keep a global clock tick counter. The interrupt routine increments the counter every clock tick. Foreground code can read the counter at any time. Often I do this for 10 ms, 100 ms, and even 1 second ticks.

    I make sure the 1 ms, 10 ms, and 100 ms ticks are of a word size that can be read in a single atomic operation. If using a high level language, make sure to tell the compiler that these variables can change asynchronously. In C, you declare them extern volatile, for example. Of course this is something that goes into a canned include file, so you don't need to remember that for every project.

    I sometimes make the 1 s tick counter the total elapsed up time counter, so make that 32 bits wide. That can't be read in a single atomic operation on many of the small micro I use, so that isn't made global. Instead, a routine is provided that reads the multi-word value, deals with possible updates between reads, and returns the result.

    Of course there could have been routines to get the smaller 1 ms, 10 ms, etc, tick counters too. However, that really does very little for you, adds a lot of instructions in place of reading a single word, and uses up another call stack location.

    What's the downside? I suppose someone could make a typo that accidentally writes to one of the counters, which then could mess up other timing in the system. Writing to a counter deliberately would make no sense, so this kind of bug would need to be something unintentional like a typo. Seems very unlikely. I don't recall that ever happening in well over 100 small microcontroller projects.

  2. Final filtered and adjusted A/D values. A common thing to do is to have a interrupt routine handle readings from a A/D. I usually read analog values faster than necessary, then apply a little low-pass filtering. There is often also scaling and offset that get applied.

    For example, the A/D may be reading the 0 to 3 V output of a voltage divider to measure the 24 V supply. The many readings are run thru some filtering, then scaled so that the final value is in millivolts. If the supply is at 24.015 V, then the final value is 24015.

    The rest of the system just sees a live updated value indicating the supply voltage. It doesn't know nor need to care when exactly that is updated, especially since it is updated much more often than the low pass filter settling time.

    Again, a interface routine could be used, but you get very little benefit from that. Just using the global variable whenever you need the power supply voltage is much simpler. Remember that simplicity isn't just for the machine, but that simpler also means less chance of human error.

5
追加された
もちろん、インライン化できない場合、選択はそれほど単純ではありません。インライン化された関数(および多くのC99以前のコンパイラは既にインライン展開をサポートしていました)では、パフォーマンスはゲッターに対する引数にはなり得ないと言っておきたいのです。合理的な最適化コンパイラでは、同じ生成されたアセンブリになるはずです。
追加された 著者 Readonly,
あなたのポイントは有効なOlinですが、これらの例でも、 extern int ticks10msinline int getTicks10ms()に置き換えても、コンパイルされたアセンブリにはまったく違いはありません。これは、プログラムの他の部分で誤って値を変更することを難しくし、またこの呼び出しに「フック」する方法を可能にします(例えば、ユニットテスト中の時間のモックアップ、この変数へのアクセスの記録など)。 )たとえあなたが、サンプログラマがこの変数をゼロに変更する可能性があると主張しても、インラインゲッターのコストはありません。
追加された 著者 Readonly,
@ Leroy105問題は「テロリスト」が意図的にグローバル変数を悪用していないことです。名前空間の汚染は大規模なプロジェクトでは問題になる可能性がありますが、良い命名で解決できます。いいえ、真の問題は、プログラマが意図したとおりにグローバル変数を使用しようとしたが、正しく実行できなかったことです。すべてのISRに存在する競合状態の問題を認識していないため、または必須の保護メカニズムの実装をめちゃくちゃにしたため、または単にコード全体でグローバル変数の使用を見つけ出して密結合を生み出し判読できないコード
追加された 著者 Neil Foley,
@Groo:インライン関数をサポートする言語を使用している場合にのみ当てはまります。ゲッター関数の定義はすべての人に見えるようにする必要があります。実際に高級言語を使用するときは、ゲッター関数を多く使用し、グローバル変数を少なくします。アセンブリでは、getter関数を使用するよりも、グローバル変数の値を取得する方がはるかに簡単です。
追加された 著者 Olin Lathrop,
私はゆっくりとした週に、私のコードを徹底的に絞り込もうとしていました。私は変数アクセスを制限することにLundinのポイントを見ます、しかし、私は私の実際のシステムを見て、それがそのような遠隔可能性であると思います。 Getter/Setter関数は、単にグローバルを使用するのではなくオーバーヘッドがかかり、これらを受け入れるのは非常に単純なプログラムです。
追加された 著者 Leroy105,

特定の割り込みはグローバルリソースになります。ただし、複数の割り込みが同じコードを共有していると便利な場合があります。たとえば、システムに複数のUARTがあり、それらのすべてが同様の送信/受信ロジックを使用する必要があります。

これを処理するための良い方法は、割り込みハンドラによって使用されるもの、またはそれらへのポインタを構造体オブジェクトに配置し、実際のハードウェア割り込みハンドラを次のようにすることです。

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

uart1_infouart2_info などのオブジェクトはグローバル変数になりますが、割り込みハンドラによって使用されるグローバル変数は only になります。ハンドラが触れようとしている他のすべてのものは、それらの中で処理されます。

割り込みハンドラとメインラインコードの両方によってアクセスされるものはすべて volatile である必要があります。割り込みハンドラによって使用されるすべてのものを volatile として宣言するのが最も簡単かもしれませんが、パフォーマンスが重要な場合は、一時的な値に情報をコピーするコードを記述したい場合があります。それからそれらを書き戻します。例えば、書く代わりに、

if (foo->timer)
  foo->timer--;

書きます:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

前者のアプローチは読みやすく理解しやすいかもしれませんが、後者よりも効率が悪いでしょう。それが懸念事項かどうかはアプリケーションによって異なります。

2
追加された

これが3つのアイデアです。

フラグ変数を静的として宣言して、スコープを単一ファイルに制限します。

フラグ変数を非公開にし、getter関数およびsetter関数を使用してフラグ値にアクセスします。

フラグ変数の代わりにセマフォなどのシグナリングオブジェクトを使用してください。 ISRはセマフォを設定/投稿します。

0
追加された

割り込み(つまり、ハンドラを指すベクトル)はグローバルリソースです。ですから、あなたがスタックやヒープ上で何らかの変数を使ったとしても

volatile bool *flag; //must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

または '仮想'機能を持つオブジェクト指向コード

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

…最初のステップでは、他のデータに到達するために実際のグローバル(または少なくとも静的な)変数を使用する必要があります。

これらのメカニズムはすべて間接指定を追加するため、割り込みハンドラから最後のサイクルを絞り出したい場合は、これは通常行われません。

0
追加された
@ next-hackありがとうございます。
追加された 著者 Julian,
flagをvolatile int *として宣言する必要があります。
追加された 著者 agfa555,

現時点ではCortex M0/M4をコーディングしています。C++で使用しているアプローチ(C ++タグがないため、この回答はトピック外の可能性があります)は次のとおりです。

コントローラの実際の割り込みベクタに格納されているすべての割り込みサービスルーチンを含むクラス CInterruptVectorTable を使用します。

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },          //0x00
  __iar_program_start,                     //0x04

  CInterruptVectorTable::IsrNMI,           //0x08
  CInterruptVectorTable::IsrHardFault,     //0x0C
  //[...]
}

クラス CInterruptVectorTable は割り込みベクトルの抽象化を実装しているので、ランタイム中にさまざまな関数を割り込みベクトルにバインドできます。

そのクラスのインターフェースは次のようになります。

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

ベクタテーブルはオブジェクトではないため、コントローラは this ポインタを提供できないため、ベクタテーブルに格納されている関数を static にする必要があります。そのため、この問題を回避するために、 CInterruptVectorTable の内部に静的な pThis ポインタがあります。静的割り込み関数の1つに入ると、 pThis ポインタにアクセスして CInterruptVectorTable の1つのオブジェクトのメンバにアクセスできます。


このプログラムでは、 SetIsrCallbackfunction を使用して、割り込みが発生したときに呼び出される static 関数への関数ポインタを指定できます。ポインタは InterruptVectorTable_t virtualVectorTable に格納されています。

割り込み関数の実装は次のようになります。

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

そのため、別のクラスの static メソッド( private になります)を呼び出します。これには別の static this </を含めることができますそのオブジェクトのメンバ変数にアクセスするためのcode> -pointer(1つだけ)。

私はあなたが IInterruptHandler のように構築してインターフェースすることができて、そしてオブジェクトへのポインタを保存することができると思います、それであなたはすべてのそれらにおいて static this - ポインタを必要としませんクラス。 (多分私達は私達の建築の次の反復でそれを試みます)

割り込みハンドラの実装を許可されているオブジェクトはハードウェア抽象化レイヤ内にあるものだけで、通常はハードウェアブロックごとに1つのオブジェクトしかないため、他のアプローチでも問題ありません。 > this ポインタ。そして、ハードウェア抽象化層は、 ICallback と呼ばれる、割り込みに対するさらに別の抽象化を提供します。これは、ハードウェア上のデバイス層に実装されます。


グローバルデータにアクセスしますか?もちろんできますが、 this ポインタや割り込み関数のように、必要なグローバルデータのほとんどをプライベートにすることができます。

それは防弾ではありません、そしてそれはオーバーヘッドを追加します。このアプローチを使ってIO-Linkスタックを実装するのに苦労するでしょう。しかし、タイミングがそれほど厳しくない場合は、どこからでもアクセス可能なグローバル変数を使用せずに、モジュール内の割り込みと通信を柔軟に抽象化するのに非常に適しています。

0
追加された
「実行時に割り込み関数にさまざまな関数をバインドできる」これは悪い考えのようです。プログラムの「循環的な複雑さ」はただ屋根を通り抜けるだけです。タイミングとスタックの使用が競合しないように、すべてのユースケースの組み合わせをテストする必要があります。非常に限られた有用性IMOを持つ機能に対する頭の痛みがたくさんあります。 (あなたがブートローダのケースを持っていない限り、それはまた別の話です)全体的にこれはメタプログラミングの匂いがします。
追加された 著者 Neil Foley,
DMAは1つのことで、割り込みベクタの実行時割り当てはまったく別のものです。実行時にDMAドライバの設定を可変にすることは理にかなっています。それほどではないベクトルテーブルです。
追加された 著者 Neil Foley,
@Lundin私たちはそれについてさまざまな見解を持っていると思います、私たちはまだそれについてあなたの問題を見ていないので、私たちはそれに関してチャットを始めることができました。
追加された 著者 Arsenal,
@ルンディン私はあなたの意見を本当に見ません。例えば、DMAがSPIに使用されている場合はDMA割り込みをSPI割り込みハンドラにバインドし、UARTに使用されている場合はUART割り込みハンドラにバインドするために使用します。両方のハンドラをテストする必要がありますが、問題はありません。そしてそれは確かにメタプログラミングとは何の関係もありません。
追加された 著者 Arsenal,