[toc]
I2C
相关知识可以参考 IIC 通信协议详解
一、AT24CXXX 系列存储器介绍
1、基本信息
下表是 AT24CXXX 的容量
:
AT24C01,AT24C02,AT24C04,AT24C08,AT24C16,AT24C32,AT24C64,AT24C128,AT24C256… 不同的 xxx 代表不同的容量。
AT24CXXX |
bit容量 |
Byte容量 |
AT24C01 |
1Kbit |
128Byte |
AT24C02 |
2Kbit |
256Byte |
AT24C04 |
4Kbit |
512Byte |
AT24C08 |
8Kbit |
1024Byte |
AT24C16 |
16Kbit |
2048Byte |
AT24C32 |
32Kbit |
4096Byte |
AT24C64 |
64Kbit |
8192Byte |
AT24C128 |
128Kbit |
16384Byte |
AT24C256 |
256Kbit |
32768Byte |
AT24C512 |
512Kbit |
65536Byte |
下表是 AT24CXXX 的页内单元数
:
总容量(Byte容量) = 页数 × 页内字节单元数
AT24CXXX |
Byte容量 |
页数 |
页内字节单元数 |
AT24C01 |
128Byte |
16页 |
8Byte |
AT24C02 |
256Byte |
32页 |
8Byte |
AT24C04 |
512Byte |
32页 |
16Byte |
AT24C08 |
1024Byte |
64页 |
16Byte |
AT24C16 |
2048Byte |
128页 |
16Byte |
AT24C32 |
4096Byte |
128页 |
32Byte |
AT24C64 |
8192Byte |
256页 |
32Byte |
AT24C128 |
16384Byte |
256页 |
64Byte |
AT24C256 |
32768Byte |
512页 |
64Byte |
AT24C512 |
65536Byte |
512页 |
128Byte |
2、寻址方式
不是 I2C 地址,是存储器内部寻址
对 AT24CXXX
进行读写操作时,都得先访问存储地址、比如 AT24C04
写一个字节的 I2C 时序:

先发送设备地址,收到应答后再发送需要写数据的地址(WORD ADDRESS
)。AT24C04
容量为 512Byte 则 WORD ADDRESS 只需要 9bit 就可以覆盖 512Byte 的数据地址。通俗的讲就是 512Byte 就占用了 512 个地址,一个 9bit 的数据范围为($0-511$)刚好 512,所以 512Byte 的字节地址需要一个 9bit 的数据来表示。
3、页地址与页内单元地址
比如 AT24C04
有 32 页每页 16 个字节,9bit 的地址数据对其寻址,低 4bit(D3-D0)为页内字节单元地址,高 5bit(D8-D4)为页地址。

如从第 16 页开始写,则 WORD ADDRESS = 0x0100(0001 0000 0000)
,则:
- 000:地址无效位
- 1 0000:5 位页地址
- 0000:4 位页内单元地址

4、I2C 地址
I2C 通信需要先向从设备发送设备地址,AT24CXXX
芯片上有 A2、A1、A0 引脚,通过这三个引脚我们就可以自定义 AT24CXXX
芯片的通信地址。

下面以 24C04 和 24C08 的官方手册为例,说明其 I2C 地址,其它型号的芯片自行查阅手册。

可以看到,前 4 位是固定的为 1010,而后的 A2、A1、P0 三个引脚以及读写标志位有我们自己设置。如果将 A2、A1、P0 接地,则 I2C 写地址为 1010 0000
(0xA0),读地址为 1010 0001
(0xA1)。
5、AT24CXX 的数据读写
5.1 写操作
5.1.1 按字节写

5.1.2 按页写

和按字节写类似,不过在往 AT24CXXX
中写数据时,每写一个 Byte 的数据页内地址 +1,==当前页写满后会重新覆盖掉这一页前面的数据==,而不会自动跳转到下一页,但是读会自动翻页。
那要如何实现翻页写呢?
按页写其实就是执行一次上面的时序,也就是发送一次从机设备和字节地址最大就可以写入 16 字节(AT24C04)的数据,如果要连写多页,就重新按照上面的时序发送从机地址和字节地址等。
5.2 读操作
写操作和读操作类似,不过 R/W
标志位要设置为 1。
5.2.1 当前地址读取

5.2.2 随机地址读取

5.2.3 顺序读取

二、代码实现
说明:
1 2 3 4 5 6 7
| ctl_i2c.h ctl_i2c.c
at24c.h at24c.c
|
1、ctl_i2c
下面是 ctl_i2c.h
文件,没什么可说的,实现了一些宏,以及相关函数的声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #ifndef _BSP_I2C_GPIO_H #define _BSP_I2C_GPIO_H #include "stm32f4xx.h" #define I2C_WR 0 #define I2C_RD 1 #define RCC_AT24CXX_I2C_PORT RCC_AHB1Periph_GPIOB #define GPIO_AT24CXX_I2C_PORT GPIOB #define GPIO_AT24CXX_I2C_SCL_Pin GPIO_Pin_8 #define GPIO_AT24CXX_I2C_SDA_Pin GPIO_Pin_9 #define I2C_SCL_H() GPIO_SetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SCL_Pin) #define I2C_SCL_L() GPIO_ResetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SCL_Pin) #define I2C_SDA_H() GPIO_SetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SDA_Pin) #define I2C_SDA_L() GPIO_ResetBits(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SDA_Pin) #define I2C_SDA_RD() GPIO_ReadInputDataBit(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SDA_Pin) #define I2C_SCL_RD() GPIO_ReadInputDataBit(GPIO_AT24CXX_I2C_PORT, GPIO_AT24CXX_I2C_SCL_Pin)
void ctl_at24cxx_i2c_init(void); void ctl_i2c_start(void); void ctl_i2c_stop(void); void ctl_i2c_sendbyte(uint8_t byte); void ctl_i2c_ack(void); void ctl_i2c_nack(void); uint8_t ctl_i2c_waitack(void); uint8_t ctl_i2c_readbyte(void); uint8_t ctl_i2c_checkdevice(uint8_t address); #endif
|
接下来看 ctl_i2c.c
文件:
初始化 I2C 的 GPIO 端口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
void ctl_at24cxx_i2c_init(void) { RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz; GPIO_InitStructure.GPIO_Pin = GPIO_AT24CXX_I2C_SCL_Pin | GPIO_AT24CXX_I2C_SDA_Pin; GPIO_Init(GPIO_AT24CXX_I2C_PORT, &GPIO_InitStructure); ctl_i2c_stop(); }
|
延时函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
static void i2c_delay(void) { uint8_t i;
for (i = 0; i < 30; i++) { __NOP(); __NOP(); } }
|
I2C 开始信号:当 SCL 线在高电平期间 SDA 线从高电平向低电平切换

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
void ctl_i2c_start(void) { I2C_SDA_H(); I2C_SCL_H(); i2c_delay(); I2C_SDA_L(); i2c_delay(); I2C_SCL_L(); i2c_delay(); }
|
I2C 停止信号:当 SCL 线在高电平期间 SDA 线由低电平向高电平切换

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
void ctl_i2c_stop(void) { I2C_SDA_L(); I2C_SCL_H(); i2c_delay(); I2C_SDA_H(); i2c_delay(); }
|
下面是应答信号和非应答信号的函数实现:

在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,给发送端传输应答或非应答信号
- SDA 为高电平:表示非应答信号(NACK)
- SDA为低电平:表示应答信号(ACK)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
void ctl_i2c_ack(void) { I2C_SDA_L(); i2c_delay(); I2C_SCL_H(); i2c_delay(); I2C_SCL_L(); i2c_delay(); I2C_SDA_H(); }
void ctl_i2c_nack(void) { I2C_SDA_H(); i2c_delay(); I2C_SCL_H(); i2c_delay(); I2C_SCL_L(); i2c_delay(); }
|
为什么数据发送端要释放 SDA 的控制权(将SDA总线置为高电平)

数据有效性:IIC 总线进行数据传送时,==SCL 信号为高电平期间,SDA 上的数据必须保持稳定==,只有在 SCL 上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化(准备下一位数据)。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。

数据传输:在 SCL 串行时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
void ctl_i2c_sendbyte(uint8_t byte) { uint8_t i; for (i = 0; i < 8; i++) { if (byte & 0x80) { I2C_SDA_H(); } else { I2C_SDA_L(); } i2c_delay(); I2C_SCL_H(); i2c_delay(); I2C_SCL_L();
if (i == 7) { I2C_SDA_H(); }
byte <<= 1; i2c_delay(); } }
uint8_t ctl_i2c_readbyte(void) { uint8_t i; uint8_t value = 0; for (i = 0; i < 8; i++) { value <<= 1;
I2C_SCL_H(); i2c_delay();
if (I2C_SDA_RD()) { value++; }
I2C_SCL_L(); i2c_delay(); }
return value; }
|
最后是等待从机 EEPROM 应答和检查设备是否已连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
uint8_t ctl_i2c_waitack(void) { uint8_t re; I2C_SDA_H(); i2c_delay(); I2C_SCL_H(); i2c_delay(); if (I2C_SDA_RD()) { re = 1; } else { re = 0; } I2C_SCL_L(); i2c_delay(); return re; }
uint8_t ctl_i2c_checkdevice(uint8_t _Address) { uint8_t ucAck; if (I2C_SDA_RD() && I2C_SCL_RD()) { ctl_i2c_start(); ctl_i2c_sendbyte(_Address | I2C_WR); ucAck = ctl_i2c_waitack(); ctl_i2c_stop(); return ucAck; } return 1; }
|
2、at24c
在 at24.h
文件中针对 AT24CX
系列的容量和页内单元数设置了不同的宏,可以针对自己使用的型号设置选择不同的宏使用,这里以 AT24C04 为例:#define AT24C04
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| #ifndef __AT24C_H #define __AT24C_H #include "stm32f4xx.h"
#define AT24C04 #ifdef AT24C01 #define AT24CX_MODEL_NAME "AT24C01" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 8 #define AT24CX_SIZE 128 #define AT24CX_ADDR_BYTES 1 #define AT24CX_ADDR_A8 0 #endif #ifdef AT24C02 #define AT24CX_MODEL_NAME "AT24C02" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 8 #define AT24CX_SIZE 256 #define AT24CX_ADDR_BYTES 1 #define AT24CX_ADDR_A8 0 #endif #ifdef AT24C04 #define AT24CX_MODEL_NAME "AT24C04" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 16 #define AT24CX_SIZE 512 #define AT24CX_ADDR_BYTES 1 #define AT24CX_ADDR_A8 1 #endif #ifdef AT24C08 #define AT24CX_MODEL_NAME "AT24C08" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 16 #define AT24CX_SIZE (16*64) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 1 #endif #ifdef AT24C16 #define AT24CX_MODEL_NAME "AT24C16" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 16 #define AT24CX_SIZE (128*16) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 1 #endif #ifdef AT24C32 #define AT24CX_MODEL_NAME "AT24C32" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 32 #define AT24CX_SIZE (128*32) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 0 #endif #ifdef AT24C64 #define AT24CX_MODEL_NAME "AT24C64" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 32 #define AT24CX_SIZE (256*32) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 0 #endif #ifdef AT24C128 #define AT24CX_MODEL_NAME "AT24C128" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 64 #define AT24CX_SIZE (256*64) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 0 #endif #ifdef AT24C256 #define AT24CX_MODEL_NAME "AT24C256" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 64 #define AT24CX_SIZE (512*64) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 0 #endif #ifdef AT24C512 #define AT24CX_MODEL_NAME "AT24C512" #define AT24CX_DEV_ADDR 0xA0 #define AT24CX_PAGE_SIZE 128 #define AT24CX_SIZE (512*128) #define AT24CX_ADDR_BYTES 2 #define AT24CX_ADDR_A8 0 #endif
uint8_t at24cx_checkok(void); uint8_t at24cx_readbytes(uint8_t *readbuf, uint16_t address, uint16_t size); uint8_t at24cx_writebytes(uint8_t *writebuf, uint16_t address, uint16_t size); #endif
|
下面是 at24c.c
函数的实现:
首先检查设备是否连接成功:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
uint8_t at24cx_checkok(void) { if (ctl_i2c_checkdevice(AT24CX_DEV_ADDR) == 0) { return 1; } else { ctl_i2c_stop(); return 0; } }
|
然后是读写函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
|
uint8_t at24cx_readbytes(uint8_t *readbuf, uint16_t address, uint16_t size) { uint16_t i;
ctl_i2c_start(); #if AT24CX_ADDR_A8 == 1 ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E)); #else ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR); #endif if (ctl_i2c_waitack() != 0) { goto cmd_fail; } if (AT24CX_ADDR_BYTES == 1) { ctl_i2c_sendbyte((uint8_t)address); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } } else { ctl_i2c_sendbyte(address >> 8); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } ctl_i2c_sendbyte(address); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } } ctl_i2c_start(); #if AT24CX_ADDR_A8 == 1 ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_RD | ((address >> 7) & 0x0E)); #else ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_RD); #endif if (ctl_i2c_waitack() != 0) { goto cmd_fail; } for (i = 0; i < size; i++) { readbuf[i] = ctl_i2c_readbyte(); if (i != size - 1) { ctl_i2c_ack(); } else { ctl_i2c_nack(); } }
ctl_i2c_stop(); return 1; cmd_fail: ctl_i2c_stop(); return 0; }
uint8_t at24cx_writebytes(uint8_t *writebuf, uint16_t address, uint16_t size) { uint16_t i, m; uint16_t addr;
addr = address; for (i = 0; i < size; i++) { if ((i == 0) || (addr & (AT24CX_PAGE_SIZE - 1)) == 0) { ctl_i2c_stop();
for (m = 0; m < 1000; m++) { ctl_i2c_start(); #if AT24CX_ADDR_A8 == 1 ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E)); #else ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR); #endif if (ctl_i2c_waitack() == 0) { break; } } if (m == 1000) { goto cmd_fail; } if (AT24CX_ADDR_BYTES == 1) { ctl_i2c_sendbyte((uint8_t)addr); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } } else { ctl_i2c_sendbyte(addr >> 8); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } ctl_i2c_sendbyte(addr); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } } } ctl_i2c_sendbyte(writebuf[i]); if (ctl_i2c_waitack() != 0) { goto cmd_fail; } addr++; } ctl_i2c_stop();
for (m = 0; m < 1000; m++) { ctl_i2c_start(); #if AT24CX_ADDR_A8 == 1 ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR | ((address >> 7) & 0x0E)); #else ctl_i2c_sendbyte(AT24CX_DEV_ADDR | I2C_WR); #endif if (ctl_i2c_waitack() == 0) { break; } } if (m == 1000) { goto cmd_fail; } ctl_i2c_stop(); return 1; cmd_fail: ctl_i2c_stop(); return 0; }
|
3、测试程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| uint8_t test_array1[3 * AT24CX_PAGE_SIZE]; uint8_t test_array2[3 * AT24CX_PAGE_SIZE];
void at24c04_test_num(void) { uint16_t i; uint16_t j;
for (i = 0; i < 3 * AT24CX_PAGE_SIZE; i++) { if (i >= 256) j = i - 256; else if (i >= 128) j = i - 128; else j = i; test_array1[i] = j + 1; }
memset(test_array2, 0x00, 3 * AT24CX_PAGE_SIZE);
if (at24cx_checkok() == 1) { at24cx_writebytes(test_array1, 80, 3 * AT24CX_PAGE_SIZE); at24cx_readbytes(test_array2, 80, 3 * AT24CX_PAGE_SIZE); }
printf("test begin\r\n"); for (i = 0; i < sizeof(test_array2); ++i) { printf("%d, ", test_array2[i]); } }
|
结果如下:
