嵌入式I2C通信实战:从寄存器配置到驱动实现与故障排查

📅 2026/7/23 7:46:16 👁️ 阅读次数 📝 编程学习
嵌入式I2C通信实战:从寄存器配置到驱动实现与故障排查

1. 项目概述:从寄存器位到通信总线

在嵌入式开发的底层世界里,我们写的每一行C代码,最终都要落到芯片内部那些由0和1组成的寄存器上。很多人觉得寄存器编程枯燥,无非是查手册、写数值。但在我看来,真正理解一个外设,尤其是像SCI(UART)和I2C这样的串行通信接口,其精髓恰恰在于对控制寄存器的精准把控。这不仅仅是“配置”,更像是在与硬件进行一场细致的对话,你告诉它如何工作,它反馈给你通信的状态。

我手头这份来自TI某款微控制器的技术手册片段,虽然零散,但信息密度极高。它详细描述了SCI模块的引脚I/O控制寄存器(SCIPIO2到SCIPIO8)以及I2C模块的概览。这些寄存器就是软件工程师操纵硬件引脚行为的“遥控器”。比如,一个简单的UART发送引脚(SCITX),是推挽输出还是开漏输出?上拉电阻是启用还是禁用?是上拉还是下拉?这些看似微小的选择,直接决定了信号的完整性、功耗,乃至整个通信网络的可靠性。特别是当SCI引脚被复用为通用GPIO,或者I2C总线需要连接多个设备时,对这些寄存器的理解就从“知道”变成了“必须精通”。

这次,我们就以这份手册为蓝本,抛开那些笼统的概念,深入到每一个寄存器位的具体含义、组合逻辑以及在实际通信协议(尤其是I2C)中的应用场景。我会结合自己调试过无数块板子的经验,告诉你哪些配置是“黄金法则”,哪些坑可以提前避开。无论你是在调通第一个I2C温湿度传感器,还是在设计一个多主机的复杂通信网络,对寄存器层的透彻理解,都是你从“功能实现”走向“稳定可靠”的必经之路。

2. SCI引脚控制寄存器深度解析

手册中给出了从SCIPIO2SCIPIO8共7个寄存器,它们共同管理着SCITX和SCIRX这两个引脚在GPIO模式下的所有行为。我们不要孤立地看每个寄存器,而要像搭积木一样,理解它们是如何协同工作的。

2.1 核心寄存器功能矩阵

首先,我们用一个表格来总览这组寄存器的分工,这比逐条阅读更清晰:

寄存器偏移地址核心功能位 (Bit 2, 1)主要作用
SCIPIO244hTX IN, RX IN只读引脚当前电平状态。
SCIPIO348hTX OUT, RX OUT设置引脚输出逻辑值(0或1)。
SCIPIO44ChTX SET, RX SET置位操作(写1输出高,写0无效果)。
SCIPIO550hTX CLR, RX CLR清零操作(写1输出低,写0无效果)。
SCIPIO654hTX PDR, RX PDR开漏输出使能控制。
SCIPIO758hTX PD, RX PD上下拉控制禁用
SCIPIO85ChTX PSL, RX PSL上下拉类型选择(上拉或下拉)。

注意:所有这些寄存器的Bit 31-3和Bit 0均为保留位(Reserved)。对保留位进行写操作是无效的,读取则返回0。这是一个良好的编程习惯:在初始化时,最好明确地将保留位写入0,以保证未来芯片版本兼容性。

2.2 输入/输出与置位/清零逻辑

SCIPIO2最简单,它是一个只读的状态寄存器。当你将引脚配置为输入,或者想在不改变输出状态的前提下读取引脚的实际电平时(例如读取一个按键或总线状态),就需要读取这里的TX INRX IN位。这相当于一个“数字万用表”功能。

SCIPIO3TX OUT/RX OUT位是直接设置输出值的。你写0,引脚就输出低电平;写1,就输出高电平(在推挽模式下)。但这里有个关键前提,手册多次强调:只有当TX FUNC/RX FUNC= 0(引脚配置为GPIO)且TX DIR/RX DIR= 1(引脚配置为输出方向)时,对此寄存器的写入才有效。这是配置GPIO输出的标准两步法:先设方向,再设数值。

那么SCIPIO4SCIPIO5存在的意义是什么?它们提供了更安全、更高效的位操作方式。SET寄存器是“写1有效,写0忽略”,CLR寄存器是“写1清零(输出低),写0忽略”。这在多任务或中断环境中特别有用。想象一下,你有一个引脚控制着LED,多个任务都可能要操作它。如果都直接写OUT寄存器,可能会发生“读-改-写”竞争条件:任务A读取OUT值,任务B也读取并修改了另一位,然后任务A写回,导致任务B的修改被覆盖。而使用SETCLR寄存器,你可以用一条原子指令(SET.bit = 1)将特定位置高,而不影响其他位,完全避免了竞争。在效率上,这也通常比“读取整个寄存器->修改某一位->写回整个寄存器”的方式更快。

2.3 开漏输出与上下拉配置

这是实现I2C等总线协议的关键。SCIPIO6TX PDR/RX PDR位控制开漏输出。当使能开漏(设为1)后,引脚输出逻辑1时,内部MOS管关闭,引脚呈现高阻态;输出逻辑0时,内部MOS管打开,将引脚拉低。这正是I2C总线实现“线与”(Wire-AND)功能的基础:总线上任何一个设备都可以将线拉低,但释放后要靠外部上拉电阻拉回高电平。手册明确指出,开漏功能仅在I/O模式下可用。

SCIPIO7SCIPIO8则管理内部上下拉电阻。PD位是“拉控制禁用”,设为1则完全关闭内部上下拉。PSL位是“拉选择”,仅在PD=0(拉控制启用)时有效,0代表下拉,1代表上拉。

这里有一个极其重要的细节,手册在27.8.3节用了一个注释(NOTE)特别强调:上下拉的禁用逻辑与引脚方向有关,与模式(I/O或功能模式)无关。如果引脚被配置为输出或发送方向,那么上下拉会自动禁用。如果引脚配置为输入或接收方向,则上下拉的启用或禁用才由PD位决定。这意味着,当你把SCITX配置为UART发送(输出)时,即使软件上PD=0,硬件也会自动断开内部上下拉,以避免影响信号。这是一个保护机制,但如果你不理解,在调试“为何我使能了上拉却没效果”时会非常困惑。

2.4 寄存器组合配置实战

理解了单个寄存器后,我们来看组合场景。手册27.8.5节的表格是精华,它总结了在不同条件下,输入缓冲器、输出缓冲器和上下拉控制的行为。我将其翻译成更易懂的配置流程:

假设我们要将SCITX引脚配置为一个标准的I2C SDA线(开漏输出,带外部上拉):

  1. 模式与方向:通过SCIPIO0/SCIPIO1(手册未给出细节,但通常存在)设置TX FUNC = 0(GPIO模式),TX DIR = 1(输出方向)。根据上述规则,一旦方向设为输出,内部上下拉自动无效。
  2. 开漏使能:设置SCIPIO6中的TX PDR = 1,启用开漏功能。
  3. 输出值:在空闲或发送逻辑1时,设置SCIPIO3TX OUT = 1,此时引脚为高阻态,由外部上拉电阻拉高。需要发送逻辑0时,设置TX OUT = 0,内部MOS管导通,将总线拉低。
  4. (可选)内部上拉:虽然输出时自动禁用,但作为好习惯,可以明确设置SCIPIO7TX PD = 1(禁用内部拉)以避免任何潜在冲突。SCIPIO8TX PSL此时无意义。

再假设我们将SCIRX配置为带内部上拉的输入引脚(例如接一个常开按键):

  1. 模式与方向:设置RX FUNC = 0RX DIR = 0(输入方向)。
  2. 上下拉控制:设置SCIPIO7RX PD = 0(启用拉控制),SCIPIO8RX PSL = 1(选择上拉)。
  3. 读取状态:通过读取SCIPIO2RX IN位来获取引脚电平。按键未按下时,读为1(被上拉拉高);按下时,读为0(被按键接地)。

3. I2C通信协议与模块架构

SCI的GPIO控制是基础,而I2C则是这种精细控制能力的一个典型应用场景。手册第28章概述了I2C模块,我们结合寄存器配置来理解其工作原理。

3.1 I2C核心特性与物理层

I2C是一个多主、半双工、串行、同步通信总线,仅需两根线:SDA(数据线)SCL(时钟线)。手册提到它兼容Philips I2C规范v2.1,支持标准模式(100kbps)和快速模式(400kbps)。所有I2C设备都必须采用开漏输出,并与正电源通过上拉电阻连接,实现“线与”逻辑。这就是为什么我们之前要深入研究开漏配置(PDR位)的原因。

物理连接上,如图28-1所示,所有设备的SDA和SCL分别并联在一起。任何一个主设备都可以发起通信。总线空闲时,两根线都被外部上拉电阻拉至高电平。

3.2 I2C模块内部时钟树

可靠的通信始于精准的时钟。手册图28-3和公式(63)-(65)描述了I2C模块的时钟生成,这是配置通信速率的核心。

时钟路径分三级:

  1. 输入时钟:通常是微控制器的外设总线时钟(VBUS_CLK)。
  2. 模块时钟:由可编程预分频器(I2CPSC寄存器)对输入时钟分频得到,频率必须在6.7MHz到13.3MHz之间。关键点I2CPSC只能在I2C模块复位(IRS=0)时配置,生效需要在配置后置IRS=1
  3. 主时钟(SCL):模块时钟再经过一个由I2CCKLI2CCKH控制的时钟分频器,产生最终在SCL引脚上出现的时钟信号。I2CCKL控制SCL低电平时间,I2CCKH控制高电平时间。

计算SCL频率的公式是:Fmaster = Fmodule / [(I2CCKL + d) + (I2CCKH + d)],其中d是一个与I2CPSC值相关的延迟因子(I2CPSC=0时d=7,=1时d=6,>1时d=5)。这个d因子体现了内部同步器的固有延迟。因此,实际通信速率会略低于理论计算值,在要求精确时序的应用中(如某些严格遵循I2C时序的传感器),必须留有余量或通过示波器校准。

3.3 数据格式、起始与停止

I2C通信以起始条件(S)开始,以停止条件(P)结束。如图28-4和28-5所示:

  • 起始条件:SCL为高时,SDA发生高到低跳变。
  • 停止条件:SCL为高时,SDA发生低到高跳变。
  • 数据有效性:SDA上的数据必须在SCL高电平期间保持稳定,只能在SCL低电平期间变化。

每个数据帧由若干字节组成。第一字节总是地址字节,可以是7位或10位格式。

  • 7位地址:如图28-7,首字节高7位是地址,最低位是R/W位(0=主写,1=主读)。后跟一个ACK位。
  • 10位地址:如图28-8,首字节是11110+地址高2位+R/W,次字节是地址低8位。需要两个ACK。
  • 重复起始条件:如图28-9,主设备可以在不释放总线(不发停止条件)的情况下,发送一个新的起始条件,以改变通信方向或寻址另一个从设备,这对于复合格式操作非常有用。

一个关键限制:手册28.1节NOTE明确指出,此I2C模块不支持高速模式(HS mode),也不支持10位地址模式下的“组合格式”(即每次发送首字节后必须紧跟次字节)。这在选型和协议设计时需要注意。

3.4 模块操作状态机与寄存器映射

图28-2的I2C模块框图揭示了其内部结构。核心部件包括:

  • 数据寄存器I2CDXR(数据发送寄存器)、I2CDRR(数据接收寄存器)、I2CXSR(发送移位寄存器)、I2CRSR(接收移位寄存器)。CPU或DMA与I2CDXR/I2CDRR交互,硬件自动完成与移位寄存器的数据搬运。
  • 控制与状态寄存器I2CMDR(模式寄存器)是大脑,控制主/从模式、起始/停止条件产生、地址格式等。I2CSR(状态寄存器)反映总线忙(BB)、仲裁丢失、地址识别等状态。
  • 地址寄存器I2COAR(自身地址寄存器,从模式用)、I2CSAR(从设备地址寄存器,主模式用)。
  • 时钟与控制寄存器I2CPSCI2CCKLI2CCKH用于时钟配置。I2CIMRI2CISR用于中断管理。
  • 引脚控制寄存器:类似于SCI的SCIPIO系列,I2C也有对应的I2CPFNCI2CPDIRI2CPDRI2CPDIS等寄存器,用于配置SDA和SCL引脚的功能(I2C或GPIO)、方向、开漏、上下拉等。其原理与SCI的GPIO控制完全相通。

模块的操作由一个状态机控制,根据I2CMDR的配置和总线事件,在空闲、地址发送、数据发送、数据接收、仲裁等状态间切换。理解这个状态机对于调试通信故障至关重要。

4. 从寄存器到代码:I2C主机驱动实现

理论说得再多,不如一行代码。我们以一个具体的场景为例:将微控制器配置为I2C主机,向一个7位地址的EEPROM(例如0x50)写入一个字节数据(0xAB)到地址0x0001。这里假设我们已经正确配置了系统时钟和引脚复用(将SDA和SCL引脚功能设置为I2C)。

4.1 初始化配置步骤

初始化是通信稳定的基石,必须严格按照顺序进行。

步骤1:模块软复位与时钟配置在修改关键配置前,先将模块置于复位状态。

// 假设 I2C_BASE 是I2C模块的基地址 #define I2C_MDR (*(volatile uint32_t *)(I2C_BASE + 0x00)) // 模式寄存器地址示例 // 1. 确保模块处于复位状态 (IRS = 0) I2C_MDR &= ~(1 << 15); // 清除IRS位 // 2. 配置预分频器 I2CPSC,生成符合要求的模块时钟 // 假设 VBUS_CLK = 60MHz,目标模块时钟 Fmodule ~= 12MHz // 根据公式(63): Fmodule = Finput_clk / (I2CPSC + 1) / 2 // 60MHz / (2+1) / 2 = 10MHz (在6.7-13.3MHz范围内) #define I2C_PSC (*(volatile uint32_t *)(I2C_BASE + 0x88)) I2C_PSC = 2; // 设置预分频值 // 3. 配置主时钟频率 (SCL) // 目标 SCL 频率为 100kHz (标准模式) // 根据公式(64)(65)及d值(I2CPSC=2, d=5),粗略计算: // Fmaster = Fmodule / [(I2CCKL+d) + (I2CCKH+d)] // 10MHz / [(ICCL+5) + (ICCH+5)] = 100kHz // 所以 (ICCL+5) + (ICCH+5) = 100 // 通常设置高低电平时间相等,即 ICCL = ICCH // 则 ICCL = ICCH = (100/2) - 5 = 45 #define I2C_CLKL (*(volatile uint32_t *)(I2C_BASE + 0x90)) #define I2C_CLKH (*(volatile uint32_t *)(I2C_BASE + 0x94)) I2C_CLKL = 45; I2C_CLKH = 45; // 4. 配置自身地址(主机模式下通常不重要,可设为任意值,但需使能) #define I2C_OAR (*(volatile uint32_t *)(I2C_BASE + 0x08)) I2C_OAR = 0x00; // 设置自身地址为0x00,并确保OAR寄存器配置正确 // 5. 退出复位状态,使能I2C模块 I2C_MDR |= (1 << 15); // 设置IRS位为1

实操心得I2CPSCI2CCKLI2CCKH的配置必须在IRS=0时进行。计算分频值时,务必用示波器测量最终的SCL频率进行验证,因为内部延迟(d值)可能导致实际频率偏慢。如果通信不稳定,尝试略微增大I2CCKLI2CCKH的值。

步骤2:引脚功能与GPIO属性配置虽然引脚复用可能在其他外设模块中设置,但I2C模块自身的引脚控制寄存器也需要关注,特别是开漏配置。

// 假设 I2C 引脚控制寄存器偏移量 #define I2C_PFNC (*(volatile uint32_t *)(I2C_BASE + 0x1C0)) // 功能选择 #define I2C_PDIR (*(volatile uint32_t *)(I2C_BASE + 0x1C4)) // 方向控制 #define I2C_PDR (*(volatile uint32_t *)(I2C_BASE + 0x1C8)) // 开漏使能 #define I2C_PDIS (*(volatile uint32_t *)(I2C_BASE + 0x1D0)) // 上下拉禁用 // 1. 将SDA和SCL引脚功能设置为I2C(假设对应位为0) // 具体位域需查手册,这里假设Bit1对应SDA,Bit0对应SCL I2C_PFNC |= (1 << 1) | (1 << 0); // 设置为I2C功能,非GPIO // 2. 方向通常由I2C模块自动管理,但确保初始为输入或高阻更安全 // I2C协议要求空闲���为高,由外部上拉拉高。开漏模式下,输出方向由硬件自动切换。 // 我们主要配置开漏和禁用内部上下拉。 // 3. 使能SDA和SCL引脚的开漏输出(这是I2C必须的) // 假设Bit1对应SDA_PDR,Bit0对应SCL_PDR I2C_PDR |= (1 << 1) | (1 << 0); // 使能开漏 // 4. 禁用内部上下拉电阻,完全依赖外部上拉 // 假设Bit1对应SDA_PDIS,Bit0对应SCL_PDIS I2C_PDIS |= (1 << 1) | (1 << 0); // 禁用内部上下拉

注意事项外部上拉电阻是必须的,典型值在2.2kΩ到10kΩ之间,取决于总线电容和通信速度。电阻太小会增加功耗,太大会导致上升沿过慢,通信不可靠。对于400kHz快速模式,通常使用2.2kΩ或更小的电阻。

4.2 主机发送流程与代码实现

现在开始实现向EEPROM写入一个字节的流程:起始条件 -> 发送设备地址+写位 -> 等待ACK -> 发送内存地址高字节 -> 等待ACK -> 发送内存地址低字节 -> 等待ACK -> 发送数据 -> 等待ACK -> 停止条件。

#define I2C_STR (*(volatile uint32_t *)(I2C_BASE + 0x04)) // 状态寄存器 #define I2C_DRR (*(volatile uint32_t *)(I2C_BASE + 0x0C)) // 数据接收寄存器 #define I2C_DXR (*(volatile uint32_t *)(I2C_BASE + 0x10)) // 数据发送寄存器 #define I2C_SAR (*(volatile uint32_t *)(I2C_BASE + 0x0A)) // 从地址寄存器 // 辅助宏:检查状态位 #define I2C_WAIT_FOR_BB_CLEAR() while(I2C_STR & (1 << 12)) // 等待总线空闲 #define I2C_WAIT_FOR_XRDY() while(!(I2C_STR & (1 << 4))) // 等待发送就绪 #define I2C_WAIT_FOR_ARDY() while(!(I2C_STR & (1 << 3))) // 等待地址就绪(非所有模块都有) #define I2C_CHECK_NACK() if(I2C_STR & (1 << 15)) { /* NACK处理 */ } uint8_t I2C_WriteByte(uint16_t slaveAddr, uint16_t memAddr, uint8_t data) { // 1. 等待总线空闲 I2C_WAIT_FOR_BB_CLEAR(); // 2. 配置为主发送模式,7位地址,准备产生起始条件 // I2CMDR 位定义示例: MST=主模式, STT=起始条件, STP=停止条件, TRX=发送模式, XA=0(7位地址) I2C_MDR = (1 << 15) | (1 << 14) | (1 << 11) | (1 << 10); // IRS | MST | TRX | STT // 3. 写入从设备地址(左移一位,最低位为R/W位。0表示写) I2C_SAR = slaveAddr << 1; // 例如 0x50 << 1 = 0xA0 I2C_WAIT_FOR_ARDY(); // 等待地址发送完成 I2C_CHECK_NACK(); // 检查是否有NACK,如果有则从设备未响应,应退出并报错 // 4. 发送内存地址高字节 I2C_DXR = (memAddr >> 8) & 0xFF; // 发送高8位地址 I2C_WAIT_FOR_XRDY(); I2C_CHECK_NACK(); // 5. 发送内存地址低字节 I2C_DXR = memAddr & 0xFF; // 发送低8位地址 I2C_WAIT_FOR_XRDY(); I2C_CHECK_NACK(); // 6. 发送数据字节 I2C_DXR = data; I2C_WAIT_FOR_XRDY(); I2C_CHECK_NACK(); // 7. 产生停止条件 I2C_MDR |= (1 << 9); // 设置STP位 // 等待STP位被硬件清除,表示停止条件已发出 while(I2C_MDR & (1 << 9)); return 0; // 成功 } // 调用示例 I2C_WriteByte(0x50, 0x0001, 0xAB);

避坑指南

  1. 状态检查顺序:在发送每个字节(包括地址)后,必须检查XRDY(发送缓冲区空)和可能的ARDY(地址就绪),更重要的是检查NACK位。如果从设备无应答(NACK),必须中止当前传输,发送停止条件并重置总线状态。
  2. STP位操作:设置STP位后,不要立即关闭模块或进行其他操作。必须等待STP位被硬件自动清除,这表示停止条件已在总线上产生完毕。过早操作可能导致停止条件未完整发出。
  3. 中断与轮询:上述代码使用轮询(Polling)方式,简单但会阻塞CPU。在实际产品中,更推荐使用中断或DMA方式。配置I2CIMR启用相应中断(如XRDYARDYNACK),在中断服务程序中处理状态和下一步操作。

5. 高级话题与故障排查实录

掌握了基本读写后,我们会遇到更复杂的情况和各种各样的故障。

5.1 多主机仲裁与时钟同步

当总线上有多个主设备时,仲裁机制防止了数据冲突。仲裁发生在SDA线上。每个主设备在发送数据的同时会监控SDA线状态。如果它发送了一个高电平(释放总线),但检测到SDA线为低(被另一个主设备拉低),那么它就失去了仲裁,必须立即切换到从接收模式,并停止驱动SCL。手册中的仲裁器(Arbitrator)和时钟同步器(Clock synchronizer)模块负责处理这些。对于开发者而言,这意味着你的代码必须能处理仲裁丢失(AL位被置位)。发生仲裁丢失后,通常需要重新初始化发送流程。

5.2 时钟延展与超时处理

某些低速从设备(如某些EEPROM或传感器)可能在处理数据时需要更多时间,它们会在接收到一个字节后,通过将SCL线拉低来强制主设备进入等待状态,这称为时钟延展。主设备的I2C模块应能检测到SCL被拉低并自动等待。但是,为了防止从设备故障导致总线死锁,主设备必须实现超时机制。可以在等待XRDYARDY的循环中加入计数器,超过一定时间后,强制产生停止条件,并重新初始化I2C模块。

#define I2C_TIMEOUT 10000 // 超时计数值,根据系统时钟调整 uint8_t I2C_WaitWithTimeout(volatile uint32_t *reg, uint32_t mask) { uint32_t timeout = I2C_TIMEOUT; while(!(*reg & mask)) { if(--timeout == 0) { // 超时处理:强制停止,复位总线 I2C_MDR |= (1 << 9); // 尝试发送停止条件 // 可能需要额外的总线恢复操作,如模拟时钟脉冲 return 1; // 返回超时错误 } } return 0; // 成功 }

5.3 常见故障排查表

以下是我在多年调试中总结的I2C问题排查清单:

现象可能原因排查步骤与解决方案
总线死锁,SCL或SDA一直为低1. 从设备故障未释放总线。
2. 主设备在仲裁丢失或异常后状态错乱。
3. 物理连接短路。
1.断电重启是最快方法。
2. 软件恢复:尝试发送多个时钟脉冲(将SCL配置为GPIO输出,手动产生9个以上时钟)直到SDA变高,然后发送一个停止条件(SDA低->高,SCL高)。
3. 检查硬件线路。
从设备无应答(NACK)1. 从设备地址错误。
2. 从设备未上电或损坏。
3. 总线电平问题(上拉电阻过大,上升沿太慢)。
4. 时序不满足从设备要求。
1. 用逻辑分析仪或示波器抓取波形,确认发送的地址是否正确(7位地址+1位R/W)。
2. 测量从设备电源和复位引脚。
3.测量SCL/SDA上升时间。对于400kHz,上升时间应小于300ns。减小上拉电阻(如从10kΩ换为2.2kΩ)或降低通信速率。
4. 检查I2CCKL/I2CCKH配置,确保高低电平时间满足从设备数据手册要求。
通信数据错误1. 电源噪声或地线干扰。
2. 总线电容过大,导致边沿畸变。
3. 主从设备时钟容差累积。
1. 增加电源去耦电容,确保地线连接良好且粗短。
2. 减少总线走线长度,移除不必要的连接器,使用更小的上拉电阻。
3. 在允许范围内,适当降低I2C时钟频率。
只能读写第一个字节,后续失败1. 从设备需要内部写周期时间(如EEPROM)。
2. 主设备未正确处理从设备的“忙”状态(时钟延展)。
3. 主设备发送停止条件过早。
1. 写操作后,等待足够时间(查从设备手册,通常1-10ms)再发起下一次通信。或轮询从设备的ACK。
2. 确保主设备I2C驱动支持时钟延展。
3. 确保在最后一个ACK后,再发送停止条件。
作为从机无法被寻址1. 自身地址寄存器(I2COAR)配置错误。
2. 模块未使能(IRS=0)。
3. 从机模式相关的中断或状态未正确配置。
1. 确认I2COAR已正确写入7位从机地址(通常需要左移一位)。
2. 确认I2CMDR中的IRS=1MST=0(从模式)。
3. 使能从机地址识别中断,并在中断中正确读取数据。

5.4 性能优化与可靠性设计

  • 中断与DMA:对于批量数据传输,务必使用中断或DMA。配置I2CDMACR寄存器启用DMA事件,将I2CDXR/I2CDRR与DMA通道关联,可以极大解放CPU,并减少因中断响应延迟导致的数据溢出(overrun)或欠载(underrun)错误。
  • 噪声滤波:I2C模块内部通常有噪声滤波器(见图28-2),可以过滤SCL和SDA线上的毛刺。在电气环境恶劣的场合(如电机附近),确保启用并合理设置滤波宽度。
  • 软件模拟I2C:当硬件I2C资源不足或遇到难以解决的兼容性问题时,可以将任意两个GPIO配置为开漏输出(使用我们前面详述的PDRPDIS等寄存器),用软件模拟时序。这提供了最大的灵活性,但会消耗CPU资源且速率较低。此时,对GPIO寄存器的快速置位/清零操作(使用SET/CLR寄存器)就显得尤为重要。
  • 电源与电平:确保总线上所有设备使用相同的参考地,并且VCCIO电平兼容。对于3.3V主设备与5V从设备通信,需使用电平转换器,而非简单的上拉电阻。