1. 项目概述与I2C总线核心价值

在嵌入式系统开发中,设备间的通信是构建复杂功能的基础。面对众多传感器、存储器和外设,如果每个都独占一组并行数据线和地址线,PCB会变得异常复杂,成本也会飙升。这时,I2C(Inter-Integrated Circuit)总线就显现出其巨大价值。它仅用两根线——串行数据线(SDA)和串行时钟线(SCL)——就构建了一个支持多主多从的通信网络,极大地简化了硬件设计。我第一次在TMS320C6000 DSP项目中使用I2C连接外部EEPROM和温度传感器时,就被这种简洁而强大的协议所折服。

TMS320C6000系列DSP集成的I2C模块,完全兼容Philips(现NXP)的I2C总线规范v2.1,支持高达400kbps的快速模式。它不仅仅是一个简单的串行接口,更是一个集成了时钟同步、噪声滤波、仲裁逻辑和DMA事件触发的智能外设。对于需要高效、可靠地进行芯片间通信的DSP应用,如工业控制、音频处理或图像采集系统,深入理解并掌握这个模块的编程,是释放DSP全部潜力的关键一步。本文将从一个资深嵌入式工程师的视角,带你深入TMS320C6000 DSP的I2C模块内部,不仅解读寄存器手册上的“是什么”,更重点分享实际项目中“怎么用”和“为什么这么用”的实战经验。

2. I2C模块架构与工作模式深度解析

2.1 模块整体架构与数据流

TMS320C6000的I2C模块远不止是两根GPIO引脚。其内部架构是一个精心设计的数字状态机,核心围绕着几组关键寄存器展开。理解数据流向是编程的第一步。如图2所示,模块的核心是两对数据寄存器: I2CDXR/I2CXSR 用于发送, I2CDRR/I2CRSR 用于接收。

当你作为CPU或EDMA控制器需要发送数据时,你将数据写入 I2CDXR 。模块内部的移位逻辑会在适当时机将数据从I2CDXR拷贝到发送移位寄存器 I2CXSR ,然后一位一位地从SDA引脚移出,整个过程与SCL引脚上产生的时钟同步。接收过程则相反:数据从SDA引脚一位位移入接收移位寄存器 I2CRSR ,当一个完整的数据字节(或1-8位数据单元)接收完毕后,再被拷贝到 I2CDRR 中,供CPU或EDMA读取。

注意 :这里有一个极易出错的细节。I2CDXR和I2CDRR是CPU/EDMA访问的“缓冲”寄存器,而I2CXSR和I2CRSR是内部“移位”寄存器,程序员无法直接访问。你必须清楚,向I2CDXR写入数据,并不意味着数据立刻出现在总线上;从I2CDRR读取数据,也不代表总线上的数据被立即捕获。数据的实际收发与总线时钟严格同步,并由模块的状态机控制。混淆这两组寄存器的角色,是导致通信时序错误的一个常见原因。

2.2 四大操作模式与角色切换逻辑

模块支持四种基本操作模式,这决定了它在总线上的行为。很多新手会混淆“主/从”和“收/发”这两组概念。实际上,它们是正交的:

  1. 主发送模式 :模块作为总线主机,并向外发送数据。这是主机发起通信时的默认起始模式。主机首先发送从机地址和写方向位(R/W=0)。
  2. 主接收模式 :模块作为总线主机,但从外部接收数据。主机不能直接进入此模式,必须先从主发送模式发送从机地址和读方向位(R/W=1),然后模块会自动切换为主接收模式。
  3. 从接收模式 :模块作为总线从机,接收主机发来的数据。这是所有从机设备上电后的初始状态,时刻监听总线上的地址。
  4. 从发送模式 :模块作为总线从机,向主机发送数据。从机必须首先在从接收模式下,识别到自己的地址且主机发送的R/W位为1(读请求),然后才能切换到此模式。

模式切换完全由硬件根据总线上的命令自动完成,但软件需要通过配置 I2CMDR 寄存器中的 MST (主模式使能)位来声明自己希望扮演的角色。例如,当你将MST置1,模块就准备好争夺总线控制权成为主机;置0,则模块乖乖做从机,等待被呼叫。

2.3 起始、停止与重复起始条件

这是I2C协议的“标点符号”,决定了数据帧的边界。

  • 起始条件 :当SCL为高电平时,SDA线发生一个从高到低的跳变。这个独特的信号告诉总线上所有设备:“注意,我要开始传输了”。在代码中,你需要同时设置 MST=1 STT=1 来让模块产生起始条件。
  • 停止条件 :当SCL为高电平时,SDA线发生一个从低到高的跳变。这表示“本次传输结束,总线即将释放”。通过设置 STP=1 来产生停止条件。
  • 重复起始条件 :在一次通信序列中,主机可以在不释放总线(不发送停止条件)的情况下,再次发送一个起始条件。这用于在不放弃总线控制权的前提下,切换通信方向或寻址另一个从机。例如,主机先写入从机A的某个寄存器地址,然后发送重复起始条件,再以读模式访问同一从机A以读取数据。当总线忙( BB=1 )时,设置 STT=1 就会产生一个重复起始条件,而非起始条件。

3. 关键寄存器配置与编程精要

寄存器是软件与I2C硬件模块对话的窗口。盲目照搬示例代码往往不通,必须理解每个关键位背后的含义。

3.1 模式寄存器详解与配置策略

I2CMDR 是模块的“大脑”,几乎所有关键功能都由此寄存器控制。

  • IRS :这是总开关。在修改任何其他I2C配置寄存器(如I2COAR, I2CCLKH/L等)之前, 必须先将IRS清零以复位模块 。配置完成后,再置1以启用模块。这是一个硬性规定,违反它会导致配置不生效或通信异常。
  • RM :重复模式。这是高效编程的关键。当 RM=1 时,模块在完成当前数据字节传输后,会自动将下一个待发送数据从I2CDXR加载到I2CXSR(发送时),或自动将I2CRSR中的数据拷贝到I2CDRR(接收时),并等待下一个数据字节。这非常适合连续的数据块传输。当 RM=0 (非重复模式)时,每传输一个字节都需要CPU干预来准备下一个数据或读取已接收数据。在需要高吞吐量的场景,务必使用RM=1并结合中断或EDMA。
  • STT/STP :如前所述,用于产生起始和停止条件。 一个重要的经验是:STT和STP位是“瞬时命令”位。硬件在执行完相应操作后会自动将其清零。 因此,你的程序不应该去“清除”它们,而是查询状态位来判断操作是否完成。
  • FDF :自由数据格式。置1时,通信帧中不包含地址字节,直接传输数据。这仅用于通信双方事先约定好的特殊场合,一般与标准I2C设备通信时保持为0。
  • BC :位计数。定义每个数据单元包含多少位(1-8)。99%的应用中都是8位(BC=000),即一个字节。除非与某些使用非标准位宽的特种设备通信,否则不要改动。

3.2 时钟配置:从DSP内核频率到SCL波特率

这是配置的难点,也是通信稳定的基石。如图11所示,SCL频率的生成是一个两级分频过程。

第一步:配置预分频器(I2CPSC) 输入时钟 SYSCLK2 经过预分频器,产生模块工作时钟。 手册中明确警告,模块工作时钟必须在6.7MHz到13.3MHz之间! 这是硬性约束。计算公式为: 模块时钟频率 = SYSCLK2频率 / (IPSC + 1) 其中 IPSC 是写入I2CPSC寄存器的值。例如,如果 SYSCLK2 = 100MHz ,要得到约10MHz的模块时钟,则 IPSC = (100 / 10) - 1 = 9

第二步:配置高低电平分频器(I2CCLKH/L) 模块时钟再经过两个独立的分频器(ICCH和ICCL),分别控制SCL线高电平和低电平的持续时间。最终的SCL频率计算公式为: SCL频率 = 模块时钟频率 / [(ICCH + d) + (ICCL + d)] 其中 d 是一个依赖于IPSC值的内部延迟因子(IPSC=0时d=7,IPSC=1时d=6,IPSC>=2时d=5)。这个 d 的存在是为了满足I2C协议对建立/保持时间的要求。

实战配置示例 :假设我们需要在 SYSCLK2=150MHz 的系统中产生一个标准的100kHz I2C时钟。

  1. 首先确定模块时钟。取中间值10MHz,则 IPSC = (150 / 10) - 1 = 14 。验证: 150 / (14+1) = 10MHz ,在6.7-13.3MHz范围内,合格。
  2. 然后计算ICCH和ICCL。我们希望SCL占空比为50%(高电平时间=低电平时间)。假设IPSC=14>=2,则d=5。设 ICCH = ICCL = N 。 公式简化为: 100kHz = 10,000,000 Hz / [(N+5) + (N+5)] = 10,000,000 / (2N+10) 解得: 2N + 10 = 100 N = 45 。 因此,设置 ICCH = ICCL = 45
// C语言配置示例
void I2C_ClockConfig(void) {
    // 1. 确保I2C模块复位 (IRS=0)
    I2CMDR &= ~(0x8000); // 清除IRS位

    // 2. 配置预分频器 (假设SYSCLK2=150MHz)
    I2CPSC = 14; // 产生10MHz模块时钟

    // 3. 配置高低电平分频器,产生100kHz SCL
    I2CCLKH = 45;
    I2CCLKL = 45;

    // 4. 重新使能I2C模块
    I2CMDR |= 0x8000; // 设置IRS位
}

3.3 地址寄存器与数据寄存器

  • I2COAR :自身地址寄存器。当模块作为从机时,它用这个地址来响应主机的寻址。7位地址模式时,地址填写在bit 6-0,bit 7保留。10位地址模式时,高7位(11110XX)由硬件处理,低8位需正确设置。
  • I2CSAR :从机地址寄存器。当模块作为主机时,它把要访问的从机地址写在这里。同样需注意7位/10位模式。
  • I2CDXR :数据发送寄存器。你要发送的数据写在这里。
  • I2CDRR :数据接收寄存器。你从这里读取接收到的数据。

一个关键技巧 :在非重复模式(RM=0)下,向I2CDXR写入数据会 自动启动 一次发送过程(如果模块已配置为发送方)。而在重复模式(RM=1)下,你通常需要提前准备好多个数据到缓冲区,由DMA或中断服务程序依次写入I2CDXR。

4. 三种数据流控制方式实战

TMS320C6000的I2C模块提供了三种控制数据传输的方式:位查询、中断和EDMA事件。选择哪种方式,取决于你对系统实时性和CPU占用率的要求。

4.1 位查询:简单直接,但效率低下

这种方式通过轮询 I2CSTR 状态寄存器中的标志位来控制流程。例如,发送时查询 XRDY (发送就绪),接收时查询 RRDY (接收就绪)。

// 位查询方式发送一个字节
void I2C_MasterWriteByte(unsigned char slaveAddr, unsigned char data) {
    // 配置为主发送模式,非重复模式,7位地址
    I2CMDR = 0x2600; // IRS=1, MST=1, TRX=1, RM=0, STT=0, STP=0, ...

    // 写入从机地址 (写方向 R/W=0 已隐含在TRX=1中)
    I2CSAR = slaveAddr;

    // 写入要发送的数据
    I2CDXR = data;

    // 设置STT位,启动传输(发送地址+数据)
    I2CMDR |= 0x2000; // 设置STT位

    // 轮询等待传输开始(总线忙)
    while((I2CSTR & 0x20) == 0); // 等待BB位变1

    // 轮询等待数据发送完成
    while((I2CSTR & 0x10) == 0); // 等待XRDY位变1 (数据已从DXR移走)

    // 设置STP位,产生停止条件
    I2CMDR |= 0x0800; // 设置STP位

    // 轮询等待停止条件完成
    while((I2CSTR & 0x20) != 0); // 等待BB位变0
}

注意 :位查询方式会 完全占用CPU ,在等待期间CPU无法执行其他任务。它仅适用于极简单的、非实时的或初始化阶段的通信。在复杂的多任务系统中应避免使用。

4.2 中断驱动:平衡性能与复杂性

中断方式允许CPU在I2C模块准备就绪时再介入,大大提高了CPU效率。你需要配置 I2CIER 中断使能寄存器,并编写中断服务程序。

关键中断源:

  • ARDY :寄存器访问就绪。当可以安全写入I2CMDR(例如配置STT/STP)时触发。
  • RRDY :接收数据就绪。当I2CDRR中有新数据时触发。
  • XRDY :发送数据就绪。当I2CDXR为空,可以写入下一个数据时触发。
  • NACK :未收到应答。当从机未返回ACK时触发,通常意味着寻址错误或从机故障。
  • AL :仲裁丢失。当模块作为主机在总线仲裁中失败时触发。

中断服务程序编写要点

  1. 进入ISR后,首先读取 I2CISR 中断源寄存器,确定是哪个事件触发了中断。
  2. 根据事件进行相应处理(如从I2CDRR读数据,或向I2CDXR写数据)。
  3. 必须手动清除中断标志 !在I2CISR中向对应位写1可以清除该中断源。不清除会导致中断持续触发。
  4. 如果是NACK或AL错误,需要进行错误恢复处理(例如重试、复位总线等)。
// 中断方式主发送示例框架
volatile int txBufferIndex = 0;
volatile int txBufferLength = 0;
unsigned char txBuffer[256];

void I2C_ISR(void) {
    unsigned int intSource = I2CISR;

    if(intSource & 0x0001) { // XRDY中断
        if(txBufferIndex < txBufferLength) {
            I2CDXR = txBuffer[txBufferIndex++]; // 写入下一个数据
        } else {
            // 所有数据发送完毕,准备产生停止条件
            I2CMDR |= 0x0800; // 设置STP位
            // 可能还需要禁用XRDY中断,避免再次进入
            I2CIER &= ~0x0001;
        }
        I2CISR = 0x0001; // 清除XRDY中断标志
    }
    // 处理其他中断源...
}

4.3 EDMA事件:极致吞吐量的选择

对于大数据量的连续传输(例如从I2C传感器读取一帧图像数据,或向I2C DAC发送一段音频流),EDMA事件是最高效的方式。I2C模块可以产生 REVT (接收事件)和 XEVT (发送事件),直接触发EDMA控制器在I2CDRR和内存之间,或内存和I2CDXR之间搬运数据,完全无需CPU干预。

配置步骤

  1. 配置I2C模块为重复模式(RM=1),并设置好其他参数。
  2. 配置EDMA通道:
    • 将I2C的REVT/XEVT链接到EDMA通道的同步事件。
    • 设置源/目标地址:对于接收,源地址是I2CDRR的固定地址,目标地址是内存缓冲区;对于发送则相反。
    • 设置传输计数和地址索引模式。
  3. 启动I2C传输(设置STT)。
  4. I2C模块每完成一个数据的接收/发送,就会触发一次EDMA事件,EDMA自动完成一次数据搬运。当整个数据块传输完毕,EDMA可以产生一个完成中断通知CPU。

这种方式将CPU从繁琐的数据搬运中彻底解放出来,特别适合与DSP的其他高带宽任务(如数字信号处理算法)并发执行。

5. 高级功能与疑难问题排查实录

5.1 时钟同步与仲裁机制

在多主机系统中,时钟同步和仲裁是保证总线正常工作的关键。

  • 时钟同步 :所有主机都在SCL线上产生自己的时钟。总线上的SCL信号是所有主机时钟的“线与”结果。低电平周期由时钟低电平期最长的主机决定,高电平周期由时钟高电平期最短的主机决定。TMS320C6000的I2C模块硬件自动处理这个过程,软件无需干预。
  • 仲裁 :当两个主机同时发起传输时,它们会继续发送数据,直到出现分歧。在SDA线上,发送“1”(释放总线为高)的主机将检测到自己发出的电平(高)与总线实际电平(被另一个主机拉低)不符,从而判定自己仲裁失败。失败的主机会立即切换到从接收模式,并设置 AL 标志。你的程序必须检测AL标志,并在仲裁失败后执行退避和重试逻辑。常见的策略是等待一个随机时间后重新尝试。

5.2 NACK处理与错误恢复

未收到应答(NACK)是I2C通信中最常见的错误。原因可能是:

  1. 从机地址错误。
  2. 从机设备不存在或未上电。
  3. 从机忙或处于写保护状态。
  4. 总线物理连接问题(上拉电阻过大、线路过长导致信号畸变)。

当I2C模块作为接收方时,你也可以主动发送NACK来告知发送方停止发送。方法是在接收倒数第二个数据位之前,设置 I2CMDR 中的 NACKMOD 位。

错误恢复流程建议

  1. 在中断服务程序中检测到NACK或AL标志。
  2. 记录错误次数,超过阈值则上报致命错误。
  3. IRS 位清零再置一,复位I2C模块内部状态机。
  4. 重新初始化I2C配置(时钟等)。
  5. 延迟一段时间(例如几毫秒)后重试操作。

5.3 常见问题排查速查表

现象 可能原因 排查步骤与解决方案
通信完全无响应,SCL/SDA一直为高 模块未使能;总线无上拉电阻;从机地址错误。 1. 检查IRS位是否为1。
2. 用示波器测量SCL/SDA,确认有上拉,电压正常。
3. 确认I2COAR/I2CSAR地址正确,7/10位模式设置正确。
能发送起始条件,但收不到ACK 从机地址错误;从机设备故障;时序不满足从机要求。 1. 用逻辑分析仪抓取波形,确认发送的地址字节正确。
2. 检查从机设备电源、复位信号。
3. 重点检查SCL频率是否在从机支持的范围内 (很多EEPROM最高只支持100kHz)。调整I2CCLKH/L。
能收到ACK,但后续数据出错 时钟频率过快,建立/保持时间不足;电源噪声干扰。 1. 降低SCL频率(增大ICCH/ICCL值)。
2. 检查电源纹波,在I2C引脚附近增加去耦电容。
3. 检查PCB布线,SCL/SDA线是否过长,是否靠近噪声源。
多字节传输中丢失数据 重复模式(RM)配置错误;中断/EDMA服务不及时。 1. 确认RM位设置符合预期。RM=1用于连续传输。
2. 在中断方式下,检查中断优先级是否被其他高优先级中断阻塞。
3. 在查询方式下,检查轮询循环是否被意外打断。
仲裁频繁丢失 多主机程序逻辑缺陷,同时发起请求;总线负载过重。 1. 优化多主机软件逻辑,增加随机退避算法。
2. 检查总线负载,减少不必要的通信。
EDMA传输数据错位 EDMA地址递增模式配置错误;传输计数与I2C数据计数不匹配。 1. 核对EDMA的源/目标地址索引(CIDX)配置,接收时目标地址应递增。
2. 如果使用了I2CCNT(数据计数寄存器),确保其值与EDMA传输计数一致。

5.4 数字回环测试

在硬件连接之前,强烈建议使用 数字回环模式 进行软件自检。将 I2CMDR 中的 DLB 位置1,模块内部会将SDA和SCL输出短接到输入,这样无需外部设备即可测试发送和接收流程。这是验证你所有配置和驱动程序逻辑是否正确的最快方法。通过回环测试后,再将DLB清零接入真实总线,可以极大降低硬件调试阶段的复杂度。

6. 一个完整的EEPROM读写示例

最后,我们以一个实际的例子收尾:使用TMS320C6000的I2C模块,以中断方式读写一个常见的24LC256 I2C EEPROM。

硬件连接 :SCL、SDA接上拉电阻(通常4.7kΩ),EEPROM的A0-A2地址引脚接地,即7位地址为0x50。

软件流程

  1. 初始化 :配置系统时钟、I2C模块时钟(100kHz)、自身地址(从机模式下用,此处可设任意值)、使能中断。
  2. 写一个字节
    • 配置为主发送模式,非重复模式。
    • 写入EEPROM地址0x50。
    • 写入EEPROM内部的高地址字节。
    • 写入EEPROM内部的低地址字节。
    • 写入要存储的数据字节。
    • 设置STT启动传输,设置STP在发送完数据后产生停止条件。
    • 等待传输完成中断或查询BB位。
    • 注意 :EEPROM写入需要一定的页编程时间(约5ms),在此期间不会响应ACK。必须加入延时或轮询ACK。
  3. 读一个字节 (当前地址读):
    • 先执行一个“哑写”来设置内部地址指针:发送起始条件、EEPROM地址(写)、内部地址(高、低),但不发送数据,而是发送一个重复起始条件。
    • 然后发送EEPROM地址(读),切换为主接收模式。
    • 接收一个数据字节后,主机发送NACK,然后发送停止条件。
    • 在中断服务程序中读取I2CDRR获得数据。

这个例子涵盖了起始、停止、重复起始、模式切换、NACK控制等关键操作,是掌握I2C编程的一个很好的综合练习。在实际项目中,我通常会为I2C操作封装一个带有超时和重试机制的状态机,并将底层寄存器操作封装成独立的驱动层,这样上层应用代码会更加清晰和健壮。调试时,一把好的逻辑分析仪是必不可少的,它能直观地展示出起始、地址、数据、ACK/NACK和停止条件,让你对总线上的每一比特都了如指掌。

更多推荐