【裸机开发】I2C 通信接口(三)—— I2C 底层驱动接口实现

目录

一、I2C 初始化

二、产生开始 / 停止信号

1、开始信号

2、重复开始信号

3、停止信号

三、向总线上发送数据(阻塞模式)

四、从总线上读取数据(阻塞模式)

五、整合:数据读写统一调用接口


一、I2C 初始化

初始化步骤如下:

  • 时钟源配置。PERCLK_CLK_ROOT 的时钟源是 66MHz
  • 关闭 I2C。在配置 I2C 相关寄存器时,我们需要先将 I2C 关闭
  • 分频。分频的目的是控制 I2C 的传输速率(如果要控制 I2C 的速率为 100K,可以640 分频)
  • 开启 I2C 

/* i2c 初始化 */
void i2c_init()
{
    // 时钟源的初始化放到了时钟初始化(CCM_init)

    // 关闭I2C
    I2C1_I2CR &= ~(1 << 7);
    // 设置分频值(分频值为 640)
    I2C1_IFDR = 0x15;
    // 打开I2C
    I2C1_I2CR |= (1 << 7);
}

二、产生开始 / 停止信号

1、开始信号

当总线空闲时,总线上的设备可以发送开始信号来占用总线。如果存在多个设备同时占用总线,此时就会发生冲裁。

  • 判断总线是否空闲
  • 设为主机模式,设为发送模式
  • 发送要通信的设备地址和通信方向
/* 产生开始信号 */
i2c_status_t i2c_master_start(unsigned char address, i2c_direction direction)
{
    /* 总线是否被占用 */
    if (is_bus_busy())
    {
        return Status_I2C_Busy;
    }

    /* 
     * 发送开始信号
     * bit 5: 1 主机
     * bit 4: 1 发送
     */
    I2C1_I2CR |= ((1 << 4) | (1 << 5));

    /* 发送通信地址和通信方向 */
    I2C1_I2DR = (((unsigned int)address << 1) | (direction == I2C_Read ? 1 : 0));

    return Status_I2C_Success;
}

2、重复开始信号

当某个设备在通信时,可能会需要临时改变通信方向,此时就可以发送一个重复开始信号来改变通信方向。发送重复开始信号要求总线空闲,且当前设备是主机,只要有一者不满足,就会发送失败

  • 判断主机是否空闲,且当前设备是否为主机模式
  • 发送一个重复开始信号
  • 发送通信地址和通信方向
/* 重复产生开始信号(可能是修改通信方向、通信设备的地址) */
i2c_status_t i2c_master_repeated_start(unsigned char address, i2c_direction direction)
{
    /* 
     * 总线是否被占用
     *  - 如果是其他设备占用总线,直接返回
     *  - 如果是当前设备占用总线,发送一个重复的开始信号
     */
    if (is_bus_busy() && is_master() == 0)
    {
        return Status_I2C_Busy;
    }

    /* 产生一个重复开始信号 */
    I2C1_I2CR |= ((1 << 2) | (1 << 4));

    /* 发送通信地址和通信方向 */
    I2C1_I2DR = (((unsigned int)address << 1U) | (direction & 0x01));

    return Status_I2C_Success;
}

3、停止信号

当主机打算继续通信时,可以发送一个停止信号来结束通信;如果从机不想继续通信,可以选择不回应主机表示想结束通信。

  • 产生一个停止信号
  • 等待总线被释放
/* 产生停止信号 */
i2c_status_t i2c_master_stop()
{
    unsigned short timeout = UINT16_MAX;

    /* 
     * 产生一个停止信号 
     * bit 3: 0 产生一个ACK
     * bit 4: 0 接收(要产生一个ACK,当前设备必须为接收模式)
     * bit 5: 0 从机
     */
    I2C1_I2CR &= ~((1 << 3) | (1 << 4) | (1 << 5));

    /* 等待总线被释放 */
    while (is_bus_busy() && (timeout--));

    // 等待超时
    if (timeout == 0)       
    {
        return Status_I2C_Timeout;
    }
    
    return Status_I2C_Success;
}

三、向总线上发送数据(阻塞模式)

只有当上一次的数据全部发送完,才能开始下一次通信。其本质是,每次传输 8 bit(需要 8 个时钟周期),当第 9 个时钟周期的边沿触发时,我们认为数据传输完毕。

  • 等待 DR 寄存器准备就绪
  • 清除中断、仲裁标志位
  • 状态设为发送状态
  • while 循环逐字节发送
    • 每发送一个字节,都要将数据保存到 DR 寄存器
    • 等待传输完成(传输完成时会触发中断)
    • 清除中断标志位
    • 判断仲裁是否失败(可能存在多主机占用总线的情况)
    • 是否收到 NACK(从机是否想继续通信)
    • 如果遇到其他问题,直接跳出while循环
/* 
 * @description: 阻塞式发送 
 * @param - sendbuf: 保存要发送的数据(允许多字节)
 * @param - len: 发送数据的长度
 * @param - stopflag: 下一次是否还要继续发送
 */
i2c_status_t i2c_master_write_blocking(unsigned char* sendbuf, unsigned int len, unsigned int stopflag)
{
    i2c_status_t i2c_stat = Status_I2C_Success;
    
    /* 等待DR寄存器准备就绪 */
    while (((I2C1_I2SR >> 7) & 0x01) == 0);

    /* 清除中断标志位 */
    clear_intpending_flag();

    /* 状态设为发送 */
    I2C1_I2CR |= (1 << 4);

    /* 逐字节发送 */
    while (len--)
    {
        // 将要发送的数据保存到寄存器
        I2C1_I2DR = *(sendbuf++);

        // 等待传输完成(传输完成时会触发中断)
        while (((I2C1_I2SR >> 1) & 0x01) == 0);

        // 清除中断标志位
        clear_intpending_flag();

        // 仲裁是否失败
        if(((I2C1_I2SR >> 4) & 0x01))
        {
            i2c_stat = Status_I2C_Lost_Arbitration;
        }

        // 是否收到 NACK
        if (((I2C1_I2SR >> 0) & 0x01))
        {
            i2c_stat = Status_I2C_NAK;
        }

        if (i2c_stat != Status_I2C_Success)
        {
            break;
        }
    }

    // 从机不想收了 或者 主机不想发了
    if ((i2c_stat == Status_I2C_NAK) || stopflag)
    {
        clear_intpending_flag();    
        i2c_stat = i2c_master_stop();     
    }
    
    return i2c_stat;
}

/* 清除中断标志位 */
void clear_intpending_flag()
{
    I2C1_I2SR &= ~(1 << 1);
}

四、从总线上读取数据(阻塞模式)

在DR寄存器准备就绪以后,要将当前状态设为接收,逐字节接收(需要考虑接收缓冲区的大小),一旦接收缓冲区大小不足以接收后续的数据,需要提前发送停止信号告诉对方,不要再继续发了。

接收缓冲区大小 = 0,表示本次接收继续,下一次接收无法进行,直接发送停止信号

接收缓冲区大小 = 1,表示下一次接收继续,但是后续接收无法继续,发送 NACK

  • 等待 DR 寄存器准备就绪
  • 清除中断标志位
  • 状态设为接收
  • 循环读取
    • 等待数据传输完毕(对方将一个字节的数据全部发送到总线上时会触发中断)
    • 清除标志位
    • 如果接收缓冲区大小为 0,直接发送停止信号
    • 如果接收缓冲区大小为 1,发送 NACK
    • 读取 DR 寄存器数据
/*
 * @description		: 阻塞式读取
 * @param - recvbuf	: 读取到数据
 * @param - len 	: 要读取的数据大小(单位:字节)
 */
i2c_status_t i2c_master_read_blocking(unsigned char* recvbuf, unsigned int len)
{
    i2c_status_t result = Status_I2C_Success;
    volatile unsigned char dummy = 0;

    /* 使用这个变量的目的是避免编译报错 */
    dummy++;

    /* 等待DR寄存器准备就绪 */
    while (((I2C1_I2SR >> 1) & 0x01) == 0);

    /* 清除中断标志位 */
    clear_intpending_flag();

    /* 状态设为接收 */
    I2C1_I2CR &= ~((1 << 3) | (1 << 4));
    /* 后续只想再接收一个字节的数据 */
    if (len == 1)
    {
        I2C1_I2CR |= (1 << 3);
    }

    /* 假读 */
    dummy = I2C1_I2DR;
    
    while (len--)
    {
        /* 有一个字节的数据被传输到总线上,中断触发 */
        while (((I2C1_I2SR >> 1) & 0x01) == 0);

        /* 清除中断标志位 */
        clear_intpending_flag();

        /* 下一次没有足够的空间去读取 */
        if (len == 0)
        {
            result = i2c_master_stop();
        }

        // 后续只能再接收一个字节的数据,告诉对方不要再发数据了
        if (len == 1)
        {
            I2C1_I2CR |= (1 << 3);
        }

        /* 从寄存器中读取 */
        *(recvbuf++) = I2C1_I2DR & 0xFF;
    }

    return result;
}

/* 清除中断标志位 */
void clear_intpending_flag()
{
    I2C1_I2SR &= ~(1 << 1);
}

五、整合:数据读写统一调用接口

实际上我们真正要调用的接口,只需要这一个即可,上述内容其实都是在为这个接口做铺垫。当数据开始传输的时候,

  • 等待 DR 寄存器准备就绪
  • 清除中断、仲裁标志位
  • 判断是否为读时序,如果是读时序,需要临时修改通信方向(因为要先发送寄存器地址让对方知道自己要读取哪个寄存器)
  • 发送开始信号(发送设备地址和通信方向)
  • 等待信号发送完毕
  • 发送寄存器地址(考虑到寄存器地址可能不止一个字节)
  • 发送 / 读取数据

这里需要分别参考读时序和写时序的通信过程:I2C 通信时序

写时序:​​​​​​​

读时序:

typedef struct 
{
    unsigned char slaveAddress;         // 要通信的设备地址
    i2c_direction direction;            // 传输方向
    unsigned char reg;                  // 要读写的寄存器地址
    unsigned int reg_size;              // 寄存器地址大小(单位: 字节, 一般是1个字节,也有可能大于1个字节)
    unsigned char stopFlag;             // 下一次是否要停止发送
    unsigned char* buf;                 // 缓冲区
    unsigned int buf_size;              // 缓冲区大小
} i2c_transfer_t;

/* 数据传输(发送 / 接收) */
i2c_status_t i2c_master_transfer(i2c_transfer_t* xfer)
{
    i2c_direction direction = xfer->direction;

    /* 传输之前清除所有的标志位(仲裁、中断挂起) */
    I2C1_I2SR &= ~((1 << 1) | (1 << 4));

    /* DR 寄存器是否准备就绪 */
    while (!((I2C1_I2SR >> 7) & 0x01));
    
    /* 如果是读时序,需要先临时修改通信方向 */
    if ((xfer->reg_size > 0) && (xfer->direction == I2C_Read))
    {
        direction = I2C_Write;
    }

    /* 1、发送开始信号 */
    i2c_status_t ret = i2c_master_start(xfer->slaveAddress, direction);
    if (ret != Status_I2C_Success)
    {
        return ret;
    }

    /* 等待传输完成 */
    while (!((I2C1_I2SR >> 1) & 0x01));
    /* 检查传输是否发生错误 */
    if ((ret = i2c_check_and_clear_error()) != Status_I2C_Success)
    {
        i2c_master_stop();
        return ret;
    }
    
    /* 2、发送寄存器地址 */
    if (xfer->reg_size > 0)
    {
        do
        {
            clear_intpending_flag();
            xfer->reg_size --;

            /* 写入地址 */
            I2C1_I2DR = ((xfer->reg) >> (xfer->reg_size * 8));      // 先传输高字节

            /* 等待传输完成 */
            while (((I2C1_I2SR >> 1) & 0x01) == 0);
            /* 检查传输是否发生错误 */
            if ((ret = i2c_check_and_clear_error()) != Status_I2C_Success)
            {
                i2c_master_stop();
                return ret;
            }
        } while (xfer->reg_size > 0);
        
        /* 如果是读时序,需要先发送重复开始信号;如果是写时序,直接发送数据 */
        if (xfer->direction == I2C_Read)
        {
            clear_intpending_flag();

            /* 发送重复开始信号 */
            ret = i2c_master_repeated_start(xfer->slaveAddress, I2C_Read);
            if (ret != Status_I2C_Success)
            {
                i2c_master_stop();
                return ret;
            }
            /* 等待传输完成 */
            while (((I2C1_I2SR >> 1) & 0x01) == 0);
            /* 检查传输是否发生错误 */
            if ((ret = i2c_check_and_clear_error()) != Status_I2C_Success)
            {
                i2c_master_stop();
                return ret;
            }
        }
    }
    
    /* 到此,准备工作完毕 */
    /* 发送数据 */
    if (xfer->direction == I2C_Write)
    {
        ret = i2c_master_write_blocking(xfer->buf, xfer->buf_size, xfer->stopFlag);
    }
    
    if (xfer->direction == I2C_Read)
    {
        ret = i2c_master_read_blocking(xfer->buf, xfer->buf_size);
    }

    return ret;
}

猜你喜欢

转载自blog.csdn.net/challenglistic/article/details/131509632
今日推荐