STM32 + LLでI2C通信を使用してICM20602と通信してみる
STM32マイコンのペリフェラル関連記事を一覧にまとめました。
こんにちは。以前LLライブラリを使用してI2Cに挑戦しましたが全然動かないため諦めました。先日、いろいろとあってI2Cの通信について少し勉強したので今ならいける!という謎の自信をもって再挑戦しました。実装方法を書いていきたいと思います。
今回は、STM32F303K8+ICM20602で通信を行いました。ただし、データシートはF3,F4を両方見ながら行ったこと等あるためF4でも多分動くと思います。F4では環境がなかったため確認をすることができていません。この記事では基本的にSTM32F3のリファレンスマニュアルを読み解いていきたいと思います。
データシートを見る限りでは送信、受信のフラグ確認まちループの確認用フラグを一部追加(変更)することでなんとかなりそうです。
データシートを確認する
マイコン編
今回はマイコンがマスタになるためマスタトランスミッタとレシーブの確認をしていきます
I2C_CR2レジスタにスレーブデバイスのアドレス(左詰め7bit)、書き込みか読み込みか、モード設定等をしてスタートをすればよし!ということがわかりました。
また、今回は割り込み処理を使用しないため監視すべきフラグはTXEフラグとRENEフラグとSTOPフラグ、BSYフラグだけでよさそうということが読み取れました。
また、I2CのデバイスアドレスにおけるR/Wバイトも設定してくれるみたいです。したがって、スレーブアドレスは左1bitシフトしていれてあげるだけでOKということが読み取れました。
また、ACKやNACKはハードウェアが自動で処理をしてくれるそうです。I2Cの面倒なところはハードウェアがやってくれるようなので楽ですね!
今回のデータシートをみるにあたって感じたことですが、STM32F4のI2CとSTM32F3のI2Cのドキュメントにわかりやすさの差が天と地ほどあるのに驚きました(是非見比べてみていただきたいです)。1
デバイス編
ICM20602のI2C設定を確認してみます。
I2Cの設定に必要なところのデータシートの部分を一部引用させていただきました。
マイコンの設定に必要なのは、通信速度、立ち上がり・立下り時間、通信bit数などです。必要に応じて確認をしましょう。
続いてデバイスID,SPI通信で使用するCSピンをどのように処理すればいいのかの確認をしましょう。下のデータシートよりCS=0のときにSPIモードになると表に書いてあるためCS = High, SD0はスレーブアドレスの一番下のビットを1/0に変化させるビットだということがわかりました。今回はSD0はLow(GND接続)にしてしまおうと思います。したがって、スレーブアドレスは(0x68)<<1より0xD0であるということが読み取れます。
STM32CubeMXの設定
今回は、I2Cの設定と同時にTIM割り込みの設定、USARTの設定をしました。センサの表示は10msecに一回割り込みで行っていきます。
ここまで設定をしたらプロジェクト名を適当に設定して、各自使い慣れたIDEやgccに対応するプロジェクトを作成してもらいましょう。
実装
ソースコードを以下に示します。
#ifndef __ICM20602_H
#define __ICM20602_H
#include <stdint.h>
void IRQ_Start(void);
void IRQ_Processing(void);
void I2C1_Start(void);
void I2C1_TransmitData(uint8_t device_id ,uint8_t *pdata, uint8_t size);
void I2C1_ReceiveData(uint8_t device_id , uint8_t *pdata, uint8_t size);
void I2C1_WriteData(uint8_t reg, uint8_t val);
uint8_t I2C1_ReadRegByte(uint8_t reg);
void ICM20602_Init(void);
float ICM20602_readGyroZ(void);
void ICM20602_UpdateAllSensor(void);
void convertParameter(int16_t *pre_convert, float *convert,float factor, uint8_t size);
#endif /* __ICM20602_H */
#include "icm20602.h"
#include "i2c.h"
// timの割り込みでセンサーを更新する
#include "tim.h"
#include <stdio.h>
#define ICM20602_DEVICEID 0xD0
#define ICM20602_WHOAMI 0x75
#define ICM20602_CONFIG 0x1A
#define ICM20602_GYRO_CONFIG 0x1B
#define ICM20602_ACCEL_CONFIG 0x1C
#define ICM20602_ACCEL_CONFIG2 0x1D
#define ICM20602_PWR_MGMT_1 0x6B
#define ICM20602_PWR_MGMT_2 0x6C
#define ICM20602_ACCEL_XOUT_H 0x3B
#define ICM20602_ACCEL_XOUT_L 0x3C
#define ICM20602_ACCEL_YOUT_H 0x3D
#define ICM20602_ACCEL_YOUT_L 0x3E
#define ICM20602_ACCEL_ZOUT_H 0x3F
#define ICM20602_ACCEL_ZOUT_L 0x40
#define ICM20602_TEMP_OUT_H 0x41
#define ICM20602_TEMP_OUT_L 0x42
#define ICM20602_GYRO_XOUT_H 0x43
#define ICM20602_GYRO_XOUT_L 0x44
#define ICM20602_GYRO_YOUT_H 0x45
#define ICM20602_GYRO_YOUT_L 0x46
#define ICM20602_GYRO_ZOUT_H 0x47
#define ICM20602_GYRO_ZOUT_L 0x48
#define GYRO_FACTOR 16.4f
#define ACCEL_FACTOR 2048.0f
#define TEMP_FACTOR 326.8f
#define TEMP_OFFSET 25.0f
void IRQ_Start(void)
{
LL_TIM_EnableIT_UPDATE(TIM2);
LL_TIM_EnableCounter(TIM2);
}
/**
* このプログラムの場合は
* stm32fxxx_it.cで呼ぶこと
* void TIM2_IRQHandler(void)
* 内のuserBegin内にこの関数を呼ぶこと。
*/
void IRQ_Processing(void)
{
if ( LL_TIM_IsActiveFlag_UPDATE(TIM2) == 1 ){
LL_TIM_ClearFlag_UPDATE(TIM2);
ICM20602_UpdateAllSensor();
}
}
void I2C1_Start(void)
{
LL_I2C_Enable(I2C1);
}
void I2C1_TransmitData(uint8_t device_id ,uint8_t *pdata, uint8_t size)
{
// busyフラグをチェック
while(LL_I2C_IsActiveFlag_BUSY(I2C1) == SET);
LL_I2C_HandleTransfer(I2C1, device_id, LL_I2C_ADDRSLAVE_7BIT, size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
for(uint8_t i = 0; i < size; i++){
while(LL_I2C_IsActiveFlag_TXE(I2C1)== RESET);
LL_I2C_TransmitData8(I2C1, *pdata++);
}
while(LL_I2C_IsActiveFlag_STOP(I2C1)==RESET);
LL_I2C_ClearFlag_STOP(I2C1);
// 設定をリセットする
(I2C1->CR2 &= (uint32_t)~((uint32_t)(I2C_CR2_SADD | I2C_CR2_HEAD10R | I2C_CR2_NBYTES | I2C_CR2_RELOAD | I2C_CR2_RD_WRN)));
}
void I2C1_ReceiveData(uint8_t device_id , uint8_t *pdata, uint8_t size)
{
// busyフラグをチェック
while(LL_I2C_IsActiveFlag_BUSY(I2C1) == SET);
// 初期設定
LL_I2C_HandleTransfer(I2C1, device_id, LL_I2C_ADDRSLAVE_7BIT, size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_READ);
for(uint8_t i = 0; i < size; i++){
while(LL_I2C_IsActiveFlag_RXNE(I2C1) == RESET);
*pdata++ = LL_I2C_ReceiveData8(I2C1);
}
while(LL_I2C_IsActiveFlag_STOP(I2C1)==RESET);
LL_I2C_ClearFlag_STOP(I2C1);
// 設定をリセットする
(I2C1->CR2 &= (uint32_t)~((uint32_t)(I2C_CR2_SADD | I2C_CR2_HEAD10R | I2C_CR2_NBYTES | I2C_CR2_RELOAD | I2C_CR2_RD_WRN)));
}
void I2C1_WriteData(uint8_t reg, uint8_t val)
{
uint8_t tx_buffer[2];
tx_buffer[0] = reg;
tx_buffer[1] = val;
I2C1_TransmitData(ICM20602_DEVICEID, tx_buffer, 2);
}
uint8_t I2C1_ReadRegByte(uint8_t reg)
{
uint8_t val = 0;
I2C1_TransmitData(ICM20602_DEVICEID, ®, 1);
I2C1_ReceiveData(ICM20602_DEVICEID ,&val, 1);
return val;
}
/**
* mainのwhileループの前に実行すること。
* これを実行した後は割り込み処理が有効になり、
* printfを実行するため他の処理を動かすのは厳しい可能性あり。
*/
void ICM20602_Init(void)
{
I2C1_Start();
LL_mDelay(10);
uint8_t check_val = 0;
check_val = I2C1_ReadRegByte(ICM20602_WHOAMI);
if(check_val != 0x12){
printf("rnre check who am irn");
LL_mDelay(10);
check_val = I2C1_ReadRegByte(ICM20602_WHOAMI);
if(check_val != 0x12){
while(1) printf("gyro connection errorr");
}
}
LL_mDelay(10);
// PWR_MGMT1 clear sleep
I2C1_WriteData(ICM20602_PWR_MGMT_1, 0x00);
LL_mDelay(10);
// PWR_MGMT2 enable sensor
I2C1_WriteData(ICM20602_PWR_MGMT_1, 0x00);
LL_mDelay(10);
// Config setting
I2C1_WriteData(ICM20602_CONFIG, 0x01);
LL_mDelay(10);
// Gyro Config Setting 2000 deg/sec
I2C1_WriteData(ICM20602_GYRO_CONFIG, 0x18);
LL_mDelay(10);
// Accel Config Setting 16g
I2C1_WriteData(ICM20602_ACCEL_CONFIG, 0x18);
LL_mDelay(10);
IRQ_Start();
}
float ICM20602_readGyroZ(void)
{
int16_t gyro = 0;
float omega = 0.0f;
gyro = (int16_t)( ((int16_t)I2C1_ReadRegByte(ICM20602_GYRO_ZOUT_H) << 8) | ((int16_t)I2C1_ReadRegByte(ICM20602_GYRO_ZOUT_L)) );
omega = gyro / GYRO_FACTOR;
return omega;
}
void ICM20602_UpdateAllSensor(void)
{
uint8_t reg = ICM20602_ACCEL_XOUT_H;
uint8_t receive_data[14];
int16_t accel_data[3];
//int16_t temp_data;
int16_t gyro_data[3];
float accel[3];
float gyro[3];
I2C1_TransmitData(ICM20602_DEVICEID, ®, 1);
I2C1_ReceiveData(ICM20602_DEVICEID , receive_data, 14);
accel_data[0] = (int16_t)( ((int16_t)receive_data[0] << 8) | ((int16_t)receive_data[1]) );
accel_data[1] = (int16_t)( ((int16_t)receive_data[2] << 8) | ((int16_t)receive_data[3]) );
accel_data[2] = (int16_t)( ((int16_t)receive_data[4] << 8) | ((int16_t)receive_data[5]) );
//temp_data = (int16_t)( ((int16_t)receive_data[6] << 8) | ((int16_t)receive_data[7]) );
gyro_data[0] = (int16_t)( ((int16_t)receive_data[8] << 8) | ((int16_t)receive_data[9]) );
gyro_data[1] = (int16_t)( ((int16_t)receive_data[10] << 8) | ((int16_t)receive_data[11]) );
gyro_data[2] = (int16_t)( ((int16_t)receive_data[12] << 8) | ((int16_t)receive_data[13]) );
convertParameter(accel_data, accel, ACCEL_FACTOR/9.8f, 3);
convertParameter(gyro_data, gyro, GYRO_FACTOR, 3);
printf("%f, %f, %f, %f, %f, %frn", accel[0], accel[1], accel[2], gyro[0], gyro[1], gyro[2]);
}
void convertParameter(int16_t *pre_convert, float *convert,float factor, uint8_t size)
{
for(uint8_t i = 0; i < size; i++){
convert[i] = (float)( pre_convert[i] / factor);
}
}
こんなかんじです。LL_I2C_HandleTransfer関数でなんとかなりました!関数内で行っている動作は設定をリセットするでリセットしている点をセットしているだけです。レジスタ直打ちのほうがわかりやすいかもしれないです。
printf,TIM割り込みの設定等に関しては下記の記事を参考にしていただければ幸いです。
ICM20602等のデバイスのデータシートの読み方等(概要)が書いてある記事。
さいごに
LLライブラリを使用したI2Cの通信はいろいろとはまりました。また、I2C通信の際にプルアップ抵抗をつけますがプルアップ抵抗の値をしっかりと気にすることの大切さもオシロスコープで確認することで学ぶことができました。デバッグするときにオシロスコープはとても有用ですね。
STM32マイコンのLLAPIを使用して周辺機能でまだいじっていないCAN通信やDAC周りを次はいじってみたいと考えています。
最後まで読んでいただきありがとうございます。何か問題等ございましたらご指摘していただけると幸いです。
参考文献
RM0316 (STM32F3 リファレンスマニュアル)