1.什么是总线
两线式通信线路
- SCL(clock) 时钟线
- SDA(data) 数据线
1.1 总线应用图
所有基于 I2C 总线的外围
器件都是在这五种底层信号的基础上进行数据的读写,这五中信号分别是:
- 1.起始信号
- 2.停止信号
- 3.写字节信号
- 4.读字节并发送应答信号
- 5.读字节并发送非应答信号
1.2 起始停止信号
- 1.起始信号: SDA 下降沿在 SCL 下降沿之前
- 2.结束信号: SDA 上升沿在 SCL 上升沿之后
SBIT(SCL_I2C, _P0, 1); //总线管脚定义
SBIT(SDA_I2C, _P0, 2);
// 模拟延时
void Delay_I2C(void)
{
NOP();
NOP();
NOP();
NOP();
}
void Start_I2C(void)
{
// 准备阶段
SCL_I2C = 0;
SDA_I2C = 1;
Delay_I2C();
SCL_I2C = 1;
Delay_I2C();
// 先将SDA拉低
SDA_I2C = 0;
Delay_I2C();
// 再将SCL拉低
SCL_I2C = 0;
Delay_I2C();
}
void Stop_I2C(void)
{
// 准备阶段
SCL_I2C = 0;
SDA_I2C = 0;
Delay_I2C();
// 拉高SCL
SCL_I2C = 1;
Delay_I2C();
// 拉高SDA
SDA_I2C = 1;
Delay_I2C();
SCL_I2C = 0;
}
1.3 写字节信号
如图所示
- 1.bit 位倒序
- 2.第九位为应答位
时序图
- 1.提前把数据装入 SDA
- 2.SCL 高位期间写数据
unsigned char Wr_I2C(unsigned char dat)
{
// 存储应答位
unsigned char ack;
// 探测字节内某一位的掩饰码变量
unsigned char mask;
// 高位到低位
// 0x80 = 0b10000000
// 0x80 >>1 = 0b0100 0000 = 0x40
// 0x80 >>2 = 0b0010 0000 = 0x20
// 0x80 >>7 = 0b0000 0001 = 0x01
// 0x80 >>8 = 0b0000 0000 = 0x00
for (mask = 0x80; mask != 0; mask >>= 1)
{
// 假设dat = 0b1010 1010
// 0b1000 0000 & 0b1010 1010 = 0b1000 0000
// 0b0100 0000 & 0b1010 1010 = 0b0000 0000
// 0b0010 0000 & 0b1010 1010 = 0b0010 0000
// 查看当前数据位置上是否为0或者不为0
if ((mask & dat) == 0)
{
SDA_I2C = 0;
}
else
{
SDA_I2C = 1;
}
// 延时
Delay_I2C();
// 拉高SCL,准备写数据
SCL_I2C = 1;
// 延时
Delay_I2C();
// 拉低SCL,一位数据写入完成
// 进入下一次循环后mask右移,探测第二位继续写入
SCL_I2C = 0;
}
// 主机释放总线
SDA_I2C = 1;
Delay_I2C();
// 准备获取应答位
SCL_I2C = 1;
// 获取ack应答位
ack = SDA_I2C;
Delay_I2C();
// 获取应答位结束
SCL_I2C = 0;
return ack;
}
1.4 读字节并发送应答信号
与写字节基本相同,
- 1.不同的是 bit7-bit0 由 I2C 上绑定的器件写出
- 2.SCL 高电平器件我们去读取 SDA 的数据
- 3.第九位由主机给出,ack=0 表示继续读,ack=1 表示不读了
1.5 读字节并发送非应答信号
// 参数 ack 给0表示SDA拉低,继续读
// 给1表示SDA拉高,不读了
unsigned char RdACK_I2C(unsigned char ack)
{
unsigned char mask;
unsigned char dat;
// 确保主机释放SDA
SDA_I2C = 1;
for (mask = 0x80; mask != 0; mask >>= 1)
{
// 延时
Delay_I2C();
// 拉高SCL准备读取SDA数据
SCL_I2C = 1;
if (SDA_I2C == 0)
{
// 比如需要读取的数据为0b0101 0101
// 第一次循环
// ~mask = ~0b1000 0000 = 0b0111 1111
// 读取 0
// 0b0000 0000 & 0b0111 1111 = 0b0000 0000
// 假如SDA=0 那么 mask反一下当前位为0,其他位为1
// & 运算符可以让dat保持当前位不变的情况下 将SDA的0 给dat
dat &= ~mask;
}
else
{
// 第二次循环
// mask = 0b0100 0000
// 读取1
// 0b0000 0000 | 0b0100 0000 = 0b0100 0000
// 整体大概意思就是,每次mask向右走一位
// 假如SDA=1 那么 mask当前位为1,无论dat当前位是否为1
// | 运算符可以让dat保持不变的情况下将当SDA前位的1 给dat
dat |= mask;
}
// 延时
Delay_I2C();
// 拉低SCL完成1位读取
SCL_I2C = 0;
}
// 8位传递完成后,第九位ack,由参数传递
SDA_I2C = ack;
// 延时
Delay_I2C();
// 拉高SCL准备发送第九位ack
SCL_I2C = 1;
// 延时
Delay_I2C();
// 发送SDA ack完毕
SCL_I2C = 0;
return dat;
}
将上面的函数封装成头文件样式
1.6 一次通讯时序
- 1.这是一张完整的时序图
- 2.起始信号-> 一字节的读取或写入-> 结束信号
- 3.在起始信号与停止信号之间的读取或写入操作,与 I2C 器件本身的通信协议有关
- 4.接下来了解一下基于 I2C 总线通信技术的 E2PROM 存储器
AT24C256
的通信协议和使用例子
2.E2PROM
如图,这是 AT24C256 器件,下图为线路图
由图可知
-
- 除去 VCC 和 GND,SCL,SDA 还剩下 A0,A1,A2,WP 四个 IO
-
- A0-A2 为地址输入引脚,每一个 AT24C256 可以设置一个独立器件,通过 A0-A2 的高低电平来设置,单片机通过这个器件选址来区分挂在总线上的 AT24C256
-
- SDA,SCL 连接到了 P01,P02 上
-
- WP 为写保护,WP=1 时不可以写数据,WP=0 可写可读
AT24C256 容量为 32768Byte
,即 0x8000Byte
,器件内每个 8 位空前都有地址值
最大为 0x8000-1 = 0x7FFF,这个地址需要两个字节存储,高位为 FIRSTWORD ADDRESS,低位为 SECOND WORD ADDRESS
3.AT24T256
3.1 写入
由上图可知:
- 1.写入过程由 START 起始信号开始
- 2.经过 DEVICE ADDRESS(写入元器件地址)
- 3.写入存储地址 高位(FIRSTWORD ADDRESS)
- 4.写入存储地址 低位(SECONDWORD ADDRESS)
- 5.写入数据 DATA
- 6.写入应答位 ACK
- 7.写入 STOP 结束地址
当 I2C 总线上挂载多个元器件时,单片机通过 Device Address
设备地址来区分器件
我们开发板上 AT24C256 器件地址如下图所示
- 1.高四位固定 1010
- 2.后四位由 A2-A0,以及 R/W 决定,
RYMCU-51
默认 A2-A0 接地,所以地址为1010000 R/W
- 3.R/W = Read/Write R/W=1 为 read,R/W=0 为 Write
- 4.综上所述 最后得出地址为
1010 000 0
最后设备地址0xA0
- 5.第三步为存储器地址,
0x0000~0x7FFF
任意写吧
void WrByte_AT24C256(unsigned int addr, unsigned char dat)
{
// 第一步START信号
Start_I2C();
// 写入设备地址 0b1010 0000 = 0xA0
Wr_I2C(0xA0);
// 写入高位地址,传入16位地址
Wr_I2C(addr >> 8);
// 写入低位地址,由于一次只能写入8位,所以前8位直接被过掉了
Wr_I2C(addr);
// 写入数据
Wr_I2C(dat);
// 最后一步STOP信号
Stop_I2C();
}
3.2 读取
如图可知
- 1.读取第一步开始信号
- 2.元器件地址,写地址
- 3.高位地址
- 4.低位地址
- 5.起始信号
- 6.元器件地址,读地址
- 7.读取 1byte 数据,发送非应答信号
- 8.停止信号
unsigned char RdByte_AT24C256(unsigned int addr)
{
unsigned char dat;
// 开始信号
Start_I2C();
Wr_I2C(0xA0);
Wr_I2C(addr >> 8);
Wr_I2C(addr);
// 开始信号
Start_I2C();
// 1010 0000 -> 1010 0001 = 0xA1
Wr_I2C(0xA1);
// 从addr读取数据发送非应答信号
dat = RdACK_I2C(1);
// 结束信号
Stop_I2C();
return dat;
}
封装
4.i2c 和 AT24C256 应用
/*
* @Author: cuihaonan
* @Email: devcui@outlook.com
* @Date: 2021-04-04 19:56:15
* @LastEditTime: 2021-04-04 21:10:28
* @LastEditors: cuihaonan
* @Description: 将数据由I2C写入AT24C26,然后读取AT24C26的数据并显示在1602上
* @FilePath: /sdcc-include/src/i2c/main.c
* @LICENSE: NONE
*/
#include "../../include/STC89xx.h"
#include "./include/1602.h"
#include "./include/I2C.h"
#include "./include/AT24C256.h"
void delayms(unsigned int z){
unsigned int x,y;
for(x=z;x>0;x--){
for(y=78;y>0;y--);
}
}
void main()
{
unsigned char d = 0;
unsigned char dat[10] = "";
Init_1602();
WrByte_AT24C256(0x0000, 1);
Disp_1602_str(1, 2, "ACT24C0X TEST!");
delayms(10);
d = RdByte_AT24C256(0x0000);
dat[0] = d / 100 + '0';
dat[1] = d % 100 / 10 + '0';
dat[2] = d % 10 + '0';
Disp_1602_str(2, 3, dat);
while (1)
;
}
5.at24c256 多字节通信
AT24C256 提供了另一种读写模式,页模式
32768bytes 分为 512 页,每页 64bytes
第一页范围为 0x0000~0x0040 依次递增
下图为页模式的通信时序图
- 1.START
- 2.DEVICE ADDRESS
- 3.FIRST WORD ADDRESS
- 4.SECOND WORD ADDRESS
- 5.DATA
- 6.STOP
WrStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
// 检测上一次是否写完了,如果越页了,那么继续写
while (len > 0)
{
// 循环检测元器件应答信号
while (1)
{
Start_I2C();
// 如果ack === 0 跳出,进行下面的写入
if (0 == Wr_I2C(0xA0))
{
// 跳出循环
break;
}
// 否则结束
Stop_I2C();
}
// 高位
Wr_I2C(addr >> 8);
// 低位
Wr_I2C(addr);
// 开始写
while (len > 0)
{
// 写一个字节,指针指向下一个自负
Wr_I2C(*str++);
// 长度--
len--;
// 存储地址+1
addr++;
// 是否达到了下一页
if (0 == (addr % 64))
{
// 上一个字节到本页的边界
// 跳出停止继续写
break;
}
}
Stop_I2C();
}
}
读取类似前面说的,唯一不同的是,读多字节,ack=0,要发送应答信号表示我还需要继续读。
void RdStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
// 循环检测ack是否为1
while (1)
{
Start_I2C();
// 如果为0跳出进行读取
if (0 == Wr_I2C(0xA0))
{
break;
}
Stop_I2C();
}
// 高低位
Wr_I2C(addr >> 8);
Wr_I2C(addr);
// 第二个Start信号
Start_I2C();
// 现在是读
Wr_I2C(0xA1);
// 如果长度大于1
while (len > 1)
{
// 读取,应答为0
*str++ = RdACK_I2C(0);
// 长度-1
len--;
}
// 如果长度没了那么读取无应答
*str = RdACK_I2C(1);
// 结束读取
Stop_I2C();
}
最后的例子
多字节读写显示到 1602 上
/*
* @Author: cuihaonan
* @Email: devcui@outlook.com
* @Date: 2021-04-04 19:12:01
* @LastEditTime: 2021-04-04 22:09:53
* @LastEditors: cuihaonan
* @Description: Basic description
* @FilePath: /sdcc-include/src/i2c/include/AT24C256.c
* @LICENSE: NONE
*/
#include "./AT24C256.h"
#include "./I2C.h"
void WrByte_AT24C256(unsigned int addr, unsigned char dat)
{
// 第一步START信号
Start_I2C();
// 写入设备地址 0b1010 0000 = 0xA0
Wr_I2C(0xA0);
// 写入高位地址,传入16位地址
Wr_I2C(addr >> 8);
// 写入低位地址,由于一次只能写入8位,所以前8位直接被过掉了
Wr_I2C(addr);
// 写入数据
Wr_I2C(dat);
// 最后一步STOP信号
Stop_I2C();
}
unsigned char RdByte_AT24C256(unsigned int addr)
{
unsigned char dat;
// 开始信号
Start_I2C();
Wr_I2C(0xA0);
Wr_I2C(addr >> 8);
Wr_I2C(addr);
// 开始信号
Start_I2C();
// 1010 0000 -> 1010 0001 = 0xA1
Wr_I2C(0xA1);
// 从addr读取数据发送非应答信号
dat = RdACK_I2C(1);
// 结束信号
Stop_I2C();
return dat;
}
void WrStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
// 检测上一次是否写完了,如果越页了,那么继续写
while (len > 0)
{
// 循环检测元器件应答信号
while (1)
{
Start_I2C();
// 如果ack === 0 跳出,进行下面的写入
if (0 == Wr_I2C(0xA0))
{
// 跳出循环
break;
}
// 否则结束
Stop_I2C();
}
// 高位
Wr_I2C(addr >> 8);
// 低位
Wr_I2C(addr);
// 开始写
while (len > 0)
{
// 写一个字节,指针指向下一个自负
Wr_I2C(*str++);
// 长度--
len--;
// 存储地址+1
addr++;
// 是否达到了下一页
if (0 == (addr % 64))
{
// 上一个字节到本页的边界
// 跳出停止继续写
break;
}
}
Stop_I2C();
}
}
void RdStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
// 循环检测ack是否为1
while (1)
{
Start_I2C();
// 如果为0跳出进行读取
if (0 == Wr_I2C(0xA0))
{
break;
}
Stop_I2C();
}
// 高低位
Wr_I2C(addr >> 8);
Wr_I2C(addr);
// 第二个Start信号
Start_I2C();
// 现在是读
Wr_I2C(0xA1);
// 如果长度大于1
while (len > 1)
{
// 读取,应答为0
*str++ = RdACK_I2C(0);
// 长度-1
len--;
}
// 如果长度没了那么读取无应答
*str = RdACK_I2C(1);
// 结束读取
Stop_I2C();
}