第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作为主机去读温度传感器,这是最常见的场景。从模式我后面会单独讲。

主模式发送流程:

  1. 配置LPI2C为主模式,设置好时钟和引脚。
  2. 发送起始条件,同时发送从机地址+写位(0)。
  3. 等待从机应答。
  4. 发送寄存器地址(告诉从机你要读哪个寄存器)。
  5. 等待从机应答。
  6. 发送停止条件。

主模式接收流程:

  1. 发送起始条件,发送从机地址+读位(1)。
  2. 等待从机应答。
  3. 读取数据,每读一个字节后主机发送应答(ACK)。
  4. 读最后一个字节时,主机发送非应答(NACK),告诉从机不用再发了。
  5. 发送停止条件。

这里有个细节:很多温度传感器需要你先写寄存器地址,再重新发送起始条件(这叫重复起始条件)去读数据。你想想看,如果不这样做,从机不知道你要读哪个寄存器。

// 伪代码示例:读取温度传感器
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快,但线也多。有什么问题可以在群里问我,我看到会回复。