마이크로 컨트롤러 외부 구성 요소 또는 대상 자체를 사용하여 작동하는 것은 펌웨어 개발의 표준입니다. 그러므로 이들을 위한 라이브러리를 개발하는 방법을 아는 것이 필수적입니다. 이러한 라이브러리를 통해 우리는 라이브러리와 상호 작용하고 정보나 명령을 교환할 수 있습니다. 그러나 레거시 코드 또는 학생(또는 학생이 아닌 사람)의 코드에서 구성 요소와의 이러한 상호 작용이 애플리케이션 코드에서 직접 수행되거나 별도의 파일에 배치된 경우에도 이러한 상호 작용이 본질적으로 발생하는 경우가 많습니다. 목표에 묶여 있습니다.
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 파일에 함수를 구현해 보겠습니다.
게시물을 너무 장황하게 작성하지 않기 위해 기능을 설명하는 댓글은 건너뛰었습니다. 해당 댓글이 저장되는 파일입니다. 오늘날 사용할 수 있는 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 사용에 대한 논쟁이 있습니다. 이는 개인 취향의 문제이며 개발팀의 모든 구성원이 같은 생각을 갖고 있는지 확인하는 것입니다.
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/comComponents/폴더 안에서 합니다.
다음 내용으로 bme280_implementation.c라는 파일을 생성합니다.
void BME280_init(void) { MX_I2C1_Init(); }
이 방법으로 다른 프로젝트나 다른 대상에서 라이브러리를 사용하려면 bme280_implementation.c 파일만 조정하면 됩니다. 나머지는 그대로 유지됩니다.
이제 우리는 라이브러리의 기본 예를 살펴보았습니다. 이 구현은 가장 간단하고 안전하며 가장 일반적입니다. 그러나 우리 프로젝트의 특성에 따라 다양한 변형이 있습니다. 이 예에서는 링크 타임에 구현 선택을 수행하는 방법을 살펴보았습니다. 즉, 컴파일/링크 프로세스 중에 인터페이스 기능의 정의를 제공하는 bme280_implementation.c 파일이 있습니다. 두 가지 구현을 원한다면 어떻게 될까요? 하나는 I2C 통신용이고 다른 하나는 SPI 통신용입니다. 이 경우 함수 포인터를 사용하여 런타임에 구현을 지정해야 합니다.
또 다른 측면은 이 예에서는 시스템에 BME280이 하나만 있다고 가정한다는 것입니다. 둘 이상이면 어떻게 될까요? 코드를 복사/붙여넣고 BME280_1 및 BME280_2와 같은 함수에 접두사를 추가해야 합니까? 아니요. 그건 이상적이지 않습니다. 우리가 할 일은 핸들러를 사용하여 구성 요소의 다른 인스턴스에서 동일한 라이브러리로 작업할 수 있도록 하는 것입니다.
하드웨어를 사용하기 전에 이러한 측면과 라이브러리를 테스트하는 방법은 다른 게시물의 주제이며 향후 기사에서 다룰 것입니다. 현재로서는 라이브러리를 제대로 구현하지 않을 변명의 여지가 없습니다. 그러나 나의 첫 번째 권장 사항(그리고 역설적이게도 끝까지 남겨둔 권장 사항)은 무엇보다도 제조업체가 해당 구성 요소에 대한 공식 라이브러리를 이미 제공하지 않는지 확인하라는 것입니다. 이것이 라이브러리를 시작하고 실행하는 가장 빠른 방법입니다. 제조업체가 제공하는 라이브러리는 오늘날 우리가 본 것과 유사한 구현을 따를 가능성이 높으며, 우리의 임무는 인터페이스 구현 부분을 목표나 제품에 맞게 조정하는 것입니다.
이 주제에 관심이 있으시면 제 블로그에서 이 게시물과 임베디드 시스템 개발과 관련된 다른 게시물을 찾아보실 수 있습니다! ?
위 내용은 재사용 가능한 구성요소 라이브러리: 대상 간 마이그레이션 단순화의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!