1. TWI/I2C接口基础解析
在嵌入式系统开发中,TWI(Two-Wire Interface)与I2C(Inter-Integrated Circuit)本质上是同一种通信协议的不同称呼。这种双线制串行通信协议因其简洁的硬件连接和灵活的扩展性,成为连接各类传感器、存储器和外设的首选方案。
1.1 物理层特性
TWI/I2C总线仅需两根信号线:
- SCL(Serial Clock):时钟线,由主设备产生
- SDA(Serial Data):双向数据线
总线采用开漏输出设计,需外接上拉电阻(通常4.7kΩ)。这种设计实现了"线与"逻辑,允许多主设备通过仲裁机制共享总线。实际应用中,我曾遇到因上拉电阻值选择不当导致的信号完整性问题——电阻过大时边沿变缓,过小时功耗增加,需要根据总线电容和传输速率仔细计算。
1.2 协议层要点
通信过程遵循严格的时序规范:
- 起始条件(START):SCL高电平时SDA由高变低
- 地址帧:7位/10位设备地址 + 1位读写标志
- 数据帧:8位数据 + 1位ACK/NACK
- 停止条件(STOP):SCL高电平时SDA由低变高
在调试I2C设备时,我习惯先用逻辑分析仪捕获总线波形,确认起始/停止条件和ACK信号是否正常。常见的问题包括地址不匹配(设备响应NACK)和时钟速率过高(导致信号畸变)。
2. 寄存器级编程实践
2.1 寄存器映射实现
对于ARM Cortex-M内核的MCU,通过结构体映射寄存器是最佳实践:
c复制typedef struct {
__IO uint32_t CR; // 控制寄存器
__IO uint32_t SR; // 状态寄存器
__IO uint32_t DR; // 数据寄存器
// ...其他寄存器
} TWI_TypeDef;
#define TWI0_BASE 0x40048000UL
#define TWI0 ((TWI_TypeDef *)TWI0_BASE)
这种映射方式:
- 利用编译器特性确保寄存器访问的原子性
- 通过指针直接访问物理地址,效率最高
- 代码可读性强,便于维护
注意:不同厂商的MCU寄存器命名可能不同,需仔细核对参考手册。例如ST的I2C_CR1 vs NXP的I2C_C1。
2.2 主模式初始化
配置主设备需要关注三个关键参数:
- 时钟频率(标准模式100kHz,快速模式400kHz)
- 时钟占空比(通常50%)
- 自身地址(主模式下可选)
c复制void TWI_Master_Init(uint32_t freq)
{
// 1. 禁用外设
TWI0->CR &= ~TWI_ENABLE;
// 2. 计算时钟分频
uint32_t pclk = SystemCoreClock;
uint32_t div = (pclk / (2 * freq)) - 4;
TWI0->CKDIV = div;
// 3. 配置控制寄存器
TWI0->CR = TWI_MASTER | TWI_ACK_EN | TWI_INT_EN;
// 4. 使能外设
TWI0->CR |= TWI_ENABLE;
}
实际项目中,我发现初始化顺序很重要——必须先配置时钟再使能模块,否则可能导致总线锁死。
2.3 从模式配置
从设备需要设置自身地址和中断响应:
c复制void TWI_Slave_Init(uint8_t addr)
{
// 1. 禁用外设
TWI0->CR &= ~TWI_ENABLE;
// 2. 设置自身地址
TWI0->ADDR = addr & 0x7F; // 7位地址
// 3. 配置控制寄存器
TWI0->CR = TWI_SLAVE | TWI_ACK_EN | TWI_ADDR_INT_EN;
// 4. 使能外设
TWI0->CR |= TWI_ENABLE;
// 5. 配置NVIC中断
NVIC_EnableIRQ(TWI0_IRQn);
}
在从机设计中,地址匹配逻辑需要特别注意。某些设备支持地址掩码(如TWIADDRMR寄存器),可以实现多地址响应。
3. 通信流程实现
3.1 主设备写操作
标准写序列实现如下:
c复制bool TWI_Write(uint8_t addr, uint8_t *data, uint8_t len)
{
// 1. 发送START
TWI0->CR |= TWI_START;
while(!(TWI0->SR & TWI_START_SENT));
// 2. 发送地址+写标志
TWI0->DR = (addr << 1) | 0;
while(!(TWI0->SR & TWI_ADDR_ACK));
// 3. 发送数据
for(uint8_t i = 0; i < len; i++) {
TWI0->DR = data[i];
while(!(TWI0->SR & TWI_DATA_SENT));
if(TWI0->SR & TWI_NACK_RCVD) {
TWI0->CR |= TWI_STOP;
return false;
}
}
// 4. 发送STOP
TWI0->CR |= TWI_STOP;
return true;
}
调试技巧:在每次状态检查后读取SR寄存器值,可以快速定位通信失败的原因(如仲裁丢失、无应答等)。
3.2 主设备读操作
读操作需要管理ACK/NACK:
c复制bool TWI_Read(uint8_t addr, uint8_t *buf, uint8_t len)
{
// 1. 发送START
TWI0->CR |= TWI_START;
while(!(TWI0->SR & TWI_START_SENT));
// 2. 发送地址+读标志
TWI0->DR = (addr << 1) | 1;
while(!(TWI0->SR & TWI_ADDR_ACK));
// 3. 接收数据
for(uint8_t i = 0; i < len; i++) {
if(i == len - 1) {
TWI0->CR &= ~TWI_ACK_EN; // 最后字节发NACK
}
while(!(TWI0->SR & TWI_DATA_RCVD));
buf[i] = TWI0->DR;
}
// 4. 发送STOP
TWI0->CR |= TWI_STOP;
return true;
}
常见错误是忘记在最后一个字节前发送NACK,这会导致从设备继续维持总线。
4. 中断驱动设计
4.1 中断服务程序
高效的ISR实现需要:
- 快速识别中断源
- 最小化处理时间
- 妥善处理错误条件
c复制void TWI0_IRQHandler(void)
{
uint32_t status = TWI0->SR;
// 1. 处理接收中断
if(status & TWI_RX_INT) {
rx_buf[rx_idx++] = TWI0->DR;
if(rx_idx >= BUF_SIZE) {
TWI0->CR &= ~TWI_RX_INT_EN;
}
}
// 2. 处理发送中断
if(status & TWI_TX_INT) {
if(tx_idx < tx_len) {
TWI0->DR = tx_buf[tx_idx++];
} else {
TWI0->CR &= ~TWI_TX_INT_EN;
}
}
// 3. 处理错误中断
if(status & TWI_ERROR_FLAGS) {
error_handler(status);
TWI0->CR |= TWI_STOP; // 强制释放总线
}
}
4.2 DMA集成
对于高速数据传输,可以结合DMA:
c复制void TWI_Setup_DMA(void)
{
// 1. 配置DMA通道
DMA_Config(TWI0_DR_ADDR, buffer, length, DMA_DIR_M2P);
// 2. 使能TWI DMA请求
TWI0->CR |= TWI_DMA_TX_EN;
// 3. 启动传输
DMA_Start();
}
DMA模式下需要注意总线超时问题,建议启用TWIEOUT超时寄存器。
5. 实战经验与排错
5.1 常见问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 总线死锁 | 异常中断导致SCL被拉低 | 发送STOP信号或硬件复位 |
| 无ACK响应 | 地址错误/设备未就绪 | 检查设备地址和电源 |
| 数据错误 | 时序不满足建立保持时间 | 降低时钟频率或调整相位 |
| 仲裁丢失 | 多主竞争 | 实现重试机制 |
5.2 性能优化技巧
-
时钟配置:在允许范围内使用最高速率,但需考虑总线电容影响。我曾通过将100kHz提升到400kHz,使24LC256 EEPROM的写入速度提升3倍。
-
中断优化:合并状态检查,减少ISR执行时间。例如:
c复制if(status & (TWI_RX_INT | TWI_TX_INT)) {
handle_data_transfer();
}
-
批量传输:对于连续地址访问,利用设备支持的页写/顺序读功能。例如AT24Cxx系列支持64字节页写。
-
总线监控:添加诊断代码记录错误计数,便于后期分析:
c复制struct {
uint32_t nack_count;
uint32_t arb_lost;
uint32_t timeouts;
} twi_stats;
在最近的一个机器人项目中,我通过优化TWI中断处理程序,将IMU传感器的数据读取延迟从1.2ms降低到0.4ms,显著提高了控制系统的响应速度。关键是在ISR中只做必要的数据搬运,将解析处理放到主循环。