018.使用I2C总线EEPROM|千篇笔记实现嵌入式全栈/裸机篇

⚠️裸机仓库:https://gitee.com/simonchina_carel_li/mini2440-bare-metal.git

⚠️Tag: 18-i2c

1. 这次要做什么?

我们要学习使用IIC外设,正巧板载一个IIC总线的从设备AT24C08,

这是一个EEPROM,只有1K的空间,用于存储少量掉电记忆的配置数据,

通过实现其读写功能,学习IIC的使用

2. IIC总线复盘

2.1 物理层面

I2C 包含 2 条线路:

  • 1 条为 SCL(串行时钟)

  • 1 条为 SDA(串行数据)

单次数据传输包含 9 个时钟脉冲,用于驱动 8 位数据和 1 位 ACK/NACK

数据传输帧包含 1 个 START 和 1 个 STOP 条件

地址类型传输的启动顺序为:

  • 1 个 START 条件,接 1 个 7 位/10 位地址、1 个 1 位 R/~W

  • 等待1 个 1 位 ACK/NACK

  • 数据类型传输包含 8 位数据和 1 位 ACK/NACK

I2C 总线条件

  • 启动 (Start) 条件 - 在 SDA 上执行从高到低转换时,SCL 线路应处于高位。

  • 停止 (Stop) 条件 - 在 SDA 上执行从低到高转换时,SCL 线路应处于高位。

  • 数据有效性 - 当 SCL 处于高位状态时,SDA 线路上的数据有效。

  • 数据变更 - 当 SCL 处于低位状态时,在 SDA 线路上发生数据变更。

  • 总线繁忙 - 处于 START 与 STOP 条件之间时,总线处于繁忙状态。

  • ACK - 在 SCL 的第 9 次时钟脉冲时,SDA 应处于低位

  • NACK - 在 SCL 处于第 9 次时钟脉冲时,SDA 应处于高位

2.2 读写

2.2.1 主器件写 (Master Write) 传输

  • 操作从 START 条件开始,后接 7 位/10 位从器件地址和 1 位写操作(等于 0)

  • 成功的从器件寻址应由从器件应答 (ACK)

  • 之后,主器件启动到从器件的数据写入,从器件将在响应中提供 N-1 字节的 ACK

  • 当 N-1 字节完成传输后,主器件会在第 N 字节传输上发送 Not Acknowledged (NACK) 以生成 STOP 条件

2.2.2 主器件读 (Master Read) 传输

  • 操作从 START 条件开始,后接 7 位/10 位从器件地址和 1 位读操作(等于 1)

  • 成功的从器件寻址应由从器件应答 (ACK)

  • 后续,从器件会向主器件发送数据,主器件将在响应中提供 N-1 字节的 ACK

  • 当主器件收到 N-1 字节后,它会在第 N 字节传输上发送 NACK 以生成 STOP 条件

  • 主器件执行的从器件寻址操作失败将导致总线上出现 NACK,故而将不启动数据读取,并生成 STOP 条件

3. AT24C08复盘和方案设计

都写在表格里面吧,

说明/方案
容量 1024 个字节
通信 I2C 总线
设备地址 格式如下:1 0 1 0 A2 P1 P0 R/W
  • 1 0 1 0:这是 EEPROM 固定的前缀
  • A2 这里接地了
  • P1 和 P0:这就是内存页地址位,也就是 10 位数据地址里的最高两位(bit 9 和 bit 8)
  • R/W:读写控制位。1 代表读,0 代表写|
    |页大小(Page Size)|支持 16 字节的页写模式 。这意味着你一次连续最多可以往里面写 16 个字节|
    |硬件连接|SCL (时钟线):连接到了 S3C2440 的 GPE14 引脚 (IICSCL/GPE14)
    SDA (数据线):连接到了 S3C2440 的 GPE15 引脚 (IICSDA/GPE15) 。并且总线上已经有了上拉电阻|
    |设备寻址| 1024 个字节需要 10 位的地址才能全部寻址( 2 10 = 1024 2^{10}=1024 210=1024) ,但 I2C 每次只能发 8 位数据字地址
    它的解决方式:AT24C08 把高 2 位地址藏在了设备地址(Device Address)里|
    |写操作|- 发送设备地址(带 P1、P0,写模式)
  • 发送低 8 位数据地址
  • 发 1 个字节数据 (如果页写就是最多发送16字节数据,注意边界)
  • 发送 Stop 信号
  • 写等待时间(最大需要 10ms)|
    |读操作|- 发送设备地址(带 P1、P0,注意还是写模式
  • 发送低 8 位数据地址
  • 重新发一个 Start 信号和设备地址(读模式,R/W=1)
  • 读字节数据(可以连续读,如果没读完回ACK,读完回NACK,读不用注意页的边界
  • 发送 Stop 信号|

4. 代码实现

关于S3C2440 SOC和片上IIC外设如何使用,其实也非常简单,操作几个寄存器进行配置、收发控制和状态监测就行了,具体的看代码注释和数据手册就行,

新建文件common/i2c.c,

实现配置、字节读写、连续读写功能,

C 复制代码
#include "reg_def.h"
#include "s3c2440a.h"

// 等待 IIC 操作完成 (轮询挂起标志位)
static void i2c_wait_ack(void) 
{
    // 等待 IICCON bit4 (Pending Bit) 置 1
    while (!(IICCON & (1 << 4))); 
}

// IIC 与引脚初始化
void i2c_init(void)
{
    // 配置 GPE14(IICSCL) 和 GPE15(IICSDA) 为 IIC 功能
    GPIO_SET_MODE(GPIOECON, 14, 0b10, 2);
    GPIO_SET_MODE(GPIOECON, 15, 0b10, 2);

    // 配置 IIC 控制器 (IICCON)
    // [7]: 1 = 使能 ACK
    // [6]: 0 = IICCLK = PCLK/16 ( PCLK=50.625MHz,分频后约 3.125MHz)
    // [5]: 1 = 使能 IIC中断
    // [4]: 0 = 清除中断挂起标志
    // [3:0]: 1111 = Tx clock = IICCLK/16 = 3.125MHz/16 ≈ 195.3125kHz (符合 AT24C08 标准模式)
    IICCON = (1 << 7)| (0 << 6) | (1 << 5) | (0 << 4) | 0x0F;

    // 配置 IIC 状态寄存器 (IICSTAT)
    // [4]: 1 = 使能 IIC 数据输出 (Rx/Tx)
    IICSTAT = (1 << 4); 
}

// AT24C08 字节写
void at24c08_write_byte(unsigned short addr, unsigned char data)
{
    // AT24C08 设备地址计算 (将 10 位地址的高 2 位藏进设备地址里)
    // 发送的第一个设备地址字节(8 位)格式如下:1 0 1 0 A2 P1 P0 R/W 。
    // 1 0 1 0:这是 EEPROM 固定的前缀 。
    // A2:对应芯片的 A2 硬件引脚电平 。
    // 注意,AT24C08 上的 A0 和 A1 引脚是悬空(NC)的,不起任何作用 。
    // P1 和 P0:这就是内存页地址位,也就是 10 位数据地址里的最高两位(bit 9 和 bit 8) 。
    // R/W:读写控制位。1 代表读,0 代表写 。
    unsigned char dev_addr = 0xA0 | ((addr >> 7) & 0x06); 

    // 1. 写入设备地址 (写模式)
    IICDS = dev_addr;
    IICSTAT = 0xF0;       // Master Tx 模式,产生 Start 信号
    IICCON &= ~(1 << 4);  // 清除 pending 位,启动传输
    i2c_wait_ack();       // 等待设备地址发送完毕并接收 ACK

    // 2. 写入数据字地址 (低8位)
    IICDS = addr & 0xFF;
    IICCON &= ~(1 << 4);  // 清除 pending 位
    i2c_wait_ack();

    // 3. 写入要存储的数据
    IICDS = data;
    IICCON &= ~(1 << 4);
    i2c_wait_ack();

    // 4. 发送 Stop 信号结束传输
    IICSTAT = 0xD0;       // 产生 Stop 信号
    IICCON &= ~(1 << 4);  // 清除 pending 位
    
    // 延时等待:EEPROM 内部烧写需要时间 (最大 10ms)
    easy_delay_ms(10);          
}

// AT24C08 随机读
unsigned char at24c08_read_byte(unsigned short addr)
{
    unsigned char data = 0;
    unsigned char dev_addr = 0xA0 | ((addr >> 7) & 0x06); 

    // ==== 步骤一:伪写(Dummy Write)设置数据地址 ====
    IICDS = dev_addr;     // 写模式的设备地址
    IICSTAT = 0xF0;       // Master Tx 模式,产生 Start 信号
    IICCON &= ~(1 << 4);  
    i2c_wait_ack();

    IICDS = addr & 0xFF;  // 写入数据字地址 (低8位)
    IICCON &= ~(1 << 4);
    i2c_wait_ack();

    // ==== 步骤二:改变方向,正式读取 ====
    IICDS = dev_addr | 0x01;  // 将 R/W 位置 1,变为读模式
    IICSTAT = 0xB0;           // Master Rx 模式,产生 Restart 信号
    IICCON &= ~(1 << 4);
    i2c_wait_ack();

    // 丢弃第一次读取的数据(这只是触发 S3C2440 接收器的假读)
    data = IICDS;            
    
    // 读取真正的数据之前,取消应答 (告诉从机发完这 1 个字节就别发了)
    IICCON &= ~(1 << 7);      // 禁用 ACK
    IICCON &= ~(1 << 4);      // 清除 pending 位,启动真实读取
    i2c_wait_ack();

    data = IICDS;             // 拿到真实数据

    // ==== 步骤三:发送 Stop 信号结束 ====
    IICSTAT = 0x90;           // 产生 Stop 信号
    IICCON &= ~(1 << 4);      // 清除 pending 位
    
    // 恢复 ACK 使能,为下一次正常通信做准备
    IICCON |= (1 << 7);      

    easy_delay_ms(1); // 稍微延时等待总线状态彻底恢复
    return data;
}

// =====================================================================
// 函数名:at24c08_write_page
// 功  能:AT24C08 页写功能(自带页边界安全保护)
// 参  数:addr: 起始数据字地址 (0~1023)
//         buf:  待写入数据缓冲区的指针
//         len:  期望写入的字节数
// 返回值:实际成功写入该页的字节数
// =====================================================================
static unsigned int at24c08_write_page(unsigned short addr, const unsigned char *buf, unsigned int len)
{
    unsigned char dev_addr;
    unsigned int i;
    unsigned int page_remain;

    if (len == 0) return 0;

    // 安全核心:计算当前地址到页尾还有多少字节容量 (AT24C08 页大小为 16 字节)
    page_remain = 16 - (addr % 16);
    
    // 强制截断:单次通信绝不允许跨页写入,防止地址翻转覆盖数据
    if (len > page_remain) {
        len = page_remain; 
    }

    // 计算设备地址,拼入 10 位地址的高 2 位 (P1, P0)
    dev_addr = 0xA0 | ((addr >> 7) & 0x06);

    // 1. 发送设备地址 (写模式)
    IICDS = dev_addr;
    IICSTAT = 0xF0;       // Master Tx 模式,产生 Start 信号
    IICCON &= ~(1 << 4);  // 清除 pending 位,启动
    i2c_wait_ack();

    // 2. 发送目标数据字地址 (低 8 位)
    IICDS = addr & 0xFF;
    IICCON &= ~(1 << 4);
    i2c_wait_ack();

    // 3. 连续发送多个数据字节 (Microcontroller does not send a stop condition here)
    for (i = 0; i < len; i++) {
        IICDS = buf[i];
        IICCON &= ~(1 << 4);
        i2c_wait_ack();
    }

    // 4. 发送 Stop 信号,终止页写序列
    IICSTAT = 0xD0;       // 产生 Stop 信号
    IICCON &= ~(1 << 4);  
    
    // 5. 必须等待内部写入周期完成 (最大 10ms)
    easy_delay_ms(10);         

    // 返回本次实际写入的字节数,方便上层继续发送剩余数据
    return len;
}

// =====================================================================
// 函数名:at24c08_write_buffer
// 功  能:任意长度、任意起始地址的安全连续写入(利用页写榨取最高效率)
// =====================================================================
void at24c08_write_buffer(unsigned short addr, const unsigned char *buf, unsigned int total_len)
{
    unsigned int written_bytes;

    while (total_len > 0) {
        // 调用我们刚才写的页写函数,它会自动告诉你这次写进去了多少个
        written_bytes = at24c08_write_page(addr, buf, total_len);
        
        // 更新指针、地址和剩余长度,准备写下一页
        addr += written_bytes;
        buf += written_bytes;
        total_len -= written_bytes;
    }
}

// =====================================================================
// 函数名:at24c08_read_buffer
// 功  能:AT24C08 任意长度的连续读取 (Sequential Read)
// 参  数:addr: 起始数据字地址 (0~1023)
//         buf:  用于存放读取数据的缓冲区指针
//         len:  期望读取的字节数
// =====================================================================
void at24c08_read_buffer(unsigned short addr, unsigned char *buf, unsigned int len)
{
    unsigned char dev_addr;
    unsigned int i;
    unsigned char dummy;

    if (len == 0) return;

    // 拼装起始设备地址 (携带 10 位地址的高 2 位)
    dev_addr = 0xA0 | ((addr >> 7) & 0x06);

    // ==== 步骤一:伪写(Dummy Write)设置起始数据地址 ====
    IICDS = dev_addr;     // 写模式的设备地址
    IICSTAT = 0xF0;       // Master Tx 模式,产生 Start 信号
    IICCON &= ~(1 << 4);  
    i2c_wait_ack();

    IICDS = addr & 0xFF;  // 写入数据字地址 (低 8 位)
    IICCON &= ~(1 << 4);
    i2c_wait_ack();

    // ==== 步骤二:改变方向,正式启动连续读取 ====
    IICDS = dev_addr | 0x01;  // 将 R/W 位置 1,变为读模式
    IICSTAT = 0xB0;           // Master Rx 模式,产生 Restart 信号
    IICCON &= ~(1 << 4);
    i2c_wait_ack();

    // S3C2440 硬件要求:进入接收模式后,必须先假读一次寄存器,才能触发 SCL 时钟去接收第一个真实数据
    dummy = IICDS;         
    (void)dummy;   

    // ==== 步骤三:流水线连续读取 ====
    for (i = 0; i < len; i++) {
        // IIC 协议核心要求:接收方在收到最后一个字节时,必须回复 NACK,告诉发送方"别再发了"
        if (i == (len - 1)) {
            IICCON &= ~(1 << 7);  // 最后一个字节,禁用 ACK (产生 NACK)
        } else {
            IICCON |= (1 << 7);   // 非最后一个字节,使能 ACK
        }

        IICCON &= ~(1 << 4);      // 清除 pending 位,让 IIC 控制器产生 SCL 时钟接收数据
        i2c_wait_ack();

        buf[i] = IICDS;           // 从移位寄存器中拿出收到的真实数据
    }

    // ==== 步骤四:发送 Stop 信号结束 ====
    IICSTAT = 0x90;           // 产生 Stop 信号
    IICCON &= ~(1 << 4);      // 清除 pending 位让 Stop 信号发出去
    
    // 恢复 ACK 使能,为系统的下一次 IIC 通信做好准备
    IICCON |= (1 << 7);      

    easy_delay_ms(1); // 稍微延时等待总线电平状态完全恢复
}

实现测试程序,

新建文件at24c08/main.c,

主要功能是写一串跨越多个页(64字节)的数据,然后读出来验证,打印结果,

C 复制代码
#include "s3c2440a.h"
#include <stdio.h>

// AT24C08 核心测试例程
void test_at24c08(void)
{
    unsigned short i;
    int error_count = 0;
    
    // 测试缓冲区 (64字节)
    unsigned char tx_buf[64];
    unsigned char rx_buf[64];
    
    // 设定一个故意不对齐的起始地址,比如 0x05
    // 这样第一页只能写 11 个字节(0x05~0x0F),之后就是完整的 16 字节页 
    unsigned short test_addr = 0x05; 

    printf("\r\n=== AT24C08 Advanced Buffer Test Start ===\r\n");

    // 1. 初始化 IIC 控制器
    i2c_init();
    printf("IIC Initialized.\r\n");

    // 2. 准备测试数据包
    for (i = 0; i < 64; i++) {
        tx_buf[i] = 0x30 + i; // 随便造点规律数据,比如 0x30, 0x31, 0x32...
        rx_buf[i] = 0x00;     // 清空接收缓冲区,防止被历史遗留数据干扰
    }

    // 3. 测试 Buffer 连续安全写入
    printf("Writing 64 bytes starting at unaligned address 0x%03X...\r\n", test_addr);
    // 底层的 write_page 会自动帮我们处理最大 10ms 的 EEPROM 内部写周期等待 
    at24c08_write_buffer(test_addr, tx_buf, 64);
    printf("Buffer write finished.\r\n");

    // 4. 测试 Buffer 连续读取
    printf("Reading 64 bytes from address 0x%03X...\r\n", test_addr);
    at24c08_read_buffer(test_addr, rx_buf, 64);
    printf("Buffer read finished.\r\n");

    // 5. 暴力比对数据
    printf("Verifying data integrity...\r\n");
    for (i = 0; i < 64; i++) {
        if (tx_buf[i] != rx_buf[i]) {
            printf("[FAIL] Offset %02d (Addr 0x%03X): expected 0x%02X, got 0x%02X\r\n", 
                   i, test_addr + i, tx_buf[i], rx_buf[i]);
            error_count++;
        }
    }

    // 6. 输出最终结果
    if (error_count == 0) {
        printf(">> AT24C08 Buffer Test PASSED! Page boundaries handled perfectly.\r\n");
    } else {
        printf(">> AT24C08 Buffer Test FAILED with %d errors. Check page split logic!\r\n", error_count);
    }
    printf("=== AT24C08 Advanced Buffer Test End ===\r\n\r\n");
}

int main(void)
{
    test_at24c08();
    return 0;
}

5. 运行

make at24c08,烧录运行,

相关推荐
九成宫1 分钟前
IT项目管理期末复习——Chapter 9 项目人力资源管理
笔记·项目管理·软件工程
2601_9498179211 分钟前
大厂Java进阶面试解析笔记文档
java·笔记·面试
代码中介商15 分钟前
手把手教你Linux 打包压缩与 gcc 编译详解
linux·运维·服务器·编译·打包·压缩
longerxin202020 分钟前
阿里云AlmaLinux操作系统允许root登录配置步骤
linux·服务器·阿里云
John.Lewis31 分钟前
C++进阶(12)附加学习:STL之空间配置器(了解)
开发语言·c++·笔记
yong999032 分钟前
基于STM32 Nucleo板的彩色LED照明灯设计(纯CubeMX开发)
stm32·单片机·嵌入式硬件
独小乐33 分钟前
019.ADC转换和子中断|千篇笔记实现嵌入式全栈/裸机篇
linux·c语言·驱动开发·笔记·嵌入式硬件·mcu·arm
xuanwenchao33 分钟前
ROS2学习笔记 - 2、类的继承及使用
服务器·笔记·学习
lingzhilab1 小时前
零知派——STM32驱动INA219电流功率监测计实现高精度电源管理
stm32·单片机·嵌入式硬件
GottdesKrieges1 小时前
OceanBase租户级物理恢复
linux·oceanbase