STM32學習心得二十九:I2C通訊實驗及相關代碼解讀

記錄一下,方便以後翻閱~
主要內容:
1) I2C通訊協議;
2) 24C02芯片介紹;
3) 相關實驗代碼解讀。
實驗功能:系統啓動後,通過KEY1按鍵來控制24C02的寫入,通過另外一個按鍵KEY0來控制24C02的讀取。並在串口調試助手上面顯示相關信息。
官方資料:《STM32中文參考手冊V10》第24章——I2C接口
1. I2C通訊協議概念
I2C(IIC,Inter-Integrated Circuit),兩線式串行總線,由PHILIPS公司開發用於連接微控制器及其外圍設備。
I2C是由數據線SDA和時鐘SCL構成的串行總線,可發送和接收數據。在CPU與被控IC之間、IC與IC之間進行雙向傳送,高速IIC總線一般可達400kbps以上。
I2C是半雙工通信方式(這個在《學習心得十四:串口通信相關知識及配置方法》有講)。
I2C總線系統結構如下圖所示(這裏先提一句,I2C協議裏,空閒狀態時SDA和SCL都是高電平!):
在這裏插入圖片描述
2. I2C通信協議及相關代碼解讀
2.1 空閒狀態
I2C總線總線的SDA和SCL兩條信號線同時處於高電平時,規定爲總線的空閒狀態。此時各個器件的輸出級場效應管均處在截止狀態,即釋放總線,由兩條信號線各自的上拉電阻把電平拉高。
2.2 開始信號和停止信號
在這裏插入圖片描述
起始信號:當SCL爲高時,SDA由高到低跳變。啓動信號是一種電平跳變時序信號,而不是一個電平信號。
起始信號相關代碼:

void IIC_Start(void)
{
  SDA_OUT();              //先設SDA線爲輸出//
  IIC_SDA=1;              //SDA輸出高//         
  IIC_SCL=1;              //SCL輸出高,此時爲空間狀態//
  delay_us(4);
  IIC_SDA=0;              //SDA輸出由高變低,IIC開始// 
  delay_us(4);
  IIC_SCL=0;              //鉗住I2C總線,準備發送或接收數據// 
}     

停止信號:當SCL爲高時,SDA由低到高跳變。停止信號是一種電平跳變時序信號,而不是一個電平信號。
停止信號相關代碼:

void IIC_Stop(void)
{
  SDA_OUT();              //先設SDA線爲輸出//
  IIC_SCL=0;              //SCL輸出低//
  IIC_SDA=0;              //SDA輸出低//
  delay_us(4);
  IIC_SCL=1;              //SCL輸出高//
  IIC_SDA=1;              //SDA輸出由低變高,發送I2C總線結束信號//
  delay_us(4);                                                   
}

2.3 應答信號
發送器每發送一個字節,就在時鐘脈衝9期間釋放數據線,由接收器反饋一個應答信號。應答信號爲低電平時,規定爲有效應答位(ACK簡稱應答位),表示接收器已經成功地接收了該字節;應答信號爲高電平時,規定爲非應答位(NACK),一般表示接收器接收該字節沒有成功。
等待應答信號的相關代碼:

u8 IIC_Wait_Ack(void)
{
 u8 ucErrTime=0;
 SDA_IN();                   //SDA設置爲輸入//  
 IIC_SDA=1;delay_us(1);      //SDA設高電平//   
 IIC_SCL=1;delay_us(1);      //SCL設高電平//
 while(READ_SDA)             //讀取PB7的值,0或1//
  {
   ucErrTime++;
   if(ucErrTime>250)
    {
     IIC_Stop();
     return 1;               //失敗//         
    }
  }
  IIC_SCL=0;                 //當PB7值爲0,則SCL時鐘輸出低電平,0//           
  return 0;                  //成功//
}

對於反饋有效應答位ACK的要求是,接收器在第9個時鐘脈衝之前的低電平期間將SDA線拉低,並且確保在該時鐘的高電平期間爲穩定的低電平(有效應答應這樣編寫)。
有效/無效應答相關代碼:

void IIC_Ack(void)        //有效應答//
{
  IIC_SCL=0;
  SDA_OUT();
  IIC_SDA=0;
  delay_us(2);
  IIC_SCL=1;
  delay_us(2);
  IIC_SCL=0;
}          
void IIC_NAck(void)       //無效應答//
{
  IIC_SCL=0;
  SDA_OUT();
  IIC_SDA=1;
  delay_us(2);
  IIC_SCL=1;
  delay_us(2);
  IIC_SCL=0;
}            

在這裏插入圖片描述

2.4 數據的有效性:
I2C總線進行數據傳送時,時鐘信號爲高電平期間,數據線上的數據必須保持穩定,只有在時鐘線上的信號爲低電平期間,數據線上的高電平或低電平狀態才允許變化。
即:數據在SCL的上升沿到來之前就需準備好。並在在下降沿到來之前必須穩定。
在這裏插入圖片描述
發送數據相關代碼(SCL爲高電平時,發送數據):

void IIC_Send_Byte(u8 txd)
{                        
 u8 t;   
 SDA_OUT();       
 IIC_SCL=0;                 //SCL爲低電平時,可以改變SDA,以實現數據的有效傳輸//
 for(t=0;t<8;t++)
  {              
   if((txd&0x80)>>7)        //(txd&0x80)>>7值要麼是0要麼是1,IIC是從最高位開始傳輸數據的//
    IIC_SDA=1;              //(txd&0x80)>>7值爲1時,SDA給1//
   Else
    IIC_SDA=0;              //(txd&0x80)>>7值爲0時,SDA給0//
   txd<<=1;    
   delay_us(2);             //對TEA5767這三個延時都是必須的//
   IIC_SCL=1;               //SCL爲高電平時,傳輸SDA//
   delay_us(2); 
   IIC_SCL=0;      
   delay_us(2);
  }       
}            

讀數據相關代碼(SCL爲高電平時,接收數據):

u8 IIC_Read_Byte(unsigned char ack)
{
 unsigned char
 i,receive=0;                   //無符號字符型,對應整數範圍0~255//
 SDA_IN();                      //SDA設置爲輸入//
 for(i=0;i<8;i++)
  {
   IIC_SCL=0; 
   delay_us(2);
   IIC_SCL=1;                   //SCL由低設高後,開始讀信號//
   receive<<=1;                 //先將之前接收的信號左移,最右位補0//
   if(READ_SDA)receive++;       //如果接收SDA爲1,則最右位改爲1//
   delay_us(1);                 //如果接收SDA爲0,則不做操作//
  }                                 
 if (!ack)
  IIC_NAck();                   //發送nACK//
 else
  IIC_Ack();                    //發送ACK//  
 return receive;
}

2.5 數據傳輸:
在I2C總線上傳送的每一位數據都有一個時鐘脈衝相對應(或同步控制),即在SCL串行時鐘的配合下,在SDA上逐位地串行傳送每一位數據。數據位的傳輸是邊沿觸發。
3. 開發版的硬件連接
如下圖所示:
在這裏插入圖片描述
PB6對應IIC的時鐘線,PB7對應IIC的數據線。
在這裏插入圖片描述
PB6和PB7接在EEPROM(24C02)的SCL和SDA上。
4. EEPROM(24C02)介紹
EEPROM (Electrically Erasable Programmable read only memory)是指帶電可擦可編程只讀存儲器。是一種掉電後數據不丟失的存儲芯片。
24C02是一個2Kbit的串行EEPROM存儲芯片,可存儲256(2K/8)個字節數據。通過I2C總線通訊讀寫芯片數據,通訊時鐘頻率可達400KHz。(備註,1Byte=8bit)
4.1 24C02芯片引腳定義如下圖所示:
在這裏插入圖片描述
4.2 A0-A2的引腳用來設置24C02的地址線,由開發版給的硬件連接圖可知,默認A0、A1、A2跟GND連接。
24C02的設備地址由下圖的第一行所示:
在這裏插入圖片描述
上圖中,24C02地址的前4位爲0b1010,即0xA,後三位由A2、A1、A0決定,即0b000,最後一位根據讀或寫決定,讀至1,寫至0,因此24CO2的地址讀寫地址分別爲:0b10100001(0xA1)和0b10100000(0xA0)。
4.3 24C02字節寫時序/讀時序
在這裏插入圖片描述
24C02寫一個字節數據的驅動程序代碼:

//寫入一個字節,WriteAddr :寫入數據的目的地址;DataToWrite:要寫入的數據//
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite) {                                                                                                                                                
 IIC_Start();  
 if(EE_TYPE>AT24C16)
  {
   IIC_Send_Byte(0XA0);                         //發送寫命令//
   IIC_Wait_Ack();
   IIC_Send_Byte(WriteAddr>>8);                 //發送高地址//
  }
 else
  {
   IIC_Send_Byte(0XA0+((WriteAddr/256)<<1));    //發送器件地址0XA0,寫數據// 
  }
 IIC_Wait_Ack();
 IIC_Send_Byte(WriteAddr%256);                  //發送低地址//
 IIC_Wait_Ack();                                                                                       
 IIC_Send_Byte(DataToWrite);                    //發送字節//                                               
 IIC_Wait_Ack();                            
 IIC_Stop();                                    //產生一個停止條件// 
 delay_ms(10);       
}

在這裏插入圖片描述
24C02讀一個字節數據的驅動程序代碼:

//讀一個字節, ReadAddr:開始讀數的地址,返回值:讀到的數據//
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
 {                              
  u8 temp=0;                                                                                                                     
  IIC_Start();  
  if(EE_TYPE>AT24C16)                         //實際用的是24C02,小於AT24C16,所以直接跳到else//
   {
    IIC_Send_Byte(0XA0);                        //發送寫命令//
    IIC_Wait_Ack();
    IIC_Send_Byte(ReadAddr>>8);               //發送高地址//
    IIC_Wait_Ack();           
   }
  else
   IIC_Send_Byte(0XA0+((ReadAddr/256)<<1));     //發送設備地址0XA0,寫數據,(ReadAddr/256)<<1)值爲0//
  IIC_Wait_Ack(); 
  IIC_Send_Byte(ReadAddr%256);                     //發送低地址//
  IIC_Wait_Ack();          
  IIC_Start();                                         
  IIC_Send_Byte(0XA1);                             //進入接收模式//                       
  IIC_Wait_Ack();       
  temp=IIC_Read_Byte(0);           
  IIC_Stop();                                    //產生一個停止條件//      
  return temp;
 }

4.4 其他24C02驅動程序
4.4.1 初始化AT24CXX函數

void AT24CXX_Init(void)
 {
  IIC_Init();
 }

4.4.2 檢查AT24C02是否正常函數

u8 AT24CXX_Check(void)
 {
  u8 temp;
  temp=AT24CXX_ReadOneByte(255);            //避免每次開機都寫AT24CXX//                        
  if(temp==0X55)return 0;                                //返回0說明正常//
  else                                       //排除第一次初始化的情況//
   {
    AT24CXX_WriteOneByte(255,0X55);
    temp=AT24CXX_ReadOneByte(255);   
    if(temp==0X55)return 0;
   }
  return 1;                                                                          
 }

4.4.3 發送一段數據函數

//WriteAddr :開始寫入的地址 對24c02爲0~255,pBuffer :數據數組首地址,NumToWrite:要寫入數據的個數//
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
 {
  while(NumToWrite--)
   {
    AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
    WriteAddr++;
    pBuffer++;
   }
 }

4.4.4 讀取一段數據函數

// ReadAddr :開始讀出的地址對24c02爲0~255,pBuffer  :數據數組首地址,NumToRead:要讀出數據的個數//
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
 {
  while(NumToRead)
   {
    *pBuffer++=AT24CXX_ReadOneByte(ReadAddr++); 
    NumToRead--;
   }
 }  

5. 實驗相關代碼解讀
5.1 myiic.h頭文件代碼解讀

#define __MYIIC_H
#include "sys.h"
#define SDA_IN()  {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}   //上拉輸入模式//
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}   //推輓輸出,50MHz//  
#define IIC_SCL    PBout(6) //SCL//
#define IIC_SDA    PBout(7) //SDA//  
#define READ_SDA   PBin(7)  //輸入SDA// 
//手把手編寫IIC所有操作函數//
void IIC_Init(void);                //初始化IIC的IO口//     
void IIC_Start(void);               //發送IIC開始信號//
void IIC_Stop(void);                //發送IIC停止信號//
void IIC_Send_Byte(u8 txd);         //IIC發送一個字節//
u8 IIC_Read_Byte(unsigned char ack);//IIC讀取一個字節//
u8 IIC_Wait_Ack(void);              //IIC等待ACK信號//
void IIC_Ack(void);                 //IIC發送ACK信號//
void IIC_NAck(void);                //IIC不發送ACK信號//
void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data);
u8 IIC_Read_One_Byte(u8 daddr,u8 addr);   
#endif

5.2 myiic.c文件代碼解讀

#include "myiic.h"
#include "delay.h"
//編寫初始化IIC函數//
void IIC_Init(void)
{          
 GPIO_InitTypeDef GPIO_InitStructure;
 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE ); //使能GPIOB時鐘//   
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;       //推輓輸出//
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(GPIOB, &GPIO_InitStructure);
 GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);              //PB6,PB7 輸出高//
}
//編寫IIC起始信號函數//
void IIC_Start(void)
{
 SDA_OUT();              //SDA線設爲輸出//
 IIC_SDA=1;              //SDA輸出高//     
 IIC_SCL=1;              //SCL輸出高,此時空間狀態//
 delay_us(4);
 IIC_SDA=0;              //SDA輸出低,IIC開始// 
 delay_us(4);
 IIC_SCL=0;              //鉗住I2C總線,準備發送或接收數據// 
}   
//編寫IIC停止信號函數//
void IIC_Stop(void)
{
 SDA_OUT();              //SDA線設爲輸出//
 IIC_SCL=0;              //SCL輸出低//
 IIC_SDA=0;              //SDA輸出低//
 delay_us(4);
 IIC_SCL=1;              //SCL輸出高//
 IIC_SDA=1;              //SDA輸出高,發送I2C總線結束信號//
 delay_us(4);           
}
//編寫IIC等待應答信號函數,返回值:1,接收應答失敗;0,接收應答成功//
u8 IIC_Wait_Ack(void)
{
 u8 ucErrTime=0;
 SDA_IN();               //SDA設置爲輸入//  
 IIC_SDA=1;delay_us(1);  //SDA設高電平//   
 IIC_SCL=1;delay_us(1);  //SCL設高電平//
 while(READ_SDA)         //讀取PB7的值,0或1//
 {
  ucErrTime++;
  if(ucErrTime>250)
  {
   IIC_Stop();
   return 1;              //失敗//
  }
 }
 IIC_SCL=0;               //當PB7值爲0,則SCL時鐘輸出低電平,0//     
 return 0;                //成功//
} 
//編寫IIC產生ACK應答函數//
void IIC_Ack(void)
{
 IIC_SCL=0;
 SDA_OUT();
 IIC_SDA=0;
 delay_us(2);
 IIC_SCL=1;
 delay_us(2);
 IIC_SCL=0;
}
//編寫IIC不產生ACK應答函數//      
void IIC_NAck(void)
{
 IIC_SCL=0;
 SDA_OUT();
 IIC_SDA=1;
 delay_us(2);
 IIC_SCL=1;
 delay_us(2);
 IIC_SCL=0;
}               
//編寫IIC發送一個字節函數,1,有應答;0,無應答//     
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
   SDA_OUT();      
    IIC_SCL=0;               //SCL爲低電平時,可以改變SDA,以實現數據的有效傳輸//
    for(t=0;t<8;t++)
    {              
    //IIC_SDA=(txd&0x80)>>7;
  if((txd&0x80)>>7)         //(txd&0x80)>>7值要麼是0要麼是1,IIC是從最高位開始傳輸數據的//
   IIC_SDA=1;               //(txd&0x80)>>7值爲1時,SDA給1//
  else
   IIC_SDA=0;               //(txd&0x80)>>7值爲0時,SDA給0//
  txd<<=1;    
  delay_us(2);              //對TEA5767這三個延時都是必須的
  IIC_SCL=1;                //SCL爲高電平時,傳輸SDA//
  delay_us(2); 
  IIC_SCL=0; 
  delay_us(2);
    }  
}      
//讀1個字節,ack=1時,發送ACK,ack=0,發送nACK   
u8 IIC_Read_Byte(unsigned char ack)
{
 unsigned char i,receive=0;     //無符號字符型,對應整數範圍0~255//
 SDA_IN();//SDA設置爲輸入
    for(i=0;i<8;i++ )
 {
    IIC_SCL=0; 
    delay_us(2);
    IIC_SCL=1;                  //SCL由低設高後,開始讀信號//
    receive<<=1;                //先將之前接收的信號左移,最右位補0//
    if(READ_SDA)receive++;      //如果接收SDA爲1,則最右位改爲1//
    delay_us(1);                //如果接收SDA爲0,則不做操作//
    }      
    if (!ack)
        IIC_NAck();             //發送nACK
    else
        IIC_Ack();              //發送ACK   
    return receive;
}

5.3 main.c文件代碼解讀

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
#include "24cxx.h"  
//要寫入到24c02的字符串數組
const u8 TEXT_Buffer[]={"這是從24C02讀取的數據"};
#define SIZE sizeof(TEXT_Buffer)  
int main(void)
 {  
  u8 key;
  u16 i=0;
  u8 datatemp[SIZE];
  delay_init();           //延時函數初始化//   
  uart_init(115200);      //串口初始化爲115200//
  LED_Init();             //初始化與LED連接的硬件接口//
  KEY_Init();       
  AT24CXX_Init();         //IIC初始化// 
  while(AT24CXX_Check())  //檢測24C02//
 {
  printf("檢測不到值\n");                 
  LED0=!LED0;             //DS0閃爍//
  LED1=!LED1;             //DS1閃爍//
  delay_ms(100);
 }
 while(1)
 {
  key=KEY_Scan(0);
  if(key==KEY1_PRES)     //KEY_UP按下,寫入24C02數據//
   {
    AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE);
    printf("向24C02寫入數據\n");
   }
  if(key==KEY0_PRES)     //KEY0按下,讀取字符串並顯示
   {
    AT24CXX_Read(0,datatemp,SIZE);
    printf("從24C02讀取數據,內容爲:%s\n",datatemp);
   }
  i++;
  delay_ms(10);
  if(i==20)
  {
   LED0=!LED0;           //提示系統正在運行 
   i=0;
  }     
 }
}

6. 實驗結果
實驗結果如下圖所示:
在這裏插入圖片描述
舊知識點
1)複習如何新建工程模板,可參考STM32學習心得二:新建工程模板
2)複習基於庫函數的初始化函數的一般格式,可參考STM32學習心得三:GPIO實驗-基於庫函數
3)複習寄存器地址,可參考STM32學習心得四:GPIO實驗-基於寄存器
4)複習位操作,可參考STM32學習心得五:GPIO實驗-基於位操作
5)複習寄存器地址名稱映射,可參考STM32學習心得六:相關C語言學習及寄存器地址名稱映射解讀
6)複習時鐘系統框圖,可參考STM32學習心得七:STM32時鐘系統框圖解讀及相關函數
7)複習延遲函數,可參考STM32學習心得九:Systick滴答定時器和延時函數解讀
8)複習ST-LINK仿真器的參數配置,可參考STM32學習心得十:在Keil MDK軟件中配置ST-LINK仿真器
9)複習ST-LINK調試方法,可參考STM32學習心得十一:ST-LINK調試原理+軟硬件仿真調試方法
10)複習串口通信相關知識,可參考STM32學習心得十四:串口通信相關知識及配置方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章