poniedziałek, 30 maja 2022

[48] STM32F4 - BMI160 I2C

W tym poście chciałbym opisać sposób działanie układu BMI160.

[Moduł czujnika BMI160]

Opis układu:


Układ MEMS Bosch BMI160 (datasheet) jest układem składającym się z 16 bitowego 3 osiowego akcelerometru i 3 osiowego żyroskopu. Można mierzyć przyśpieszenie liniowe w zakresach +/- 2g, 4g, 8, 16g oraz prędkość kątową w zakresie od -/+ 125 dps.


Arduino ESP32:


Na samym początku przetestuje podstawowe działanie czujnika z układem ESP32 komunikacja po interfejsie I2C. Skorzystam z gotowych bibliotek i przykładów dostępnych w serwisie GitHub.

Zacznę od sprawdzenie połączenia z czujnikiem przez zastosowanie scanera I2C w celu ustalenia adresu. Poniższy przykład można znaleźć w Internecie:

  1. #include <Wire.h>
  2.  
  3. void setup()
  4. {
  5.   Serial.begin (115200);  
  6.   Wire.begin (21, 22);   // sda= GPIO_21 /scl= GPIO_22
  7. }
  8.  
  9. void Scanner ()
  10. {
  11.   Serial.println ();
  12.   Serial.println ("I2C scanner. Scanning ...");
  13.   byte count = 0;
  14.  
  15.   Wire.begin();
  16.   for (byte i = 8; i < 120; i++)
  17.   {
  18.     Wire.beginTransmission (i);          // Begin I2C transmission Address (i)
  19.     if (Wire.endTransmission () == 0)  // Receive 0 = success (ACK response)
  20.     {
  21.       Serial.print ("Found address: ");
  22.       Serial.print (i, DEC);
  23.       Serial.print (" (0x");
  24.       Serial.print (i, HEX);     // PCF8574 7 bit address
  25.       Serial.println (")");
  26.       count++;
  27.     }
  28.   }
  29.   Serial.print ("Found ");      
  30.   Serial.print (count, DEC);        // numbers of devices
  31.   Serial.println (" device(s).");
  32. }
  33.  
  34. void loop()
  35. {
  36.   Scanner ();
  37.   delay (100);
  38. }

Czujnik podłączony do urządzenia w następujący sposób:

  • 3V3 - 3V3
  • GND - GND
  • SCL  - D22
  • SDA - D21
  • SAO  - GND

W odpowiedzi w terminalu otrzymamy adres 0x68. Gdy pin SAO zostanie odłączony od linii GND adres urządzenia wynosi 0x69.

Program odczytujący dane z układu (biblioteka do pobrania z serwisu GitHub):

  1. #include <BMI160Gen.h>

  2. const int i2c_addr = 0x68;
  3.  
  4. void setup() {
  5.   Serial.begin(115200);
  6.   BMI160.begin(BMI160GenClass::I2C_MODE, i2c_addr);
  7. }
  8.  
  9. void loop() {
  10.   int gx, gy, gz;
  11.  
  12.   BMI160.readGyro(gx, gy, gz);
  13.  
  14.   Serial.print("g:\t");
  15.   Serial.print(gx);
  16.   Serial.print("\t");
  17.   Serial.print(gy);
  18.   Serial.print("\t");
  19.   Serial.print(gz);
  20.   Serial.println();
  21.  
  22.   delay(500);
  23. }

Otrzymane odpowiedzi z odczytem danych wyglądają następująco:

  1. 01:33:38.004 -> g:  15069   189 4871
  2. 01:33:38.802 -> g:  2800    -2667   -1027
  3. 01:33:39.032 -> g:  -2903   -2179   8126
  4. 01:33:39.505 -> g:  -18955  -3761   -12845
  5. 01:33:40.003 -> g:  -2875   -2031   -6746
  6. 01:33:40.516 -> g:  14811   5472    8239
  7. 01:33:41.009 -> g:  8308    -7069   8401
  8. 01:33:41.493 -> g:  11115   -1527   3109
  9. 01:33:42.008 -> g:  -32768  -4731   -25564
  10. 01:33:42.494 -> g:  -3444   -2422   -13484
  11. 01:33:43.028 -> g:  1199    -3269   -6408

Poniżej przykład wykonujący to samo zadanie z wykorzystaniem komunikacji po SPI. Tutaj biblioteka jest dość dobrze przygotowana i wystarczy zmienić deklarację z podaniem modu pracy jako SPI i zdefiniować pin dla CS:

  1. #include <BMI160Gen.h>
  2.  
  3. const int select_pin = 5;
  4. const int i2c_addr = 0x68;
  5.  
  6. void setup() {
  7.   Serial.begin(115200);
  8.   BMI160.begin(BMI160GenClass::SPI_MODE, select_pin);
  9. }
  10.  
  11. void loop() {
  12.   int gx, gy, gz;
  13.  
  14.   BMI160.readGyro(gx, gy, gz);
  15.  
  16.   Serial.print("g:\t");
  17.   Serial.print(gx);
  18.   Serial.print("\t");
  19.   Serial.print(gy);
  20.   Serial.print("\t");
  21.   Serial.print(gz);
  22.   Serial.println();
  23.  
  24.   delay(500);
  25. }

Podłączenie:

  • 3V3 - 3V3
  • GND - GND
  • CS (SS) - D5
  • MISO (SAO) - D19 
  • MOSI (SDA) - D23
  • SCK (SCL) - D18

W odpowiedzi otrzymane zostaną następujące dane:

  1. 01:10:36.214 -> g:  2865    -2576   2111
  2. 01:10:36.724 -> g:  -92 -22 -56
  3. 01:10:37.204 -> g:  -1988   -10649  12775
  4. 01:10:37.723 -> g:  2604    -19088  4062
  5. 01:10:38.205 -> g:  3755    -16539  -6777
  6. 01:10:38.724 -> g:  -5743   20632   9237
  7. 01:10:39.204 -> g:  -10513  23714   2920
  8. 01:10:39.728 -> g:  -2843   -7475   -6811
  9. 01:10:40.194 -> g:  1084    -17247  -2291
  10. 01:10:40.721 -> g:  13147   -8942   -6255
  11. 01:10:41.208 -> g:  990 15187   4411
  12. 01:10:41.704 -> g:  -5058   5372    12371
  13. 01:10:42.219 -> g:  -330    2668    1128
  14. 01:10:42.704 -> g:  -247    -41 193

STM32:


W tej części chciałbym opisać bibliotekę czujnika przygotowaną i przetestowaną na układzie STM32F4. Biblioteka jest przystosowana do obsługi w interfejsie I2C.

Podłączenie jest następujące:

  • 3V3 - 3V3
  • GND - GND
  • SCL  - PB6
  • SDA - PB7

Do stworzenia biblioteki dla STM32 posłużyłem się wcześniej opisaną biblioteką da Arduino.

Czujnik podłączony jest do interfejsu I2C1. Wygenerowana inicjalizacja wygląda następująco:

  1. static void MX_I2C1_Init(void)
  2. {
  3.   /* USER CODE BEGIN I2C1_Init 0 */
  4.   /* USER CODE END I2C1_Init 0 */
  5.   /* USER CODE BEGIN I2C1_Init 1 */
  6.   /* USER CODE END I2C1_Init 1 */
  7.   hi2c1.Instance = I2C1;
  8.   hi2c1.Init.ClockSpeed = 100000;
  9.   hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  10.   hi2c1.Init.OwnAddress1 = 0;
  11.   hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  12.   hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  13.   hi2c1.Init.OwnAddress2 = 0;
  14.   hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  15.   hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  16.   if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  17.   {
  18.     Error_Handler();
  19.   }
  20.   /* USER CODE BEGIN I2C1_Init 2 */
  21.   /* USER CODE END I2C1_Init 2 */
  22. }
  23.  

Poniżej przedstawię kilka funkcji stosowanych do uruchomienia układu i obsługi. Na samym początku inicjalizacja:

  1. HAL_StatusTypeDef BMI160_Initialize(void)
  2. {
  3.     HAL_StatusTypeDef opStatus = HAL_ERROR;
  4.  
  5.     opStatus = BMI160_PerformSoftReset();
  6.     if(opStatus != HAL_OK) { return opStatus; }
  7.  
  8.     HAL_Delay(2);
  9.  
  10.     if(BMI160_CheckSensorID() == 0) { return HAL_ERROR; }
  11.  
  12.     opStatus = BMI160_PowerUpAccelerometer();
  13.     if(opStatus != HAL_OK) { return opStatus; }
  14.  
  15.     opStatus = BMI160_PowerUpGyroscope();
  16.     if(opStatus != HAL_OK) { return opStatus; }
  17.  
  18.     opStatus = BMI160_SetFullScaleGyroRange(BMI160_GYRO_RANGE_250);
  19.     if(opStatus != HAL_OK) { return opStatus; }
  20.  
  21.     opStatus = BMI160_SetFullScaleAccelRange(BMI160_ACCEL_RANGE_2G);
  22.     if(opStatus != HAL_OK) { return opStatus; }
  23.  
  24.     opStatus = BMI160_WriteRegister(BMI160_RA_INT_MAP_0, 0xFF);
  25.     if(opStatus != HAL_OK) { return opStatus; }
  26.  
  27.     opStatus = BMI160_WriteRegister(BMI160_RA_INT_MAP_1, 0xF0);
  28.     if(opStatus != HAL_OK) { return opStatus; }
  29.  
  30.     opStatus = BMI160_WriteRegister(BMI160_RA_INT_MAP_2, 0x00);
  31.     if(opStatus != HAL_OK) { return opStatus; }
  32.  
  33.     return HAL_OK;
  34. }

Komunikacja z czytnikiem odbywa się za pomocą kilku różnych funkcji:

  1. static HAL_StatusTypeDef BMI160_WriteRegisterBits(uint8_t reg, uint8_t data, unsigned pos, unsigned len)
  2. {
  3.     uint8_t readData = 0;
  4.     HAL_StatusTypeDef opStatus = BMI160_ReadRegister(reg, 1, &readData);
  5.  
  6.     if(opStatus != HAL_OK)
  7.     {
  8.         return opStatus;
  9.     }
  10.  
  11.     uint8_t mask = ((1 << len) - 1) << pos;
  12.  
  13.     data <<= pos;
  14.     data &= mask;
  15.     readData &= ~(mask);
  16.     readData |= data;
  17.  
  18.     return BMI160_WriteRegister(reg, readData);
  19. }
  20.  
  21. static uint8_t BMI160_ReadRegBits(uint8_t reg, unsigned pos, unsigned len)
  22. {
  23.     uint8_t readData = 0;
  24.     HAL_StatusTypeDef opStatus = BMI160_ReadRegister(reg, 1, &readData);
  25.  
  26.     if(opStatus != HAL_OK)
  27.     {
  28.         return 0x00;
  29.     }
  30.  
  31.     uint8_t mask = (1 << len) - 1;
  32.     readData >>= pos;
  33.     readData &= mask;
  34.  
  35.     return readData;
  36. }
  37.  
  38. static HAL_StatusTypeDef BMI160_WriteRegister(uint8_t address, uint8_t cmd)
  39. {
  40.     uint8_t data[2] = {0x00};
  41.     HAL_StatusTypeDef opResoult = HAL_ERROR;
  42.     data[0] = address;
  43.     data[1] = cmd;
  44.  
  45.     opResoult = HAL_I2C_Master_Transmit(&hi2c1, BMI160_I2C_ADDR, data, 2, 10);
  46.  
  47.     return opResoult;
  48. }
  49.  
  50. static HAL_StatusTypeDef BMI160_ReadRegister(uint8_t address, uint8_t dataSize, uint8_t *rec)
  51. {
  52.     HAL_StatusTypeDef opResoult = HAL_ERROR;
  53.     uint8_t data[1] = {0x00};
  54.     data[0] = address;
  55.  
  56.     opResoult = HAL_I2C_Master_Transmit(&hi2c1, BMI160_I2C_ADDR, data, 1, 10);
  57.     opResoult = HAL_ERROR;
  58.     opResoult = HAL_I2C_Master_Receive(&hi2c1, BMI160_I2C_ADDR + 1, &rec[0], dataSize, 10);
  59.  
  60.     return opResoult;
  61. }

W pętli pobieram dane z czujnika za pomocą funkcji ReadGyro oraz GetAcceleration. Pobierają one te same dane co GetMotion6. 

  1. HAL_StatusTypeDef opstat3 = BMI160_ReadGyro(&x, &y, &z);
  2. HAL_Delay(100);
  3. int16_t tempVal = BMI160_GetTemperature();
  4. convertTemp = BMI160_ConvertRawTemp(tempVal);
  5. HAL_Delay(100);
  6. HAL_StatusTypeDef opstat4 = BMI160_GetAcceleration(&x1, &y1, &z1);
  7. HAL_Delay(100);
  8. HAL_StatusTypeDef opstat5= BMI160_GetMotion6(&ax1, &ay1, &az1, &gx1, &gy1, &gz1);
  9. HAL_Delay(300);

Dodatkowo pobieram jeszcze dane z czujnika temperatury. 

Pobieranie danych z czujnika następuje przez odczyt odpowiednich rejestrów;

  1. int16_t BMI160_GetTemperature(void) {
  2.     uint8_t buffer[2] = {0x00};
  3.  
  4.     HAL_StatusTypeDef opStat = BMI160_ReadRegister(BMI160_RA_TEMP_L, 2, &buffer[0]);
  5.  
  6.     if(opStat != HAL_OK) { return HAL_ERROR; }
  7.  
  8.     return (((int16_t)buffer[1]) << 8) | buffer[0];
  9. }
  10.  
  11. float BMI160_ConvertRawTemp(int16_t tempRaw)
  12. {
  13.     float convertTemp = 0;
  14.  
  15.     if(tempRaw & 0x8000) { convertTemp = (23.0F - ((0x10000 - tempRaw)/512.0F)); }
  16.     else { convertTemp = ((tempRaw/512.0F) + 23.0F); }
  17.  
  18.     return convertTemp;
  19. }
  20.  
  21. HAL_StatusTypeDef BMI160Class_GetAcceleration(int16_t* x, int16_t* y, int16_t* z) {
  22.     uint8_t buffer[6] = {0x00};
  23.  
  24.     HAL_StatusTypeDef opStat = BMI160_ReadRegister(BMI160_RA_ACCEL_X_L, 6, &buffer[0]);
  25.  
  26.     *x = (((int16_t)buffer[1]) << 8) | buffer[0];
  27.     *y = (((int16_t)buffer[3]) << 8) | buffer[2];
  28.     *z = (((int16_t)buffer[5]) << 8) | buffer[4];
  29.  
  30.     return opStat;
  31. }
  32.  
  33. HAL_StatusTypeDef BMI160Class_GetMotion6(int16_t* ax, int16_t* ay, int16_t* az, int16_t* gx, int16_t* gy, int16_t* gz) {
  34.     uint8_t buffer[12] = {0x00};
  35.  
  36.     HAL_StatusTypeDef opStat = BMI160_ReadRegister(BMI160_RA_GYRO_X_L, 12, &buffer[0]);
  37.  
  38.     *gx = (((int16_t)buffer[1])  << 8) | buffer[0];
  39.     *gy = (((int16_t)buffer[3])  << 8) | buffer[2];
  40.     *gz = (((int16_t)buffer[5])  << 8) | buffer[4];
  41.     *ax = (((int16_t)buffer[7])  << 8) | buffer[6];
  42.     *ay = (((int16_t)buffer[9])  << 8) | buffer[8];
  43.     *az = (((int16_t)buffer[11]) << 8) | buffer[10];
  44.  
  45.     return opStat;
  46. }

Konwersja danych z żyroskopu jest wykonywana przez następującą funkcję:

  1. float BMI160_ScaledData(int16_t gRaw, BMI160GyroRange gyroRange) {
  2.   float g = 0.0F;
  3.  
  4.   if(gyroRange == BMI160_GYRO_RANGE_2000) {
  5.       g = (gRaw / SENS_2000_DPS_LSB_PER_DPS);
  6.   }
  7.   else if(gyroRange == BMI160_GYRO_RANGE_1000) {
  8.       g = (gRaw / SENS_1000_DPS_LSB_PER_DPS);
  9.   }
  10.   else if(gyroRange == BMI160_GYRO_RANGE_500) {
  11.       g = (gRaw / SENS_500_DPS_LSB_PER_DPS);
  12.   }
  13.   else if(gyroRange == BMI160_GYRO_RANGE_250) {
  14.       g = (gRaw / SENS_250_DPS_LSB_PER_DPS);
  15.   }
  16.   else if(gyroRange == BMI160_GYRO_RANGE_125) {
  17.       g = (gRaw / SENS_125_DPS_LSB_PER_DPS);
  18.   }
  19.  
  20.   return g;
  21. }

W kolejny poście postaram się umieścić wykorzystywanie tego układu poprzez komunikację przez SPI.

Bibliotekę wraz z funkcjami nie wypisanymi w tym poście można pobrać z dysku Google pod tym linkiem.