AT24C256 EEPROM 驱动详解:从 I2C 协议到嵌入式工程实战
2026/8/17 18:38:20 网站建设 项目流程

1. 引言

在嵌入式系统中,非易失性存储始终是不可或缺的一环。无论是保存设备序列号、校准参数、用户配置,还是记录运行日志,都需要一块掉电后仍然保留数据的存储器。Flash 虽然容量大、成本低,但擦写寿命有限、写入粒度粗、操作复杂度高;FRAM 虽然性能优秀,但价格昂贵、供应渠道相对有限。因此,基于 EEPROM 的存储方案在中低容量、频繁写入、需要字节级擦写的场景中仍然占据重要地位。

AT24C256 是 Microchip 公司 AT24C 系列 EEPROM 中的一款大容量器件,存储容量为 256 Kbit,即 32 KByte。它采用标准 I2C 总线接口,支持字节写、页写、随机读、顺序读等多种访问方式,写周期短、功耗低、接口简单,非常适合集成到各类单片机系统中。本文将从芯片特性、引脚定义、I2C 协议基础、内存组织、读写时序、驱动设计、代码实现、移植适配、错误处理以及常见问题排查等多个维度,对 AT24C256 的驱动进行系统化、工程化的详细讲解,力求让读者不仅会调用现成驱动,更能够深入理解其工作原理,并在实际项目中灵活、可靠地使用它。

阅读本文前,建议读者具备基本的 C 语言编程能力和一定的单片机外设使用经验。文中代码示例以 STM32 平台和标准 C 语言为主,同时也给出 Linux I2C 子系统的驱动示例,方便不同平台的开发者理解和迁移。

2. AT24C256 芯片概述

AT24C256 属于 AT24C 系列串行 EEPROM,该系列器件通过两线制 I2C 总线与主机通信,广泛应用于工业控制、消费电子、医疗设备、通信设备等领域。AT24C256 的内部存储阵列被组织成 512 页,每页 64 字节,总共 32768 字节。该器件支持 1.7 V 至 5.5 V 的宽电压工作范围,部分型号还支持 1.8 V、2.5 V、3.3 V、5 V 等多种标准电压等级,方便在不同电平系统中使用。

2.1 主要特性

  • 存储容量:256 Kbit,即 32768 字节。
  • 页大小:64 字节,一次页写最多可以写入一页。
  • 接口类型:标准 I2C,支持 100 kHz、400 kHz、1 MHz 时钟频率。
  • 工作电压:常见型号为 1.7 V ~ 5.5 V,具体以数据手册为准。
  • 擦写寿命:典型值超过 100 万次擦写循环。
  • 数据保持时间:大于 100 年。
  • 硬件写保护:WP 引脚接地时禁止写入,悬空或接高电平时允许写入。
  • 设备级联:通过 A0、A1、A2 三个地址引脚最多支持 8 个器件并联在同一总线上。
  • 工作温度范围:工业级一般为 -40 ℃ 至 +85 ℃。
  • 封装形式:常见的有 SOIC-8、TSSOP-8、DIP-8 等。

2.2 容量与地址空间

AT24C256 的容量为 32 KByte,因此需要 15 位地址线才能覆盖整个地址空间。在 I2C 通信中,地址被拆分为两个字节发送:首先发送高 7 位地址,然后发送低 8 位地址。这一点是 AT24C256 与 AT24C01、AT24C02 等小容量 EEPROM 的重要区别。小容量器件通常只需要一个字节地址,而 AT24C256 必须发送两个地址字节,否则无法访问整个 32 K 空间。驱动人员在编写代码时必须注意这一差异,否则会导致地址错位、读写失败等问题。

2.3 引脚定义

AT24C256 常见 SOP-8 封装的引脚排列如下:

  • A0、A1、A2:器件地址输入引脚。这三个引脚的电平决定器件 I2C 地址中的低三位,用于多片并联时的片选。
  • GND:电源地。
  • SDA:I2C 串行数据线,双向,需外接上拉电阻。
  • SCL:I2C 串行时钟线,输入,需外接上拉电阻。
  • WP:写保护输入。接 GND 时禁止写入;悬空或接 VCC 时允许正常写入。
  • VCC:电源正极。

在实际设计中,A0、A1、A2 引脚不能浮空,必须明确接地或接电源,否则器件地址不确定,可能导致总线冲突或通信异常。WP 引脚如果不使用写保护功能,可以接地或通过电阻接电源,但不要悬空;若直接接地,则会禁止写入,使用前务必确认硬件连接。

3. I2C 总线基础

AT24C256 使用 I2C 总线进行数据收发,因此驱动开发前必须掌握 I2C 的物理层和协议层知识。I2C 是一种半双工、同步、多主多从的串行总线,由飞利浦公司发明,后被广泛应用。它只需要两条线:串行数据线 SDA 和串行时钟线 SCL,所有器件通过开漏或集电极开路方式连接到总线上,并通过上拉电阻将总线空闲电平拉高。

3.1 I2C 物理层特征

I2C 总线上每个器件都有唯一地址,主机通过发送地址来选择从机。总线空闲时,SCL 和 SDA 均为高电平。数据传输时,SDA 上的数据只有在 SCL 为低电平时才允许变化;当 SCL 为高电平时,SDA 的电平必须保持稳定。如果 SCL 为高电平期间 SDA 发生变化,则会被解释为起始条件或停止条件。这种设计保证了数据采样的可靠性和协议同步的准确性。

上拉电阻的阻值选择需要根据总线电容、工作频率和供电电压综合考虑。阻值过大会导致上升沿缓慢,限制最高通信速率;阻值过小会增加功耗,甚至可能因驱动能力不足而无法拉低总线。通常 3.3 V 系统中常用 4.7 kΩ 至 10 kΩ,5 V 系统中常用 2.2 kΩ 至 4.7 kΩ。总线挂载器件较多或走线较长时,应适当减小上拉电阻,并尽量降低分布电容。

3.2 I2C 协议层:起始、停止、应答

I2C 通信由主机发起,一次完整的数据传输包含以下基本元素:

  • 起始条件 START:SCL 为高电平期间,SDA 从高电平跳变到低电平。
  • 停止条件 STOP:SCL 为高电平期间,SDA 从低电平跳变到高电平。
  • 应答 ACK:接收方在成功接收一个字节后,在第 9 个时钟脉冲期间将 SDA 拉低,表示应答。
  • 非应答 NACK:接收方在第 9 个时钟脉冲期间保持 SDA 为高电平,表示非应答。主机发送 NACK 通常用于终止从机发送,或表示接收结束。

每个字节传输完成后,发送方释放 SDA,由接收方控制 SDA 电平来产生 ACK 或 NACK。主机向从机写入数据时,从机接收完每个字节后会产生 ACK;主机读取数据时,主机接收完一个字节后,除最后一个字节外都应产生 ACK,最后一个字节则发送 NACK 以通知从机停止发送,然后主机产生停止条件。

在软件模拟 I2C 时,起始、停止、发送字节、接收字节等基本操作必须严格遵循上述时序。硬件 I2C 外设则通常由硬件自动完成这些操作,但仍需要正确配置时序参数。

3.3 设备地址与读写位

I2C 设备地址由 7 位地址位和 1 位读写位组成,构成一个完整字节。AT24C256 的 7 位设备地址高 4 位固定为1010,低 3 位由 A2、A1、A0 引脚电平决定。因此,当 A2、A1、A0 全部接地时,7 位地址为0x50。读写位为 1 表示读操作,为 0 表示写操作。实际发送时,将 7 位地址左移 1 位,再与读写位组合成一个字节:写操作为0xA0,读操作为0xA1

以 STM32 HAL 库为例,调用HAL_I2C_Mem_Write()HAL_I2C_Mem_Read()时,设备地址参数通常直接传入 7 位地址,如0x50,由 HAL 库内部自动左移并添加读写位;也有部分平台要求传入已经左移的 8 位地址,移植时务必查看对应库的说明。

4. AT24C256 内存组织与地址寻址

AT24C256 的内部存储空间为 32768 字节,地址范围为0x00000x7FFF。存储阵列被划分为 512 页,每页 64 字节,即地址低 6 位表示页内偏移,高 9 位表示页地址。

页是 AT24C256 写操作的基本约束单位。当执行页写时,写入的数据不能跨越页边界:即从某个页内地址开始写,写到该页末尾(页内偏移达到 63)后,地址会自动回卷到本页起始地址,而不是进入下一页。这一行为称为“页地址回卷”。例如,从地址0x003E开始执行 4 字节页写,数据会依次写入0x003E0x003F0x00000x0001,而不是0x003E0x003F0x00400x0041。驱动程序如果忽略了页边界,就会导致数据被错误覆盖。因此,在封装页写函数时,要么强制调用方传入页对齐的长度,要么在函数内部自动拆分为多个不跨页的写操作。

读操作没有页边界限制,可以从任意地址开始顺序读,连续读取时会自动递增地址,超过0x7FFF后回卷到0x0000。顺序读功能非常适合整片数据备份或批量读取,可以显著减少总线交互次数,提高读取效率。

5. 写操作详解

AT24C256 的写操作分为字节写和页写两种模式。所有写操作完成后,芯片都会进入内部写周期,将数据从内部页缓冲器固化到非易失性存储单元。内部写周期期间,芯片不响应新的指令,主机必须通过查询应答或延时方式等待写周期完成。

5.1 字节写

字节写用于向指定地址写入一个字节的数据。一次完整的字节写时序如下:

  1. 主机发送起始条件 START。
  2. 主机发送器件地址字节0xA0,等待从机 ACK。
  3. 主机发送地址高字节,即目标地址的高 7 位,等待从机 ACK。
  4. 主机发送地址低字节,即目标地址的低 8 位,等待从机 ACK。
  5. 主机发送要写入的一个数据字节,等待从机 ACK。
  6. 主机发送停止条件 STOP。

停止条件发出后,AT24C256 启动内部写周期,将缓冲器中的数据写入存储阵列。写周期典型值为 5 ms,最大 10 ms。在内部写周期内,器件不会响应 I2C 通信,也不会产生 ACK。因此,每次写操作之后必须等待写周期结束,再进行下一次写操作或读操作,否则数据可能写入失败。常见的等待方式有两种:一是直接延时 5 ~ 10 ms,二是在循环中发送起始条件和写地址字节,检测是否收到 ACK,一旦收到 ACK 表示内部写周期结束。第二种方式称为“应答查询”,比较灵活,可以在写周期较短时减少等待时间。

5.2 页写

页写允许在一次 I2C 事务中连续写入最多 64 字节,前提是写入数据不能跨越页边界。页写的时序与字节写类似,只是在发送完地址后连续发送多个数据字节,每个字节都会收到从机 ACK,最后主机发送停止条件。

典型的页写时序如下:

  1. 主机发送起始条件 START。
  2. 主机发送器件地址字节0xA0,等待 ACK。
  3. 主机发送地址高字节,等待 ACK。
  4. 主机发送地址低字节,等待 ACK。
  5. 主机依次发送数据字节 0、数据字节 1,直到数据字节 N-1,每个字节等待 ACK。
  6. 主机发送停止条件 STOP。

需要注意的是,页写过程中的地址低字节会在页内自动递增,但不会跨页。页内地址从0x00增加到0x3F后,下一个地址会回卷到0x00。因此,页写的起始地址和写入长度必须保证不会跨越页边界。若需要写入的数据跨越页边界,应将其拆分为两次或多次页写,分别写入不同页。

5.3 写周期与写保护

写周期是 EEPROM 内部编程过程所需的时间,与写入字节数无关,只与器件特性有关。AT24C256 的写周期典型值为 5 ms,最大值为 10 ms。在写周期内,器件不会提供 ACK,主机若尝试通信会失败。因此,驱动设计中必须在每次写操作后正确等待写周期结束,否则后续操作可能丢失。

硬件写保护通过 WP 引脚实现。当 WP 引脚接地时,内部写使能电路被禁止,任何写操作都不会生效;当 WP 引脚接 VCC 或悬空时,写操作正常。若系统不需要硬件写保护,可将 WP 引脚通过电阻接到 VCC 或直接接地后通过软件逻辑控制。注意,如果 WP 直接接地,而驱动代码却尝试写入数据,将无法写入成功,且通常不会产生异常提示,排查时容易忽略硬件连接。

6. 读操作详解

AT24C256 支持三种读操作模式:当前地址读、随机读和顺序读。读操作不涉及内部写周期,因此可以连续执行,速度较快。

6.1 当前地址读

当前地址读用于读取 EEPROM 内部地址计数器当前指向地址的数据。内部地址计数器会在每次读写操作后自动递增,并在复位或上电时清零。当前地址读的时序为:先发送起始条件,再发送器件地址字节并设置读写位为 1,即0xA1,然后从机返回当前地址存储的数据,主机收到数据后发送 NACK,最后发送停止条件。

当前地址读适用于地址已知且连续读取的场景,但在实际驱动中应用较少,更多使用随机读来明确指定起始地址。

6.2 随机读

随机读允许主机读取任意指定地址处的数据。由于 EEPROM 的地址计数器需要先被设置为目标地址,随机读通常由一个虚拟写操作加一个读操作组成。时序如下:

  1. 主机发送起始条件 START。
  2. 主机发送器件地址字节0xA0,等待 ACK。
  3. 主机发送地址高字节,等待 ACK。
  4. 主机发送地址低字节,等待 ACK。
  5. 主机再次发送起始条件 START,这称为重复起始条件。
  6. 主机发送器件地址字节0xA1,等待 ACK。
  7. 从机发送目标地址数据。
  8. 主机发送 NACK,表示读取结束。
  9. 主机发送停止条件 STOP。

随机读是驱动中最常用的读函数,用来读取单个字节或作为顺序读的起始操作。多数 I2C 硬件外设和库函数都封装了随机读,例如 STM32 HAL 的HAL_I2C_Mem_Read()可以一次性完成“发送地址 + 读取数据”的过程。使用硬件库时,不必手动发送重复起始条件,但要确保传入正确的内存地址大小,因为 AT24C256 需要使用两个地址字节。

6.3 顺序读

顺序读可以在随机读之后连续读取多个字节,地址自动递增。完成随机读的地址设置后,主机连续发送 ACK 接收数据,直到最后一个字节前主机发送 NACK,最后发送停止条件。顺序读没有页边界限制,可以一次读取任意长度,地址越过0x7FFF后会回卷到0x0000

顺序读非常适合读取连续配置数据或整片备份。在驱动中,通常将随机读和顺序读合并为一个函数:先设置起始地址,然后连续读取指定长度的数据,内部自动处理 ACK 和 NACK。这样可以大幅减少总线事务次数,提升读取吞吐量。

7. 典型驱动实现

AT24C256 的驱动实现方式主要分为三类:软件模拟 I2C、硬件 I2C 外设加库函数、操作系统内核驱动。下面分别给出典型实现,重点展示软件模拟 I2C 和 STM32 HAL 库两种方式,并简要介绍 Linux 内核下的驱动模型。

7.1 软件模拟 I2C 驱动

软件模拟 I2C 不依赖硬件 I2C 外设,通过普通 GPIO 翻转来产生时序,适用于引脚资源受限或硬件 I2C 不够稳定的场景。模拟 I2C 的代码可移植性强,但会占用 CPU 资源,通信速率受主频影响。以下代码基于伪 GPIO 操作,假设已经实现了I2C_SDA_HIGHI2C_SDA_LOWI2C_SCL_HIGHI2C_SCL_LOWI2C_SDA_READ等宏。

/* 软件 I2C 起始条件 */ void i2c_start(void) { I2C_SDA_HIGH(); I2C_SCL_HIGH(); delay_us(2); I2C_SDA_LOW(); delay_us(2); I2C_SCL_LOW(); } /* 软件 I2C 停止条件 */ void i2c_stop(void) { I2C_SDA_LOW(); I2C_SCL_HIGH(); delay_us(2); I2C_SDA_HIGH(); delay_us(2); } /* 发送一个字节,返回从机 ACK 状态:0 表示 ACK,1 表示 NACK */ uint8_t i2c_send_byte(uint8_t data) { uint8_t i; for (i = 0; i < 8; i++) { if (data & 0x80) I2C_SDA_HIGH(); else I2C_SDA_LOW(); delay_us(1); I2C_SCL_HIGH(); delay_us(2); I2C_SCL_LOW(); delay_us(1); data <<= 1; } /* 释放 SDA,读取 ACK */ I2C_SDA_HIGH(); delay_us(1); I2C_SCL_HIGH(); delay_us(2); uint8_t ack = I2C_SDA_READ(); I2C_SCL_LOW(); delay_us(1); return ack; } /* 读取一个字节,ack=0 表示主机回复 ACK,ack=1 表示主机回复 NACK */ uint8_t i2c_read_byte(uint8_t ack) { uint8_t i, data = 0; I2C_SDA_HIGH(); for (i = 0; i < 8; i++) { delay_us(1); I2C_SCL_HIGH(); delay_us(2); data <<= 1; if (I2C_SDA_READ()) data |= 0x01; I2C_SCL_LOW(); delay_us(1); } /* 主机发送 ACK 或 NACK */ if (ack) I2C_SDA_HIGH(); else I2C_SDA_LOW(); delay_us(1); I2C_SCL_HIGH(); delay_us(2); I2C_SCL_LOW(); I2C_SDA_HIGH(); return data; }

基于上述基本操作,可以实现 AT24C256 的字节写和页写。页写函数内部需要判断是否跨页,如果跨页则拆分为多次写入。

#define AT24C256_ADDR_W 0xA0 #define AT24C256_ADDR_R 0xA1 #define AT24C256_PAGE_SIZE 64 /* 等待内部写周期结束:通过应答查询方式 */ uint8_t at24c256_wait_ready(void) { uint16_t timeout = 2000; uint8_t ack; do { i2c_start(); ack = i2c_send_byte(AT24C256_ADDR_W); i2c_stop(); if (--timeout == 0) return 1; /* 超时未就绪 */ } while (ack != 0); return 0; /* 就绪 */ } /* 字节写:向指定 16 位地址写一个字节 */ uint8_t at24c256_byte_write(uint16_t addr, uint8_t data) { i2c_start(); if (i2c_send_byte(AT24C256_ADDR_W)) { i2c_stop(); return 1; /* 器件无应答 */ } if (i2c_send_byte((uint8_t)(addr >> 8))) { i2c_stop(); return 1; } if (i2c_send_byte((uint8_t)(addr & 0xFF))) { i2c_stop(); return 1; } if (i2c_send_byte(data)) { i2c_stop(); return 1; } i2c_stop(); return at24c256_wait_ready(); } /* 页写:向指定地址写入最多 64 字节,函数内部自动处理跨页拆分 */ uint8_t at24c256_page_write(uint16_t addr, const uint8_t *buf, uint16_t len) { uint16_t cur; uint8_t page_remain; while (len > 0) { page_remain = AT24C256_PAGE_SIZE - (addr & (AT24C256_PAGE_SIZE - 1)); cur = (len > page_remain) ? page_remain : len; i2c_start(); if (i2c_send_byte(AT24C256_ADDR_W)) { i2c_stop(); return 1; } if (i2c_send_byte((uint8_t)(addr >> 8))) { i2c_stop(); return 1; } if (i2c_send_byte((uint8_t)(addr & 0xFF))) { i2c_stop(); return 1; } while (cur--) { if (i2c_send_byte(*buf++)) { i2c_stop(); return 1; } addr++; } i2c_stop(); if (at24c256_wait_ready()) return 1; len -= (uint16_t)(addr - (addr - cur)); /* 实际消耗长度 */ /* 注意:cur 在上面的循环中已递减,这里使用循环前保存的长度 */ } return 0; }

上面页写函数中循环后len的计算逻辑有误,正确做法应在循环前保存本次写入长度,然后更新lenaddr。为便于理解,这里给出更清晰的版本:

uint8_t at24c256_write(uint16_t addr, const uint8_t *buf, uint16_t len) { uint16_t write_len; uint8_t page_remain; while (len > 0) { page_remain = AT24C256_PAGE_SIZE - (addr & (AT24C256_PAGE_SIZE - 1)); write_len = (len > page_remain) ? page_remain : len; i2c_start(); if (i2c_send_byte(AT24C256_ADDR_W) || i2c_send_byte((uint8_t)(addr >> 8)) || i2c_send_byte((uint8_t)(addr & 0xFF))) { i2c_stop(); return 1; } for (uint16_t i = 0; i < write_len; i++) { if (i2c_send_byte(buf[i])) { i2c_stop(); return 1; } } i2c_stop(); if (at24c256_wait_ready()) return 1; buf += write_len; addr += write_len; len -= write_len; } return 0; }

读操作则可以封装为随机读和顺序读的组合函数,一次读取任意长度数据。

/* 随机读起始地址设置 */ uint8_t at24c256_set_addr(uint16_t addr) { i2c_start(); if (i2c_send_byte(AT24C256_ADDR_W) || i2c_send_byte((uint8_t)(addr >> 8)) || i2c_send_byte((uint8_t)(addr & 0xFF))) { i2c_stop(); return 1; } return 0; /* 注意:此处不应发送停止条件,后面接着重复起始 */ } /* 从指定地址读取 len 字节到 buf */ uint8_t at24c256_read(uint16_t addr, uint8_t *buf, uint16_t len) { if (len == 0) return 0; /* 设置起始地址 */ i2c_start(); if (i2c_send_byte(AT24C256_ADDR_W) || i2c_send_byte((uint8_t)(addr >> 8)) || i2c_send_byte((uint8_t)(addr & 0xFF))) { i2c_stop(); return 1; } /* 重复起始并进入读模式 */ i2c_start(); if (i2c_send_byte(AT24C256_ADDR_R)) { i2c_stop(); return 1; } for (uint16_t i = 0; i < len; i++) { if (i == len - 1) buf[i] = i2c_read_byte(1); /* 最后一个字节发送 NACK */ else buf[i] = i2c_read_byte(0); /* 其它字节发送 ACK */ } i2c_stop(); return 0; }

7.2 STM32 HAL 库驱动

STM32 系列微控制器一般配备硬件 I2C 外设,使用 HAL 库可以简化时序处理。AT24C256 作为标准 I2C 存储芯片,HAL 库的HAL_I2C_Mem_Write()HAL_I2C_Mem_Read()已经覆盖了设备地址、内存地址、数据缓冲区等参数,并自动处理起始、停止、应答等时序。需要注意的是,AT24C256 的内存地址宽度为 16 位,因此MemAddress参数传入 16 位地址,MemAddSize参数必须为I2C_MEMADD_SIZE_16BIT

下面给出基于 STM32 HAL 库的 AT24C256 驱动实现。假设 I2C 外设句柄为hi2c1,设备 7 位地址为0x50

#include "main.h" #include "i2c.h" #define AT24C256_ADDR (0x50 << 1) /* 8 位地址形式,写为 A0,读为 A1 */ #define AT24C256_I2C &hi2c1 #define AT24C256_TIMEOUT 100 /* 字节写 */ uint8_t at24c256_byte_write(uint16_t addr, uint8_t data) { HAL_StatusTypeDef status; status = HAL_I2C_Mem_Write(AT24C256_I2C, AT24C256_ADDR, addr, I2C_MEMADD_SIZE_16BIT, &data, 1, AT24C256_TIMEOUT); return (status == HAL_OK) ? 0 : 1; } /* 页写,自动拆分跨页 */ uint8_t at24c256_write(uint16_t addr, const uint8_t *buf, uint16_t len) { uint16_t write_len; uint8_t page_remain; HAL_StatusTypeDef status; while (len > 0) { page_remain = 64 - (addr & 0x3F); write_len = (len > page_remain) ? page_remain : len; status = HAL_I2C_Mem_Write(AT24C256_I2C, AT24C256_ADDR, addr, I2C_MEMADD_SIZE_16BIT, (uint8_t *)buf, write_len, AT24C256_TIMEOUT); if (status != HAL_OK) return 1; /* 等待内部写周期完成 */ HAL_Delay(5); buf += write_len; addr += write_len; len -= write_len; } return 0; } /* 任意地址读 */ uint8_t at24c256_read(uint16_t addr, uint8_t *buf, uint16_t len) { HAL_StatusTypeDef status; status = HAL_I2C_Mem_Read(AT24C256_I2C, AT24C256_ADDR, addr, I2C_MEMADD_SIZE_16BIT, buf, len, AT24C256_TIMEOUT); return (status == HAL_OK) ? 0 : 1; }

上述代码中设备地址传入了0x50 << 1,即0xA0形式的 8 位地址。这是 HAL 库HAL_I2C_Mem_Write/Read的规范用法:DevAddress参数需要传入已经左移 1 位的 8 位地址。很多初学者容易在这一处出错,直接传入0x50会导致无法寻址到器件。此外,HAL_I2C_Mem_Read在读取小于等于 2 字节时使用的是标准读写流程,读取多个字节时会自动使用连续读模式,并在最后一个字节后发送 NACK,不需要手动干预。

在实际项目中,为了提高写操作效率,常用“应答查询”替代固定延时。HAL 库可以借助HAL_I2C_IsDeviceReady()函数检测器件是否就绪。该函数会尝试发送设备地址并检查应答,直到器件返回 ACK 或超时。上述代码直接使用HAL_Delay(5),虽然简单可靠,但会阻塞 CPU,时间敏感场景可以替换为如下实现:

uint8_t at24c256_wait_ready(void) { if (HAL_I2C_IsDeviceReady(AT24C256_I2C, AT24C256_ADDR, 100, AT24C256_TIMEOUT) == HAL_OK) { return 0; } return 1; }

7.3 Linux I2C 子系统驱动

在 Linux 系统中,AT24C256 通常由内核自带的at24驱动程序支持,用户空间可以通过 sysfs、eeprom 设备节点或 I2C 用户态工具访问。Linux 的 I2C 子系统将设备抽象为struct i2c_client,驱动通过i2c_transfer()i2c_smbus_*()系列函数进行读写。

如果需要在 Linux 用户空间直接驱动 AT24C256,可以使用ioctli2c-dev接口。下面是一个简单的用户态读取示例:

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> int main(void) { int file; char filename[20] = "/dev/i2c-1"; uint8_t data[32]; uint16_t addr = 0x0000; file = open(filename, O_RDWR); if (file < 0) { perror("open i2c device"); return 1; } /* 指定从机地址 */ if (ioctl(file, I2C_SLAVE, 0x50) < 0) { perror("ioctl I2C_SLAVE"); close(file); return 1; } /* 写入 2 字节内存地址 */ uint8_t mem_addr[2] = { (uint8_t)(addr >> 8), (uint8_t)(addr & 0xFF) }; if (write(file, mem_addr, 2) != 2) { perror("write memory address"); close(file); return 1; } /* 读取 32 字节 */ if (read(file, data, sizeof(data)) != sizeof(data)) { perror("read data"); close(file); return 1; } for (int i = 0; i < sizeof(data); i++) printf("%02X ", data[i]); printf("\n"); close(file); return 0; }

Linux 内核态驱动通常使用i2c_transfer()组合多条消息完成一次带内存地址的读写操作。AT24C256 需要两条消息:第一条为写,携带 2 字节内存地址;第二条为读,接收数据。这种写法灵活且高效,适合嵌入式 Linux 系统下的驱动开发。

8. 驱动移植与适配

AT24C256 驱动在不同平台间移植时,主要关注 I2C 底层接口的抽象。良好的驱动设计应将总线操作和器件操作分离:总线操作负责起始、停止、发送字节、接收字节或使用硬件库的读写函数;器件操作负责组装地址、数据、应答判断、写周期等待等逻辑。这样在更换 MCU 或操作系统时,只需重新实现总线操作层,器件逻辑可以保持不变。

常见的抽象接口可以定义如下:

typedef struct { uint8_t (*write_bytes)(uint16_t dev_addr, uint8_t *data, uint16_t len); uint8_t (*read_bytes)(uint16_t dev_addr, uint8_t *data, uint16_t len); uint8_t (*wait_ready)(uint16_t dev_addr); } eeprom_bus_ops_t;

上层驱动调用bus_ops中的函数即可,不关心底层是软件模拟还是硬件 I2C。例如,基于该抽象的页写函数与平台无关,只需调用write_bytes()wait_ready()。这种分层设计在量产项目中非常实用,可以显著降低不同硬件平台移植的工作量。

在移植过程中,还要特别注意以下事项:

  • 确认目标平台 I2C 设备地址的表示方式:7 位地址还是 8 位地址。
  • 确认内存地址宽度参数:AT24C256 必须使用 16 位地址。
  • 确认 I2C 时钟频率:400 kHz 通常稳定,1 MHz 需保证总线电容和上拉电阻合适。
  • 如果使用 DMA 或中断方式,注意数据缓冲区的生命周期,避免函数返回后缓冲区被释放或修改。
  • 在多任务系统中,需要添加互斥锁保护 I2C 总线,防止多个任务同时访问。

9. 错误处理与可靠性设计

EEPROM 驱动在实际运行中可能遇到多种异常情况:总线忙、器件无应答、数据校验失败、写周期超时、供电异常等。完善的驱动应当进行必要的错误检测和恢复处理,并预留校验接口。

9.1 应答检测与超时

每次发送地址或数据后,都应检查 ACK。若从机无应答,可能原因包括:设备未上电、地址错误、总线被拉低、写周期未结束等。驱动应返回明确错误码,并设置合理超时,避免总线操作无限等待。示例中的at24c256_wait_ready()就有超时机制,防止程序死循环。

9.2 数据校验

EEPROM 写入后,推荐读回校验,确保数据正确。尤其是在执行重要配置升级时,校验是必要步骤。可以封装成以下函数:

uint8_t at24c256_write_verified(uint16_t addr, const uint8_t *buf, uint16_t len) { uint8_t tmp[128]; if (at24c256_write(addr, buf, len)) return 1; if (at24c256_read(addr, tmp, len)) return 1; for (uint16_t i = 0; i < len; i++) { if (tmp[i] != buf[i]) return 1; } return 0; }

9.3 掉电保护与数据备份

EEPROM 掉电写入可能导致最后写入的数据损坏或丢失。对于关键参数,建议采用双备份机制:将数据存储到两个不同区域,写入时先写备份区再写主区,读取时校验主区,失败则切换到备份区。同时,尽量避免在电源不稳定时进行写操作,必要时使用掉电检测电路,在电压下降到安全阈值前完成写入。

9.4 互斥与重入

在实时操作系统或多任务环境中,I2C 总线是共享资源。多个任务同时操作 EEPROM 会引发总线冲突。驱动应在总线操作入口处加互斥锁,同一时刻只允许一个任务访问。如果使用中断传输,还需要注意中断与任务之间的同步。

10. 常见问题排查

在 AT24C256 使用和调试过程中,以下问题较为常见:

10.1 器件无应答

可能原因:设备地址错误、A0/A1/A2 引脚电平与软件地址不符、上拉电阻缺失或阻值过大、SDA/SCL 连接反接、芯片未供电。可以先用示波器或逻辑分析仪观察总线波形,确认起始条件和地址字节是否正确发出。

10.2 读写数据错位

常见原因是内存地址宽度配置错误。若把 16 位地址当作 8 位地址处理,只有低 8 位地址被发送,高 8 位丢失,导致访问地址错位。务必检查库函数的MemAddSize参数或自行封装的地址发送逻辑。

10.3 页写跨页导致数据覆盖

如果写入数据跨越页边界,超出部分会回卷到页起始地址,造成数据覆盖。解决办法是在写函数中自动按页拆分,或在调用前确保地址和长度对齐页边界。

10.4 写入后立即读取失败

EEPROM 在写操作后进入内部写周期,此时芯片不响应任何通信。若未等待写周期就进行下一次操作,会收到 NACK 或总线错误。应添加写周期等待逻辑,通常使用应答查询方法。

10.5 WP 引脚意外禁止写入

如果 WP 引脚被接地或驱动电路误动作,芯片会进入写保护状态,写操作无效但没有异常提示。排查时应测量 WP 引脚电平。

10.6 通信速率过高导致不稳定

I2C 速率过高、上拉电阻过大或总线电容过大时,可能出现波形畸变、采样错误。可降低 I2C 时钟频率,或减小上拉电阻,并检查 PCB 布线长度。

11. 总结

AT24C256 作为一款大容量串行 EEPROM,凭借其标准 I2C 接口、字节级擦写、低功耗和高可靠性,在嵌入式项目中有着广泛应用。掌握其驱动开发,关键在于理解 I2C 协议、明确 16 位内存地址、正确处理页写边界和内部写周期,并在此基础上构建可移植、健壮的软件接口。

本文详细讲解了 AT24C256 的芯片特性、引脚定义、I2C 通信基础、内存组织、读写时序、软件模拟与硬件库驱动实现,以及移植、错误处理和常见问题排查,涵盖了从原理到工程实践的完整链路。希望通过本文,读者能够独立完成 AT24C256 驱动设计与调试,并在实际项目中灵活应对各种异常场景。

建议读者在实践时,优先使用硬件 I2C 外设提高效率,同时保留一套软件模拟 I2C 作为紧急调试备选。对于关键数据,一定要做好校验和备份,确保系统长期稳定运行。

参考

  • Microchip AT24C256 数据手册
  • I2C-Bus Specification and User Manual
  • STM32 HAL 库用户手册
  • Linux 内核 I2C 子系统文档

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询