4

組み込み C では、いくつかの固定/汎用アルゴリズムを使用することは非常に自然ですが、複数の実装が可能です。これは、いくつかの製品プレゼンテーション、オプションの場合もあれば、オプションの RAM、さまざまな IP セット MCU、アップグレードされた周波数など、製品ロードマップ戦略の一部に過ぎないことによるものです。

私のプロジェクトのほとんどでは、外部の状態評価、時間管理、メモリ ストレージなどを実装する実際の関数から、コア部分、アルゴリズム、およびロジック アーキテクチャを分離することによって、それに対処しています。

当然のことながら、私は C 関数ポインター メカニズムを使用し、それらのポインターに意味のある名前のセットを使用します。例えば

unsigned char (*ucEvalTemperature)(int *);

それは温度を int に格納し、戻り値は OKness です。

ここで、製品の特定の構成について、私が持っていると想像してください

unsigned char ucReadI2C_TMP75(int *);

TMP75 デバイスから I2C バス上の温度を読み取る関数と、

unsigned char ucReadCh2_ADC(unsigned char *);

ADC によって読み取られるダイオードの電圧降下を読み取る関数。これは、非常に広範なストロークで温度を評価する方法です。

基本的な機能は同じですが、オプション セットの製品が異なります。

一部の構成では、ucEvalTemperature を ucReadI2C_TMP75 に設定しますが、他の構成では ucReadCh2_ADC を使用します。この軽度のケースでは、問題を回避するために、引数の型を int に変更する必要があります。ポインターは常に同じサイズですが、関数のシグネチャは同じサイズではなく、コンパイラーは文句を言うからです。わかりました... それは殺人者ではありません。

この問題は、異なる引数のセットが必要な関数で明らかになります。署名は決して正しくなく、コンパイラは Fpointers を解決できません。

だから、私は3つの方法があります:

  • 引数のグローバル スタックを使用し、すべての関数は unsigned char Func(void); です。
  • 各実装にヘルパー関数を使用して、make/call への適切な割り当てをオンに切り替えます。
  • JMP / LCALL アセンブリ呼び出しを使用すると (もちろんこれは恐ろしいことです)、コール スタックで大きな問題が発生する可能性があります。

どちらもエレガントでもなければ、落ち着いてもいません...あなたのアプローチ/アドバイスは何ですか?

4

3 に答える 3

2

可能であれば、構造体「インターフェイス」を使用します。

struct Device {
  int (*read_temp)(int*, Device*);
} *dev;

あれを呼べ:

dev->read_temp(&ret, dev);

追加の引数が必要な場合は、それらをデバイス内にパックします

struct COMDevice {
  struct Device d;
  int port_nr;
};

これを使用するときは、単にダウンキャストします。

次に、デバイス用の関数を作成します。

int foo_read_temp(int* ret, struct Device*)
{
  *ret = 100;
  return 0;
}

int com_device_read_temp(int* ret, struct Device* dev)
{
  struct COMDevice* cdev = (struct COMDevice*)dev; /* could be made as a macro */
  ... communicate with device on com port cdev->port_nr ...
  *ret = ... what you got ...
  return error;
}

次のようにデバイスを作成します。

Device* foo_device= { .read_temp = foo_read_temp };
COMDevice* com_device_1= { .d = { .read_temp = com_read_temp },
  .port_nr = 0x3e8
};
COMDevice* com_device_1= { .d = { .read_temp = com_read_temp },
  .port_nr = 0x3f8
};

温度を読み取る必要がある関数に Device 構造体を渡します。

これ (または類似のもの) は Linux カーネルで使用されますが、構造体内に関数ポインターを配置せず、そのための特別な静的構造体を作成し、この構造体へのポインターをデバイス構造体内に格納します。これは、C++ のようなオブジェクト指向言語がポリモーフィズムを実装する方法とほぼ同じです。

関数を参照する Device 構造体を含む別のコンパイル ユニットに配置した場合でも、スペースを節約し、リンク中にそれらを除外できます。

異なるタイプの引数が必要な場合、または引数が少ない場合は、忘れてください。これは、変更したいものに対して (何らかの意味で) 共通のインターフェースを設計することはできませんが、共通のインターフェースがなければ、変更可能な実装は不可能であることを意味します。コンパイル時のポリモーフィズムを使用できます (たとえば、typedef と異なる実装用の個別のコンパイル ユニット。そのうちの 1 つはバイナリにリンクされます)。ただし、少なくともソース互換性がある必要があります。仕方。

于 2009-06-15T10:21:29.430 に答える
2

私は通常、階層化されたアーキテクチャを好みます。ハードウェアとの通信は「ドライバー」によって実現されます。アルゴリズム層は、ドライバーによって実装される関数 (readTemp) を呼び出します。重要な点は、インターフェイスを定義する必要があり、それをすべてのドライバー実装で尊重する必要があるということです。

上位層は、温度がどのように読み取られるかについて何も知らない必要があります (TMP75 を使用するか ADC を使用するかは問題ではありません)。ドライバー アーキテクチャの欠点は、通常、実行時にドライバーを切り替えることができないことです。ほとんどの組み込みプロジェクトでは、これは問題になりません。これを行う場合は、実装関数ではなく、ドライバーによって公開される関数 (共通インターフェイスに従う) への関数ポインターを定義します。

于 2009-06-15T10:43:59.157 に答える
0

正しい方法は、ヘルパー関数を使用することです。確かに、unsigned char ucReadCh2_ADC(unsigned char *);は結果を値[0,255]として格納しているように見える場合があります。しかし、実際の範囲は[0,255]だと誰が言いますか?そして、たとえそうだったとしても、それらの値は何を表していますか?

一方、typedefを使用する場合unsigned long milliKelvinは、typedefunsigned char (*EvalTemperature)(milliKelvin *out);の方がはるかに明確です。そして、すべての関数について、それがどのようにラップされるべきかが明らかになります-非常に多くの場合、些細な関数です。

関数がunsignedcharを返さなかったため、typedefから「uc」プレフィックスを削除したことに注意してください。ブール値のOK-nessを返します。(コルベールファンは真実を示すためにフロートを使用したいかもしれません;-))

于 2009-06-15T11:08:05.993 に答える