1. STM32F103硬件I2C模块深度解析
STM32F103系列微控制器内置了硬件I2C外设模块,该模块完全兼容标准I2C协议规范。与软件模拟I2C相比,硬件I2C具有更高的可靠性和更低的CPU占用率。STM32F103通常提供两个I2C接口(I2C1和I2C2),每个接口都可以配置为主机或从机模式。
硬件I2C模块的核心寄存器包括:
- CR1/CR2:控制寄存器,用于配置I2C工作模式和各种功能开关
- OAR1/OAR2:自身地址寄存器,在从机模式下使用
- DR:数据寄存器,用于收发数据
- SR1/SR2:状态寄存器,反映I2C总线当前状态
- CCR:时钟控制寄存器,设置I2C通信速率
- TRISE:上升时间寄存器,与总线速度相关
关键提示:STM32F103的硬件I2C存在一些已知的硬件缺陷,特别是在从机模式下。建议在使用前查阅最新的勘误手册(Errata Sheet),确认芯片版本是否已修复相关问题。
2. I2C引脚配置与初始化流程
2.1 引脚复用配置
STM32F103的I2C1默认使用PB6(SCL)和PB7(SDA),I2C2默认使用PB10(SCL)和PB11(SDA)。这些引脚需要配置为复用开漏输出模式:
c复制// I2C1 GPIO配置示例
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN; // 使能GPIOB和AFIO时钟
// 配置PB6为复用开漏输出,50MHz
GPIOB->CRL &= ~GPIO_CRL_CNF6;
GPIOB->CRL |= GPIO_CRL_CNF6_1; // 复用开漏输出
GPIOB->CRL |= GPIO_CRL_MODE6; // 输出模式,50MHz
// 配置PB7为复用开漏输出,50MHz
GPIOB->CRL &= ~GPIO_CRL_CNF7;
GPIOB->CRL |= GPIO_CRL_CNF7_1; // 复用开漏输出
GPIOB->CRL |= GPIO_CRL_MODE7; // 输出模式,50MHz
2.2 时钟与速率配置
I2C模块挂载在APB1总线上,时钟频率通常为36MHz或72MHz。通信速率通过CCR寄存器设置:
c复制// I2C初始化核心代码
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; // 使能I2C1时钟
I2C1->CR1 |= I2C_CR1_SWRST; // 复位I2C外设
I2C1->CR1 &= ~I2C_CR1_SWRST;
I2C1->CR2 |= 36; // 设置APB1时钟频率(MHz)
I2C1->CCR = 180; // 100kHz @ 36MHz APB1时钟
I2C1->TRISE = 37; // 最大上升时间(TRISE = (1000ns / (1/36MHz)) + 1)
I2C1->CR1 |= I2C_CR1_PE; // 使能I2C外设
计算CCR值的公式:
- 标准模式(100kHz):CCR = APB1时钟/(2*100kHz)
- 快速模式(400kHz):CCR = APB1时钟/(3*400kHz)
3. 主机模式通信实现
3.1 基本发送流程
主机模式下发送数据的完整流程如下:
c复制void I2C_WriteByte(I2C_TypeDef* I2Cx, uint8_t devAddr, uint8_t data) {
// 等待总线空闲
while(I2Cx->SR2 & I2C_SR2_BUSY);
// 发送起始条件
I2Cx->CR1 |= I2C_CR1_START;
while(!(I2Cx->SR1 & I2C_SR1_SB)); // 等待起始位确认
// 发送设备地址(写模式)
I2Cx->DR = devAddr << 1;
while(!(I2Cx->SR1 & I2C_SR1_ADDR)); // 等待地址确认
(void)I2Cx->SR2; // 清除ADDR标志
// 发送数据
while(!(I2Cx->SR1 & I2C_SR1_TXE)); // 等待数据寄存器空
I2Cx->DR = data;
// 等待传输完成
while(!(I2Cx->SR1 & I2C_SR1_BTF));
// 发送停止条件
I2Cx->CR1 |= I2C_CR1_STOP;
}
3.2 接收数据流程
主机模式下接收数据的实现:
c复制uint8_t I2C_ReadByte(I2C_TypeDef* I2Cx, uint8_t devAddr) {
uint8_t data;
// 等待总线空闲
while(I2Cx->SR2 & I2C_SR2_BUSY);
// 发送起始条件
I2Cx->CR1 |= I2C_CR1_START;
while(!(I2Cx->SR1 & I2C_SR1_SB));
// 发送设备地址(读模式)
I2Cx->DR = (devAddr << 1) | 0x01;
while(!(I2Cx->SR1 & I2C_SR1_ADDR));
(void)I2Cx->SR2; // 清除ADDR标志
// 准备接收数据(发送NACK+STOP)
I2Cx->CR1 &= ~I2C_CR1_ACK;
I2Cx->CR1 |= I2C_CR1_STOP;
// 读取数据
while(!(I2Cx->SR1 & I2C_SR1_RXNE));
data = I2Cx->DR;
return data;
}
4. 从机模式实现与中断处理
4.1 从机初始化
配置STM32F103作为I2C从机需要设置自身地址并启用相关中断:
c复制void I2C_Slave_Init(I2C_TypeDef* I2Cx, uint8_t slaveAddr) {
// 复位并初始化I2C
I2Cx->CR1 |= I2C_CR1_SWRST;
I2Cx->CR1 &= ~I2C_CR1_SWRST;
// 设置自身7位地址
I2Cx->OAR1 = slaveAddr << 1;
I2Cx->OAR1 |= I2C_OAR1_ADDMODE; // 7位地址模式
// 配置中断
I2Cx->CR2 |= I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN;
NVIC_EnableIRQ(I2C1_EV_IRQn);
I2Cx->CR1 |= I2C_CR1_PE; // 使能I2C
I2Cx->CR1 |= I2C_CR1_ACK; // 启用ACK
}
4.2 中断服务例程
从机模式下通常需要处理以下事件:
- 地址匹配(ADDR)
- 接收数据(RxNE)
- 发送数据(TxE)
- 停止条件检测(STOPF)
c复制void I2C1_EV_IRQHandler(void) {
uint32_t sr1 = I2C1->SR1;
// 地址匹配事件
if(sr1 & I2C_SR1_ADDR) {
// 区分读/写模式
if(I2C1->SR2 & I2C_SR2_TRA) {
// 主机请求读数据(从机发送模式)
txBufferReady = 1;
} else {
// 主机请求写数据(从机接收模式)
rxIndex = 0;
}
(void)I2C1->SR2; // 清除ADDR标志
}
// 接收数据事件
if(sr1 & I2C_SR1_RXNE) {
rxBuffer[rxIndex++] = I2C1->DR;
}
// 发送数据事件
if(sr1 & I2C_SR1_TxE) {
if(txBufferReady && txIndex < txLength) {
I2C1->DR = txBuffer[txIndex++];
} else {
I2C1->DR = 0xFF; // 发送空数据
}
}
// 停止条件检测
if(sr1 & I2C_SR1_STOPF) {
I2C1->CR1 |= I2C_CR1_PE; // 重新使能外设
dataReceived = 1; // 标记数据接收完成
}
}
5. 常见问题与调试技巧
5.1 总线锁死处理
STM32F103的I2C模块在某些异常情况下可能出现总线锁死(BUSY标志一直为1)。解决方法包括:
- 软件复位序列:
c复制I2C1->CR1 |= I2C_CR1_SWRST;
I2C1->CR1 &= ~I2C_CR1_SWRST;
I2C1->CR1 |= I2C_CR1_PE;
- GPIO模拟I2C总线复位:
c复制// 配置SCL为通用输出
GPIOB->CRL &= ~GPIO_CRL_CNF6;
GPIOB->CRL |= GPIO_CRL_MODE6;
// 发送9个时钟脉冲
for(int i=0; i<9; i++) {
GPIOB->BSRR = GPIO_BSRR_BR6; // SCL低
delay_us(5);
GPIOB->BSRR = GPIO_BSRR_BS6; // SCL高
delay_us(5);
}
// 发送停止条件
GPIOB->BSRR = GPIO_BSRR_BR7; // SDA低
delay_us(5);
GPIOB->BSRR = GPIO_BSRR_BS6; // SCL高
delay_us(5);
GPIOB->BSRR = GPIO_BSRR_BS7; // SDA高
// 恢复GPIO复用功能
GPIOB->CRL |= GPIO_CRL_CNF6_1; // 复用开漏
5.2 时序调试建议
-
使用逻辑分析仪或示波器检查SCL/SDA波形,确认:
- 起始/停止条件正确
- 数据建立/保持时间满足规格
- 时钟频率符合预期
-
检查上拉电阻值:
- 标准模式(100kHz):通常4.7kΩ
- 快速模式(400kHz):通常2.2kΩ
-
测量总线电容:
- 总线上所有器件的输入电容之和不应超过400pF
5.3 多主机仲裁
当多个主机同时尝试控制总线时,STM32F103的I2C模块支持仲裁丢失检测:
c复制if(I2C1->SR1 & I2C_SR1_ARLO) {
// 仲裁丢失处理
I2C1->SR1 &= ~I2C_SR1_ARLO; // 清除标志
I2C1->CR1 |= I2C_CR1_STOP; // 发送停止条件
// 重新初始化I2C
}
6. 性能优化与高级应用
6.1 DMA传输配置
对于大数据量传输,可以使用DMA减少CPU开销:
c复制// 配置I2C1 TX DMA
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
DMA1_Channel6->CCR = DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_TCIE;
DMA1_Channel6->CPAR = (uint32_t)&I2C1->DR;
DMA1_Channel6->CMAR = (uint32_t)txBuffer;
DMA1_Channel6->CNDTR = txLength;
// 启用I2C DMA请求
I2C1->CR2 |= I2C_CR2_DMAEN;
6.2 时钟延展支持
某些I2C从设备可能需要时钟延展(clock stretching):
c复制// 启用时钟延展
I2C1->CR1 |= I2C_CR1_NOSTRETCH;
// 在从机中断中处理
if(I2C1->SR1 & I2C_SR1_AF) {
// 从机需要更多时间处理数据
delay_us(100);
I2C1->SR1 &= ~I2C_SR1_AF;
}
6.3 多从机系统设计
在具有多个从机的系统中,建议:
- 为每个从机分配唯一地址
- 使用GPIO扩展器管理从机使能信号
- 实现软件重试机制应对总线冲突
c复制#define MAX_RETRY 3
uint8_t I2C_WriteWithRetry(I2C_TypeDef* I2Cx, uint8_t devAddr, uint8_t reg, uint8_t data) {
uint8_t retry = 0;
uint8_t status = 0;
while(retry < MAX_RETRY) {
status = I2C_WriteByte(I2Cx, devAddr, reg, data);
if(status == SUCCESS) break;
// 复位I2C总线
I2C_ResetBus(I2Cx);
retry++;
}
return status;
}
7. 实际应用案例:连接LCD1602(I2C接口)
以下是通过I2C控制常见的LCD1602显示模块的完整示例:
7.1 硬件连接
- LCD1602的I2C接口通常使用PCF8574T芯片(地址0x27或0x3F)
- SCL连接PB6,SDA连接PB7
- VCC接5V,GND接地
7.2 初始化序列
c复制void LCD_Init(I2C_TypeDef* I2Cx, uint8_t devAddr) {
// 初始化命令序列
uint8_t initSeq[] = {
0x30, // 8位模式
0x30, // 重复
0x30, // 重复
0x20, // 切换到4位模式
0x28, // 2行,5x8点阵
0x0C, // 显示开,光标关
0x06, // 增量模式,不移位
0x01 // 清屏
};
// 发送初始化命令
for(int i=0; i<sizeof(initSeq); i++) {
LCD_SendCommand(I2Cx, devAddr, initSeq[i]);
delay_ms(5);
}
}
void LCD_SendCommand(I2C_TypeDef* I2Cx, uint8_t devAddr, uint8_t cmd) {
uint8_t highNibble = (cmd & 0xF0) | 0x04; // EN=1
uint8_t lowNibble = ((cmd << 4) & 0xF0) | 0x04; // EN=1
// 发送高4位
I2C_WriteByte(I2Cx, devAddr, highNibble);
delay_us(1);
I2C_WriteByte(I2Cx, devAddr, highNibble & ~0x04); // EN=0
delay_us(100);
// 发送低4位
I2C_WriteByte(I2Cx, devAddr, lowNibble);
delay_us(1);
I2C_WriteByte(I2Cx, devAddr, lowNibble & ~0x04); // EN=0
delay_ms(2);
}
7.3 显示字符串
c复制void LCD_PrintString(I2C_TypeDef* I2Cx, uint8_t devAddr, const char* str) {
while(*str) {
LCD_SendData(I2Cx, devAddr, *str++);
}
}
void LCD_SendData(I2C_TypeDef* I2Cx, uint8_t devAddr, uint8_t data) {
uint8_t highNibble = (data & 0xF0) | 0x05; // RS=1, EN=1
uint8_t lowNibble = ((data << 4) & 0xF0) | 0x05; // RS=1, EN=1
// 发送高4位
I2C_WriteByte(I2Cx, devAddr, highNibble);
delay_us(1);
I2C_WriteByte(I2Cx, devAddr, highNibble & ~0x04); // EN=0
delay_us(100);
// 发送低4位
I2C_WriteByte(I2Cx, devAddr, lowNibble);
delay_us(1);
I2C_WriteByte(I2Cx, devAddr, lowNibble & ~0x04); // EN=0
delay_ms(2);
}
通过以上代码,我们可以实现完整的LCD1602控制功能。在实际项目中,建议将I2C操作封装为独立的驱动层,提高代码复用性。
