
简介TMS320F28335是一款广泛应用于工业控制与电力电子的高性能浮点DSP本资源针对其I2C外设模块提供了一套完整的硬件实现方案覆盖I2CCON、I2CDATA、I2CCLKL、I2CSA等核心寄存器的配置与使用并以AT24C02 EEPROM通信为实例系统展示了从启动条件、地址发送、应答检测到主写/主读、重复起始条件及停止条件的完整流程以及中断服务程序的应用。压缩包共收录83个文件主要包含C源码、头文件、汇编启动文件、CCS工程配置、链接命令文件(.cmd)、生成产物(.out/.map)以及makefile、项目描述等辅助文件整体仅235KB便于在CCS中直接加载快速对照代码理解I2C低层驱动细节。资源基于TI的DSP2833x标准外设库编写包含完整的CCS工程结构示例工程Example16_I2C_H中实现了AT24C02的页写、顺序读等典型操作并加入LED显示与按键控制辅助模块有助于直观观察通信过程和数据变化。已有1972人学习浏览适合正在学习DSP外设编程、或需要在TMS320F28335上快速落地I2C通信功能的开发人员参考和复用。1. 为什么 F28335 的 I2C 一定要用硬件实现大多数第一次接触 DSP28335 的人会先写 GPIO 翻转读按键之后看到“I2C”第一反应是抄一份模拟时序。但在 F28335 上做实时控制时模拟 I2C 会在收发过程中被 PWM 中断打断状态机一旦错过 SCL 电平变化总线就挂在半高电平。硬件 I2C 外设把起始位、停止位、ACK 位和时钟延展都收进了模块内部CPU 只需要写数据寄存器、看状态标志或中断代码量少且收发节奏不会被随机中断冲乱。下面直接讲 TMS320F28335 硬件 I2C 的工程落地从引脚复用、模块时钟、主模式读写 AT24C02到中断处理和总线故障恢复。适合用 DSP28335 做板级通信又不想被 I2C 总线问题耗掉一周的工程师。2. F28335 I2C 模块硬件骨架引脚、时钟、寄存器2.1 模块是一个独立位时序状态机F28335 把 I2C 集成成了一个完整外设不是简单把 SDA/SCL 接到 GPIO 上。内部有移位寄存器、仲裁检测、总线忙检测和 FIFO 增强逻辑。I2C 通信协议里的 START、STOP、ACK 都由硬件在 SCL 配合下生成CPU 不干预。收发时只要把目标从机地址写进 I2CSAR把要传的字节数写进 I2CCNT然后往 I2CDXR 写数据外设会自己处理每一个 SCL 脉冲。这一点和 STM32F407 模拟 I2C 完全不同和 DSP28335 原生的 SCI 也是两回事后者只负责 UART不负责多主机仲裁。使用硬件 I2C 还有个现实好处它自带仲裁丢失检测和总线忙标志。多主机挂在同一条总线上时两个主机同时拉低 SDA硬件会在位级仲裁失败后立刻置 AL 位从而让出总线。模拟 I2C 要做同样的仲裁需要每个位都采样总线状态代码复杂度和出错概率都高出不少。2.2 引脚复用先让 GPIO32/33 变成 I2C 专用脚在 F28335 上I2C-A 模块对应两个管脚SDA 默认在 GPIO32SCL 默认在 GPIO33。开发板如果引出了其他脚位除非板级做了重映射否则大概率还是这两根。配置引脚时要先打开 GPIO 时钟再改 GPIO 复用寄存器最后处理上拉。常见做法是void i2c_pin_init(void) { EALLOW; // 把 GPIO32/33 从普通 IO 切到 I2C 功能 GpioCtrlRegs.GPBMUX1.bit.GPIO32 1; // SDAA GpioCtrlRegs.GPBMUX1.bit.GPIO33 1; // SCLA // 使能内部上拉外部上拉电阻不足时能兜底 GpioCtrlRegs.GPBPUD.bit.GPIO32 0; GpioCtrlRegs.GPBPUD.bit.GPIO33 0; EDIS; }两个GPBMUX1位域把引脚从 IO 功能切到外设功能置 1 是在多数 28335 封装上选 I2C 复用具体值应以自己板子数据手册为准。强制把内部上拉打开只是为了调试起步量产板上一般要外接 4.7k 或 2.2k 上拉到 3.3V否则总线电容大时波形边沿会变软I2C 时序余量不够400kHz 高速模式下尤其明显。I2C 是开漏总线引脚内部上拉只能解决“有没有波形”解决不了“边沿够不够陡”。2.3 时钟树I2CPSC、I2CCLKL、I2CCLKH 怎么配才不出鬼硬件 I2C 的时钟分两级先由 I2CPSC 把 SYSCLKOUT 分频成模块时钟 module_clk再由 I2CCLKL 和 I2CCLKH 把 module_clk 二分形成 SCL。公式是module_clk SYSCLKOUT / (I2CPSC 1)SCL module_clk / (I2CCLKL I2CCLKH 7)这里的 7 是 TI 在外设手册里给出的同步和毛刺滤波延迟补偿项不同型号略有差别。F28335 跑 150MHz 时配 100kHz 总线时钟常见是 I2CPSC9I2CCLKL72I2CCLKH71配 400kHz 时把预分频调低让 module_clk 走高一点。目标 SCLI2CPSC模块时钟I2CCLKLI2CCLKH实际 SCL100kHz915MHz7271100kHz400kHz430MHz3434400kHz小参数手工算容易错我一般写成一个小函数启动时传入 SYSCLKOUT 和期望的 SCL 频率自动填寄存器void i2c_clock_config(unsigned long sysclk_hz, unsigned long target_hz) { Uint32 module_clk, total, h, l; // 这里的 sysclk_hz 是 150MHz 专用写法 if (target_hz 400000) { I2cRegs.I2CPSC 4; // 模块时钟 30MHz module_clk sysclk_hz / 5; } else { I2cRegs.I2CPSC 9; // 模块时钟 15MHz module_clk sysclk_hz / 10; } total module_clk / target_hz; // 总周期数 if (total 8) total 8; h (total - 7) / 2; // 高电平时间 l total - 7 - h; // 低电平时间 I2cRegs.I2CCLKH h; I2cRegs.I2CCLKL l; }total - 7是扣掉同步补偿后的有效计数随后均分给高电平和低电平保证占空比接近 50%。I2CPSC 越大module_clk 越低省电但 SCL 抖动会变大建议保持 module_clk 在 10MHz 到 40MHz 之间。若把 target_hz 设成 400k 以上先确认从机支持 Fast Mode Plus否则示波器上看着有波形读回来的数据也全是 0xFF。2.4 关键寄存器I2CSAR、I2CCNT 与 I2CMDR 的分工F28335 的 I2C 模块寄存器不算多但每一个都会直接影响时序。地址寄存器 I2CSAR 保存 7 位从机地址I2CCNT 告诉外设本次要处理的字节数I2CDXR 是发送缓冲I2CDRR 是接收缓冲I2CSTR 里的 BB、ARDY、NACK、SCD 是对外通信状态I2CMDR 则是整个外设的开关和方向位。写数据前必须先设地址和字节数再动 I2CCNT最后才置 I2CMDR 里的 STT 启动传输顺序错了外设会把当前 I2CCNT 当成上一次残留值产生一个莫名其妙的多余字节。I2CIER 是中断使能寄存器错误恢复时常需要临时关闭它。I2CISRC 则是一个只读寄存器中断服务程序里通过它判断到底是 NACK、ARDY 还是仲裁丢失读完后该寄存器自动清空不用手动写状态位这一点和串口中断处理逻辑很不一样。3. 主模式读写 AT24C02初始化与收发代码3.1 初始化把散件粘成一个能跑的 I2C 外设把前面两段合并成 init 函数void i2c_master_init(void) { // 1. 引脚复用 EALLOW; GpioCtrlRegs.GPBMUX1.bit.GPIO32 1; GpioCtrlRegs.GPBMUX1.bit.GPIO33 1; GpioCtrlRegs.GPBPUD.bit.GPIO32 0; GpioCtrlRegs.GPBPUD.bit.GPIO33 0; EDIS; // 2. 模块复位 I2cRegs.I2CMDR.bit.IRS 0; // 3. 清残留状态 I2cRegs.I2CSTR.all 0xFFFF; // 4. 时钟默认 100kHz i2c_clock_config(150000000, 100000); // 5. 工作模式主模式、发送方向 I2cRegs.I2CMDR.bit.MST 1; I2cRegs.I2CMDR.bit.TRX 1; // 6. 使能模块 I2cRegs.I2CMDR.bit.IRS 1; }I2CSTR.all 0xFFFF是把所有写 1 清零的状态位一次性清掉避免上电残留进入仲裁判断。IRS0 后再写寄存器等于给外设一次干净的启动。主模式和发送方向先摆好后面具体事务靠 STT 触发不要在每次读写时反复改 MST否则状态机容易跑飞。3.2 写一个字节I2CCNT、I2CDXR 和起始/停止位协同要写 AT24C02 的 0x10 地址主机需要发送START 设备地址写位(0xA0) 内存地址(0x10) 数据(0x55) STOP。设备地址由 I2CSAR 提供R/W 位由 I2CMDR.TRX 给出剩下两个字节全部走 I2CDXR。I2CCNT 设为 2表示在 STOP 之前要移出两个数据字节。#define AT24_DEV_ADDR 0x50 // 7 位从机地址A2A1A0 接地 void i2c_byte_write(Uint16 dev7, Uint16 mem_addr, Uint16 data) { while (I2cRegs.I2CSTR.bit.BB 1); // 总线忙则等 I2cRegs.I2CSAR dev7; // 装入 7 位地址 I2cRegs.I2CCNT 2; // 本次 2 个字节 I2cRegs.I2CMDR.bit.MST 1; I2cRegs.I2CMDR.bit.TRX 1; // 发送方向 I2cRegs.I2CMDR.bit.STP 1; // 先告知外设结束后要发 STOP I2cRegs.I2CDXR mem_addr; // 第 1 个数据字节片内地址 I2cRegs.I2CMDR.bit.STT 1; // 最后触发 START while (I2cRegs.I2CSTR.bit.XRDY 0); // 等第一个字节移出 I2cRegs.I2CDXR data; // 第 2 个数据字节真实数据 while (I2cRegs.I2CSTR.bit.SCD 0); // 等 STOP 完成 I2cRegs.I2CSTR.bit.SCD 1; // 手动清 SCD否则下次误判 DELAY_US(5000); // EEPROM 内部写周期 }XRDY 是发送保持寄存器空标志它置 1 表示外设已经把当前数据从 I2CDXR 搬进移位寄存器可以安全写入下一字节。注意不要在置 STT 前把两个字节都填进 I2CDXRF28335 的硬件 I2C 不是深 FIFO多余写入会覆盖发送缓冲造成数据错位。SCD 是检测到 STOP 的标志写 1 清除必须在下一笔事务开始前清掉。3.3 读一个字节先生成“虚拟写”再切换 TRX 读回AT24C02 支持随机读但要先把内部地址指针指过去。标准做法是先发一个“虚拟写”起始 设备地址写位 内存地址 停止之后再做一次起始 设备地址读位 数据。第一次事务不写数据纯粹是给 EEPROM 定地址。第二次事务要把 I2CMDR.TRX 从 1 改成 0外设才会在地址后自动进入接收状态。Uint16 i2c_byte_read(Uint16 dev7, Uint16 mem_addr) { Uint16 data; while (I2cRegs.I2CSTR.bit.BB 1); // 虚拟写把 mem_addr 送给 EEPROM I2cRegs.I2CSAR dev7; I2cRegs.I2CCNT 1; I2cRegs.I2CMDR.bit.MST 1; I2cRegs.I2CMDR.bit.TRX 1; I2cRegs.I2CMDR.bit.STP 1; I2cRegs.I2CDXR mem_addr; I2cRegs.I2CMDR.bit.STT 1; while (I2cRegs.I2CSTR.bit.SCD 0); I2cRegs.I2CSTR.bit.SCD 1; // 真正读地址线要切到读方向 I2cRegs.I2CCNT 1; I2cRegs.I2CMDR.bit.TRX 0; // 切到接收 I2cRegs.I2CMDR.bit.STP 1; I2cRegs.I2CMDR.bit.STT 1; // 再次触发 START while (I2cRegs.I2CSTR.bit.RRDY 0); // 等接收寄存器就绪 data I2cRegs.I2CDRR; // 取出数据 while (I2cRegs.I2CSTR.bit.SCD 0); I2cRegs.I2CSTR.bit.SCD 1; return data; }第二次启动前先把 TRX 改成 0这一步必须在 STT 置位之前完成。这里没有做真正意义的重复起始而是在 STOP 之后空闲总线上重新启动对 AT24C02 完全可以只是比同一事务内重复起始多占了一点总线时间。如果从机要求必须在同一个事务内连续读比如读取 BMP280 这类传感器的校准参数就需要用到 F28335 的重复起始模式和 I2CCNT 配合那是进阶用法。3.4 EEPROM 写周期不等待会丢第一个字节AT24C02 在写周期里不响应任何命令所以在完成写字节后必须等它内部的 tWR通常 5ms结束才能进行下一次读或写。用DELAY_US(5000)最简单可靠前提是系统能容忍这 5ms 阻塞。如果系统不能阻塞就启动一个软件定时器等定时器到后再发下一笔。下面表里这几种错误基本都来自对 EEPROM 写周期的不了解现象真正原因解决写后立刻读回 0xFFEEPROM 还没完成内部写周期加 5ms 延时连续写超过 8 字节覆盖AT24C02 页边界只有 8 字节分页写入二次写同一个地址出现重复字节上一次 SCD 没清事务结束清 SCDI2C 波形正常但 ACK 丢失地址线 A2A1A0 接法不匹配核对 7 位地址4. 中断与错误处理别等挂总线才出手4.1 中断源和标志I2CSTR 里的每个位都不是摆设轮询在调试器下好用但 F28335 做实际控制时PWM 中断会随时抢占轮询等待 SCD 会把 DSP 卡死在忙等循环里。I2C 外设会给出几个明确中断事件XRDY 表示发送保持寄存器空RRDY 表示接收寄存器满ARDY 表示本次寄存器访问完成NACK 表示从机没有应答AL 表示仲裁丢失SCD 表示检测到停止条件。I2CIER 的对应使能位打开后这些事件会汇总到 PIE 组 8 的中断线在 ISR 里读取 I2CISRC就能知道这次中断属于哪一类。I2CSTR 位中断类型触发时机常见原因XRDY发送保持寄存器空数据从 DXR 移入移位寄存器可以写下一个字节RRDY接收寄存器满一个字节接收完成可以读 I2CDRRARDY寄存器访问完成硬件完成一次寄存器访问地址或 FIFO 操作完成NACK无应答从机没拉低 ACK地址错、从机掉电、总线异常AL仲裁丢失多主机同时抢总线电平冲突、上拉太弱SCD停止条件检测硬件发完 STOP一次事务结束4.2 中断服务程序的结构一个能处理 I2C 事件的 ISR 骨架__interrupt void i2c_int_handler(void) { Uint16 code I2cRegs.I2CISRC.bit.INTCODE; switch (code) { case 1: // ARDY上一次寄存器访问结束 break; case 2: // NACK从机不应答 I2cRegs.I2CSTR.bit.NACK 1; retry_count; if (retry_count 3) { i2c_master_init(); // 重新初始化 retry_count 0; } break; case 3: // AL仲裁丢失 I2cRegs.I2CMDR.bit.IRS 0; I2cRegs.I2CMDR.bit.IRS 1; break; default: break; } }I2CISRC.bit.INTCODE 每次只能读取一个中断源处理完后该位自动清除所以 ISR 里不要再去写 I2CSTR 清整个状态字否则可能误清掉刚到的新事件。真实项目里我会把每次读写拆成状态机发送完一个字节在 ISR 里顺手把下一字节写入 I2CDXR接收时则在 RRDY 里直接读 I2CDRR。这样单个字节交换时间可以压在微秒级不受忙等循环干扰。注意上面代码中 case 数值需要以你手上的头文件定义为准不同 C2000 系列略有差异。这个样例的重点是 switch-case 的处理骨架而不是那几个数字。4.3 总线错误恢复NACK、总线忙和超时复位硬件 I2C 对外部干扰的容错能力比模拟 I2C 强但不在一个量级。总线上一旦出现毛刺导致 SDA 被拉低外设的 BB 位会一直为 1整个模块锁死。恢复惯用三步先把 I2CMDR 的 IRS 清 0令模块复位释放总线再把 I2CSTR 写 0xFFFF 清掉全部状态最后把 IRS 置 1 并重新初始化引脚。如果从机本身卡在时钟延展状态最简单的手段是用 GPIO 把 SCL 手动翻转 10 次触发从机内部状态复位然后再切回 I2C 功能。这套恢复代码要放在超时判断里不要放在正常收发流程中。可以让收发函数传入一个超时计数例如等待 SCD 时最多转 1000 圈超时就调用i2c_bus_recover()。这个超时时间要远大于最慢速率下传输一个字节的时间否则低速 100kHz 的从机也会被误判为死机。一个实际经验硬件 I2C 的 NACK 不会自动重发。模拟 I2C 里你可以在代码里直接补发起始条件但硬件外设在检测到 NACK 后会停止当前状态机必须先把 NACK 标志清掉再把 STT 置 1才能重新启动。很多人把 NACK 当成普通标志位清掉后继续读结果总线下一次事务还是失败原因就在这里。5. 调试带硬件 I2C 的 DSP 板从寄存器断点到示波器收尾5.1 在 CCS 里用寄存器窗口判断外设状态程序跑飞了不想去翻代码先在断点停下打开 Registers 窗口找 I2CSTRBB0 且 SCD0 说明总线空闲BB1 但 SCD1 说明刚发完停止总线还挂在忙状态NACK1 就直接查 I2CSAR多半是地址写错或从机没供电。如果看到 XRDY 和 RRDY 同时为 1那是配置成了回环模式检查 I2CMDR 的 DLB 位。用条件断点在I2cRegs.I2CSTR.bit.NACK 1时停下比单步跟踪数据字节高效得多。5.2 示波器看 I2C 时序的三个判据第一个判据是起始条件SCL 高电平期间 SDA 出现下降沿。用示波器单次触发抓 SDA 的下降沿如果只看到 SCL 单独动作没有 SDA 配合说明 GPIO 复用没切过来。第二个判据是应答位SCL 第 9 个脉冲时 SDA 被拉低就是 ACK如果第 9 个脉冲 SDA 始终为高从机没应答。第三个判据停止后的总线释放STOP 后 SCL 和 SDA 都回到高电平如果反复来回抖说明上拉电阻太大或者总线上挂了太多容性负载。时间轴放到 500ns/div400kHz 的一个位周期约 2.5us如果看到的位宽明显超过 5us就是时钟分频没算对。逻辑分析仪可以直接解出 I2C 数据报文但示波器看到的边沿质量是逻辑分析仪看不到的两个配合才是完整的调 I2C 姿势。5.3 写在最后的三个坑第一个坑是上拉用内部上拉时示波器上 SCL/SDA 电压勉强到 3.3V但边沿很缓400kHz 下数据容易错量产板用外部 2.2k 到 4.7k 更稳。第二个坑是地址AT24C02 的 7 位地址是 0x50加上 R/W 位才变成 0xA0/0xA1别把 0xA0 直接塞进 I2CSAR否则硬件会把 8 位地址当成 7 位加方向位再翻一倍地址偏得离谱。第三个坑是写周期读回来是 0xFF 时先查写完 EEPROM 后有没有等tWR这一条能消除一大半“I2C 硬件坏了”的误判。如果你在 400kHz 下读 BQ76952 这类电池监控芯片总在 NACK试着先把 I2C 时钟降到 100kHz等 ACK 边界找回来后再逐步升频往往比直接改分频寄存器更快。本文还有配套的精品资源点击获取