AT24C02 EEPROM I2C驱动移植与读写实战:基于TI C2000 TMS320F28P550开发板
AT24C02 EEPROM I2C驱动移植与读写实战:基于TI C2000 TMS320F28P550开发板
最近在做一个基于TI C2000 DSP的项目,需要保存一些校准参数和运行状态,断电后还不能丢失。用芯片内部的Flash吧,操作起来有点麻烦,而且擦写次数有限。这时候,外挂一颗EEPROM就成了一个非常靠谱的选择。AT24C02这个小芯片,价格便宜、容量够用(256字节)、接口简单(I2C),是很多嵌入式项目的“老朋友”了。
今天,我就手把手带你,把AT24C02的驱动移植到TI的TMS320F28P550这款DSP开发板上。咱们不光是调通代码,更重要的是理解I2C通信的底层时序,以及如何根据芯片手册来编写可靠的读写函数。整个过程从硬件连接到软件配置,再到代码编写和验证,我都会详细拆解。如果你也在用C2000系列芯片,或者对I2C设备驱动感兴趣,这篇教程应该能帮到你。
1. 认识我们的“存储小助手”:AT24C02
在动手接线写代码之前,咱们先花几分钟了解一下AT24C02到底是个啥,以及它怎么工作。这能帮你后面理解代码时,知道每一步是在干什么。
EEPROM,中文叫“电可擦可编程只读存储器”。这个名字听起来有点绕,其实核心就两点:一是掉电数据不丢失,像U盘一样;二是可以用电信号反复擦写,不像以前的ROM只能写一次。它经常用来保存一些需要修改但又不能丢的数据,比如设备的配置参数、用户的设定值或者运行日志。
我们用的AT24C02,是EEPROM家族里很常用的一款。它的“02”代表容量是2K位(bit),换算成字节就是256字节。别看容量不大,存几十个参数绰绰有余。它通过I2C总线和我们的主控芯片(比如TMS320F28P550)通信。I2C总线特别省引脚,只需要两根线(SCL时钟线和SDA数据线)就能挂一堆设备,非常适合连接各种传感器、存储器。
这里有几个关键参数,你接线和编程时得心里有数:
- 工作电压:1.8V到5.5V都行,兼容性很好,咱们开发板的3.3V供电完全没问题。
- 通信接口:I2C。
- 内存大小:256字节。
- 页大小:16字节。这是它一次连续写入的最大单位,超过这个长度地址会“翻卷”,覆盖之前写的数据,这个坑后面会重点讲。
- 设备地址:这是I2C设备寻址的关键。AT24C02的7位地址固定是
1010开头,后面三位(A2, A1, A0)可以通过芯片上的硬件引脚电平来设置(高或低)。这样,一条I2C总线上最多能挂8个(2的3次方)AT24C02。我们通常把模块的这三个引脚都接地,所以地址就是1010000。加上最后一位读写位(0写,1读),完整的8位写地址就是0xA0,读地址是0xA1。
2. 硬件连接与引脚配置
理论清楚了,咱们就来动手接线。这一步很简单,但一定要接对,不然后面调试全是白费功夫。
2.1 模块与开发板连线
我用的AT24C02模块和TI TMS320F28P550开发板(立创开发板)。你需要准备杜邦线,按照下面的对应关系连接:
| AT24C02模块引脚 | 开发板引脚 | 说明 |
|---|---|---|
| VCC | 3V3 | 接3.3V电源 |
| GND | GND | 接地 |
| SCL | GPIO51 | I2C时钟线 |
| SDA | GPIO50 | I2C数据线 |
注意:AT24C02模块上的A0, A1, A2地址选择引脚,我建议你都用跳线帽接到GND(低电平),这样设备地址就是
0xA0/0xA1,和我们的代码一致。
2.2 使用SysConfig工具配置GPIO
TI的C2000芯片现在推荐用SysConfig图形化工具来配置引脚和外围设备,比直接啃寄存器手册方便太多了。咱们就用它来把GPIO50和GPIO51配置成普通的数字输出/输入引脚,用来模拟I2C时序(这种方式叫“软件I2C”或“GPIO模拟I2C”)。
- 打开工程:在你的CCS(Code Composer Studio)工程里,找到并双击
c2000.syscfg这个文件。 - 添加GPIO配置:在SysConfig界面,找到GPIO相关的配置部分,点击“ADD”或“+”号,添加两个GPIO配置项。
- 配置引脚参数:分别配置这两个GPIO。
- 第一个,我们用来做SCL(时钟线):
- Pin: 选择
GPIO51。 - Direction: 选择
Output(输出)。虽然I2C的SCL只能是主机输出,但我们先统一配成输出。 - Initial Value: 设为
Low(低电平初始状态)。 - Name (optional): 可以起个易懂的名字,比如
Module_SCL。
- Pin: 选择
- 第二个,用来做SDA(数据线):
- Pin: 选择
GPIO50。 - Direction: 选择
Output(输出)。注意,SDA线是双向的,我们会在代码里动态切换输入输出模式。 - Initial Value: 设为
High(高电平初始状态,I2C总线空闲时为高)。 - Name (optional): 比如
Module_SDA。
- Pin: 选择
- 第一个,我们用来做SCL(时钟线):
- 保存并生成代码:按
Ctrl + S保存配置文件。然后按Ctrl + B编译一次工程。这时SysConfig会自动根据你的配置,在board.h等文件中生成对应的引脚宏定义。比如,你刚才起的Module_SCL和Module_SDA名字,现在就成了可以在代码里直接使用的标识符。
提示:编译时可能会有些警告,只要不是错误,通常可以忽略。关键是要确认
board.h文件里已经生成了我们定义的引脚宏。
3. 手把手编写驱动代码
硬件准备好了,接下来就是重头戏——写代码。我们会创建两个文件:bsp_at24c02.c(源文件)和bsp_at24c02.h(头文件)。我把它们放在工程里新建的module_driver文件夹里。
3.1 头文件定义 (bsp_at24c02.h)
头文件主要是宏定义和函数声明,干净利落。
#ifndef __BSP_AT24C02_H__ #define __BSP_AT24C02_H__ #include "tjx_init.h" // 这个头文件包含了board.h,所以我们的引脚宏在这里可用 // 定义SDA引脚的方向控制宏(非常关键!) #define SDA_IN() GPIO_setDirectionMode(Module_SDA, GPIO_DIR_MODE_IN) // 设置为输入模式 #define SDA_OUT() GPIO_setDirectionMode(Module_SDA, GPIO_DIR_MODE_OUT) // 设置为输出模式 // 定义SCL和SDA引脚的读写操作宏 #define SCL(BIT) GPIO_writePin(Module_SCL, BIT) // 控制SCL电平,BIT=0/1 #define SDA(BIT) GPIO_writePin(Module_SDA, BIT) // 控制SDA电平(输出模式时用) #define SDA_GET() GPIO_readPin(Module_SDA) // 读取SDA电平(输入模式时用) // 根据硬件连接定义的器件地址(A2=A1=A0=0) #define AT24C02_ADDRESS_READ 0xA1 // 读操作地址 #define AT24C02_ADDRESS_WRITE 0xA0 // 写操作地址 // 高层应用函数声明 void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Data); unsigned char AT24C02_ReadByte(unsigned char WordAddress); #endif代码解读:
SDA_IN()和SDA_OUT()是软件模拟I2C的精髓。因为SDA线既要输出数据(主机发送),又要输入数据(主机接收从机应答和数据),所以必须在发送和接收的瞬间动态切换GPIO的方向。- 引脚操作宏(
SCL,SDA,SDA_GET)让代码更简洁,底层调用了TI的驱动库函数。
3.2 底层I2C时序模拟 (bsp_at24c02.c)
这部分是驱动的基础,我们通过控制GPIO电平的高低和延时,来“画”出I2C通信的时序图。一定要理解每个时序的意义。
起始信号 (IIC_Start) 和停止信号 (IIC_Stop)
I2C通信总是以起始信号开始,以停止信号结束。
- 起始信号:当SCL为高电平时,SDA出现一个下降沿。
- 停止信号:当SCL为高电平时,SDA出现一个上升沿。
void IIC_Start(void) { SDA_OUT(); // 确保SDA为输出模式 SDA(1); // SDA拉高 delay_us(5); // 短暂延时 SCL(1); // SCL拉高 delay_us(5); SDA(0); // 在SCL高期间,SDA拉低,产生起始条件 delay_us(5); SCL(0); // 拉低SCL,准备后续数据传输 delay_us(5); } void IIC_Stop(void) { SDA_OUT(); SCL(0); // 确保SCL先拉低 SDA(0); // SDA拉低 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(1); // 在SCL高期间,SDA拉高,产生停止条件 delay_us(5); }发送一个字节 (Send_Byte) 和接收一个字节 (Read_Byte)
I2C的数据传输在SCL为低电平时变化,在SCL为高电平时保持稳定并被读取。
- 发送:从最高位(MSB)开始,依次将数据的每一位放到SDA线上,然后制造一个SCL脉冲(低->高->低),对方就在SCL高的时候读取这一位。
- 接收:主机先释放SDA线(设置为输入),然后在每个SCL高电平期间去读取SDA线的状态,拼接到接收变量里。
void Send_Byte(uint8_t dat) { int i = 0; SDA_OUT(); // 设置为输出模式 SCL(0); // 拉低时钟线开始 for(i = 0; i < 8; i++) // 循环8次,发送8个bit { // 取出最高位,右移7位得到0或1,然后设置到SDA引脚 SDA( (dat & 0x80) >> 7 ); delay_us(1); SCL(1); // 拉高SCL,从机在此刻采样数据位 delay_us(5); SCL(0); // 拉低SCL,为下一个数据位做准备 delay_us(5); dat <<= 1; // 数据左移,准备发送下一位 } } unsigned char Read_Byte(void) { unsigned char i, receive = 0; SDA_IN(); // 关键!设置为输入模式,释放SDA线,让从机控制 for(i = 0; i < 8; i++) { SCL(0); delay_us(5); SCL(1); // 拉高SCL,此时从机会将数据位放到SDA上 delay_us(5); receive <<= 1; // 左移,为接收新位腾出空间 if( SDA_GET() ) // 读取SDA引脚电平 { receive |= 1; // 如果为高,最低位置1 } delay_us(5); } SCL(0); return receive; }应答机制 (IIC_Send_Ack 与 I2C_WaitAck)
I2C每传输完一个字节(8位),接收方必须发送一个应答信号(ACK)。
- 主机等待从机应答:主机发送完地址或数据后,需要释放SDA线(设为输入),然后产生一个SCL脉冲。在这个脉冲的高电平期间,主机去检查SDA线是否被从机拉低(ACK=0)。如果从机无应答(SDA仍为高),可能地址错误或设备忙。
- 主机发送应答:主机接收完从机的一个字节数据后,需要在下一个时钟周期通过拉低SDA线来发送一个应答信号(ACK),告诉从机“我收到了,请发下一个”。如果不想再接收了,就发送非应答(NACK),然后发停止信号。
void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); SDA(0); // 先拉低,准备发送ACK delay_us(5); if(!ack) SDA(0); // 发送ACK (0) else SDA(1); // 发送NACK (1) SCL(1); // 产生一个时钟脉冲,从机在此刻读取应答位 delay_us(5); SCL(0); SDA(1); // 释放SDA线 } unsigned char I2C_WaitAck(void) { char ack = 0; unsigned char ack_flag = 10; // 超时计数 SCL(0); SDA(1); // 主机释放SDA线 SDA_IN(); // 设置为输入,等待从机拉低 delay_us(5); SCL(1); // 产生时钟脉冲 delay_us(5); // 等待SDA被拉低(ACK),同时做超时判断 while( (SDA_GET()==1) && ( ack_flag ) ) { ack_flag--; delay_us(5); } if( ack_flag <= 0 ) // 超时,无应答 { IIC_Stop(); // 发送停止信号 return 1; // 返回错误 } else { SCL(0); SDA_OUT(); // 恢复输出模式 } return 0; // 返回成功 }3.3 AT24C02应用层读写函数
底层时序函数搭好了,现在来写真正操作EEPROM的函数。根据AT24C02的数据手册,它的读写操作有固定的帧格式。
字节写函数 (AT24C02_WriteByte)
向指定地址写入一个字节。
- 发送起始信号。
- 发送器件写地址 (
0xA0)。 - 等待应答。
- 发送要写入的内存地址(0-255)。
- 等待应答。
- 发送要写入的数据字节。
- 等待应答。
- 发送停止信号。
void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Data) { IIC_Start(); Send_Byte(AT24C02_ADDRESS_WRITE); // 发送写地址 I2C_WaitAck(); Send_Byte(WordAddress); // 发送内存地址 I2C_WaitAck(); Send_Byte(Data); // 发送要存储的数据 I2C_WaitAck(); IIC_Stop(); // 停止信号触发芯片内部写周期 delay_ms(5); // 重要!等待内部写周期完成(典型值5ms) }注意:
IIC_Stop()后,AT24C02开始内部擦写操作,这段时间(最多5ms)它不会响应I2C总线。所以写操作后必须加延时,或者用查询方式等待它写完,否则紧接着的读写操作会失败。这是最容易忽略的坑!
随机读函数 (AT24C02_ReadByte)
从指定地址读取一个字节。这个过程稍微复杂点,叫做“随机读”,需要先“假装”写一下来告诉芯片我们要读哪个地址。
- 发送起始信号。
- 发送器件写地址 (
0xA0)。 - 等待应答。
- 发送要读取的内存地址。
- 等待应答。
- 再次发送起始信号(称为“重复起始条件”)。
- 发送器件读地址 (
0xA1)。 - 等待应答。
- 读取一个字节数据(此时主机不再发送ACK,而是发送NACK)。
- 发送停止信号。
unsigned char AT24C02_ReadByte(unsigned char WordAddress) { unsigned char Data; IIC_Start(); Send_Byte(AT24C02_ADDRESS_WRITE); // 第一步:发送写地址 I2C_WaitAck(); Send_Byte(WordAddress); // 第二步:发送要读的地址 I2C_WaitAck(); IIC_Start(); // 第三步:重新起始 Send_Byte(AT24C02_ADDRESS_READ); // 第四步:发送读地址 I2C_WaitAck(); Data = Read_Byte(); // 第五步:读取数据 IIC_Send_Ack(1); // 第六步:发送非应答(NACK),表示只读一个字节 IIC_Stop(); return Data; }4. 上机验证:读写测试
代码都写好了,最后一步就是烧录到板子上跑起来看看。我们在主函数里写个简单的测试程序。
void main(void) { // ... 芯片初始化代码(CCS自动生成)... Device_init(); Device_initGPIO(); // ... 其他初始化 ... lc_printf("\nAT24C02 Demo Start.....\r\n"); uint8_t read_back_data_1 = 0; uint8_t read_back_data_2 = 0; // 测试1:向地址0写入数据48 AT24C02_WriteByte(0, 48); delay_ms(10); // 等待写入完成 // 测试2:向地址8写入数据66 AT24C02_WriteByte(8, 66); delay_ms(10); // 测试3:从地址0读出数据 read_back_data_1 = AT24C02_ReadByte(0); // 测试4:从地址8读出数据 read_back_data_2 = AT24C02_ReadByte(8); // 通过串口打印结果 lc_printf("Read from Address 0: %d (should be 48)\r\n", read_back_data_1); lc_printf("Read from Address 8: %d (should be 66)\r\n", read_back_data_2); while(1) { // 主循环,可以加个LED闪烁表示程序在跑 // ... } }验证结果: 如果一切顺利,你通过串口助手应该能看到:
Read from Address 0: 48 (should be 48) Read from Address 8: 66 (should be 66)这就说明你的AT24C02驱动移植成功了!你可以尝试修改写入的地址和数据,甚至写一个循环去读写整个256字节的空间,看看是否都正常。
几个调试心得:
- 时序是关键:如果读写失败,首先检查
delay_us的延时是否足够。不同主频的MCU需要调整。可以用逻辑分析仪或者示波器抓一下SCL和SDA的波形,对照I2C时序图看起始、停止、数据位、应答位对不对。 - 地址别搞错:确认你的AT24C02模块A0,A1,A2的接法,和代码里的地址定义是否匹配。
- 写后等待:
AT24C02_WriteByte函数后一定要有足够延时(delay_ms(5)),这是芯片硬件要求的。 - 上拉电阻:I2C总线需要上拉电阻(通常4.7kΩ到10kΩ)。如果你的模块上没有集成,需要在SCL和SDA线上各接一个上拉到3.3V。很多开发板的I2C接口已经内置了上拉,用GPIO模拟时则需要自己外接。
好了,关于AT24C02在TI C2000上的驱动移植和基本读写操作,到这里就全部讲完了。掌握了这些,你就能在项目里用它来可靠地存储数据了。如果需要存储超过一个字节的数据块,你可以基于WriteByte和ReadByte函数,很容易地封装出页写和连续读的函数,原理都是一样的,注意页边界不要跨页写入就行。
