国产MCU学习Day6——CW32F030C8T6: I2C功能详解与应用案例

发布于:2025-07-07 ⋅ 阅读:(27) ⋅ 点赞:(0)

每日更新教程,评论区答疑解惑,小白也能变大神!"

目录

一.CW32F030C8T6的I2C功能

二.CW32F030C8T6的I2C案例——通过I2C中断方式读取eeprom

三.总结

 

一.CW32F030C8T6的I2C功能

  • CW32x030 内部集成 2 个 I2C 控制器,能按照设定的传输速率(标准,快速,高速)将需要发送的数据按照 I2C 规范串行发送到 I2C 总线上,并对通信过程中的状态进行检测,另外还支持多主机通信中的总线冲突和仲裁处理。

1.1主要特性

  • 支持主机发送 / 接收,从机发送 / 接收四种工作模式
  • 支持时钟延展 ( 时钟同步 ) 和多主机通信冲突仲裁
  • 支持标准 (100Kbps)/ 快速 (400Kbps)/ 高速 (1Mbps) 三种工作速率
  • 支持 7bit 寻址功能
  • 支持 3 个从机地址
  • 支持广播地址
  • 支持输入信号噪声过滤功能
  • 支持中断状态查询功能

1.2协议描述

  • I2C 总线使用两根信号线(数据线 SDA 和时钟线 SCL)在设备间传输数据。SCL 为单向时钟线,固定由主机驱动。 SDA 为双向数据线,在数据传输过程中由收发两端分时驱动。
  • I2C 总线上可以连接多个设备,所有设备在没有进行数据传输时都处于空闲状态(未寻址从机接收模式),任一 设备都可以作为主机发送 START 起始信号来开始数据传输,在 STOP 停止信号出现在总线上之前,总线一直处于 被占用状态。
  • I2C 通信采用主从结构,并由主机发起和结束通信。主机通过发送 START 起始信号来发起通信,之后发送 SLA+W/R 共 8bit 数据(其中,SLA 为 7bit 从机地址,W/R 为读写位),并在第 9 个 SCL 时钟释放 SDA 总线, 对应的从机在第 9 个 SCL 时钟占用 SDA 总线并输出 ACK 应答信号,完成从机寻址。此后根据主机发送的第 1 字 节的 W/R 位来决定数据通信的发端和收端,发端每发送 1 个字节数据,收端必须回应 1 个 ACK 应答信号。数据 传输完成后,主机发送 STOP 信号结束本次通信。

1.3协议帧格式

  • 标准 I2C 传输协议帧包含四个部分:起始信号 (START) 或重复起始信号 (Repeated START) 信号,从机地址及读 写位,数据传输,停止信号 (STOP)。如下图所示。

1.4起始信号 (START)

  • 当总线处于空闲状态时(SCL 和 SDA 线同时为高),SDA 线上出现由高到低的下降沿信号,则被定义为起始 信号。主机向总线发出起始信号后开始数据传输,并占用总线。

1.5重复起始信号 (Repeated START)

  • 当一个起始信号后未出现停止信号之前,出现了新的起始信号,新的起始信号被定义为重复起始信号。在主 机发送停止信号前,SDA 总线一直处于占用状态,其它主机无法占用总线。

1.6停止信号 (STOP)

  • 当 SCL 线为高时,SDA 线上出现由低到高的上升沿信号,则被定义为停止信号。主机向总线发出停止信号以 结束数据传输,并释放总线。

1.7从机地址及读写位

  • 当起始信号产生后,主机开始传输第 1 字节数据:7 bit 从机地址 + 读写位。读写位(1:读;0:写)控制总 线上数据传输方向。被寻址的从机在第 9 个 SCL 时钟周期内占用 SDA 总线,并将 SDA 置为低电平作为 ACK 应答。

1.8数据传输

  • 主机在 SCL 线上输出串行时钟信号,主从机通过 SDA 线进行数据传输。数据传输过程中,1 个 SCL 时钟脉 冲传输 1 个数据位(最高有效位 MSB 在前),且 SDA 线上的数据只在 SCL 为低时改变,每传输 1 个字节跟 随 1 个应答位。如下图所示:

1.9传输应答

  • 在总线上传输数据时,发端每传输完 1 个字节数据,在第 9 个 SCL 时钟周期发端放弃对 SDA 的控制,收端须在 第 9 个 SCL 时钟周期回复 1 个应答位:接收成功,发送 ACK 应答,接收异常发送 NACK 应答。

 1.10冲突检测与仲裁

  • 在多主机通信系统中,总线上的每个节点都有从机地址。每个节点可以作为从节点被其它节点访问,也可以作为 主节点向其它的节点发送控制字节和传送数据。如果有两个或两个以上的节点同时向总线发出起始信号并开始传 输数据,就会造成总线冲突。I2C 控制器内置一个仲裁器,可对 I2C 总线冲突进行检测和仲裁,以保证数据通信 的可靠性和完整性。

1.11冲突检测原理

  • 在物理实现上, SDA 和 SCL 引脚电路结构相同,引脚的输出驱动与输入缓冲连在一起。输出结构为漏极开 路的场效应管、输入结构为高输入阻抗的同相器。基于该结构: 1. 由于 SDA、SCL 为漏极开路结构,借助于外部的上拉电阻实现了信号的“线与”逻辑; 2. 设备向总线写数据的同时读取数据,可用来检测总线冲突,实现 “时钟同步”和“总线仲裁”。 根据“线与”逻辑,如果 2 个主机同时发送逻辑 1 或逻辑 0,则 2 个主机都检测不到冲突,需要等到下一位 数据发送再继续检测冲突;如果 2 个主机一个发送逻辑 1,一个发送逻辑 0,此时总线上为逻辑 0,发送逻 辑 1 的主机检测到冲突,发送逻辑 0 的主机没有检测到冲突。

1.12冲突仲裁原理

  • 当主机检测到总线冲突后,该主机丢失仲裁,退出主机发送模式,进入未寻址从机模式,释放 SDA 数据线, 并回到地址侦测状态,之后根据接收到的 SLA+W/R 进入相应的从机模式 (SLA 地址匹配进入已寻址从机模式, SLA 地址不匹配则进入未寻址从机模式 )。仲裁失败的主机,仍会发送 SCL 串行时钟,直到当前字节传输结束。 当主机没有检测到总线冲突,该主机赢得仲裁,继续主导本次数据传输,直到通信完成。

1.13功能框图

1.14串行时钟发生器

1.15仲裁和同步逻辑

1.16工作模式

1.17多主机通讯

二.CW32F030C8T6的I2C案例——通过I2C中断方式读取eeprom

/* 定义测试使用的I2C模块,I2C1=1,I2C2=2 */
#define TESTI2C 2    

/* I2C1引脚配置 */
#define I2C1_SCL_GPIO_PORT CW_GPIOB  // I2C1 SCL引脚所在GPIO端口
#define I2C1_SCL_GPIO_PIN GPIO_PIN_10 // I2C1 SCL引脚编号
#define I2C1_SDA_GPIO_PORT CW_GPIOB  // I2C1 SDA引脚所在GPIO端口
#define I2C1_SDA_GPIO_PIN GPIO_PIN_11 // I2C1 SDA引脚编号

/* I2C2引脚配置 */  
#define I2C2_SCL_GPIO_PORT CW_GPIOB  // I2C2 SCL引脚所在GPIO端口
#define I2C2_SCL_GPIO_PIN GPIO_PIN_0  // I2C2 SCL引脚编号
#define I2C2_SDA_GPIO_PORT CW_GPIOB  // I2C2 SDA引脚所在GPIO端口
#define I2C2_SDA_GPIO_PIN GPIO_PIN_1  // I2C2 SDA引脚编号

/* EEPROM相关参数 */
uint8_t u8Addr = 0x00;        // EEPROM内部访问地址
#define WRITELEN 8           // 写入数据长度
#define READLEN 8            // 读取数据长度  
#define WriteReadCycle 35    // 写读循环次数

/* 数据缓冲区 */
uint8_t u8Senddata[8] = {0x66, 0x02, 0x03, 0x04, 0x05, 0x60, 0x70, 0x20}; // 写入数据1
uint8_t u8Senddata2[8] = {0x55, 0xAA, 0xAA, 0x55, 0x55, 0xAA, 0x55, 0xAA}; // 写入数据2
uint8_t u8Recdata[16] = {0x00}; // 接收数据缓冲区
uint8_t u8SendLen = 0;        // 已发送数据长度
uint8_t u8RecvLen = 0;        // 已接收数据长度

/* 状态标志 */
uint8_t SendFlg = 0;         // 发送标志:0=写,1=读
uint8_t Comm_flg = 0;        // 通信完成标志
uint8_t u8recvflg = 0;       // 接收完成标志
uint8_t u8State = 0;         // I2C状态机状态
uint8_t receivedflag = 0;    // 数据接收完成标志

/**
 * @brief I2C1 EEPROM读写中断处理函数
 * @note 根据I2C状态机处理不同阶段的通信流程
 */
void I2c1EepromReadWriteInterruptFunction(void)
{
    u8State = I2C_GetState(CW_I2C1); // 获取当前I2C状态
    
    switch (u8State)
    {
        case 0x08:     // START信号已发送
            I2C_GenerateSTART(CW_I2C1, DISABLE); // 关闭START生成
            I2C_Send7bitAddress(CW_I2C1, I2C_SLAVEADDRESS, 0X00); // 发送从机地址+写命令
            break;
            
        case 0x10:     // 重复START信号已发送
            I2C_GenerateSTART(CW_I2C1, DISABLE); // 关闭START生成
            if (0 == SendFlg) // 写模式
                I2C_Send7bitAddress(CW_I2C1, I2C_SLAVEADDRESS, 0X00); 
            else              // 读模式
                I2C_Send7bitAddress(CW_I2C1, I2C_SLAVEADDRESS, 0X01); 
            break;
            
        case 0x18:    // SLA+W/R已发送
            I2C_GenerateSTART(CW_I2C1, DISABLE); // 关闭START生成
            I2C_SendData(CW_I2C1, u8Addr);      // 发送EEPROM内部地址
            break;
            
        case 0x20:    // SLA+W后从机返回NACK
        case 0x38:    // 丢失仲裁
        case 0x30:    // 发送数据后从机返回NACK  
        case 0x48:    // SLA+R后从机返回NACK
            I2C_GenerateSTOP(CW_I2C1, ENABLE);  // 生成STOP信号
            I2C_GenerateSTART(CW_I2C1, ENABLE); // 重新开始传输
            break;
            
        case 0x58:    // 接收完最后一个数据字节
            u8Recdata[u8RecvLen++] = I2C_ReceiveData(CW_I2C1); // 保存接收数据
            receivedflag = 1;                   // 设置接收完成标志
            I2C_GenerateSTOP(CW_I2C1, ENABLE);  // 生成STOP信号
            break;
            
        case 0x28:    // 数据字节已发送
            if (0 == SendFlg) // 写模式
            {
                if (u8SendLen < WRITELEN) // 还有数据要发送
                    I2C_SendData(CW_I2C1, u8Senddata[u8SendLen++]); 
                else // 写操作完成
                {
                    u8SendLen = 0;
                    Comm_flg = 1;       // 设置通信完成标志
                    SendFlg = 1;        // 切换为读模式
                    I2C_GenerateSTOP(CW_I2C1, ENABLE); // 生成STOP信号
                }
            }
            else // 读模式:发送完地址后需重新START
            {
                CW_I2C1->CR_f.STA = 1;  // 手动设置START位
                I2C_GenerateSTOP(CW_I2C1, DISABLE); // 禁用STOP
            }
            break;
            
        case 0x40:     // SLA+R已发送,开始接收数据
            u8RecvLen = 0; // 重置接收计数器
            if (READLEN > 1) // 多字节读取时启用ACK
                I2C_AcknowledgeConfig(CW_I2C1, ENABLE); 
            break;
            
        case 0x50:     // 接收到数据字节
            u8Recdata[u8RecvLen++] = I2C_ReceiveData(CW_I2C1); // 保存数据
            if (u8RecvLen == READLEN - 1) // 倒数第二个字节
                I2C_AcknowledgeConfig(CW_I2C1, DISABLE); // 最后一个字节发送NACK
            break;
    }
    I2C_ClearIrq(CW_I2C1); // 清除中断标志
}
int32_t main(void)
{
    I2C_InitTypeDef I2C_InitStruct;  // I2C初始化结构体
    uint16_t tempcnt = 0 ;           // 循环计数器
    
    //硬件初始化部分
    RCC_Configuration();            // 初始化系统时钟
    GPIO_Configuration();           // 初始化GPIO引脚
    I2C_InitStruct.I2C_Baud = 0x01; // 设置I2C时钟频率(500KHz)
    I2C_InitStruct.I2C_BaudEn = ENABLE;
    I2C_InitStruct.I2C_FLT = DISABLE;
    I2C_InitStruct.I2C_AA =  DISABLE;

// 根据TESTI2C宏选择初始化I2C1或I2C2
#if(0x01 == TESTI2C)
    I2C1_DeInit();
    I2C_Master_Init(CW_I2C1, &I2C_InitStruct);
#elif(0x02 == TESTI2C)
    I2C2_DeInit();
    I2C_Master_Init(CW_I2C2, &I2C_InitStruct);
#endif

    NVIC_Configuration();           // 配置中断控制器

// 使能对应I2C模块
#if(0x01 == TESTI2C)
    I2C_Cmd(CW_I2C1, ENABLE);
#elif(0x02 == TESTI2C)
    I2C_Cmd(CW_I2C2, ENABLE);
#endif

    // 初始化发送数据缓冲区
    tempcnt = 0;
    for (uint8_t i = 0; i < 8; i++)
    {
        u8Senddata[i] = i;
    }

    // 主循环实现I2C读写测试
    while (1)
    {
        // 发送START信号
#if(0x01 == TESTI2C)
        I2C_GenerateSTART(CW_I2C1, ENABLE);
#elif(0x02 == TESTI2C)
        I2C_GenerateSTART(CW_I2C2, ENABLE);
#endif

        while (1)
        {
            // 等待发送完成
            while (!Comm_flg) ;
            FirmwareDelay(3000);     // 延时3ms
            
            // 准备读取数据
            Comm_flg = 0;
            receivedflag = 0;
#if(0x01 == TESTI2C)
            I2C_GenerateSTART(CW_I2C1, ENABLE);
#elif(0x02 == TESTI2C)
            I2C_GenerateSTART(CW_I2C2, ENABLE);
#endif

            // 等待接收完成
            while (!receivedflag) ;
            
            // 重置标志位和数据长度
            receivedflag = 0;
            SendFlg = 0;
            u8RecvLen = 0;
            
            // 更新发送数据内容
            tempcnt++;
            for (uint8_t i = 0; i < 8; i++)
            {
                u8Senddata[i] = tempcnt + i;
            }
            break;
        }
        
        // 达到测试次数后退出
        if (tempcnt >= WriteReadCycle)
        {
            break;
        }
    }
    while (1);  // 测试完成后进入死循环
}

/**
 * @brief  系统时钟配置
 * @note   使能GPIOB和I2C外设时钟
 */
void RCC_Configuration(void)
{
    CW_SYSCTRL->AHBEN_f.GPIOB  = 1;
#if(0x01 == TESTI2C)
    CW_SYSCTRL->APBEN1_f.I2C1 = 1U;
#elif(0x02 == TESTI2C)
    CW_SYSCTRL->APBEN1_f.I2C2 = 1U;
#endif
}

/**
 * @brief  GPIO配置
 * @note   配置I2C引脚为开漏输出模式
 */
void GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
#if(0x01 == TESTI2C)
    PB10_AFx_I2C1SCL();  // 配置PB10为I2C1 SCL
    PB11_AFx_I2C1SDA();  // 配置PB11为I2C1 SDA
    GPIO_InitStructure.Pins = I2C1_SCL_GPIO_PIN | I2C1_SDA_GPIO_PIN;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏输出模式
    GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
    GPIO_Init(I2C1_SCL_GPIO_PORT, &GPIO_InitStructure);
#elif(0x02 == TESTI2C)
    PB00_AFx_I2C2SCL();  // 配置PB00为I2C2 SCL
    PB01_AFx_I2C2SDA();  // 配置PB01为I2C2 SDA
    GPIO_InitStructure.Pins = I2C2_SCL_GPIO_PIN | I2C2_SDA_GPIO_PIN;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
    GPIO_Init(I2C2_SCL_GPIO_PORT, &GPIO_InitStructure);
#endif
}

/**
 * @brief  中断控制器配置
 * @note   使能I2C中断
 */
void NVIC_Configuration(void)
{
    __disable_irq();  // 全局中断禁用
#if(0x01 == TESTI2C)
    NVIC_EnableIRQ(I2C1_IRQn);  // 使能I2C1中断
#elif(0x02 == TESTI2C)
    NVIC_EnableIRQ(I2C2_IRQn);  // 使能I2C2中断
#endif
    __enable_irq();   // 全局中断使能
}

三.总结

I2C通信测试流程:

  1. 初始化阶段配置时钟、GPIO和I2C参数
  2. 主循环中执行写-读操作序列
  3. 每次循环更新发送数据内容
  4. 通过中断标志位同步通信状态

硬件配置特点:

  • 支持I2C1和I2C2模块选择
  • GPIO配置为开漏输出模式
  • 使用中断机制处理通信事件
  • 时钟频率设置为500KHz

实现I2C EEPROM的读写操作,主要特点包括:

  1. 支持I2C1和I2C2两个接口的配置
  2. 使用状态机方式处理I2C通信流程
  3. 包含完整的写操作和读操作流程
  4. 处理了NACK、仲裁丢失等异常情况
  5. 通过中断方式实现非阻塞通信

代码中各种状态码对应I2C协议的不同阶段,注释中已详细说明每个状态的处理逻辑。实际使用时需要根据具体硬件平台调整GPIO和I2C外设的配置。