
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模块引脚开发板引脚说明VCC3V3接3.3V电源GNDGND接地SCLGPIO51I2C时钟线SDAGPIO50I2C数据线注意AT24C02模块上的A0, A1, A2地址选择引脚我建议你都用跳线帽接到GND低电平这样设备地址就是0xA0/0xA1和我们的代码一致。2.2 使用SysConfig工具配置GPIOTI的C2000芯片现在推荐用SysConfig图形化工具来配置引脚和外围设备比直接啃寄存器手册方便太多了。咱们就用它来把GPIO50和GPIO51配置成普通的数字输出/输入引脚用来模拟I2C时序这种方式叫“软件I2C”或“GPIO模拟I2C”。打开工程在你的CCSCode Composer Studio工程里找到并双击c2000.syscfg这个文件。添加GPIO配置在SysConfig界面找到GPIO相关的配置部分点击“ADD”或“”号添加两个GPIO配置项。配置引脚参数分别配置这两个GPIO。第一个我们用来做SCL时钟线Pin: 选择GPIO51。Direction: 选择Output输出。虽然I2C的SCL只能是主机输出但我们先统一配成输出。Initial Value: 设为Low低电平初始状态。Name (optional): 可以起个易懂的名字比如Module_SCL。第二个用来做SDA数据线Pin: 选择GPIO50。Direction: 选择Output输出。注意SDA线是双向的我们会在代码里动态切换输入输出模式。Initial Value: 设为High高电平初始状态I2C总线空闲时为高。Name (optional): 比如Module_SDA。保存并生成代码按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电平BIT0/1 #define SDA(BIT) GPIO_writePin(Module_SDA, BIT) // 控制SDA电平输出模式时用 #define SDA_GET() GPIO_readPin(Module_SDA) // 读取SDA电平输入模式时用 // 根据硬件连接定义的器件地址A2A1A00 #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线是否被从机拉低ACK0。如果从机无应答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函数很容易地封装出页写和连续读的函数原理都是一样的注意页边界不要跨页写入就行。