引言
在STM32开发中,IIC通信是连接各类外设的核心技能。然而,面对标准库(Standard Peripheral Library)的硬件IIC配置和软件IIC的手动时序模拟,许多开发者常因时序混乱、配置复杂等问题陷入困境。本文将基于标准库,分享我在软件IIC和硬件IIC开发中的实战经验,并提供代码示例与调试技巧。
一、IIC协议核心回顾
IIC协议核心不变,但标准库的实现细节需特别注意:
-
起始/停止条件:硬件IIC由外设自动生成,软件IIC需手动控制GPIO。
-
应答机制:标准库需通过状态寄存器(如
I2C_CheckEvent()
)判断ACK/NACK。
二、软件IIC:基于标准库的GPIO模拟
1. 实现思路
通过GPIO手动拉高/拉低SCL和SDA引脚,模拟IIC时序。优势是灵活选择引脚,缺点是需消耗CPU资源且时序调试复杂。
2. 核心代码(标准库版)
// 定义GPIO引脚(以PB6为SCL,PB7为SDA为例)
#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_PIN GPIO_Pin_6
#define IIC_SDA_GPIO_PORT GPIOB
#define IIC_SDA_PIN GPIO_Pin_7
// 初始化GPIO为开漏输出
void Soft_I2C_Init() {
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = IIC_SCL_PIN | IIC_SDA_PIN;
GPIO_Init(IIC_SCL_GPIO_PORT, &GPIO_InitStruct);
// 初始拉高总线
GPIO_SetBits(IIC_SCL_GPIO_PORT, IIC_SCL_PIN);
GPIO_SetBits(IIC_SDA_GPIO_PORT, IIC_SDA_PIN);
}
// 生成起始信号
void I2C_Start() {
SDA_HIGH();
SCL_HIGH();
Delay_us(5);
SDA_LOW();
Delay_us(5);
SCL_LOW();
}
// 发送单字节(需包含ACK检查)
uint8_t I2C_WriteByte(uint8_t data) {
for (uint8_t i = 0; i < 8; i++) {
(data & 0x80) ? SDA_HIGH() : SDA_LOW();
data <<= 1;
SCL_HIGH();
Delay_us(5);
SCL_LOW();
Delay_us(5);
}
// 释放SDA线,等待ACK
SDA_HIGH();
SCL_HIGH();
if (GPIO_ReadInputDataBit(IIC_SDA_GPIO_PORT, IIC_SDA_PIN)) {
return 1; // NACK
}
SCL_LOW();
return 0; // ACK
}
3. 常见问题
-
问题:从机无应答(ACK失败)
原因:时序延时不足、从机地址错误或总线冲突。
解决:-
用逻辑分析仪校准延时(通常SCL高电平保持4μs以上)。
-
确认从机地址是否为7位(需左移1位并补读写位)。
-
三、硬件IIC:标准库配置与实战
1. 标准库配置步骤
步骤1:启用GPIO和I2C时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // SCL:PB6, SDA:PB7
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
步骤2:配置GPIO为复用开漏模式
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
步骤3:初始化I2C外设
I2C_InitTypeDef I2C_InitStruct;
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 50%占空比
I2C_InitStruct.I2C_OwnAddress1 = 0x00; // 主机地址(可忽略)
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; // 使能ACK
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStruct.I2C_ClockSpeed = 100000; // 100kHz
I2C_Init(I2C1, &I2C_InitStruct);
I2C_Cmd(I2C1, ENABLE); // 使能I2C
2. 数据收发核心代码
发送数据到从机(以AT24C02为例)
void I2C_WriteData(uint8_t devAddr, uint8_t regAddr, uint8_t data) {
// 发送START条件
I2C_GenerateSTART(I2C1, ENABLE);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送从机地址(写模式)
I2C_Send7bitAddress(I2C1, devAddr, I2C_Direction_Transmitter);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送寄存器地址
I2C_SendData(I2C1, regAddr);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 发送数据
I2C_SendData(I2C1, data);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 发送STOP条件
I2C_GenerateSTOP(I2C1, ENABLE);
}
3. 硬件IIC的“深坑”与填坑
-
问题:
I2C_CheckEvent
卡死
原因:总线被锁(如未收到ACK或异常中断)。
解决:// 强制复位I2C总线 I2C_SoftwareResetCmd(I2C1, ENABLE); I2C_SoftwareResetCmd(I2C1, DISABLE); I2C_Cmd(I2C1, ENABLE); // 重新使能
四、软件IIC vs 硬件IIC(标准库对比)
对比项 软件IIC 硬件IIC(标准库) 开发复杂度 高(需逐行调试时序) 中(需熟悉状态机与事件检查) 性能 低(受CPU速度限制) 高(支持DMA,最高1MHz) 稳定性 依赖延时精度 硬件保证时序,抗干扰强 适用场景 低速、多引脚需求 高速、复杂多设备通信
五、调试技巧
-
调试工具:
-
逻辑分析仪:确认起始/停止信号、ACK波形。
-
ST-Link Debugger:通过断点监控
I2C_SR1
和I2C_SR2
寄存器状态。
-
-
代码技巧:
-
封装超时检测:避免
while(I2C_CheckEvent)
死循环。
-