1. 程式人生 > >STM32硬體IIC驅動設計

STM32硬體IIC驅動設計

前言

stm32的硬體IIC一直是令人詬病的地方,以至於很多情況下我們不得不選擇使用模擬IIC的方式來在stm32上進行iic通訊。我在stm32 iic通訊上也浪費了幾多青春。。。經過不斷地探索最終還是成功了(可喜可賀啊),現在把我的探索成功的經驗分享出來,如果能減少讀者在硬體iic上面浪費的時間,那真是太棒了!
我把驅動的一些描述做成了表格如下:

問題 描述
MCU型號 STM32F407VET6
庫函式 標準外設庫
作業系統 FreeRTOS

關於IIC通訊

眾所周知IIC是一種通訊方式。。。所以有必要先介紹一下IIC通訊,省的下面不知道不知道我在寫什麼。當然這些都是基礎,你可以選擇跳過,直接看第三部分STM32的IIC

IIC是什麼

說實話這個問題有點難,我就百度了一下,描述如下

IIC 即Inter-Integrated Circuit(積體電路匯流排),這種匯流排型別是由飛利浦半導體公司在八十年代初設計出來的一種簡單、雙向、二線制、同步序列匯流排,主要是用來連線整體電路(ICS) ,IIC是一種多向控制匯流排,也就是說多個晶片可以連線到同一匯流排結構下,同時每個晶片都可以作為實時資料傳輸的控制源。這種方式簡化了訊號傳輸匯流排介面。

通過這幾句描述我發現——我更加不知道它是什麼了。不過至少我看到一個關鍵字——二線制,那就表明IIC需要兩根線進行通訊(不算電源和地),那麼就先看一下iic硬體介面吧,這是我唯一可以看到的東西。

介面1 介面2
SCL SDA

從名稱可以看出這兩個介面SCL為clock即時鐘線,SDA為date即資料線。就是通過這兩根線進行iic通訊,相當精簡的硬體連線!但是就我所知越是精簡的硬體介面其軟體越複雜。比如並行通訊的話我們的軟體直接讀高低電平就好了。。。建議不懂什麼是並行通訊的人直接百度之,當然這並不是重點。我們的重點是序列通訊,而序列通訊的理論支撐是其內在的通訊協議,當然iic也是個有協議的人。。。那麼接下來就該我們的主角出場了——IIC協議,

IIC協議

說起協議我不知道你想到了什麼,於是百度之:協議:共同計議,協商。其實就是人為規定了一種通訊規則,不過這個規則是給機器執行的。所以不要讓協議兩個字嚇到。下面是兩個基本的規則:

  • 1、只要求兩條匯流排線路 一條序列資料線 SDA 一條序列時鐘線 SCL
  • 2、每個連線到匯流排的器件都可以通過唯一的地址和一直存在的簡單的主機/從機關係軟體設定地址;主機可以作為主傳送器或主機接收器

其中第一條我們已經知道了,第二條是說iic通訊需要主機和從機,至於什麼是主機什麼是從機,一般情況下比如我使用stm32讀取MPU6050那麼stm32就是主機,mpu6050就是從機。還有一點iic可以支援一主多從的模式,這也就表示如果我們有多個從機裝置如我既想讀取MPU6050,又想讀取HMC5883資料(這兩個都支援iic通訊)我們不需要為這兩個裝置分別引出兩個iic介面,只需要把stm32、mpu6050、hmc5883三者的SCL和SDA引腳對應連線起來即可。那麼問題來了:主機是怎麼知道現在在跟哪個從機通訊?其實這個問題很白痴。如果換個問題類比一下,把主機想象成一個老地主,他手下有N個奴僕(從機),地主是怎麼知道現在他在跟誰說話?當然是通過名字啊。於是每個iic裝置就被人為規定了“名字”,我們稱為地址。如MPU6050的地址為0x68(在AD0引腳為低電平情況下)。
iic的協議還是很多的,這裡就不一一列舉了,新手的話知道這兩個就好了。如果這些內容理解起來沒毛病的話就該進入iic時序這個環節了。

IIC通訊時序

時序即電平變化的順序,對於iic來說即SCL和SDA兩根線上的電平變化順序。對於IIC其訊號主要有一下幾個
開始訊號、結束訊號和應答/非應答訊號。這就像上面的地主和奴僕例子裡的,地主要命令僕役工作要有開始的訊號,命令結束工作也要有停止訊號,地主還要根據僕役的反饋來判斷僕役是否能完成任務,如果僕役回答能夠完成工作就是應答訊號,若回答無法完成工作就是非應答訊號。對於iic其規定了這幾種訊號的電平變化時序如下:

開始和結束訊號

起始和終止訊號

開始訊號:SCL 為高電平時,SDA 由高電平向低電平跳變,開始傳送資料。
結束訊號:SCL 為高電平時,SDA 由低電平向高電平跳變,結束傳送資料。

應答/非應答 訊號

應答訊號

應答訊號:接收資料的 IC 在接收到 8bit 資料後,向傳送資料的 IC 發出特定的低電平脈衝,表示已收到資料。CPU 向受控單元發出一個訊號後,等待受控單元發出一個應答訊號,CPU 接收到應答訊號後,根據實際情況作出是否繼續傳遞訊號的判斷。若未收到應答訊號,由判斷為受控單元出現故障。

對時序的總結如下:

  • 1、IIC通訊進行資料傳輸時,時鐘訊號為高電平期間資料線訊號必須保持穩定,只有時鐘訊號為低電平時才允許資料訊號變化。

  • 2、SCL高電平期間,SDA由高電平變為低電平則代表起始訊號,SDA由低電平變為高電平時代表停止訊號。

  • 3、起始訊號和停止訊號都是由主機發起,當有起始訊號發出後匯流排處於佔用狀態,當停止訊號被髮出後匯流排處於空閒狀態。

當我們瞭解到這幾種時序的原理後便可以通過操作兩個IO的高低電平來實現IIC通訊了,這就是常說的模擬IIC的方式。以上這些知識只需要知道原理即可,如果真要寫一個模擬iic的程式你還需要精確的時序,比如起始訊號SDA脈衝持續的最短時間,其變為低電平後SCL至少需要保持高電平時間等等問題,所以你需要一個比較精確的微秒級延時函式。。。但這一切並不是今天我們的重點。我們的重點是STM32的硬體IIC驅動設計。

STM32的IIC

STM32的IIC還是很強大的,只是很多功能我們根本用不上。如下是資料手冊的裡關於IIC的描述:

I 2 C(內部積體電路)匯流排介面用作微控制器和 I 2 C 序列匯流排之間的介面。它提供多主模式功
能,可以控制所有 I 2 C 匯流排特定的序列、協議、仲裁和時序。它支援標準和快速模式。它還
與 SMBus 2.0 相容。
它可以用於多種用途,包括 CRC 生成和驗證、SMBus(系統管理匯流排)以及 PMBus(電源
管理匯流排)。
根據器件的不同,可利用 DMA 功能來減輕 CPU 的工作量。

我設計驅動的目的是通過STM32讀取MPU6050所以該驅動的特性是stm32作為主機而不是從機,所以想了解關於從機部分的就不用往下看了。其次由於stm32的IIC支援DMA的並且我的stm32大部分時間是在讀取感測器資料。所以該驅動設計時把IIC接收部分設計為DMA接收,而傳送部分沒有使用DMA。關於是否使用中斷,我的回答是一定要用中斷,排除STM32採用查詢方式各種堵死在while(1)裡的囧狀,使用中斷還可以提升系統的效能,while(xxx);查詢是要佔據一定的時間的啊。所以果斷採取中斷處理的方式,只是stm32的中斷情況比較多。所以。。。我們需要冷靜地縷一縷。以下是資料手冊時間。有該寶典的同學一定要珍惜啊,特別是中文版的,很好很強大。

stm32硬體IIC通訊流程

首先stm32的iic分四種模式:從傳送器、從接收器、主傳送器、主接收器。而上面我們也提到了只用到了主機模式,所以我們只關注主傳送器、主接收器兩種模式。
在主模式下,I 2 C 介面會啟動資料傳輸並生成時鐘訊號。序列資料傳輸始終是在出現起始位時開始,在出現停止位時結束。起始位和停止位均在主模式下由軟體生成
資料和地址均以 8 位位元組傳輸,MSB 在前。起始位後緊隨地址位元組(7 位地址佔據一個位元組;10 位地址佔據兩個位元組)。地址始終在主模式下傳送。
其設計框圖如下
stm32 IIC設計框圖

這幅圖列出了所有關於iic的暫存器,而我們真正用到最多的也就是兩個控制暫存器CR1和CR2,兩個狀態暫存器SR1、SR2。
以下是我對這四個暫存器進行的總結性描述:

  • CR1:主要是控制IIC的一些訊號生成(起始和結束訊號等)以及是否使能IIC的某些特定功能,如應答使能是、資料校驗使能等。
  • CR2主要控制一些IIC中斷或者DMA是否開啟以及配置IIC的通訊速率。因為此次設計的驅動中需要用到iic中斷,所以需要了解下iic的中斷。如下表:
中斷名稱 描述
ITBUF 緩衝中斷
ITEVT 事件中斷
ITERR 錯誤中斷

在這裡不細講,下面會有介紹。只需要知道這些中斷的開啟都需要配置CR2相應位。

  • SR1和SR2裡面的內容就比較雜了,不過好多功能我們用不到,只需要知道這兩個暫存器儲存了很多標識,如是否傳送了起始位,應答失敗?等狀態,在程式裡會有體現。

具體內容還是要參考資料手冊裡關於iic暫存器的描述,當然這部分也不需要記住,因為我們要呼叫庫函式來實現很多的功能。

接下來分析stm32的iic通訊流程,我們主要分析在主模式下如何傳送以及通過DMA方式接收資料。

1、主模式下發送資料:

  • 1、生成起始訊號:CR1裡的START 位置 1 後,介面會在 SR2的BUSY 位清零後生成一個起始位並切換到主模式。生成起始訊號成功後SR1的SB 位會由硬體置 1 ,如果使能了事件中斷(CR2的 ITEVFEN 位置 1 )則生成一箇中斷。這個中斷在資料手冊裡官方命名為EV5。即為事件5中斷。至於為什麼從5開始,因為事件1到4在從機部分用了。。。接下來主裝置會等待軟體對 SR1 執行讀操作,然後把從裝置地址寫入 DR 暫存器。只有這樣才能清零SR1的SB位。

  • 2、從地址傳輸,接下來從地址會通過內部移位暫存器傳送到 SDA 線。stm32支援10位和7位地址。因為大多數我們接觸到的都是7位地址的,所以這裡只介紹7位地址位的。在 7 位定址模式下,會發送一個地址位元組。地址位元組被髮出後,SR1的ADDR 位會由硬體置 1 如果使能了事件中斷(CR2的 ITEVFEN 位置 1 )則生成一箇中斷EV6。接下來主裝置會等待對 SR1 暫存器執行讀操作,然後對 SR2 暫存器執行讀操作,只有這樣才能清零SR1的ADDR 位。

  • 3、主傳送器,在傳送出地址並將 ADDR 清零後,主裝置會通過內部移位暫存器將 DR 暫存器中的位元組傳送到 SDA 線。在向資料暫存器寫資料前如果開啟了事件中斷會進入中斷EV8,接收到應答脈衝後,TxE 位會由硬體置 1 並在 ITEVFEN 和 ITBUFEN 位均置 1 時生成一箇中斷EV8。如果在上一次資料傳輸結束之前 TxE 位已置 1 但資料位元組尚未寫入 DR 暫存器,則 BTF 位會置 1,而介面會一直延長 SCL 低電平,等待I2C_DR 暫存器被寫入,以將 BTF 清零。結束通訊當最後一個位元組寫入 DR 暫存器後,軟體會將 STOP 位置 1 以生成一個停止位EV8_2。介面會自動返回從模式(M/SL 位清零)。

  • 結束通訊:主裝置會針對自從裝置接收的最後一個位元組傳送 NACK。在接收到此 NACK 之後,從裝置會釋放對 SCL 和 SDA 線的控制。隨後,主裝置可傳送一個停止位/重複起始位。

    1. 為了在最後一個接收資料位元組後生成非應答脈衝,必須在讀取倒數第二個資料位元組後(倒數第二個 RxNE 事件之後)立即將 ACK 位清零。
    2. 要生成停止位/重複起始位,軟體必須在讀取倒數第二個資料位元組後(倒數第二個 RxNE事件之後)將 STOP/START 位置 1。
    3. 在只接收單個位元組的情況下,會在 EV6 期間(在 ADDR 標誌清零之前)禁止應答並在EV6 之後生成停止位。
    4. 生成停止位後,介面會自動返回從模式(M/SL 位清零)。

以上是stm32的iic主傳送模式通訊流程,俗話說一圖頂千字,資料手冊把這個流程以圖表展現出來如下:

這裡寫圖片描述

還是比較清楚的,只需要對照該圖結合上面的文字描述來分析stm32的iic通訊流程。

2、使用 DMA 進行接收

將 I2C_CR2 暫存器中的 DMAEN 位置 1 可以使能 DMA 模式進行接收。接收資料位元組時,資料會從 I2C_DR 暫存器載入到使用 DMA 外設配置的儲存區中(參見 DMA 規範)。要對映一個 DMA 通道以便進行 I 2 C 接收,請按以下步驟操作:其中的 x 表示通道編號。
1. 設定 DMA_CPARx 暫存器中的 I2C_DR 暫存器地址。每次發生 RxNE 事件後,資料都會從此地址移動到儲存器。
2. 設定 DMA_CMARx 暫存器中的儲存器地址。每次發生 RXNE 事件後,資料都會從 I2C_DR暫存器載入到此儲存區。
3. 在 MA_CNDTRx 暫存器中配置要傳輸的總位元組數。在每次 RxNE 事件後,此值都會遞減。
4. 使用 DMA_CCRx 暫存器中的 PL[0:1] 位來配置通道優先順序。
5. 在完成半數傳輸或全部傳輸(取決於應用的需求)之後,將 DMA_CCRx 暫存器中的 DIR位重新置 1 並配置中斷。
6. 將 DMA_CCRx 暫存器中的 EN 位置 1 以啟用通道。
當傳輸的資料量達到 DMA 控制器中程式設計設定的值時,DMA 控制器會發送一個結束傳輸EOT/EOT_1 訊號給 I 2 C 介面,而 DMA 會在 DMA 通道中斷向量上生成一箇中斷(如果已使能):
注意: 如果使用 DMA 進行接收,請勿使能 I2C_CR2 暫存器中的 ITBUFEN 位。

IIC驅動設計

終於到了最重要的環節,(鼓掌)。在這裡我們需要坐下來靜靜地分析一下該怎樣設計這個驅動。設計驅動的目的是儘量使得呼叫者方便呼叫。所以應該儘量封裝一些不常更改的變數和方法,如在這裡就忽略了stm32作為從機,並且預設是使能中斷的。暴露給呼叫者經常要變更的變數和方法,如對於stm32經常要根據實際情況更改使用的iic外設編號(IIC1、IIC2、IIC3)、複用引腳、引腳時鐘、DMA通道等。所以為了程式碼的複用率高這裡以結構體來組合一些經常需要更改的變數。這些變數主要用來初始化IIC。又因為要設計的功能是資料傳輸,所以這裡定義一個Message結構體,裡面可以包含這個Message包含的資料長度、傳輸方向、傳輸狀態等。
以上是一些資料結構的分析,然後是方法的設計。因為要傳輸資料,所以這裡需要一個數據傳輸的方法,該方法根據Message結構體裡的資料得出要傳送或者接收的資料資訊,根據初始化結構體來判斷要進行通訊的iic編號。最後根據該方法生成讀取或者傳送資訊的API。以下是具體實現。It’s time for Code!

IIC初始化

首先定義iic初始化結構體,如下:

///i2c定義
typedef struct
{
  I2C_TypeDef*        i2cPort;                      ///i2cx
  uint32_t            i2cPerif;                     ///i2c時鐘
  uint32_t            i2cEVIRQn;                    ///i2c事件中斷
  uint32_t            i2cERIRQn;                    ///i2c錯誤處理中斷
  uint32_t            i2cClockSpeed;                ///通訊速率
  uint32_t            gpioSCLPerif;                 ///scl時鐘
  GPIO_TypeDef*       gpioSCLPort;                  ///scl埠
  uint32_t            gpioSCLPin;                   ///scl引腳
  uint32_t            gpioSCLPinSource;             ///scl引腳source
  uint32_t            gpioSDAPerif;                 ///sda時鐘
  GPIO_TypeDef*       gpioSDAPort;                  ///sda埠
  uint32_t            gpioSDAPin;                   ///sda引腳
  uint32_t            gpioSDAPinSource;             ///sda埠source

  uint32_t            gpioAF;                       ///複用pack

  uint32_t            dmaPerif;                     ///dma時鐘
  uint32_t            dmaChannel;                   ///dma通道
  DMA_Stream_TypeDef* dmaRxStream;                  ///dma資料流
  uint32_t            dmaRxIRQ;                     ///dma中斷
  uint32_t            dmaRxTCFlag;                  ///dma接收完成中斷
  uint32_t            dmaRxTEFlag;                  ///dma接收錯誤中斷

} I2cDef;

這裡包含了stm32 的iic初始化需要的所有資訊,這個結構體最終包含在I2cDrv上,如下:

///i2c驅動結構體
typedef struct
{
  const I2cDef *def;                    //< i2c定義
  I2cMessage txMessage;                 //< i2c通訊message
  uint32_t messageIndex;                //< 傳送或者接收位元組的索引
  SemaphoreHandle_t isBusFreeSemaphore; //< 訊號量用來同步傳輸
  SemaphoreHandle_t isBusFreeMutex;     //< 互斥訊號量來保護傳輸資料
  DMA_InitTypeDef DMAStruct;            //< DMA 配置
} I2cDrv;

注意const I2cDef *def; 這表明I2cDrv包含了I2cDef的指標。然後根據I2cDrv裡的資料進行初始化工作,如下:

static void i2cdrvInitBus(I2cDrv* i2c)
{
  I2C_InitTypeDef  I2C_InitStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;

  // 使能IIC埠時鐘
  RCC_AHB1PeriphClockCmd(i2c->def->gpioSDAPerif, ENABLE);
  RCC_AHB1PeriphClockCmd(i2c->def->gpioSCLPerif, ENABLE);
  // 使能IIC匯流排時鐘
  RCC_APB1PeriphClockCmd(i2c->def->i2cPerif, ENABLE);

  // 配置引腳屬性
  GPIO_StructInit(&GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
  GPIO_InitStructure.GPIO_Pin = i2c->def->gpioSCLPin; // SCL
  GPIO_Init(i2c->def->gpioSCLPort, &GPIO_InitStructure);

  GPIO_InitStructure.GPIO_Pin =  i2c->def->gpioSDAPin; // SDA
  GPIO_Init(i2c->def->gpioSDAPort, &GPIO_InitStructure);
    /*解鎖匯流排*/
  i2cdrvdevUnlockBus(i2c->def->gpioSCLPort, i2c->def->gpioSDAPort, i2c->def->gpioSCLPin, i2c->def->gpioSDAPin);

  //配置iic埠複用
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Pin = i2c->def->gpioSCLPin; // SCL
  GPIO_Init(i2c->def->gpioSCLPort, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin =  i2c->def->gpioSDAPin; // SDA
  GPIO_Init(i2c->def->gpioSDAPort, &GPIO_InitStructure);

  //埠重對映
  GPIO_PinAFConfig(i2c->def->gpioSCLPort, i2c->def->gpioSCLPinSource, i2c->def->gpioAF);
  GPIO_PinAFConfig(i2c->def->gpioSDAPort, i2c->def->gpioSDAPinSource, i2c->def->gpioAF);

  // I2C配置
  I2C_DeInit(i2c->def->i2cPort);
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
  I2C_InitStructure.I2C_OwnAddress1 = I2C_SLAVE_ADDRESS7;
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
  I2C_InitStructure.I2C_ClockSpeed = i2c->def->i2cClockSpeed;
  I2C_Init(i2c->def->i2cPort, &I2C_InitStructure);

  // 使能IIC錯誤處理中斷
  I2C_ITConfig(i2c->def->i2cPort, I2C_IT_ERR, ENABLE);

  NVIC_InitStructure.NVIC_IRQChannel = i2c->def->i2cEVIRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 7;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  NVIC_InitStructure.NVIC_IRQChannel = i2c->def->i2cERIRQn;
  NVIC_Init(&NVIC_InitStructure);

  i2cdrvDmaSetupBus(i2c);//IIC DMA使能

    /*建立訊號量*/
  i2c->isBusFreeSemaphore = xSemaphoreCreateBinary();
  i2c->isBusFreeMutex = xSemaphoreCreateMutex();
}

最後兩句 i2c->isBusFreeSemaphore = xSemaphoreCreateBinary();和 i2c->isBusFreeMutex = xSemaphoreCreateMutex();建立訊號量和互斥訊號量,是freeRTOS裡的函式,用來同步傳送/接收訊號,互斥訊號量來保護傳輸資料,這兩種訊號的用法需要讀者自己查閱資料,這裡不深入分析。
通過以上兩個步驟便可以自定義初始化任何stm32的iic了,只需要定義一個I2cDef用來包含板級iic設定了,如可以這樣定義:

static const I2cDef sensorBusDef =
{
  .i2cPort            = I2C1,
  .i2cPerif           = RCC_APB1Periph_I2C1,
  .i2cEVIRQn          = I2C1_EV_IRQn,
  .i2cERIRQn          = I2C1_ER_IRQn,
  .i2cClockSpeed      = I2C_DECK_CLOCK_SPEED,
  .gpioSCLPerif       = RCC_AHB1Periph_GPIOB,
  .gpioSCLPort        = GPIOB,
  .gpioSCLPin         = GPIO_Pin_6,
  .gpioSCLPinSource   = GPIO_PinSource6,
  .gpioSDAPerif       = RCC_AHB1Periph_GPIOB,
  .gpioSDAPort        = GPIOB,
  .gpioSDAPin         = GPIO_Pin_7,
  .gpioSDAPinSource   = GPIO_PinSource7,
  .gpioAF             = GPIO_AF_I2C1,
  .dmaPerif           = RCC_AHB1Periph_DMA1,
  .dmaChannel         = DMA_Channel_1,
  .dmaRxStream        = DMA1_Stream0,
  .dmaRxIRQ           = DMA1_Stream0_IRQn,
  .dmaRxTCFlag        = DMA_FLAG_TCIF0,
  .dmaRxTEFlag        = DMA_FLAG_TEIF0,
};

不過以上很多函式這裡都不能一一給出原型,因為太多了,比如解鎖匯流排這個函式還有DMA配置這個函式需要讀者自己實現了,這裡主要講設計的思路和方法。

資料的傳輸

資料傳輸的函式如下:

bool i2cdrvMessageTransfer(I2cDrv* i2c, I2cMessage* message)
{
  bool status = false;
    //取互斥訊號會使得該互斥訊號變成無效狀態,直到再給一次訊號
  xSemaphoreTake(i2c->isBusFreeMutex, portMAX_DELAY); // Protect message data
  // Copy message
  memcpy((char*)&i2c->txMessage, (char*)message, sizeof(I2cMessage));
  // 開始通過ISR方式傳送訊號.
  i2cdrvStartTransfer(i2c);
  // 等待傳輸完成
  if (xSemaphoreTake(i2c->isBusFreeSemaphore, I2C_MESSAGE_TIMEOUT) == pdTRUE)
  {
    if (i2c->txMessage.status == i2cAck)
    {
      status = true;
    }
  }
  else
  {
    i2cdrvClearDMA(i2c);
    i2cdrvTryToRestartBus(i2c);
    //TODO: If bus is really hanged... fail safe
  }
  xSemaphoreGive(i2c->isBusFreeMutex);//傳送訊號

  return status;
}

其中主要是呼叫i2cdrvStartTransfer(i2c);這個函式,其作用是傳送起始訊號以及開啟對應中斷,原型如下:

static void i2cdrvStartTransfer(I2cDrv *i2c)
{
  if (i2c->txMessage.direction == i2cRead)///DMA讀取
  {
    i2c->DMAStruct.DMA_BufferSize = i2c->txMessage.messageLength;
    i2c->DMAStruct.DMA_Memory0BaseAddr = (uint32_t)i2c->txMessage.buffer;
    DMA_Init(i2c->def->dmaRxStream, &i2c->DMAStruct);
    DMA_Cmd(i2c->def->dmaRxStream, ENABLE);
  }

  I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);//失能buff中斷
  I2C_ITConfig(i2c->def->i2cPort, I2C_IT_EVT, ENABLE);//使能事件中斷
  i2c->def->i2cPort->CR1 = (I2C_CR1_START | I2C_CR1_PE);//傳送起始訊號
}

可以看出在進行傳送時只需要傳送一個起始訊號以及開啟事件中斷即可,進行接收時還需要開啟DMA對應通道,並且設定記憶體地址即可。最後的一系列處理均在中斷裡執行。那麼接下來就開始中斷處理了。

中斷處理

在這裡一共需要處理三個中斷函式

事件中斷
錯誤處理中斷
DMA完成及錯誤中斷

事件中斷

事件中斷主要處理一系列事件,主要是上面定義的EV5、EV6等事件,原型如下,該函式被iic事件中斷函式呼叫:

static void i2cdrvEventIsrHandler(I2cDrv* i2c)
{
  uint16_t SR1;
  uint16_t SR2;

  // 首先讀取狀態暫存器
  SR1 = i2c->def->i2cPort->SR1;



  // 起始事件EV5
  if (SR1 & I2C_SR1_SB)
  {
    i2c->messageIndex = 0;

    if(i2c->txMessage.direction == i2cWrite ||
       i2c->txMessage.internalAddress != I2C_NO_INTERNAL_ADDRESS)
    {
      I2C_Send7bitAddress(i2c->def->i2cPort, i2c->txMessage.slaveAddress << 1, I2C_Direction_Transmitter);
    }
    else
    {
      I2C_AcknowledgeConfig(i2c->def->i2cPort, ENABLE);
      I2C_Send7bitAddress(i2c->def->i2cPort, i2c->txMessage.slaveAddress << 1, I2C_Direction_Receiver);
    }
  }
  // 地址事件,代表從機響應地址的發生EV6
  else if (SR1 & I2C_SR1_ADDR)
  {
    if(i2c->txMessage.direction == i2cWrite ||
       i2c->txMessage.internalAddress != I2C_NO_INTERNAL_ADDRESS)
    {
      SR2 = i2c->def->i2cPort->SR2;                               // 清除addr位
      // 判斷內部地址是有的
      if (i2c->txMessage.internalAddress != I2C_NO_INTERNAL_ADDRESS)
      {
        if (i2c->txMessage.isInternal16bit)
        {
          I2C_SendData(i2c->def->i2cPort, (i2c->txMessage.internalAddress & 0xFF00) >> 8);
          I2C_SendData(i2c->def->i2cPort, (i2c->txMessage.internalAddress & 0x00FF));
        }
        else
        {
          I2C_SendData(i2c->def->i2cPort, (i2c->txMessage.internalAddress & 0x00FF));
        }
        i2c->txMessage.internalAddress = I2C_NO_INTERNAL_ADDRESS;
      }
      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, ENABLE);        // 使能EV7
    }
    //為讀取,開啟DMA
    else 
    {
      if(i2c->txMessage.messageLength == 1)
      {
        I2C_AcknowledgeConfig(i2c->def->i2cPort, DISABLE);
      }
      else
      {
        I2C_DMALastTransferCmd(i2c->def->i2cPort, ENABLE); 
      }
      // 進位制iic buff中斷
      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_EVT | I2C_IT_BUF, DISABLE);
      // 使能DMA傳輸完成中斷
      DMA_ITConfig(i2c->def->dmaRxStream, DMA_IT_TC | DMA_IT_TE, ENABLE);
      I2C_DMACmd(i2c->def->i2cPort, ENABLE); // Enable before ADDR clear

      __DMB();                         
      SR2 = i2c->def->i2cPort->SR2;    // 讀取SR2來清除addr
    }
  }
  // 傳輸完成EV8-2
  else if (SR1 & I2C_SR1_BTF)
  {
    SR2 = i2c->def->i2cPort->SR2;
    if (SR2 & I2C_SR2_TRA) // 是在寫的模式?
    {
      if (i2c->txMessage.direction == i2cRead) // read
      {

        i2c->def->i2cPort->CR1 = (I2C_CR1_START | I2C_CR1_PE); // 生成起始訊號
      }
      else
      {
        i2cNotifyClient(i2c);

        i2cTryNextMessage(i2c);
      }
    }
    else // 讀取模式,在DMA接收下不會發生
    {
      i2c->txMessage.buffer[i2c->messageIndex++] = I2C_ReceiveData(i2c->def->i2cPort);
      if(i2c->messageIndex == i2c->txMessage.messageLength)
      {
        i2cNotifyClient(i2c);

        i2cTryNextMessage(i2c);
      }
    }

    while (i2c->def->i2cPort->CR1 & 0x0100) { ; }
  }
  // 位元組接收
  else if (SR1 & I2C_SR1_RXNE) // 讀取模式,在DMA接收下不會發生
  {
    i2c->txMessage.buffer[i2c->messageIndex++] = I2C_ReceiveData(i2c->def->i2cPort);
    if(i2c->messageIndex == i2c->txMessage.messageLength)
    {
      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);   
    }
  }

  else if (SR1 & I2C_SR1_TXE)
  {
    if (i2c->txMessage.direction == i2cRead)
    {

      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);
    }
    else
    {
      I2C_SendData(i2c->def->i2cPort, i2c->txMessage.buffer[i2c->messageIndex++]);
      if(i2c->messageIndex == i2c->txMessage.messageLength)
      {

        I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);
      }
    }
  }
}

錯誤處理

在錯誤處理中斷裡主要需要處理應答失敗,其餘的錯誤直接清除標誌位,原型如下

static void i2cdrvErrorIsrHandler(I2cDrv* i2c)
{
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_AF))
  {
    if(i2c->txMessage.nbrOfRetries-- > 0)
    {
      // 重新生成開始訊號
      i2c->def->i2cPort->CR1 = (I2C_CR1_START | I2C_CR1_PE);
    }
    else
    {
      // 重試幾次後還未成功則嘗試下一個
      i2c->txMessage.status = i2cNack;
      i2cNotifyClient(i2c);
      i2cTryNextMessage(i2c);
    }
    I2C_ClearFlag(i2c->def->i2cPort, I2C_FLAG_AF);
  }
  ///剩下幾個錯誤直接清除就行了
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_BERR))
  {
      I2C_ClearFlag(i2c->def->i2cPort, I2C_FLAG_BERR);
  }
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_OVR))
  {
      I2C_ClearFlag(i2c->def->i2cPort, I2C_FLAG_OVR);
  }
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_ARLO))
  {
      I2C_ClearFlag(i2c->def->i2cPort,I2C_FLAG_ARLO);
  }
}

DMA中斷

DMA中斷裡分完成和錯誤中斷,原型如下:

static void i2cdrvDmaIsrHandler(I2cDrv* i2c)
{
  if (DMA_GetFlagStatus(i2c->def->dmaRxStream, i2c->def->dmaRxTCFlag)) // 傳輸完成
  {
    i2cdrvClearDMA(i2c);
    i2cNotifyClient(i2c);

    i2cTryNextMessage(i2c);
  }
  if (DMA_GetFlagStatus(i2c->def->dmaRxStream, i2c->def->dmaRxTEFlag)) //傳輸錯誤
  {
    DMA_ClearITPendingBit(i2c->def->dmaRxStream, i2c->def->dmaRxTEFlag);
    i2c->txMessage.status = i2cNack;
    i2cNotifyClient(i2c);
    i2cTryNextMessage(i2c);
  }
}

函式封裝

最後是對函式進行封裝,因為我們最終要使用iic進行讀取或者傳送資料,所以需要對函式封裝,這裡舉個例子,如讀取函式封裝如下:

/**
 * 從i2c外設中讀取資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param len  要讀取的位元組長度
 * @param data[OUT]  讀取資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevRead(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
               uint16_t len, uint8_t *data);

函式原型如下:

bool i2cdevRead(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
               uint16_t len, uint8_t *data)
{
  I2cMessage message;

    i2cdrvCreateMessageIntAddr(&message, devAddress, false, memAddress,
                            i2cRead, len, data);

  return i2cdrvMessageTransfer(dev, &message);
}

這裡首先建立message然後呼叫i2cdrvMessageTransfer進行資料傳輸。i2cdrvCreateMessageIntAddr原型如下:

void i2cdrvCreateMessageIntAddr(I2cMessage *message,
                             uint8_t  slaveAddress,
                             bool IsInternal16,
                             uint16_t intAddress,
                             uint8_t  direction,
                             uint32_t length,
                             uint8_t  *buffer)
{
  message->slaveAddress = slaveAddress;
  message->direction = direction;
  message->isInternal16bit = IsInternal16;
  message->internalAddress = intAddress;
  message->messageLength = length;
  message->status = i2cAck;
  message->buffer = buffer;
  message->nbrOfRetries = I2C_MAX_RETRIES;
}

以上只是一個讀取的example,最終的封裝如下:(原型就不寫了太多)

/**
 * 從i2c外設中讀取資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param len  要讀取的位元組長度
 * @param data[OUT]  讀取資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevRead(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
               uint16_t len, uint8_t *data);

/**
 * 從16位內部地址的裝置中讀取資料
 * @param dev  指向i2c外設的指標
 * @param devAddress  從裝置地址
 * @param memAddress  內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param len  要讀取的長度.
 * @param data[OUT]  讀取資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevRead16(I2C_Dev *dev, uint8_t devAddress, uint16_t memAddress,
               uint16_t len, uint8_t *data);

/**
 * 初始化i2c外設
 * @param dev  指向i2c外設指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
int i2cdevInit(I2C_Dev *dev);

/**
 * 從i2c外設中讀取一個位元組資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param data[OUT]  讀取資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevReadByte(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                    uint8_t *data);

/**
 * 從i2c外設中讀取一個bit資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param bitNum      要讀取bit的位置(0-7)
 * @param data[OUT]  讀取資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevReadBit(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                     uint8_t bitNum, uint8_t *data);
/**
 * 從i2c外設中讀取多個bit資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param bitNum      要讀取bit的起始位置(0-7)
 * @param length      要讀取的長度
 * @param data[OUT]  讀取資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevReadBits(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                    uint8_t bitStart, uint8_t length, uint8_t *data);

/**
 * 向i2c外設中寫入資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param len  要寫入的位元組長度
 * @param data[OUT]  寫入資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevWrite(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                 uint16_t len, uint8_t *data);

/**
 * 向16位內部地址的裝置中寫入資料
 * @param dev  指向i2c外設的指標
 * @param devAddress  從裝置地址
 * @param memAddress  內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param len  要寫入的長度.
 * @param data[OUT]  寫入資料的快取區指標
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevWrite16(I2C_Dev *dev, uint8_t devAddress, uint16_t memAddress,
                   uint16_t len, uint8_t *data);

/**
 * 向i2c外設中寫入1byte資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要讀取資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param data  寫入資料
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevWriteByte(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                     uint8_t data);

/**
 * 向i2c外設中寫入一個bit資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要寫入資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param bitNum      要寫入bit的位置(0-7)
 * @param data  寫入的資料
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevWriteBit(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                    uint8_t bitNum, uint8_t data);

/**
 * 向i2c外設中寫入多個bit資料
 * @param dev 指向i2c外設的指標
 * @param devAddress  從機地址
 * @param memAddress  要寫入資料的內部地址, I2CDEV_NO_MEM_ADDR 代表沒有地址.
 * @param bitStart      要寫入bit的起始位置(0-7)
 * @param length      要寫入bit的長度
 * @param data  寫入的資料
 *
 * @return TRUE =成功 FALSE=失敗.
 */
bool i2cdevWriteBits(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                     uint8_t bitStart, uint8_t length, uint8_t data);

尾言

經過以上步驟一個stm32 的iic驅動便完成了,我主要使用它讀取mpu9250,經過驗證沒有問題,讀取mpu9250上的AK8963(pass by模式)也毫無壓力。再也沒有出現鎖死在while(1)的尷尬了(因為這裡根本沒有while(1))。。。也使用它讀取過ak8975.仍無壓力。最後宣告一下不要找我要原始碼,這是我接下來自己開發飛控程式裡的一部分,等我完成這一工程後興許會開放出來。。。在這之前保密!
祝你早日擺脫硬體IIC的煩惱 ∩_∩

相關推薦

STM32硬體IIC驅動設計

前言 stm32的硬體IIC一直是令人詬病的地方,以至於很多情況下我們不得不選擇使用模擬IIC的方式來在stm32上進行iic通訊。我在stm32 iic通訊上也浪費了幾多青春。。。經過不斷地探索最終還是成功了(可喜可賀啊),現在把我的探索成功的經驗分享出來,

STM32硬體IIC操作 (轉)

轉自:http://blog.csdn.net/dengrengong/article/details/39831577  Stm32具有IIC介面,介面有以下主要特性 多主機功能:該模組既可做主裝置也可做從裝置 主裝置功能 C地址檢測 產生和檢測7位/10位地

利用HAL庫硬體IIC驅動OLED

利用CubeMX生成工程文件就不用細說了,網上很多類似的教程.主要談一下自己將原來驅動OLED的庫例程 移植為HAL庫的驅動,本質上沒有多大的區別,只是幾個函式運用的問題.  利用CubeMX 選用I2C1,配置預設即可生成工程之後,單獨建立oled.c  oled.h 

gpio軟體模擬IIC硬體IIC驅動有什麼區別

最近做一個專案,涉及到晶片級的通訊方面的知識(IIC和SPI方面的通訊)。但是方案選擇的時候,發現自身對模擬IO口通訊還是韌體驅動通訊一直沒有一個很全面的認識,所以就在此記錄一下。 所謂硬體I2C對應晶片上的I2C外設,有相應I2C驅動電路,其所使用的I2C管

關於 stm32 硬體iic

最近在做一個stm32專案。用到兩路iic,其中一路是用於iic通訊。另一路用於iic從機。都不是傳統的iic主機讀取eeprom的形式。 開始做的時候,網上搜資料,一大片的吐槽。都在說stm32的硬體iic設計有問題,都在說蛋疼。 (1)iic通訊。形式是一個iic裝置對

STM32硬體IIC之DMA傳輸資料

這裡給出一個實現用DMA傳輸IIC資料的實現過程 這裡咱們說3個點 1.檢查IIC總線上是否有指定地址的器件 2.IIC讀取資料 3.IIC寫資料 下面來一個一個詳細說明 1.檢查IIC總

STM32硬體IIC與51模擬IIC通訊

IIC介紹   IIC協議規定:SDA上傳輸的資料必須在SCL為高電平期間保持穩定,SDA上的資料只能在SCL為低電平期間變化。IIC期間在脈衝上升沿把資料放到SDA上,在脈衝下降沿從SAD上讀取資料。這樣的話,在SCL高電平期間,SDA上的資料是穩定的。在脈

STM32 硬體IIC操作

就三個函式  簡單明瞭   初始化  讀   寫  int main(void){  u8 i;  SystemInit();  Iic1_Init();  LED_GPIO_Config();  I2C1_WriteByte(0xA0,1,0x89);  //寫EEPRO

STM32硬體SPI驅動0.96寸的OLED

1.OLED相關 2.硬體SPI 3.驅動程式 驅動程式參照51微控制器進行移植,只不過模擬的SPI換成STM32硬體SPI,不用再寫時序部分的程式碼。對於STM32的硬體SPI,我們在驅動FLASH中已有介紹,這裡就不再做介紹。 O

STM32微控制器硬體I2C驅動程式(查詢方式)

本文章原始地址:http://feotech.com/?p=69 本程式主要用於驅動STM32微控制器晶片的硬體I2C暫存器,實現通過使用晶片自帶的I2C暫存器進行資料的傳送與接收. 本例程中採用I2C暫存器查詢的方式來實現資料傳輸,當I2C對應暫存器指定狀態時方可執行下一步操作.

STM32微控制器硬體I2C驅動程式(軟體輪詢方式)---摘自:FeoTech

感謝原作者:FeoTech   原文網址:http://feotech.com/?p=69 本程式主要用於驅動STM32微控制器晶片的硬體I2C暫存器,實現通過使用晶片自帶的I2C暫存器進行資料的傳送與接收. 本例程中採用I2C暫存器查詢的方式來實現資料傳輸,當I2C對應

Linux驅動設計硬體基礎(六)硬體時序分析

2.6 硬體時序分析2.6.1 時序分析的概念    驅動工程師一般不需要分析硬體的時序,但許多企業內驅動工程師還需要承擔電路板除錯的任務,因此,掌握時序分析的方法也就比較必要了。    對驅動工程師或硬體工程師而言,時序分析是讓晶片之間的訪問滿足晶片資料手冊中時序圖訊號有效

Linux驅動設計硬體基礎(四)介面與匯流排之乙太網介面

2.3.5 乙太網介面    乙太網介面由MAC(乙太網媒體接入控制器)和PHY(物理介面收發器)組成。乙太網MAC由IEEE802.3乙太網標準定義,實現了資料鏈路層。常用的MAC支援10Mbit/s或100Mbit/s兩種速率。千兆位乙太網是快速乙太網的下一代技術,將網速

linux驅動設計硬體基礎

一。處理器      微處理器(MPU)通常代表一個CPU,而微控制器(MCU)則強調把中央處理器、儲存器,和外圍電路整合在一個晶片中     CPLD(複雜可程式設計邏輯器件)     FPGA(現場可程式設計門列陣) 二。儲存器      Flash的程式設計原理都

Linux驅動設計硬體基礎(四)介面與匯流排之USB

2.3.4 USB    USB(通用序列匯流排)是Intel、Microsoft等廠商為解決計算機外設種類的日益增加與有限的主機板插槽和埠之間的矛盾於1995年提出的,它具有資料傳輸率高、易擴充套件、支援即插即用和熱插拔的優點。    USB 1.1包含全速和低速兩種模式,

Linux驅動設計硬體基礎(五)原理圖分析

    原理圖分析的含義是指通過閱讀電路板的原理圖獲得各種儲存器、外設所使用的硬體資源、介面和引腳連線關係。若要整體理解整個電路板的硬體組成,原理圖的分析方法是以主CPU為中心向儲存器和外設輻射,步驟如下。1)閱讀CPU部分,獲知CPU的哪些片選、中斷和整合的外設控制器在使用

第二章 驅動設計硬體基礎

2.1 處理器 2.1.1 通用處理器——ARM 主流的通用處理器(GPP)多采用SoC(片上系統)的晶片設計方法,集成了各種功能模組,每一種功能都是有硬體描述語言設計程式,然後在SoC內由電流實現的。 中央處理器的體系結構可以分為兩類: 1.馮諾依曼結構。 程式指令儲存器

關於STM32硬體IIC使用問題解決方案

最近公司上STM32,對新的東西不太熟悉。直接上手,平臺配置啥的都還算順利,畢竟八位機平臺的東西在。到硬體IIC的時候就出大問題了,剛剛上板子的PCF8563(RTC),我也懶,直接就用ST官方給的庫。剛剛開始幾次可以讀寫PCF8563,後來直接就杯具。查了兩天,發現連STA

stm32IIC通信協議

art code strong typedef col 上傳 bps eight 系統結構 1 //3?ê??ˉIIC 2 void IIC_Init(void) 3 { 4 GPIO_InitTy

領域驅動設計架構風格

des 設計 表達 對象 切入點 解決 基於 1.5 pattern 領域驅動設計 (DDD) 是面向對象的軟件設計方法,基於業務領域、元素和行為,以及它們之間的關系。其目標是將潛在業務領域的實現用業務領域專家語言定義的領域模型來表達出來。領域模型可以看一個框架,讓業務變得