上一篇《I2C協議詳解》
我們瞭解了I2C的操作流程,這一篇,我們就使用I2C,來對EEPROM進行操作吧。
我們做兩種選擇:
1.時序由IO口模擬高低電平,需要了解協議並按照協議操作相應的IO口。
2.時序由硬件自行產生,不需要人工干預;
由硬件產生的I2C時序,我們藉助Stm32Cube配置實現便可,我們這一篇,拋開Stm32Cube,手撕代碼,根據I2C的時序,一步步地實現I2C對EEPROM的讀寫吧。
我們分爲幾個步驟來對EEPROM進行操作:
1. 發動"看硬件原理圖"技能,確定I2C連線;
2. 發動"乾坤大拷貝"技能,配置對應的IO口;
3. 發動"手撕代碼"技能,寫出相應時序;
1. 發動"看硬件原理圖"技能,確定I2C連線;
第9頁,找到EEPROM那一塊,EEPROM用的是AT24C02,只有2K Bits,不是很大。AT24C02由ATMEL公司生產,其命名規則爲 AT24Cxx,xx可以=02,04,32,64,128,256,512,1M 等等,xx也表示容量,單位是 Bits,注意,是位,不是字節,要換成字節要除以8,所以,AT24C02只有 256 個字節。別問我怎麼知道的,AT24C02的規格書告訴我的,要記得看原理圖,看規格書喔~~~
再找呀找呀找朋友,找到 SCL連到PB6,SDA連到PB9。咋找?搜索唄,搜引腳 I2C1_SCL就行了。
確定了I2C連線,SCL=PB6,SDA=PB9,接下來,我們就
2. 發動"乾坤大拷貝"技能,配置對應的IO口;
先新建兩個文件,io_i2c.c/io_i2c.h,我們就在這裏面寫i2c時序。
並且把文件添加進工程項目裏參與編譯。
配置IO口,詳見《STM32CubeMx 創建第一個工程》,把那段代碼拷貝過來,改一下配置就行了。
void IOI2C_GpioInit(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
// PB6 = SCL/PB9 = SDA
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6|GPIO_PIN_9, GPIO_PIN_SET);
/*Configure GPIO pin : PC7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 記不記得i2c要上拉?
GPIO_InitStruct.Pull = GPIO_PULLUP; //
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
記得在初始化時調用一下它喔:
MX_GPIO_Init();
MX_DMA_Init();
MX_UART4_Init();
MX_TIM1_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
USR_UartInit();
IOI2C_GpioInit(); // 我在這,快看這裏~
/* USER CODE END 2 */
printf("Start System: Io I2C.\r\n");
3. 發動"手撕代碼"技能,寫出相應時序,I2C時序,請參照《I2C協議詳解》。
看圖,會有高電平持續一段時間,低電平持續一段時間的操作,這個持續一段時間,就是延時,關於延時,詳細參照《STM32精準延時》。
這個持續多長時間怎麼算呢?
根據I2C時鐘頻率,可以算出週期,再根據週期,算出持續多長時間。
For Example:
100kHz的頻率,也就是1秒鐘內,有100,000個時鐘週期。
那1個時鐘週期,也就是 1/100,000秒 = 1,000/100,000毫秒 = 1,000,000/100,000微秒,根據小學數學,算出,100kHz頻率的時鐘週期是 10微秒,一高一低一週期,那麼,延時 = 5us。
在《STM32精準延時》篇中,我們做了個us級的精準延時,用上:
#define I2C_Delay USER_Delay1us(5)
如果要其它延時呢?
delay = 1,000,000 / freq / 2 = 500,000 / freq
算出來delay與時鐘頻率的關係:
1us = 500kHz,2us = 250kHz,3us = 166.67kHz,4us = 125kHz,5us = 100kHz,
6us = 83.33kHz,7us = 71.4kHz,8us = 62.5kHz,9us = 55.56kHz,10us = 50kHz。
選一個,但要符合芯片的最高支持速率喔。圖:400k
知道延時,也知道SCL對應的IO口pb6,那麼,要在SCL上生成一個週期爲 10us 的方波,怎麼寫程序呢?
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // 高電平
USER_Delay1us(5); // 持續5us
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // 低電平
USER_Delay1us(5); // 持續5us
我們還可以把IO口拉高拉低寫成宏定義:
#define SwI2cSetScl() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET)
#define SwI2cClrScl() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET)
#define SwI2cCheckScl() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6)
#define SwI2cSetSda() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET)
#define SwI2cClrSda() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET)
#define SwI2cCheckSda() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9)
這樣的好處是,如果後面,你不想用 pb6 和 pb9 作爲 scl 和 sda,你想換兩根io口,只需要改初始化和這些宏定義就行了。
現在,可以開始寫時序了。
a、START: SCL爲高電平,SDA從高電平跳變至低電平,表示開始發送數據:
SwI2cSetSda();
SwI2cSetScl();
I2C_Delay;
SwI2cClrSda();
I2C_Delay;
SwI2cClrScl();
I2C_Delay;
b、TRANSMIT: START之後,就可以發送數據了,數據在SCL高電平有效,發送8-Bit數據(1BYTE),
i2c傳輸數據,是從高位最先傳輸的,比如,0xaa = 10101010b,先傳 bit7=1,接下來再傳bit6=0……最後傳bit0=0:
uint8_t bit_sel;
bit_sel = 0x80;
while (bit_sel) {
if (bit_sel & data)
SwI2cSetSda();
else
SwI2cClrSda();
I2C_Delay;
SwI2cSetScl();
I2C_Delay;
SwI2cClrScl();
bit_sel >>= 1;
}
data&0x80,data&0x40,data&0x20,data&0x10, data&0x08,data&0x04,data&0x02,data&0x01,
這一系列的操作,就把data從bit7取到bit0,按照要求,設置 sda 高低電平,記得,scl一個週期取一次sda值,高電平有效。
3、ACK:在第9個SCL週期,次設備將SDA拉低,主設備檢測SDA爲低電平,表示收到數據;如果在一個時鐘週期內都沒有ACK,說明從設備有問題,主設備必須發STOP,表示傳輸結束。
// ack
SwI2cSetSda(); // 釋放sda線,等待從設備把sda接低
SwI2cSetScl();
I2C_Delay;
ack = SwI2cCheckSda(); // 檢測 sda 電平
SwI2cClrScl();
I2C_Delay;
if(ack) // 從設備在一個時鐘週期內沒有響應。
{
// 發送 stop 信號,傳輸結束
debug_msg("io_i2c not ack. \r\n"); // 打印
return I2CSW_BUSY; // 返回錯誤信息
}
如果繼續有數據要發,重複b、c步驟,如果沒有,發STOP。
d、STOP:SCL爲高電平,SDA從低電平跳變至高電平,表示發送數據結束;
SwI2cClrSda();
SwI2cSetScl();
I2C_Delay;
SwI2cSetSda();
I2C_Delay;
好啦,I2C時序就這樣了,我們接着就用 I2C時序,來對 AT24C02 進行操作。
首先,需要一個設備地址,因爲I2C總線上允許掛多個設備,設備地址,用於識別哪個設備。
看原理圖+AT24C02 datasheet
A2/A1/A0引腳接地,爲000,最低位(LSB)爲低電平是寫,高電平爲讀。
所以,地址爲 0xA0(寫),0xA1(讀)。
根據datasheet描述,AT24C02的操作一般有 字節寫、頁寫、隨機讀、連續讀。
字節寫和頁寫:
字節寫:就是寫一個字節。
START -> 設備地址 -> 寄存器地址(寫eeprom哪個存儲單元) -> 數據 -> STOP
頁寫:就是寫一頁。
START -> 設備地址 -> 寄存器地址(起始) -> 一連串數據 -> STOP
頁寫中的一連串數據,到底是多大的串呢?最大8BYTE,別問我怎麼知道,DATASHEET。
根據描述,頁寫,其實跟字節寫很相似,只不過是,寄存器地址的低3位,會自動增加。
頁寫存儲單元 0x10,8個字節數據,
0x10 = 0001 0000b,寄存器低3位自動增加就是:
0001 0000b = 數據1,0001 0001b = 數據2,……,0001 0111b = 數據8
頁寫存儲單元 0x12,8個字節數據呢?
0x12 = 0001 0010b,寄存器低3位自動增加就是:
0001 0010b = 數據1,0001 0011b = 數據2,……,0001 0111b = 數據5
0001 0000b = 數據6,0001 0001b = 數據7
結合上面的文字描述,存儲單元地址到0001 0111b後,回滾到0001 0000b了,這是需要注意的地方,所以,頁寫,要注意處理頁的邊界問題。
寫的代碼如下:
1、存儲單元地址傳入*pMemAddr和memAddrLen,因爲AT24C32/64/128/256,它的存儲單元地址是16位的,這樣這個函數也用在其它EEPROM上,而不需要再寫一個函數了。
2、這裏暫未對頁寫的頁邊界進行處理,因爲其它EEPROM的頁不是8個字節,頁的處理,留在EEPROM寫的時候做。
3、如果需要寫一個字節,這個函數的最後一個參數 len 寫1就行了。
uint8_t IOI2C_WriteBlock(uint8_t devAddr, uint8_t *pMemAddr, uint8_t memAddrLen, const uint8_t *pData, uint16_t len)
{
uint8_t ctl;
uint8_t startLimit = I2C_TIME_OUT;
//Validate input parameter
if (pData == NULL)
return FALSE;
if (len == 0)
return FALSE;
// I2C Start, Wait while device is busy
ctl = devAddr | I2C_WR;
while (IOI2C_Start(ctl) != IOI2C_OK) {
startLimit--;
if(!startLimit) {
debug_msg("IOI2C_WriteBlock start error.\r\n");
return IOI2C_BUSY;
}
}
// send slave write sub-address
while (memAddrLen--) {
if (IOI2C_TransmitByte(*pMemAddr++) != IOI2C_OK) {
debug_msg("IOI2C_WriteBlock write mem address error.\r\n");
return IOI2C_BUSY;
}
}
// write data from buffer into slave device
while(len--)
{
if(IOI2C_TransmitByte(*pData++) != IOI2C_OK) // Data address
{
debug_msg("IOI2C_WriteBlock write data(%d) error.\r\n", len);
return IOI2C_BUSY;
}
}
IOI2C_Stop(); // end stop operation
return IOI2C_OK;
}
隨機讀和連續讀:
隨機讀,就是隨便讀個字節,讀哪就是哪。
START->設備地址->存儲單元地址->START->設備地址->讀->STOP
連續讀,就是隨便讀n個字節,讀哪塊算哪塊,就是在隨便讀的基礎上不停地讀,直到讀夠了再發STOP
START->設備地址->存儲單元地址->START->設備地址->讀->讀->讀->……->讀->STOP
直接撕代碼:
1、存儲單元地址傳入*pMemAddr和memAddrLen,因爲AT24C32/64/128/256,它的存儲單元地址是16位的,這樣這個函數也用在其它EEPROM上,而不需要再寫一個函數了。
2、既然叫隨便讀,那就不存在着頁邊界的問題了。
3、如果需要讀一個字節,這個函數的最後一個參數 len 寫1就行了。
4、每讀完一個字節,第9個SCL,需要主設備向從設備發ACK,告訴從設備收到,下一個。
uint8_t IOI2C_ReadBlock(uint8_t devAddr, uint8_t *pMemAddr, uint8_t memAddrLen, uint8_t *pData, uint16_t len)
{
uint8_t ctl;
uint8_t startLimit = I2C_TIME_OUT;
//Validate input parameter
if (pData == NULL)
return FALSE;
if (len == 0)
return FALSE;
// I2C Start, Wait while device is busy
ctl = devAddr | I2C_WR;
while (IOI2C_Start(ctl) != IOI2C_OK) {
startLimit--;
if(!startLimit) {
debug_msg("IOI2C_ReadBlock start error.\r\n");
return IOI2C_BUSY;
}
}
// send slave read sub-address
while (memAddrLen--) {
if (IOI2C_TransmitByte(*pMemAddr++) != IOI2C_OK) {
debug_msg("IOI2C_ReadBlock write mem address error.\r\n");
return IOI2C_BUSY;
}
}
// Repeat start for read operation
ctl = devAddr | I2C_RD;
if (IOI2C_Start(ctl) != IOI2C_OK)
{
debug_msg("IOI2C_ReadBlock Repeat Start Error.\r\n");
return IOI2C_BUSY;
}
//Read data from slave device into buffer
while(--len)
{
*pData++ = IOI2C_GetByte(); // Read data into buffer
IOI2C_SendAck(); // send ack to slave
}
*pData = IOI2C_GetByte(); // Read last data byte
IOI2C_Stop(); // end stop operation
return IOI2C_OK;
}
其實寫完這兩個函數,基本上也差不多了,在應用中調用這兩個函數,便可對EEPROM進行讀寫,不過爲了程序可讀性和可移植性,我們再來寫個at24c02.c和at24c02.h,將 at24c32 的讀寫邏輯封裝一下,在這個封裝裏面,我們會處理"頁寫"的回滾邏輯,爲了保證可靠性,我們會多讀或者多寫幾次。
套路:新建文件 -> 加入編譯器,往上看,新建 io_i2c.c 裏有步驟。
開始繼續手撕代碼,我們就實現以下5個函數:
uint8_t AT24C02_ReadByte(uint8_t memAddr)
{
uint8_t data, count=0;
uint8_t result = IOI2C_BUSY;
do {
result = I2C_READBUF(&memAddr, &data, 1);
if(++count > 1)
printf("AT24C04_ReadByte error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
return data;
}
uint8_t AT24C02_WriteByte(uint8_t memAddr, uint8_t data)
{
uint8_t count=0;
uint8_t result = IOI2C_BUSY;
do {
result = I2C_WRITEBUF(&memAddr, &data, 1);
if(++count > 1)
printf("AT24C04_WriteByte error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
return result;
}
uint8_t AT24C02_ReadBuffer(uint8_t memAddr, uint8_t *buff, uint16_t len)
{
uint8_t count=0;
uint8_t result = IOI2C_BUSY;
do {
result = I2C_READBUF(&memAddr, buff, len);
if(++count > 1)
printf("AT24C04_ReadBuffer error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
return result;
}
uint8_t AT24C02_WriteBuffer(uint8_t memAddr, uint8_t *buff, uint16_t len)
{
uint8_t dataCnt, count=0;
uint8_t result = IOI2C_BUSY;
uint8_t pagesize;
// Write buffer of data
while (len) {
// Number of bytes available in current page.
pagesize = PAGE_SIZE - (memAddr % PAGE_SIZE);
// Is current page has enough byte for the buffer.
if (len > pagesize)
{
// Open new page?
if (pagesize == PAGE_SIZE) // No
{
// CASE3: The rest of the buffer is more than a page size.
dataCnt = PAGE_SIZE; // Yes
}
else
{
// CASE2: Current page has not enough byte for the buffer,
// write some data in this page and the rest in other
// page(s).
dataCnt = pagesize; // No
}
}
else
{
// CASE1: Current page has enough byte for the buffer.
// CASE4: Finished up the rest of the buffer.
dataCnt = len; // Yes
}
// Write to EEPROM and make sure success
count = 0;
do {
result = I2C_WRITEBUF(&memAddr, buff, dataCnt);
if(++count > 1)
printf("AT24C04_WriteBuffer error,count = %d \r\n",count);
} while ((count < CHECK_LIMIT) && (IOI2C_OK != result));
buff += dataCnt; // Adjust pointer to B_Count
memAddr += dataCnt; // Adjust register address
len -= dataCnt; // Adjust buffer count
delay_ms(10);
}
return result;
}
void AT24C02_Debug(uint8_t func, uint16_t pa1, uint16_t pa2, uint16_t pa3)
{
uint16_t cnt;
uint8_t temp[256];
printf("AT24C02_Debug: func(%d), pa1(%d), pa2(%d), pa3(%d).\r\n",
func, pa1, pa2, pa3);
switch (func) {
case 0: {
printf("func0: read at24c02 byte: addr(0x%x) data(0x%x).\r\n",
pa1, AT24C02_ReadByte((uint8_t)pa1) );
}
break;
case 1: {
memset(temp, 0, 256);
AT24C02_ReadBuffer((uint8_t)pa1, temp, pa2);
printf("func1: read buffer: addr(0x%x) len(%d).", pa1, pa2);
for (cnt = 0; cnt < (uint8_t)pa2; cnt++) {
if (cnt % 16 == 0)
printf("\r\n");
printf("0x%x, ", temp[cnt]);
}
printf("\r\n");
}
break;
case 2: {
printf("func2: at24c02 write: addr(0x%x) data(0x%x).\r\n", pa1, pa2);
AT24C02_WriteByte((uint8_t)pa1, (uint8_t)pa2);
}
break;
case 3: {
for (cnt = 0; cnt < 256; cnt++)
temp[cnt] = cnt;
printf("func3: write buffer: addr(0x%x) len(%d).\r\n", pa1, pa2);
AT24C02_WriteBuffer((uint8_t)pa1, temp, pa2);
}
break;
default:
printf("func0: read at24c04 byte.\r\n");
printf("func1: read at24c04 buffer.\r\n");
printf("func2: write at24c04 byte.\r\n");
printf("func3: write at24c04 buffer.\r\n");
break;
}
}
好了,在Debug函數裏,調用
static void DebugCmdProceed(void)
{
switch(debArray[0])
{
case DEBCMD_TEST:
printf("DEBCMD_TEST: Parm1: %d / parm2: %d \r\n", debArray[1], debArray[2]);
break;
case DEBCMD_EEPROM:
AT24C02_Debug((uint8_t)debArray[1], debArray[2], debArray[3], debArray[4]);
break;
}
}
編譯、燒錄、運行、連上串口調試助手、執行:
1.、寫字節
@cmd 0x1 2 0 0xac
@cmd 0x1 2 1 0xac
@cmd 0x1 2 2 0xac
2、讀字節
@cmd 0x1 0 0
@cmd 0x1 0 1
@cmd 0x1 0 2
看,讀回的就是寫進去的吧?
3.、寫一串,就往0x13存儲單元寫14個字節吧。
@cmd 0x1 3 0x13 14
4、讀一串,就從0x12讀16個字節吧。
@cmd 0x1 1 0x12 16
看0x13起,14個字節,就是寫入的。
整個工程及代碼呢,請上百度網盤上下載:
鏈接:https://pan.baidu.com/s/19usUcgZPX8cCRTKt_NPcfg
密碼:07on
文件夾:\Stm32CubeMx\Code\IoI2c.rar
本章自己手撕的代碼主要是 io_i2c.c/io_i2c.h 和 at24c02.c/at24c02.h
問題:
上面講到使用延時,編寫I2C的時序,根據計算得出,延時和時鐘頻率的關係是:
1us = 500kHz,2us = 250kHz,3us = 166.67kHz,4us = 125kHz,5us = 100kHz,
6us = 83.33kHz,7us = 71.4kHz,8us = 62.5kHz,9us = 55.56kHz,10us = 50kHz。
那麼,如果,我想要用 400kHz 呢?300kHz呢?懵了吧?
下一篇,我帶你進入,硬件I2C的世界,不管你用 400k,300k都沒有問題。
上一篇:《I2C協議詳解》
下一篇:《》
回目錄:《目錄》