ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

28335 I2C EEPROM驱动调通:协议、寄存器与读写时序详解

28335 I2C EEPROM驱动调通:协议、寄存器与读写时序详解 简介面向DSP28335嵌入式开发者这份压缩包提供I2C总线读写EEPROM的完整CCS工程解决I2C模块初始化、读写字时序搭建与EEPROM数据存储验证等实际问题。包内共78个文件压缩后182KB以29个h头文件、8个c源文件、8个asm汇编文件为主另有CMD链接脚本、CCS工程配置与makefile辅助文件可基于CCS直接导入编译。工程内同时包含DSP2833x标准外设库和SysCtrl、PieCtrl、CpuTimers等基础模块源码便于理解系统启动与时钟配置。目前已有462人学习。其中核心例程是Example_2833xI2C_eeprom.c围绕I2C起始/停止条件、7位从机地址发送、应答检测等步骤实现28335作为主设备对EEPROM的读写并校验数据配合头文件中的寄存器定义和源码注释可以清晰梳理由硬件初始化到应用层调用的完整链路。通过查看工程结构与源码读者能快速掌握28335配置I2C的完整流程和EEPROM持久化存储的调试思路对后续驱动移植与外设扩展也很有参考价值。1. 掉电丢参之后我还是回头把 28335 的 I2C EEPROM 调通了手头一块 28335 控制板控制参数每次一断电就回到出厂值查到最后是通信时序问题EEPROM 芯片明明挂在 I2C 总线上驱动却卡在地址应答上。后来把i2c_eeprom.rar这套工程完整拆了一遍——从Example_2833xI2C_eeprom.c到DSP2833x_I2C.c再到两个.cmd链接文件才意识到问题不在于读写的宏定义而在于我把 I2C 的“帧边界”理解得太粗了。这套基于 TI DSP2833x 例程风格的工程适合手里有 28335、需要把参数存进 AT24C 系列 EEPROM 的人它把 I2C 主模式、从设备寻址、EEPROM 页写这三件事串成了一个可以直接跑通的框架。下面从协议边界讲起把关键寄存器、读写时序和工程落地一起说清楚。2. 先把 I2C 总线的两个协议事实讲清楚再动手写 28335 寄存器2.1 7 位地址、应答位和一帧的边界I2C 总线上一帧数据不是“发完就完事”的。任何一个主设备发起通信都要先产生起始条件随后发送 7 位从设备地址加 1 位读写位然后等待从设备的应答位。这里面最容易忽略的是“应答位”本身的时间窗口主设备在 SCL 第 9 个时钟周期释放 SDA由从设备决定拉低还是保持高电平。28335 的 I2C 模块把这个过程固化在硬件里但软件需要检查状态寄存器里的 NACK 标志才能知道从设备是否真的接收了控制字节。另一个事实是停止条件的位置。EEPROM 这类从设备很较真写操作只有收到停止条件之后才会真正把数据写入内部存储阵列而读操作如果主设备不主动发出非应答信号再收尾总线就会一直处于“读下一个地址”的状态。所以驱动里必须在最后一个字节传输结束后明确写入 STOP而不是等模块自己结束。还有一个边界是重复起始条件。在随机读 EEPROM 时需要先发一个伪写命令把内部地址指针指过去再重新发起 START 切换为读方向。这个过程中没有 STOP而是用重复起始连接两个方向相反的事务。28335 的 I2CMDR 寄存器里置位 STT 位就能让硬件在已经占住总线的情况下再次产生起始条件不会先把总线释放掉。2.2 28335 I2C 模块的关键寄存器分组28335 的 I2C 外设寄存器不算多但每个寄存器的位域含义直接影响驱动写法。把最重要的几个整理如下寄存器关键位/字段驱动中扮演的角色I2CMDRMST、TRM、STT、STP、IRS主从模式、收发方向、起停控制、模块复位I2CSTRBB、SCD、NACK、RRDY、XRDY总线忙、停止完成、无应答、收发就绪I2CSAR7 位从机地址每次事务前必须写入目标地址I2CCNT本次传输字节数主模式下硬件按该计数自动结束数据段I2CCLKL / I2CCLKH时钟低/高电平分频值决定 SCL 频率I2CDXR / I2CRXR发送/接收数据寄存器读写数据的入口驱动代码中最常用的状态位是 BB 和 SCD。BB 为 1 表示总线正被占用发送前要等它清零否则会插入到别的设备事务中间SCD 表示停止条件已完成用于确认一帧事务安全落地。NACK 则是排错时的第一现场从设备地址写错、EEPROM 处于内部写周期、总线上拉电阻没接好都会在这里暴露出来。2.3 主模式下一字节发送的基础实现下面的函数是一个最基础的 I2C 主发送原语单元化设计后续 EEPROM 驱动都复用它Uint16 i2c_send_byte(Uint16 dev_addr, Uint16 byte, Uint16 enable_stop) { Uint16 timeout 0xFFFF; /* 等待总线空闲超时退出防止死等 */ while (I2caRegs.I2CSTR.bit.BB 1) { if (--timeout 0) return 1; } I2caRegs.I2CSAR dev_addr; /* 目标从设备地址 */ I2caRegs.I2CCNT 1; /* 本帧只发一个数据字节 */ I2caRegs.I2CDXR byte; /* 待发送数据 */ I2caRegs.I2CMDR.bit.MST 1; /* 主模式 */ I2caRegs.I2CMDR.bit.TRM 1; /* 发送方向 */ I2caRegs.I2CMDR.bit.STT 1; /* 产生起始条件 */ if (enable_stop) { I2caRegs.I2CMDR.bit.STP 1; /* 需要时产生停止条件 */ } /* 等待事务结束SCD 在停止条件完成后置位 */ timeout 0xFFFF; while (I2caRegs.I2CSTR.bit.SCD 0) { if (--timeout 0) return 1; } /* 检查从设备是否应答 */ if (I2caRegs.I2CSTR.bit.NACK 1) { I2caRegs.I2CSTR.bit.NACK 1; /* 写 1 清除标志 */ return 2; } return 0; }这个函数里I2CCNT 1是主模式下必写的字段硬件在计数到 0 时会配合停止条件结束传输。I2CDXR的写入时机在STT之前原因是硬件一旦产生起始条件就会按顺序搬运从地址和 DXR 中的数据提前把数据放好可以避免地址发出后数据还没就位。enable_stop参数则给了调用方控制帧边界的自由在连续写 EEPROM 页的时候中间字节不置 STP最后一字节才置。注意如果一个从设备没有回应而你又把 NACK 检查放在 SCD 等待之后那么 SCD 本身也可能不来。所以实际工程里两个等待都要加超时不能只靠一个标志。3. EEPROM 读写的协议实现从 24LC02 到通用 AT24C 系列3.1 控制字节与器件地址模型EEPROM 的控制字节不是单纯的“芯片地址”。以 AT24C02 为例控制字节格式是1 0 1 0 A2 A1 A0 R/W高 4 位固定为 1010接下来是芯片引脚 A2/A1/A0 的硬件电平最低位是读写方向。大部分板子上三个引脚直接接地所以写方向的控制字节就是 0xA0读方向是 0xA1。如果换成容量更大的 AT24C16页选择位会占用控制字节的低地址位这时内存地址和器件地址要联合计算。而 24LC02 这类小容量芯片只有 8 位内存地址控制字节和内存地址各管各的用起来最简单。下面是地址模型对比型号容量内存地址宽度控制字节中的页选择位AT24C02 / 24LC022 Kbit8 位无A0/A1/A2 由引脚决定AT24C044 Kbit9 位A0 位借用为存储页选择AT24C1616 Kbit11 位A0/A1/A2 全部借用驱动里区分这几种型号的关键是“内存地址映射到控制字节”的公式。下面代码里用dev 0xA0 | ((mem_addr 8) 0x0E)就是为兼容 16 位内存地址空间留的口子当内存地址超过 8 位时高于第 7 位的部分映射到控制字节的 P2/P1/P0 位置。3.2 写一个字节把控制字、内存地址、数据装进同一帧EEPROM 写操作的标准时序是起始条件 → 控制字写方向→ 内存地址 → 数据字节 → 停止条件。整帧必须连续不能拆成独立的两笔事务来发否则 EEPROM 会把后半段当成新的命令序列。下面是基于底层多字节发送接口的实现Uint16 i2c_write_buf(Uint16 dev_addr, Uint16 *data, Uint16 len, Uint16 issue_stop) { Uint16 i; /* 等待总线空闲 */ while (I2caRegs.I2CSTR.bit.BB 1) {} I2caRegs.I2CSAR dev_addr; I2caRegs.I2CCNT len; I2caRegs.I2CDXR data[0]; I2caRegs.I2CMDR.bit.MST 1; I2caRegs.I2CMDR.bit.TRM 1; I2caRegs.I2CMDR.bit.STT 1; /* 剩余数据在 XRDY 置位后再写入 DXR */ for (i 1; i len; i) { while (I2caRegs.I2CSTR.bit.XRDY 0) {} I2caRegs.I2CDXR data[i]; } if (issue_stop) { I2caRegs.I2CMDR.bit.STP 1; while (I2caRegs.I2CSTR.bit.SCD 0) {} } return 0; } Uint16 eeprom_write_byte(Uint16 mem_addr, Uint16 value) { Uint16 dev 0xA0 | ((mem_addr 8) 0x0E); Uint16 buf[2]; buf[0] mem_addr 0xFF; /* 内存地址低 8 位 */ buf[1] value; /* 要写入的数据 */ /* 控制字 地址 数据一次发完帧尾带停止条件 */ return i2c_write_buf(dev, buf, 2, 1); }i2c_write_buf里值得在意的是I2CCNT len的语义它表示本次事务中从地址之后的数据字节总数。硬件自动发送从地址后会依照计数器逐个搬移 DXR 中的数据计数归零后配合 STP 位收尾。XRDY标志在数据从 DXR 移入移位寄存器后置位表示可以装载下一个字节不等待它而连续写 DXR会出现数据覆盖这在快速连续写页时是常见故障点。3.3 随机读与顺序读伪写、重复起始、非应答收尾随机读比写多了一步“伪写”操作。首先用写方向的控制字把内存地址发出去但不发送数据、不发停止条件随后再次发起起始条件把控制字的 R/W 位改成 1此时 EEPROM 会把内部地址指针指向刚才锁存的内存位置并从该地址开始输出数据。实现代码如下Uint16 eeprom_read_byte(Uint16 mem_addr) { Uint16 dev 0xA0 | ((mem_addr 8) 0x0E); Uint16 addr_low mem_addr 0xFF; Uint16 val; /* 伪写发送内存地址不带停止条件 */ i2c_write_buf(dev, addr_low, 1, 0); /* 重复起始切换为读方向 */ I2caRegs.I2CSAR dev | 0x01; I2caRegs.I2CCNT 1; I2caRegs.I2CMDR.bit.TRM 0; /* 切换为接收方向 */ I2caRegs.I2CMDR.bit.STT 1; /* 在总线上产生重复起始 */ /* 等待接收数据就绪 */ while (I2caRegs.I2CSTR.bit.RRDY 0) {} val (Uint16)I2caRegs.I2CRXR; /* 对最后一字节不回 ACK直接停止 */ I2caRegs.I2CMDR.bit.STP 1; return val; }这里面TRM 0必须放在STT 1之前原因是 28335 的 I2C 模块会按照 I2CMDR 的当前配置来生成起始后的第一个时钟节拍如果先置 STT 再切方向硬件可能已经按发送模式把总线上没用的数据送出。而读方向最后一个字节的处理也值得留意主设备在接收模式下倒数第二个字节要设置 NACK 模式最后一个字节结束后才发 STOP28x 系列寄存器里没有单独的主设备 NACK 模式位时依靠I2CCNT 1的计数配合 STP 也能完成因为硬件会在最后一个字节后自动发送非应答。提示eeprom_read_byte里的伪写如果被强加 STOP读取的就不是目标地址的数据而是伪写完成后内部指针自增后的内容。所以i2c_write_buf的issue_stop参数在随机读场景里必须传 0。4. 把例程工程跑通CMD 文件、外设时钟与 NACK 恢复4.1 从 i2c_eeprom.rar 到 CCS 工程的关键文件这个压缩包里的文件分三类第一类是例程入口和驱动源文件包括Example_2833xI2C_eeprom.c、DSP2833x_I2C.c、DSP2833x_SysCtrl.c第二类是头文件包DSP2833x_Device.h、DSP2833x_I2c.h、DSP2833x_Examples.h等构成了外设寄存器和例程公共函数的声明层第三类是两个.cmd文件28335_RAM_lnk.cmd负责把代码段、数据段分配到片上 RAMDSP2833x_Headers_nonBIOS.cmd则把外设寄存器结构体映射到各自的内存地址。这两个cmd文件的分工经常被搞混。28335_RAM_lnk.cmd决定程序在哪里跑DSP2833x_Headers_nonBIOS.cmd决定外设寄存器变量名对应哪个物理地址。后者如果被删掉I2caRegs这个符号在链接阶段就会报 unresolved symbol而不是编译错误。排查工程导入后报错时先确认两个 cmd 都在且没有被 CCS 自动排除出构建列表。工程导入后第一步不要急着改代码而是先编译一次。因为包里的sources.mk和objects.mk是 CCS 自动生成的换工作区后路径会失效。常见错误是找不到DSP2833x_Headers_nonBIOS.cmd这通常是因为工程没有把文件包含进构建。右键 cmd 文件选 Include in Build 即可。4.2 I2C 外设时钟与 100kHz 模式配置28335 的 I2C 模块属于片内外设使用前要先打开时钟门控。下面的初始化代码是实际工程中最常见的基础配置void InitI2C(void) { EALLOW; /* 使能 I2C 外设时钟 */ SysCtrlRegs.PCLKCR0.bit.I2CAENCLK 1; EDIS; /* 先复位模块配置完成后重新使能 */ I2caRegs.I2CMDR.bit.IRS 0; /* 150MHz 系统时钟分频I2C 输入时钟 150MHz / 15 10MHz */ I2caRegs.I2CPSC 14; /* SCL 高低电平各 10 个周期约 100kHz */ I2caRegs.I2CCLKL 10; I2caRegs.I2CCLKH 10; I2caRegs.I2CMDR.bit.MST 1; /* 主模式 */ I2caRegs.I2CMDR.bit.IRS 1; /* 模块退出复位 */ }I2CPSC 14时I2C 模块的输入时钟是系统时钟的 1/15。I2CCLKL和I2CCLKH则用于进一步分频产生 SCL把两者都设为 10输出接近 100kHz。如果 EEPROM 型号支持 400kHz 快速模式可以把这两个值调到 4 左右但前提是总线电容和上拉电阻允许板级设计不佳时400kHz 下波形边沿会明显变缓反而更容易出现数据错误。建议在验证阶段先用 100kHz 跑通再视示波器波形决定是否提速。4.3 NACK 后的恢复动作EEPROM 上电后如果处于内部写周期约 5ms 的窗口内总线上的从设备不会响应任何命令。此时软件最容易掉进两种错误一是把 NACK 当成致命错误直接复位总线二是不清 NACK 标志就继续下一笔事务导致后续通信全部错位。正确的恢复路径是检测到 NACK → 清标志 → 延时覆盖写周期 → 重新发送刚才的事务。下面是一个带恢复机制的写调用Uint16 eeprom_write_with_retry(Uint16 mem_addr, Uint16 value) { Uint16 retry; for (retry 0; retry 3; retry) { if (eeprom_write_byte(mem_addr, value) 0) { return 0; } /* NACK 后清标志等待内部写周期结束 */ I2caRegs.I2CSTR.bit.NACK 1; DELAY_US(5000); } return 1; }这种重试策略利用的是 EEPROM 在写周期结束后的“内部门锁释放”行为一旦可以应答控制字节再次执行完整写时序就能成功。需要强调的是重试三次还失败问题大概率不在写周期上而在硬件连接。用示波器在 SDA 和 SCL 上观察上拉是否到 3.3V 或 5V、控制字节是否发出去这个诊断比改软件更有效。5. 更稳一点的 I2C EEPROM 驱动页写对齐与回读校验5.1 页写时先做地址对齐EEPROM 页写虽然能一次写入多个字节但边界约束和内存地址对齐相关。AT24C02 的页大小是 8 字节页内地址从 0x00 开始写满后内部地址指针会回卷到本页开头而不是自动进入下一页。因此跨页写时要把数据切块每一块的起始地址都落在页边界上。下面是一个把任意长度数据块“拆页”的封装#define EE_PAGE_SIZE 8 Uint16 write_user_block(Uint16 start, Uint16 *src, Uint16 len) { Uint16 chunk; while (len 0) { /* 当前页剩余可写字节数 */ chunk EE_PAGE_SIZE - (start % EE_PAGE_SIZE); if (chunk len) { chunk len; } if (eeprom_write_page(start, src, chunk) ! 0) { return 1; } start chunk; src chunk; len - chunk; } return 0; }chunk的计算逻辑是整个封装的要点先算出本页剩余的字节数再取它和剩余长度的较小值。这样每次调用eeprom_write_page时起始地址加上本次长度一定不超过当前页的末尾。如果不做这个对齐连续写超过页边界时EEPROM 会从页内部回卷覆盖掉本页开头已写的数据。5.2 写入后立刻回读把脏数据挡在应用层外面EEPROM 内部写周期结束时不会主动通知主设备软件只有通过轮询应答来判断“是否写完了”。但应答正常只说明芯片准备好了说明不了数据真的写对了。稳妥的做法是在写完关键参数后立刻随机读回该地址和写入值做一次比较。这个过程开销很小却能覆盖写周期错乱、页回卷覆盖、总线干扰三类问题。Uint16 eeprom_verify_byte(Uint16 mem_addr, Uint16 expect) { Uint16 actual eeprom_read_byte(mem_addr); return (actual expect) ? 0 : 1; }验证函数不要只调用一次也不要把它放在写函数内部无条件执行。合理的节奏是系统上电初始化时读一封配置块并做校验写入参数时在写完成后调用eeprom_verify_byte失败就返回错误码给上层由应用决定是重试还是恢复默认值。这个做法比在驱动底层死等总线更有工程价值毕竟 EEPROM 的写入损坏多数发生在掉电瞬间而这种故障不是软件重试能解决的需要在存储布局上做双备份或加校验字。把跨页循环和回读校验封装好之后再往上一层加坏块标记、双备份切换都会顺手很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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