關於stm32f4xx的片上外設I2C模塊用作主模式下BUSY位總是置1的解決方法

1. 假設

本文假設讀者:

  1. 有使用stm32的經驗
  2. 有使用stm32庫函數編程的經驗
  3. 瞭解IIC通訊協議

本文適合初學者參考

2. stm32f4xx系列的芯片的IIC接口

1. 模式選擇

stm32f4xx的IIC模塊有四種工作模式,默認的工作模式是從模式,在發送起始位會自動由從模式切換爲主模式.
2
相關概念如下:

  1. 發送器:發送數據到總線的器件
  2. 接收器:從總線接收數據的器件
  3. 主機:發起/停止數據傳輸、提供時鐘信號的器件
  4. 從機:被主機尋址的器件
  5. 多主機:可以有多個主機試圖去控制總線,但是不會破壞數據
  6. 仲裁:當多個主機試圖去控制總線時,通過仲裁可以使得只有一個主機獲得總線控制權,並且它傳輸的信息不會被破壞
  7. 同步:多個器件同步時鐘信號的過程
2. 通訊過程

1

儘管已經假設讀者瞭解iic總線協議,但還是對iic通訊的一些概念做簡單介紹.

  1. 開始信號(S):SCL爲高電平時,SDA由高電平向低電平跳變,表示起始信號,開始傳送數據
  2. 結束信號(P):SCL爲高電平時,SDA由低電平向高電平跳變,表示結束信號,結束傳送數據
  3. 響應信號(ACK):接收器在接收到8位數據後,在第9個時鐘週期,拉低SDA電平。即接收數據的IC在接收到8bit數據後,向發送數據的IC發出特定的低電平脈衝,表示已收到數據。
  4. I2C總線進行數據傳送時,
    4.1 SCL高電平期間,數據穩定(不允許變化),此時用於接收器讀數據;
    4.2 SCL低電平期間,數據允許變化,此時用於發送器發(寫)數據;

  5. stm32f4xx的IIC模塊的通訊時序如下:
    5.1 模塊默認工作在從模式, 發送起始位後自動由從模式切換爲主模式. 在主模式下,I2C 接口會啓動數據傳輸並生成時鐘信號。串行數據傳輸始終是在出現起始位時開始,在出現停止位時結束。起始位和停止位均在主模式下由軟件生成。
    5.2 IIC模塊的應答位可以由軟件使能或者禁止. 可以軟件選擇IIC模塊的尋址方式( 7 位/10 位雙尋址模式和/或廣播呼叫地址)
    3
    5.3 IIC模塊作爲主發送器發送數據的過程如下(更加詳細的說明請參考《stm32f4xx中文參考手冊》654頁,這裏不再贅述)
    4
    5.4 IIC模塊作爲主接收器接收數據的過程如下(更加詳細的說明請參考《stm32f4xx中文參考手冊》655頁,這裏不再贅述)
    5

3.“BUSY位總是置1”的問題

1. 問題描述

在STM32F4xx的IIC模塊的狀態寄存器SR2有一個BUSY位用於指示IIC總線的佔用狀態。
6
然而在實際的應用中,使能了IIC模塊的時鐘之後,BUSY位就自動置位了,但是我們知道,事實上總線上並無數據在傳輸. 在主機發送停止位之後,也無法將BUSY位復位.

2. 問題解決

注意當IIC模塊處於復位的狀態的時候,BUSY位也是復位的. 有兩種方法可以將IIC模塊復位,一種是直接使用RCC模塊的寄存器將IIC模塊復位,另一種是方法是將IIC模塊控制寄存器CR1中的SWRST置位,使IIC模塊處於復位狀態. 本文介紹的是第二種方法, 第一種方法也是類似第二種方法,只不過寫的寄存器不一樣而已.
7
由於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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章