I2C总线协议深度解析与Stellaris微控制器实战应用

1. I2C总线协议深度解析:从物理层到数据链路层

I2C,全称Inter-Integrated Circuit,是飞利浦半导体(现恩智浦)在1980年代推出的一种同步、多主从、串行、半双工通信总线。在嵌入式开发领域,尤其是资源受限的单片机系统中,I2C几乎无处不在。你手边的温湿度传感器、OLED屏幕、EEPROM存储器,大概率都通过这两根线与你MCU的GPIO相连。它的魅力在于极简的硬件需求——仅需两根线(串行数据线SDA和串行时钟线SCL),就能构建一个支持多设备通信的网络,这对于引脚资源宝贵的微控制器而言,无疑是巨大的福音。

但“简单”往往意味着协议层面的精巧与复杂。I2C协议在两根线上实现了完整的通信规则,包括起始/停止条件、地址寻址、数据有效性、应答机制和时钟同步。理解这些底层原理,是写出稳定、可靠I2C驱动代码的前提。比如,为什么SDA线上的数据必须在SCL为低电平时变化?这涉及到数据稳定性的问题。为什么每个字节传输后必须跟一个应答位?这确保了通信的可靠性。多主竞争时,如何决定谁获得总线控制权?这依赖于线与逻辑和仲裁机制。这些细节,手册上可能一笔带过,但在实际调试中,任何一个环节理解不透,都可能导致通信失败,而你用逻辑分析仪抓到的波形,将成为破解谜题的唯一线索。

1.1 物理层与电气特性:线与逻辑的智慧

I2C总线的物理连接极其简单:所有设备(主设备和从设备)的SDA和SCL引脚分别并联在一起,并通过上拉电阻连接到正电源VCC。这个上拉电阻的阻值选择颇有讲究,通常在1kΩ到10kΩ之间,它直接影响了总线的上升时间和最大通信速率。阻值太小,电流过大,可能损坏IO口;阻值太大,上升沿过缓,在高频下会导致数据采样失败。我个人的经验是,在标准模式(100kHz)下,4.7kΩ是个比较通用的选择;在快速模式(400kHz)下,可能需要减小到2.2kΩ甚至更低,具体需根据总线电容和电源电压计算。

这里隐藏着一个关键设计:I2C接口是开漏(Open-Drain)或开集(Open-Collector)输出。这意味着任何设备只能将总线拉低(输出低电平),而不能主动拉高。总线的高电平状态完全由上拉电阻建立。这种“线与”(Wired-AND)逻辑是实现多主仲裁和时钟同步的基础。当多个主机同时发送数据时,只要有一个主机输出低电平,总线就是低电平;只有所有主机都释放总线(输出高阻态),上拉电阻才能将总线拉至高电平。这种机制使得总线上的电平状态是所有设备输出的“逻辑与”结果。

注意 :务必确保总线上每个设备(包括MCU)的I2C引脚都配置为开漏模式,并启用内部上拉或外接上拉电阻。如果配置为推挽输出,当两个设备一个输出高、一个输出低时,会形成电源到地的直接短路,可能瞬间烧毁IO口。这是新手最容易犯的硬件错误之一。

1.2 数据链路层:帧结构与状态机

一次完整的I2C通信,遵循着严格的帧格式。我们可以把它想象成一次有礼貌的对话:

  1. 起始条件(S) :主设备在SCL为高电平时,将SDA从高拉低。这个独特的下降沿信号告诉总线上所有设备:“注意,我要开始讲话了”。
  2. 地址帧 :主设备发送7位(或10位)从设备地址,后面紧跟1位读写方向位(0表示写,1表示读)。例如,向地址0x68的设备写入数据,发送的字节是 0xD0 (0x68 << 1 | 0)。
  3. 应答位(ACK/NACK) :地址帧发送后的第9个时钟周期,主设备释放SDA线(输出高阻态),由被寻址的从设备将SDA拉低作为应答(ACK),表示“我在,请继续”。如果地址无设备响应,SDA将保持高电平(NACK)。
  4. 数据帧 :在得到ACK后,发送方(可能是主或从)开始传输8位数据,每个字节后同样跟一个应答位。
  5. 停止条件(P) :主设备在SCL为高电平时,将SDA从低拉高。表示“本次对话结束”。

对于突发(Burst)传输,主设备发送完一个字节后,如果不发停止条件,而是再次发送起始条件(称为“重复起始条件”,Sr),就可以在不释放总线的情况下,改变数据传输方向或与另一个从设备通信,这提高了总线利用效率。

理解这些状态转换,对于调试至关重要。当你用逻辑分析仪抓取波形时,你需要能清晰地辨认出S、地址、ACK、数据、P这些元素,并判断在哪一步出现了问题。是地址不对?还是从设备没有应答?或者是数据位采样错误?

2. Stellaris微控制器I2C模块架构与API设计哲学

Stellaris(现属于TI的Tiva系列)微控制器的I2C模块是一个高度集成化的硬件外设,它将上述复杂的协议状态机用硬件逻辑实现,极大地减轻了CPU的负担。开发者无需手动翻转GPIO来模拟时序,只需配置好寄存器,操作数据缓冲区,并通过查询或中断方式获知传输状态即可。

2.1 主从模块的双重角色与并发支持

Stellaris的I2C模块设计精妙之处在于,它同时集成了独立的主机(Master)和从机(Slave)控制器。这意味着同一颗MCU既可以作为主机去控制其他传感器,也可以作为从机被其他主机(如另一颗MCU或上位机)访问。更强大的是,它支持主从模式同时运行。例如,你的设备可以作为一个从机,接收来自中央控制器的指令;同时,它又可以作为主机,去读取本地传感器的数据。这种灵活性在复杂的嵌入式网络中非常有用。

模块支持两种标准速率:标准模式(100 kbps)和快速模式(400 kbps)。初始化时通过 ROM_I2CMasterInitExpClk() 函数的 bFast 参数进行选择。这里有一个细节: ulI2CClk 参数是输入给I2C模块的时钟频率,通常是系统时钟。模块内部的分频器会根据这个时钟和设定的总线速率,自动计算出SCL时钟的分频系数。如果计算出的分频系数过小(时钟源太快而目标速率太慢),可能会导致配置失败。因此,在系统时钟很高的MCU上配置100kHz的I2C时,需要检查分频器是否溢出。

2.2 ROM API vs 直接寄存器操作

你提供的资料中,所有函数都以 ROM_ 开头。这是Stellaris系列的一个特色:它将常用的外设驱动库(DriverLib)固化在了芯片内部的ROM中。调用这些ROM API,可以节省宝贵的Flash空间。对于Flash容量紧张的项目,这非常有用。当然,你也可以选择使用链接到Flash中的最新版DriverLib,以获得可能的功能更新和Bug修复。两者的函数名和功能基本一致,只是前缀不同(ROM API带 ROM_ )。

使用ROM API时,需要注意其入口地址通过查表获得。例如 ROM_I2CMasterInitExpClk 的指针位于 ROM_APITABLE[3][1] 。在代码中,我们直接调用函数即可,链接器会自动处理这些地址。但了解这一点,有助于你在调试时,如果遇到跳转到奇怪地址的HardFault,可以联想到是否是ROM API表损坏或访问越界。

3. 主模式(Master)操作全流程与实战代码剖析

作为主机,是I2C通信的发起者和控制者。Stellaris的API将主机操作抽象为几个清晰的步骤:初始化、设置从机地址、检查总线忙、放置数据、发送控制命令、等待完成、检查错误、读取数据。下面我们结合代码,深入每一步的细节和陷阱。

3.1 初始化与总线配置

一切始于初始化。 ROM_I2CMasterInitExpClk(I2C0_BASE, SysCtlClockGet(), false) 是一个典型的调用。

  • I2C0_BASE :指定使用哪个I2C模块(如I2C0, I2C1)。
  • SysCtlClockGet() :获取当前系统时钟频率,作为 ulI2CClk 参数传入。 这里有个坑 :如果你在系统初始化早期就调用I2C初始化,此时系统时钟可能还未配置到最终频率(例如还是用的内部振荡器低速时钟)。这会导致计算出的分频系数错误,I2C实际速率远高于设定值,从而通信失败。稳妥的做法是,在系统时钟稳定配置后再初始化外设。
  • false :选择标准模式(100kbps)。设为 true 则为快速模式(400kbps)。

初始化函数内部会启用该I2C模块的时钟(如果尚未启用),并配置相应GPIO引脚为I2C功能。 务必查阅数据手册的“引脚复用”章节 ,确认你使用的SDA和SCL引脚是否正确映射到了I2C功能上。例如,某个引脚可能默认是GPIO,需要设置AFSEL(交替功能选择)寄存器才能作为I2C使用,而 ROM_I2CMasterInitExpClk 可能不会帮你做这件事(取决于具体型号和库版本)。最保险的方法是,在初始化I2C前,先调用GPIO的复用配置函数。

3.2 单次发送(Single Send)流程与轮询实现

单次发送是最基本的操作:主机向从机写入一个字节的数据。我们以向地址0x50的EEPROM写入一个字节0xAB为例,拆解轮询方式的代码:

#include <stdbool.h>
#include <stdint.h>
#include "inc/hw_i2c.h"
#include "inc/hw_memmap.h"
#include "inc/hw_types.h"
#include "driverlib/i2c.h"
#include "driverlib/rom.h"
#include "driverlib/sysctl.h"

// 假设系统时钟已正确配置
void I2C_WriteSingleByte(uint8_t slaveAddr, uint8_t data) {
    uint32_t ui32Status;

    // 1. 设置目标从机地址和写方向
    ROM_I2CMasterSlaveAddrSet(I2C0_BASE, slaveAddr, false); // false 表示写

    // 2. 在多发系统中,检查总线是否被其他主机占用
    while(ROM_I2CMasterBusBusy(I2C0_BASE)) {
        // 忙等待,或可以加入超时机制
    }

    // 3. 将要发送的数据放入数据寄存器
    ROM_I2CMasterDataPut(I2C0_BASE, data);

    // 4. 发起“单次发送”命令,启动传输
    // 此命令会:仲裁总线、发送起始位、发送地址帧+写位、发送数据字节、等待ACK、发送停止位
    ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_SINGLE_SEND);

    // 5. 轮询等待本次传输完成(主机不再繁忙)
    while(ROM_I2CMasterBusy(I2C0_BASE));

    // 6. 关键步骤:检查传输过程中是否发生错误
    ui32Status = ROM_I2CMasterErr(I2C0_BASE);
    if(ui32Status != I2C_MASTER_ERR_NONE) {
        // 错误处理
        switch(ui32Status) {
            case I2C_MASTER_ERR_ADDR_ACK:
                // 从机地址无应答(设备不存在或地址错误)
                break;
            case I2C_MASTER_ERR_DATA_ACK:
                // 数据无应答(从机可能忙或写入被拒绝)
                break;
            case I2C_MASTER_ERR_ARB_LOST:
                // 仲裁丢失(在多主系统中被其他主机抢占)
                break;
            default:
                break;
        }
        // 通常需要重置I2C主机状态或进行其他恢复操作
        ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_ERROR_STOP); // 发送错误停止序列
        return; // 或进行重试
    }
    // 7. 传输成功
}

关键点与避坑指南

  • 步骤2的 ROM_I2CMasterBusBusy :在单主系统中,通常可以省略。但在多主系统或总线可能被其他硬件(如DMA)占用时,这是必需的。否则,直接发起传输会导致仲裁丢失错误。
  • 步骤5的轮询 ROM_I2CMasterBusy 函数查询的是主机控制器内部状态,而非总线物理状态。它会在主机完成(或出错)当前命令后返回 false 。这是一个阻塞调用,在实时性要求高的系统中,应考虑使用中断方式。
  • 步骤6的错误检查 这是最容易被忽略但最重要的环节! 很多初学者代码只发送不检查错误,通信失败时现象诡异,难以调试。 ROM_I2CMasterErr 必须在传输完成后立即检查,因为它反映的是 上一次 传输的错误状态。
  • 超时机制 :上述代码中的 while 循环是无限等待,在实际产品中必须加入超时判断,防止因从机故障导致程序死锁。

3.3 突发传输(Burst Transfer)与流控制

单次传输效率低。读写多个字节时,应使用突发传输。突发传输分为开始(START)、继续(CONT)、结束(FINISH)和错误停止(ERROR_STOP)几个阶段。以连续写入多个字节为例:

bool I2C_WriteMultipleBytes(uint8_t slaveAddr, const uint8_t *pData, uint32_t dataLen) {
    if(dataLen == 0) return true;
    if(ROM_I2CMasterBusBusy(I2C0_BASE)) return false; // 总线忙

    ROM_I2CMasterSlaveAddrSet(I2C0_BASE, slaveAddr, false); // 写方向

    // 1. 发送突发开始命令,并发送第一个字节
    ROM_I2CMasterDataPut(I2C0_BASE, pData[0]);
    ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_START);
    while(ROM_I2CMasterBusy(I2C0_BASE));
    if(ROM_I2CMasterErr(I2C0_BASE) != I2C_MASTER_ERR_NONE) {
        // 处理错误...
        return false;
    }

    // 2. 循环发送中间字节
    for(uint32_t i = 1; i < dataLen - 1; i++) {
        ROM_I2CMasterDataPut(I2C0_BASE, pData[i]);
        ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_CONT);
        while(ROM_I2CMasterBusy(I2C0_BASE));
        if(ROM_I2CMasterErr(I2C0_BASE) != I2C_MASTER_ERR_NONE) {
            // 发送错误停止序列,释放总线
            ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_ERROR_STOP);
            return false;
        }
    }

    // 3. 发送最后一个字节,并以停止条件结束
    ROM_I2CMasterDataPut(I2C0_BASE, pData[dataLen - 1]);
    ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_FINISH);
    while(ROM_I2CMasterBusy(I2C0_BASE));
    if(ROM_I2CMasterErr(I2C0_BASE) != I2C_MASTER_ERR_NONE) {
        // 即使最后一个字节出错,总线也已由FINISH命令释放
        return false;
    }
    return true;
}

突发传输的核心逻辑

  • BURST_SEND_START :发起传输,发送地址和第一个数据字节,但 不产生停止条件 ,总线保持占用。
  • BURST_SEND_CONT :在总线已被占用的情况下,继续发送后续数据字节。从机会在每个字节后回复ACK。
  • BURST_SEND_FINISH :发送最后一个数据字节,并在其后产生停止条件,释放总线。
  • BURST_SEND_ERROR_STOP :在传输任何阶段出错时调用,它会在总线上产生一个停止条件,目的是 将总线状态机复位到一个已知的空闲状态 ,这是错误恢复的关键一步。如果不调用,总线可能处于一种“挂起”状态,导致后续所有通信失败。

重要经验 :突发读操作( BURST_RECEIVE_* )的流程类似,但有一个关键区别:在发送了 BURST_RECEIVE_START (包含地址和读方向位)后,主机需要 在发送CONT命令之前,先读取数据寄存器 。因为从机在收到读地址并回复ACK后,就会开始发送数据。主机需要发一个“虚”的CONT命令(实际上是为从机提供时钟以读出数据),然后立即读取。具体顺序是:START -> (等待忙) -> 读取数据 -> 发送CONT -> (等待忙) -> 读取数据 -> ... -> 发送FINISH前读取最后一个数据。

3.4 中断驱动模式实现

轮询方式简单,但占用CPU。在复杂的多任务系统中,使用中断是更好的选择。中断方式的核心是配置I2C主机中断,并在中断服务程序(ISR)中处理传输完成事件。

volatile bool g_bI2CTransferComplete = false;
volatile uint32_t g_ui32I2CErrorStatus = 0;

void I2C0MasterIntHandler(void) {
    // 清除中断标志,防止重复进入。注意:需在ISR早期清除。
    ROM_I2CMasterIntClear(I2C0_BASE);

    // 检查错误
    g_ui32I2CErrorStatus = ROM_I2CMasterErr(I2C0_BASE);

    // 传输完成,设置标志位
    g_bI2CTransferComplete = true;
}

void I2C_WriteWithInt(uint8_t slaveAddr, uint8_t data) {
    // 初始化、设置地址、放数据等步骤与轮询相同...
    ROM_I2CMasterSlaveAddrSet(I2C0_BASE, slaveAddr, false);
    ROM_I2CMasterDataPut(I2C0_BASE, data);

    // 清除完成标志
    g_bI2CTransferComplete = false;
    g_ui32I2CErrorStatus = I2C_MASTER_ERR_NONE;

    // 使能I2C主机中断(需提前注册中断向量和启用总中断)
    ROM_I2CMasterIntEnable(I2C0_BASE);

    // 启动传输
    ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_SINGLE_SEND);

    // 主循环或其他任务可以继续执行...
    // 等待中断标志被设置
    while(!g_bI2CTransferComplete) {
        // 可以进入低功耗模式或执行其他任务
    }

    // 传输完成,检查错误
    if(g_ui32I2CErrorStatus != I2C_MASTER_ERR_NONE) {
        // 错误处理
    }
    ROM_I2CMasterIntDisable(I2C0_BASE); // 关闭中断
}

中断模式注意事项

  1. 中断清除时机 ROM_I2CMasterIntClear 必须在ISR中尽早调用。如资料所述,Cortex-M3有写缓冲区,中断标志清除操作可能需几个周期才生效。如果在ISR末尾清除,可能刚清除完就退出,但标志位实际还未清除,导致处理器立即再次进入同一中断,形成“中断风暴”。
  2. 共享变量 g_bI2CTransferComplete 这类在ISR和主程序间共享的变量,应声明为 volatile ,防止编译器优化。在更复杂的系统中,可能需要关中断来保护这些变量的访问。
  3. 中断使能/禁用 :在启动一次传输前使能中断,在该次传输处理完成后禁用,是一种清晰的流程控制方式,避免无关的中断干扰。

4. 从模式(Slave)操作与多主系统考量

从机模式让你的MCU可以响应其他主机的请求。这在构建分层式或对等网络时非常有用。

4.1 从机初始化与状态查询

从机初始化比主机更简单,主要工作是设置自己的7位从机地址: ROM_I2CSlaveInit(I2C0_BASE, 0x50); 。初始化后,从机模块就开始监听总线上的地址匹配。

从机采用被动响应机制。主程序需要定期轮询(或通过中断)检查从机状态寄存器,看主机发来了什么请求。 ROM_I2CSlaveStatus() 函数返回四种状态:

  • I2C_SLAVE_ACT_NONE :无请求。
  • I2C_SLAVE_ACT_RREQ :主机请求向从机 写入 数据(主机要发数据过来)。此时从机应调用 ROM_I2CSlaveDataGet() 读取数据。
  • I2C_SLAVE_ACT_TREQ :主机请求从从机 读取 数据(主机要读数据)。此时从机应调用 ROM_I2CSlaveDataPut() 放入要发送的数据。
  • I2C_SLAVE_ACT_RREQ_FBR :主机发送了从机地址(匹配成功)且第一个数据字节已收到。这是 RREQ 的一个子状态,用于需要根据第一个字节(通常是寄存器地址)来决定后续操作的情况。

一个简单的轮询从机处理框架如下:

void I2CSlave_PollingTask(void) {
    uint32_t slaveStatus = ROM_I2CSlaveStatus(I2C0_BASE);
    static uint8_t myDataBuffer[10];
    static uint8_t index = 0;

    switch(slaveStatus) {
        case I2C_SLAVE_ACT_RREQ:
            // 主机要写数据给我
            myDataBuffer[index++] = (uint8_t)ROM_I2CSlaveDataGet(I2C0_BASE);
            // 注意:读取数据这个动作本身,就会自动回复ACK给主机。
            // 如果缓冲区满或发生错误,从机无法主动发送NACK,这是I2C从机协议的限制。
            break;
        case I2C_SLAVE_ACT_TREQ:
            // 主机要读我的数据
            ROM_I2CSlaveDataPut(I2C0_BASE, myDataBuffer[index++]); // 准备数据
            // 放入数据后,从机硬件会在主机提供时钟时自动将数据移出。
            break;
        case I2C_SLAVE_ACT_RREQ_FBR:
            // 收到第一个字节,可解析为命令或地址
            index = (uint8_t)ROM_I2CSlaveDataGet(I2C0_BASE); // 假设第一个字节是索引
            break;
        case I2C_SLAVE_ACT_NONE:
        default:
            // 无事可做
            break;
    }
}

4.2 从机中断与高级事件处理

对于实时性要求高的从机,轮询效率低。应使用中断。从机中断源更丰富,包括数据请求、开始条件、停止条件等,可以通过 ROM_I2CSlaveIntEnableEx() 函数分别使能。

// 使能数据中断和停止条件中断
ROM_I2CSlaveIntEnableEx(I2C0_BASE, I2C_SLAVE_INT_DATA | I2C_SLAVE_INT_STOP);

void I2C0SlaveIntHandler(void) {
    uint32_t intStatus = ROM_I2CSlaveIntStatusEx(I2C0_BASE, true); // 获取已使能的中断状态
    ROM_I2CSlaveIntClearEx(I2C0_BASE, intStatus); // 清除对应中断标志

    if(intStatus & I2C_SLAVE_INT_DATA) {
        // 处理数据请求(读或写)
        uint32_t slaveAct = ROM_I2CSlaveStatus(I2C0_BASE);
        // ... 根据slaveAct进行相应的数据放入或取出操作
    }
    if(intStatus & I2C_SLAVE_INT_STOP) {
        // 主机发送了停止条件,一次传输结束。
        // 可以在这里进行缓冲区重置、状态更新等操作。
        g_slaveRxComplete = true;
    }
}

从机开发难点

  • 时钟拉伸 :当从机需要时间准备数据时(例如从Flash读取),它可以拉低SCL线,迫使主机等待,直到从机释放SCL。Stellaris的I2C从机硬件支持此功能,但需要正确配置和处理。
  • 时钟同步 :在多主系统中,所有主机和从机都需要能同步时钟。从机在检测到起始条件后,必须开始对SCL进行采样和同步。
  • 仲裁监听 :作为从机,也需要监听总线仲裁,虽然不参与竞争,但需要知道当前通信是否因仲裁丢失而中断。

5. 实战调试技巧与常见问题排查实录

理论终须实践检验。调试I2C通信,逻辑分析仪或示波器是必备工具。下面是我在多年项目中总结的排查清单。

5.1 硬件连接与电源检查

  1. 上拉电阻 :确认SDA和SCL线上都有上拉电阻(通常4.7kΩ到VCC)。用万用表测量总线空闲时的电压,应接近VCC。
  2. 电源与地 :确保主机和所有从机共地。电平不匹配是常见问题,如果从机是3.3V而主机是5V,需要电平转换电路。
  3. 总线电容 :总线过长或设备过多会导致电容过大,上升沿变缓。表现为波形“圆润”,在高频下数据出错。解决方法:减小上拉电阻阻值(增加驱动能力),但注意不要超过IO口 sink current 限值;或降低通信速率。
  4. 引脚配置 :再三确认MCU的I2C引脚已正确配置为开漏模式和复用功能。一个快速测试方法:将引脚配置为GPIO输入,用逻辑分析仪看总线是否有正常波形。如果没有,问题可能出在主机或前级设备。

5.2 软件逻辑与波形分析

当硬件无误后,问题往往在软件。使用逻辑分析仪抓取波形,对照I2C协议逐帧分析。

常见问题1:无ACK(NACK)

  • 现象 :地址帧或数据帧后的第9个时钟周期,SDA线为高(无下拉)。
  • 可能原因及排查
    • 从机地址错误 :用分析仪查看发送的7位地址是否与从设备手册一致。注意,手册地址常是7位,而 ROM_I2CMasterSlaveAddrSet 函数要求传入7位地址,它会自动左移并加上R/W位。
    • 从机未上电或损坏 :检查从机电源和复位。
    • 从机忙 :某些设备(如EEPROM在写周期内)会拉低SDA(时钟拉伸)或回复NACK。需查阅从机数据手册,加入适当延时。
    • 总线冲突 :在多主系统中,地址可能与其他主机冲���。

常见问题2:仲裁丢失

  • 现象 ROM_I2CMasterErr 返回 I2C_MASTER_ERR_ARB_LOST 。波形上可能看到SDA线电平与主机试图发送的电平不一致。
  • 可能原因 :多主系统中,两个主机同时开始传输,且发送的数据位不同。根据“线与”逻辑,发送0的设备赢得仲裁(因为0拉低了总线)。你的主机发送了1但检测到总线为0,于是退出竞争。
  • 解决方案 :在发起传输前,务必调用 ROM_I2CMasterBusBusy() 等待总线空闲。仲裁丢失后,软件应等待一个随机时间后重试,避免再次碰撞。

常见问题3:数据错误

  • 现象 :能收到ACK,但数据内容不对。
  • 可能原因及排查
    • 时序问题 :SCL频率过高,从机来不及建立数据。尝试降低速率到100kHz测试。
    • 电压问题 :高低电平阈值不满足从机要求。
    • 软件顺序错误 :对于突发读, ROM_I2CMasterDataGet ROM_I2CMasterControl 的调用顺序错误。记住:先放命令启动接收,然后读数据,再放CONT命令接收下一个。
    • 中断冲突 :高优先级中断打断了I2C时序关键部分。可以考虑在I2C关键操作段临时关闭全局中断。

常见问题4:Stellaris特定问题——ROM API调用死机

  • 现象 :调用 ROM_I2CMasterInitExpClk 等函数后程序跑飞。
  • 排查
    1. 检查 ulBase 参数是否正确。 I2C0_BASE 等宏定义是否与芯片型号匹配。
    2. 检查系统时钟 ulI2CClk 是否已正确配置并稳定。在时钟切换过程中调用初始化函数会导致错误。
    3. 检查芯片是否真的支持ROM API。有些早期型号或定制型号可能没有。
    4. 尝试使用Flash中的DriverLib库(函数名去掉 ROM_ 前缀)进行对比测试。

5.3 调试辅助函数

在代码中加入以下调试函数,可以快速定位问题:

void I2C_DumpStatus(uint32_t ui32Base) {
    uint32_t err = ROM_I2CMasterErr(ui32Base);
    if(err != I2C_MASTER_ERR_NONE) {
        UARTprintf("[I2C ERR] Code: ");
        switch(err) {
            case I2C_MASTER_ERR_ADDR_ACK: UARTprintf("ADDR_ACK (No slave)\n"); break;
            case I2C_MASTER_ERR_DATA_ACK: UARTprintf("DATA_ACK\n"); break;
            case I2C_MASTER_ERR_ARB_LOST: UARTprintf("ARB_LOST\n"); break;
            default: UARTprintf("UNKNOWN: 0x%X\n", err); break;
        }
    }
    UARTprintf("[I2C] Busy: %d, BusBusy: %d\n",
                ROM_I2CMasterBusy(ui32Base),
                ROM_I2CMasterBusBusy(ui32Base));
}

// 一个带重试和超时的安全发送函数
bool I2C_SafeSend(uint8_t addr, uint8_t reg, uint8_t val, uint8_t retries) {
    for(uint8_t i = 0; i < retries; i++) {
        if(I2C_WriteSingleByteToReg(addr, reg, val)) { // 你的写寄存器函数
            return true;
        }
        I2C_DumpStatus(I2C0_BASE);
        SysCtlDelay(SysCtlClockGet() / 1000); // 延迟约1ms后重试
        // 尝试发送一个停止条件复位总线(如果之前出错可能总线被挂起)
        ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_ERROR_STOP);
        while(ROM_I2CMasterBusy(I2C0_BASE));
    }
    UARTprintf("[I2C] Send failed after %d retries.\n", retries);
    return false;
}

6. 性能优化与高级应用场景

掌握了基础操作后,可以进一步优化和拓展I2C应用。

6.1 结合DMA提升吞吐量

对于高速、大数据量的I2C传输(例如从传感器读取大量采样数据),使用CPU轮询或中断处理每个字节开销巨大。Stellaris的I2C模块支持DMA(直接存储器访问)。你可以配置DMA通道,在I2C接收FIFO达到一定阈值或发送FIFO有空闲时,自动将数据从内存搬移到I2C数据寄存器,或反之。这几乎将CPU从数据传输中完全解放出来。

配置DMA涉及I2C的FIFO中断触发级别设置( ROM_I2CMasterFIFOLimitSet ,注意资料中未列出此函数,需查最新DriverLib)和DMA控制器的配置。这是一个相对高级的话题,但能极大提升系统效率。

6.2 应对复杂的从设备协议

许多I2C从设备(如传感器、IO扩展芯片)有自己的一套寄存器读写协议。常见的是“写寄存器地址-读/写数据”模式。你需要封装更上层的函数。

bool Sensor_ReadRegister(uint8_t devAddr, uint8_t regAddr, uint8_t *pData) {
    // 1. 发送要读的寄存器地址(写操作)
    ROM_I2CMasterSlaveAddrSet(I2C0_BASE, devAddr, false);
    ROM_I2CMasterDataPut(I2C0_BASE, regAddr);
    ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_SINGLE_SEND);
    while(ROM_I2CMasterBusy(I2C0_BASE));
    if(ROM_I2CMasterErr(I2C0_BASE) != I2C_MASTER_ERR_NONE) return false;

    // 2. 发送重复起始条件,并切换为读操作
    ROM_I2CMasterSlaveAddrSet(I2C0_BASE, devAddr, true); // true 表示读
    ROM_I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_SINGLE_RECEIVE);
    while(ROM_I2CMasterBusy(I2C0_BASE));
    if(ROM_I2CMasterErr(I2C0_BASE) != I2C_MASTER_ERR_NONE) return false;

    // 3. 读取数据
    *pData = (uint8_t)ROM_I2CMasterDataGet(I2C0_BASE);
    return true;
}

注意步骤1和2之间没有停止条件,而是通过 I2C_MASTER_CMD_SINGLE_RECEIVE 隐含了重复起始条件。这是标准的I2C复合格式。

6.3 10位地址模式

除了常见的7位地址,I2C还支持10位地址,允许连接更多设备。10位地址的传输帧格式与7位不同:它先发送一个特殊的前导字节(11110xx),其中xx是10位地址的最高两位,并且R/W位为写;然后发送地址的低8位;之后再跟重复起始条件和真正的读写操作。Stellaris的I2C模块硬件支持10位地址模式,通常需要通过配置特定控制寄存器来启用,API函数 ROM_I2CMasterSlaveAddrSet 可能也支持10位地址参数(需查阅具体库函数说明)。使用10位地址时,务必确认从设备和主机都支持并正确配置。

最后,I2C是一个看似简单实则精妙的协议。从理解“线与”逻辑和开漏输出开始,到熟练运用Stellaris提供的ROM API实现稳定通信,再到用逻辑分析仪破解棘手的时序问题,每一步都需要动手实践和思考。我建议在项目初期就搭建一个简单的测试环境,用一块开发板连接一个常见的I2C设备(如AT24Cxx EEPROM或BMP280气压传感器),从读写一个字节开始,逐步实现完整的读写、扫描总线设备等功能,把上面提到的错误处理、中断、DMA都尝试一遍。当你能够游刃有余地驾驭它时,你会发现面前打开了一扇连接大量优秀外设的大门。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值