1. 引言
IIC(Inter-Integrated Circuit,也称 I2C)是一种广泛使用的串行通信协议,由飞利浦公司于上世纪 80 年代推出。GD32 系列微控制器内置了硬件 IIC 外设,支持主机和从机模式,可用于连接传感器、存储器、显示屏等外围设备。本文将从 IIC 协议基础出发,结合 GD32 的硬件特性,介绍 IIC 通信的配置方法、代码实现和常见问题排查。
2. IIC 协议基础
IIC 总线由两根线组成:串行数据线 SDA 和串行时钟线 SCL。所有设备通过这两根线并联连接,每个设备都有唯一的地址。通信由主机发起,主机负责产生时钟信号并控制数据传输方向。
IIC 通信的基本时序包括以下几个关键环节:
- 起始条件:SCL 为高电平时,SDA 由高变低,表示通信开始。
- 停止条件:SCL 为高电平时,SDA 由低变高,表示通信结束。
- 地址帧:主机发送 7 位从机地址加 1 位读写标志位。
- 应答信号:接收方在收到每个字节后发送 ACK(低电平)或 NACK(高电平)。
- 数据帧:每个字节 8 位,高位在前,传输完成后等待应答。
IIC 支持多种传输速率,标准模式为 100 kbit/s,快速模式为 400 kbit/s,高速模式可达 3.4 Mbit/s。GD32 的硬件 IIC 支持标准模式和快速模式,实际速率可通过时钟配置寄存器调整。
3. GD32 IIC 硬件特性
GD32 系列微控制器的 IIC 外设具有以下主要特性:
- 支持主机模式和从机模式,可动态切换。
- 支持 7 位和 10 位地址模式。
- 内置发送和接收缓冲区,支持中断和 DMA 传输。
- 支持多主机仲裁和时钟同步。
- 具备错误检测功能,包括总线错误、应答错误、仲裁丢失等。
以 GD32F103 系列为例,其 IIC0 和 IIC1 外设分别挂载在不同的总线上,引脚可通过复用功能映射到多个 GPIO 端口。开发者需要根据实际硬件连接选择合适的引脚和复用配置。
4. 硬件连接与引脚配置
在使用 GD32 的 IIC 外设之前,需要完成以下硬件准备工作:
- 确认 SDA 和 SCL 引脚对应的 GPIO 端口和引脚号。
- 在 SDA 和 SCL 线上各接一个上拉电阻(通常为 4.7 kΩ 或 10 kΩ),保证空闲状态为高电平。
- 确认从机设备的 IIC 地址和供电电压,避免电平不匹配。
以 GD32F103 的 IIC0 为例,其默认引脚为 PB6(SCL)和 PB7(SDA),复用功能为 AF1。初始化代码如下:
c
void iic_gpio_init(void)
{
gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_6 | GPIO_PIN_7);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_6 | GPIO_PIN_7);
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7);
}
这里将引脚配置为复用功能、开漏输出并启用内部上拉。开漏输出是 IIC 协议的要求,允许多个设备共享总线。
5. IIC 外设初始化
完成 GPIO 配置后,需要初始化 IIC 外设本身。GD32 的 IIC 初始化主要涉及时钟使能、速率配置和模式设置。以下代码演示了 IIC0 的初始化过程:
c
void iic_periph_init(void)
{
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_I2C0);
i2c_clock_config(I2C0, 100000, I2C_DTCY_2);
i2c_enable(I2C0);
}
其中 i2c_clock_config 的第一个参数指定 IIC 外设,第二个参数为目标速率(单位 Hz),第三个参数为占空比配置。对于标准模式,占空比通常设置为 2:1。
如果需要使用中断方式接收数据,还需要配置 NVIC 并使能相应中断:
c
nvic_irq_enable(I2C0_EV_IRQn, 0, 0);
nvic_irq_enable(I2C0_ER_IRQn, 1, 0);
i2c_interrupt_enable(I2C0, I2C_INT_ERR | I2C_INT_BUF | I2C_INT_EV);
6. 主机发送数据
作为主机发送数据时,需要依次完成起始条件、发送从机地址、发送数据字节和停止条件。GD32 提供了状态标志位来同步每一步操作。以下是一个向从机写入多个字节的示例:
c
void iic_master_write(uint8_t slave_addr, uint8_t reg_addr, uint8_t *data, uint8_t len)
{
i2c_start_on_bus(I2C0);
while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
i2c_master_addressing(I2C0, slave_addr, I2C_TRANSMITTER);
while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
i2c_data_transmit(I2C0, reg_addr);
while (!i2c_flag_get(I2C0, I2C_FLAG_TBE));
for (uint8_t i = 0; i < len; i++) {
i2c_data_transmit(I2C0, data[i]);
while (!i2c_flag_get(I2C0, I2C_FLAG_TBE));
}
i2c_stop_on_bus(I2C0);
while (!i2c_flag_get(I2C0, I2C_FLAG_STPDET));
i2c_flag_clear(I2C0, I2C_FLAG_STPDET);
}
这段代码先发送起始条件,等待 SBSEND 标志置位后发送从机地址。ADDSEND 标志表示地址已发送并收到应答,需要手动清除。随后依次发送寄存器地址和数据字节,每发送一个字节都要等待发送缓冲区为空(TBE 标志)。最后发送停止条件并等待停止检测标志。
7. 主机接收数据
主机从从机读取数据时,需要先发送从机地址(写方向)和寄存器地址,然后重新发送起始条件,再发送从机地址(读方向),最后读取数据。以下示例演示了读取多个字节的过程:
c
void iic_master_read(uint8_t slave_addr, uint8_t reg_addr, uint8_t *buf, uint8_t len)
{
i2c_start_on_bus(I2C0);
while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
i2c_master_addressing(I2C0, slave_addr, I2C_TRANSMITTER);
while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
i2c_data_transmit(I2C0, reg_addr);
while (!i2c_flag_get(I2C0, I2C_FLAG_TBE));
i2c_start_on_bus(I2C0);
while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
i2c_master_addressing(I2C0, slave_addr, I2C_RECEIVER);
while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
for (uint8_t i = 0; i < len; i++) {
while (!i2c_flag_get(I2C0, I2C_FLAG_RBNE));
if (i == len - 1) {
i2c_ack_config(I2C0, I2C_ACK_DISABLE);
}
buf[i] = i2c_data_receive(I2C0);
}
i2c_stop_on_bus(I2C0);
while (!i2c_flag_get(I2C0, I2C_FLAG_STPDET));
i2c_flag_clear(I2C0, I2C_FLAG_STPDET);
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
}
读取最后一个字节前需要关闭应答(NACK),通知从机不再发送数据,然后发送停止条件。读取完成后重新使能应答,以便后续通信使用。
8. 中断方式接收数据
对于需要实时响应的应用场景,可以使用中断方式接收数据。GD32 的 IIC 中断分为事件中断和错误中断。以下是一个简单的接收中断处理示例:
c
volatile uint8_t rx_buffer[16];
volatile uint8_t rx_index = 0;
void I2C0_EV_IRQHandler(void)
{
if (i2c_interrupt_flag_get(I2C0, I2C_INT_FLAG_RBNE)) {
rx_buffer[rx_index++] = i2c_data_receive(I2C0);
if (rx_index >= 16) {
rx_index = 0;
}
}
}
在中断处理函数中,通过检查 RBNE 标志判断接收缓冲区是否有数据,读取后存入缓冲区。实际项目中还需要处理地址匹配、停止条件等更多事件,建议根据 GD32 标准外设库的示例代码进行扩展。
9. 常见问题与排查
IIC 通信在实际调试中经常遇到以下问题,这里给出对应的排查思路:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 总线一直为低电平 | SDA 或 SCL 被从机拉低,或上拉电阻缺失 | 检查上拉电阻,断开从机逐个排查 |
| 发送地址后无应答 | 从机地址错误或从机未上电 | 核对地址,用逻辑分析仪抓取波形 |
| 数据接收乱码 | 速率不匹配或时钟线干扰 | 降低速率,缩短线缆长度,检查布线 |
| 偶尔通信失败 | 缺少错误处理或总线竞争 | 增加重试机制,检查多主机仲裁 |
调试 IIC 通信时,逻辑分析仪是最有效的工具。通过抓取 SDA 和 SCL 的波形,可以直观地看到起始条件、地址帧、数据帧和应答信号是否正常,快速定位问题所在。
10. 总结
GD32 的硬件 IIC 外设功能完善,配合标准外设库可以快速实现可靠的 IIC 通信。本文从协议基础、硬件配置、主机收发、中断处理和问题排查几个方面进行了介绍,希望能帮助开发者少走弯路。实际项目中建议优先使用硬件 IIC 而非软件模拟,以获得更好的稳定性和更低的 CPU 占用。遇到问题时,结合逻辑分析仪和分步排查法,通常能较快定位并解决。