从零开始学习嵌入式P45----I2C(上)
串口可以让开发板和电脑交换字符;I2C 则常用于板上芯片之间的通信。这一篇让 i.MX6ULL 的I2C1向一颗 EEPROM 写入六个字节。即使没有学过电路和通信时序,也可以从“引脚为什么要上拉”一路读到“为什么向I2DR写一个字节后还要等待应答”。
本文的例子使用i.MX6ULL-Mini 开发板,把 UART4_TX_DATA 焊盘复用为 I2C1_SCL,把 UART4_RX_DATA 焊盘复用为 I2C1_SDA,外接一颗3.3 V 供电、7 位地址为0x50的 AT24C08A EEPROM。板上的 I2C1 两根线各有约 4.7 kΩ 的 3.3 V 上拉电阻;外接模块若也带上拉,要避免重复并联过多电阻。程序是裸机、主机模式、轮询传输,不依赖操作系统,也不启用 I2C 中断。本文给出完整的最小工程文件和预期总线字节,无须先读其他文章才能理解或构建它。
**接线先确认:**开发板 I2C1_SCL 接 EEPROM SCL,I2C1_SDA 接 EEPROM SDA,双方 GND 相连,EEPROM VCC 接与器件型号匹配的 3.3 V,
WP接 GND 才允许写入。本文按 AT24C08A 的A2=0、存储块 0 说明地址0x50;A0/A1对这颗 8 Kbit 器件不参与硬件设备地址选择。板上有 I2C 上拉,并不意味着板上一定装有地址0x50的 EEPROM。I2C 焊盘不能接到被 5 V 上拉的总线。
一、I2C 究竟是什么
I2C(Inter-Integrated Circuit,也常写作 I²C;有些工程文件把它写成 IIC)是一种在短距离内让多颗芯片共享线路的串行总线。它只用两根信号线:
| 线名 | 全称 | 用途 |
|---|---|---|
SCL | Serial Clock | 给每一位数据提供时钟节拍 |
SDA | Serial Data | 双向传送地址、数据和应答 |
还要有共同的GND,否则双方没有一致的电压参照。I2C 不是“只要两根线相连就能说话”:双方还必须使用相容的供电和逻辑电平、上拉电阻、地址及总线速率。
本例里 i.MX6ULL 是主机:它发起通信、产生 SCL,并选择要访问的从机。EEPROM 是从机:只有看到自己的地址才回应。I2C 也允许总线上存在不止一个主机,因此控制器有仲裁机制;本例只接一个主机,但程序仍检查“仲裁丢失”状态。
I2C 是半双工的。SDA 可以向两个方向传,但同一时刻只有一方负责当前数据位;收发双方会在规定的时刻交换 SDA 的控制权。
为什么必须是开漏和上拉
普通推挽输出可以主动拉高,也可以主动拉低。如果一颗芯片强推高电平,另一颗芯片同时强拉低电平,就会发生输出冲突。I2C 改用开漏输出:芯片只能主动把线拉低;要表达高电平,就释放这根线,由外部上拉电阻把它拉到 3.3 V。
3.3 V │ 上拉电阻 │ SDA ────────────┼──────── i.MX6ULL SDA 输入/开漏输出 └──────── EEPROM SDA 输入/开漏输出 没人拉低:SDA = 1 任一设备拉低:SDA = 0同样的连接方式也用于 SCL。这种连接使“任意设备拉低”优先,所以常称为线与。它还能支持从机把 SCL 暂时保持为低电平,让主机放慢速度,这叫时钟延展。上拉电阻并非越小越好:太小会增大低电平时的电流,太大又会让上升沿过慢。本文开发板上的 4.7 kΩ 是这个 3.3 V、短线实验的现成配置。
**开漏与“把寄存器写成 1”不是一回事。**焊盘要配置开漏功能,硬件在需要高电平时才会真正释放线路。本例后面的焊盘值是0x18B0;其中 bit11ODE=1。常见的0x10B0少了这一位,不能把它照搬为规范的 I2C 焊盘设置。
二、看懂一笔 I2C 传输
总线空闲时,上拉使 SCL、SDA 都为高电平。主机开始传输时,在SCL 为高的时候让 SDA 从高变低,形成START。结束时,在SCL 为高的时候让 SDA 从低变高,形成STOP。在普通数据位期间,发送方主要在 SCL 低时改变 SDA,接收方在 SCL 高时识别其值;这样就不会把数据变化误判成 START 或 STOP。
空闲高电平 ── START ── 8 个数据位 ── 第 9 个时钟:ACK/NACK ── … ── STOP │ MSB 先发 │ SDA 高→低、SCL 高 接收方回应每发送8 位,发送方要释放 SDA;接收方在第 9 个时钟给出回应:
- ACK = 0:接收方把 SDA 拉低,表示这一字节已接收。
- NACK = 1:接收方不拉低 SDA。可能是没有对应设备、设备暂时不能接收,或接收方有意结束读取。
“数据字节发送完成”与“对方真的确认接收”是两件事。控制器的完成标志告诉软件第 9 个时钟结束了;还要检查RXAK才知道对方回的是 ACK 还是 NACK。
地址不是寄存器地址
常用的 I2C7 位设备地址用于选中某颗芯片。传到总线上的第一个字节则是这 7 位左移一位,再把最低位填成方向位:0表示主机写,1表示主机读。
设备 7 位地址:0x50 = 1010000₂ 写方向字节: (0x50 << 1) | 0 = 0xA0 = 10100000₂ 读方向字节: (0x50 << 1) | 1 = 0xA1 = 10100001₂因此代码传入0x50,写进数据寄存器的第一个字节却是0xA0;它们不是两个不同的设备地址。地址字节之后发出的0x00是EEPROM 内部存储位置,也叫字地址,含义与 7 位设备地址不同。
这次写入的完整顺序是:
START → 0xA0(设备地址 0x50 + 写方向) → EEPROM ACK → 0x00(从存储块 0 的字地址 0 开始)→ EEPROM ACK → '1' '2' '3' '4' '5' '6' → 每字节后 EEPROM ACK → STOP字符'1'在总线上不是数值1,而是 ASCII0x31。所以六个数据字节依次是31 32 33 34 35 36(十六进制)。若用逻辑分析仪查看,解码结果应接近START A0 ACK 00 ACK 31 ACK … 36 ACK STOP。
EEPROM 为什么不能无限连续写
AT24C08A 是 8 Kbit,即1024 字节的 EEPROM。它按 256 字节分为 4 个存储块,块号占用设备地址中的两个位;块内位置由后面的 8 位字地址给出。本文只写块 0 的0x00~0x05,所以0x50加0x00足够定位。
它一次页写最多容纳 16 个数据字节,并且必须留在同一 16 字节页内。地址从页尾继续写时,会回绕到同一页开头覆盖前面的数据,而不会自动进入下一页。本例从0x00写 6 字节,没有跨页。STOP 之后 EEPROM 还要进行内部非易失写入,最长可到约10 ms;总线 ACK 只表示字节被接受,不能把它当成数据已经落入存储单元的证明。为避免反复消耗擦写寿命,完整程序只执行一次写入。
三、i.MX6ULL I2C1 从焊盘到寄存器
同一个芯片焊盘可以承担多种功能。名称里虽然写着UART4,选择ALT2后,它们在本例承担的是 I2C1 功能,与 UART4 通信无关。
| 项目 | 本例配置 | 为什么需要 |
|---|---|---|
| UART4_TX_DATA 焊盘 | ALT2 + SION→ I2C1_SCL | 把物理引脚接到控制器,并始终打开输入路径 |
| UART4_RX_DATA 焊盘 | ALT2 + SION→ I2C1_SDA | SDA 需要双向输入、输出 |
| SCL 输入选择 | 0x020E05A4 = 1 | 在多个可选 SCL 焊盘中选 UART4_TX_DATA |
| SDA 输入选择 | 0x020E05A8 = 2 | 在多个可选 SDA 焊盘中选 UART4_RX_DATA |
| 两个焊盘电气配置 | 0x18B0 | 在0x10B0的基础上设置ODE开漏位 |
| I2C1 时钟门控 | CCM_CCGR2[CG3] = 11₂ | 允许 I2C1 工作 |
SION是Software Input On:即使该复用功能看似在“输出”,也强制打开焊盘输入路径,让控制器能观察总线真实电平。I2C 要判断线是否被其他设备拉低,所以两个焊盘都要保留输入路径。SION位是复用寄存器 bit4,ALT2是低位的2,合起来写0x12。
1. 五个 16 位寄存器
i.MX6ULL 的 I2C 外设寄存器要求按16 位半字访问。I2C1 的基址是0x021A0000,本例用到的寄存器如下:
| 寄存器 | 地址偏移 | 作用 |
|---|---|---|
IADR | 0x00 | 本控制器作为从机时响应的地址;本例是主机,不用它选择 EEPROM |
IFDR | 0x04 | 设置 SCL 分频 |
I2CR | 0x08 | 使能、主从选择、收发方向、START/STOP |
I2SR | 0x0C | 完成、总线忙、仲裁丢失、ACK 状态 |
I2DR | 0x10 | 写入要发的字节,或读出接收的字节 |
注意IADR不是 EEPROM 设备地址寄存器。主机要访问谁,是把“7 位地址 + 读写位”作为普通的第一个发送字节写入I2DR。
I2CR的关键位:
| 位 | 名称 | 本文里的含义 |
|---|---|---|
| bit7 | IEN | 1使能 I2C 控制器 |
| bit6 | IIEN | 1开硬件中断;本例始终为0 |
| bit5 | MSTA | 0→1产生 START,1→0产生 STOP |
| bit4 | MTX | 1发送模式,0接收模式 |
| bit3 | TXAK | 主机接收数据时回复 ACK 或 NACK;写入实验不需要改它 |
| bit2 | RSTA | 不先 STOP,直接产生重复 START;本次写入不需要 |
I2SR的关键位:
| 位 | 名称 | 程序如何使用 |
|---|---|---|
| bit7 | ICF | 一个字节及其应答时钟已经完成 |
| bit5 | IBB | 总线忙:START 后为 1,STOP 后回到 0 |
| bit4 | IAL | 仲裁丢失;置位后不能继续假装自己是主机 |
| bit1 | IIF | 本次字节完成等事件发生;即使不开硬件中断也会置位 |
| bit0 | RXAK | 读到0是 ACK,读到1是 NACK |
IIF名字里有“中断”,并不表示一定要写中断处理函数。只要IIEN=0,我们就可以在主循环轮询它。IIF、IAL都是写 0 清除,不应以为“写 1 清除”。
2. 时钟频率怎么确定
I2C1 的传输节拍来自PERCLK,不是 CPU 主频。为了让示例无需猜测前级时钟树状态,初始化时把 PERCLK 的来源明确选为开发板的24 MHz 晶振,分频字段设为 0(实际除以 1)。再将IFDR=0x0F,对应的 I2C 分频系数是240:
PERCLK = 24 MHz / (0 + 1) = 24 MHz 名义 SCL = 24 MHz / 240 = 100 kHz焊盘和上拉使边沿需要一定时间,真实线上测得的周期可能比这个名义值略长。100 kHz 与本例 3.3 V AT24C08A 的标准模式相匹配。
另一种时钟组合是 PERCLK 为 66 MHz、IFDR=0x15,而0x15在分频表里代表÷640,名义 SCL 约为103.1 kHz。IFDR字段不是直接写频率,也不是把十六进制0x15当成“除以 21”。对于最高 100 kHz 的 3.3 V 器件,本文不沿用那组稍高的名义速率。时钟来源与 IFDR 必须成套设置。
四、把写入过程翻译成程序
一笔主机写入可以按“每写一个字节就等待一次”来理解:
/* 核心顺序,省略错误处理细节 */I2CR=IEN|MTX|MSTA;/* 发送模式,产生 START */I2DR=(0x50U<<1)|0U;/* 0xA0:选中 EEPROM,方向为写 */wait_for_byte_and_ack();I2DR=0x00U;/* EEPROM 块内字地址 */wait_for_byte_and_ack();I2DR='1';/* 后续每个数据字节也一样 */wait_for_byte_and_ack();I2CR=IEN;/* 清 MSTA,产生 STOP */第一段让控制器掌握总线并送出 START;第二段才真正开始发送地址字节。每次写I2DR后,控制器负责把 8 位数据及第 9 个 ACK 时钟放到 SCL/SDA 上,软件等IIF变为 1,再检查ICF与RXAK。如果没有 EEPROM,地址字节通常就会得到 NACK,后续数据不能继续发。
原型驱动常写成while ((I2SR & IIF) == 0) {}。线路断开、SCL 被外设一直拉低时,它可能永远出不来。原型函数若返回void,主程序也不知道是地址 NACK、数据 NACK 还是等待超时。完整示例改为有限轮询并返回错误码,同时在失败时尝试释放总线。
五、完整可构建的最小示例
把下面四个文件放在同一个目录。代码使用寄存器物理地址,假定前级启动程序已经完成 DDR 初始化,并把裸机镜像装载到 DDR0x87800000后跳转到该地址;CPU 在可访问外设物理地址的特权状态运行。本例不负责 i.MX6ULL 从 SD 卡上电所需的 IVT、DCD 和 DDR 初始化封装。所用启动地址、交叉编译器和装载条件都在这里写明,代码本身不需要额外 SDK 头文件。
1.main.c:寄存器、初始化与写入
#include<stdint.h>#include<stddef.h>#defineREG32(a)(*(volatileuint32_t*)(uintptr_t)(a))#defineREG16(a)(*(volatileuint16_t*)(uintptr_t)(a))#defineCCM_CSCMR1REG32(0x020C401CU)#defineCCM_CCGR1REG32(0x020C406CU)#defineCCM_CCGR2REG32(0x020C4070U)#defineMUX_UART4_TXREG32(0x020E00B4U)#defineMUX_UART4_RXREG32(0x020E00B8U)#definePAD_UART4_TXREG32(0x020E0340U)#definePAD_UART4_RXREG32(0x020E0344U)#defineSEL_I2C1_SCLREG32(0x020E05A4U)#defineSEL_I2C1_SDAREG32(0x020E05A8U)#defineI2C1_IFDRREG16(0x021A0004U)#defineI2C1_I2CRREG16(0x021A0008U)#defineI2C1_I2SRREG16(0x021A000CU)#defineI2C1_I2DRREG16(0x021A0010U)#defineIEN(1U<<7)#defineMSTA(1U<<5)#defineMTX(1U<<4)#defineICF(1U<<7)#defineIBB(1U<<5)#defineIAL(1U<<4)#defineIIF(1U<<1)#defineRXAK(1U<<0)#defineGPIO1_DRREG32(0x0209C000U)#defineGPIO1_GDIRREG32(0x0209C004U)#defineMUX_LEDREG32(0x020E0068U)#definePAD_LEDREG32(0x020E02F4U)enum{I2C_OK=0,I2C_BUSY=-1,I2C_TIMEOUT=-2,I2C_NACK=-3,I2C_ARBITRATION_LOST=-4,I2C_BAD_LENGTH=-5};/* 有限轮询,防止硬件或接线异常时永远卡住。不是精确的毫秒计时器。 */staticintwait_bus_idle(void){for(uint32_tn=0;n<200000U;++n){if((I2C1_I2SR&IBB)==0U)returnI2C_OK;}returnI2C_BUSY;}/* 写 I2DR 后调用:等待一个字节传完,并确认接收方应答。 */staticintwait_byte_ack(void){uint16_tstatus;for(uint32_tn=0;n<200000U;++n){status=I2C1_I2SR;if((status&IAL)!=0U){I2C1_I2SR=0U;/* 写 0 清 IAL/IIF */returnI2C_ARBITRATION_LOST;}if((status&IIF)!=0U){I2C1_I2SR=0U;/* 为下一字节清除旧 IIF */if((status&ICF)==0U)returnI2C_TIMEOUT;if((status&RXAK)!=0U)returnI2C_NACK;returnI2C_OK;}}returnI2C_TIMEOUT;}staticvoidi2c1_init(void){/* 本例尚未启动其他 PERCLK 外设,选择 24 MHz 晶振,分频为 1。 */CCM_CCGR2&=~(3U<<6);/* 临时关闭 I2C1 时钟门 */CCM_CSCMR1=(CCM_CSCMR1&~0x7FU)|(1U<<6);CCM_CCGR2|=(3U<<6);/* CCGR2[CG3] = 11 */MUX_UART4_TX=0x12U;/* ALT2 + SION:SCL */MUX_UART4_RX=0x12U;/* ALT2 + SION:SDA */SEL_I2C1_SCL=1U;/* 输入来自 UART4_TX_DATA */SEL_I2C1_SDA=2U;/* 输入来自 UART4_RX_DATA */PAD_UART4_TX=0x18B0U;/* ODE=1,开漏 */PAD_UART4_RX=0x18B0U;I2C1_I2CR=0U;/* 停止控制器,内部软件复位 */I2C1_IFDR=0x0FU;/* 24 MHz / 240 = 100 kHz */I2C1_I2SR=0U;/* 清旧 IIF、IAL */I2C1_I2CR=IEN;/* 使能;仍是空闲从机态 */}/* 只写 AT24C08A 存储块 0 中的一页;本例从字地址 0 开始。 */staticinteeprom_write_page0(uint8_tword_addr,constuint8_t*data,size_tlength){intresult;if(data==NULL||length==0U||length>(16U-(word_addr&0x0FU)))returnI2C_BAD_LENGTH;/* 不允许跨 16 字节页 */result=wait_bus_idle();if(result!=I2C_OK)returnresult;I2C1_I2SR=0U;/* 清旧标志 */I2C1_I2CR=IEN|MTX|MSTA;/* START、主机发送 */I2C1_I2DR=(uint16_t)(0x50U<<1);/* 7 位地址 0x50 + 写位 0 */result=wait_byte_ack();if(result!=I2C_OK)gotostop;I2C1_I2DR=word_addr;/* 块内字地址 */result=wait_byte_ack();if(result!=I2C_OK)gotostop;for(size_ti=0;i<length;++i){I2C1_I2DR=data[i];result=wait_byte_ack();if(result!=I2C_OK)gotostop;}stop:if(result==I2C_ARBITRATION_LOST){/* 已失去主机资格,不能再假定清 MSTA 能产生本机 STOP。 */I2C1_I2CR=0U;I2C1_I2SR=0U;I2C1_I2CR=IEN;returnresult;}I2C1_I2CR=IEN;/* 清 MSTA:产生 STOP */if(wait_bus_idle()!=I2C_OK&&result==I2C_OK)result=I2C_BUSY;returnresult;}/* Mini 板 LED 接 GPIO1_IO03,低电平点亮。 */staticvoidled_init(void){CCM_CCGR1|=(3U<<26);/* GPIO1 时钟门 */MUX_LED=5U;/* GPIO 模式 */PAD_LED=0x10B0U;/* LED 不需要开漏 */GPIO1_DR|=(1U<<3);/* 默认熄灭 */GPIO1_GDIR|=(1U<<3);/* 输出 */}intmain(void){staticconstuint8_tmessage[]={'1','2','3','4','5','6'};intresult;led_init();i2c1_init();result=eeprom_write_page0(0x00U,message,sizeofmessage);if(result==I2C_OK)GPIO1_DR&=~(1U<<3);/* 全部字节 ACK 后,LED 点亮 */for(;;);/* 只写一次,不循环磨损 EEPROM */}这个文件前半段用REG16访问 I2C 控制器,用REG32访问 CCM、IOMUXC、GPIO。volatile使每次硬件寄存器读写都保留下来;如果去掉它,编译器可能把“反复看 IIF 有没有变”优化成一次读取。它并不能代替超时、ACK 检查或总线仲裁。
i2c1_init()先锁定时钟与焊盘,再停用控制器、设分频、清旧标志,最后打开IEN。这里选择晶振作为 PERCLK,会影响所有使用 PERCLK 的模块;所以这一设置只适用于本篇这种尚未启用其他 PERCLK 外设的独立程序。若它被放进已有系统,必须统一管理整条 PERCLK 时钟路径。
wait_byte_ack()先看IAL,因为仲裁丢失时即使出现IIF,也不能按“成功传完一个字节”处理。正常完成后,它用保存下来的同一份status检查ICF和RXAK,随后把IIF清零,让下一字节的完成事件不会与上一字节混淆。这里的轮询次数是一个有界防卡死措施,不是保证任何 CPU 频率下恰好等待多少毫秒;如果总线被物理故障拉住,软件返回错误也无法凭空把电平拉高。
eeprom_write_page0()先检查数据长度,保证不会在同一次传输里越过 16 字节页尾。然后按地址、字地址、数据的顺序写I2DR,每一字节之后都等待 ACK。错误路径尽量产生 STOP;若仲裁已经丢失,则复位本机 I2C 控制器而不冒充仍然占有总线。函数返回0表示这次传输的字节被应答且 STOP 后总线空闲,不代表 EEPROM 内部 10 ms 写入周期已经结束。
LED 只是一个简单的成功信号:全程 ACK 时点亮。LED 不亮可能是没有接 EEPROM、设备地址或WP不对、上拉/供电/接线错误、超时或仲裁丢失;它本身不能把这些错误逐一显示。若使用逻辑分析仪,观察 SDA/SCL 的字节和 ACK 最直接。
2.start.S:进入 C 程序前准备栈和 BSS
.syntax unified .cpu cortex-a7 .arm .section .text.startup, "ax", %progbits .balign 32 .global _start _start: ldr pc, =reset_handler b fault_handler b fault_handler b fault_handler b fault_handler nop b fault_handler b fault_handler reset_handler: cpsid if cps #0x1f ldr sp, =0x84000000 ldr r0, =__bss_start ldr r1, =__bss_end mov r2, #0 1: cmp r0, r1 bhs 2f str r2, [r0], #4 b 1b 2: bl main 3: b 3b fault_handler: b fault_handler .ltorg .section .note.GNU-stack, "", %progbits最前面的 8 个槽位按 ARM 异常向量的常见布局预留。本文不启用 IRQ,因此先屏蔽 IRQ/FIQ,切到 SYS 模式后设置栈指针,再把未初始化的静态数据区 BSS 清零。message在只读数据区,局部变量和函数调用仍需要栈。这里没有把VBAR显式指向向量表,所以这些预留槽位不能当成已经配置完毕的完整异常处理系统;正常路径不依赖它们。
3.imx6ull.lds:把代码链接到约定的装载地址
ENTRY(_start) SECTIONS { . = 0x87800000; .text : { KEEP(obj/start.o(.text.startup)) *(.text .text.*) } .rodata ALIGN(4) : { *(.rodata .rodata.*) } .ARM.extab ALIGN(4) : { *(.ARM.extab*) } .ARM.exidx ALIGN(4) : { *(.ARM.exidx*) } .data ALIGN(4) : { *(.data .data.*) } .bss ALIGN(4) (NOLOAD) : { __bss_start = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); __bss_end = .; } }链接地址0x87800000必须与前级装载程序实际放置镜像的位置一致。NOLOAD表示 BSS 不占原始镜像内容,交给start.S在运行前清零。更换装载位置时,必须连同链接地址一起修改,不能只改其中一边。
4.Makefile:编译并生成原始二进制
.DEFAULT_GOAL := all CROSS_COMPILE ?= arm-linux-gnueabihf- CC := $(CROSS_COMPILE)gcc OBJCOPY := $(CROSS_COMPILE)objcopy OBJDUMP := $(CROSS_COMPILE)objdump TARGET := i2c FLAGS := -std=c11 -Wall -Wextra -O0 -g -mcpu=cortex-a7 -marm \ -mfloat-abi=soft -ffreestanding -fno-builtin \ -fno-stack-protector -fno-pie \ -fno-unwind-tables -fno-asynchronous-unwind-tables .PHONY: all clean all: $(TARGET).bin $(TARGET).dis obj: mkdir -p obj obj/start.o: start.S | obj $(CC) $(filter-out -std=c11,$(FLAGS)) -c $< -o $@ obj/main.o: main.c | obj $(CC) $(FLAGS) -c $< -o $@ $(TARGET).elf: obj/start.o obj/main.o imx6ull.lds $(CC) $(FLAGS) -static -nostdlib -Wl,-T,imx6ull.lds \ -Wl,-Map,$(TARGET).map -o $@ obj/start.o obj/main.o -lgcc $(TARGET).bin: $(TARGET).elf $(OBJCOPY) -O binary $< $@ $(TARGET).dis: $(TARGET).elf $(OBJDUMP) -D $< > $@ clean: rm -f obj/start.o obj/main.o $(TARGET).elf $(TARGET).bin \ $(TARGET).dis $(TARGET).map在安装 GNU ARM 交叉编译器与 GNU Make 的环境中执行make,会得到i2c.bin、i2c.elf和反汇编文件i2c.dis。如果工具链前缀是arm-none-eabi-,可用make CROSS_COMPILE=arm-none-eabi-。i2c.bin是裸程序镜像,不是包含 i.MX6ULL 上电引导头和 DDR 初始化信息的完整 SD 启动镜像;执行它的前提已经在本节开头说明。
六、如何判断结果与定位问题
启动后,程序尝试写入一次。若从地址、字地址到六个数据字节都得到 ACK,且 STOP 后总线恢复空闲,板上 LED 点亮。若 EEPROM 未连接,LED 应保持熄灭;这只是状态提示,不能证明掉电后数据已保存。EEPROM 在 STOP 后最多仍可能需要约 10 ms 完成内部写入,若要立即发起下一笔读/写,应等待其完成或用设备地址反复探测 ACK。
| 现象 | 首先检查 |
|---|---|
| SCL、SDA 一直高,LED 不亮 | I2C1 焊盘复用、输入选择、CCGR2 门控、程序是否真的运行 |
发出0xA0后看到 NACK | EEPROM 是否上电、GND 是否共地、A2 是否为 0、是否选对 7 位地址,以及 EEPROM 是否仍在内部写入周期 |
| SDA/SCL 一直低 | 是否短路、错误的 5 V/3.3 V 上拉、某个设备持续拉低、焊盘开漏设置 |
| 地址 ACK、数据阶段 NACK | 线路与供电是否稳定、外设是否确实按这套写入时序工作;页边界问题通常表现为内容回绕,并非 NACK |
| 分析仪显示速率与计算不符 | PERCLK 是否真来自 24 MHz,IFDR是否为0x0F,上拉与线路电容是否使上升沿过慢 |
| LED 亮,但随后读出的内容不对 | EEPROM 内部写入是否完成、WP是否为低电平、是否读同一存储块和字地址、是否跨页回绕;WP禁止写入时仍可能应答总线字节 |
读 EEPROM 时,常见的“指定地址读”会先发START → 0xA0 → 字地址,不发 STOP,而是发重复 START,接着发0xA1,然后切换主机接收方向。最后一个数据字节后由主机发送 NACK,再发 STOP。这说明0xA0和0xA1只是同一颗设备的写/读方向字节,也说明RSTA、MTX、TXAK在读路径中各有用途;本文写入程序只需要前一种方向。
当你看到I2DR=0xA0、随后IIF=1且RXAK=0,可以把它直接翻译成一句话:**主机把“我要写地址为 0x50 的设备”送上总线,对方在第 9 个时钟回应了 ACK。**后面的字地址和六个数据字节重复同样的“发送—等待—检查”过程,最后清除MSTA才构成完整的一次 I2C 写入。