1. 假設
本文假設讀者:
- 有使用stm32的經驗
- 有使用stm32庫函數編程的經驗
- 瞭解IIC通訊協議
本文適合初學者參考
2. stm32f4xx系列的芯片的IIC接口
1. 模式選擇
stm32f4xx的IIC模塊有四種工作模式,默認的工作模式是從模式,在發送起始位會自動由從模式切換爲主模式.
相關概念如下:
- 發送器:發送數據到總線的器件
- 接收器:從總線接收數據的器件
- 主機:發起/停止數據傳輸、提供時鐘信號的器件
- 從機:被主機尋址的器件
- 多主機:可以有多個主機試圖去控制總線,但是不會破壞數據
- 仲裁:當多個主機試圖去控制總線時,通過仲裁可以使得只有一個主機獲得總線控制權,並且它傳輸的信息不會被破壞
- 同步:多個器件同步時鐘信號的過程
2. 通訊過程
儘管已經假設讀者瞭解iic總線協議,但還是對iic通訊的一些概念做簡單介紹.
- 開始信號(S):SCL爲高電平時,SDA由高電平向低電平跳變,表示起始信號,開始傳送數據
- 結束信號(P):SCL爲高電平時,SDA由低電平向高電平跳變,表示結束信號,結束傳送數據
- 響應信號(ACK):接收器在接收到8位數據後,在第9個時鐘週期,拉低SDA電平。即接收數據的IC在接收到8bit數據後,向發送數據的IC發出特定的低電平脈衝,表示已收到數據。
I2C總線進行數據傳送時,
4.1 SCL高電平期間,數據穩定(不允許變化),此時用於接收器讀數據;
4.2 SCL低電平期間,數據允許變化,此時用於發送器發(寫)數據;stm32f4xx的IIC模塊的通訊時序如下:
5.1 模塊默認工作在從模式, 發送起始位後自動由從模式切換爲主模式. 在主模式下,I2C 接口會啓動數據傳輸並生成時鐘信號。串行數據傳輸始終是在出現起始位時開始,在出現停止位時結束。起始位和停止位均在主模式下由軟件生成。
5.2 IIC模塊的應答位可以由軟件使能或者禁止. 可以軟件選擇IIC模塊的尋址方式( 7 位/10 位雙尋址模式和/或廣播呼叫地址)
5.3 IIC模塊作爲主發送器發送數據的過程如下(更加詳細的說明請參考《stm32f4xx中文參考手冊》654頁,這裏不再贅述)
5.4 IIC模塊作爲主接收器接收數據的過程如下(更加詳細的說明請參考《stm32f4xx中文參考手冊》655頁,這裏不再贅述)
3.“BUSY位總是置1”的問題
1. 問題描述
在STM32F4xx的IIC模塊的狀態寄存器SR2有一個BUSY位用於指示IIC總線的佔用狀態。
然而在實際的應用中,使能了IIC模塊的時鐘之後,BUSY位就自動置位了,但是我們知道,事實上總線上並無數據在傳輸. 在主機發送停止位之後,也無法將BUSY位復位.
2. 問題解決
注意當IIC模塊處於復位的狀態的時候,BUSY位也是復位的. 有兩種方法可以將IIC模塊復位,一種是直接使用RCC模塊的寄存器將IIC模塊復位,另一種是方法是將IIC模塊控制寄存器CR1中的SWRST置位,使IIC模塊處於復位狀態. 本文介紹的是第二種方法, 第一種方法也是類似第二種方法,只不過寫的寄存器不一樣而已.
由於STM32F4XX的IIC模塊,在使能了模塊時鐘之後,BUSY位總是爲1,不能夠自動跳轉狀態,那麼我們就只能夠通過復位和取消復位IIC模塊來手動控制BUSY位的狀態跳轉.
另外還需要注意的是,當我們復位IIC模塊的時候,我們先前對IIC模塊做的所有配置也復位了. 在取消IIC復位狀態之後,還需要重新配置IIC模塊,再開始數據傳輸.
3. 參考代碼如下:
// ************ iic.h ************
#ifndef __IIC_H
#define __IIC_H
#include "stm32f4xx.h"
#include "stm32f4xx_i2c.h"
#include "stm32f4xx_gpio.h"
void IIC1_init(void);
u8 I2C1_Write(u8 address, u8 *pData, u8 bytes);
u8 I2C1_Read(u8 address, u8 *pData, u8 bytes);
void IIC1_Register_Detected(void);
#endif
// ************ iic.c ************
#include "iic.h"
static void IIC1_BUSY_RESET(void);
static void IIC1_BUSY_SET(void);
/*
IIC1_SCL : PB8
IIC1_SDA : PB9
*/
void IIC1_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
// ********** GPIOB PB8 PB9 配置成複用I2C模式 **********
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_OD;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource8,GPIO_AF_I2C1);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource9,GPIO_AF_I2C1);
// ********** 配置I2C1模塊 **********
/*I2C Peripheral clock enable */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
}
// 通過復位來手動復位BUSY位,表示IIC總線不忙
static void IIC1_BUSY_RESET(void)
{
I2C1->CR1 |= 0x1<<15; // SWRST 位置1
}
// 取消復位,BUSY位自動置1,然後初始化並且使能IIC外設
static void IIC1_BUSY_SET(void)
{
I2C_InitTypeDef I2C_InitStruct;
I2C1->CR1 &= ~(0x1<<15); // SWRST 位清0;
// 初始化IIC1
/* I2C Struct Initialize */
I2C_InitStruct.I2C_ClockSpeed = 100000; // 100 kHz
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_16_9;
I2C_InitStruct.I2C_OwnAddress1 = 0xA0;
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* I2C Initialize */
I2C_Init(I2C1, &I2C_InitStruct);
/* 使能 IIC1 模塊 */
I2C_Cmd(I2C1,ENABLE);
}
/* ***************************** I2C1讀寫函數 ****************************** */
#define FLAG_TIMEOUT 25*1000*10
u32 Timeout = FLAG_TIMEOUT; // 10毫秒超時
/* IIC1 寫數據
功能: 向地址爲address的從機寫入bytes個字節,pData指向要寫入的數據內容
輸入參數:
u8 address : 7位的地址(在傳入從機的地址之前,要先將從機的地址右移1位到[7:1])
u8 *pData : 指向要寫的數據的指針
u8 bytes : 要寫的字節數量
返回值:
成功返回0,失敗返回1
*/
u8 I2C1_Write(u8 address, u8 *pData, u8 bytes)
{
// *************** S ***************
/*!< While the bus is busy */
Timeout = FLAG_TIMEOUT;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
{
if((Timeout--) == 0)
return 1;
}
IIC1_BUSY_SET();
/*!< Send START condition */
// 發送起始條件
I2C_GenerateSTART(I2C1, ENABLE); // I2C1->CR1 的START位置1,接着I2C1->SR1 SB位置1,I2C1->SR2 MSL 位置1
// *************** EV5: SB = 1, 通過先讀取SR1寄存器再將地址寫入DR寄存器來清零 ***************
/*!< Test on EV5 and clear it */
Timeout = FLAG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
if((Timeout--) == 0) return 1;
}
/*!< Send slave address for write */
// 發送address
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
// *************** EV6: ADDR=1,通過先讀取SR1寄存器再讀取SR2寄存器來清零 ***************
/*!< Test on EV6 and clear it */
Timeout = FLAG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) /* BUSY, MSL, ADDR, TXE and TRA flags */
{
if((Timeout--) == 0) return 1;
}
// *************** EV8_1: TXE=1 數據寄存器空,移位寄存器空,向DR寫入Data1
bytes--;
I2C_SendData(I2C1, *pData++);
// *************** EV8: TXE=1 移位寄存器非空,數據寄存器空,向DR寫入數據清零
while(bytes--)
{
Timeout = FLAG_TIMEOUT;
// 接收到ASK TXE纔可以置1
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)) /* TRA=1, BUSY=1, MSL=1, TXE=1 */
{
if((Timeout--) == 0) return 1;
}
I2C_SendData(I2C1, *pData++);
/*!< Test on EV8 and clear it */
}
// *************** EV8_2: TXE=1 BTF=1 通過停止條件清零 ***************
//BTF:字節傳輸完成 (Byte transfer finished)
// 在最後一個字節發送完成後,不會再寫一個字節到DR,BTF會置1,指示"發送過程中將發送一個新字節但尚未向 DR 寄存器寫入數據 (TxE=1)。"
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((Timeout--) == 0) return 1;
}
I2C_GenerateSTOP(I2C1,ENABLE);
IIC1_BUSY_RESET();
return 0;
}
/* IIC1 讀數據
功能: 向地址爲address的從機讀取bytes個字節到pData指向的內存中
輸入參數:
u8 address : 7位的地址(在傳入從機的地址之前,要先將從機的地址右移1位到[7:1])
u8 *pData : 指向要保存數據的內存中
u8 bytes : 要讀的字節數量
返回值:
成功返回0,失敗返回1
*/
u8 I2C1_Read(u8 address, u8 *pData, u8 bytes)
{
// *************** S ***************
/*!< While the bus is busy */
Timeout = FLAG_TIMEOUT;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
{
if((Timeout--) == 0)
return 1;
}
IIC1_BUSY_SET();
/*!< Send START condition */
// 發送起始條件
I2C_GenerateSTART(I2C1, ENABLE);
// *************** EV5: SB = 1, 通過先讀取SR1寄存器再將地址寫入DR寄存器來清零 ***************
/*!< Test on EV5 and clear it */
// EV5: SB = 1, 通過先讀取SR1寄存器再將地址寫入DR寄存器來清零
Timeout = FLAG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
if((Timeout--) == 0) return 1;
}
/*!< Send slave address for write */
// 發送 address
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Receiver);
// *************** EV6: ADDR=1,通過先讀取SR1寄存器再讀取SR2寄存器來清零 ***************
/*!< Test on EV6 and clear it */
// EV6: ADDR=1,通過先讀取SR1寄存器再讀取SR2寄存器來清零
Timeout = FLAG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) /* BUSY, MSL and ADDR flags */
{
if((Timeout--) == 0) return 1;
}
// *************** EV7: ***************
while(--bytes>=0) /* while(){} 語句是入口循環,所以使用減量前綴確保while()中byte的值和循環體中byte的值是一致的 */
{
/*!< Test on EV7 and clear it */
Timeout = FLAG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) /* BUSY, MSL and RXNE flags */
{
if((Timeout--) == 0) return 1;
}
*pData++ = I2C_ReceiveData(I2C1);
if(bytes == 1)
{ // *************** EV7_1: ***************
I2C_AcknowledgeConfig(I2C1,DISABLE);
I2C_GenerateSTOP(I2C1,ENABLE);
}
}
IIC1_BUSY_RESET();
return 0;
}