Cの外部デバイスレジスタの操作、パート1

前の投稿の疑いのない成功に触発されました(記事が面白くなく、Habr向けではないことを書いた人はいません-それはすでに成功しており、多くの人々がデザインについて読んでコメントを書き、アドバイスを与えました-ところで、さらに大きな成功、ありがとうございました) MKのプログラミングについての考え。 今日のメモは、C言語の一般的なプログラミングの問題、つまり、特定のMKやプログラミング環境に関係なくビットフィールドを操作することに当てられています(ただし、特定のCORTEX-M1およびIARについては例を示します)。 このトピックは新しいものではないようですが、さまざまな方法の短所と利点を示したいと思います。 だから、私たちは始めています...



高水準言語でMKをプログラミングする場合、外部デバイスのレジスタと対話するという絶え間なく発生するタスクがあります(埋め込みはこれによって特徴付けられるように思えます)。 まず、この相互作用を整理するために、これらのレジスターは使用される言語の手段によって何らかの形で指定されなければなりません(これがCであると仮定しましょう)。 VUレジスターは、そのアドレスと構成によって特徴付けられ、言語を使用して表現する必要があります。 すぐに、標準Cはメモリ内の変数の場所に特定のアドレスを指定する可能性を表していないことに注意してください(少なくともそのようなことは知りません)。したがって、標準の拡張機能を使用するか、トリックを適用する必要があります。 アドレス0x40000004にある外部デバイスの32ビットレジスタに値3を書き込む必要があるとします。次の小さな松葉杖により、言語ツールを使用してこれを行うことができます。
*(uint32_t *) (0x40000004)=3;
      
      



この行をより詳しく考えてみましょう。 上記のどこか(stdint.hファイル内)に定義があります
 typedef unsigned int uint32_t;
      
      



、これにより、Cコンパイラのバージョンで32ビット数の表現について考える必要がなくなります。 コンパイラの別のバージョンに切り替える必要がある場合、独自のstdintファイルがあり、移植性の問題はありません。 この方法は非常に便利であり、組み込みプログラミングでの使用を強く推奨している著者のみ参加できます。

次に、この行を右から左に分析し、定数を作成します.32ビットの数値への参照と見なし、定数が指すメモリ領域を参照して命名を実行し、目的の結果を取得することをコンパイラに提案します。 結果として得られる構造はあまり美しくありません。まず、マジックナンバーが使用され、次に、人工的なものがいくつかあります。 少しきれいに書き直します:
 #define IO_DATA_ADRESS 0x40000004 #define WORD(ADR) *(uint32_t *) (ADR) WORD(IO_DATA_ADRESS)=3;
      
      



ここではすべてがすでにほとんど問題ありません。素晴らしいことではない唯一のことは、テキストでマクロを使用する必要があるため、(当然)別のマクロを追加することです。
 #define IO_DATA WORD(IO_DATA_ADRESS) IO_DATA=3;
      
      



これらのマクロを1つにまとめたい人には、すぐに答えます。特に、関数をラップするのが嫌いな場合は、マクロは実行中に価値がありません。 お互いの友人の内部に任意の数のマクロを埋め込むことができ、同時にすべてがコンパイラーによって処理され、唯一の定数が結果のコード(マクロの折りたたみの結果)に分類されます。 さて、コンパイル時間の増加はあまり重要ではないため、気付くことはありません。 もちろん、この状況は乱用されるべきではありません(彼らが言うように、狂信なしで)が、ネストされたマクロを使用するとコードが明確になります-ためらうことなくそれらを使用します(明日、新しい機能を備えたパッチをお客様に提示する必要があります)。

完全に機能するコードを取得し、標準の言語ツールを使用してすべて実装しましたが、どちらが良いと思われますか? それにもかかわらず、それは可能であり、より良いです(まあ、私はそれが好きです)-プリプロセッサの後のコードを見ると、レジスタへのアクセスは、拡張された形式で2つのアスタリスクのある同じい行に変わります。 そして、ここで私たちの助けにポインタが来ます(チップとデールではありません)。 次のコードを検討してください
 volatile uint32_t *pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); *pIO_DATA=3;
      
      



今、レジスタにアクセスするとき、マクロはまったくなく、すべては言語によって表現され、コードは完全に透過的であり、(私の意見では)より論理的です。 残っているのは、それほど必要ではないスターですが、それについては後で詳しく説明します。

今のところ、このような実装の両方のオプションの1つの欠点に注意してください。誰も何も書くことを止めることはできません
 #define IO_DATA_ADRESS 0x40000003
      
      



型コンパイラは型変換をチェックせず、足を踏み入れるのに干渉しないため、プログラムの実行中に例外が発生します(これはCであり、ADAではなくベイビーです)。 ASSERTの助けを借りてロープの長さを短くすることができますが、正直なところ、それらは常に書かれているわけではなく、どこにでも不十分な量で書かれています。

両方の構造の実行の効率については(私の投稿を読んだ人はすでにこれが私の流行であることに気づいていました)、私のコンパイラ(IAR C / C ++コンパイラARM 6.60.1.5097)では、ポインタを持つオプションが長くなります(過剰なインデックス付けのため) )、次の構造を使用して処理されます
 volatile uint32_t * const pIO_DATA = (uint32_t *) (IO_DATA_ADRESS);
      
      



、その後、コンパイラの結果は区別できなくなります。
 LDR.N R0, DATA_TABLE1 MOVS R1,#3 STR R1,[R0] ... DATA_TABLE: DC32 0x40000004
      
      



ちなみに、constキーワードを追加することは優れたプログラミングスタイルに対応します。これは、ポインターが明らかに変更されておらず、次のような不快な(および長年求められていた)エラーからも保護するためです。
 pIO_DATA=&i;
      
      



この形式では、レジスタの操作方法は非常に優れており、値の検証がないという欠陥がない場合は、ほぼ理想的です(理想的なものはありませんが、ほとんど)。 それにもかかわらず、問題があり、それがどのように解決されるのかを喜んで示します(知識を誇示するための素晴らしい機会です)。 MKで動作するように指向されたC言語の拡張では、アドレスの絶対値を示すための手段が導入されています。 私の場合、これは@演算子(および#pragma locationディレクティブ)であり、次の例を使用して説明できます。
 volatile uint32_t io_data @ IO_DATA_ADRESS; volatile uint32_t * const pIO_DATA = &io_data; i0_data=3; *pIO_DATA=3;
      
      



ここでこのオプションでは、コンパイラを使用してアドレスをチェックし、単語と一致しない値を入力しようとすると、(tadam!)エラーメッセージ(些細ですが、いい)が返されます。 この設計の有効性は前のものと同じであり、コンパイラー依存性がない場合(興味深い言葉が判明した場合)、使用することをお勧めします。 それでも、しぶしぶ、型変換とポインターを使用したオプションを選択します。 読者は、いくつかのフラグに応じて、1つまたは別のオプションを実装するマクロを作成することをお勧めします。

ここで、唯一の欠点、つまり余分な星を考慮し、欠点を否定できない利点に変えてください(手を見てください)。 MKプログラマーが知っているように、1つのレジスタとのみ相互作用するデバイスは存在しません 。 原則として、デバイスを制御し、そのステータスを報告するための一連のレジスタがあり、通常はMKのアドレススペースの近くに配置されます。 デバイスのステータスレジスタが0x40000008にあり、データを書き込む前に、このレジスタにゼロがあることを確認する必要があるとします。 もちろん、各レジスタを個別に定義し、無関係なオブジェクトのようにそれらを操作することを誰も気にしません。
 #define IO_DATA_ADRESS 0x40000004 #define IO_STATUS_ADRESS 0x40000008 ( - #define IO_STATUS_ADRESS IO_DATA_ADRESS +4) volatile uint32_t pIO_DATA = (uint32_t *) (IO_DATA_ADRESS); volatile uint32_t pIO_STATUS = (uint32_t *) (IO_STATUS_ADRESS); while {*pIO_STATUS) {}; *pIO_DATA=3;
      
      



ただし、より興味深い論理的に正当化された方法、つまり、メンバーが別個のレジスターである構造を作成する方法があります。 この場合、コードレベルでレジスタ間に接続があることをすでに理解しています。なぜなら、それらは単にまとめられただけではないためです(プログラムの作者がばかではない-しかし、他の説明がスローバックされるときにこのバージョンを残すため)、プログラムのロジックを理解するのに役立ちます。 それで何が起こるか:
 #define IO_DATA_ADRESS 0x40000004 typedef struct { uint32_t data; uint32_t status; } IO_DEVICE; volatile IO_DEVICE * const pio_device = (IO_DEVICE *) (IO_DATA_ADRESS); while (pio_device->status==0) {}; pio_device->data=3;
      
      



、コンパイラはロードしない2番目のコマンドのためにレジスタにポインタを保持するため、パフォーマンスは再び低下せず、わずかに向上しました。 この方法の唯一の欠点は、空のフィールドを構造体に挿入することでパスを調整できますが、レジスタのアドレスは実際には近く、理想的には厳密に追従する必要があることです。 もう1つの欠点は、フィールドを実際のアドレスにパックするという点でコンパイラに完全に依存しており、データのアライメント要件を明確に表す必要があることです。

アドレス指定についてはあまりにも多くのことが判明したため、トピックが興味深い場合はパート2でビットフィールドを使用することを検討します。



All Articles