⚠️裸机仓库: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,烧录运行,
