1. 项目概述与核心价值
最近在做一个基于dsPIC33F的小型数据采集项目,需要存储一些校准参数和运行日志。硬件上已经预留了I2C总线,手头正好有几片AT24C02的EEPROM芯片。按理说,用dsPIC33F自带的硬件I2C模块是最省事的,但项目里那几个I2C引脚已经被其他功能占用了,重新画板子时间上又不允许。于是,一个很现实的问题摆在了面前:如何用普通的GPIO口,通过软件模拟I2C时序,来驱动这颗24XXX系列的EEPROM?
这其实就是“软件模拟I2C”(Software I2C或Bit-Banging I2C)的典型场景。对于dsPIC33F这类MCU来说,虽然它性能不错,但用软件模拟通信协议,尤其是像I2C这样有时序要求的协议,需要考虑的细节远比调用硬件库函数要多。你需要精确控制两根线(SDA和SCL)的高低电平变化,严格遵守起始、停止、应答等信号的时序,还要处理好总线仲裁、时钟拉伸等潜在问题。网上能找到的代码片段很多,但直接搬到dsPIC33F上,往往因为时钟频率、指令周期、端口操作速度的差异而无法工作,或者稳定性很差。
所以,我决定结合这次实战,把从GPIO初始化、时序微调、到读写函数封装、再到异常处理的全过程梳理出来。目标很明确:写出一份针对dsPIC33F平台优化过的、稳定可靠的24XXX系列EEPROM软件I2C驱动。这份指南不仅会给出可以直接编译运行的代码,更会重点解释每一个延时参数是怎么算出来的,为什么某些操作顺序不能颠倒,以及在调试过程中用逻辑分析仪抓波形时,应该重点关注哪些地方。无论你是正在遭遇类似的引脚冲突问题,还是想深入理解I2C协议的底层时序,这篇文章都能提供一条清晰的路径。
2. 核心思路与方案选型
2.1 为什么选择软件模拟I2C?
在dsPIC33F项目中使用软件模拟I2C,通常源于以下几个硬性约束或考量:
- 引脚资源冲突 :这是最常见的原因。dsPIC33F的硬件I2C模块引脚是固定的(例如I2C1通常在RC8/SDA1和RC9/SCL1)。当这些引脚被分配给更重要的功能(如关键的PWM输出、ADC输入或另一个通信接口)时,软件模拟就成了唯一的选择。
- 项目成本与硬件简化 :在某些超低成本设计中,可能选用的dsPIC33F具体型号不包含硬件I2C模块,或者为了节省几毛钱而选用无硬件I2C的版本。此时软件模拟是必选项。
- 调试与教学目的 :通过软件模拟,可以更直观、更深入地理解I2C协议的每一个比特、每一个时序阶段。这对于学习协议本质、调试复杂的I2C总线问题(如从设备无应答、波形畸变)非常有帮助。
- 多主设备支持 :虽然硬件I2C也支持多主模式,但其仲裁逻辑相对复杂且由硬件自动完成。软件模拟可以让你以更可控的方式实现多主逻辑(尽管更复杂),方便进行一些特殊的总线控制测试。
当然,软件模拟的缺点也很明显: 占用CPU资源 。MCU需要不断地执行指令来翻转GPIO和插入延时,在高速通信或MCU本身任务繁重时,可能会影响系统实时性。但对于读写24XXX这类EEPROM(标准模式100kHz,快速模式400kHz),dsPIC33F以几十MIPS的速度运行,软件模拟的负担是完全可以接受的。
2.2 24XXX系列EEPROM关键特性解析
在编写驱动前,必须吃透目标器件的数据手册。24XXX系列(如AT24C02/04/08/16等)是Microchip(原Atmel)的经典I2C EEPROM,虽然基本协议相同,但细节决定成败。
-
设备地址
:7位地址格式为
1010 A2 A1 A0 R/W。其中高4位1010是固定标识。A2, A1, A0 这三个地址位由芯片的硬件引脚电平决定。对于容量小于等于2Kbit(256字节)的芯片(如AT24C02),其内部地址空间只需一个8位地址即可寻址,因此器件地址中的A0/A1/A2完全由硬件引脚决定。对于更大容量的芯片,内部地址需要更多位(如16位),此时硬件地址引脚可能被部分用于构成内部地址的高位,这涉及到“字地址”的概念,是驱动兼容不同容量的关键。 -
写周期时间
:这是EEPROM最重要的参数之一。在向EEPROM发送一个字节或一页数据后,芯片内部需要时间(
t_WR,典型值5ms)将数据从缓存写入非易失性存储单元。在此期间,芯片不会应答I2C查询(即“忙状态”)。驱动必须妥善处理这个延时,通常采用“查询应答”或直接延时等待的方式。 - 页写与字节写 :24XXX支持页写操作,即一次性写入一页数据(不同容量页大小不同,常见为8、16、32字节)。这比分多次写入单个字节效率高得多。但页写操作有严格的地址对齐限制,不能跨页写入。驱动需要实现页写逻辑,并处理好地址边界检查。
- 时序要求 :数据手册会给出SCL时钟的最小高低电平时间、数据建立/保持时间等。软件模拟必须满足这些最小时序要求,否则通信会失败。
基于以上分析,我们的驱动设计目标确定为: 一个针对dsPIC33F GPIO特性优化的、支持标准模式(100kHz)的、具备字节读写和页写功能的、并能正确处理写周期等待的24XXX系列EEPROM软件I2C驱动。
3. 硬件连接与GPIO初始化
3.1 电路连接要点
软件模拟I2C对硬件连接的要求与硬件I2C基本一致,但因为我们完全控制GPIO,所以可以更灵活。
- 引脚选择 :选择任意两个通用GPIO口,一个作为SDA(数据线),一个作为SCL(时钟线)。建议选择同一端口组(如PORTB或PORTC)的引脚,这样在某些需要同时操作两条线的指令优化上会更方便。
- 上拉电阻 :I2C总线是开漏/开集输出,必须依赖上拉电阻将总线拉至高电平。 这是必须的! 典型阻值在2.2kΩ到10kΩ之间,具体取决于总线电容和通信速度。对于软件模拟和100kHz速率,4.7kΩ是一个稳妥的选择。将电阻一端接MCU的VDD(如3.3V),另一端分别接SDA和SCL引脚。
-
EEPROM地址引脚
:根据你的电路连接,确定A2, A1, A0的硬件电平(接VDD或GND),这将决定该EEPROM的7位I2C地址。例如,若A2=A1=A0=GND,则写地址为
0xA0,读地址为0xA1。
我的测试连接如下:
- dsPIC33F: RC2 -> SDA, RC3 -> SCL (通过4.7kΩ电阻上拉到3.3V)
- AT24C02: A2=A1=A0=GND, VCC=3.3V, GND=GND, WP(写保护)接GND(允许写入)。
3.2 GPIO方向与状态控制策略
软件模拟I2C的核心在于精确控制GPIO的输入/输出方向以及输出电平。I2C协议中,SDA线需要在主机输出数据和接收从机应答之间切换方向。
- 输出低电平 :将引脚配置为数字输出模式,并输出逻辑0。
- 输出高电平 :将引脚配置为数字输出模式,并输出逻辑1。 注意 :由于外部有上拉电阻,即使MCU输出高电平,电流路径也是从VDD通过上拉电阻流入MCU引脚,这通常是可以接受的,但有些MCU在输出高时是推挽输出,与上拉电阻形成“线与”关系。更规范的做法是,当需要释放总线(输出高)时,将引脚 配置为输入模式 ,这样外部上拉电阻自然会将总线拉高,同时也实现了开漏输出。这是软件模拟中一个关键技巧。
- 读取输入 :将引脚配置为数字输入模式,读取端口值。
因此,我们需要为SDA和SCL定义一组底层操作宏或函数:
// 假设使用RC2作为SDA,RC3作为SCL
#define I2C_SDA_DIR TRISCbits.TRISC2 // 1=input, 0=output
#define I2C_SDA_LAT LATCbits.LATC2 // 输出锁存
#define I2C_SDA_PORT PORTCbits.RC2 // 端口读取
#define I2C_SCL_DIR TRISCbits.TRISC3
#define I2C_SCL_LAT LATCbits.LATC3
#define I2C_SCL_PORT PORTCbits.RC3
// 宏定义:设置引脚为输出并输出低电平(驱动总线为低)
#define SDA_LOW() do { I2C_SDA_DIR = 0; I2C_SDA_LAT = 0; } while(0)
#define SCL_LOW() do { I2C_SCL_DIR = 0; I2C_SCL_LAT = 0; } while(0)
// 宏定义:释放总线(设置为输入,由上拉电阻拉高)
#define SDA_HIGH() do { I2C_SDA_DIR = 1; } while(0) // 注意:不操作LAT,方向改为输入即释放
#define SCL_HIGH() do { I2C_SCL_DIR = 1; } while(0)
// 宏定义:读取总线电平(前提是引脚已被设置为输入)
#define SDA_READ() (I2C_SDA_PORT)
#define SCL_READ() (I2C_SCL_PORT)
// 初始化函数:将总线置于空闲状态(均为高)
void I2C_Soft_Init(void) {
SDA_HIGH(); // 先释放SDA
SCL_HIGH(); // 再释放SCL
// 可选:添加一小段延时,确保总线稳定
__delay_us(5);
}
注意 :
SDA_HIGH()和SCL_HIGH()的实现是关键。这里采用将引脚设为输入的方式来释放总线,完美模拟了开漏输出。确保你的代码中没有在设置为输入后还去操作输出锁存器(LAT),否则可能引起内部冲突。
4. 时序生成与底层信号模拟
软件模拟I2C的稳定性,几乎完全取决于时序的精确性。我们需要根据dsPIC33F的指令周期和I2C标准模式的时序要求,计算出必要的延时。
4.1 时序参数计算
假设我们的dsPIC33F运行在40 MIPS(每秒4千万条指令)下,每条指令周期为25 ns。I2C标准模式(100kHz)的时钟周期为10us。我们需要满足数据手册中的几个关键时间参数(以AT24C02为例):
-
t_{HD,STA}: 起始条件保持时间,SCL为高时SDA从高到低的变化后,需要保持至少4.0us的低电平。 -
t_{LOW}: SCL低电平周期,至少4.7us。 -
t_{HIGH}: SCL高电平周期,至少4.0us。 -
t_{SU,DAT}: 数据建立时间,SDA数据在SCL上升沿前必须稳定至少100ns。 -
t_{HD,DAT}: 数据保持时间,SCL下降沿后数据必须保持至少0ns(通常我们留一些余量)。 -
t_{SU,STA}: 重复起始条件建立时间,在SCL为高时,SDA从低到高再到低的变化中,高电平至少保持4.7us。 -
t_{SU,STO}: 停止条件建立时间,在SCL为高时,SDA从低到高的变化前,低电平至少保持4.0us。
为了编程方便,我们定义几个基础延时函数,并使其满足最严格的要求:
// 简单延时函数,基于__delay_us或软件循环。这里以循环为例。
// 注意:__delay_us() 依赖于编译器且需要正确的时钟定义,在时序要求不苛刻时可用。
// 为了更精确的控制,我们使用基于指令周期的空循环。
#define FCY 40000000UL // 假设指令周期频率为40 MHz
#define US_DELAY(us) { unsigned int _dcnt; _dcnt = (unsigned int)((FCY/1000000.0)*(us)); while(_dcnt--); }
// 定义关键延时
#define I2C_DELAY_HALF_BIT() US_DELAY(5) // 半个比特周期,约5us (100kHz对应10us周期)
#define I2C_DELAY_FULL_BIT() US_DELAY(10) // 一个完整比特周期
#define I2C_DELAY_SETUP() US_DELAY(1) // 数据建立时间,留足余量
实操心得 :在实际调试中,我发现直接用
__delay_us()函数,其实际延时受编译器优化和中断影响可能不稳定。对于这种底层时序,使用经过校准的指令循环延时更可靠。你可以通过逻辑分析仪测量一个循环的实际时间,来反推并微调US_DELAY宏中的系数。例如,如果测量发现US_DELAY(5)实际只有4us,那就需要稍微增加循环次数。
4.2 起始、停止、应答信号生成
有了底层GPIO控制和延时函数,我们就可以构建I2C协议的基本信号了。
// 产生起始条件:SCL高电平期间,SDA从高变低
void I2C_Soft_Start(void) {
SDA_HIGH();
SCL_HIGH();
I2C_DELAY_HALF_BIT(); // 确保总线空闲稳定
SDA_LOW();
I2C_DELAY_HALF_BIT(); // 满足 t_HD,STA
SCL_LOW(); // 钳住SCL,准备发送数据
}
// 产生停止条件:SCL高电平期间,SDA从低变高
void I2C_Soft_Stop(void) {
SDA_LOW();
I2C_DELAY_HALF_BIT();
SCL_HIGH();
I2C_DELAY_HALF_BIT(); // 满足 t_SU,STO
SDA_HIGH();
I2C_DELAY_HALF_BIT(); // 确保停止条件完成,总线释放
}
// 发送应答信号(ACK):在SCL低电平期间,将SDA拉低,然后产生一个时钟脉冲
void I2C_Soft_Send_Ack(void) {
SDA_LOW(); // 准备ACK (低电平)
I2C_DELAY_SETUP();
SCL_HIGH(); // 产生时钟脉冲
I2C_DELAY_FULL_BIT();
SCL_LOW();
SDA_HIGH(); // 释放SDA线,为后续数据做准备
}
// 发送非应答信号(NACK):在SCL低电平期间,将SDA释放(高),然后产生一个时钟脉冲
void I2C_Soft_Send_Nack(void) {
SDA_HIGH(); // 准备NACK (高电平)
I2C_DELAY_SETUP();
SCL_HIGH(); // 产生时钟脉冲
I2C_DELAY_FULL_BIT();
SCL_LOW();
// SDA保持释放状态
}
// 等待并读取应答信号:返回0表示收到ACK,非0表示NACK或超时
unsigned char I2C_Soft_Wait_Ack(void) {
unsigned char ack;
unsigned short timeout = 1000; // 超时计数,防止死等
SDA_HIGH(); // 主机释放SDA线,准备读取从机应答
I2C_DELAY_SETUP();
SCL_HIGH(); // 产生时钟脉冲
// 在SCL高电平期间读取SDA
while (SCL_READ() == 0) { // 等待SCL被从机释放(时钟拉伸)
if (--timeout == 0) {
SCL_LOW();
return 1; // 超时,视为NACK
}
}
I2C_DELAY_HALF_BIT(); // 等待数据稳定
ack = SDA_READ(); // 读取应答位,0为ACK,1为NACK
SCL_LOW(); // 拉低SCL,结束应答周期
return ack;
}
注意事项 :
I2C_Soft_Wait_Ack()函数中的“时钟拉伸”处理非常重要。有些I2C从设备(尤其是EEPROM在内部写周期时)会通过拉低SCL来通知主机“我正忙,请等待”。我们的代码必须能检测并等待SCL被从机释放,否则会破坏通信。超时机制是为了防止从机故障导致程序死锁。
5. 字节读写与页读写功能实现
5.1 单字节发送与接收
这是构建所有高层读写操作的基础。
// 发送一个字节(MSB first),并返回从机的应答状态
unsigned char I2C_Soft_Write_Byte(unsigned char data) {
unsigned char i;
unsigned char ack;
for (i = 0; i < 8; i++) {
SCL_LOW(); // 确保在SCL低电平时改变SDA
I2C_DELAY_SETUP();
if (data & 0x80) { // 发送最高位
SDA_HIGH();
} else {
SDA_LOW();
}
data <<= 1; // 左移,准备发送下一位
I2C_DELAY_SETUP();
SCL_HIGH(); // 产生时钟上升沿,从机在此刻采样数据
I2C_DELAY_FULL_BIT();
// SCL将在循环开始处被拉低
}
SCL_LOW(); // 拉低SCL,为应答周期做准备
SDA_HIGH(); // 释放SDA,准备读取ACK
ack = I2C_Soft_Wait_Ack(); // 等待并读取应答
return ack; // 0=成功(ACK), 1=失败(NACK或超时)
}
// 读取一个字节(MSB first),并在读取后发送ACK或NACK
unsigned char I2C_Soft_Read_Byte(unsigned char ack_flag) {
unsigned char i;
unsigned char data = 0;
SDA_HIGH(); // 确保SDA为输入模式(已由宏定义)
for (i = 0; i < 8; i++) {
data <<= 1; // 先左移,最后一位在循环末尾赋值
SCL_LOW();
I2C_DELAY_HALF_BIT();
SCL_HIGH(); // 产生时钟上升沿
// 等待SCL变高,处理时钟拉伸
while (SCL_READ() == 0) { /* 等待 */ }
I2C_DELAY_HALF_BIT(); // 等待数据稳定
if (SDA_READ()) {
data |= 0x01; // 读取当前位
}
// SCL将在循环开始处被拉低
}
SCL_LOW(); // 拉低SCL,为发送应答做准备
if (ack_flag) {
I2C_Soft_Send_Ack(); // 发送ACK,表示还要继续读
} else {
I2C_Soft_Send_Nack(); // 发送NACK,表示这是最后一个字节
}
return data;
}
5.2 24XXX EEPROM的字节写与随机读
现在我们可以组合底层函数,实现针对24XXX EEPROM的特定操作。首先需要定义设备地址。
#define EEPROM_I2C_ADDR_W 0xA0 // 假设A2=A1=A0=0,写地址
#define EEPROM_I2C_ADDR_R 0xA1 // 读地址
#define EEPROM_PAGE_SIZE 16 // AT24C02的页大小为16字节
#define EEPROM_WRITE_DELAY 5 // 写周期等待时间,单位ms(保守起见用5ms)
// 向指定地址写入一个字节
unsigned char EEPROM_Write_Byte(unsigned short addr, unsigned char data) {
unsigned char ret;
I2C_Soft_Start();
// 发送设备地址(写)
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_W);
if (ret) { I2C_Soft_Stop(); return 1; } // 无应答,失败
// 发送内存地址(对于AT24C02,是8位地址)
// 注意:对于容量更大的芯片,这里需要发送两个字节的地址
ret = I2C_Soft_Write_Byte((unsigned char)addr);
if (ret) { I2C_Soft_Stop(); return 2; }
// 发送要写入的数据
ret = I2C_Soft_Write_Byte(data);
if (ret) { I2C_Soft_Stop(); return 3; }
I2C_Soft_Stop();
// **关键步骤:等待写周期完成**
// 方法1:延时等待(简单粗暴,但浪费CPU时间)
// __delay_ms(EEPROM_WRITE_DELAY);
// 方法2:查询应答(推荐)
// 不断发送起始条件和设备地址(写),直到收到ACK,表示内部写周期结束
__delay_us(100); // 先等待一小段时间,EEPROM不会立即响应
do {
I2C_Soft_Start();
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_W);
if (ret == 0) { // 收到ACK
I2C_Soft_Stop();
break;
}
I2C_Soft_Stop();
__delay_us(100); // 等待一段时间再重试
} while(1); // 可以加上超时机制防止死循环
return 0; // 成功
}
// 从指定地址读取一个字节
unsigned char EEPROM_Read_Byte(unsigned short addr) {
unsigned char data;
unsigned char ret;
// 首先发送“伪写”操作来指定要读取的地址
I2C_Soft_Start();
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_W);
if (ret) { I2C_Soft_Stop(); return 0xFF; }
ret = I2C_Soft_Write_Byte((unsigned char)addr);
if (ret) { I2C_Soft_Stop(); return 0xFF; }
// 发送重复起始条件,然后发送读地址
I2C_Soft_Start(); // 重复起始条件
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_R);
if (ret) { I2C_Soft_Stop(); return 0xFF; }
// 读取数据,并发送NACK表示读取结束
data = I2C_Soft_Read_Byte(0); // 参数0表示发送NACK
I2C_Soft_Stop();
return data;
}
5.3 页写与顺序读操作
页写可以大幅提升写入连续数据的效率。
// 页写函数:从指定地址开始写入多个字节(不能跨页)
// 参数:addr - 起始地址, pData - 数据指针, len - 写入长度
unsigned char EEPROM_Page_Write(unsigned short addr, unsigned char *pData, unsigned char len) {
unsigned char i, ret;
if (len == 0 || len > EEPROM_PAGE_SIZE) return 1; // 长度无效
// 检查是否跨页边界:(addr % EEPROM_PAGE_SIZE) + len > EEPROM_PAGE_SIZE
if ((addr & (EEPROM_PAGE_SIZE-1)) + len > EEPROM_PAGE_SIZE) return 2;
I2C_Soft_Start();
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_W);
if (ret) { I2C_Soft_Stop(); return 3; }
ret = I2C_Soft_Write_Byte((unsigned char)addr);
if (ret) { I2C_Soft_Stop(); return 4; }
for (i = 0; i < len; i++) {
ret = I2C_Soft_Write_Byte(pData[i]);
if (ret) { I2C_Soft_Stop(); return 5 + i; } // 写入失败
}
I2C_Soft_Stop();
// 等待页写周期完成(同样使用查询应答法)
__delay_us(100);
do {
I2C_Soft_Start();
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_W);
if (ret == 0) {
I2C_Soft_Stop();
break;
}
I2C_Soft_Stop();
__delay_us(100);
} while(1);
return 0; // 成功
}
// 顺序读函数:从指定地址开始连续读取多个字节
// 参数:addr - 起始地址, pBuf - 数据缓冲区, len - 读取长度
unsigned char EEPROM_Sequential_Read(unsigned short addr, unsigned char *pBuf, unsigned short len) {
unsigned short i;
unsigned char ret;
// 发送目标地址(伪写)
I2C_Soft_Start();
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_W);
if (ret) { I2C_Soft_Stop(); return 1; }
ret = I2C_Soft_Write_Byte((unsigned char)addr);
if (ret) { I2C_Soft_Stop(); return 2; }
// 重复起始,转为读操作
I2C_Soft_Start();
ret = I2C_Soft_Write_Byte(EEPROM_I2C_ADDR_R);
if (ret) { I2C_Soft_Stop(); return 3; }
// 连续读取字节
for (i = 0; i < len; i++) {
if (i == len - 1) {
pBuf[i] = I2C_Soft_Read_Byte(0); // 最后一个字节,发送NACK
} else {
pBuf[i] = I2C_Soft_Read_Byte(1); // 非最后一个字节,发送ACK
}
}
I2C_Soft_Stop();
return 0;
}
6. 调试技巧与常见问题排查
软件模拟I2C的调试,逻辑分析仪几乎是必备工具。它能让你直观地看到SDA和SCL线上的每一个比特。
6.1 使用逻辑分析仪抓取波形
将逻辑分析仪的通道0和通道1分别连接到SDA和SCL线,设置合适的采样率(如10MHz)。触发条件可以设置为SDA的下降沿(起始条件)。抓取一次完整的读写操作波形后,重点观察以下几点:
- 起始/停止条件 :起始条件是否满足“SCL高时SDA下降沿”?停止条件是否满足“SCL高时SDA上升沿”?波形是否干净,没有毛刺?
- 时钟频率 :测量SCL一个完整周期的时间,是否接近10us(100kHz)?高低电平时间是否满足数据手册要求?
-
数据建立/保持时间
:在SCL上升沿前后,SDA数据是否稳定?建立时间(
t_{SU,DAT})和保持时间(t_{HD,DAT})是否足够? - 应答位 :在第9个时钟脉冲期间,SDA是否被从机拉低(ACK)?如果从机没有拉低,说明地址错误、设备不存在或设备正忙。
- 写周期等待 :在发送停止条件后,立即尝试发送起始条件和设备地址,观察是否收到NACK。持续发送直到收到ACK,这个间隔就是写周期时间。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无应答(NACK) |
1. 设备地址错误。
2. 硬件连接问题(电源、上拉电阻、焊接)。 3. EEPROM写保护(WP)引脚被拉高。 4. 设备损坏。 |
1. 用逻辑分析仪确认发送的7位地址与硬件引脚匹配。
2. 检查电源电压,用万用表测量SDA/SCL线在空闲时是否为高电平(约VCC)。 3. 确认WP引脚已接地。 4. 更换芯片测试。 |
| 偶尔通信失败,数据错误 |
1. 时序不满足要求,处于临界状态。
2. 中断干扰了延时函数。 3. 总线电容过大,上升沿太慢。 |
1.
关键步骤
:用逻辑分析仪精确测量SCL高低电平时间、数据建立/保持时间。适当增加
I2C_DELAY
的数值。
2. 在关键的I2C底层函数(如
I2C_Soft_Write_Byte
)中临时关闭全局中断。
3. 减小上拉电阻阻值(如从10kΩ换为2.2kΩ),但注意会增加功耗。 |
| 页写后读取数据错误 |
1. 写入数据跨页了。
2. 页写后没有等待足够的写周期时间。 |
1. 在页写函数中加入地址边界检查,确保
(addr % PAGE_SIZE) + len <= PAGE_SIZE
。
2. 将写后等待(查询应答)的代码加上超时判断,并确保循环等待的延时
__delay_us(100)
不会因为编译器优化而被移除。
|
| 只能读写前256字节 | 对于容量大于256字节的EEPROM(如AT24C04/08/16),需要发送2字节的地址。 |
修改
EEPROM_Write_Byte
和
EEPROM_Read_Byte
函数中发送地址的部分。例如,对于16Kbit(2KB)的AT24C16,地址是11位,需要将设备地址的低3位(A2,A1,A0)与内存地址的高3位组合。具体需参考对应型号的数据手册。
|
| 逻辑分析仪显示波形畸形 |
1. 软件模拟的GPIO切换速度太快,超过了IO口的压摆率。
2. 探头接地不良引入噪声。 |
1. 在GPIO状态改变后(特别是
SDA_LOW()
到
SDA_HIGH()
切换时),增加一个极短的延时(如
__delay_us(0.1)
),让电平稳定。
2. 确保逻辑分析仪探头的地线与板子地线紧密连接。 |
6.3 dsPIC33F特定优化与避坑指南
-
端口操作速度
:dsPIC33F的端口写操作很快,但连续对同一端口组(如PORTC)的不同位进行“读-改-写”操作时,需要注意中间状态。例如,先读PORTC,修改某几位,再写回PORTC,如果期间其他位被硬件或中断改变,可能会丢失更改。对于SDA和SCL这种需要独立控制的线,如果它们在同一端口,使用
LATC寄存器进行输出,使用PORTC进行输入,可以避免一些读-改-写问题。我们的宏定义已经采用了这种方式。 -
编译器优化
:高优化等级可能会删除或重排你用来做延时的空循环。对于
US_DELAY这类宏,可以将其中的循环变量声明为volatile类型,或者将延时函数放在独立的、不被优化的源文件中。 -
中断干扰
:如果系统中断频繁,可能会打断一个字节的发送,导致时序错乱。一个简单的解决办法是在整个字节读写函数(如
I2C_Soft_Write_Byte)的开始和结束处,使用__builtin_disable_interrupts()和__builtin_enable_interrupts()(XC16编译器)临时关闭和打开全局中断。对于更复杂的多字节传输(如页写),需要权衡实时性和通信可靠性。 - 引脚配置 :确保用于模拟I2C的GPIO引脚已被配置为数字功能(如果复用模拟功能,需要关闭ADC),并且输出类型为推挽(默认即可)。输出驱动强度一般无需特别设置。
经过以上步骤,你应该能得到一个在dsPIC33F上稳定工作的软件I2C驱动,并能可靠地读写24XXX系列EEPROM。整个过程的核心在于对时序的精确把控和对异常情况的妥善处理。最后,将所有这些函数模块化,封装成独立的
.c
和
.h
文件,你的项目就可以方便地调用
EEPROM_Write_Byte()
和
EEPROM_Read_Byte()
这样的高级接口了。在实际项目中,我还会建议增加一个驱动自检函数,上电时对EEPROM进行简单的读写校验,确保存储系统从开始就是正常的。



被折叠的 条评论
为什么被折叠?



