マイクロコントローラーまたはターゲット自体の外部のコンポーネントを操作することは、ファームウェア開発における標準です。したがって、それらのライブラリを開発する方法を知ることが不可欠です。これらのライブラリを使用すると、ライブラリと対話し、情報やコマンドを交換できるようになります。ただし、レガシー コードや学生 (またはそうでない学生) のコードでは、コンポーネントとのこれらの対話がアプリケーション コード内で直接行われたり、別のファイルに配置されている場合でも、これらの対話が本質的に行われたりすることは珍しくありません。ターゲットに関連付けられています。
STMicroelectronics STM32F401RE のアプリケーションにおける Bosch BME280 温度、湿度、圧力センサーのライブラリ開発の悪い例を見てみましょう。この例では、コンポーネントを初期化し、1 秒ごとに温度を読み取ります。 (コード例では、さまざまなクロックやペリフェラルの初期化、USER CODE BEGIN や USER CODE END などのコメントなど、STM32CubeMX/IDE によって生成されるすべての「ノイズ」を省略します。)
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
この例に基づいて、ターゲットを変更する必要がある場合はどうなるでしょうか (在庫不足、コスト削減、または単に同じコンポーネントを使用する別の製品の開発に取り組んでいるなど)。システム内に同じタイプのコンポーネントが複数ある場合はどうなりますか?別の製品が同じコンポーネントを使用するとどうなりますか?ハードウェアをまだ持っていない場合、開発をテストするにはどうすればよいですか (プロの世界では、プロセスの特定の時点でファームウェアとハードウェアの開発フェーズが重なることがよくある非常に一般的な状況です)?
最初の 3 つの質問の答えは、ターゲットを切り替えるときにコードを完全に変更するか、同じタイプの追加コンポーネントで動作するように既存のコードを複製するか、または同じコードを実装するか、コードを編集することです。他のプロジェクト/製品。最後の質問では、コードを実行するハードウェアがなければコードをテストする方法はありません。これは、ハードウェアが完成して初めてコードのテストを開始し、ファームウェア開発自体に固有のエラーの修正を開始できることを意味し、その結果、製品の開発時間が延長されます。このため、この投稿のきっかけとなった疑問が生じます。ターゲットから独立し、再利用できるコンポーネントのライブラリを開発することは可能でしょうか?答えは「はい」です。これがこの投稿でわかります。
ターゲットからライブラリを分離するには、2 つのルールに従います。1) ライブラリを独自のコンパイル単位、つまり独自のファイルに実装します。2) ターゲット固有のヘッダーや関数への参照は一切ありません。 。 BME280 用の単純なライブラリを実装することでこれを実証します。まず、プロジェクト内に bme280 というフォルダーを作成します。 bme280 フォルダー内に、bme280.c、bme280.h、および bme280_interface.h のファイルを作成します。念のため言っておきますが、ファイルに bme280_interface.c という名前を付けるのを忘れていません。このファイルはライブラリの一部ではありません。
私は通常、ライブラリフォルダーを Application/lib/ 内に配置します。
bme280.h ファイルは、アプリケーションによって呼び出されるライブラリ内で使用可能なすべての関数を宣言します。一方、bme280.c ファイルは、ライブラリに含まれる可能性のある補助関数およびプライベート関数とともに、これらの関数の定義を実装します。では、bme280_interface.h ファイルには何が含まれているのでしょうか?さて、私たちのターゲットは、それが何であれ、何らかの方法で BME280 コンポーネントと通信する必要があります。この場合、BME280 は SPI または I2C 通信をサポートします。どちらの場合も、ターゲットはコンポーネントに対してバイトを読み書きできる必要があります。 bme280_interface.h ファイルはこれらの関数を宣言し、ライブラリから呼び出せるようにします。これらの関数の定義は、特定のターゲットに関連付けられる唯一の部分であり、ライブラリを別のターゲットに移行する場合に編集する必要がある唯一の部分になります。
まず、bme280.h ファイル内のライブラリで使用可能な関数を宣言します。
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
私たちが作成しているライブラリは非常に単純で、基本的な初期化関数と温度測定値を取得するための関数のみを実装します。次に、bme280.c ファイルに関数を実装しましょう。
投稿が冗長になりすぎないように、関数を説明するコメントは省略しています。これは、コメントが保存されるファイルです。現在、非常に多くの AI ツールが利用できるため、コードを文書化しない理由はありません。
bme280.c ファイルのスケルトンは次のようになります:
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
初期化に焦点を当てましょう。前述したように、BME280 は I2C 通信と SPI 通信の両方をサポートします。どちらの場合も、ターゲットの適切なペリフェラル (I2C または SPI) を初期化する必要があり、その後、それらを介してバイトを送受信できるようにする必要があります。 I2C 通信を使用していると仮定すると、STM32F401RE では次のようになります。
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
ペリフェラルが初期化されたら、コンポーネントを初期化する必要があります。ここでは、メーカーがデータシートで提供する情報を使用する必要があります。簡単な概要は次のとおりです。温度サンプリング チャネル (デフォルトではスリープ モード) を開始し、コンポーネントの ROM に保存されているいくつかの校正定数を読み取る必要があります。これは、後で温度を計算するために必要になります。
この投稿の目的は、BME280 の使用方法を学ぶことではないため、データシートに記載されている使用方法の詳細は省略します。
初期化は次のようになります:
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
詳細についてはコメントしてください。読み取ったキャリブレーション値は、dig_temp1、dig_temp2、および dig_temp3 という変数に保存されます。これらの変数はグローバルとして宣言されているため、ライブラリ内の残りの関数で使用できます。ただし、これらは静的として宣言されているため、ライブラリ内でのみアクセス可能です。ライブラリ外部の誰もこれらの値にアクセスしたり変更したりする必要はありません。
I2C 命令からの戻り値がチェックされ、失敗した場合には関数の実行が停止されることもわかります。これは問題ありませんが、改善できる可能性があります。その場合、BME280_init 関数の呼び出し元に何か問題が発生したことを通知した方がよいのではないでしょうか?これを行うには、bme280.h ファイルで次の列挙型を定義します。
私はそれらに typedef を使用します。 typedef は詳細を隠す代わりにコードの可読性を向上させるため、typedef の使用については議論があります。これは個人的な好みの問題であり、開発チームのメンバー全員が同じ認識を持っていることを確認する必要があります。
void BME280_init(void) { } float BME280_get_temperature(void) { }
2 つの注意事項: 通常、typedef であることを示すために typedef に _t 接尾辞を追加し、typedef の値またはメンバー (この場合は BME280_Status_) に typedef 接頭辞を追加します。後者は、異なるライブラリの列挙型間の衝突を避けるためのものです。誰もが OK を列挙型として使用していたら、私たちは困ったことになるでしょう。
これで、ステータスを返すように BME280_init 関数の宣言 (bme280.h) と定義 (bme280.c) の両方を変更できるようになりました。関数の最終バージョンは次のようになります:
void BME280_init(void) { MX_I2C1_Init(); }
#include "i2c.h" #include <stdint.h> #define BME280_TX_BUFFER_SIZE 32U #define BME280_RX_BUFFER_SIZE 32U #define BME280_TIMEOUT 200U #define BME280_ADDRESS 0x77U #define BME280_REG_CTRL_MEAS 0xF4U #define BME280_REG_DIG_T 0x88U static uint16_t dig_temp1 = 0U; static int16_t dig_temp2 = 0; static int16_t dig_temp3 = 0; void BME280_init(void) { uint8_t idx = 0U; uint8_t tx_buffer[BME280_TX_BUFFER_SIZE] = {0}; uint8_t rx_buffer[BME280_RX_BUFFER_SIZE] = {0}; HAL_StatusTypeDef status = HAL_ERROR; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; status = HAL_I2C_Mem_Write( &hi2c1, BME280_ADDRESS << 1U, BME280_REG_CTRL_MEAS, 1U, tx_buffer, (uint16_t)idx, BME280_TIMEOUT); if (status != HAL_OK) return; status = HAL_I2C_Mem_Read( &hi2c1, BME280_ADDRESS << 1U, BME280_REG_DIG_T, 1U, rx_buffer, 6U, BME280_TIMEOUT); if (status != HAL_OK) return; dig_temp1 = ((uint16_t)rx_buffer[0]); dig_temp1 = dig_temp1 | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = ((int16_t)rx_buffer[2]); dig_temp2 = dig_temp2 | (((int16_t)rx_buffer[3]) << 8U); dig_temp3 = ((int16_t)rx_buffer[4]); dig_temp3 = dig_temp3 | (((int16_t)rx_buffer[5]) << 8U); return; }
ステータス列挙型を使用しているため、bme280.c ファイルに bme280.h ファイルを含める必要があります。ライブラリはすでに初期化されています。次に、温度を取得する関数を作成しましょう。次のようになります:
typedef enum { BME280_Status_Ok, BME280_Status_Status_Err, } BME280_Status_t;
もうお気づきですね?関数シグネチャを変更して、コンポーネントとの通信に問題があったかどうかを示すステータスを返し、関数にパラメータとして渡されたポインタを通じて結果が返されるようにしました。この例に従っている場合は、一致するように bme280.h ファイル内の関数宣言を変更することを忘れないでください。
BME280_Status_t BME280_init(void);
すごいですね!この時点で、アプリケーションには次のものを含めることができます。
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
超きれい!これは読みやすいです。 STM32CubeMX/IDE からの Error_Handler 関数の使用を無視します。通常、これを使用することは推奨されませんが、たとえば、私たちにとっては機能します。それで、終わりましたか?
まあ、いいえ!コンポーネントとのやり取りを独自のファイルにカプセル化しました。しかし、そのコードはまだターゲット関数 (HAL 関数) を呼び出しています。ターゲットを変更すると、ライブラリを書き直す必要があります。ヒント: bme280_interface.h ファイルにはまだ何も書いていません。今すぐそれに取り組みましょう。
bme280.c ファイルを見ると、ターゲットとの対話は 3 つあります: ペリフェラルの初期化、バイトの書き込み/送信、およびバイトの読み取り/受信です。したがって、これら 3 つのインタラクションを bme280_interface.h ファイルで宣言します。
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
お気づきかと思いますが、インターフェイス ステータスの新しいタイプも定義しました。ここで、ターゲット関数を直接呼び出す代わりに、これらの関数を bme280.c ファイルから呼び出します。
void BME280_init(void) { } float BME280_get_temperature(void) { }
これで完了です! ターゲットの依存関係がライブラリから消えました。これで、STM32、MSP430、PIC32 などで動作するライブラリが完成しました。3 つのライブラリ ファイルには、ターゲットに固有のものは何も表示されません。唯一残っているものは何ですか?さて、インターフェイス関数を定義します。これは、各ターゲットに移行/適応する必要がある唯一の部分です。
通常は、Application/bsp/components/ フォルダー内で実行します。
次の内容を含む bme280_implementation.c というファイルを作成します。
void BME280_init(void) { MX_I2C1_Init(); }
このように、別のプロジェクトまたは別のターゲットでライブラリを使用したい場合は、bme280_implementation.c ファイルを調整するだけで済みます。残りはまったく同じままです。
これで、ライブラリの基本的な例を見てきました。この実装は最も単純で、最も安全で、最も一般的です。ただし、プロジェクトの特性に応じてさまざまなバリエーションがあります。この例では、リンク時に実装の選択を実行する方法を説明しました。つまり、コンパイル/リンク プロセス中にインターフェイス関数の定義を提供する bme280_implementation.c ファイルがあります。 2 つの実装が必要な場合はどうなるでしょうか? 1 つは I2C 通信用、もう 1 つは SPI 通信用です。その場合、実行時に関数ポインターを使用して実装を指定する必要があります。
もう 1 つの側面は、この例ではシステム内に BME280 が 1 つだけあると仮定していることです。複数ある場合はどうなるでしょうか?コードをコピー/ペーストして、BME280_1 や BME280_2 などの関数にプレフィックスを追加する必要がありますか?いいえ、それは理想的ではありません。私たちがやるべきことは、ハンドラーを使用して、コンポーネントの異なるインスタンス上で同じライブラリを操作できるようにすることです。
これらの側面と、ハードウェアを利用可能にする前にライブラリをテストする方法については、別の記事で取り上げます。これについては、今後の記事で取り上げます。今のところ、ライブラリを適切に実装しないという言い訳はできません。ただし、私の最初の推奨事項 (逆説的ですが、最後に残した推奨事項) は、何よりもまず、メーカーがコンポーネントの公式ライブラリをまだ提供していないことを確認することです。これはライブラリを立ち上げて実行する最も速い方法です。メーカーが提供するライブラリは、今日私たちが見たものと同様の実装に従う可能性が高く、私たちの仕事はインターフェイスの実装部分をターゲットまたは製品に適応させることであるため、ご安心ください。
このトピックに興味がある場合は、この投稿や組み込みシステム開発に関連するその他の記事を私のブログで見つけることができます。 ?
以上が再利用可能なコンポーネント ライブラリ: ターゲット間の移行を簡素化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。