STM32 使用IO口模擬I2C時序

上一篇《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協議詳解

下一篇:《》

回目錄:《目錄

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