ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DSP2812读写EEPROM驱动详解:基于AT24C的I2C实现与掉电保存

DSP2812读写EEPROM驱动详解:基于AT24C的I2C实现与掉电保存

简介:面向TMS320F2812(DSP2812)嵌入式开发者的EEPROM操作完整工程源码,重点演示在CCS3.3集成开发环境下,通过SPI或I²C接口访问外部EEPROM的实现方式。内容覆盖DSP2812外设初始化、SPI模式选择(CPOL/CPHA)与I²C地址寻址、读写擦除流程、写保护机制、返回状态检测以及数据完整性校验,便于使用者理解串行EEPROM驱动的完整框架。工程压缩包共49个文件、约449KB,包含DSP281x系列头文件(18个)、C源文件(7个)、汇编启动文件、库文件、out可执行文件、map映射文件和CMD链接脚本,其中代码与头文件为主体,能够支撑工程重建、烧写与在线调试。资源在CSDN已有407人学习,适合刚接触DSP2812外设编程的初学者,也适合需要快速移植EEPROM驱动模块的工程师参考;借助完整外设支持库,还可同时学习时钟、中断、GPIO等基础配置,真正把底层外设与EEPROM应用串起来,便于二次开发。

1. 从DSP2812裸机读写EEPROM说起:这份资源到底解决什么问题

做DSP2812开发的人,迟早会撞上“掉电丢数据”这堵墙。程序里算出来的校准系数、用户设置的运行参数、故障历史记录,一断电全清零,每次上电都得重新标定,现场调试简直磨人。最常见的出路就是外挂一颗EEPROM,把要保的数据在掉电前写进去。理论上这事儿不难,但真到自己画板子、写驱动时,坑一个接一个:I2C时序不对读回全0xFF、页写入跨边界丢字节、写周期没等够数据直接没落盘。这份基于DSP2812的EEPROM操作程序,就是把“初始化外部存储、按地址写入、按地址读出、整页刷写”这一整套流程打包好的参考实现,拿来改改引脚和器件地址就能用。适合手里有DSP2812开发板、正在做参数存储或掉电保存功能的人,也适合刚接触DSP + I2C外设、想找一份能抄的驱动模板的入门者。

2. 存储映射与硬件连接:先把DSP2812和EEPROM对上号

2.1 EEPROM选型与I2C地址:为什么多数人直接用AT24C系列

DSP2812这颗芯片本身不带EEPROM,也不带硬件I2C外设,所以操作EEPROM只有两条路:一是用GPIO软件模拟I2C,二是外接带并口的存储芯片。实际项目里鲜少有人为了一颗存储芯片去占用DSP的并行总线,绝大多数方案都是I2C接口的EEPROM,其中AT24C系列是绝对的主流。这个系列容量从2Kbit到256Kbit都有,工作电压覆盖1.8V到5.5V,和DSP2812的3.3V IO电平直接对接,不用加电平转换,省事。

选型时有几个硬指标要看清。AT24C02是2Kbit也就是256字节,适合存几个校准参数;AT24C64是64Kbit也就是8K字节,能塞下故障记录表或波形曲线。地址方面,AT24C系列的7位设备地址是固定的1010加上A2、A1、A0三个引脚的电平组合。比如A2A1A0全接地,写地址就是0xA0,读地址是0xA1。板上如果只挂一颗芯片,建议把地址引脚全部接地,省得软件里还要动态切地址。这个资源里的程序默认就是按AT24C128来写的,7位地址0xA0/0xA1,如果你的板上用的是AT24C02系列,容量小于等于256字节,页大小、写周期都要跟着调。

2.2 GPIO模拟I2C的引脚分配与上拉电阻

DSP2812没有I2C控制器,所有时序全靠GPIO翻转。资源里默认用两个普通IO引脚来做SCL和SDA,通常选GPIOB组的引脚,因为它们靠近芯片边缘,布板走线短。需要特别注意的是I2C总线必须接上拉电阻,SCL和SDA各接一颗4.7kΩ的上拉电阻到3.3V,电阻值根据总线长度和通信速率调整。

注意:上拉电阻不是可选项,是硬性要求。不接上拉,SDA线拉不上去,通信必然失败。

GPIO配置要按开漏输出来处理。DSP2812的GPIO模块本身没有真正的开漏模式,但可以通过方向寄存器模拟:做输出时只拉低,释放时把方向寄存器切回输入模式,这样外部上拉电阻就能把电平拉高。这个资源里的驱动初始化函数已经把这一步封装好了,你在移植时只需要确认引脚编号对应的是你自己的硬件连接。

2.3 读写时序的宏观理解:起始、停止、应答与数据帧

在贴代码之前,先把I2C的帧结构在大脑里过一遍。一次完整的字节写操作包含五个阶段:起始信号、发送设备地址加写标志、发送寄存器地址、发送要写入的数据、停止信号。其中每一步都要等从设备回一个ACK应答位,没等到ACK基本可以判定从设备不在线或者地址写错了。

读操作稍微绕一点。如果是随机读,需要先发一个“伪写”序列把目标地址写进去,然后重新发起始信号,再发设备地址加读标志,之后连续读字节。顺序读则是在随机读的基础上,读完一个字节后继续发时钟,EEPROM内部的地址指针会自动累加,连续输出后续地址的数据。资源里分别封装了单字节读、顺序读、页写这三种接口,实际使用中顺序读和页写是效率最高的两种。

3. 驱动代码拆解:初始化、单字节读写与页写入实现

3.1 宏定义与私有变量:把器件参数和引脚配置隔离出来

项目里驱动代码的头部定义集中在两个头文件中,一个管硬件引脚,一个管器件参数。先说引脚相关的部分。

// EEPROM_Driver.h #define EEPROM_SCL_PORT GPIOB #define EEPROM_SCL_PIN GPIO_PIN_4 #define EEPROM_SDA_PORT GPIOB #define EEPROM_SDA_PIN GPIO_PIN_5 #define EEPROM_SCL_HIGH() GpioDataRegs.GPBDAT.all |= 0x0010 #define EEPROM_SCL_LOW() GpioDataRegs.GPBDAT.all &= 0xFFEF #define EEPROM_SDA_INPUT_MODE() GpioMuxRegs.GPBDIR.all &= 0xFFDF #define EEPROM_SDA_OUTPUT_MODE() GpioMuxRegs.GPBDIR.all |= 0x0020

这段代码的作用是把SCL和SDA抽象成一组宏,后续驱动函数里只调用这些宏,不直接操作寄存器。这样如果换板子、换引脚,只需要改这几行宏定义,驱动逻辑一概不动。GPBDAT和GPBDIR都是DSP2812的GPIOB组寄存器,GPBDAT管电平数据,GPBDIR管方向。SCL只做输出,所以不必切方向;SDA需要双向切换,所以单独留了输入输出两个模式宏。

器件参数部分定义了AT24C128的关键时序参数:

#define EEPROM_DEV_ADDR_W 0xA0 #define EEPROM_DEV_ADDR_R 0xA1 #define EEPROM_PAGE_SIZE 64 #define EEPROM_WRITE_CYCLE_MS 10 #define EEPROM_MAX_ADDR 0x3FFF

AT24C128的页大小是64字节,最大地址0x3FFF,对应16K字节容量。写周期定义为10ms,虽然数据手册上典型值是5ms,但考虑到电压波动和温度影响,留出一倍裕量更稳妥。

3.2 底层时序函数:I2C起始、停止与应答检测的实现

这三段是整个驱动的地基。起始和停止的时序在I2C协议里有严格定义:起始是SCL高电平期间SDA产生下降沿,停止是SCL高电平期间SDA产生上升沿。代码实现时直接按这个状态机来写:

static void I2C_Start(void) { EEPROM_SDA_OUTPUT_MODE(); // SDA切到输出模式 EEPROM_SDA_HIGH(); EEPROM_SCL_HIGH(); DELAY_US(5); // 保持建立时间 EEPROM_SDA_LOW(); // SCL高期间SDA拉低 -> 起始信号 DELAY_US(5); EEPROM_SCL_LOW(); // 拉低SCL准备传数据 }

DELAY_US是DSP2812专用延时函数,需要自己实现或借用官方库的DSP28x_Delay。起始信号的关键在于SDA的下降沿必须发生在SCL为高时,所以代码顺序是SCL先拉高再拉低SDA。停止信号则是SDA上升沿在SCL高时发生,逻辑刚好相反。

ACK检测是最容易出问题的地方。EEPROM在接收到正确字节后,会在第九个时钟周期把SDA拉低作为应答。检测代码如下:

static unsigned char I2C_WaitAck(void) { unsigned char timeout = 0; unsigned char ack = 1; EEPROM_SDA_INPUT_MODE(); // SDA切输入,准备读从设备电平 EEPROM_SCL_HIGH(); // 第九个时钟,从设备把SDA拉低表示ACK DELAY_US(5); while(EEPROM_SDA_READ()) // 读GPIO电平 { timeout++; if(timeout > 200) { ack = 0; // 超时判定无应答 break; } DELAY_US(1); } EEPROM_SCL_LOW(); // 结束第九个时钟 EEPROM_SDA_OUTPUT_MODE(); // SDA切回输出 return ack; }

无应答的后果是致命的,所以我在这段加了超时计数器。实际调试中发现,有时候EEPROM上电瞬间还没准备好,前几百毫秒会一直不回ACK,软件里做重试机制比单纯加大超时时间更管用。

3.3 单字节写入与随机读:最常用的两个接口

单字节写是最基础的接口,也最适合用来验证硬件连接是否正常。代码里把地址分成高字节和低字节分别发送,因为AT24C128内部是16位地址空间。

unsigned char EEPROM_WriteByte(unsigned int addr, unsigned char data) { unsigned char ret = 0; I2C_Start(); ret += I2C_SendByte(EEPROM_DEV_ADDR_W); // 发送设备地址 + 写标志 ret += I2C_SendByte((addr >> 8) & 0xFF); // 地址高字节 ret += I2C_SendByte(addr & 0xFF); // 地址低字节 ret += I2C_SendByte(data); // 目标数据 I2C_Stop(); DELAY_MS(EEPROM_WRITE_CYCLE_MS); // 等内部写周期完成 if(ret == 0) return 1; // 成功 else return 0; // 失败 }

I2C_SendByte内部会在每个字节发送完后调用I2C_WaitAck,收到ACK返回0,无ACK返回1。返回值累加的意义在于,哪一步没得到应答,ret就不为零,方便定位是设备地址不对还是总线接触不良。写周期延时是必须的,EEPROM在接收到数据后内部要擦写,这个过程里不响应任何外部命令,延时不够的话紧接着读出来的数据是错的。

随机读接口和写入流程不一样,它要先发一个伪写操作把地址指针定位,然后重新走一遍起始信号再切换成读模式:

unsigned char EEPROM_ReadByte(unsigned int addr, unsigned char *data) { unsigned char ret = 0; I2C_Start(); ret += I2C_SendByte(EEPROM_DEV_ADDR_W); // 伪写:定位地址 ret += I2C_SendByte((addr >> 8) & 0xFF); ret += I2C_SendByte(addr & 0xFF); I2C_Start(); // 重新产生起始信号 ret += I2C_SendByte(EEPROM_DEV_ADDR_R); // 设备地址 + 读标志 *data = I2C_RecvByte(); // 读一个字节 I2C_NoAck(); // 最后一字节回非应答 I2C_Stop(); return (ret == 0) ? 1 : 0; }

这里有个容易看迷糊的点:为什么地址定位之后不能直接读,非得重新发一次起始信号?因为上一次总线上是写模式,从设备的I2C状态机在收到写地址后一直等在接收数据的状态,必须用起始信号打断它,让它重新解析接下来收到的是读地址。I2C_RecvByte返回前,主机要在第九个时钟不发应答,否则从设备会以为还要继续传。这个NoAck信号是读多字节时区分结束的关键。

3.4 页写入与顺序读:批量操作的正确姿势

单字节写简单可靠,但效率太低。AT24C128内部有SRAM缓冲,一次最多可以写入64字节,只有最后一个字节的写周期需要等待。页写入就是利用这个缓冲的批量操作:

unsigned char EEPROM_WritePage(unsigned int addr, unsigned char *buf, unsigned int len) { unsigned int i; unsigned char ret = 0; if(len > EEPROM_PAGE_SIZE) len = EEPROM_PAGE_SIZE; I2C_Start(); ret += I2C_SendByte(EEPROM_DEV_ADDR_W); ret += I2C_SendByte((addr >> 8) & 0xFF); ret += I2C_SendByte(addr & 0xFF); for(i = 0; i < len; i++) ret += I2C_SendByte(buf[i]); I2C_Stop(); DELAY_MS(EEPROM_WRITE_CYCLE_MS); return (ret == 0) ? 1 : 0; }

页写入的典型坑是跨页问题。AT24C128内部地址指针在页边界不会自动回卷,比如你从地址0x0030开始写40字节,前面32字节落在64字节页内,超过页边界后剩下的8字节会写到下一页的开头,整片数据直接错位。这个资源里特意在头文件注释里提醒,多字节写入必须由调用方保证地址和长度不跨64字节边界。我自己封装上层函数时加了一道防线,把超过页边界的数据拆分两次写,虽然耗时增加,但数据绝对安全。

顺序读接口则简单得多,连续读时主机每收一个字节都要回ACK,直到最后一个字节回NoAck:

unsigned char EEPROM_ReadSeq(unsigned int addr, unsigned char *buf, unsigned int len) { unsigned int i; unsigned char ret = 0; I2C_Start(); ret += I2C_SendByte(EEPROM_DEV_ADDR_W); // 伪写 ret += I2C_SendByte((addr >> 8) & 0xFF); ret += I2C_SendByte(addr & 0xFF); I2C_Start(); ret += I2C_SendByte(EEPROM_DEV_ADDR_R); for(i = 0; i < len; i++) { buf[i] = I2C_RecvByte(); if(i < len - 1) I2C_Ack(); // 前N-1字节回ACK } I2C_NoAck(); // 最后一字节回NoAck I2C_Stop(); return (ret == 0) ? 1 : 0; }

注意ACK的顺序不能乱。读多字节时,主机在收到每一字节的第九个时钟要拉低SDA表示“继续传”,但收到最后一个字节时必须把SDA放开,让上拉电阻拉高,表示“结束”。这个信号一旦发错,要么多读一个字节,要么从设备停在原地等下一个时钟,总线直接卡死。

4. 避坑指南:I2C通信中五个典型故障与定位方法

4.1 现象:读回数据全是0xFF

原因很简单,EEPROM没有响应,SDA线上无数据,读出来的就是上拉电平,也就是高电平0xFF。这种故障十有八九是设备地址不对。AT24C系列挂在总线上的地址由A2A1A0引脚决定,如果你板上A0A1A2接了不同的电平,地址就不是0xA0。

现场排查先量SDA线的静态电平,正常情况下总线空闲时应该被上拉到高,如果量到低电平说明有设备把总线拉死了。然后逐个检查地址引脚的实际电平,用万用表量一遍A0到A2的电压,再对照驱动里的设备地址宏。我遇到过地址引脚虚焊的情况,A1引脚浮空,时好时坏,换了板子才找到问题。

4.2 现象:连续写入多字节后,前面对后面错

这个坑基本就是页写入跨页导致的。AT24C系列内部地址指针是11位或者更宽,但页内偏移只有低位几位,超过页大小后低位回卷,高位不变,数据就写到错误位置了。

解决方法是写入前判断当前页剩余空间是否够本次数据量,不够就把数据拆成两段,第一段写满当前页,第二段从下一页开头接着写。这个逻辑可以封装在高层接口里,所有调用写页函数的地方都走这个封装,就再也不会踩跨页的雷。

4.3 现象:偶发写入失败,重试一次就好了

真实现场里这种问题最磨人。有时候写入返回成功,但断电重启后数据是旧的;有时候写入中途卡死,程序跑飞。

这类偶发故障最常见的原因就是写周期延时不够,或者写周期内来了中断打断了延时。DSP2812的中断系统在这个项目里是个隐患,写周期内如果有定时器中断或其他外设中断触发,延时函数会被拉长,但主程序可能已经开始下一次读写。另外上电瞬间电压不稳也会导致EEPROM内部写周期异常。

解决思路分了三个层面。第一,写周期延时尽量用时间戳轮询代替死等,比如记录当前系统计时,超过10ms后再继续执行,期间允许中断发生。第二,关键参数写入后立刻回读校验,读回来不一致就重新写,重试三次还不行再报错。第三,整机断电时,在电压跌落检测之后立即触发数据保存,优先保证写周期完整。

4.4 现象:SCL波形看起来有毛刺,通信偶发异常

用示波器抓SCL和SDA波形时,如果边沿不干净、有毛刺,大概率是走线太长或者上拉电阻取值不匹配。I2C是开漏结构,电平上升靠上拉电阻,电阻太大会导致上升沿变缓,电阻太小的导致灌电流过大。

现场处理建议把上拉电阻换成3.3kΩ到4.7kΩ之间,总线长度控制在10cm以内。DSP2812的GPIO驱动能力本身有限,走线超过20cm时通信速率会明显下降。

4.5 现象:掉电保存的数据偶尔丢失

这是最严重的故障,通常发生在系统电压跌落瞬间。DSP2812检测到电压低时开始执行保存流程,但此时EEPROM的工作电压可能已经低于最低值,内部写周期没有完成。

解决办法是在硬件上增加电源监控芯片,电压低于阈值时给出中断信号,DSP在中断里赶紧保存参数。同时保存动作要精简,只把最核心的几十字节写进EEPROM,别指望掉电瞬间把几百个字节都写完。这个资源里有一颗硬核的写保护逻辑,它会在第一个字节写完后就切到只写关键数据模式。

5. 进阶验证技巧:用轮询状态机和上位机回读把驱动调到滴水不漏

把驱动跑通只是开始,真正能扛住现场的是验证方法。我这边推荐两步走,一是改造成轮询方式等待写周期,二是用串口把EEPROM里的内容回传到电脑上对比。

轮询写周期的方式其实很简单,EEPROM在内部写周期内不会应答任何I2C命令,但每次写操作都等待固定延时是浪费。你可以把固定延时换成“写完后立刻尝试读取设备地址,如果读不到ACK就继续等待,直到读到ACK为止”。这段逻辑的代码长这样:

unsigned char EEPROM_WaitWriteComplete(void) { unsigned char timeout = 0; I2C_Start(); while(I2C_SendByte(EEPROM_DEV_ADDR_W)) // 写地址,等ACK { timeout++; if(timeout > 100) { I2C_Stop(); return 0; } DELAY_MS(1); } I2C_Stop(); return 1; }

这段代码放在每次写操作之后,替代固定延时,写周期实际多长就等多久,整体运行时间能省接近一半。但对于DSP2812这种实时性要求高的场景,固定延时仍然是最稳妥的,轮询方式适合你对时序有把握之后再做优化。

数据回读验证是我每次调试必走的一步。把所有EEPROM地址空间读出来,通过串口发送到电脑上,用十六进制编辑器打开对比。重点看三个位置:写入区数据是否与源数据完全一致、未写入区是否全0xFF、地址边界处是否有错位数据。曾经碰到过一次驱动收发反了的问题,就是靠回读全量数据才发现所有高字节和低字节换了位置。

从那以后,我每写完一版驱动,都强制自己在仿真器里跑一遍“全空间随机写入再全量回读”的测试脚本,跑完不通过绝不进下一步。这套流程听着笨重,但在现场帮我挤掉了大半隐性故障。这个资源最大的价值也就在于此,它给的不只是一段能跑的驱动,更是一套从初始化到校验完整的思考路径,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进