最近学んだ STM32 の I2C について、今日は学習ノートを書いて記録します。
(一)I2C とは?#
I2C(Inter IC Bus)は、Philips 社が開発した汎用データバスです。
通信線は 2 本あり:SCL(Serial Clock)、SDA(Serial Data)で、同期、半二重、データ応答を伴い、バスに複数のデバイスを接続することをサポートします(一主多従、多主多従)。
(二)I2C のハードウェア回路#
(画像がまた出せませんでしたので、口頭で説明します)
I2C には SCL と SDA の 2 本の通信線があり、SCL はクロック線、SDA はデータ線です。
マスターとスレーブはこの 2 本の線で接続されており、どちらもオープンドレイン出力の弱プルアップモードに設定されています。これは、デバイスが信号を送信していないときは、SCL と SDA が高電平に設定され、デバイスが信号を送信するときは、能動的に低電平に引き下げ、放すと自動的に高電平に戻ることを意味します。
(三)I2C のタイミング#
I2C のタイミングの基本単位には、開始、終了、1 バイトの送信、1 バイトの受信が含まれます。
開始:SDA を低に引き下げ、その後 SCL を低に引き下げます。
終了:SCL を高電平に解放し、その後 SDA を高電平に解放します。
1 バイトの送信:SDA を低に引き下げ / 解放し、このとき SCL を解放し、スレーブは SCL の高電平でデータを読み取ります。
1 バイトの受信:SCL を高電平に解放し、マスターはデータを読み取ります。
I2C では主に 2 つのタイミングを学びます。一つは指定位置への書き込み、もう一つは指定位置からの読み取りです。
指定位置への書き込み:開始 -> スレーブアドレス + 読み書きビット(書き込み)を送信 -> 応答を受信 -> レジスタアドレスを送信 -> 応答を受信 -> データを送信 -> 応答を受信 -> 終了
指定位置からの読み取り:開始 -> スレーブアドレス + 読み書きビット(書き込み)を送信 -> 応答を受信 -> レジスタアドレスを送信 -> 応答を受信 -> 開始(再開始)-> スレーブアドレス + 読み書きビット(読み取り)を送信 -> 応答を受信 -> レジスタアドレスを送信 -> 応答を受信 -> データを受信 -> 応答を送信 -> 終了
(四)ソフトウェア I2C#
タイミングに基づいて、ソフトウェアで I2C を設定するコードを書くことができます:
void MyI2C_W_SCL(uint8_t Bitvalue)//SCLを高/低電平に設定
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)Bitvalue);
Delay_us(10);
}
void MyI2C_W_SDA(uint8_t Bitvalue)//SDAを高/低電平に設定
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)Bitvalue);
Delay_us(10);
}
uint8_t MyI2C_R_SDA()//SDAのデータを読み取る
{
uint8_t Bitvalue;
Bitvalue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return Bitvalue;
}
void MyI2C_Init()//I2Cを初期化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
}
void MyI2C_Start()//開始タイミング
{
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
void MyI2C_Stop()//終了タイミング
{
MyI2C_W_SCL(0);
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
void MyI2C_SendByte(uint8_t Byte)//1バイトを送信
{
for(int i=0;i<8;i++)
{
MyI2C_W_SCL(0);
MyI2C_W_SDA(!!(Byte & (0x80 >> i)));
MyI2C_W_SCL(1);
}
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveByte()//1バイトを受信
{
uint8_t Byte = 0;
MyI2C_W_SCL(0);
MyI2C_W_SDA(1);
for(int i=0;i<8;i++)
{
MyI2C_W_SCL(1);
if(MyI2C_R_SDA())Byte|=(0x80>>i);
MyI2C_W_SCL(0);
}
return Byte;
}
void MyI2C_SendAck(uint8_t Ackbit)//応答を送信
{
MyI2C_W_SCL(0);
MyI2C_W_SDA(Ackbit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveAck()//応答を受信
{
uint8_t Ackbit = 0;
MyI2C_W_SCL(0);
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
Ackbit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return Ackbit;
}
これらの基本タイミングユニットのコードを使って、完全なタイミングを組み立てることができます:
void MPU6050_WriteReg(uint8_t regAddress,uint8_t Data)//指定位置への書き込み
{
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);
MyI2C_ReceiveAck();
MyI2C_SendByte(regAddress);
MyI2C_ReceiveAck();
MyI2C_SendByte(Data);
MyI2C_ReceiveAck();
MyI2C_Stop();
}
uint8_t MPU6050_ReadReg(uint8_t regAddress)//指定位置からの読み取り
{
uint8_t Data;
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);
MyI2C_ReceiveAck();
MyI2C_SendByte(regAddress);
MyI2C_ReceiveAck();
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS|1);
MyI2C_ReceiveAck();
Data=MyI2C_ReceiveByte();
MyI2C_SendAck(1);
MyI2C_Stop();
return Data;
}
これらの 2 つのタイミングを利用することで、私たちの期待する目的を達成できます。
(五)ハードウェア I2C#
STM32 には内蔵の I2C 周辺機器がありますが、固定のピンのみで使用されます。以下は設定コードです:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed=50000;
I2C_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;
I2C_InitStructure.I2C_Mode=I2C_Mode_I2C;
I2C_InitStructure.I2C_OwnAddress1=0x00;
I2C_Init(I2C2,&I2C_InitStructure);
I2C_Cmd(I2C2,ENABLE);
//初期化
void MPU6050_Wait(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)//フラグをチェックし、タイムアウト割り込みを設定
{
for(int time = 10000 ; time > 0 ; time--)
{
if(I2C_CheckEvent(I2Cx,I2C_EVENT) == SUCCESS)
return;
}
}
void MPU6050_WriteReg(uint8_t regAddress,uint8_t Data)
{
I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
I2C_SendData(I2C2,regAddress);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);
I2C_SendData(I2C2,Data);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);
I2C_GenerateSTOP(I2C2,ENABLE);
}
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
I2C_SendData(I2C2,regAddress);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);
I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
I2C_AcknowledgeConfig(I2C2,DISABLE);
I2C_GenerateSTOP(I2C2,ENABLE);
MPU6050_Wait(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE);
return Data;
}
これが I2C の使用に関するすべての内容です。ここで MPU6050 モジュールについても触れておきます。これはジャイロセンサーで、I2C 線で接続し、指定位置への書き込みで機能を設定し、指定位置からの読み取りで必要なデータを取得します(前のコードのいくつかの命名は MPU6050 のままですが、江協が I2C を使用して MPU6050 を使うように教えたからです)。
とにかく I2C 部分はこれで終了です。次は ISP です。振り返るとこのレイアウトは本当に笑ってしまいますが、修正するのは面倒です。本来この数編を書く目的は復習することだったので、私の表現も不適切です。いつか学び終えたら、しっかりと復習してから戻って修正します。(悲)