一、概述
(1)背景
- I2C(IIC,Inter-Integrated Circuit)總線是由Philips公司開發的一種簡單、雙向二線制同步串行總線。
- 它只需要兩根線即可在連接於總線上的器件之間傳送信息。
- 主器件用於啟動總線傳送數據,並產生時鍾以開放傳送的器件,此時任何被尋址的器件均被認為是從器件。
- I2C總線簡化了硬件電路PCB布線,降低了系統成本,提高了系統可靠性。因為I2C芯片(如mpu6050、ft5x06等)除了這兩根線和少量中斷線,與系統再沒有連接的線,用戶常用IC可以很容易形成標准化和模塊化,便於重復利用。
(2)傳輸方向
- 在總線上主和從、發和收的關系不是恆定的,而取決於此時數據傳送方向。
- 如果主機要發送數據給從器件,則主機首先尋址從器件,然后主動發送數據至從器件,最后由主機終止數據傳送。
- 如果主機要接收從器件的數據,首先由主器件尋址從器件.然后主機接收從器件發送的數據,最后由主機終止接收過程。在這種情況下,主機負責產生定時時鍾和終止數據傳送。
(3)速度
- 連接到相同總線上的IC數量只受總線最大電容的限制,串行的8位雙向數據傳輸位速率在標准模式下可達100Kbit/s,快速模式下可達400Kbit/s,高速模式下可達3.4Mbit/s。
- 總線具有極低的電流消耗.抗高噪聲干擾,增加總線驅動器可以使總線電容擴大10倍,傳輸距離達到15m;兼容不同電壓等級的器件,工作溫度范圍寬。
(4)地址
I2C總線上的每一個設備都可以作為主設備或者從設備,而且每一個設備都會對應一個唯一的地址(地址通過物理接地或者拉高,可以從I2C器件的數據手冊得知,如AT24C02芯片,7位地址依次1010xxx, 最低三位可配,如果全部物理接地,則該設備地址為0x50),主從設備之間就通過這個地址來確定與哪個器件進行通信,在通常的應用中,我們把STM32作為主設備,把掛接在總線上的其他設備都作為從設備。
二、AT240C02 EEPROM介紹
(1)特點
- 寬范圍的工作電壓1.8V~5.5V低電壓技術
- 1mA典型工作電流- 1uA典型待機電流·存儲器組織結構
- 24CO2,256 X8(2K bits)- 24CO4,512×8(4K bits)- 24C08,1024 × 8 (8K bits)-24C16,2048 ×8(16K bits)-24C32,4096 X8(32K bits)- 24C64,8192 × 8(64K bits)
- 2線串行接口,完全兼容l2C總線
- I2C時鍾頻率為1 MHz (5V),400 kHz (1.8V,2.5V,2.7V)施密特觸發輸入噪聲抑制
- 硬件數據寫保護
- 內部寫周期(最大5 ms)可按字節寫
- 頁寫:8字節頁(24C02),16字節頁(24CO4/08/16),32字節頁(24C32/64)可按字節,隨機和序列讀
- 自動遞增地址
- 高可靠性擦寫壽命:100萬次-數據保持時間:100年
有規格書可知,EEPROM的讀寫速率是100KHZ.
(2)引腳說明
AT24C02芯片,7位地址依次1010xxx, 最低三位(A0~A2)可配,由原理圖可知三個腳物理接地,地址為1010000,即該設備地址為0x50)。
(3)起始和停止時序
數據和時鍾線都為高則稱總線處在空閑狀態。當SCL為高電平時SDA的下降沿(高到低叫做起始條件(START,簡寫為S),SDA的上升沿(低到高)則叫做停止條件(STOP,簡寫為P)。參見圖5。
(4)數據有效性
SCL高電平時SDA數據有效,低電平時SDA數據交換,數據變換為高電平或者低電平。
(5)應答信號:
三、讀寫操作
1、寫操作
(1).字節寫
寫操作要求在接收器件地址和ACK應答后,接收8位的字地址。接收到這個地址后EEPROM應答"0",然后是一個8位數據。在接收8位數據后EEPROM應答"0",接着必須由主器件發送停止條件來終止寫序列。
此時EEPROM進入內部寫周期twR,數據寫入非易失性存儲器中,在此期間所有輸入都無效。直到寫周期完成,EEPROM才會有應答(見圖9)。
(2)頁寫
- 24C02器件按8字節/頁執行頁寫,24C04/08/16器件按16字節/頁執行頁寫,24C32/64器件按32字節/頁執行頁寫。
- 頁寫初始化與字節寫相同,只是主器件不會在第一個數據后發送停止條件,而是在EEPROM的ACK以后,接着發送7個(24C02數據。EEPROM收到每個數據后都應答“0”。最后仍需由主器件發送停止條件,終止寫序列(見圖10)。接收到每個數據后,字地址的低3位(24C02)內部自動加1,高位地址位不變,維持在當前頁內。當內部產生的字地址達到該頁邊界地址時,隨后的數據將寫入該頁的頁首。如果超過8個(24C02)數據傳送給了EEPROM,字地址將回轉到該頁的首字節,先前的字節將會被覆蓋。
MCU向AT24CT02寫數據時,MCU為主機,EEPROM為從機。由上圖可知:
- 主機通過SDA先發一個起始信號。
- 發設備地址尋找設備,並設置是寫入還是讀取。
- 從機回一個Ack應答信號。
- 主機發送要寫入的EEPROM片內地址。
- 從機回Ack應答信號。
- 主機寫數據到從機,每寫一byte,從機回發一個ack應答。
- 主機發送停止信號,結束數據寫入。
(3)代碼分析:
#include "iic.h" /*軟件模擬IIC寫數據到EEPROM*/ GPIO_InitTypeDef GPIO_InitStructure; #define SDA_OUT PBout(9) #define SDA_IN PBin(9) #define SCL_OUT PBout(8) //初始化SDL、SCL GPIO void Iic_AT24C02_Init(void) { //1.初始化時鍾 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //2.初始化硬件 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;//PB8 PB9 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//輸出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); //空閑狀態高電平 SDA_OUT = 1; SCL_OUT = 1; } //設置引腳模式 void sda_pin_mode(GPIOMode_TypeDef mode) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//PB9 GPIO_InitStructure.GPIO_Mode = mode;//模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); } //IIC起始信號 void iic_start(void) { //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); //保證開始是一個高電平 SDA_OUT = 1; SCL_OUT = 1; delay_us(5); //AT24C02的讀寫速度為100kHZ,一個周期為10us. SDA_OUT = 0;//時鍾線為高電平期間,數據線由高到低 delay_us(5); SCL_OUT = 0;//高速總線上的從機,有通信的設備 delay_us(5); } //發送數據 void iic_send_byte(uint8_t d)// 10101110 { int32_t i; //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); for(i=7;i>=0;i--) { if(d & (0x1<<i)) SDA_OUT = 1; else SDA_OUT = 0; delay_us(5); SCL_OUT = 1; delay_us(5);//當前數據是可靠的,告訴從機可以讀取數據 SCL_OUT = 0; delay_us(5);//當前數據是不可靠的,正在准備 } } //應答信號 uint8_t iic_wait_ack(void) { uint8_t ack = 0; //保證SDA引腳為輸入模式 sda_pin_mode(GPIO_Mode_IN); SCL_OUT = 1; delay_us(5);//當前數據是可靠的,主機可以訪問 if(SDA_IN == 0) ack = 0;//有應答 要 else ack = 1;//無應答 不要了 SCL_OUT = 0; delay_us(5); return ack; }
void iic_stop(void) { //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1;//時鍾線為高電平期間,數據線由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //發送起始信號 iic_start(); //設備尋址,設備寫訪問地址為 10100000 = 0xA0 iic_send_byte(0xA0); //等待應答 ack = iic_wait_ack(); if(ack) { printf("devices address failed!\r\n"); return -1; } //告訴從機我要訪問的數據存儲地址 iic_send_byte(word_addr); //等待應答 ack = iic_wait_ack(); if(ack) { printf("word address failed!\r\n"); return -2; } //連續寫數據 while(len--) { //寫入數據 iic_send_byte(*p); //等待應答 ack = iic_wait_ack(); if(ack) { printf("data failed!\r\n"); return -3; } p++; } //發送停止信號 iic_stop(); printf("write success!\r\n"); return 0; }
2、讀操作
讀操作與寫操作初始化相同,只是器件地址中的讀/寫選擇位應為"1"。有三種不同的讀操作方式:當前地址讀,隨機讀和順序讀。
(1)當前地址讀
- 內部地址計數器保存着上次訪問時最后一個地址加1的值。只要芯片有電,該地址就一直保存。當讀到最后頁的最后字節,地址會回轉到0;當寫到某頁尾的最后一個字節,地址會回轉到該頁的首字節。
- 接收器件地址(讀/寫選擇位為"1")、EEPROM應答ACK后,當前地址的數據就隨時鍾送出。主器件無需應答"O",但需發送停止條件(見圖12)。
2.隨機讀
隨機讀需先寫一個目標字地址,一旦EEPROM接收器件地址和字地址並應答了ACK,主器件就產生一個重復的起始條件。
然后,主器件發送器件地址(讀/寫選擇位為"1"),EEPROM應答ACK,並隨時鍾送出數據。主器件無需應答"O",但需發送停止條件(見圖13)。
- 主機通過SDA先發一個起始信號。
- 發設備地址尋找設備,並設置是寫入(因為要寫入讀取數據的地址)。
- 寫入要讀取的地址
- 從機回一個Ack應答信號。
- 主機再發一個起始信號開始讀
- 主機發送要寫入的EEPROM片內地址(讀寫位設置為讀)。
- 讀數據。
- 主機發送停止信號,結束數據讀取。
3.順序讀
順序讀可以通過“當前地址讀"或“隨機讀"啟動。主器件接收到一個數據后,應答ACK。只要EEPROM接收到ACK,將自動增加字地址並繼續隨時鍾發送后面的數據。若達到存儲器地址末尾,地址自動回轉到0,仍可繼續順序讀取數據。
主器件不應答"0",而發送停止條件,即可結束順序讀操作(見圖14)。
#include "iic.h" GPIO_InitTypeDef GPIO_InitStructure; void Iic_AT24C02_Init(void) { //1.初始化時鍾 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //2.初始化硬件 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8;//PB8 PB9 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//輸出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); //空閑狀態高電平 SDA_OUT = 1; SCL_OUT = 1; } void sda_pin_mode(GPIOMode_TypeDef mode) { GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//PB9 GPIO_InitStructure.GPIO_Mode = mode;//模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽輸出 GPIO_InitStructure.GPIO_Speed = GPIO_Fast_Speed;//速度 快速 25MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOB,&GPIO_InitStructure); } void iic_start(void) { //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); //保證開始是一個高電平 SDA_OUT = 1; SCL_OUT = 1; delay_us(5); SDA_OUT = 0;//時鍾線為高電平期間,數據線由高到低 delay_us(5); SCL_OUT = 0;//高速總線上的從機,有通信的設備 delay_us(5); } void iic_send_byte(uint8_t d)// 10101110 { int32_t i; //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5); for(i=7;i>=0;i--) { if(d & (0x1<<i)) SDA_OUT = 1; else SDA_OUT = 0; delay_us(5); SCL_OUT = 1; delay_us(5);//當前數據是可靠的,告訴從機可以讀取數據 SCL_OUT = 0; delay_us(5);//當前數據是不可靠的,正在准備 } } uint8_t iic_recv_byte(void) { int8_t i; uint8_t data; //保證SDA引腳為輸入模式 sda_pin_mode(GPIO_Mode_IN); //當前數據不可靠,切換數據 SCL_OUT = 0; delay_us(5); for(i=7;i>=0;i--)//MSB 10101111 { SCL_OUT = 1; delay_us(5);//當前數據可靠,讀把 if(SDA_IN == 1) { data |= (0x1<<i); } else { data &= ~(0x1<<i); } SCL_OUT = 0; delay_us(5);//當前數據不可靠,切換數據 } return data; } void iic_ack(uint8_t ack) { //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 0; delay_us(5);//當前數據不可靠 //發送高/低電平 SDA_OUT = ack; delay_us(5);//准備數據 SCL_OUT = 1; delay_us(5);//當前數據可靠,然后從機可以訪問 SCL_OUT = 0; delay_us(5);//當前數據bu可靠,切換數據 } uint8_t iic_wait_ack(void) { uint8_t ack = 0; //保證SDA引腳為輸入模式 sda_pin_mode(GPIO_Mode_IN); SCL_OUT = 1; delay_us(5);//當前數據是可靠的,主機可以訪問 if(SDA_IN == 0) ack = 0;//有應答 要 else ack = 1;//無應答 不要了 SCL_OUT = 0; delay_us(5); return ack; } void iic_stop(void) { //保證SDA引腳為輸出模式 sda_pin_mode(GPIO_Mode_OUT); SDA_OUT = 0; SCL_OUT = 1; delay_us(5); SDA_OUT = 1;//時鍾線為高電平期間,數據線由低到高 delay_us(5); } int32_t at24c02_write(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //發送起始信號 iic_start(); //設備尋址,設備寫訪問地址為 10100000 = 0xA0 iic_send_byte(0xA0); //等待應答 ack = iic_wait_ack(); if(ack) { printf("devices address failed!\r\n"); return -1; } //告訴從機我要訪問的數據存儲地址 iic_send_byte(word_addr); //等待應答 ack = iic_wait_ack(); if(ack) { printf("word address failed!\r\n"); return -2; } //連續寫數據 while(len--) { //寫入數據 iic_send_byte(*p); //等待應答 ack = iic_wait_ack(); if(ack) { printf("data failed!\r\n"); return -3; } p++; } //發送停止信號 iic_stop(); printf("write success!\r\n"); return 0; } int32_t at24c02_read(uint8_t word_addr,uint8_t *buf,uint8_t len) { uint8_t ack = 0; uint8_t *p = buf; //發送開始信號 iic_start(); //設備尋址,設備寫訪問地址 0xA0 iic_send_byte(0xA0); //等待應答 ack = iic_wait_ack(); if(ack) //1 無應答 0 應答 { printf("devices address failed!\r\n"); return -1; } //告訴從機我要訪問的數據存儲地址 iic_send_byte(word_addr); //等待應答 ack = iic_wait_ack(); if(ack) //1 無應答 0 應答 { printf("word address failed!\r\n"); return -2; } //再次發送開始信號 iic_start(); //設備尋址,設備讀訪問地址 0xA1 iic_send_byte(0xA1); //等待應答 ack = iic_wait_ack(); if(ack) //1 無應答 0 應答 { printf("devices address failed!\r\n"); return -3; } //連續接收數據 len = len - 1; while(len--) { *p++ = iic_recv_byte(); //發送應答 iic_ack(0); } //接收最后一個字節 *p = iic_recv_byte(); //發送不應答 iic_ack(1); //發送停止信號 iic_stop(); printf("read success!\r\n"); return 0; }