STM32の下では、Arduinoの下にあるようなさまざまな種類のi2cセンサー用の既製のドライバーが存在しないことを知り、私は非常に驚きました。 私が見つけたものはいくつかのOS(ChubiOS、FreeRTOS、NuttXなど)の一部であり、よりPOSIXに似ていました。 そして、私はHALの下で書きたいと思いました:(
Arduinoコミュニティはi2cdevlibライブラリを使用して、センサードライバーを記述するときに鉄から抽象化します。 実際、私は自分の仕事を共有しています-STM32 HALのi2cdevlibポート (すでにプルリクエストを送信しました)、そして猫の下で、私は途中で集めた小石について話します。 さて、コード例があります。
私たちは何に取り組んでいますか
私の手には、開発ボードstm32f429i-disco、gy-87センサー、arduino uno、開発環境EmBitz 0.40(ex Em :: Blocks)およびArduinoを搭載したボードがあります。
Arduinkaを使用して、レジスタ値の読み取り結果を比較しました。 最初のポートセンサーはBMP085 / BMP180です。 センサーの存在とそのドライバー内の少量のコードのために選択されました。
手続き
- コードをC ++からCに書き換えます。ライブラリおよびドライバー用
- i2cdevlibで、arduino関連のコードを破棄する方法に沿って、関数をi2cからHAL'ovskieに書き換えます
- テスト結果、デバッグ
コードを書き直す
手始めに、C ++からCに書き換えます。いいえ、手始めに-理由を説明します:)
組み込みの世界では、純粋なCがはるかに一般的に使用されています。 人気のある開発環境(EmBlocks、Keil)はCプロジェクトを作成し、STM32CubeMXが生成するコードも大きくなります。 また、プロジェクト全体をC ++に変換するよりも、C ++プロジェクトでsybnaya libを使用する方が簡単です。
行こう 関数の名前を変更します。たとえば、 I2Cdev :: readByteはI2Cdev_readByteになりました 。 また、存在しないクラス内のすべての関数呼び出しにこのようなプレフィックスを追加することを忘れないでください( readByte- > I2Cdev_readByte )。 ルーチン、特別なことは何もありません。
並行して、ライブラリのアーキテクチャを理解しています-ハードウェアで動作する関数は4つしかありません。
uint8_t I2Cdev_readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout); uint8_t I2Cdev_readWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t *data, uint16_t timeout); uint16_t I2Cdev_writeBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t* data); uint16_t I2Cdev_writeWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t* data);
BMP085ドライバーを使用して同様の手順を実行します。 欠落しているインクルージョン(math.h、stdint.h、stdlib.h、string.h)をパスに沿って追加し、bool型を宣言します。 これはC、ベイビーです)bool-> uint8_tで関数を書き換えるだけの価値があるかもしれません...
また、I2CDevでは、通信に使用する初期化されたi2cを持つ構造へのリンクを追加する必要があります。
#include "stm32f4xx_hal.h" I2C_HandleTypeDef * I2Cdev_hi2c;
HALで機能を実装する
行の最初はI2Cdev_readBytesです。 以下に、元のリストを示します。デバッグ用の断片や、さまざまなライブラリ/バージョンの実装はありません
/** Read multiple bytes from an 8-bit device register. * @param devAddr I2C slave device address * @param regAddr First register regAddr to read from * @param length Number of bytes to read * @param data Buffer to store read data in * @param timeout Optional read timeout in milliseconds (0 to disable, leave off to use default class value in I2Cdev::readTimeout) * @return Number of bytes read (-1 indicates failure) */ int8_t I2Cdev::readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout) { int8_t count = 0; uint32_t t1 = millis(); // Arduino v1.0.1+, Wire library // Adds official support for repeated start condition, yay! // I2C/TWI subsystem uses internal buffer that breaks with large data requests // so if user requests more than BUFFER_LENGTH bytes, we have to do it in // smaller chunks instead of all at once for (uint8_t k = 0; k < length; k += min(length, BUFFER_LENGTH)) { Wire.beginTransmission(devAddr); Wire.write(regAddr); Wire.endTransmission(); Wire.beginTransmission(devAddr); Wire.requestFrom(devAddr, (uint8_t)min(length - k, BUFFER_LENGTH)); for (; Wire.available() && (timeout == 0 || millis() - t1 < timeout); count++) { data[count] = Wire.read(); } } // check for timeout if (timeout > 0 && millis() - t1 >= timeout && count < length) count = -1; // timeout return count; }
length> BUFFER_LENGTHの場合、初期レジスタを新しい方法で指定するため、この松葉杖がループでどのように機能するかはよくわかりません。 私はコードを推測する
Wire.beginTransmission(devAddr); Wire.write(regAddr); Wire.endTransmission(); Wire.beginTransmission(devAddr);
ループの前にある必要があります。 いずれにせよ、意味は明確であり、HALで次のように記述します。
uint8_t I2Cdev_readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout) { uint16_t tout = timeout > 0 ? timeout : I2CDEV_DEFAULT_READ_TIMEOUT; HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, ®Addr, 1, tout); if (HAL_I2C_Master_Receive(I2Cdev_hi2c, devAddr << 1, data, length, tout) == HAL_OK) return length; else return -1; }
アドレスシフトに注意してください-devAddr << 1.ドライバーでライブラリのテストに切り替えたとき、最初にしたことは、モジュールとバススキャナーの接続を確認することでした。
uint8_t i = 0; for(i = 0; i<255; i++) { if(HAL_I2C_IsDeviceReady(&hi2c3, i, 10, 100) == HAL_OK) printf("Ready: 0x%02x", i); }
気づいたように、仕様で許可されている112のアドレスだけでなく、0〜255の値をすべて意図的に取りました。 これにより、エラーを特定できました。回線上の各デバイスは、アドレスではなく、同時に2回応答しました。
Wire.begin()は7ビットアドレスを使用し、HALは8ビット表現を使用します。 少しの反射と修正の後、正常なスキャナーコードを取得します。
uint8_t i = 0; for(i = 15; i<127; i++) { if(HAL_I2C_IsDeviceReady(&hi2c3, i << 1, 10, 100) == HAL_OK) printf("Ready: 0x%02x", i); }
結論-HAL_I2C _ ***関数を呼び出す前に、デバイスアドレス自体を少し左にシフトする必要があります
さらにi2cdevlibに戻ります。 次の行はI2Cdev_readWordsです。
uint8_t I2Cdev_readWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t *data, uint16_t timeout) { uint16_t tout = timeout > 0 ? timeout : I2CDEV_DEFAULT_READ_TIMEOUT; HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, ®Addr, 1, tout); if (HAL_I2C_Master_Receive(I2Cdev_hi2c, devAddr << 1, (uint8_t *)data, length*2, tout) == HAL_OK) return length; else return -1; }
オリジナルでは、手動で読み取り、MSBとLSBを順番にバッファに書き込みます。
私は嘘をついていません
for (uint8_t k = 0; k < length * 2; k += min(length * 2, BUFFER_LENGTH)) { Wire.beginTransmission(devAddr); Wire.write(regAddr); Wire.endTransmission(); Wire.beginTransmission(devAddr); Wire.requestFrom(devAddr, (uint8_t)(length * 2)); // length=words, this wants bytes bool msb = true; // starts with MSB, then LSB for (; Wire.available() && count < length && (timeout == 0 || millis() - t1 < timeout);) { if (msb) { // first byte is bits 15-8 (MSb=15) data[count] = Wire.read() << 8; } else { // second byte is bits 7-0 (LSb=0) data[count] |= Wire.read(); #ifdef I2CDEV_SERIAL_DEBUG Serial.print(data[count], HEX); if (count + 1 < length) Serial.print(" "); #endif count++; } msb = !msb; } Wire.endTransmission(); }
データ記録の機能に渡します。 ここで、動的配列の少しの作業を待っています。 実際には、記録を開始するためのレジスタアドレスと書き込み用のデータは、1つのSTARTトランザクション-STOPビットである必要があります。 そして、それらは個別に関数に転送されます。 Wireライブラリのarduinoの場合、これは問題ではありません。なぜなら、プログラマは自分自身で開始/終了を書き込み、それらの間でデータを送信するからです。 これらすべてを1つのバッファーに入れて転送する必要があります。 ループ内の単純なコピーよりも効率的なmallocとmemcpyを使用します。
UPD 07/13/2016 : mallocとmemcpyで踊る代わりに、 HAL_I2C_Mem_Write関数が使用され 、デバイスアドレス、レジスタアドレス、およびそこに書き込む必要があるデータを受け入れます。 これが差分コミットです
/** Write multiple bytes to an 8-bit device register. * @param devAddr I2C slave device address * @param regAddr First register address to write to * @param length Number of bytes to write * @param data Buffer to copy new data from * @return Status of operation (true = success) */ uint16_t I2Cdev_writeBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data) { // Creating dynamic array to store regAddr + data in one buffer uint8_t * dynBuffer; dynBuffer = (uint8_t *) malloc(sizeof(uint8_t) * (length+1)); dynBuffer[0] = regAddr; // copy array memcpy(dynBuffer+1, data, sizeof(uint8_t) * length); HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, dynBuffer, length+1, 1000); free(dynBuffer); return status == HAL_OK; }
同様に、 I2Cdev_writeWordsの場合、メモリのみがuint16_t + uint8_t regAddrごとに1バイトに割り当てられます。 HALはuint8_tへのポインターですが、配列の長さは正しく指定されています:)
/** Write multiple words to a 16-bit device register. * @param devAddr I2C slave device address * @param regAddr First register address to write to * @param length Number of words to write * @param data Buffer to copy new data from * @return Status of operation (true = success) */ uint16_t I2Cdev_writeWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t* data) { // Creating dynamic array to store regAddr + data in one buffer uint8_t * dynBuffer; dynBuffer = (uint8_t *) malloc(sizeof(uint8_t) + sizeof(uint16_t) * length); dynBuffer[0] = regAddr; // copy array memcpy(dynBuffer+1, data, sizeof(uint16_t) * length); HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(I2Cdev_hi2c, devAddr << 1, dynBuffer, sizeof(uint8_t) + sizeof(uint16_t) * length, 1000); free(dynBuffer); return status == HAL_OK; }
テスト結果、デバッグ
テストでは、i2cを初期化し、I2Cdev_hi2cの構造にポインターを割り当ててから、ドライバー関数を操作してセンサーからデータを受信する必要があります。 以下は、プログラムの実際のリストとその作業の結果です。
BMP180の例
#include "stm32f4xx.h" #include "stm32f4xx_hal.h" #include <stdint.h> #include <stdio.h> #include <string.h> #include "I2Cdev.h" #include "BMP085.h" I2C_HandleTypeDef hi2c3; int main(void) { SystemInit(); HAL_Init(); GPIO_InitTypeDef GPIO_InitStruct; /**I2C3 GPIO Configuration PC9 ------> I2C3_SDA PA8 ------> I2C3_SCL */ __GPIOA_CLK_ENABLE(); __GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = GPIO_AF4_I2C3; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = GPIO_AF4_I2C3; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); __I2C3_CLK_ENABLE(); hi2c3.Instance = I2C3; hi2c3.Init.ClockSpeed = 400000; hi2c3.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c3.Init.OwnAddress1 = 0x10; hi2c3.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c3.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c3.Init.OwnAddress2 = 0x11; hi2c3.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c3.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c3); I2Cdev_hi2c = &hi2c3; // init of i2cdevlib. // You can select other i2c device anytime and // call the same driver functions on other sensors while(!BMP085_testConnection()) ; BMP085_initialize(); while (1) { BMP085_setControl(BMP085_MODE_TEMPERATURE); HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_TEMPERATURE)); float t = BMP085_getTemperatureC(); BMP085_setControl(BMP085_MODE_PRESSURE_3); HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_PRESSURE_3)); float p = BMP085_getPressure(); float a = BMP085_getAltitude(p, 101325); printf("T: %3.1f P: %3.0f A: %3.2f", t, p ,a); HAL_Delay(1000); } } void SysTick_Handler() { HAL_IncTick(); HAL_SYSTICK_IRQHandler(); }
温度をCで、圧力をパスカルで、高度をメートルで表示します
結果
ライブラリは移植されており、BMP085 / BMP180およびMPU6050用の2つのドライバーも使用できます。 写真で後者の作業を示し、コード例を示します。
写真
コード例
#include "stm32f4xx.h" #include "stm32f4xx_hal.h" #include <stdint.h> #include <stdio.h> #include <string.h> #include "I2Cdev.h" #include "BMP085.h" #include "MPU6050.h" I2C_HandleTypeDef hi2c3; int main(void) { SystemInit(); HAL_Init(); GPIO_InitTypeDef GPIO_InitStruct; /**I2C3 GPIO Configuration PC9 ------> I2C3_SDA PA8 ------> I2C3_SCL */ __GPIOA_CLK_ENABLE(); __GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = GPIO_AF4_I2C3; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = GPIO_AF4_I2C3; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); __I2C3_CLK_ENABLE(); hi2c3.Instance = I2C3; hi2c3.Init.ClockSpeed = 400000; hi2c3.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c3.Init.OwnAddress1 = 0x10; hi2c3.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c3.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c3.Init.OwnAddress2 = 0x11; hi2c3.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c3.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c3); I2Cdev_hi2c = &hi2c3; // init of i2cdevlib. // You can select other i2c device anytime and // call the same driver functions on other sensors while(!BMP085_testConnection()) ; int16_t ax, ay, az; int16_t gx, gy, gz; int16_t c_ax, c_ay, c_az; int16_t c_gx, c_gy, c_gz; MPU6050_initialize(); BMP085_initialize(); MPU6050_setFullScaleGyroRange(MPU6050_GYRO_FS_250); MPU6050_setFullScaleAccelRange(MPU6050_ACCEL_FS_2); MPU6050_getMotion6(&c_ax, &c_ay, &c_az, &c_gx, &c_gy, &c_gz); while (1) { BMP085_setControl(BMP085_MODE_TEMPERATURE); HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_TEMPERATURE)); float t = BMP085_getTemperatureC(); BMP085_setControl(BMP085_MODE_PRESSURE_3); HAL_Delay(BMP085_getMeasureDelayMilliseconds(BMP085_MODE_PRESSURE_3)); float p = BMP085_getPressure(); float a = BMP085_getAltitude(p, 101325); printf(buf, "T: %3.1f P: %3.0f A: %3.2f", t, p ,a); MPU6050_getMotion6(&ax, &ay, &az, &gx, &gy, &gz); printf("Accel: %d %d %d", ax - c_ax, ay - c_ay, az - c_az); printf("Gyro: %d %d %d", gx - c_gx, gy - c_gy, gz - c_gz); HAL_Delay(1000); } } void SysTick_Handler() { HAL_IncTick(); HAL_SYSTICK_IRQHandler(); }
センサーデータは、同じセンサーに接続されたarduino unoを介して受信したデータで確認されました。
近い将来、手持ちの他のセンサー(ADXL345およびHMC5883L)のドライバーを追加します。 残りは、おそらく、必要に応じて独立して移植することは難しくありません。 どちらかといえば-書き込み、私は助けます:)
私の仕事が誰かの時間を節約し、そして/またはArduinokからSTM32への移行を促進することを願っています。
ご関心をお寄せいただきありがとうございます!
UPD 07/13/2016 : HAL_I2C_Mem_Writeを使用して、 mallocとmemcpyを削除しました 。 元のコードは、コメント内の議論の論理が理解できるように残されました。 繰り返しますが、ここに変更点があります
読むべき資料:
I2c仕様
ドライバーおよびその他のユーティリティを備えたI2cdevlibライブラリサイト