MSPM0 I2C模块深度解析:从协议原理到寄存器配置与实战避坑
1. 项目概述与I2C核心价值
在嵌入式开发领域,尤其是面对传感器、存储芯片、显示屏等外设时,如何高效、可靠地进行板级设备间通信,是每个工程师都要解决的基础问题。早年大家可能用过UART、SPI,但当你需要连接多个设备,又不想占用太多宝贵的GPIO引脚时,I2C总线协议的优势就凸显出来了。它仅凭两根线——一根数据线(SDA)和一根时钟线(SCL),就能构建起一个支持多主多从的通信网络,极大地简化了硬件设计和布线复杂度。今天,我们就以德州仪器(TI)的MSPM0 G系列微控制器为例,深入拆解其内置的I2C模块。这不仅仅是一个简单的“如何配置寄存器”的教程,我更想和你聊聊,在真实的项目里,如何理解I2C的“脾气”,如何利用MSPM0提供的丰富特性(比如独立的8字节FIFO、可编程的毛刺抑制、时钟低超时检测)来构建一个既稳定又高效的通信链路,以及那些数据手册里不会明说,但实际调试中一定会遇到的“坑”和应对技巧。无论你是刚接触MSPM0的新手,还是想优化现有I2C通信的老鸟,相信这篇从原理到寄存器、再到实战心得的解析,都能给你带来一些直接的参考。
2. I2C协议基础与MSPM0模块架构解析
2.1 I2C总线协议的精髓:简单背后的严谨
I2C协议的精妙之处在于其极简的硬件需求与严谨的时序逻辑。两根线都是开漏(Open-Drain)输出,这意味着它们只能主动拉低到逻辑‘0’,而靠外部的上拉电阻回到逻辑‘1’。这种设计天然支持了“线与”(Wire-AND)功能,是实现多主设备仲裁和时钟同步的物理基础。
一次完整的I2C通信以起始条件(START)开始,以停止条件(STOP)结束。起始条件是SCL为高时,SDA线上一个由高到低的跳变;停止条件则是SCL为高时,SDA线上一个由低到高的跳变。在这两个条件之间,总线被认为是“忙”的。每个传输的字节(8位数据)后,都必须跟一个应答(ACK)时钟周期。在这个周期,发送方会释放SDA线,接收方则需将SDA线拉低以示应答。若接收方不应答(保持SDA为高),则发送方会认为传输失败,通常以停止条件或重复起始条件结束本次传输。
地址帧是通信的“敲门砖”。MSPM0的I2C模块支持最常用的7位地址模式。在起始条件后,主控制器会先发送一个7位的从设备地址,紧接着的第8位是读写方向位(R/W):0表示主设备要向从设备写入数据(控制器发送模式),1表示主设备要从从设备读取数据(控制器接收模式)。从设备如果识别到自己的地址,则会在第9个时钟周期发出ACK。
注意:很多初学者容易混淆“读”和“写”的方向。这里的“读/写”是从主控制器的视角定义的。“写”意味着主设备发送数据到从设备;“读”意味着主设备从从设备接收数据。务必在配置寄存器时明确这一点。
2.2 MSPM0 I2C模块功能框图与核心特性
MSPM0的I2C模块不是一个简单的比特流发生器,而是一个高度集成、功能完整的通信引擎。从功能框图看,它清晰地分为控制器(Controller)核心和目标(Target)核心两大部分。这意味着同一个物理模块,既可以作为总线上的主设备发起通信,也可以作为从设备响应其他主设备的呼叫。这种设计非常灵活,例如,你的设备既可以主动读取传感器数据(控制器模式),也可以响应上位机的查询命令(目标模式)。
其核心特性决定了它的能力边界和应用场景:
- 多模式与多速率支持:完整支持控制器发送、控制器接收、目标发送、目标接收四种操作模式。通信速率覆盖从标准模式(Sm,最高100 kbps)到快速模式(Fm,最高400 kbps),再到快速模式Plus(Fm+,最高1 Mbps),足以应对绝大多数外设的速度需求。
- 独立的8字节FIFO:这是提升效率的关键。发送和接收各有独立的8字节FIFO缓冲区。这意味着你可以在一次中断服务程序中批量处理多个字节的数据,而不是每收发一个字节就产生一次中断,极大地降低了CPU中断负载,为实现更高性能的轮询或DMA传输奠定了基础。
- 双目标地址:模块可以响应两个独立的7位从设备地址。这在一些复杂的系统中非常有用,例如,你的设备可以同时以“设备A”和“设备B”两个逻辑身份存在于总线上,响应不同的命令集。
- 硬件级增强功能:
- 毛刺抑制(Glitch Suppression):总线暴露在复杂的PCB环境中,难免会受到噪声干扰。模块提供了模拟和数字两级毛刺滤波器,可以滤除指定宽度的干扰脉冲(默认模拟滤波50ns),确保信号稳定。
- 时钟低超时(Clock Low Timeout):这是一个针对总线挂死的保护机制。如果某个从设备异常拉低SCL线超过预设时间,模块会触发超时中断,让主设备有机会执行总线恢复操作,避免整个通信总线瘫痪。这对于实现SMBus/PMBus协议兼容性至关重要。
- 仲裁与时钟同步:支持多主竞争总线时的仲裁逻辑,以及多个主设备时钟的同步机制,确保了多主系统的稳定运行。
- DMA与中断支持:收发FIFO均可触发DMA请求,实现数据搬移的“零CPU开销”。同时,丰富的中断源(传输完成、FIFO阈值、仲裁丢失、起始/停止条件检测等)为事件驱动型编程提供了极大便利。
理解这个架构,你就明白了MSPM0的I2C模块不是一个需要你手动“bit-banging”的简单外设,而是一个配备了智能“协处理器”的通信引擎。我们的配置工作,本质上是在告诉这个引擎:以什么速度运行(时钟配置)、如何响应(目标地址)、数据来了放哪里/怎么取(FIFO与中断),以及遇到异常怎么办(超时、仲裁)。
3. 核心寄存器详解与配置实战
数据手册里的寄存器列表常常让人望而生畏,但只要我们抓住主线,按功能分组理解,就会清晰很多。MSPM0的I2C寄存器主要分为几大类:时钟控制、控制器操作、目标操作、FIFO控制、中断状态以及一些特殊功能控制。下面我们挑最核心、最常用的几个寄存器,结合具体配置场景来深入讲解。
3.1 时钟配置寄存器:通信速率的基石
I2C通信的“心跳”由SCL时钟决定,而SCL的频率又源于微控制器的系统时钟。MSPM0通过I2Cx.CLKSEL、I2Cx.CLKDIV和I2Cx.MTPR三个寄存器协同工作,来生成精确的I2C时钟。
1. 功能时钟源选择 (I2Cx.CLKSEL): 这个寄存器决定I2C模块的“工作时钟”(I2C_CLK)从哪里来。通常有两个选择:BUSSCLK(当前总线时钟)或MFCLK(主功能时钟)。在大多数应用中,为了简化时钟树,我们直接选择BUSSCLK。你需要根据你的系统主频和I2C目标速率,确保I2C_CLK满足最低要求(标准模式≥2MHz,快速模式≥8MHz,快速模式Plus≥20MHz)。
2. 功能时钟分频 (I2Cx.CLKDIV): 在选定了时钟源后,你可以通过这个寄存器对其进行1到8的分频,以得到最终的I2C_CLK。这给了你更精细的调整能力。例如,系统主频80MHz,直接作为I2C_CLK可能太高,你可以先分频到40MHz或20MHz,再通过MTPR计算波特率。
3. 波特率发生器 (I2Cx.MTPR): 这是设置SCL频率最关键的寄存器。其核心是TPR(Timer Period)位域。SCL频率的计算公式为:I2C_FREQ = I2C_CLK / ((1 + TPR) * (SCL_LP + SCL_HP))其中,SCL_LP(SCL低电平时间)固定为6个I2C_CLK周期,SCL_HP(SCL高电平时间)固定为4个I2C_CLK周期。因此,公式简化为:I2C_FREQ = I2C_CLK / ((1 + TPR) * 10)进而推导出:TPR = (I2C_CLK / (I2C_FREQ * 10)) - 1
实战配置示例: 假设你的系统BUSSCLK为32MHz,你想配置I2C为快速模式(400kHz)。
- 选择时钟源:
I2Cx.CLKSEL = BUSSCLK(假设值为0)。 - 暂不进行额外分频:
I2Cx.CLKDIV = DIV1(分频系数为1,I2C_CLK仍为32MHz)。 - 计算
TPR:TPR = (32,000,000 / (400,000 * 10)) - 1 = (32,000,000 / 4,000,000) - 1 = 8 - 1 = 7。 - 设置寄存器:
I2Cx.MTPR = 0x07。
实操心得:数据手册中的表25-1给出了常见时钟下的推荐
TPR值,可以作为快速参考。但强烈建议你理解公式并自己计算一遍,因为你的系统时钟可能并非标准值。计算出的TPR必须是整数,如果不是,则需要调整I2C_CLK(通过CLKDIV)或接受一个最接近的速率。实际速率与目标速率的微小偏差(通常在±2%内)I2C总线是可以容忍的。
3.2 控制器模式核心寄存器:发起通信的指挥官
当MSPM0作为主设备时,主要操作以下几个寄存器:
1. 控制器从机地址寄存器 (I2Cx.MSA):
SADDR(位域): 写入你要通信的从设备的7位地址。DIR(位): 设置传输方向。0= 控制器发送(主设备写),1= 控制器接收(主设备读)。这个位必须在每次传输开始前,根据本次操作的类型正确设置。
2. 控制器控制寄存器 (I2Cx.MCTR): 这是发起和控制传输的“开关”。
RUN(位): 使能控制器状态机。通常与START位一起置位来发起传输。START(位): 生成起始(START)或重复起始(Repeated START)条件。置1表示开始一次传输。STOP(位): 生成停止(STOP)条件。在一次传输的最后一个数据字节后,通过置位此位来结束传输。ACK(位): 控制是否在接收数据后发送应答。在控制器接收模式下,通常除了最后一个字节,前面所有字节都需要发送ACK(ACK=1),最后一个字节发送NACK(ACK=0)以告知从设备停止发送。BURSTRUN(位): 用于连续传输(Burst Transfer)。当置位时,控制器会在当前传输(由MBLEN定义长度)结束后自动开始下一个,而无需软件重复配置START。这在连续读写大量数据时非常高效。
3. 控制器数据寄存器 (I2Cx.MTXDATA/I2Cx.MRXDATA): 这是与FIFO交互的接口。向MTXDATA写入数据,数据会被压入发送FIFO;从MRXDATA读取数据,会从接收FIFO中弹出数据。重要:直接读写这两个寄存器操作的是FIFO,而非移位寄存器。
4. 控制器FIFO控制与状态寄存器 (I2Cx.MFIFOCTL,I2Cx.MFIFOSR):
MFIFOCTL: 设置FIFO中断触发阈值(TXTRIG,RXTRIG),控制FIFO清空(TXFLUSH,RXFLUSH)。MFIFOSR: 查看FIFO当前状态,如发送FIFO中还有多少数据(TXNUM),接收FIFO中有多少数据(RXNUM),以及FIFO空/满标志。
一次典型的控制器发送流程(查询方式):
// 1. 初始化I2C模块(时钟、GPIO等)... // 2. 准备发送数据到地址0x50的从设备 I2C0->MSA.SADDR = 0x50; // 设置从机地址 I2C0->MSA.DIR = 0; // 方向:控制器发送(写) // 3. 将数据写入发送FIFO (假设发送3个字节) I2C0->MTXDATA = data1; I2C0->MTXDATA = data2; I2C0->MTXDATA = data3; // 4. 设置本次传输字节数(MBLEN在MCR寄存器中,需提前配置,例如为3) // 5. 发起传输:生成START,并在传输结束后生成STOP I2C0->MCTR = (1 << MCTR_START_Pos) | (1 << MCTR_STOP_Pos) | (1 << MCTR_RUN_Pos); // 6. 等待传输完成(查询BUSY位或中断) while (I2C0->MSR.BUSY) { // 等待... } // 7. 检查状态寄存器(MSR)是否有错误(如NACK, ARBLOST) if (I2C0->MSR.ERROR) { // 错误处理... }3.3 目标模式核心寄存器:响应呼叫的从机
当MSPM0作为从设备时,配置相对简单,更多是“被动响应”。
1. 目标自身地址寄存器 (I2Cx.SOAR,I2Cx.SOAR2):
SOAR.OAR: 设置主地址。当总线上出现与此地址匹配的呼叫时,模块会应答。SOAR2.OAR2: 设置第二个地址(如果启用)。SOAR2.OAR2EN: 使能双地址匹配。置1后,模块会同时响应OAR和OAR2两个地址。
2. 目标控制寄存器 (I2Cx.SCTR):
GCEN: 使能广播呼叫(General Call,地址0x00)响应。
3. 目标应答控制寄存器 (I2Cx.SACKCTL):
ACKOEN: 手动应答使能。通常我们使用自动应答(ACKOEN=0),让硬件在FIFO未满时自动回复ACK。但在某些需要逐字节检查数据的场景,可以设置为手动模式(ACKOEN=1),然后通过ACKOVAL位来软件控制每个字节后的应答是ACK还是NACK。
目标模式的工作流程:
- 配置好自身地址(
SOAR)和可能需要的选项(如广播呼叫使能)。 - 使能目标模式(通过
I2Cx.PWREN等使能位)。 - 当主设备发起呼叫并地址匹配时,硬件会自动应答,并根据R/W位进入发送或接收模式。
- 如果是目标接收模式(主设备写数据过来),数据会自动存入接收FIFO(
SRXDATA),并可能触发接收中断。 - 如果是目标发送模式(主设备读数据),你需要提前将待发送数据写入发送FIFO(
STXDATA)。当主设备发起读请求时,硬件会自动从FIFO中取出数据发送。
3.4 中断与状态管理寄存器
高效的程序离不开中断。MSPM0的I2C中断系统层次清晰:
- 原始中断状态寄存器 (
I2Cx.RIS):记录了所有已发生但未被屏蔽的中断事件标志位。例如,TXEMP(发送FIFO空)、RXFF(接收FIFO满)、STOP(检测到停止条件)、START(检测到起始条件)、ARBLOST(仲裁丢失)等。 - 中断屏蔽寄存器 (
I2Cx.IMASK):用于使能或禁用特定的中断源。只有RIS中对应位为1且IMASK中对应位也为1时,才会产生CPU中断。 - 中断清除寄存器 (
I2Cx.ICLR):向某位写1,可以清除RIS中对应的中断标志位。这是清除中断状态的正确方式,通常在中断服务程序(ISR)中执行。
中断服务程序设计要点:
void I2C0_IRQHandler(void) { uint32_t ris_status = I2C0->RIS; // 读取原始中断状态 if (ris_status & I2C_RIS_TXEMP) { // 发送FIFO空中断:可以填充新的数据到MTXDATA // ... 填充数据 ... I2C0->ICLR.TXEMP = 1; // 清除中断标志 } if (ris_status & I2C_RIS_RXFF) { // 接收FIFO满(或达到阈值)中断:从MRXDATA读取数据 // ... 读取数据 ... I2C0->ICLR.RXFF = 1; // 清除中断标志 } if (ris_status & I2C_RIS_ARBLOST) { // 仲裁丢失:通常需要重置FIFO,等待总线空闲后重试 I2C0->MFIFOCTL.TXFLUSH = 1; I2C0->MFIFOCTL.RXFLUSH = 1; // ... 其他恢复操作 ... I2C0->ICLR.ARBLOST = 1; } // ... 处理其他中断 ... }4. 高级功能与实战避坑指南
掌握了基础配置,我们来看看那些能让你的I2C通信更稳健、更高效的高级功能,以及我踩过的一些“坑”。
4.1 FIFO的智慧使用:提升效率的关键
8字节的FIFO不是摆设,用好了能大幅提升性能。关键在于设置合理的中断触发阈值(TXTRIG/RXTRIG)。
- 发送场景:假设你要发送20字节数据。如果你设置
TXTRIG=1(FIFO空时触发中断),那么每发完1个字节就会进一次中断,CPU频繁被打断,效率极低。更好的策略是设置TXTRIG=7(当FIFO中数据少于等于7个时触发),在初始化时先写入8个字节,然后启动传输。当发送完1个字节,FIFO剩下7个(达到阈值)时触发中断,你在ISR中再填充8个字节进去。这样,20字节数据只需要产生3次中断(8+8+4)。 - 接收场景:类似地,设置
RXTRIG=7(当FIFO中数据达到或超过7个时触发)。这样你可以一次中断读取多个字节,减少了中断次数。
避坑指南:在清空(Flush)FIFO时,务必确保I2C模块处于空闲(IDLE)状态(检查
MSR.IDLE位)。在繁忙状态下刷新FIFO可能导致数据错乱或总线异常。安全的做法是:先禁用相关FIFO中断,执行Flush操作,然后等待Flush完成(查询相应状态位),最后再重新使能中断。
4.2 时钟低超时:总线挂死的守护者
这是I2C总线的一个经典故障场景:某个从设备(比如一个卡死的传感器)异常地将SCL线持续拉低,导致整个总线瘫痪,所有通信中断。MSPM0的时钟低超时功能就是应对此问题的。
配置与原理: 超时时间由I2CTIMEOUT_CTL.TCNTLA寄存器配置。它是一个12位计数器的高8位(低4位固定为0)。超时时间的计算基于BUSSCLK和TPR值:超时时间 = (TCNTLA值 << 4) * (1 / BUSSCLK) * (1 + TPR) * 12
示例:BUSSCLK=20MHz,TPR=19(对应~100kHz SCL),设置TCNTLA=0xDA(218)。
- 计数器值 = 0xDA0 = 3488。
- 一个超时周期 = (1/20MHz) * (1+19) * 12 = 12µs。
- 总超时时间 = 3488 * 12µs ≈ 41.86ms。
这意味着,如果SCL线被持续拉低超过41.86ms,I2Cx.RIS.TIMEOUTA标志位会被置位,如果中断使能,则会触发中断。
中断服务程序中的恢复操作:
- 读取
I2Cx.MBMON寄存器,查看SDA和SCL线的实际电平状态,辅助判断故障类型。 - 必须刷新TX和RX FIFO(
MFIFOCTL.TXFLUSH=1,MFIFOCTL.RXFLUSH=1)。 - 根据情况,可以尝试软件模拟产生几个SCL时钟脉冲(通过临时配置GPIO),尝试“踢”一下卡死的从设备,使其释放总线。如果不行,可能需要复位整个I2C外设或那个故障的从设备。
- 清除超时中断标志 (
ICLR.TIMEOUTA=1)。
4.3 毛刺抑制与时钟拉伸:应对复杂环境
- 毛刺抑制:在工业环境或长距离通信中,总线容易引入噪声。MSPM0的模拟滤波器(默认使能,50ns)能滤除大部分窄脉冲干扰。如果环境特别恶劣,可以额外使能数字滤波器(
GFCTL.DGFSEL),通过增加若干I2C_CLK周期的延迟来进一步稳定信号。注意:数字滤波器会增加信号延迟,在高速模式(如1Mbps)下需谨慎评估是否启用。 - 时钟拉伸:这是I2C协议允许从设备降低通信速率的一种机制。从设备可以通过拉低SCL来暂停通信,直到它准备好继续。在MSPM0作为控制器时,如果确信总线上所有从设备都不支持时钟拉伸,可以禁用此功能(
MCR.CLKSTRETCH=0)以获得最高通信速率。否则,应保持使能。在目标模式下,时钟拉伸是自动使用的,当目标TX FIFO空或RX FIFO满时,硬件会自动拉低SCL,等待软件处理。
4.4 多主模式与仲裁
当多个主设备(比如两个MSPM0)共享一条I2C总线时,就需要多主模式。配置很简单:设置MCR.MMST=1。关键在于理解仲裁过程:当两个主设备同时发起起始条件时,它们会继续发送地址和数据,直到某个时刻,一个主设备发送‘1’(释放SDA为高),而另一个发送‘0’(拉低SDA),发送‘0’的设备赢得总线,发送‘1’的设备检测到仲裁丢失(MSR.ARBLOST置位),并自动转换为目标模式,监听总线。
仲裁丢失处理:
- 立即刷新TX FIFO,因为已发送的数据可能无效。
- 清除并屏蔽TX FIFO空中断,防止错误的数据发送请求。
- 等待总线空闲(
MSR.BUSBSY为0)。 - 重新填充TX FIFO,取消中断屏蔽,准备下一次传输尝试。
5. 从零开始:一个完整的I2C驱动实现框架
理论说了这么多,我们最后用一个完整的、基于中断和FIFO的控制器模式驱动框架来收尾,并附上关键步骤的注释。
// i2c_driver.c #include "mspm0.h" #include "i2c_driver.h" // 定义I2C实例和全局状态/缓冲区 #define I2C_INST I2C0 #define I2C_TX_FIFO_SIZE 8 #define I2C_RX_FIFO_SIZE 8 volatile i2c_state_t i2c_state = I2C_STATE_IDLE; uint8_t i2c_tx_buffer[256]; uint8_t i2c_rx_buffer[256]; volatile uint16_t i2c_tx_index = 0, i2c_tx_len = 0; volatile uint16_t i2c_rx_index = 0, i2c_rx_len = 0; void I2C_Init(uint32_t bus_clk_freq, uint32_t i2c_freq) { // 1. 使能I2C模块时钟 (依赖于具体型号的时钟树配置) // ... (例如: CLK->PERIPH_EN |= CLK_PERIPH_EN_I2C0_MASK) // 2. 配置GPIO引脚为I2C功能 (开漏输出,上拉) // ... 配置SDA和SCL对应的GPIO为复用功能,并使能内部上拉电阻 // 3. 复位I2C模块 (可选,确保干净的状态) I2C_INST->PWREN.ENABLE = 0; // 先关闭 // ... 短暂延时 I2C_INST->PWREN.ENABLE = 1; // 重新使能 // 4. 配置时钟 I2C_INST->CLKSEL = I2C_CLKSEL_BUSSCLK; // 选择总线时钟作为源 I2C_INST->CLKDIV = I2C_CLKDIV_DIV1; // 不分频 // 计算并设置TPR uint32_t tpr = (bus_clk_freq / (i2c_freq * 10)) - 1; if (tpr > 0xFF) tpr = 0xFF; // 确保在寄存器范围内 I2C_INST->MTPR = tpr; // 5. 配置FIFO阈值 (例如:发送阈值7,接收阈值7) I2C_INST->MFIFOCTL.TXTRIG = 7; I2C_INST->MFIFOCTL.RXTRIG = 7; // 6. 配置中断 // 使能TX FIFO空、RX FIFO满、传输完成、错误中断 I2C_INST->IMASK.TXEMP = 1; I2C_INST->IMASK.RXFF = 1; I2C_INST->IMASK.MTXDONE = 1; I2C_INST->IMASK.MRXDONE = 1; I2C_INST->IMASK.NACK = 1; I2C_INST->IMASK.ARBLOST = 1; // 使能NVIC中的I2C中断 NVIC_EnableIRQ(I2C0_IRQn); // 7. 使能毛刺抑制 (使用默认模拟滤波即可) // I2C_INST->GFCTL.AGFEN = 1; // 默认已使能 // 8. 可选:配置时钟低超时 (例如 40ms) // uint32_t timeout_cnt = (40000 * bus_clk_freq) / (1000000 * (1+tpr) * 12); // I2C_INST->TIMEOUT_CTL.TCNTLA = (timeout_cnt >> 4) & 0xFF; // I2C_INST->IMASK.TIMEOUTA = 1; } // 非阻塞式发送函数 i2c_error_t I2C_MasterWrite(uint8_t slave_addr, uint8_t *data, uint16_t len) { if (i2c_state != I2C_STATE_IDLE) { return I2C_ERR_BUSY; } if (len == 0) { return I2C_ERR_OK; } // 保存发送参数到全局变量 memcpy(i2c_tx_buffer, data, len); i2c_tx_len = len; i2c_tx_index = 0; i2c_state = I2C_STATE_TX_BUSY; // 配置从机地址和方向 I2C_INST->MSA.SADDR = slave_addr; I2C_INST->MSA.DIR = 0; // 控制器发送 // 设置本次传输的字节数 (MBLEN在MCR寄存器中) I2C_INST->MCR.MBLEN = (len > 255) ? 255 : len; // 注意MBLEN可能有限制 // 先填充第一批数据到FIFO (最多8个) uint8_t bytes_to_fill = (len > I2C_TX_FIFO_SIZE) ? I2C_TX_FIFO_SIZE : len; for (uint8_t i = 0; i < bytes_to_fill; i++) { I2C_INST->MTXDATA = i2c_tx_buffer[i]; i2c_tx_index++; } // 启动传输,生成START,传输完成后生成STOP I2C_INST->MCTR = (1 << MCTR_START_Pos) | (1 << MCTR_STOP_Pos) | (1 << MCTR_RUN_Pos); return I2C_ERR_OK; } // I2C中断服务程序 void I2C0_IRQHandler(void) { uint32_t ris = I2C_INST->RIS; // 处理发送FIFO空中断 if (ris & I2C_RIS_TXEMP) { if (i2c_state == I2C_STATE_TX_BUSY) { // 还有数据要发送 while ((I2C_INST->MFIFOSR.TXNUM < I2C_TX_FIFO_SIZE) && (i2c_tx_index < i2c_tx_len)) { I2C_INST->MTXDATA = i2c_tx_buffer[i2c_tx_index++]; } // 如果所有数据都已填入FIFO,可以提前关闭TX FIFO空中断以减少中断次数 if (i2c_tx_index >= i2c_tx_len) { I2C_INST->IMASK.TXEMP = 0; } } I2C_INST->ICLR.TXEMP = 1; } // 处理接收FIFO满中断 if (ris & I2C_RIS_RXFF) { if (i2c_state == I2C_STATE_RX_BUSY) { // 从FIFO中读取数据 while (I2C_INST->MFIFOSR.RXNUM > 0 && i2c_rx_index < i2c_rx_len) { i2c_rx_buffer[i2c_rx_index++] = I2C_INST->MRXDATA; } } I2C_INST->ICLR.RXFF = 1; } // 处理控制器发送完成中断 if (ris & I2C_RIS_MTXDONE) { I2C_INST->ICLR.MTXDONE = 1; if (i2c_state == I2C_STATE_TX_BUSY) { i2c_state = I2C_STATE_IDLE; // 可以在这里调用用户回调函数,通知发送完成 } } // 处理控制器接收完成中断 if (ris & I2C_RIS_MRXDONE) { I2C_INST->ICLR.MRXDONE = 1; if (i2c_state == I2C_STATE_RX_BUSY) { // 最后读取FIFO中剩余的数据 while (I2C_INST->MFIFOSR.RXNUM > 0 && i2c_rx_index < i2c_rx_len) { i2c_rx_buffer[i2c_rx_index++] = I2C_INST->MRXDATA; } i2c_state = I2C_STATE_IDLE; // 调用用户回调函数,通知接收完成 } } // 处理NACK错误 if (ris & I2C_RIS_NACK) { I2C_INST->ICLR.NACK = 1; i2c_state = I2C_STATE_ERROR; // 错误处理:刷新FIFO,重置状态等 I2C_RecoverFromError(); } // 处理仲裁丢失错误 if (ris & I2C_RIS_ARBLOST) { I2C_INST->ICLR.ARBLOST = 1; i2c_state = I2C_STATE_ERROR; I2C_RecoverFromError(); } // 处理时钟低超时 if (ris & I2C_RIS_TIMEOUTA) { I2C_INST->ICLR.TIMEOUTA = 1; i2c_state = I2C_STATE_ERROR; I2C_RecoverFromTimeout(); } } void I2C_RecoverFromError(void) { // 1. 停止当前任何传输 I2C_INST->MCTR.RUN = 0; // 2. 刷新FIFO I2C_INST->MFIFOCTL.TXFLUSH = 1; I2C_INST->MFIFOCTL.RXFLUSH = 1; // 3. 等待刷新完成和总线空闲 (简单延时或查询状态) // ... // 4. 重置状态变量 i2c_tx_index = i2c_tx_len = 0; i2c_rx_index = i2c_rx_len = 0; i2c_state = I2C_STATE_IDLE; // 5. 可选:重新初始化I2C模块 // I2C_Init(...); }这个框架实现了基于中断的非阻塞传输,利用FIFO减少了中断频率,并包含了基本的错误处理。在实际项目中,你还需要根据具体需求添加超时管理、重试机制、以及更完善的用户回调接口。
调试I2C时,一台逻辑分析仪是必不可少的。通过抓取SDA和SCL的波形,你可以清晰地看到起始、停止、地址、数据、ACK/NACK位,直观地定位是协议问题、时序问题还是从设备响应问题。MSPM0丰富的状态寄存器是你的第二双眼睛,遇到问题多查看MSR、RIS、MBMON等寄存器的值,它们往往能告诉你故障的根源。
