不想看《跟着Hugh学开发--51单片机篇》pdf版本?没关系,网页版 markdown 格式为你准好了!
在单片机应用系统中,串行通信总线技术是非常重要的通信手段。常用的串行总线通信方式包括异步串行通信 UART 、 I2C (Inter IC BUS)、单总线(One WIRE BUS)以及 SPI 总线(Serial Peripheral Interface BUS)等。单片机的串口通信为 UART 的一种, DS18B20 的通信方式为单总线。采用 I2C 总线通信方式的常用器件包括 E2PROM 存储器件 AT24C256 ,这章内容主要讲解 I2C 总线通信工作原理并结合 AT24C256 进行应用试验。
I2C (Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式串行通信总线,由于连接主机以及外围设备。两根数据线一个为时钟线 SCL,另一根为数据线 SDA,可实现数据的发送或接收。通常将 I2C 通信速率分为:低速模式 100Kbit/s、快速模式 400Kbit/s 以及高速模式3.4Mbit/s, I2C 器件为向下兼容模式,一般所有 I2C 器件均支持低速模式。 I2C 通信器件典型电路如下图所示:
图 15-1 I2C 器件典型应用原理图
如上图所示,在 I2C 总线上挂载多个外围器件,总线与电源之间配置了上拉电阻,使所有器件之间形成了”线与”的逻辑关系,任何一个器件将总线拉低,总线将保持低电平,因此任意一个器件都可以当成主设备或者从设备。
I2C 通信最底层的时序操作包涵四种类型的信号,所有基于 I2C 总线的外围器件都是在这五种底层信号的基础上进行数据的读写,这五种信号分别是:
起始信号;
停止信号;
写字节信号;
读字节并发送应答信号;
读字节并发送非应答信号。
起始信号,功能为通知 I2C 器件可以开始进行数据操作,操作时序为:当 SCL 为高电平时, SDA 由高电平向低电平跳变。停止信号,功能为通知 I2C 器件数据操作已结束,操作时序为:当 SCL 为高电平时, SDA 由低电平向高电平跳变。时序如下图所示:
图 15-2 I2C 起始信号、停止信号时序
I2C 起始信号 Start_I2C (void)、停止信号 Stop_I2C (void)时序,C语言函数如下图所示:
sbit SCL_I2C = P0^1;// 总线管脚定义
sbit SDA_I2C = P0^2;
void Delay_I2C(void)
{// 延时函数,设置传输速率
_nop_();
_nop_();
_nop_();
_nop_();
}
// 总线起始信号
void Start_I2C(void)
{
//SCL 高电平期间,拉低 SDA
SCL_I2C = 0;
SDA _I2C = 1;// 在 SCL 低电平期间先将 SDA 拉高,为起始信号做准备
Delay_I2C();
SCL_I2C = 1;
Delay_I2C();
SDA_I2C = 0;// 拉低 SDA,发送起始信号
Delay_I2C();
SCL_I2C = 0;
}
// 总线停止信号
void Stop_I2C(void)
{
//SCL 高电平期间,拉高 SDA
SCL_I2C = 0;
SDA _I2C = 0;// 在 SCL 低电平期间先将 SDA 拉低,为停止信号做准备
Delay_I2C();
SCL_I2C = 1;
Delay_I2C();
SDA_I2C = 1;// 拉高 SDA,发送停止信号
Delay_I2C();
SCL_I2C = 0;
}
图 15-3 I2C 起始信号、停止信号C语言函数代码
I2C 写字节信号,功能为向总线写入1字节的数据,操作时序下图所示:
图 15-4 I2C 写字节信号时序
在写入数据的过程中,数据顺序为从高位到低位,最先写入的数据为 bit7,依次到 bit0 共8位数据。如果接收器件收到了上述1字节的数据,会在 SCL 的第9个周期的高电平期间将 SDA 拉低为 “0”,这个第9位数据称为应答位 ACK ,作用为通知主机已经收到了1字节的数据。因此,在主机程序中通过 ACK 位判断1字节数据是否写入成功。在写数据的过程中要求,数据在 SCL 高电平期间要保持 SDA 数据稳定,在 SCL 低电平期间, SDA 可由高电平变为低电平或者低电平变为高电平,如下图所示。
图 15-5 I2C 数据有效性时序规定
I2C 写字节信号 Wr_I2C (unsigned char dat), C语言函数代码如下图所示:
//I2C 写入字节 dat,返回应答信号
bit Wr_I2C(unsigned char dat)
{
bit ack; // 存储应答位
unsigned char mask; // 探测字节内某一位值的掩码变量
for(mask=0x80;mask!=0;mask>>=1)// 从高位依次到低位
{
if((mask & dat)==0) SDA_I2C=0;
else SDA_I2C=1;
Delay_I2C();
SCL_I2C = 1;
Delay_I2C();
SCL_I2C = 0; // 完成一位的传送
}
SDA_I2C=1; // 主机释放总线
Delay_I2C();
SCL_I2C = 1;
ack = SDA_I2C;// 获取应答位
Delay_I2C();
SCL_I2C = 0;
return ack; // 返回0写入成功,返回1写入失败
}
图 15-6 I2C 写字节信号C语言函数代码
I2C 读字节并发送应答信号时序与图 15-4 基本相同,只不过 bit7-bit0 由 I2C 从器件给出,在 SCL 高电平期间主机将数据读取,第9位应答信号 ACK 由主机给出, ACK 为 “0” 表示主机后续还要继续读取数据,为 “1” 时主机不再读取后续数据,可以结束通信。C语言函数如下图所示:
//I2C 读操作,并发送应答信号
unsigned char RdACK_I2C(void)
{
unsigned char mask; // 探测字节内某一位值的掩码变量
unsigned char dat;
SDA_I2C=1;// 确保主机释放 SDA
for(mask=0x80;mask!=0;mask>>=1)// 从高位依次到低位
{
Delay_I2C();
SCL_I2C = 1;
if(SDA_I2C==0) dat &= ~mask;// 为0时, dat 对应位清零
else dat |= mask;// 否则置1
Delay_I2C();
SCL_I2C = 0;
}
SDA _I2C=0; //8 位数据传送完后,拉低 SDA 发送应答信号
Delay_I2C();
SCL_I2C = 1;
Delay_I2C();
SCL_I2C = 0;
return dat;
}
图 15-7 I2C 读字节并发送应答信号函数
与读字节并发送应答信号相同,唯一的区别为主机发出非应答信号,即 ACK=1,主机不再读取后续数据,可以结束通信。C语言函数如下图所示:
//I2C 读操作,并发送非应答信号
unsigned char RdNAK_I2C(void)
{
unsigned char mask; // 探测字节内某一位值的掩码变量
unsigned char dat;
SDA_I2C=1;// 确保主机释放 SDA
for(mask=0x80;mask!=0;mask>>=1)// 从高位依次到低位
{
Delay_I2C();
SCL_I2C = 1;
if(SDA_I2C==0) dat &= ~mask;// 为0时, dat 对应位清零
else dat |= mask;// 否则置1
Delay_I2C();
SCL_I2C = 0;
}
SDA _I2C=1; //8 位数据传送完后,拉高 SDA 发送非应答信号
Delay_I2C();
SCL_I2C = 1;
Delay_I2C();
SCL_I2C = 0;
return dat;
}
图 15-8 I2C 读字节并非应答信号函数
将上述5个底层 I2C 总线操作函数放到文件 “Drive_ I2C .c” 以及 “Drive_ I2C .h”。 “Drive_ I2C .c” 文件前4行代码如下图所示。其它代码与上面的函数相同,这里不再赘述。
#include <reg52.h>
#include <intrins.h>
sbit SCL_I2C = P0^1;// 总线管脚定义
sbit SDA_I2C = P0^2;
图 15-9 I2C 总线底层驱动函数 Drive_I2C .c部分代码
"Drive_I2C.h" 代码如下图所示:
#ifndef __I2C_H__
#define __I2C_H__
extern void Start_I2C(void); // 起始信号
extern void Stop_I2C(void); // 停止信号
extern unsigned char RdACK_I2C(void); // 读字节并发送应答信号
extern unsigned char RdNAK_I2C(void); // 读字节并发送非应答信号
extern bit Wr_I2C(unsigned char dat); // 读字节信号
#endif
图 15-10 Drive_I2C.h代码
所有基于 I2C 总线通信设备都是以上面5条最底层操作为基础的,完成一次完整的 I2C 通信时序如下图所示:
图 15-11 I2C 一次通信时序图
如图所示,一次完整的 I2C 总线通信至少包含起始信号 、一次字节读或写,或者多次读或写,以及停止信号。在起始信号与停止信号之间读或写的具体内容与 I2C 器件本身的上层通信协议有关。接下来我们将讲解基于 I2C 总线通信技术的 E2PROM 存储器 AT24C256 的上层通信协议以及具体使用实例。
AT24C256 是 Atmel 公司生产的一款 E2PROM 数据存储器,容量为 32768 字节,具有掉电不丢失的功能。在单片机中也有存储器,一种为数据存储器 RAM ,一种为程序存储器,在掉电的情况下 RAM 内的数据会丢失,而程序存储器一般不支持在线编程。然而在很多应用场合我们希望把运行过程中重要的数据存储下来,而在掉电的情况下数据不丢失。 AT24C256 能满足这样的要求,它与单片之间通过 I2C 总线通信实现信息的交换。
图 15-12 片外存储器原理图
下面介绍一下芯片管脚:
a. A0~A2 为地址输入引脚,每一个 AT24C256 可以设置一个独立的器件地址,通过 A0~A2 的高低电平来设置,单片机通过这个器件地址来区分挂在总线上的 AT24C256 ;
b. SDA、SCL 为 I^2^C 总线接口,分别连接到了单片机的 P01, P02 引脚上。
c. VCC, GND 分别为电源和地
d. WP 为写保护管脚,当 WP 接高电平时,禁止外部对它进行写数据,只能读取它的数据。如上图所示,将 WP 接地,单片机即可对它进行读也可以写,原理图中将 WP 接地了。
AT24C256 的容量为 32768Byte,即 0x8000Byte,在器件内部给每一个字节的存储空间安排了一个地址,并且从0开始,因此,地址的最大值为:0x8000-1=0x7FFF,即需要2个字节的地址空间,地址的高字节称为 First Word Address,低字节称为 Second Word Address,单片机就是通过这个地址来读取和存储数据的。
AT24C256 写字节如下图所示:
图 15-13 写字节操作
如上图所示, AT24C256 的写字节时序步骤如下:
1) 起始信号;
2) 写器件地址;
3) 写存储地址, First Word Address;
4) 写存储地址, Second Word Address;
5) 写存储数据;
6) 停止信号。
当 I2C 总线上挂载多个从器件时,单片机通过器件地址来区别器件,那在我们开发板上的 AT24C256 的器件地址是多少呢? AT24C256 器件地址如下所示:
图 15-14 AT24C256 器件地址定义
如上图所示,地址的高4位为 “1010” 固定值,即 0xA,后四位分别由 A2~A0 、 R/W 决定,在 Nebula Pi 开发板上将器件的 A2~A0 都接地,如图 15-12 所示,因此为 “000”,最后一位为读写方向位。当 R/W =0时,表示我们接下来写数据,当 R/W =1时,表示我们接下来要读数据。很显然我们这里是要写数据,因此 R/W =0。合并起来,要写的地址为 0xA0 。第3步为写存储器地址, AT24C256 总共有 32768 字节的存储器,它的地址分别为 0x0000~0x7FFF,因此可以选择任一地址存入数据。第4步为写存储数据,即为你写存储的8位数据。总结上述,往 AT24C256 写入一个字节数据函数如下所示:
// 往 AT24Cxx 地址 addr 写入单字节数据 dat
void WrByte_AT24Cxx(unsigned int addr,unsigned char dat)
{
Start_I2C();
Wr_I2C(0x 50 <<1);// 通知地址 50 的器件,接下来写操作 0x 50 <<1=0xA0
Wr_I2C( addr >>8); // 写入要操作的地址 addr 高字节
Wr_I2C( addr ); // 写入要操作的地址 addr 的低字节
Wr_I2C( dat); // 向 addr 写入数据 dat
Stop_I2C();
}
图 15-15 写字节操作函数
随机读取 AT24C256 单字节通信如下图所示:
图 15-16 随机读通信协议
随机读取单字节数据通信协议如上图所示,步骤如下:
1) 起始信号;
2) 写器件地址,方向为写;
3) 写存储器地址 addr;
4) 起始信号;
5) 写器件地址,方向为读;
6) 读单字节数据,并发送非应答信号
7) 停止信号。
第1步至第3步为告诉 AT24C256 我将从地址 addr 处读取数据,第4步到第6步为读取存储器地址 addr 处的数据,并告诉 AT24C256 后面不再继续读数据了,第7步结束本次通信。具体函数代码如下图所示:
// 读取 AT24Cxx 存储地址 addr 处的数据
unsigned char RdByte_AT24Cxx(unsigned int addr)
{
unsigned char dat;
Start_I2C();
Wr_I2C(0x 50 <<1);// 通知地址 50 的器件,接下来写操作
Wr_I2C( addr >>8); // 写入要操作的地址 addr 高字节
Wr_I2C( addr ); // 写入要操作的地址 addr 的低字节
Start_I2C();
Wr_I2C((0x 50 <<1)|0x01);// 通知地址 50 的器件,接下来读操作
dat = RdNAK_I2C();// 从地址 addr 读出数据,读出数据后不应答 E2Prom
Stop_I2C();
return dat;
}
图 15-17 随机读取单字节函数
到这里我们便完成了对 AT24C256 单字节的读、写通信函数,按照惯例我们将函数封装到 “Drive_AT 24Cxx.c”、 “Drive_AT 24Cxx.h>,如下图所示:
#ifndef __AT24Cxx_H__
#define __AT24Cxx_H__
extern void WrByte_AT24Cxx(unsigned int addr,unsigned char dat);// 写单字节
extern unsigned char RdByte_AT24Cxx(unsigned int addr); // 读单字节
#endif
图 13-18 Drive_AT24Cxx.h代码
#include <reg52.h>
#include <Drive_I2C.h>
// 往 AT24Cxx 地址 addr 写入单字节数据 dat
void WrByte_AT24Cxx(unsigned int addr,unsigned char dat)
{
Start_I2C();
Wr_I2C(0x 50 <<1);// 通知地址 50 的器件,接下来写操作
Wr_I2C( addr >>8); // 写入要操作的地址 addr 高字节
Wr_I2C( addr ); // 写入要操作的地址 addr 的低字节
Wr_I2C( dat); // 向 addr 写入数据 dat
Stop_I2C();
}
// 读取 AT24Cxx 存储地址 addr 处的数据
unsigned char RdByte_AT24Cxx(unsigned int addr)
{
unsigned char dat;
Start_I2C();
Wr_I2C(0x 50 <<1);// 通知地址 50 的器件,接下来写操作
Wr_I2C( addr >>8); // 写入要操作的地址 addr 高字节
Wr_I2C( addr ); // 写入要操作的地址 addr 的低字节
Start_I2C();
Wr_I2C((0x 50 <<1)|0x01);// 通知地址 50 的器件,接下来读操作
dat = RdNAK_I2C();// 从地址 addr 读出数据,读出数据后不应答 E2Prom
Stop_I2C();
return dat;
}
图 15-19 Drive_AT24Cxx.c代码
如上图所示,所有的代码都是以 I2C 通信的5个底层函数为基础的,因此我们需要将 “Drive_ I2C .h” 文件包含到代码中,如上图第 02 行代码所示。
下面我们建立一个工程,写一个实例来展示单片机对 AT24C256 的读写应用。应用的功能为首先往 AT24C256 存储器地址 0x08 处写入数据 110,然后从该处把数据读出来显示在 1602 液晶上,以此来验证读写操作的正确性,主文件 “Main AT24C256 .c” 代码如下图所示:
*********************************** * AT24C256(I2C)功能测试
* ******************************************************************
* 【主芯片】:STC89SC52/STC12C5A60S2
* 【主频率】: 11.0592MHz
*
* 【版 本】: V1.0
* 【作 者】: stephenhugh
* 【网 站】:https://rymcu.taobao.com/
* 【邮 箱】:
*
* 【版 权】All Rights Reserved
* 【声 明】此程序仅用于学习与参考,引用请注明版权和作者信息!
*
*******************************************************************/
#include <reg52.h>
#include <Drive_ AT24Cxx .h> // 包含 AT24Cxx 头文件
#include <Drive_1602.h>
#define uchar unsigned char
#define uint unsigned int
sbit DU = P0^6;// 数码管段选、位选引脚定义
sbit WE = P0^7;
uchar str[10]=0;
void delayms(unsigned int z)// 延时函数
{
unsigned int x,y;
for(x=z;x>0;x--)
for(y=78;y>0;y--);
}
void main()
{
uchar dat=0;
Init_1602();
P2 = 0xff;// 关闭所有数码管
WE = 1;
WE = 0;
// 往 AT24Cxx 存储器地址 0x08 处写入数字 110
WrByte_AT24Cxx(0x08,110);
Disp_1602_str(1,2,"AT24C0x test!");
// 读和写之间至少需要间隔 10ms,增加如下代码达到延时的目的
delayms(10);
// 读取 AT24Cxx 存储器地址 0x08 处的数据
dat = RdByte_AT24Cxx(0x08);
str[0]=dat/100+'0';
str[1]=dat%100/10+'0';
str[2]=dat%10+'0';
// 将数据显示在 1602 的第2行第6列处
Disp_1602_str(2,6,str);
while(1);
}
图 15-20 主文件代码
将工程编译后,下载到开发板验证功能的正确性。
图 15-21 显示结果
AT24C256 对写时序有一个特殊的要求,当完成一次数据通信后,需要延迟 tWR 才能开始下一次起始信号如下图所示, tWR 为 AT24C256 内部处理数据时间,查询 AT24C256 数据手册可知为 10ms。因此,我们在主程序代码第 46 行与第 50 行之间插入了第 47 行的 delayms(10); 代码达到延时的目的,我们可以将第 47 行代码注释掉,将无法正确的读出数据。
图 15-22 时序约束
上述工程中需要添加 1602, I2C, AT24Cxx 的驱动文件,如下图所示:
图 15-23 驱动文件
根据上面的介绍,大家很容易发现每隔 10ms 才能进行一次正常的写数据操作是非常浪费时间的,尤其是在进行多个字节写操作的时候。 AT24C256 提供了另外一种多字节的写模式,为页操作模式。首先介绍一下 AT24C256 的内部存储器的分页结构。 AT24C256 总共有 32768 个字节的存储空间,总共分为 512 页,每一页总有 64 各字节。第一页的地址范围为 0x0000~0x0040,依次往下均分为 512 页。页操作模式通信时序如下图所示:
图 15-24 页写通信时序
如上如所示,首先发送起始信号、写器件地址、写起始地址,紧接着写入多个字节,写完一个字节,器件内部会将地址自动加1,最后结束信号。这里需要注意的是,连续写的多个字节必须在同一页内,不能进行跨页连续读写,因此一次通信周期内最多可以写入 64 个字节的数据。如果我们需要写的数据很多,而一页写不下怎么办?首先,启动页写通信,在写的过程中判断是否要写到页的边界了,当到达页边界后停止该次页写通信,再重新发起页写通信将剩余的数据写入,这样便实现了任意个字节的写入,具体函数如下图所示。其中, str 为需要写入的字符串, addr 为写入 AT24C256 的起始地址, len 为写入字符个数。
// 多字节写
void WrStr_AT24Cxx(unsigned char *str,unsigned int addr,unsigned char len)
{
while(len > 0)// 检测上一次是否完成所以数据写操作
{
while(1)
{// 循环检测器件应答信号
Start_I2C();
if(0 == Wr_I2C(0x50<<1)) break;// 收到应答,跳出循环
Stop_I2C();// 没收到应答,发送停止信号,继续循环检测
}
Wr_I2C( addr >>8); // 写入要操作的地址 addr 高字节
Wr_I2C( addr ); // 写入要操作的地址 addr 的低字节
while(len > 0)
{
Wr_I2C(*str++);// 写入一个字节,并将字符串指针指向下一个字符
len--;// 字符数减1
addr++;// 存储地址加1
if(0 == (addr % 64))// 检测是否到达了下一页的起始地址,
break; // 即上一个字节已经写到页的最后边界了
// 跳出停止继续写,每页的起始地址为 64 的倍数,
// 判断对 64 取余是否等于0即可。
}
Stop_I2C();
}
}
图 15-25 多字节写数据
多字节读时序如下图所示,与单字节类似,只是在第一个数据紧接着读取多个数据。这里要注意的是:只有最后一个字节的数据发送非应答信号,前面的数据均发送应答信号。这个也很好理解,因为我们读到最后一个字节时要告诉 AT24C256 我们不再继续读数据了,因此发送非应答信号。
图 15-26 读多字节通讯时序
多字节读取函数代码如下图所示:
// 多字节读
void RdStr_AT24Cxx(unsigned char *str,unsigned int addr,unsigned char len)
{
while(1)
{// 循环检测器件应答信号
Start_I2C();
if(0 == Wr_I2C(0x50<<1)) break;// 收到应答,跳出循环
Stop_I2C();// 没收到应答,发送停止信号,继续循环检测
}
Wr_I2C( addr >>8); // 写入要操作的地址 addr 高字节
Wr_I2C( addr ); // 写入要操作的地址 addr 的低字节
Start_I2C();// 再次发送起始信号
Wr_I2C((0x 50 <<1)|0x01);// 通知地址 50 的器件,接下来读操作
while(len > 1)
{
*str++ = RdACK_I2C();// 读字节并应答
len--;
}
*str = RdNAK_I2C();// 最后一个字节,读字节并非应答
Stop_I2C();
}
图 13-27 多字节读取函数
将上述的多字节写、多字节读取函数添加到驱动文件 “Drive_AT24Cxx.c”、”Drive_AT 24Cxx.h>中,后续应用中只需要将文件添加到项目中,调用相关的函数就可以了。
本小节实现 AT24C256 多字节的读写应用,先向 AT24C256 连续写入多字节数据,然后将数据读出显示在 1602 液晶显示模块上,验证读写的正确性。主程序 MainAT24Cxx-01.c代码如下图所示:
/*******************************************************************
* AT24C256(I2C)功能测试
* ******************************************************************
* 【主芯片】:STC89SC52/STC12C5A60S2
* 【主频率】: 11.0592MHz
*
* 【版 本】: V1.0
* 【作 者】: stephenhugh
* 【网 站】:https://rymcu.taobao.com/
* 【邮 箱】:
*
* 【版 权】All Rights Reserved
* 【声 明】此程序仅用于学习与参考,引用请注明版权和作者信息!
*
*******************************************************************/
#include <reg52.h>
#include <Drive_ AT24Cxx .h> // 包含 AT24Cxx 头文件
#include <Drive_1602.h>
#define uchar unsigned char
#define uint unsigned int
sbit DU = P0^6;// 数码管段选、位选引脚定义
sbit WE = P0^7;
uchar str1[]="AT24c256 Wr Str!";
uchar str2[20];
void main()
{
P2 = 0xff;// 关闭所有数码管
WE = 1;
WE = 0;
Init_1602();//1602 初始化
WrStr_AT24Cxx(str1,0x05, 16 );// 写入 16 个字节
RdStr_AT24Cxx(str2,0x05, 16 );// 读取 16 个字节
Disp_1602_str(1,1,str2); // 将数据从第一行第一列开始显示
while(1);
}
图 13-29 多字节读写验证试验
从地址 0x05 开始写入 16 个字节的数据,有效的验证的多字节的读、写操作。
图 15-29 多字节读写试验结果
本章学习了 I2C 通信原理,并且驱动了基于 I2C 通信的外部存储器芯片 AT24C256。