第4章 I2C驱动移植:从协议到实战
各位同学,今天我们来聊聊I2C。说实话,I2C这玩意儿在汽车电子里太常见了——温度传感器、EEPROM、触摸控制器,几乎每个板子上都有。我最早接触I2C是在做车身控制器的时候,当时被一个从机应答问题折腾了两天,后来才发现是上拉电阻选错了。嗯,这些坑咱们今天一并讲清楚。
4.1 I2C总线协议速览
I2C总线,说白了就是两根线:SCL(时钟)和SDA(数据)。它是个多主从架构,但咱们在嵌入式里最常见的就是单主机多从机模式。S32K作为主机,外设作为从机。
几个关键点你得记住:
- 起始条件:SCL高电平时,SDA从高变低。这是通信开始的标志。
- 停止条件:SCL高电平时,SDA从低变高。通信结束。
- 数据有效性:SCL低电平时改变SDA,SCL高电平时采样SDA。这个顺序搞反了,数据就全乱了。
- 应答机制:每发送8位数据后,接收方要拉低SDA表示应答(ACK)。如果没应答(NACK),主机就得停止或重发。
我个人习惯:在写I2C驱动前,先用逻辑分析仪抓一下波形,确认总线上没有异常。我曾经遇到过从机地址冲突,两个器件用了同一个地址,结果通信时好时坏,查了三天才找到原因。
I2C的速率分几种:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。S32K的LPI2C模块支持到1MHz,但实际项目中我建议别跑太快,尤其是线比较长的时候。400kHz是个稳妥的选择。
4.2 S32K LPI2C模块详解
S32K的LPI2C模块,全称是Low Power Inter-Integrated Circuit。它比传统I2C模块多了几个好处:
- 低功耗:可以在停止模式下工作,适合需要休眠唤醒的场景。
- FIFO支持:发送和接收各有4个字节的FIFO,减少CPU干预。
- DMA支持:可以配合DMA实现批量数据传输,不占CPU。
寄存器配置要点:
| 寄存器 | 功能 | 我常用的值 |
|---|---|---|
| MCR | 模块控制寄存器,使能LPI2C | 0x00000001 |
| MCFGR1 | 配置时钟分频、引脚极性等 | 根据总线速率计算 |
| MCCR0 | 主模式时钟配置,决定SCL频率 | 见下方代码 |
| MSR | 状态寄存器,检查传输是否完成 | 轮询或中断使用 |
配置时钟分频时,有个公式你得记住:
SCL频率 = 模块时钟 / (2 * (CLKLO + 1) * (CLKHI + 1))
举个例子,如果模块时钟是40MHz,想要400kHz的SCL,那么CLKLO和CLKHI都设为49,算出来就是:
40000000 / (2 * 50 * 50) = 400000 Hz = 400kHz
避坑指南:我曾经把CLKLO和CLKHI设成一样的值,结果发现上升沿和下降沿时间不对称,导致从机采样出错。后来我习惯把CLKLO设得比CLKHI大一点,比如CLKLO=50,CLKHI=48,这样占空比更均衡。
4.3 主从模式实现
咱们这次只讲主模式,因为S32K作为主机去读温度传感器,这是最常见的场景。从模式我后面会单独讲。
主模式发送流程:
- 配置LPI2C为主模式,设置好时钟和引脚。
- 发送起始条件,同时发送从机地址+写位(0)。
- 等待从机应答。
- 发送寄存器地址(告诉从机你要读哪个寄存器)。
- 等待从机应答。
- 发送停止条件。
主模式接收流程:
- 发送起始条件,发送从机地址+读位(1)。
- 等待从机应答。
- 读取数据,每读一个字节后主机发送应答(ACK)。
- 读最后一个字节时,主机发送非应答(NACK),告诉从机不用再发了。
- 发送停止条件。
这里有个细节:很多温度传感器需要你先写寄存器地址,再重新发送起始条件(这叫重复起始条件)去读数据。你想想看,如果不这样做,从机不知道你要读哪个寄存器。
// 伪代码示例:读取温度传感器
uint8_t read_temp_sensor(uint8_t slave_addr, uint8_t reg_addr) {
uint8_t temp_value = 0;
// 第一步:写寄存器地址
LPI2C0->MDR = (slave_addr << 1) | 0; // 从机地址 + 写位
LPI2C0->MCR |= LPI2C_MCR_MEN_MASK; // 使能模块
// 等待传输完成...
LPI2C0->MDR = reg_addr; // 发送寄存器地址
// 等待传输完成...
// 第二步:重复起始条件,读数据
LPI2C0->MCR |= LPI2C_MCR_RSTA_MASK; // 重复起始
LPI2C0->MDR = (slave_addr << 1) | 1; // 从机地址 + 读位
// 等待传输完成...
temp_value = LPI2C0->MDR; // 读取温度值
// 发送NACK和停止条件...
return temp_value;
}
注意:上面的代码是简化版,实际项目中要加超时处理。我曾经遇到过从机死锁,一直不应答,如果没有超时,程序就卡死在那里了。建议加一个100ms的超时,超时后复位I2C模块。
4.4 实战:读取温度传感器
咱们用S32K144读取一个常见的温度传感器——LM75。这个芯片的I2C地址是0x48(7位地址),温度寄存器地址是0x00,数据格式是11位有符号数。
硬件连接:
- SDA -> PTC1 (LPI2C0)
- SCL -> PTC0 (LPI2C0)
- VCC -> 3.3V
- GND -> GND
- 上拉电阻:4.7kΩ(这个很重要!)
完整代码示例:
#include "S32K144.h"
#define LM75_ADDR 0x48
#define TEMP_REG 0x00
#define I2C_TIMEOUT 100000
void LPI2C0_Init(void) {
// 使能时钟
PCC->PCCn[PCC_LPI2C0_INDEX] = PCC_PCCn_CGC_MASK;
// 配置引脚为LPI2C功能
PORTD->PCR[1] = PORT_PCR_MUX(6); // PTC1 -> SDA
PORTD->PCR[0] = PORT_PCR_MUX(6); // PTC0 -> SCL
// 复位模块
LPI2C0->MCR = LPI2C_MCR_RST_MASK;
LPI2C0->MCR = 0;
// 配置时钟:40MHz模块时钟,400kHz SCL
LPI2C0->MCFGR1 = LPI2C_MCFGR1_PINCFG(0); // 开源模式
LPI2C0->MCCR0 = LPI2C_MCCR0_CLKLO(50) | LPI2C_MCCR0_CLKHI(48);
// 使能模块
LPI2C0->MCR = LPI2C_MCR_MEN_MASK;
}
int16_t LM75_ReadTemp(void) {
uint8_t temp_h, temp_l;
int16_t temperature;
// 写寄存器地址
LPI2C0->MTDR = (LM75_ADDR << 1) | 0; // 地址+写
while(!(LPI2C0->MSR & LPI2C_MSR_TDF_MASK)); // 等待发送完成
LPI2C0->MTDR = TEMP_REG; // 温度寄存器地址
while(!(LPI2C0->MSR & LPI2C_MSR_TDF_MASK));
// 重复起始,读数据
LPI2C0->MCR |= LPI2C_MCR_RSTA_MASK;
LPI2C0->MTDR = (LM75_ADDR << 1) | 1; // 地址+读
while(!(LPI2C0->MSR & LPI2C_MSR_TDF_MASK));
// 读高字节(发送ACK)
LPI2C0->MCR |= LPI2C_MCR_ACK_MASK; // 使能应答
temp_h = LPI2C0->MRDR;
while(!(LPI2C0->MSR & LPI2C_MSR_RDF_MASK));
// 读低字节(发送NACK)
LPI2C0->MCR &= ~LPI2C_MCR_ACK_MASK; // 禁用应答
temp_l = LPI2C0->MRDR;
while(!(LPI2C0->MSR & LPI2C_MSR_RDF_MASK));
// 发送停止条件
LPI2C0->MCR |= LPI2C_MCR_STOP_MASK;
// 计算温度值(11位有符号数)
temperature = (temp_h << 8) | temp_l;
temperature = temperature >> 5; // 右移5位,得到11位数据
// 如果是负数,扩展符号位
if(temperature & 0x0400) {
temperature |= 0xF800;
}
return temperature; // 单位:0.125°C
}
int main(void) {
int16_t temp;
LPI2C0_Init();
while(1) {
temp = LM75_ReadTemp();
// 温度值 = temp * 0.125
// 比如temp=200,表示25.0°C
// 这里可以加串口打印或CAN发送
for(int i=0; i<1000000; i++); // 简单延时
}
}
经验之谈:我第一次调这个代码时,读出来的温度一直是0x7FFF。查了半天,发现是引脚复用没配对。S32K的引脚功能很多,一定要对照数据手册确认MUX值。另外,上拉电阻我建议用4.7kΩ,如果总线电容大,可以换成2.2kΩ。
4.5 常见问题与调试技巧
问题1:总线卡死,SCL或SDA一直低电平
这通常是从机异常导致的。解决办法:在初始化时加一个总线恢复序列——发送9个时钟脉冲,让从机复位。我习惯在LPI2C初始化前先做这个操作。
void I2C_Bus_Recovery(void) {
// 模拟9个SCL脉冲
for(int i=0; i<9; i++) {
// 拉高SCL
// 拉低SCL
}
// 发送停止条件
}
问题2:读取温度值跳动很大
可能是电源噪声或者布线问题。我建议在传感器VCC和GND之间加一个0.1μF的电容,离芯片越近越好。另外,I2C线不要太长,超过10cm就容易出问题。
问题3:从机无应答
先检查地址对不对。LM75的7位地址是0x48,但有些手册写的是8位地址0x90(左移一位)。我刚开始就搞混过,浪费了半天时间。记住:S32K的LPI2C模块用的是7位地址,不需要左移。
调试小技巧:用逻辑分析仪抓波形是最直接的方法。如果没有逻辑分析仪,可以用GPIO模拟I2C时序,先确认硬件没问题,再用LPI2C模块。我早期做项目时,经常用这种方法隔离问题。
好了,I2C驱动移植就讲到这里。下一章咱们聊SPI,那个比I2C快,但线也多。有什么问题可以在群里问我,我看到会回复。