简介:面向嵌入式开发者的 TMS320F28335 数字信号处理器 I2C 总线硬件实现工程包,适合正在学习 C28x 系列 DSP 以及 I2C 通信协议的工程师和高年级学生。该工程以 DSP 主控芯片与 AT24C02 电可擦除存储器之间的读写通信为线索,完整呈现了寄存器初始化、起始与停止条件、器件地址发送、数据读写等关键流程,同时提及 TMS320F281x 系列 DSP 的常见外部接口,帮助读者建立对 C28x 家族外设资源的整体认知。压缩包共包含 83 个文件,体积仅为 235KB,文件类型以 C/C++ 源码、汇编程序、链接命令文件、工程配置文件以及编译生成的输出文件为主,内有完整 CCS 工程模板和库文件,可直接导入开发环境进行编译和调试。该资源已有 634 人浏览学习。通过这套工程代码,读者可以快速掌握 DSP 的 I2C 驱动编写与调试方法,并将其灵活移植到电机控制、传感器采集等实际项目中。
1. 把I2C当UART写的DSP工程师,先停下来看这套硬件时序
做DSP开发的人大多对SCI和SPI很熟,遇到EEPROM、RTC、温度传感器这类I2C器件时,第一反应往往是拿GPIO软模拟一套时序。在TMS320F28335上这么做不是不行,但你会失去一个关键优势:这款芯片的I2C模块自带16字节发送/接收FIFO、多主模式仲裁和总线错误检测,软模拟把这些全部归零。更现实的问题是,软模拟的时序在中断频繁、主频跑到150MHz的系统中很容易被拉长,400kHz快速模式下SCL高电平宽度抖动一旦超过300ns,器件不认账,问题还难以复现。本文直接从TMS320F28335的I2C硬件模块出发,把引脚配置、时钟计算、寄存器操作和C/C++代码串成一条完整链路,顺带讲清楚tms320f281x系列DSP那些外部接口——很多人以为281x和28335外设差不多,实际在I2C这件事上两代芯片的差异直接决定方案选型。
2. TMS320F28335的I2C硬件模块:FIFO、从机地址和总线仲裁
2.1 I2C外设在28335里的位置与整体结构
TMS320F28335的I2C模块是一个符合Philips I2C规范标准(后为NXP)的外设,支持最高400kbit/s的快速模式,也支持100kbit/s标准模式。它的核心结构是:一个串行时钟发生器、一个发送/接收移位寄存器、一个从机地址比较器、两个中断源(发送和接收各一个)、以及各16字节深度的发送FIFO和接收FIFO。
这个模块在C2000系列里属于中规模外设,不需要像EMIF那样配置等待状态和位宽,也不像McBSP那样要和DMA联动才能高效工作。它的寄存器全部映射在外设帧空间,地址从0x6C00开始连续排列。和很多MCU的I2C外设不同,28335的I2C模块没有独立的时钟使能位——它挂在系统时钟上,模块使能通过I2CMDR寄存器里的IRS位控制。
#define I2C_BASE 0x00006C00 #define I2CMDR 0x00006C20 // 模块模式寄存器2.2 时钟计算:I2CPSC、I2CCLKL、I2CCLKH三个寄存器的配合
硬件I2C区别于软件模拟的核心就是它有时钟发生器。SCL的每个周期由低电平宽度和高电平宽度两部分决定,分别由I2CCLKL和I2CCLKH寄存器控制。I2CPSC则负责把系统时钟分频得到模块内部时钟,这个内部时钟是I2CCLKL/I2CCLKH的计数基准,在标准模式下要求模块时钟频率在7MHz到12MHz之间,快速模式下也要在7MHz到12MHz之间。
具体的计算公式如下:
module_clock = SYSCLKOUT / (I2CPSC + 1) SCL_low_time = (I2CCLKL + 1) * module_clock_period SCL_high_time = (I2CCLKH + 1) * module_clock_period SCL_freq = 1 / (SCL_low_time + SCL_high_time)以28335默认的150MHz系统时钟为例,目标400kbit/s快速模式,取I2CPSC = 2,模块时钟就是150 / 3 = 50MHz,这已经超出12MHz范围,说明分频不够。取I2CPSC = 14,模块时钟为150 / 15 = 10MHz,落在允许区间。SCL周期为1 / 400000 = 2500ns,对应2500 / 100 = 25个模块时钟周期,按照占空比接近50%来分配,I2CCLKL = 12,I2CCLKH = 12,即低电平13个时钟周期(1300ns),高电平13个周期(1300ns)。
void i2c_clk_init(void) { I2CPSC = 14; // 模块时钟 = 150MHz / (14+1) = 10MHz I2CCLKL = 12; // SCL低电平 13 * 100ns = 1300ns I2CCLKH = 12; // SCL高电平 13 * 100ns = 1300ns }这里有个容易踩的坑:I2CPSC在I2C模块未复位时写入不会立即生效,必须先设I2CMDR的IRS = 0让模块复位,配置完PSC和CLK后再置IRS = 1。TI的例程经常把这段写在后面,很多人习惯性地先开IRS,导致时钟还是默认值。
2.3 从机地址与寄存器位域:用C语言结构体操作外设寄存器
28335的I2C寄存器在头文件里被定义成结构体形式,I2C模块的寄存器组可以按下面的方式映射:
typedef volatile struct { Uint32 I2COAR; // 自身地址 Uint32 I2CIER; // 中断使能 Uint32 I2CSTR; // 状态 Uint32 I2CCLKL; // 时钟低 Uint32 I2CCLKH; // 时钟高 Uint32 I2CCNT; // 数据计数 Uint32 I2CDRR; // 接收数据 Uint32 I2CSAR; // 从机地址 Uint32 I2CMDR; // 模式 Uint32 I2CISRC; // 中断源 Uint32 I2CMDR2; // 模式扩展(含FIFO相关) } I2C_Registers;写从机地址时注意,I2CSAR的低7位是从机地址,第8位在扩展寻址模式下是扩展地址位。对于最常见的写入EEPROM(比如AT24C02从机地址0xA0),I2C总线上的第一字节是7位地址左移一位,I2CSAR寄存器实际存入的是低7位地址,也就是0x50。C/C++工程师容易在这里把0xA0当成地址写进寄存器,结果发送到总线上的地址变成了0x140,从机根本不应答。
3. tms320f281x系列的外部接口全景:从2812的SPI到28335的I2C
3.1 281x系列外设接口清单与I2C缺失问题
tms320f281x系列(典型型号TMS320F2812)和TMS320F28335在外部接口上的差异不只是I2C这么简单。2812有一组128K×16位的外部存储器接口(XINTF),支持三个独立片选区,地址总线19位,数据总线16位,可以并行接SRAM、NOR Flash,这是28335没有的——28335只有一个14位地址、16位数据的XINTF,使用范围窄很多。但反过来,2812整颗芯片没有硬件I2C模块。
281x系列可用的串行接口包括:两个SCI(UART)、一个SPI、一个McBSP。用2812接EEPROM或者SHT30这类传感器,标准做法是用SPI接口或者GPIO软件模拟I2C。SPI接EEPROM是最省事的,AT25系列和AT24系列选型时先看主控端——2812只有SPI,就直接选AT25,不要选AT24然后自己去软件模拟I2C,时序不稳定还会拖慢主循环。
3.2 28335的接口升级:新增I2C与更灵活的GPIO复用
F28335与281x相比,接口层面最重要的变化就是加入了I2C和CAN模块的增强。28335的I2C引脚复用在GPIO28(SDAA)和GPIO29(SCLA)上,这两个引脚同时也可以配置成普通的通用数字量IO。这个复用关系在代码里是这样处理的:
// 使能GPIO28为I2C SDA功能 GpioCtrlRegs.GPAMUX2.bit.GPIO28 = 2; // 选择第2功能:I2C SDA // 使能GPIO29为I2C SCL功能 GpioCtrlRegs.GPAMUX2.bit.GPIO29 = 2; // 选择第2功能:I2C SCLGPIO28/29在28335里属于A组第二组复用引脚,每个引脚有4个可选功能,对应GPAMUX2寄存器的两位字段。值0是通用IO,值1是SCI相关或外部中断,值2是I2C功能,值3是某种外设输出。C2000的GPIO复用规则在不同系列里并不一致,2812的GPIO是单独I/O复用寄存器,28335则引入MUX寄存器组的概念,迁移代码时不能直接照抄。
3.3 外部接口选型:从模块化角度决定I2C还是SPI
| 对比维度 | TMS320F2812 | TMS320F28335 |
|---|---|---|
| I2C硬件模块 | 无 | 有(标准/快速模式) |
| SPI | 1路,16级FIFO | 1路,16级FIFO |
| SCI(UART) | 2路,16级FIFO | 3路,16级FIFO |
| McBSP | 1路,可配SPI/语音 | 2路,可配SPI/语音 |
| CAN | 1路eCAN | 2路eCAN(A/B) |
| XINTF外部存储器接口 | 3个片选区,19位地址 | 1个片选区,14位地址 |
| 主频 | 150MHz | 150MHz |
从这张表能看出一个设计结论:在F28335上优先使用硬件I2C接低速传感器和EEPROM,把SPI留给高速ADC或者Flash芯片。I2C在28335上只占用GPIO28/29两个引脚,而且SCL是硬件产生,时序不会因为中断响应延迟而变形。2812上如果非要接I2C设备,要么用McBSP配置成SPI模式再接SPI转I2C桥接芯片(常见做法不推荐),要么用GPIO软件模拟并保证I2C总线上拉电阻和线路电容在允许范围内。
3.4 硬件设计要点:上拉电阻和电平匹配
I2C是开漏结构,SCL和SDA两根线必须接上拉电阻。28335的GPIO28/29在I2C模式下是开漏输出,内部没有上拉,必须在PCB上放4.7kΩ到10kΩ的上拉电阻。取值逻辑是:总线速度快则电阻取小(400kHz时常见4.7kΩ),线路电容大则也要取小,但要权衡灌电流——电阻太小会让引脚灌入过多电流。3.3V供电时2kΩ电阻灌流约1.65mA,5V系统就要重新计算,一般不推荐28335直接和5V器件连接,需要电平转换或串接限流电阻。
4. C/C++实现:从零配置到读写EEPROM的完整代码
4.1 寄存器级初始化函数:复用GPIO、FIFO、中断的完整设置
用寄存器操作写28335 I2C驱动时,从TI的官方库函数往底层转换的关键在于理解初始化顺序。下面这段C函数是硬件I2C最精简可用的初始化流程:
void I2C_init(Uint32 sysclk) { // 1. 配置GPIO复用 EALLOW; GpioCtrlRegs.GPAMUX2.bit.GPIO28 = 2; // SDA GpioCtrlRegs.GPAMUX2.bit.GPIO29 = 2; // SCL // 2. 复位I2C模块,使PSC和CLK写入生效 I2cRegs.I2CMDR.bit.IRS = 0; // 3. 配置分频系数(以150MHz主频、400kbit/s为例) I2cRegs.I2CPSC = 14; // 150MHz/(14+1)=10MHz模块时钟 I2cRegs.I2CCLKL = 12; // 低电平=13*100ns=1300ns I2cRegs.I2CCLKH = 12; // 高电平=13*100ns=1300ns // 4. 使能模块和FIFO,使能发送/接收FIFO中断 I2cRegs.I2CMDR.bit.IRS = 1; I2cRegs.I2CMDR.bit.DLB = 0; // 正常模式(非回环) I2cRegs.I2CMDR.bit.TRM = 1; // 主机模式 I2cRegs.I2CMDR.bit.MST = 1; // 主控器模式 I2cRegs.I2CMDR2.bit.FIFOEN = 1; // 使能FIFO I2cRegs.I2CIER.bit.RXRDY_IE = 1; // 接收就绪中断 I2cRegs.I2CIER.bit.XRDY_IE = 1; // 发送就绪中断 EDIS; }对这段代码有几个参数要说明:I2CPSC的分频值决定了模块时钟的频率,在400kHz快速模式下,模块时钟应该在7MHz到12MHz之间,所以10MHz是合理选择。I2CCLKL和I2CCLKH分别决定SCL低电平和高电平持续多少个模块时钟周期,各达到1300ns的原因是400kHz要求单周期总时长2.5µs,300ns以内的偏差通常都能容忍。I2CMDR里MST和TRM两个位必须同时为1才是主控发送模式,如果漏了MST位,模块会进入从机模式,这时发送操作永远得不到ACK信号。
4.2 主控发送函数:起始条件、从机地址、数据、停止条件
硬件I2C的发送流程比软件模拟简单很多——你只需要把数据写入发送数据寄存器(I2CDXR),模块自己处理SCL时钟、ACK采样和总线仲裁。但要注意写入I2CDXR的时机,必须等前一个数据已经移位发送完毕。
void I2C_write_bytes(Uint16 slave_addr, Uint16 *data, Uint16 len) { Uint16 i; // 1. 设置从机地址(低7位有效) I2cRegs.I2CSAR = slave_addr & 0x7F; // 2. 设置要发送的字节数 I2cRegs.I2CCNT = len; // 3. 清除状态寄存器中的总线忙和停止条件位 I2cRegs.I2CSTR.all = 0; // 4. 发送起始条件(STT=1),模块自动产生START并发送地址 I2cRegs.I2CMDR.bit.STT = 1; I2cRegs.I2CMDR.bit.STP = 0; // 5. 循环写入数据 for(i = 0; i < len; i++) { while(I2cRegs.I2CSTR.bit.XRDY == 0); // 等待发送就绪 I2cRegs.I2CDXR = data[i]; // 最后一个字节写入后,检查是否发送完成 if(i == len - 1) { while(I2cRegs.I2CSTR.bit.XRDY == 0); // 等待SCD(停止条件已发出) } } }这个函数的关键点不在循环本身,而在I2CCNT的用法。I2CCNT需要在发送起始条件之前设置,模块在发送START后自动按照I2CCNT的数值连续发数据。最后一个字节写入I2CDXR后,STP位必须在SCL时钟高电平期间被写入才能正确产生停止条件。如果STP过晚写入,总线会停在低电平,从机认为数据传输未结束。常见做法是在发送最后一个字节前先设置I2CMDR的STP位,或者通过硬件FIFO配合I2CCNT一次完成。
4.3 主控接收函数:读EEPROM时的重复起始位
从EEPROM读数据时,总线上的时序是先发一个写命令(从机地址+0),再发寄存器地址,然后发重复起始位,再发读命令(从机地址+1),最后读取数据并产生停止条件。28335硬件模块对重复起始位的支持方式是:在读操作时把I2CMDR的STT位再次置1,同时保持STP=0。
void I2C_read_bytes(Uint16 slave_addr, Uint16 mem_addr, Uint16 *buf, Uint16 len) { Uint16 i; // 第一阶段:假装要写,发送从机地址和内存寄存器地址 I2cRegs.I2CSAR = slave_addr & 0x7F; I2cRegs.I2CCNT = 1; // 先发1个字节(内存地址) I2cRegs.I2CMDR.bit.STT = 1; // 起始 I2cRegs.I2CMDR.bit.TRM = 1; // 发送模式 I2cRegs.I2CMDR.bit.STP = 0; while(I2cRegs.I2CSTR.bit.XRDY == 0); I2cRegs.I2CDXR = mem_addr; // 发送内存地址 while(I2cRegs.I2CSTR.bit.XRDY == 0); // 第二阶段:切换为接收,发重复起始位 I2cRegs.I2CCNT = len; // 要接收的字节数 I2cRegs.I2CMDR.bit.TRM = 0; // 切换为接收模式 I2cRegs.I2CMDR.bit.STT = 1; // 重复起始位 I2cRegs.I2CMDR.bit.STP = 1; // 最后一个字节后自动停止 // 第三阶段:等待接收 for(i = 0; i < len; i++) { while(I2cRegs.I2CSTR.bit.RXRDY == 0); buf[i] = I2cRegs.I2CDRR; // 读出数据自动清RXRDY } while(I2cRegs.I2CSTR.bit.SCD == 0); // 等待总线释放 }第二阶段里STT和STP同时置1是硬件模块的合法操作,模块会先发重复起始位,然后在I2CCNT计数值递减到0时自动产生停止条件。注意I2CMDR的TRM位在重复起始位发出之前必须切到接收模式,否则模块会继续发数据而不是等从机返回数据。接收完成循环里的每次读取I2CDRR都会清除RXRDY标志,这个清除动作是硬件自动的,不能像发送循环那样额外去清零状态位。
4.4 FIFO模式下的中断处理:批量传输时用16字节缓冲
上面的代码是逐字节查询方式,在连续读写大批量数据(比如128字节EEPROM页写)时效率不高。28335的I2C模块支持FIFO模式,发送和接收各有16字节深度的FIFO,与CPU的交互可以变成一次中断处理多个字节:
interrupt void I2C_INT_ISR(void) { Uint16 int_source; int_source = I2cRegs.I2CISRC.bit.INT_SRC; // 读取中断源 switch(int_source) { case 1: // 发送FIFO中断 if(tx_index < tx_len) { // 一次把剩余数据写入FIFO,最多16字节 while(tx_index < tx_len && tx_index - tx_hw_index < 16) { I2cRegs.I2CDXR = tx_buffer[tx_index++]; } } else { // 数据发完,允许停止条件 I2cRegs.I2CMDR.bit.STP = 1; } break; case 2: // 接收FIFO中断 while(rx_index < rx_len && (I2cRegs.I2CSTR.bit.RXFFST != 0)) { rx_buffer[rx_index++] = I2cRegs.I2CDRR; } break; } }FIFO中断的核心是I2CFFTX和I2CFFRX两个寄存器的配合。I2CFFTX的TXFFIL位设置发送FIFO中断触发阈值,I2CFFRX的RXFFIL位设置接收FIFO中断触发阈值。例如TXFFIL = 8表示FIFO中剩余字节数小于等于8时触发中断。使用FIFO模式后,注意I2CMDR里的重复模式位和FIFO使能位是在I2CMDR2里单独控制的,和I2CIER的中断使能是两套独立逻辑。
4.5 从C到C++:封装I2C为类时的注意事项
用C++写28335的驱动时,最常见的问题是外设寄存器的访问权限。28335的头文件把寄存器定义成了全局性结构体指针,如果封装成类,要避免在构造函数里访问寄存器——C++全局对象构造时机在main之前,此时DSP的时钟和GPIO配置都还没执行,构造函数里访问I2C寄存器等于访问一个还没上电的外设。
class I2C_Device { private: uint16_t slave_addr; void wait_for_free(); public: I2C_Device(uint16_t addr) : slave_addr(addr) {} void init(); void write(uint16_t reg, uint16_t* data, uint16_t len); void read(uint16_t reg, uint16_t* data, uint16_t len); };这个类的设计里,init()是普通成员函数而不在构造函数中完成,确保在main明确执行后再调用。类的初始化列表也指向了一个关键测试点:如果某个I2C设备地址是固定的,可以在构造函数里就赋值。C++封装还有一个好处是可以用模板让一个类适配不同地址的设备,而不必复制函数代码。
5. 总线仲裁与多主模式:DSP和MCU同时挂在I2C上
单主模式是绝大多数28335 I2C应用的默认形态,但不少电力电子控制系统里,DSP和一个管理MCU会挂同一条I2C总线,多点通信时总线仲裁机制就值得关注。硬件I2C模块在发送数据时,每个SCL高电平期间采样SDA,如果发现总线上SDA电平与自己输出的不一致(比如自己输出高、总线被别的设备拉低),就判定发生仲裁丢失,模块会关闭输出并置位仲裁丢失标志位。
void I2C_check_arb_lost(void) { if(I2cRegs.I2CSTR.bit.ARDY == 0) { // 仲裁没有发生或已经完成 } if(I2cRegs.I2CSTR.bit.AL == 1) { // 仲裁丢失,需要重新发起传输 I2cRegs.I2CSTR.bit.AL = 1; // 写1清除 I2cRegs.I2CMDR.bit.STT = 1; // 重新发起起始条件 } }多主模式下有一个容易忽略的寄存器:I2COAR(自身地址寄存器)。它决定了28335作为从机时能响应哪些地址。如果I2COAR的值和总线上另一个主设备地址冲突,仲裁几乎必然失败。检查I2COAR的值是最快的定位方法。另外,I2C总线上的上拉电阻在多主模式下要特别注意,两个主设备共用一组上拉没有问题,但任一键合了过小的下拉会使总线整体性能下降。
6. 时序验证三招:示波器看波形、回环测试、总线出错恢复
硬件I2C的调试和SPI不一样——SPI有时钟,对着时钟沿采数据就行,I2C只有两根线,没有参考时钟,出了时序问题只能靠示波器或者逻辑分析仪。第一招是把示波器两个通道分别接在SDA和SCL上,触发方式设为SDA下降沿,抓一次读EEPROM的完整时序。重点看SCL高电平的宽度是否稳定在1µs左右(100kHz模式)或1.25µs(400kHz模式,占空比50%时),如果发现个别SCL高电平被拉长到5µs以上,说明代码在等待期间被中断打断,模块时钟没有继续输出,这是正常现象但会影响低速器件的容错——此时可把I2CCLKKL调小一点补偿平均频率。
第二招是回环测试,把I2CMDR的DLB位(数字回环)置1,模块的发送输出被内部连接到接收输入。这个模式下不需要外部连接就能验证初始化代码是否正确。写一个数据到I2CDXR,再从I2CDRR读回来,数据一致说明模块时钟、分频和中断配置都工作正常。回环测试是纯数字测试,不涉及IO电气特性,只能用来验证寄存器配置。
void I2C_loopback_test(void) { Uint16 test_val = 0x5A; // 进入回环模式(数字回环) I2cRegs.I2CMDR.bit.DLB = 1; // 复位FIFO指针 I2cRegs.I2CFFTX.bit.TXFFRST = 1; I2cRegs.I2CFFRX.bit.RXFFRST = 1; // 写一个字节 I2cRegs.I2CDXR = test_val; // 等待接收就绪 while(I2cRegs.I2CSTR.bit.RXRDY == 0); // 读取回环数据 Uint16 rd_val = I2cRegs.I2CDRR; if(rd_val == test_val) { // 模块配置正确 } else { // 寄存器配置错误,检查时钟分频 } // 退出回环模式 I2cRegs.I2CMDR.bit.DLB = 0; }第三招是总线出错恢复。I2C总线最常见的错误是SCL正常、SDA被拉死永远为低——通常是因为某个从机处于某种异常状态等不到停止条件。硬件恢复方式是让主机在SCL上打9个时钟脉冲,从机会在收到第9个脉冲后释放SDA。由于28335的I2C模块没有直接发送时钟脉冲的功能位,常见做法是把GPIO28(SDA)切回普通GPIO输出高电平,GPIO29(SCL)配置成普通GPIO并手动翻转9次。很多I2C总线锁死的问题都出在EEPROM写周期未完成时就断电,恢复操作已经够用。
本文还有配套的精品资源,点击获取