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的规格书告诉我的,要记得看原理图,看规格书喔~~~

 

STM32 使用IO口模拟I2C时序_第1张图片STM32 使用IO口模拟I2C时序_第2张图片

再找呀找呀找朋友,找到 SCL连到PB6,SDA连到PB9。咋找?搜索呗,搜引脚 I2C1_SCL就行了。

STM32 使用IO口模拟I2C时序_第3张图片

确定了I2C连线,SCL=PB6,SDA=PB9,接下来,我们就

 

2. 发动"乾坤大拷贝"技能,配置对应的IO口;

先新建两个文件,io_i2c.c/io_i2c.h,我们就在这里面写i2c时序。

并且把文件添加进工程项目里参与编译。

STM32 使用IO口模拟I2C时序_第4张图片

配置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精准延时》。

STM32 使用IO口模拟I2C时序_第5张图片

这个持续多长时间怎么算呢?

根据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

STM32 使用IO口模拟I2C时序_第6张图片

知道延时,也知道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

STM32 使用IO口模拟I2C时序_第7张图片

A2/A1/A0引脚接地,为000,最低位(LSB)为低电平是写,高电平为读。

所以,地址为 0xA0(写),0xA1(读)。

根据datasheet描述,AT24C02的操作一般有 字节写、页写、随机读、连续读

字节写和页写:

STM32 使用IO口模拟I2C时序_第8张图片

 

STM32 使用IO口模拟I2C时序_第9张图片

字节写:就是写一个字节。

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;

}

随机读和连续读

STM32 使用IO口模拟I2C时序_第10张图片

随机读,就是随便读个字节,读哪就是哪。

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

STM32 使用IO口模拟I2C时序_第11张图片

2、读字节

@cmd 0x1 0 0

@cmd 0x1 0 1

@cmd 0x1 0 2

STM32 使用IO口模拟I2C时序_第12张图片

看,读回的就是写进去的吧?

3.、写一串,就往0x13存储单元写14个字节吧。

@cmd 0x1 3 0x13 14

STM32 使用IO口模拟I2C时序_第13张图片

4、读一串,就从0x12读16个字节吧。

@cmd 0x1 1 0x12 16

STM32 使用IO口模拟I2C时序_第14张图片

看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协议详解》

下一篇:《STM32 使用硬件I2C接口读写EEPROM》

回目录:《目录》

你可能感兴趣的:(单片机)