1. 从一次通信失败说起:为什么DSP的I2C总让人头疼?
如果你正在用TI的TMS320F2803x系列DSP做项目,并且需要和外部传感器、EEPROM或者RTC时钟芯片打交道,那你大概率绕不开I2C总线。我刚开始接触2803x的I2C模块时,也以为它会像GPIO或者PWM那样“开箱即用”,结果却被现实狠狠教育了一番。最典型的场景就是,代码写好了,逻辑看起来也没问题,但用示波器一抓波形,要么是起始信号(S)没出来,要么是设备地址(Slave Address)发出去后收不到应答(ACK),总线直接卡死,SCL线被拉低,程序陷入超时等待。
这其实不是你的代码写错了,而是DSP的I2C模块和常见的单片机(比如STM32)在设计和用法上有一些根本性的不同。2803x的I2C模块更偏向于一种“状态机驱动”的模式,它把整个通信过程拆解成了非常细粒度的步骤,需要你手动去查询状态、清除标志位、然后写入下一个动作。它不像某些库函数那样,一个I2C_Write()函数就帮你搞定从起始到停止的所有事情。这种设计带来了极高的灵活性,但也对开发者的理解深度提出了要求。如果你只是照搬网上STM32的I2C例程,十有八九会失败。
所以,这篇内容我们就来彻底拆解2803x的I2C模块。我们不只讲怎么配置寄存器让总线跑起来,更要深入它的状态机,理解每一个状态转移的条件,以及在实际调试中如何根据示波器波形和寄存器状态快速定位问题。你会发现,一旦掌握了它的“脾气”,2803x的I2C会变得非常可靠和强大。
2. 核心差异:状态机驱动的I2C vs 库函数封装的I2C
在开始写代码之前,我们必须先建立正确的认知模型。很多人调不通,根源在于用错了模型。
2.1 常见的“黑盒”模型及其局限
许多单片机(尤其是使用HAL库或标准外设库的ARM Cortex-M芯片)将I2C操作封装成了高级API。例如,你想往EEPROM的0x00地址写一个字节,可能只需要调用三个函数:
I2C_Start()I2C_WriteByte(DEVICE_ADDR | WRITE)并检查ACKI2C_WriteByte(0x00)并检查ACKI2C_WriteByte(data)I2C_Stop()
在这个过程中,开发者不需要关心底层硬件具体在哪个时钟周期发出了SDA信号,只需要关心“发送”这个动作是否成功。库函数内部帮你处理了所有状态标志和时序。这种模型简单易用,但一旦出现问题(比如ACK失败),调试起来就像个黑盒,你很难知道具体卡在哪一步。
2.2 2803x的“白盒”状态机模型
2803x的I2C模块则完全不同。它有一个非常清晰的状态寄存器(I2CSTR)和对应的状态机。整个通信过程,你必须像操作一台精密的机床,手动推动状态机前进。
它的基本工作流程是这样的:
- 你通过写数据寄存器(
I2CDXR)或配置寄存器来“下达指令”,告诉模块下一步要做什么(比如:发送起始条件)。 - 模块执行这个指令,同时内部状态机跳转。
- 模块完成动作后,会在
I2CSTR寄存器中设置一个特定的状态标志位(比如XSMT标志变为0表示发送移位寄存器空了,ARDY标志变为1表示地址寄存器就绪)。 - 你必须主动查询这些标志位,确认上一步动作已完成。
- 然后,你必须手动清除这个标志位(通过向特定比特位写1),为下一个状态做准备。
- 最后,你才能进行下一步操作(比如写入从机地址)。
如果跳过第4和第5步,直接进行下一步操作,状态机就会混乱,导致通信失败。这就是为什么“抄代码”容易失败的原因——例程里那些while(!I2C_IsBusBusy())或while(!I2C_GetStatusFlag())的语句,以及后面紧跟着的I2C_ClearStatusFlag(),都不是可有可无的,而是状态机运转的核心逻辑。
理解了这个模型,我们再去看数据手册里的状态转移图,就不会觉得是天书了。它其实是在告诉你:“当你处在状态A,并且完成了动作X,标志位Y会置起;你清掉Y后,才能写入命令Z,进入状态B。”
3. 硬件初始化与配置:奠定稳定的通信基础
在操作状态机之前,我们需要先把硬件舞台搭建好。2803x的I2C初始化主要涉及引脚、时钟和模块基本参数。
3.1 引脚功能复用与上拉电阻配置
2803x的I2C引脚通常是GPIO的多功能复用引脚。以常见的GPIO32(SDA)和GPIO33(SCL)为例,你需要将它们配置为外设功能,而不是普通的数字IO。
// 假设使用GPIO32和GPIO33作为I2C引脚 EALLOW; // 允许写入受保护的寄存器 GpioCtrlRegs.GPBMUX2.bit.GPIO32 = 1; // 将GPIO32配置为外设功能(I2CA_SDA) GpioCtrlRegs.GPBMUX2.bit.GPIO33 = 1; // 将GPIO33配置为外设功能(I2CA_SCL) // 通常I2C引脚需要配置为开漏输出,但2803x的GPIO模块在复用为I2C时硬件会自动处理。 // 更关键的是,需要禁用这些引脚的上拉电阻,因为I2C总线需要外部上拉。 GpioCtrlRegs.GPBPUD.bit.GPIO32 = 1; // 禁用内部上拉 GpioCtrlRegs.GPBPUD.bit.GPIO33 = 1; // 禁用内部上拉 EDIS; // 禁止写入受保护的寄存器注意:这里有一个极易忽略的坑。有些开发板为了省事,可能使能了GPIO的内部上拉电阻。如果内部上拉和外部上拉同时存在,会导致总线上拉过强,影响上升沿速度,在高速模式(400kHz)下可能造成时序问题。最稳妥的做法是始终禁用I2C引脚的内部上拉,依靠可靠的外部上拉电阻。典型的上拉电阻值在3.3V系统下为4.7kΩ或10kΩ,具体取决于总线电容和通信速度。
3.2 模块时钟与通信速率计算
I2C模块的时钟来源于系统时钟(SYSCLKOUT)经过一个预分频器。通信速率(100kbps标准模式或400kbps快速模式)由I2CPSC和I2CCLKL/I2CCLKH寄存器共同决定。
计算过程比想象中要绕一点,因为涉及一个预分频和一个主时钟分频:
- IPSC分频:
I2CPSC寄存器用于产生模块工作的基础时钟I2C_CLK。公式为:I2C_CLK = SYSCLKOUT / (I2CPSC + 1)。这个I2C_CLK必须介于7-12 MHz之间(根据数据手册),以保证模块正常工作。通常我们取一个中间值,例如SYSCLKOUT=60MHz,设置I2CPSC = 5,则I2C_CLK = 60 / (5+1) = 10 MHz。 - 主时钟分频:
I2CCLKL和I2CCLKH寄存器用来对I2C_CLK进行分频,以产生最终的SCL时钟。SCL的低电平周期由I2CCLKL决定,高电平周期由I2CCLKH决定。它们都是I2C_CLK周期数的整数倍。- SCL低电平时间 =
(I2CCLKL + 7) * (1 / I2C_CLK) - SCL高电平时间 =
(I2CCLKH + 5) * (1 / I2C_CLK) - 整个SCL周期 = 低电平时间 + 高电平时间
- 通信速率 =
1 / SCL周期
- SCL低电平时间 =
例如,我们要配置100kHz标准模式,I2C_CLK=10MHz:
- 目标周期 T = 1/100kHz = 10 us。
- 我们需要分配高低电平时间。通常为了占空比接近50%,设低电平时间 ~5us,高电平时间 ~5us。
- 计算
I2CCLKL:5us = (I2CCLKL + 7) * 0.1us=>I2CCLKL + 7 = 50=>I2CCLKL = 43 - 计算
I2CCLKH:5us = (I2CCLKH + 5) * 0.1us=>I2CCLKH + 5 = 50=>I2CCLKH = 45 - 验证:周期 = (43+7+45+5)0.1us = 1000.1us = 10us,速率=100kHz。
初始化代码如下:
void I2CA_Init(void) { // 1. 使能I2C模块时钟(在PCLKCR0寄存器中) EALLOW; SysCtrlRegs.PCLKCR0.bit.I2CAENCLK = 1; // 使能I2C-A模块时钟 EDIS; // 2. 初始化I2C模块为宿主/主模式 I2caRegs.I2CMDR.all = 0x0000; // 先复位MDR寄存器 I2caRegs.I2CMDR.bit.IRS = 0; // 先将模块复位(IRS=0) // 3. 配置时钟预分频,产生合适的I2C_CLK // 假设SYSCLKOUT = 60MHz,目标I2C_CLK = 10MHz I2caRegs.I2CPSC.all = 5; // I2C_CLK = 60/(5+1)=10MHz // 4. 配置SCL时钟分频,产生100kHz I2caRegs.I2CCLKL = 43; // SCL低电平时间 I2caRegs.I2CCLKH = 45; // SCL高电平时间 // 5. 配置其他参数:自由数据格式模式(FDF=0,即7位地址模式),主模式 I2caRegs.I2CMDR.all = 0x0000; // 清空 I2caRegs.I2CMDR.bit.BC = 0; // 字节数,在自由模式下忽略 I2caRegs.I2CMDR.bit.FDF = 0; // 非自由数据格式,使用7位地址 I2caRegs.I2CMDR.bit.STT = 0; // 起始条件位,由软件置位 I2caRegs.I2CMDR.bit.TRX = 1; // 传输方向:1=主发送,0=主接收 I2caRegs.I2CMDR.bit.MST = 1; // 主模式 I2caRegs.I2CMDR.bit.IRS = 1; // 使能模块(退出复位) // 6. 配置超时寄存器(防止总线死锁) I2caRegs.I2CTOA.all = 0xFFFF; // 设置一个较大的超时值 // 7. 清除所有可能挂起的标志位 I2caRegs.I2CSTR.all = 0xFFFF; // 向标志位写1清除它们 }这段初始化代码的关键是IRS位。你必须先将其清零(复位模块),在配置完所有参数后,再置1(使能模块)。在模块禁用时配置参数是安全的。
4. 主发送模式实战:以写入EEPROM为例
理论讲完了,我们用一个最经典的例子——向24C02 EEPROM写入一个字节——来串联整个状态机的操作流程。假设设备地址是0x50(7位地址,左移一位后写地址为0xA0)。
我们的目标是:向EEPROM的0x00地址写入数据0x55。
4.1 操作流程与状态机推演
一次完整的“主发送”写操作包含以下步骤,每一步都对应着状态机的变迁和标志位的操作:
产生起始条件(S):
- 动作:将
I2CMDR寄存器的STT位设为1。 - 状态变化:模块检测到总线空闲后,会在SCL为高时拉低SDA,产生起始条件。完成后,状态寄存器
I2CSTR中的ARDY(地址就绪)标志位会置1。 - 关键操作:必须等待
ARDY置1,然后写1清除它,才能进行下一步。
- 动作:将
发送从机地址+写位(SLA+W):
- 动作:将7位地址和写位(0)组合成的字节(0xA0)写入发送数据寄存器
I2CDXR。 - 状态变化:模块会自动将这个字节发出。发送完成后,会等待从机的应答(ACK)。如果收到ACK,
I2CSTR中的ARDY标志会再次置1(表示地址阶段完成)。如果没收到ACK(NACK),则NACK标志会置1,通信失败。 - 关键操作:等待
ARDY再次置1,然后清除它。同时,必须检查NACK标志是否为0。
- 动作:将7位地址和写位(0)组合成的字节(0xA0)写入发送数据寄存器
发送内存地址(Word Address):
- 动作:将EEPROM的内部地址(0x00)写入
I2CDXR。 - 状态变化:模块发送这个字节,等待ACK。完成后,
I2CSTR中的ARDY标志置1(对于数据字节,实际上是XRDY发送就绪标志更准确,但ARDY在非重复起始条件下也会响应)。 - 关键操作:等待
ARDY置1,清除它。
- 动作:将EEPROM的内部地址(0x00)写入
发送要写入的数据(Data):
- 动作:将数据(0x55)写入
I2CDXR。 - 状态变化:同上,发送字节,等待ACK,标志位置起。
- 关键操作:等待
ARDY或XRDY置1,清除它。
- 动作:将数据(0x55)写入
产生停止条件(P):
- 动作:将
I2CMDR寄存器的STP位设为1。 - 状态变化:模块在完成最后一个字节的传输后,会产生停止条件(SCL为高时,SDA由低变高)。完成后,
I2CSTR中的ARDY标志会置1。 - 关键操作:等待
ARDY置1,清除它。至此,一次完整的写操作结束。
- 动作:将
4.2 代码实现与逐行解析
下面是根据上述流程编写的函数,我加入了大量的注释,解释了每一步“为什么”要这么做。
#define I2C_EEPROM_ADDR_W 0xA0 // 7位地址0x50,左移一位,写操作位为0 #define I2C_TIMEOUT 10000 // 超时计数,防止死循环 Uint16 I2CA_WriteByte(Uint16 slaveAddr, Uint16 memAddr, Uint16 data) { volatile Uint16 i; Uint16 timeout = I2C_TIMEOUT; // 步骤1:发送起始条件 I2caRegs.I2CMDR.bit.STT = 1; // 置位STT,命令模块产生起始条件 // 等待地址就绪标志ARDY置位,表示起始条件已成功发出 while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout == 0) return 0; // 超时,返回错误 I2caRegs.I2CSTR.bit.ARDY = 1; // **关键!** 写1清除ARDY标志,为下一步做准备 timeout = I2C_TIMEOUT; // 步骤2:发送从机地址(写) I2caRegs.I2CDXR = slaveAddr; // 写入从机地址+写位 // 等待ARDY再次置位,表示地址字节已发送并(期望)收到ACK while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout == 0) return 0; // 清除ARDY前,先检查是否收到NACK(无应答)。这是排查设备未连接或地址错误的关键。 if(I2caRegs.I2CSTR.bit.NACK == 1) { // 如果收到NACK,需要清除NACK标志并发送停止条件来释放总线 I2caRegs.I2CSTR.bit.NACK = 1; I2caRegs.I2CMDR.bit.STP = 1; return 0; // 返回错误 } I2caRegs.I2CSTR.bit.ARDY = 1; // 清除ARDY标志 timeout = I2C_TIMEOUT; // 步骤3:发送内存地址(EEPROM内部地址) I2caRegs.I2CDXR = memAddr; while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout == 0) return 0; I2caRegs.I2CSTR.bit.ARDY = 1; timeout = I2C_TIMEOUT; // 步骤4:发送数据 I2caRegs.I2CDXR = data; while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout == 0) return 0; I2caRegs.I2CSTR.bit.ARDY = 1; // 步骤5:发送停止条件 I2caRegs.I2CMDR.bit.STP = 1; // 置位STP,命令模块产生停止条件 // 等待停止条件完成 while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout == 0) return 0; I2caRegs.I2CSTR.bit.ARDY = 1; // 额外步骤:等待总线空闲。STP位不会自动清零,但停止条件发出后,BB(总线忙)标志应变0。 timeout = I2C_TIMEOUT; while((I2caRegs.I2CSTR.bit.BB == 1) && (timeout--)); // 等待总线空闲 if(timeout == 0) return 0; return 1; // 写入成功 } // 调用示例 void main(void) { // ... 系统初始化,I2C初始化 ... if(I2CA_WriteByte(I2C_EEPROM_ADDR_W, 0x00, 0x55)) { // 写入成功处理 } else { // 写入失败处理 } }4.3 关键点与避坑指南
标志位清除的玄机:
I2CSTR寄存器中的标志位(如ARDY,NACK)是通过写1来清除的,而不是写0。这是一个常见误区。I2caRegs.I2CSTR.bit.ARDY = 1;这行代码的意思就是“清除ARDY中断标志”。超时处理必不可少:所有
while等待循环必须搭配超时机制。否则,一旦总线故障(如从机掉线、SCL被意外拉低),程序将永远卡死。NACK处理:在发送地址后检查
NACK标志至关重要。如果从机不存在或地址错误,模块会收到NACK。你的代码必须能检测到这种情况,并主动发送停止条件 (STP=1) 来释放总线,否则总线会一直处于忙状态。STP位的特殊性:
STP位在你写入1后,模块会在当前传输完成后自动产生停止条件,然后硬件会自动清零STP位。所以你不需要像清除ARDY那样去清除STP。总线忙(BB)标志:在停止条件发出后,虽然
ARDY置位了,但SDA/SCL线恢复到高电平还需要一点时间。通过检查BB标志是否为0,可以更可靠地确认总线已完全释放,便于发起下一次传输。
5. 主接收模式与重复起始条件:读取EEPROM数据
写操作相对简单,因为方向一致。读操作则复杂一些,因为它涉及方向改变,必须使用“重复起始条件”(Repeated Start, Sr)。流程是:先发送写地址告诉从机要读哪个内存地址,然后不停止总线,直接发送一个重复起始条件,接着发送读地址,最后读取数据。
目标:从EEPROM的0x00地址读取一个字节。
5.1 读操作流程解析
- 发送起始条件(S)。
- 发送从机地址+写位(SLA+W = 0xA0)。告诉EEPROM:“我要写操作”。
- 发送内存地址(0x00)。告诉EEPROM:“我要写入/操作这个地址”。(注意,这里我们并不真的写数据,只是把内部地址指针定位过去)。
- 发送重复起始条件(Sr)。这是与写操作最大的不同。我们不发送停止条件,而是再次发送起始条件。在2803x中,这通过再次置位
STT位来实现,而此时STP位为0且总线处于忙状态。 - 发送从机地址+读位(SLA+R = 0xA1)。告诉EEPROM:“现在我要读数据了”。
- 切换为主接收模式,并读取数据。将
I2CMDR.bit.TRX位改为0(主接收),然后从I2CDRR数据接收寄存器读取数据。注意,在接收最后一个字节前,需要配置为发送NACK(非应答)和停止条件。 - 发送停止条件(P)。
5.2 代码实现:理解状态机的方向切换
Uint16 I2CA_ReadByte(Uint16 slaveAddr, Uint16 memAddr, Uint16 *data) { volatile Uint16 i; Uint16 timeout = I2C_TIMEOUT; // --- 第一部分:发送设备地址和内存地址(伪写操作)--- I2caRegs.I2CMDR.bit.STT = 1; // 起始条件 while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout==0) return 0; I2caRegs.I2CSTR.bit.ARDY = 1; timeout = I2C_TIMEOUT; I2caRegs.I2CDXR = slaveAddr & 0xFFFE; // 确保最低位是0,写操作 while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout==0) return 0; if(I2caRegs.I2CSTR.bit.NACK) { I2caRegs.I2CSTR.bit.NACK=1; I2caRegs.I2CMDR.bit.STP=1; return 0;} I2caRegs.I2CSTR.bit.ARDY = 1; timeout = I2C_TIMEOUT; I2caRegs.I2CDXR = memAddr; // 发送要读取的内存地址 while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout==0) return 0; I2caRegs.I2CSTR.bit.ARDY = 1; timeout = I2C_TIMEOUT; // --- 第二部分:重复起始,切换为读操作 --- I2caRegs.I2CMDR.bit.STT = 1; // **关键!** 在总线忙时再次置位STT,产生的是重复起始条件(Sr) while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout==0) return 0; I2caRegs.I2CSTR.bit.ARDY = 1; timeout = I2C_TIMEOUT; // 发送读地址,并准备切换为接收模式 I2caRegs.I2CMDR.bit.TRX = 0; // **关键!** 先将传输方向改为接收 I2caRegs.I2CDXR = slaveAddr | 0x0001; // 发送从机地址+读位(1) while((I2caRegs.I2CSTR.bit.ARDY == 0) && (timeout--)); if(timeout==0) return 0; if(I2caRegs.I2CSTR.bit.NACK) { I2caRegs.I2CSTR.bit.NACK=1; I2caRegs.I2CMDR.bit.STP=1; return 0;} I2caRegs.I2CSTR.bit.ARDY = 1; timeout = I2C_TIMEOUT; // --- 第三部分:接收数据 --- // 对于单字节读取,我们希望在接收数据后发送NACK和停止条件 // 可以通过配置MDR寄存器一次性完成 // 在发送读地址前或后,配置MDR寄存器为:主接收、发送NACK、发送停止条件 // 这里我们在发送读地址后重新配置MDR I2caRegs.I2CMDR.all = 0x2C20; // 解析:MST=1(主), TRX=0(收), STT=0, STP=1, NACK=1, IRS=1 // STP=1: 接收完成后自动发停止条件 // NACK=1: 在接收数据字节后发送NACK(因为只读一个字节) // 注意:这里STT是0,因为重复起始条件已经在前面发出了。 // 等待接收数据就绪标志 RRDY while((I2caRegs.I2CSTR.bit.RRDY == 0) && (timeout--)); if(timeout==0) return 0; *data = I2caRegs.I2CDRR; // 读取数据 I2caRegs.I2CSTR.bit.RRDY = 1; // 清除接收就绪标志 // 等待停止条件完成,总线空闲 while((I2caRegs.I2CSTR.bit.BB == 1) && (timeout--)); if(timeout==0) return 0; return 1; }5.3 重复起始与NACK/STP的联合配置
这是读操作中最精妙也最容易出错的地方。对于单字节读取:
- NACK:因为只读一个字节,主机在接收到这个字节后,需要回复一个NACK(非应答)信号,告诉从机“不要再发数据了”。
- STP:接收完成后,主机需要产生停止条件来结束本次传输。
在2803x中,我们可以通过I2CMDR寄存器中的NACK和STP位,让硬件自动完成这些操作。如上面代码所示,在接收数据前,将I2CMDR配置为NACK=1和STP=1。这样,当硬件接收到一个字节后,会自动在SDA上产生NACK信号,紧接着产生停止条件。这极大地简化了软件流程。
重要提示:
NACK和STP位是“命令位”,它们的作用是指示模块在本次传输的最后一个字节后执行相应动作。因此,它们必须在接收最后一个字节之前就配置好。对于多字节读取,只有在接收倒数第二个字节后,才需要将NACK和STP位置1,以在接收最后一个字节后结束传输。
6. 调试实战:当通信失败时,我们该如何排查?
即使代码逻辑正确,在实际硬件调试中,I2C通信仍然可能失败。这时,示波器(或逻辑分析仪)和寄存器状态是你最好的朋友。
6.1 使用示波器抓取波形
将示波器的两个通道分别连接到SDA和SCL线,设置为单次触发,触发条件为SCL下降沿或SDA下降沿(起始条件)。执行一次I2C操作,抓取完整的波形。
对照检查以下关键点:
- 起始条件(S):SCL高电平期间,SDA是否有从高到低的跳变?幅度和电压是否正常(3.3V)?
- 地址字节:起始条件后,第一个8位数据(7位地址+1位读写)是否正确?波形是否干净,没有明显的毛刺或振铃?
- 应答位(ACK):每个字节(包括地址和数据)后的第9个时钟周期,SDA是否被从机拉低(ACK)?如果为高(NACK),说明从机未应答。
- 数据字节:发送或接收的数据位是否正确?
- 重复起始条件(Sr):在读操作中,在停止条件之前,是否有第二个起始条件(SCL高,SDA从高到低)?
- 停止条件(P):SCL高电平期间,SDA是否有从低到高的跳变?
- 时钟频率:测量SCL周期,计算频率是否与你配置的100kHz或400kHz相符?
- 上升/下降时间:SDA和SCL的上升沿是否陡峭?缓慢的上升沿可能是上拉电阻过大或总线电容过大的表现。
6.2 通过寄存器状态诊断问题
如果波形异常,或者程序卡在某个等待循环,就需要查询I2CSTR状态寄存器。
程序卡在
while(!I2CSTR.bit.ARDY):- 检查
BB(总线忙)位。如果BB=0,说明总线根本就没启动,可能是起始条件发送失败。检查STT位是否已置位,SCL/SDA引脚配置是否正确,外部上拉电阻是否接好。 - 如果
BB=1但ARDY一直不来,可能是总线被锁定了。检查SCL线是否被意外拉低(比如某个从机故障)。这时可以尝试通过软件复位I2C模块(IRS=0再IRS=1),或者尝试发送停止条件 (STP=1) 来强制释放总线。
- 检查
程序因
NACK标志失败:- 这是最常见的问题。首先用示波器确认发送的7位地址是否正确。
- 确认从机设备是否已上电,电源电压是否正常。
- 确认从机设备的地址引脚(如A0, A1, A2)电平配置是否与代码中地址匹配。
- 总线上是否有多个地址相同的设备导致冲突?
- SDA/SCL的上拉电阻是否合适?电阻太大导致上升沿太慢,也可能导致从机采样失败。
数据错误:
- 检查
I2CDXR写入的数据和I2CDRR读出的数据。 - 在发送端,检查
XRDY(发送就绪)标志,确保数据已移入移位寄存器。 - 在接收端,检查
RRDY(接收就绪)标志,确保数据已从移位寄存器移到I2CDRR。
- 检查
6.3 软件复位与总线恢复
当总线死锁(SCL被拉低)时,需要强制恢复。2803x提供了非标准的总线清除流程。
- 将
I2CMDR寄存器中的BC位设置为1(总线清除模式),STP位设置为1。 - 将
I2CMDR寄存器中的IRS位清零(模块复位)。 - 将 SCL 和 SDA GPIO 暂时配置为数字输出,并循环产生时钟脉冲(先拉低再拉高SCL),同时检测SDA状态,直到SDA被释放为高。
- 恢复GPIO的I2C功能,重新初始化I2C模块(
IRS=1)。
这是一个底层的硬件恢复操作,代码略复杂,但作为开发者需要知道有这么一个“救命”的后手。
7. 中断驱动与FIFO使用:提升效率的高级玩法
轮询方式简单,但占用CPU。对于频繁或大数据量的I2C操作,使用中断和FIFO是更好的选择。
7.1 中断配置
2803x的I2C模块有丰富的中断事件,最常用的是ARDY(寄存器访问就绪)和RRDY/XRDY(接收/发送就绪)。我们可以让这些事件触发CPU中断,在中断服务程序(ISR)中处理数据。
// 初始化中断 EALLOW; PieVectTable.I2CINT1A = &I2CA_ISR; // 将I2C-A的中断服务函数地址填入向量表 EDIS; I2caRegs.I2CIER.all = 0x0000; // 先禁用所有中断 I2caRegs.I2CIER.bit.ARDY = 1; // 使能ARDY中断 I2caRegs.I2CIER.bit.RRDY = 1; // 使能接收就绪中断 // I2caRegs.I2CIER.bit.XRDY = 1; // 使能发送就绪中断(根据需要) PieCtrlRegs.PIEIER8.bit.INTx1 = 1; // 使能PIE组8的第1个中断(I2CINT1A) IER |= M_INT8; // 使能CPU第8组中断 EINT; // 全局开中断 // 在I2C发送或接收函数中,启动传输后,CPU就可以去做别的事了。 // 状态机推进和数据处理将在ISR中完成。中断服务程序需要做三件事:
- 判断中断源(读取
I2CISRC寄存器)。 - 执行相应的操作(如读取
I2CDRR,或写入I2CDXR)。 - 清除中断标志(
I2CSTR中的标志位)和PIE应答位。
7.2 FIFO功能的使用
2803x的I2C模块内置了发送和接收FIFO,深度各为4字节。启用FIFO可以缓冲数据,减少中断频率。
// 启用并配置FIFO I2caRegs.I2CFFTX.all = 0x0000; // 先禁用发送FIFO I2caRegs.I2CFFRX.all = 0x0000; // 先禁用接收FIFO I2caRegs.I2CFFTX.bit.I2CFFEN = 1; // 使能I2C FIFO模式 I2caRegs.I2CFFTX.bit.TXFFIL = 0; // 发送FIFO中断级别:当FIFO为空时产生中断 I2caRegs.I2CFFTX.bit.TXFFINTCLR = 1; // 清除发送FIFO中断标志 I2caRegs.I2CFFTX.bit.TXFFIEN = 1; // 使能发送FIFO中断 I2caRegs.I2CFFRX.bit.RXFFIL = 3; // 接收FIFO中断级别:当FIFO中有4个或更多数据时产生中断 I2caRegs.I2CFFRX.bit.RXFFINTCLR = 1; // 清除接收FIFO中断标志 I2caRegs.I2CFFRX.bit.RXFFIEN = 1; // 使能接收FIFO中断启用FIFO后,数据的读写就变成了对I2CDXR和I2CDRR寄存器的连续访问,硬件会自动管理FIFO指针。例如,在发送多字节时,你可以一次性向I2CDXR写入多个字节(直到FIFO满),硬件会按顺序发送。当发送FIFO空时,会产生中断,你可以在中断中继续填充数据。这特别适合与DMA配合进行大批量数据传输。
从轮询到中断,再到FIFO,这是一个逐步优化CPU占用率和程序效率的过程。对于简单的单字节读写,轮询足够;对于连续的数据流,中断+FIFO是更专业的选择。理解状态机是基础,掌握了它,你就能驾驭2803x I2C模块的所有高级功能。