1. 程式人生 > 實用技巧 >STM32之IO口模擬IIC

STM32之IO口模擬IIC

本文介紹如何使用STM32標準外設庫的GPIO埠模擬IIC,本例程使用PB6和PB7模擬一路IIC。

本文適合對微控制器及C語言有一定基礎的開發人員閱讀,MCU使用STM32F103VE系列。

1. 簡介

IIC (Inter-Integrated Circuit)匯流排,也可寫作I2C,是PHILIPS 公司開發的兩線式序列匯流排,用於多裝置之間通訊,分為主機Master和從機Slave,主機和從機可以有多個,但一般情況下只有一個主機,從機之間可以通過地址進行區分,不同種類的裝置地址不同,如果同時接入多個相同種類的裝置,可以通過片選訊號對從機進行選擇。通訊只能由主機發起,支援的操作分為讀取和寫入,即主機讀取從機的資料,以及向從機寫入資料。

I2C兩線分別是時鐘線SCL和資料線SDA,其中SCL和SDA均由主機控制,可以設定成開漏輸出模式。

2. 協議說明

2.1. 匯流排傳輸訊號

  • 空閒狀態:IIC空閒狀態時SCL和SDA均輸出高電平,初始狀態以及傳送結束訊號之後均為空閒狀態。
  • 開始訊號:START,簡寫S,SCL為高電平,SDA由高電平向低電平跳變。
  • 結束訊號:STOP,簡寫P,SCL為高電平,SDA由低電平向高電平跳變。
  • 從機地址:SLAVE_ADDRESS,每種從機都有一個表示該裝置的地址,地址一般為7位,主機發起通訊時,通過 SDA 訊號線傳送裝置地址(SLAVE_ADDRESS)來查詢從機,緊跟裝置地址的一個數據位用來表示資料傳輸方向(R/W位),資料方向位為“1”時表示主機由從機讀資料,資料方向位為“0”時表示主機向從機寫資料。從機接收到匹配的地址後,會返回一個應答(ACK)訊號,只有接收到應答訊號後,主機才能繼續傳送或接收資料。
  • 響應訊號:ACK/NACK,簡寫A,響應包括應答(ACK)和非應答(NACK),當資料接收端時,當裝置(無論主從機)接收到 I2C 傳輸的一個位元組資料或地址後,若希望對方繼續傳送資料,則需要向對方傳送“應答(ACK)”訊號,即SDA為低電平,傳送方會繼續傳送下一個資料;若接收端希望結束資料傳輸,則向對方傳送“非應答(NACK)”訊號,即SDA為高電平,傳送方接收到該訊號後會產生一個停止訊號,結束訊號傳輸。
  • 主機寫入:SCL為高電平時,SDA有效,SDA為輸出模式,主機控制SDA輸出高電平時表示寫入1,SDA輸出低電平時表示寫入0。
  • 主機讀取:SCL為高電平時,SDA有效,SDA為輸入模式,主機讀取SDA高電平時表示輸入1,SDA低電平時表示輸入0。此時主機釋放對 SDA 訊號線的控制,由從機控制 SDA 訊號線,主機接收訊號。

2.2. 基本讀寫過程

  • 寫資料:主機先發一個開始訊號(S),然後傳送從機地址(SLAVE ADDRESS),後續跟上寫訊號(R/W位為0),然後等待從機的應答訊號(ACK位為0),主機向從機傳輸資料(DATA),資料包為1個位元組共8位,從高位到低位依次傳送,主機每傳送完1個位元組資料,都要等待從機的應答訊號(ACK),重複這個過程,可以向從機傳輸 N 個數據,當資料傳輸結束時,主機向從機發送一個停止傳輸訊號(P),表示不再傳輸資料。
  • 讀資料:主機先發一個開始訊號(S),然後傳送從機地址(SLAVE ADDRESS),後續跟上讀訊號(R/W位為1),然後等待從機的應答訊號(ACK位為0),主機從從機讀取資料(DATA),資料包為1個位元組共8位,從高位到低位依次傳送,從機每傳送完1個位元組資料,都要等待主機的應答訊號(ACK),重複這個過程,可以從從機讀取 N 個數據,當主機希望停止接收資料時,就向從機返回一個非應答訊號(NACK),則從機自動停止資料傳輸,主機再向從機發送一個停止傳輸訊號(P),表示不再傳輸資料。
  • 讀和寫資料:除了基本的讀寫,I2C 通訊還有一種是複合讀寫模式,該傳輸過程有兩次起始訊號(S)。一般在第一次傳輸中,主機通過 SLAVE_ADDRESS 尋找到從裝置後,傳送一段“資料”,這段資料通常用於表示從裝置內部的暫存器或儲存器地址(注意區分它與 SLAVE_ADDRESS 的區別);在第二次的傳輸中,對該地址的內容進行讀或寫。也就是說,第一次通訊是告訴從機讀寫地址,第二次則是讀寫的實際內容。

2.3. 速度模式

標準模式傳輸速率為100kbit/s,即10us可以傳輸一個bit,如果用GPIO模擬I2C時電平變換時需要增加適當的延時。

3. 初始化

初始化跟普通GPIO類似,只是輸出模式設定為開漏輸出。

其中SCL始終輸出訊號,但SDA需要支援輸出訊號和讀取訊號,當設定為開漏輸出時,如果需要輸出訊號,則正常輸出即可,如果需要讀取訊號,則MCU將SDA輸出高電平,此時如果從機輸出低電平,則SDA被拉低,此時MCU可以讀到低電平。即GPIO引腳為開漏輸出模式時,MCU輸出高電平時,即釋放了該引腳的控制,此時該引腳的電平取決於從機的輸出,且MCU仍可以讀取該引腳的電平。

GPIO初始化完成之後,可以將SCL和SDA置為高電平,即釋放該引腳的控制,如果總線上有多個主機,則不會干擾其他裝置的通訊。

4. 訊號模擬

需要按照通訊訊號的時序,實現START、STOP、ACK、NACK、Read、Write和WaitAck訊號。

完整程式碼(僅自己編寫的部分)

  1 #define IIC_SCL_1    GPIO_SetBits(GPIOB, GPIO_Pin_6)            /* SCL = 1 */
  2 #define IIC_SCL_0    GPIO_ResetBits(GPIOB, GPIO_Pin_6)        /* SCL = 0 */
  3 
  4 #define IIC_SDA_1    GPIO_SetBits(GPIOB, GPIO_Pin_7)            /* SDA = 1 */
  5 #define IIC_SDA_0    GPIO_ResetBits(GPIOB, GPIO_Pin_7)        /* SDA = 0 */
  6 
  7 #define IIC_READ_SDA()    GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)    /* 讀SDA口線狀態 */
  8 
  9 //初始化IIC
 10 void IIC_Init(void)
 11 {                         
 12     GPIO_InitTypeDef GPIO_InitStructure;
 13 
 14     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    
 15        
 16     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
 17     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;   //開漏輸出
 18     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 19     GPIO_Init(GPIOB, &GPIO_InitStructure);
 20  
 21     IIC_Stop();
 22 }
 23 
 24 //產生IIC起始訊號
 25 //SCL為高電平時SDA由高變低
 26 /*
 27 SCL: ̄ ̄ ̄ ̄ ̄\_
 28 SDA: ̄ ̄\____
 29 */
 30 void IIC_Start(void)
 31 {
 32     IIC_SDA_1;
 33     IIC_SCL_1;
 34     delay_us(4);
 35      IIC_SDA_0;
 36     delay_us(4);
 37     IIC_SCL_0;
 38 }      
 39 
 40 //產生IIC停止訊號
 41 //SCL為高電平時SDA由低變高
 42 //IIC空閒時SCL和SDA均輸出高電平,這樣不會干擾其他裝置的收發
 43 /*
 44 SCL: ̄ ̄ ̄ ̄
 45 SDA:__/ ̄
 46 */
 47 void IIC_Stop(void)
 48 {
 49     IIC_SDA_0;
 50     IIC_SCL_1;
 51     delay_us(4);
 52     IIC_SDA_1;
 53 }
 54 
 55 //等待應答訊號到來
 56 //返回值:1,接收應答失敗
 57 //        0,接收應答成功
 58 uint8_t IIC_WaitAck(void)
 59 {
 60     uint8_t errCount = 0;
 61     uint8_t ack = 0;
 62     
 63     IIC_SDA_1;
 64     delay_us(4);
 65     IIC_SCL_1;
 66     delay_us(4);     
 67 
 68     while(IIC_READ_SDA())
 69     {
 70         errCount++;
 71         if(errCount > 250){
 72             ack = 1;
 73             break;
 74         }
 75     }
 76     IIC_SCL_0;
 77     
 78     return ack;
 79 } 
 80 
 81 //產生應答ACK
 82 //SCL為高電平時SDA為低電平表示應答
 83 /*
 84 SCL:     ̄ ̄\____
 85 SDA:_______/ ̄
 86 */
 87 void IIC_Ack(void)
 88 {
 89     IIC_SDA_0;
 90     delay_us(4);
 91     IIC_SCL_1;
 92     delay_us(4);
 93     IIC_SCL_0;
 94     delay_us(4);
 95     IIC_SDA_1;        //釋放SDA
 96 }
 97 
 98 //產生非應答NACK
 99 //SCL為高電平時SDA為高電平表示非應答
100 /*
101 SCL:     ̄ ̄\__
102 SDA: ̄ ̄ ̄ ̄ ̄ ̄ ̄
103 */
104 void IIC_NAck(void)
105 {
106     IIC_SDA_1;
107     delay_us(4);
108     IIC_SCL_1;
109     delay_us(4);
110     IIC_SCL_0;
111     delay_us(4);
112 }        
113 
114 //IIC傳送一個位元組
115 /*
116 SCL:_  _/ ̄\__  _/ ̄ ̄\__  _/ ̄ ̄\__  _/ ̄ ̄\__  _/ ̄ ̄\__  _/ ̄ ̄\__  _/ ̄ ̄\__  _/ ̄ ̄\__
117 SDA:--  ------------  --------------  --------------  --------------  --------------  --------------  --------------  --------------
118 */
119 void IIC_WriteByte(uint8_t txd)
120 {                        
121     uint8_t i;   
122 
123     IIC_SCL_0;
124     for(i = 0; i < 8; i++)
125     {   
126         (txd & 0x80) ? IIC_SDA_1 : IIC_SDA_0;
127         txd <<= 1;
128 
129         delay_us(4);
130         IIC_SCL_1;
131         delay_us(4);
132         IIC_SCL_0;
133         delay_us(4);
134     }
135     IIC_SDA_1;
136 }        
137 
138 //讀1個位元組,ack=1時,傳送ACK,ack=0,傳送NACK
139 /*
140 SCL: ̄ ̄\__  / ̄ ̄\__  / ̄ ̄\__  / ̄ ̄\__  / ̄ ̄\__  / ̄ ̄\__  / ̄ ̄\__  / ̄ ̄\__
141 SDA:==========  ============  ============  ============  ============  ============  ============  ============  
142 */
143 uint8_t IIC_ReadByte(uint8_t ack)
144 {
145     uint8_t i, rcv = 0;
146     
147     for(i = 0; i < 8; i++)
148     {
149         rcv <<= 1;
150         IIC_SCL_1;
151         delay_us(4); 
152         if(IIC_READ_SDA()){
153             rcv++;   
154         }
155         IIC_SCL_0; 
156         delay_us(4);
157     }
158     
159     ack ? IIC_Ack() : IIC_NAck();
160     
161     return rcv;
162 }