I2C软件实现

软件模拟I2C实现的几个重点:

  1. 主机初始化时将SCL和SDA都初始化为输出高电平(两个都是高表明空闲状态,I2C器件是一个被动器件,主机不通过总线控制器件,器件就不会主动变更总线上的电平状态)。
  2. 主机应该将SDA线初始化为开漏极输出模式,防止“线与”。
  3. 主机发送数据时,SCL为低电平的时候才能改变SDA的电平状态,SCL为高电平时要确保SDA的电平状态稳定。
  4. 主机接收数据时,SCL先拉低一段时间让器件有足够的时间去改变SDA上的电平状态,然后主机拉高SCL后再去读取SDA的电平状态。
  5. 数据传输必须是一个字节一个字节的传输,传输时高bit位在前。
  6. 每个字节传输之后必须发送一个应答位(0表示继续发送也就是所谓的ACK,1表示不再发送也就是所谓的NACK)。
  7. 不同的器件速度不一样,所以要充分控制器件的延时(我这里的代码没有把延时逻辑提取到handle结构体里面去,写死的)。

疑问:

我在I2C规格书里面看到说:

“If a slave cannot receive or transmit another complete byte of data until it has performed some other function, for example servicing an internal interrupt, it can hold the clock line SCL LOW to force the master into a wait state.”

难道器件这边也能将SCL拉低,那这样的话主机岂不是也要将SCL初始化为开漏极输出模式?

如果将SCL初始化为开漏输出,那么SCL怎么有效检测到器件那边是不是强制拉低了SCL呢?

如果器件给SCL输出一个低电平,主机同时也输出一个高电平,咋整呢?

如下代码是实测有效的(I2C的时序图在下面)。

/* I2C的SDA引脚要配置成开漏输出,因为会发生“线与” */

/* 是高电平还是低电平 */
typedef enum {
    GPIO_LEVEL_LOW = 0,
    GPIO_LEVEL_HIGH = 1,
} GPIOLevel;


/* 输入或是输出模式 */
typedef enum {
    GPIO_Mode_OUT = 0,
    GPIO_Mode_IN = 1,
} GPIODirect;

/* 根据芯片/平台有所不同,我这里自己封装的 */
typedef struct {
    uint8_t    port:4; // GPIOA=0;GPIOB=1;GPIOC=2...
    uint8_t    pin:4; // PIN0=0;PIN1=1;PIN2=2...
} GPIO;

typedef struct iic_gpio {
    GPIO     scl;
    GPIO     sda;
} IICGPIO;

typedef struct i2c_handle {
    uint8_t         devWrAdr;
    uint8_t         devRdAdr;
    IICGPIO         gpio;
} IICHandle;

typedef enum iic_ack {
    IIC_ACK     = 0,
    IIC_NACK    = 1,
} IICACK;


#if 1 /* 这一段里面的代码,需要修改成具体芯片/平台对应的,我这里是stm32f411 */
/* GPIO口方向变更,待后面改成直接操作寄存器,避免使用函数 */
static inline void __gpio_change_direct(const GPIO *gpio, GPIODirect dirout)
{
    GPIO_InitTypeDef gpioInitData;
    gpioInitData.GPIO_Pin = 0x01 << gpio->pin;
    gpioInitData.GPIO_Mode = (GPIO_DIR_OUT == dirout) ? GPIO_Mode_OUT : GPIO_Mode_IN;
    gpioInitData.GPIO_Speed = GPIO_High_Speed;
    gpioInitData.GPIO_OType = GPIO_OType_OD;
    gpioInitData.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(((GPIO_TypeDef*)(AHB1PERIPH_BASE + 0x400 * (gpio->port))), &gpioInitData);
}

/* 微秒级延时,根据系统主频有所不同  */
static inline void __delay_us(uint32_t us)
{
    uint32_t n;
    
    while(us--)
        for(n=0;n<23;n++);
}

static inline GPIOLevel __gpio_read_level(const GPIO *gpio)
{
    if (Bit_RESET == GPIO_ReadInputDataBit(((GPIO_TypeDef*)(AHB1PERIPH_BASE + 0x400 * (gpio->port))), 0x01 << gpio->pin))
        return GPIO_LEVEL_LOW;

    return GPIO_LEVEL_HIGH;
}
#endif


/* 从 GPIO_SetBits / GPIO_ResetBits 拷贝过来,循环中调用函数没必要,直接这样好 */
#define GPIO_OUTPUT_HIGH(P)    ((GPIO_TypeDef*)(AHB1PERIPH_BASE + 0x400 * ((P).port)))->BSRRL = 0x01 << ((P).pin)
#define GPIO_OUTPUT_LOW(P)     ((GPIO_TypeDef*)(AHB1PERIPH_BASE + 0x400 * ((P).port)))->BSRRH = 0x01 << ((P).pin)
#define GPIO_CHANGE_DIROUT(P)  __gpio_change_direct(P, GPIO_DIR_OUT)
#define GPIO_CHANGE_DIRIN(P)   __gpio_change_direct(P, GPIO_DIR_IN)
#define GPIO_READ_LEVEL(P)     __gpio_read_level(P)
#define DELAY_US(u)            __delay_us(u)


/* name:
    i2c_read_ack
   feature:
    读取slave的ACK电平
    本函数执行完毕后 SCL 和 SDA 都输出低电平。
   paramters:
    <handle>
   return:
    返回slave的应答状态电平(ACK低/NACK高)
*/
extern IICACK i2c_read_ack(const IICHandle *handle)
{
    uint8_t level = GPIO_LEVEL_HIGH, n = 0;

    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    GPIO_CHANGE_DIRIN(&handle->gpio.sda);
    DELAY_US(2);
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    while(GPIO_LEVEL_HIGH == (level = GPIO_READ_LEVEL(&handle->gpio.sda)))
    {
        n++;
        if (n >= 30)
            break;
    }
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_CHANGE_DIROUT(&handle->gpio.sda);
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(2);
    return (GPIO_LEVEL_HIGH == level) ? IIC_NACK : IIC_ACK;
}

/* name:
    i2c_send_byte
   feature:
    发送一个字节的数据
    本函数执行完毕后 SCL 为低电平,SDA为master输出方向且输出电平为已发送字节的最低bit位对应的电平。
   paramters:
    <handle>
    <b> 需要发送的字节
   return:
    无
*/
extern void i2c_send_byte(const IICHandle *handle, uint8_t b)
{
    for (int32_t i=0; i<8; i++)
    {
        GPIO_OUTPUT_LOW(handle->gpio.scl);
        if (0x80 & (b<<i))
        {
            GPIO_OUTPUT_HIGH(handle->gpio.sda);
        }
        else
        {
            GPIO_OUTPUT_LOW(handle->gpio.sda);
        }
        DELAY_US(2);
        GPIO_OUTPUT_HIGH(handle->gpio.scl);
        DELAY_US(2);
    }
}

/* name:
    i2c_send_byte_and_read_ack
   feature:
    发送一个字节的数据并读取对方的响应结果
    本函数执行完毕后 SDA 和 SCL 都输出低电平。
   paramters:
    <handle>
    <b> 需要发送的字节
   return:
    返回 ACK(SDA低电平)表明slave有能力继续处理字节,master可以继续发送字节
    返回NACK(SDA高电平)表明slave没法处理更多字节,master需要暂停发送
*/
extern IICACK i2c_send_byte_and_read_ack(const IICHandle *handle, uint8_t b)
{
    GPIOLevel level;
    int32_t i, n=0;
    // NACK: SDA=1,SCL=1
    // AKC: SDA=0,SCL=1
    
    for (i=0; i<8; i++)
    {
        GPIO_OUTPUT_LOW(handle->gpio.scl);
        if (0x80 & (b<<i))
        {
            GPIO_OUTPUT_HIGH(handle->gpio.sda);
        }
        else
        {
            GPIO_OUTPUT_LOW(handle->gpio.sda);
        }
        DELAY_US(2);
        GPIO_OUTPUT_HIGH(handle->gpio.scl);
        DELAY_US(2);
    }

    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    GPIO_CHANGE_DIRIN(&handle->gpio.sda);
    DELAY_US(2);
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    while(GPIO_LEVEL_HIGH == (level = GPIO_READ_LEVEL(&handle->gpio.sda)))
    {
        n++;
        if (n >= 30)
            break;
    }
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_CHANGE_DIROUT(&handle->gpio.sda);
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(2);
    return (GPIO_LEVEL_HIGH == level) ? IIC_NACK : IIC_ACK;
}

/* name:
    i2c_recv_byte
   feature:
    接收一个来自slave的字节数据
    本函数执行完毕后 SDA 和 SCL 都输出低电平。
   paramters:
    <handle>
   return:
    返回已接收到的字节
*/
extern uint8_t i2c_recv_byte(const IICHandle *handle)
{
    uint8_t b = 0;

    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    GPIO_CHANGE_DIROUT(&handle->gpio.sda);
    for (int32_t i=0; i<8; i++)
    {
        GPIO_OUTPUT_LOW(handle->gpio.scl);
        DELAY_US(2);
        GPIO_OUTPUT_HIGH(handle->gpio.scl);
        DELAY_US(2);
        if (GPIO_LEVEL_HIGH == GPIO_READ_LEVEL(&handle->gpio.sda))
        {
            b |= 0x80 >> i;
        }
    }
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_CHANGE_DIROUT(&handle->gpio.sda);
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(2);
    return b;
}

/* name:
    i2c_recv_byte_and_send_ack
   feature:
    接收一个来自slave的字节数据并给出相应电平。
    本函数执行完毕后 SCL 和 SDA 都输出低电平。
   paramters:
    <handle>
    <b> 需要发送的字节
   return:
    返回已接收到的字节
*/
extern uint8_t i2c_recv_byte_and_send_ack(const IICHandle *handle, IICACK rtype)
{
    int16_t b = 0;

    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    GPIO_CHANGE_DIROUT(&handle->gpio.sda);
    for (int32_t i=0; i<8; i++)
    {
        GPIO_OUTPUT_LOW(handle->gpio.scl);
        DELAY_US(2);
        GPIO_OUTPUT_HIGH(handle->gpio.scl);
        DELAY_US(2);
        if (GPIO_LEVEL_HIGH == GPIO_READ_LEVEL(&handle->gpio.sda))
        {
            b |= 0x80 >> i;
        }
    }
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_CHANGE_DIROUT(&handle->gpio.sda);
    // NACK: SDA=1,SCL=1
    // AKC: SDA=0,SCL=1
    if (IIC_NACK == rtype)
        GPIO_OUTPUT_HIGH(handle->gpio.sda);
    else
        GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(2);
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    DELAY_US(2);
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(2);
    return b;
}

/* name:
    i2c_send_ack
   feature:
    从master发出一个ACK信号(低电平)给slave。
    本函数执行完毕后 SCL 和 SDA 都输出低电平。
   paramters:
    <handle>
    <b> 需要发送的字节
   return:
    返回已接收到的字节
*/
extern void i2c_send_ack(const IICHandle *handle)
{
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(2);
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    DELAY_US(2);
    GPIO_OUTPUT_LOW(handle->gpio.scl);
}

/* name:
    i2c_send_nack
   feature:
    从master发出一个NACK信号(高电平)给slave。
    本函数执行完毕后 SCL 输出低电平,SDA 输出高电平。
   paramters:
    <handle>
    <b> 需要发送的字节
   return:
    返回已接收到的字节
*/
extern void i2c_send_nack(const IICHandle *handle)
{
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    DELAY_US(2);
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    DELAY_US(2);
    GPIO_OUTPUT_LOW(handle->gpio.scl);
}

extern int32_t i2c_start(const IICHandle *handle)
{
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    DELAY_US(5);
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    DELAY_US(1);
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    DELAY_US(5);
    return NO_ERROR;
}

extern int32_t i2c_stop(const IICHandle *handle)
{
    GPIO_OUTPUT_LOW(handle->gpio.sda);
    GPIO_OUTPUT_LOW(handle->gpio.scl);
    DELAY_US(1);
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    DELAY_US(5);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
    DELAY_US(5);
    return NO_ERROR;
}

/* name:
    i2c_write_lis2dh12
   feature:
    向IIC器件写入数据,适用于“LIS2DH12”器件
    流程 (Start)
            ->(sendDevWrAdr)->(waitACK)
            ->(sendRegAdr)->(waitACK)
            ->(sendByte)->(waitACK)[->(sendByte)->(waitACK)]{n}
            ->(Stop)
   paramters:
    <handle>
    <reg> 寄存器地址指针
    <data> 数据指针
    <length> 数据长度
    <pWroteSize> 已经写入的数据长度
   return:
    返回 NO_ERROR 表示没有出错,否则 表示出错
*/
extern int32_t i2c_write_lis2dh12(const IICHandle *handle, const uint8_t *reg, const uint8_t *data, uint16_t length, uint16_t *pWroteSize)
{
    BOOL sendFully = TRUE;
    uint16_t wrote = 0;
    
    do {
        i2c_start(handle);
        i2c_send_byte(handle, handle->devWrAdr);
        if (IIC_ACK != i2c_read_ack(handle)) 
        {
            break;
        }
        
        // send register address
        // while(regSize--)
        // {
            //i2c_send_byte(handle, *reg++);
            i2c_send_byte(handle, *reg);
            if (IIC_ACK != i2c_read_ack(handle)) 
            {
                sendFully = FALSE;
                break;
            }
        // }
        // if (!sendFully)
        //     break;

        // send register data
        while(length--)
        {
            i2c_send_byte(handle, *data++);
            if (IIC_ACK != i2c_read_ack(handle)) 
            {
                sendFully = FALSE;
                break;
            }
            wrote ++;
        }
    }while(0);

    if (NULL != pWroteSize)
        *pWroteSize = wrote;

    i2c_stop(handle);
    return sendFully ? NO_ERROR : ERROR_VALUE;
}

/* name:
    i2c_read_lis2dh12
   feature:
    从IIC器件读取数据,适用于“LIS2DH12”器件
    流程 (Start)
            ->(sendDevWrAdr)->(waitACK)
            ->(sendRegAdr)->(waitACK)
            ->(restart)
            ->(sendDevRdAdr)->(waitACK)
            ->(recvByte)->(seedACK)[->(sendByte)->(waitACK)]{n}->(recvByte)->(seedNACK)
            ->(Stop)
   paramters:
    <handle>
    <reg> 寄存器地址指针
    <buffer> 缓冲区首地址
    <bufSize> 欲读取的数据长度,长度必须小于等于缓冲区的长度
    <pReadSize> 已经读取的数据长度
   return:
    返回 NO_ERROR 表示没有出错,否则 表示出错
*/
extern int32_t i2c_read_lis2dh12(const IICHandle *handle, const uint8_t *reg, uint8_t *buffer, uint16_t bufSize, uint16_t *pReadSize)
{
    BOOL sendFully = TRUE;
    uint16_t read = 0;
    
    do {
        i2c_start(handle);
        i2c_send_byte(handle, handle->devWrAdr);
        if (IIC_ACK != i2c_read_ack(handle)) 
        {
            break;
        }
        
        // send register address
        // while(regSize--)
        // {
            //i2c_send_byte(handle, *reg++);
            i2c_send_byte(handle, *reg);
            if (IIC_ACK != i2c_read_ack(handle)) 
            {
                sendFully = FALSE;
                break;
            }
        // }
        // if (!sendFully)
        //     break;

        // restart
        i2c_start(handle);
        i2c_send_byte(handle, handle->devRdAdr);
        if (IIC_ACK != i2c_read_ack(handle)) 
        {
            sendFully = FALSE;
            break;
        }
        
        // send register data
        while(bufSize--)
        {
            *buffer++ = i2c_recv_byte(handle);
            if (0 == bufSize)
            {
                i2c_send_nack(handle);
            }
            else
            {
                i2c_send_ack(handle);
            }
            read ++;
        }

    }while(0);

    if (NULL != pReadSize)
        *pReadSize = read;

    i2c_stop(handle);
    return sendFully ? NO_ERROR : ERROR_VALUE;
}

/* name:
    i2c_init
   feature:
    IIC器件初始化,仅仅是将GPIO口初始化为输出模式并输出高电平(空闲态)
   paramters:
    <handle>
   return:
    void
*/
extern void i2c_init(const IICHandle *handle)
{
    GPIO_InitTypeDef gpioInitData;
    uint32_t port = (0x01 << handle->gpio.scl.port) | (0x01 << handle->gpio.sda.port);  // RCC_AHB1Periph_GPIOB

    RCC_AHB1PeriphClockCmd(port, ENABLE);
    gpioInitData.GPIO_Mode = GPIO_Mode_OUT;
    gpioInitData.GPIO_Speed = GPIO_Fast_Speed;
    gpioInitData.GPIO_PuPd = GPIO_PuPd_UP;

    gpioInitData.GPIO_OType = GPIO_OType_PP;
    gpioInitData.GPIO_Pin = 0x01 << handle->gpio.scl.pin;
    GPIO_Init((GPIO_TypeDef*)(AHB1PERIPH_BASE + 0x400 * (handle->gpio.scl.port)), &gpioInitData); // GPIOB
    // 数据脚要配置成开漏输出,因为master和slave会发生“线与”
    gpioInitData.GPIO_OType = GPIO_OType_OD;
    gpioInitData.GPIO_Pin = 0x01 << handle->gpio.sda.pin;
    GPIO_Init((GPIO_TypeDef*)(AHB1PERIPH_BASE + 0x400 * (handle->gpio.sda.port)), &gpioInitData); // GPIOB
    
    GPIO_OUTPUT_HIGH(handle->gpio.scl);
    GPIO_OUTPUT_HIGH(handle->gpio.sda);
}


void test(void)
{
    const IICHandle mDev_lis2dh12 = {
        .devWrAdr = 0x32,
        .devRdAdr = 0x33,
        .gpio.scl = {PORT_B, 13},
        .gpio.sda = {PORT_B, 15},
    };
    
    const struct {
        uint8_t   reg;
        uint8_t   val;
    } settings[] = {
        {0x1F, 0xC0},
        {0x20, 0x2F},
        {0x21, 0x09},
        {0x22, 0x40},
        {0x23, 0x08},
        {0x24, 0x09},
        {0x25, 0x40},        
        /* 在这一行的上面添加寄存器配置信息 */
        {0x00, 0x00}
    };
    int32_t i=0, r;
    
    i2c_init(&mDev_lis2dh12);
    
    while(0x00 != settings[i].reg)
    {
        r = i2c_write_lis2dh12(&mDev_lis2dh12, &settings[i].reg, &settings[i].val, 1, NULL);
        i ++;
    }
}

 补充些时序图

下面这张图是STM32F4XX规格书里面的截图。

下面这张图是LIS2DH12规格书里面的截图。

下面这张图是I2C规格书里面的截图。

  

如果转载,请注明出处。https://www.cnblogs.com/ssdq/
原文地址:https://www.cnblogs.com/ssdq/p/13187921.html