IIC读写AT24C08
IIC协议
起始、停止条件:IIC的起始信号为当时钟信号线(SCL)为高电平时,数据线(SDA)产生一个下降沿,停止信号为当时钟信号线(SCL)为高电平时,数据线(SDA)产生一个上升沿。

应答位、非应答位:当主机传送8位数据结束后,主机会将SDA线拉高,此时如果从机正确接收数据则会将SDA拉低并在SCL高电平时保持低电平,这个信号为ACK信号。如果在传输8位数据后从机没有将SDA拉低则该信号为NACK。如果出现NACK则表示数据传输出错。


数据有效性:当时钟信号为高电平的时候,数据线上的信号需要保持不变也就是在时钟线为高电平的时候数据线出现上升下降沿的话就会产生停止和启动信号,从而导致数据的传输出错。

byte组织:SDA上的数据传输是以8bit即一个字节为单位传输的,每一次传输的字节数没有限制,每传输完一个字节后必须跟随一个应答位。
我们以01001001(0X49)为例,其时序图如下:

写操作:主机确定了从机的设备地址后,生成一个开始信号,然后向IIC总线上面发送设备的地址和读写方向标志。从机检测到该地址和自己设备地址相对应后,回复主机一个应答信号。主机接收到应答信号后就开始向这个设备以字节为单位发送数据,每一个字节后面都会带有从机的应答信号,直到主机发送完成最后一个数据后生成一个停止信号结束此次数据的传输。

读操作:读操作与写操作有一些类似,同样的是需要确定需要读取的从设备的地址。然后主机生成开始信号,再向IIC总线上发送从设备的地址和读数据的指令。从设备接收到地址与自己的吻合后会产生一个应答信号。就这从设备就开始向主机发送主机想要读取的数据,主机正确接收数据后会向从机回复应答信号,当主机想要结束读取操作时,主机会回复一个非应答信号,然后生成停止信号结束数据的读取。

首先我们定义SCL和SDA引脚,以及AT24C08的地址:
1 2 3
| #define AT24C08_ADDR 0xA0 #define SCL P36 #define SDA P37
|
定义一个函数用于短暂延时
1 2 3 4 5 6 7 8
| void flash() { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); }
|
定义一个函数用于发送开始信号,IIC的起始信号为当时钟信号线(SCL)为高电平时,数据线(SDA)产生一个下降沿
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
static void iicStart() { SDA = 1; flash(); SCL = 1; flash(); SDA = 0; flash(); SCL = 0; flash(); }
|
定义一个函数发送一个字节数据
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
|
static void iicSendByte(uint8_t byte) { uint8_t i; for(i = 0 ; i < 8 ; i++) { SCL = 0; flash(); if (byte & 0x80) { SDA = 1; flash(); } else { SDA = 0; flash(); } SCL = 1; flash(); byte <<= 1; } SCL = 0; flash(); SDA = 1; flash(); }
|
定义一个函数用于发送停止信号,停止信号为当时钟信号线(SCL)为高电平时,数据线(SDA)产生一个上升沿。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
static void iicStop() { SDA = 0; flash(); SCL = 1; flash(); SDA = 1; flash(); }
|
定义一个函数用于检测应答信号
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
|
static bit iicDetectAck() { uint8_t i = 0; SCL = 1; flash(); while( i < 255 ) { i++; if (!SDA) { SCL = 0; _nop_(); return(1); } 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
|
static bit iicWriteByte(uint8_t addr, uint8_t dat) { iicStart(); iicSendByte(AT24C08_ADDR); if (!iicDetectAck()) { return(0); } iicSendByte(addr); if (!iicDetectAck()) { return(0); } iicSendByte(dat); if (!iicDetectAck()) { return(0); } iicStop(); return(1); }
|
定义一个函数读取一个字节的数据:
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
|
static uint8_t iicReceiveByte() { uint8_t i, dat = 0; SCL = 0; flash(); SDA = 1; flash(); for(i = 0 ; i < 8 ; i++) { SCL = 1; flash(); dat = dat << 1; if (SDA) { dat |= 0x01; } SCL = 0 ; flash(); } return(dat); }
|
定义一个函数发送应答和否应答信号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
static void iicAck(bit i) { SCL = 0; _nop_(); SDA = i; _nop_(); SCL = 1; flash(); SCL = 0; _nop_(); SDA = 1; _nop_(); }
|
综合开始信号函数、停止信号函数、检测应答信号函数、发送字节数据函数结合、读取字节数据函数、发送应答信号函数,封装成一个可以直接传入地址读取数据的函数。
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
|
static uint8_t iicReadByte(uint8_t addr) { uint8_t dat; iicStart(); iicSendByte(AT24C08_ADDR); if (!iicDetectAck()) { return(0); } iicSendByte(addr); if (!iicDetectAck()) { return(0); } iicStart(); iicSendByte(AT24C08_ADDR+1); if (!iicDetectAck()) { return(0); } dat = iicReceiveByte(); iicAck(1); iicStop(); return(dat); }
|
测试结果:

读写一页数据(16位):
在51单片机中读写无符号整型数据及uint16_t类型数据,需要2个字节(16位),此时我们可以使用连续读写,就是在读写完一个字节后不发停止信号或者应答否信号,转而继续读写数据代码如下:
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
|
static bit writePageAt(uint8_t addr, uint16_t dat) { iicStart(); iicSendByte(AT24C08_ADDR); if (!iicDetectAck()) { return(0); } iicSendByte(addr); if (!iicDetectAck()) { return(0); } iicSendByte((uint8_t)(dat>>8)); if (!iicDetectAck()) { return(0); } iicSendByte((uint8_t) dat); if (!iicDetectAck()) { return(0); } iicStop(); return(1); }
|
读写单精度数据
- 使用共同体构建一个浮点数和
uint8_t数组共享内存的共同体变量,当修改共同体任意成员时会同步更新到其他成员
1 2 3 4 5
| typedef union{ uint8_t bytes[4]; float float_dat; }FLOAT_U;
|
- 利用
memcpy内存拷贝函数将单精度数据拷贝到数组所在uint8_t数组所在的内存地址中。
1 2 3 4 5 6 7 8 9 10
| float number = 3.14159; uint8_t array[4];
memcpy(array, &number, sizeof(float));
for (int i = 0; i < sizeof(float); i++) { printf("%02X ", array[i]); } printf("\n");
|

读写单精度浮点数,我选择了使用共同体将浮点数与字节数相互转换:

一个注意事项:51单片机不是基于堆栈的架构。 当参数不能放入CPU寄存器时,Keil Cx51编译器默认使用直接的内存位置来传递参数。 这种技术生成非常高效的代码,但限制了可以传递给间接调用的函数的参数。 当通过函数指针传递给函数的形参不适合寄存器时,编译器无法确定将形参放在内存中的哪个位置,因为函数在调用时是未知的。
- 使用可重入函数属性
reentrant 创建可重入函数。编译器模拟了一种基于堆栈的体系结构,它可以将几乎无限数量的参数传递给间接调用的函数。
- 限制参数的数量和类型,使它们都适合CPU寄存器。在参数前指明存储的位置,比如data、xdata
data:固定指前面0x00-0x7F的128个RAM,可以用acc直接读写,速度最快,生成的代码也最小。
idata:固定指前面0x00-0xFF的256个RAM,其中前128和data的128完全相同,只是访问的方式不同。
xdata:外部扩展RAM。
code:ROM。
使用第一种方法:
1 2 3 4 5 6 7 8 9
| typedef struct { bit (*writeFloatAt)(uint8_t, float); }AT24C08_T;
typedef struct { bit (*writeFloatAt)(uint8_t, float)reentrant; }AT24C08_T;
|
连续读写
数据手册第一页提到自写入周期为5ms,因此进行跨页连续写入的时候需要等待5ms
AT24C08一页16个字节,当连续写入地址跨页时,不会自动翻页,如果不手动翻页那么就会回到这一页的首部,覆盖原来的数据。
翻页需要发送停止信号,再次发送开始信号、设备地址和目标写入地址,下面是多字节写入函数的代码
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
|
bit writeBytesAt(uint8_t addr,uint8_t *dat,uint8_t len) { iicStart(); iicSendByte(AT24C08_ADDR0); if (!iicDetectAck()) { return(0); } iicSendByte(addr); if (!iicDetectAck()) { return(0); } while(len > 0) { iicSendByte(*dat++); if(!iicDetectAck()) { return(0); } len--; addr++; if((addr & 0x0F) == 0) { iicStop(); Delay_ms(6); iicStart(); iicSendByte(AT24C08_ADDR0); if (!iicDetectAck()) { return(0); } iicSendByte(addr); if (!iicDetectAck()) { return(0); } } } iicStop(); return(1); }
|
参考文章:KEIL C51中的data、idata、xdata、code详解