使用微控制器或目標本身外部的元件進行操作是韌體開發的常態。因此,了解如何為他們開發庫至關重要。這些庫允許我們與它們互動並交換資訊或命令。然而,在遺留程式碼或學生(或非學生)的程式碼中,經常會發現這些與元件的互動是直接在應用程式程式碼中完成的,或者即使放在單獨的檔案中,這些互動本質上也是如此與目標綁定。
讓我們來看看 STMicroelectronics STM32F401RE 應用程式中 Bosch BME280 溫度、濕度和壓力感測器的程式庫開發的一個糟糕範例。在範例中,我們要初始化組件並每 1 秒讀取一次溫度。 (在範例程式碼中,我們將省略STM32CubeMX/IDE產生的所有“噪音”,例如各種時鐘和周邊的初始化,或者諸如USER CODE BEGIN或USER CODE END之類的註解。)
#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. } }
基於這個例子,我們可以提出一系列問題:如果我需要更改目標(無論是由於庫存短缺、想要降低成本,還是只是開發使用相同組件的另一個產品),會發生什麼?如果系統中有多個相同類型的元件,會發生什麼情況?如果另一個產品使用相同的組件會發生什麼?如果我還沒有硬件,如何測試我的開發(這是專業領域中非常常見的情況,韌體和硬體開發階段經常在過程中的某些點重疊)?
對於前三個問題,答案是編輯程式碼,是否在切換目標時完全更改它,複製現有程式碼以與相同類型的附加元件一起操作,或者為目標實現相同的程式碼其他項目/產品。在最後一個問題中,如果沒有硬體執行程式碼,就無法測試程式碼。這意味著只有硬體完成後我們才能開始測試程式碼並開始修復韌體開發本身固有的錯誤,從而延長產品開發時間。這就提出了引發這篇文章的問題:是否可以為獨立於目標並允許重複使用的元件開發庫?答案是肯定的,這就是我們將在這篇文章中看到的內容。
為了將庫與目標隔離,我們將遵循兩條規則: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檔案中的函數。
為了避免貼文過於冗長,我跳過了記錄功能的註解。這是這些評論所在的文件。如今有這麼多的人工智慧工具可用,沒有理由不記錄您的程式碼。
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 的使用存在爭議,因為它們以隱藏細節為代價提高了程式碼的可讀性。這是個人喜好的問題,並確保開發團隊的所有成員都在同一頁上。
void BME280_init(void) { } float BME280_get_temperature(void) { }
兩個注意事項:我通常在 typedef 中添加 _t 後綴以表明它們是 typedef,並且在 typedef 的值或成員中添加 typedef 前綴,在本例中為 BME280_Status_。後者是為了避免來自不同函式庫的枚舉之間的衝突。如果每個人都使用 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 文件,我們與目標的交互有三重:初始化週邊、寫入/發送位元組以及讀取/接收位元組。因此,我們要做的就是在 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) { }
Et voilà! 目標依賴項已從庫中消失。我們現在有了一個適用於 STM32、MSP430、PIC32 等的函式庫。在這三個庫文件中,不應出現任何特定於任何目標的內容。唯一剩下的是什麼?好了,定義介面函數。這是唯一需要針對每個目標遷移/調整的部分。
我通常在資料夾Application/bsp/components/中進行。
我們建立一個名為 bme280_implementation.c 的文件,其中包含以下內容:
void BME280_init(void) { MX_I2C1_Init(); }
這樣,如果我們想在另一個專案或另一個目標上使用該程式庫,我們只需要修改 bme280_implementation.c 檔案即可。其餘部分保持完全相同。
至此,我們已經看到了一個函式庫的基本範例。這種實作是最簡單、最安全、最常見的。然而,根據我們項目的特點,有不同的變體。在此範例中,我們了解如何在連結時執行實作選擇。也就是說,我們有 bme280_implementation.c 文件,它提供了編譯/連結過程中介面函數的定義。如果我們想要有兩個實作會怎樣?一個用於 I2C 通信,另一個用於 SPI 通信。在這種情況下,我們需要使用函數指標在運行時指定實作。
另一方面是,在這個例子中,我們假設系統中只有一個 BME280。如果我們有多個的話會發生什麼事?我們是否應該複製/貼上程式碼並為 BME280_1 和 BME280_2 等函數添加前綴?不,這並不理想。我們要做的是使用處理程序來允許我們在元件的不同實例上使用相同的函式庫。
這些方面以及如何在硬體可用之前測試我們的庫是另一篇文章的主題,我們將在以後的文章中介紹。目前,我們沒有理由不正確實作庫。然而,我的第一個建議(矛盾的是,我留到最後的建議)是,首先也是最重要的,確保製造商尚未為其組件提供官方庫。這是啟動和運行庫的最快方法。請放心,製造商提供的程式庫可能會遵循與我們今天看到的類似的實現,我們的工作將是使介面實現部分適應我們的目標或產品。
如果您對這個主題感興趣,您可以在我的部落格上找到這篇文章以及其他與嵌入式系統開發相關的文章! ?
以上是可重複使用元件庫:簡化目標之間的遷移的詳細內容。更多資訊請關注PHP中文網其他相關文章!