Rumah >pembangunan bahagian belakang >C++ >Pustaka Komponen Boleh Digunakan Semula: Memudahkan Penghijrahan Antara Sasaran
Beroperasi dengan komponen luar mikropengawal atau sasaran itu sendiri adalah kebiasaan dalam pembangunan perisian tegar. Oleh itu, mengetahui cara membangunkan perpustakaan untuk mereka adalah penting. Perpustakaan ini membolehkan kami berinteraksi dengan mereka dan bertukar maklumat atau arahan. Walau bagaimanapun, adalah perkara biasa untuk mencari, dalam kod atau kod warisan daripada pelajar (atau bukan pelajar), bahawa interaksi dengan komponen ini dilakukan secara langsung dalam kod aplikasi atau, walaupun diletakkan dalam fail berasingan, interaksi ini secara intrinsik. terikat pada sasaran.
Mari kita lihat contoh buruk pembangunan perpustakaan untuk sensor suhu, kelembapan dan tekanan Bosch BME280 dalam aplikasi untuk STMicroelectronics STM32F401RE. Dalam contoh, kami ingin memulakan komponen dan membaca suhu setiap 1 saat. (Dalam kod contoh, kami akan meninggalkan semua "bunyi" yang dijana oleh STM32CubeMX/IDE, seperti pengamulaan pelbagai jam dan persisian, atau ulasan seperti KOD PENGGUNA BERMULA atau KOD PENGGUNA TAMAT.)
#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. } }
Berdasarkan contoh ini, kami boleh menimbulkan satu siri soalan: apakah yang berlaku jika saya perlu menukar sasaran (sama ada disebabkan kekurangan stok, ingin mengurangkan kos atau hanya mengusahakan produk lain yang menggunakan komponen yang sama)? Apakah yang berlaku jika saya mempunyai lebih daripada satu komponen daripada jenis yang sama dalam sistem? Apakah yang berlaku jika produk lain menggunakan komponen yang sama? Bagaimanakah saya boleh menguji pembangunan saya jika saya belum mempunyai perkakasan (situasi yang sangat biasa dalam dunia profesional di mana fasa pembangunan perisian tegar dan perkakasan sering bertindih pada titik tertentu dalam proses)?
Untuk tiga soalan pertama, jawapannya ialah mengedit kod, sama ada menukarnya sepenuhnya apabila menukar sasaran, untuk menduplikasi kod sedia ada untuk beroperasi dengan komponen tambahan daripada jenis yang sama, atau untuk melaksanakan kod yang sama untuk projek/produk lain. Dalam soalan terakhir, tiada cara untuk menguji kod tanpa mempunyai perkakasan untuk melaksanakannya. Ini bermakna hanya selepas perkakasan selesai kami boleh mula menguji kod kami dan mula membetulkan ralat yang wujud pada pembangunan perisian tegar itu sendiri, sekali gus memanjangkan masa pembangunan produk. Ini menimbulkan persoalan yang menimbulkan siaran ini: adakah mungkin untuk membangunkan perpustakaan untuk komponen yang bebas daripada sasaran dan membenarkan penggunaan semula? Jawapannya ya, dan inilah yang akan kita lihat dalam siaran ini.
Untuk mengasingkan perpustakaan daripada sasaran, kami akan mengikut dua peraturan: 1) kami akan melaksanakan perpustakaan dalam unit kompilasinya sendiri, bermakna failnya sendiri dan 2) tidak akan ada rujukan kepada mana-mana pengepala atau fungsi khusus sasaran . Kami akan menunjukkan ini dengan melaksanakan perpustakaan mudah untuk BME280. Untuk memulakan, kami akan mencipta folder yang dipanggil bme280 dalam projek kami. Di dalam folder bme280, kami akan mencipta fail berikut: bme280.c, bme280.h dan bme280_interface.h. Untuk menjelaskan, tidak, saya tidak lupa menamakan fail bme280_interface.c. Fail ini tidak akan menjadi sebahagian daripada pustaka.
Saya biasanya meletakkan folder perpustakaan di dalam Application/lib/.
Fail bme280.h akan mengisytiharkan semua fungsi yang tersedia dalam perpustakaan kami untuk dipanggil oleh aplikasi kami. Sebaliknya, fail bme280.c akan melaksanakan takrifan fungsi tersebut, bersama-sama dengan mana-mana fungsi tambahan dan peribadi yang mungkin mengandungi perpustakaan. Jadi, apakah yang terkandung dalam fail bme280_interface.h? Nah, sasaran kami, walau apa pun, perlu berkomunikasi dengan komponen BME280 dalam satu cara atau yang lain. Dalam kes ini, BME280 menyokong komunikasi SPI atau I2C. Dalam kedua-dua kes, sasaran mesti boleh membaca dan menulis bait kepada komponen. Fail bme280_interface.h akan mengisytiharkan fungsi tersebut supaya ia boleh dipanggil dari perpustakaan. Takrif fungsi ini akan menjadi satu-satunya bahagian yang terikat pada sasaran tertentu dan ia akan menjadi satu-satunya perkara yang perlu kita edit jika kita memindahkan perpustakaan ke sasaran lain.
Kami bermula dengan mengisytiharkan fungsi yang tersedia dalam perpustakaan dalam fail 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. } }
Pustaka yang kami cipta adalah sangat mudah, dan kami hanya akan melaksanakan fungsi permulaan asas dan satu lagi untuk mendapatkan ukuran suhu. Sekarang, mari kita laksanakan fungsi dalam fail bme280.c.
Untuk mengelakkan siaran terlalu bertele-tele, saya melangkau komen yang akan mendokumentasikan fungsi. Ini ialah fail tempat komen tersebut akan pergi. Dengan begitu banyak alatan AI yang tersedia hari ini, tiada alasan untuk tidak mendokumenkan kod anda.
Rangka fail bme280.c adalah seperti berikut:
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
Mari fokus pada permulaan. Seperti yang dinyatakan sebelum ini, BME280 menyokong komunikasi I2C dan SPI. Dalam kedua-dua kes, kita perlu memulakan persisian sasaran yang sesuai (I2C atau SPI), dan kemudian kita perlu boleh menghantar dan menerima bait melaluinya. Dengan mengandaikan kami menggunakan komunikasi I2C, dalam STM32F401RE ia akan menjadi:
#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. } }
Setelah peranti dimulakan, kita perlu memulakan komponen. Di sini, kita mesti menggunakan maklumat yang diberikan oleh pengilang dalam lembaran datanya. Berikut ialah ringkasan pantas: kita perlu memulakan saluran pensampelan suhu (yang berada dalam mod tidur secara lalai) dan membaca beberapa pemalar penentukuran yang disimpan dalam ROM komponen, yang kita perlukan kemudian untuk mengira suhu.
Matlamat siaran ini bukan untuk mempelajari cara menggunakan BME280, jadi saya akan melangkau butiran penggunaannya, yang boleh anda temui dalam lembaran datanya.
Permulaan akan kelihatan seperti ini:
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
Butiran untuk diulas. Nilai penentukuran yang kita baca disimpan dalam pembolehubah yang dipanggil dig_temp1, dig_temp2 dan dig_temp3. Pembolehubah ini diisytiharkan sebagai global supaya ia tersedia untuk seluruh fungsi dalam perpustakaan. Walau bagaimanapun, ia diisytiharkan sebagai statik supaya ia hanya boleh diakses dalam perpustakaan. Tiada sesiapa di luar perpustakaan perlu mengakses atau mengubah suai nilai ini.
Kami juga melihat bahawa nilai pulangan daripada arahan I2C disemak, dan sekiranya berlaku kegagalan, pelaksanaan fungsi dihentikan. Ini bagus, tetapi ia boleh diperbaiki. Bukankah lebih baik untuk memberitahu pemanggil fungsi BME280_init bahawa ada masalah, jika itu berlaku? Untuk melakukan ini, kami mentakrifkan enum berikut dalam fail bme280.h.
Saya menggunakan typedef untuk mereka. Terdapat perdebatan tentang penggunaan typedef kerana mereka meningkatkan kebolehbacaan kod dengan kos menyembunyikan butiran. Ini soal keutamaan peribadi dan memastikan semua ahli pasukan pembangunan berada di halaman yang sama.
void BME280_init(void) { } float BME280_get_temperature(void) { }
Dua nota: Saya biasanya menambah akhiran _t pada typedef untuk menunjukkan bahawa ia adalah typedef, dan saya menambah awalan typedef pada nilai atau ahli typedef, dalam kes ini BME280_Status_. Yang terakhir adalah untuk mengelakkan perlanggaran antara enum dari perpustakaan yang berbeza. Jika semua orang menggunakan OK sebagai enum, kami akan menghadapi masalah.
Kini kita boleh mengubah suai kedua-dua pengisytiharan (bme280.h) dan takrifan (bme280.c) bagi fungsi BME280_init untuk mengembalikan status. Versi akhir fungsi kami ialah:
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; }
Memandangkan kami menggunakan enum status, kami mesti memasukkan fail bme280.h dalam fail bme280.c. Kami telah pun memulakan perpustakaan. Sekarang, mari kita cipta fungsi untuk mendapatkan semula suhu. Ia akan kelihatan seperti ini:
typedef enum { BME280_Status_Ok, BME280_Status_Status_Err, } BME280_Status_t;
Anda perasan, bukan? Kami telah mengubah suai tandatangan fungsi supaya ia mengembalikan status untuk menunjukkan sama ada terdapat isu komunikasi dengan komponen atau tidak, dan hasilnya dikembalikan melalui penuding yang diluluskan sebagai parameter kepada fungsi. Jika anda mengikuti contoh, ingat untuk mengubah suai pengisytiharan fungsi dalam fail bme280.h supaya ia sepadan.
BME280_Status_t BME280_init(void);
Hebat! Pada ketika ini, dalam aplikasi kita boleh mempunyai:
#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. } }
Sangat bersih! Ini boleh dibaca. Abaikan penggunaan fungsi Error_Handler daripada STM32CubeMX/IDE. Biasanya tidak disyorkan untuk menggunakannya, tetapi sebagai contoh, ia berfungsi untuk kami. Jadi, sudah selesai?
Nah, tidak! Kami telah merangkum interaksi kami dengan komponen ke dalam failnya sendiri. Tetapi kodnya masih memanggil fungsi sasaran (fungsi HAL)! Jika kita menukar sasaran, kita perlu menulis semula perpustakaan! Petunjuk: kami belum menulis apa-apa dalam fail bme280_interface.h lagi. Mari kita atasinya sekarang.
Jika kita melihat pada fail bme280.c, interaksi kita dengan sasaran adalah tiga kali ganda: untuk memulakan peranti, untuk menulis/menghantar bait dan untuk membaca/menerima bait. Jadi, apa yang akan kami lakukan ialah mengisytiharkan ketiga-tiga interaksi tersebut dalam fail bme280_interface.h.
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
Jika anda perasan, kami juga telah menentukan jenis baharu untuk status antara muka. Kini, daripada memanggil fungsi sasaran secara langsung, kami akan memanggil fungsi ini daripada fail bme280.c.
void BME280_init(void) { } float BME280_get_temperature(void) { }
Et voilà! Kebergantungan sasaran telah hilang daripada pustaka. Kami kini mempunyai perpustakaan yang berfungsi untuk STM32, MSP430, PIC32, dsb. Dalam tiga fail pustaka, tiada apa yang khusus untuk mana-mana sasaran akan dipaparkan. Apa yang tinggal? Nah, mentakrifkan fungsi antara muka. Ini adalah satu-satunya bahagian yang perlu dipindahkan/disesuaikan untuk setiap sasaran.
Saya biasanya melakukannya di dalam folder Application/bsp/components/.
Kami mencipta fail yang dipanggil bme280_implementation.c dengan kandungan berikut:
void BME280_init(void) { MX_I2C1_Init(); }
Dengan cara ini, jika kita ingin menggunakan perpustakaan dalam projek lain atau pada sasaran lain, kita hanya perlu menyesuaikan fail bme280_implementation.c. Selebihnya tetap sama.
Dengan ini, kami telah melihat contoh asas perpustakaan. Pelaksanaan ini adalah yang paling mudah, paling selamat dan paling biasa. Walau bagaimanapun, terdapat variasi berbeza bergantung pada ciri projek kami. Dalam contoh ini, kita telah melihat cara melaksanakan pemilihan pelaksanaan pada masa pautan. Iaitu, kami mempunyai fail bme280_implementation.c, yang menyediakan takrifan fungsi antara muka semasa proses penyusunan/pautan. Apa yang akan berlaku jika kita mahu mempunyai dua pelaksanaan? Satu untuk komunikasi I2C dan satu lagi untuk komunikasi SPI. Dalam kes itu, kita perlu menentukan pelaksanaan pada masa jalan menggunakan penunjuk fungsi.
Aspek lain ialah dalam contoh ini, kami menganggap hanya terdapat satu BME280 dalam sistem. Apa yang akan berlaku jika kita mempunyai lebih daripada satu? Patutkah kita menyalin/menampal kod dan menambah awalan pada fungsi seperti BME280_1 dan BME280_2? Tidak. Itu tidak ideal. Apa yang kami akan lakukan ialah menggunakan pengendali untuk membolehkan kami beroperasi dengan perpustakaan yang sama pada contoh komponen yang berbeza.
Aspek-aspek ini dan cara menguji perpustakaan kami sebelum perkakasan kami tersedia adalah topik untuk siaran lain, yang akan kami bincangkan dalam artikel akan datang. Buat masa ini, kami tidak mempunyai alasan untuk tidak melaksanakan perpustakaan dengan betul. Walau bagaimanapun, cadangan pertama saya (dan secara paradoks, yang saya tinggalkan untuk penghujungnya) ialah, pertama sekali, pastikan pengilang belum menyediakan perpustakaan rasmi untuk komponen mereka. Ini adalah cara terpantas untuk menyediakan perpustakaan dan berjalan. Yakinlah bahawa perpustakaan yang disediakan oleh pengilang berkemungkinan akan mengikuti pelaksanaan yang serupa dengan yang telah kita lihat hari ini, dan tugas kami adalah untuk menyesuaikan bahagian pelaksanaan antara muka dengan sasaran atau produk kami.
Jika anda berminat dengan topik ini, anda boleh mencari siaran ini dan lain-lain yang berkaitan dengan pembangunan sistem terbenam di blog saya! ?
Atas ialah kandungan terperinci Pustaka Komponen Boleh Digunakan Semula: Memudahkan Penghijrahan Antara Sasaran. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!