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


13

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

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

volatile uint8_t flag;

int main() {
    ...

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

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

基本的にスコーピングの問題が何であるかを見逃すことはできません。ISRと残りのプログラムの両方がアクセスできる変数は、必ず本質的にグローバルでなければなりませんか?それにもかかわらず、「グローバル変数はISRとプログラムの残りの部分との間の通信を実装する1つの方法」(強調は私のもの)という言葉に沿って人々が言うことをよく目にします。他の方法がある場合、それらは何ですか?



1
プログラムの残りすべてがアクセスできるとは限りません。変数を静的として宣言した場合、変数が宣言されたファイルだけがそれを認識します。1つのファイル全体に表示される変数を持つことはまったく難しくありませんが、プログラムの残りの部分では表示されず、それが役立ちます。
DiBosco 2017

1
さらに、通常のプログラムフローの外でフラグを使用/変更しているため、フラグは揮発性として宣言する必要があります。これにより、コンパイラはフラグに対する読み取り/書き込みを最適化せず、実際の読み取り/書き込み操作を実行します。
次のハック

@ next-hackはい、それは完全に正しいです。申し訳ありませんが、私はすぐに例を考え出そうとしていました。

回答:


18

これを行うための事実上の標準的な方法があります(Cプログラミングを想定)。

  • 割り込み/ ISRは低レベルであるため、割り込みを生成するハードウェアに関連するドライバー内にのみ実装する必要があります。それらは他の場所ではなく、そのドライバー内に配置する必要があります。
  • ISRとのすべての通信は、ドライバーとドライバーのみによって行われます。プログラムの他の部分がその情報にアクセスする必要がある場合は、セッター/ゲッター関数などを介してドライバーに要求する必要があります。
  • 「グローバル」変数は宣言しないでください。外部リンケージを持つグローバルな意味のファイルスコープ変数。つまり、externキーワードまたは単純に誤って呼び出される可能性のある変数です。
  • 代わりに、ドライバー内でプライベートカプセル化を強制するために、ドライバーとISRの間で共有されるそのようなすべての変数が宣言されstaticます。このような変数はグローバルではありませんが、宣言されたファイルに制限されています。
  • コンパイラの最適化の問題を防ぐには、そのような変数をとしても宣言する必要がありますvolatile。注:これはアトミックアクセスを提供したり、再入可能性を解決したりするものではありません!
  • ISRが変数に書き込む場合に備えて、ドライバーでは何らかの方法で再入可能メカニズムが必要になることがよくあります。例:割り込み無効化、グローバル割り込みマスク、セマフォ/ミューテックス、または保証されたアトミック読み取り。

注:ISR関数プロトタイプを別のファイルにあるベクターテーブルに配置するには、ヘッダーを介してISR関数プロトタイプを公開する必要がある場合があります。しかし、それが割り込みであり、プログラムによって呼び出されるべきではないことを文書化する限り、それは問題ではありません。
ランディン2017

カウンター引数が、setter / getting関数を使用することによるオーバーヘッド(および追加のコード)の増加だった場合、どうしますか 私自身、8ビットの組み込みデバイスのコード標準について考えながら、自分でこれを検討してきました。
Leroy105

2
@ Leroy105 C言語は、永遠にインライン関数をサポートするようになりました。の使用でもinline時代遅れになっ、コンパイラーはコードの最適化にますます賢くなっています。オーバーヘッドを心配することは「時期尚早の最適化」だと思います。マシンコードに存在する場合でも、ほとんどの場合、オーバーヘッドは問題ではありません。
ランディン2018年

2
そうは言っても、ISRドライバーを作成する場合、すべてのプログラマー(ここでは誇張ではありません)の約80〜90%が常に何らかの問題を抱えています。結果は微妙なバグです。フラグが正しくクリアされなかったり、揮発性の欠如による不正なコンパイラの最適化、競合状態、リアルタイムパフォーマンスの低下、スタックオーバーフローなどが発生します。ISRがドライバー内に適切にカプセル化されていない場合、このような微妙なバグが発生する可能性があります。さらに増加し​​ました。セッター/ゲッターがわずかなオーバーヘッドをもたらすかどうかなど、周辺の関心事について心配する前に、バグのないドライバーを書くことに集中してください。
ランディン2018年

10
このグローバル変数の使用は、私には不利です

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

ひざまずいている人がこれがいかに汚れているかについてすぐに怒鳴る前に、少しそれを認めさせてください。グローバル変数を使いすぎると、確かに危険があります。しかし、これらは効率を向上させることもできます。これは、リソースが限られた小さなシステムでは重要な場合があります。

重要なのは、それらを合理的に使用でき、問題が発生する可能性が低い場合と、発生するのを待っているバグとを比較することです。常にトレードオフがあります。一方で、一般的に、割り込みと前景のコードの間で通信するためのグローバル変数を回避することundertandable指針である、それを取っては、他のほとんどのガイドラインと同様に、宗教の極端に逆効果です。

グローバル変数を使用して、割り込みコードとフォアグラウンドコードの間で情報を受け渡す例は次のとおりです。

  1. システムクロック割り込みによって管理されるクロックティックカウンター。通常、1 msごとに実行される定期的なクロック割り込みがあります。これは、システムのさまざまなタイミングで役立ちます。この情報を割り込みルーチンから取得して、システムの残りの部分で使用できるようにする1つの方法は、グローバルクロックティックカウンターを保持することです。割り込みルーチンは、クロック刻みごとにカウンターをインクリメントします。フォアグラウンドコードはいつでもカウンターを読み取ることができます。多くの場合、これは10ミリ秒、100ミリ秒、さらには1秒の目盛りで行われます。

    1ミリ秒、10ミリ秒、100ミリ秒のティックが、単一のアトミック操作で読み取ることができるワードサイズであることを確認します。高水準言語を使用している場合は、これらの変数が非同期で変更される可能性があることをコンパイラーに伝えてください。たとえば、Cでは、それらをextern volatileとして宣言します。もちろん、これは組み込みのインクルードファイルに含まれるものなので、すべてのプロジェクトで覚えておく必要はありません。

    1ティックカウンターを合計経過時間カウンターにする場合があるので、32ビット幅にします。これは、私が使用している多くの小さなmicroの単一のアトミック操作では読み取ることができないため、グローバル化されていません。代わりに、マルチワード値を読み取り、読み取り間の可能な更新を処理し、結果を返すルーチンが提供されます。

    もちろんあります 、小さい1ミリ秒、10ミリ秒などのティックカウンターを取得するルーチンます。ただし、これは実際にはほとんど機能せず、1つの単語を読み取る代わりに多くの命令を追加し、別の呼び出しスタックの場所を使い果たします。

    欠点は何ですか?誰かが誤ってカウンターの1つに書き込むタイプミスをすると、それがシステムの他のタイミングを台無しにする可能性があります。意図的にカウンターに書き込んでも意味がないので、この種のバグはタイプミスのような意図しないものである必要があります。非常にありそうにないようです。100以上の小さなマイクロコントローラープロジェクト起こったことを思い出しません。

  2. 最終的にフィルタリングおよび調整されたA / D値。一般的なことは、A / Dからの読み取りを割り込みルーチンで処理することです。私は通常、必要以上に速くアナログ値を読み取ってから、少しローパスフィルタリングを適用します。多くの場合、適用されるスケーリングとオフセットもあります。

    たとえば、A / Dが24 V電源を測定するために、分圧器の0〜3 V出力を読み取っている場合があります。多くの読み取り値は、フィルタリングによって実行され、最終的な値がミリボルト単位になるようにスケーリングされます。電源が24.015 Vの場合、最終値は24015です。

    システムの残りの部分には、供給電圧を示す最新の更新値が表示されます。特にローパスフィルターのセトリングタイムよりもはるかに頻繁に更新されるため、それが正確に更新されるタイミングはわかりませんし、気にする必要もありません。

    この場合も、インターフェースルーチン使用できますが、そのメリットはほとんどありません。電源電圧が必要なときにグローバル変数を使用するだけの方がはるかに簡単です。単純化はマシンだけでなく、人為的エラーの可能性が少ないことも意味することを忘れないでください。


遅い週に、私は実際に自分のコードをひっくり返すことを試みて、治療に行ってきました。変数へのアクセスを制限するというLundinのポイントはわかりますが、実際のシステムを見て、それは非常に可能性の低いことだと思います。ゲッター/セッター関数は、グローバルを使用してこれらを受け入れるだけの場合と比べて、オーバーヘッドが
高くなり、

3
@ Leroy105問題は、意図的にグローバル変数を悪用する「テロリスト」ではありません。名前空間の汚染は、より大きなプロジェクトでは問題になる可能性がありますが、適切な名前を付けることで解決できます。いいえ、本当の問題は、プログラマーがグローバル変数を意図したとおりに使用しようとしているが、正しく使用できないことです。すべてのISRに存在する競合状態の問題を認識していないため、または必須の保護メカニズムの実装を台無しにしているため、または単にコード全体でグローバル変数の使用を吐き出して、密結合を作成し、読めないコード。
ランディン2018年

あなたのポイントは有効なオリンですが、これらの例でも、で置き換えextern int ticks10msinline int getTicks10ms()もコンパイルされたアセンブリにはまったく違いがありませんが、一方で、プログラムの他の部分でその値を誤って変更しにくくなり、またこの呼び出しに「フック」する方法(たとえば、ユニットテスト中に時間を模擬する、この変数へのアクセスをログに記録するなど)。sanプログラマーがこの変数をゼロに変更する可能性があると主張しても、インラインゲッターのコストはかかりません。
Groo

@Groo:これは、インライン関数をサポートする言語を使用している場合にのみ当てはまり、ゲッター関数の定義がすべてのユーザーに表示される必要があることを意味します。実際、高水準言語を使用する場合、ゲッター関数を多く使用し、グローバル変数を少なく使用します。アセンブリでは、グローバル変数の値を取得する方が、getter関数を使用するよりもはるかに簡単です。
Olin Lathrop

もちろん、インライン化できない場合、選択はそれほど単純ではありません。インライン関数(およびC99以前のコンパイラーの多くはすでにインライン拡張をサポートしている)では、パフォーマンスはゲッターに対する引数にはなり得ないことを言いたかった。合理的な最適化コンパイラを使用すると、最終的に同じ生成されたアセンブリになるはずです。
Groo

2

特定の割り込みはグローバルリソースになります。ただし、複数の割り込みで同じコードを共有すると便利な場合があります。たとえば、システムに複数の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などはグローバル変数ですが、割り込みハンドラーが使用する唯一のグローバル変数です。ハンドラーが触れる他のすべてはそれらの中で処理されます。

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

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

書く:

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

前者の方法は読みやすく理解しやすいかもしれませんが、後者よりも効率が悪くなります。それが問題になるかどうかは、アプリケーションによって異なります。


0

ここに3つのアイデアがあります:

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

フラグ変数をプライベートにし、ゲッター関数とセッター関数を使用してフラグ値にアクセスします。

フラグ変数の代わりに、セマフォなどのシグナリングオブジェクトを使用します。ISRはセマフォを設定/通知します。


0

割り込み(つまり、ハンドラーを指すベクター)はグローバルリソースです。したがって、スタックまたはヒープで変数を使用した場合でも、

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

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

または「仮想」関数を備えたオブジェクト指向コード:

HandlerObject *obj;

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

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

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


flagをvolatile int *として宣言する必要があります。
next-hack

0

私は現在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;
};

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


プログラムでは、を使用しSetIsrCallbackfunctionstatic、割り込みが発生したときに呼び出される関数への関数ポインターを提供できます。ポインタはに保存されますInterruptVectorTable_t virtualVectorTable

そして、割り込み関数の実装は次のようになります:

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

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

IInterruptHandlerオブジェクトへのポインタのように構築してインターフェイスし、ポインタを格納できるのでstatic this、これらすべてのクラスで-pointer は必要ありません。(たぶん、私たちはアーキテクチャの次の反復でそれを試すでしょう)

割り込みハンドラーの実装が許可されているオブジェクトはハードウェアアブストラクションレイヤー内のオブジェクトのみであり、通常、ハードウェアブロックごとに1つのオブジェクトしかないため、static this-pointersでうまく機能するため、他のアプローチはうまく機能します。また、ハードウェアアブストラクションレイヤーは、割り込みにさらに別のアブストラクションを提供します。ICallbackこれは、ハードウェアの上のデバイスレイヤーに実装されます。


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

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


1
「ランタイム中にさまざまな関数を割り込みベクトルにバインドできる」これは悪い考えのように思えます。プログラムの「循環的複雑度」はただ屋根を通り抜けるだけです。すべてのユースケースの組み合わせは、タイミングもスタックの使用も競合しないようにテストする必要があります。IMOの有用性が非常に限られている機能には頭痛がたくさんあります。(ブートローダーのケースがない限り、それは別の話です)全体的に、これはメタプログラミングのにおいがします。
ランディン2017

@ルンディン私はあなたの主張を本当に理解していません。たとえば、DMAがSPIに使用されている場合はSPI割り込みハンドラーに、DMAがUARTに使用されている場合はUART割り込みハンドラーにバインドするために使用します。両方のハンドラーをテストする必要がありますが、問題はありません。そして、それは確かにメタプログラミングとは何の関係もありません。
アーセナル

DMAは1つですが、割り込みベクトルのランタイム割り当ては完全に別のものです。実行時にDMAドライバーの設定を可変にすることは理にかなっています。それほど多くはありませんが、ベクトルテーブルです。
ランディン2017

@Lundin私はそれについて異なる見解を持っていると思います、私はそれについてチャットを始めることができます、それでも私はそれに関するあなたの問題が見えないので-それで私の答えがひどく書かれていて、全体の概念が誤解されているかもしれません。
アーセナル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.