介绍
I2C(Inter-Integrated Circuit) 总线是由 PHILIPS 公司开发的两线式串行总线, 用于连接微控制器及其外围设备。 是微电子通信控制领域广泛采用的一种总线标准。 它是同步通信的一种特殊形式, 具有接口线少, 控制方式简单,器件封装形式小, 通信速率较高等优点。 I2C 总线只有两根双向信号线。 一根是数据线 SDA, 另一根是时钟线 SCL。 由于其管脚少, 硬件实现简单, 可扩展性强等特点, 因此被广泛的使用在各大集成芯片内。
I2C物理层
- 支持多设备的总线
- 一个I2C总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。
- 每个连接到总线的设备都有一个独立的地址,主机可以通过这个地址来进行不同设备的访问。
- 总线通过上拉电阻连接到电源。当 I2C 设备空闲时, 会输出高阻态, 而当所有设备都空闲, 都输出高阻态时, 由上拉电阻把总线拉成高电平。
- 多个主机同时使用总线时, 为了防止数据冲突, 会利用仲裁方式决定由哪个设备占用总线。
- 具有三种传输模式: 标准模式传输速率为 100kbit/s, 快速模式为400kbit/s, 高速模式下可达 3.4Mbit/s, 但目前大多 I2C 设备尚不支持高速模式。
- 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
I2C总线的一些术语
I2C协议层
(1)数据有效性规定
I2C 总线进行数据传送时, 时钟信号为高电平期间, 数据线上的数据必须保持稳定, 只有在时钟线上的信号为低电平期间, 数据线上的高电平或低电平状态才允许变化。
每次数据传输都以字节为单位, 每次传输的字节数不受限制。
(2)起始和停止信号
SCL 线为高电平期间, SDA 线由高电平向低电平的变化表示起始信号; SCL线为高电平期间, SDA 线由低电平向高电平的变化表示终止信号。
起始和终止信号都是由主机发出的, 在起始信号产生后, 总线就处于被占用的状态; 在终止信号产生后, 总线就处于空闲状态。
(3)应答响应
每当发送器件传输完一个字节的数据后, 后面必须紧跟一个校验位, 这个校验位是接收端通过控制 SDA(数据线) 来实现的, 以提醒发送端数据我这边已经接收完成, 数据传送可以继续进行。 这个校验位其实就是数据或地址传输过程中的响应。 响应包括“应答(ACK)” 和“非应答(NACK)” 两种信号。 作为数据接收端时, 当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后, 若希望对方继续发送数据, 则需要向对方发送“应答(ACK)” 信号即特定的低电平脉冲,
发送方会继续发送下一个数据; 若接收端希望结束数据传输, 则向对方发送“非应答(NACK)” 信号即特定的高电平脉冲, 发送方接收到该信号后会产生一个停止信号, 结束信号传输。
每一个字节必须保证是 8 位长度。 数据传送时, 先传送最高位(MSB) , 每一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。
由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据) , 它必须将数据线置于高电平, 而由主机产生一个终止信号以结束总线的数据传送。
如果从机对主机进行了应答, 但在数据传送一段时间后无法继续接收更多的数据时, 从机可以通过对无法接收的第一个数据字节的“非应答” 通知主机, 主机则应发出终止信号以结束数据的继续传送。
当主机接收数据时, 它收到最后一个数据字节后, 必须向从机发出一个结束传送的信号。 这个信号是由对从机的“非应答” 来实现的。 然后, 从机释放 SDA线, 以允许主机产生终止信号。
这些信号中, 起始信号是必需的, 结束信号和应答信号都可以不要。
(4)总线的寻址方式
I2C 总线寻址按照从机地址位数可分为两种, 一种是 7 位, 另一种是 10位。 采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节) 的位定义如下:
D7~D1 位组成从机的地址。 D0 位是数据传送方向位, 为“ 0” 时表示主机向从机写数据, 为“1” 时表示主机由从机读数据。
10 位寻址和 7 位寻址兼容, 而且可以结合使用。 10 位寻址不会影响已有的 7 位寻址, 有 7 位和 10 位地址的器件可以连接到相同的 I2C 总线。
7位寻址:当主机发送了一个地址后, 总线上的每个器件都将头 7 位与它自己的地址比较, 如果一样, 器件会判定它被主机寻址, 其他地址不同的器件将被忽略后面的数据信号。 至于是从机接收器还是从机发送器, 都由 R/W 位决定的。 从机的地址由固定部分和可编程部分组成。 在一个系统中可能希望接入多个相同的从机, 从机地址中可编程部分决定了可接入总线该类器件的最大数目。 如一个从机的 7 位寻址位有 4 位是固定位, 3 位是可编程位, 这时仅能寻址 8 个同样的
器件, 即可以有 8 个同样的器件接入到该 I2C 总线系统中。
(5)数据传输
在起始信号后必须传送一个从机的地址(7 位) , 第 8 位是数据的传送方向位(R/W) , 用“ 0” 表示主机发送(写) 数据(W) , “ 1” 表示主机接收数据(R) 。 每次数据传送总是由主机产生的终止信号结束。 但是, 若主机希望继续占用总线进行新的数据传送, 则可以不产生终止信号, 马上再次发出起始信号对另一从机进行寻址。
a、 主机向从机发送数据, 数据传送方向在整个传送过程中不变
注意: 有阴影部分表示数据由主机向从机传送, 无阴影部分则表示数据由从机向主机传送。 A 表示应答, A 非表示非应答(高电平) 。 S 表示起始信号, P 表示终止信号。
b、 主机在第一个字节后, 立即从从机读数据
c、 在传送过程中, 当需要改变传送方向时, 起始信号和从机地址都被重复产生一次, 但两次读/写方向位正好相反。
AT24C02介绍
AT24C01/02/04/08/16...是一个 1K/2K/4K/8K/16K 位串行 CMOS, 内部含有128/256/512/1024/2048 个 8 位字节, AT24C01 有一个 8 字节页写缓冲器,AT24C02/04/08/16 有一个 16 字节页写缓冲器。 该器件通过 I2C 总线接口进行操作, 它有一个专门的写保护功能。 我们开发板上使用的是 AT24C02(EEPROM)芯片, 此芯片具有 I2C 通信接口, 芯片内保存的数据在掉电情况下都不丢失,所以通常用于存放一些比较重要的数据等。
AT24C02 器件地址为 7 位, 高 4 位固定为 1010, 低 3 位由 A0/A1/A2 信PRECHIN号线的电平决定。 因为传输地址或数据是以字节为单位传送的, 当传送地址时,器件地址占 7 位, 还有最后一位(最低位 R/W) 用来选择读写方向, 它与地址无关。
我们开发板已经将芯片的 A0/A1/A2 连接到 GND, 所以器件地址为1010000, 即 0x50(未计算最低位) 。 如果要对芯片进行写操作时, R/W 即为 0,写器件地址即为 0XA0; 如果要对芯片进行读操作时, R/W 即为 1, 此时读器件地址为 0XA1。 开发板上也将 WP 引脚直接接在 GND 上, 此时芯片允许数据正常读写。
I2C 总线时序如下图所示:
硬件设计
开发板上 EEPROM 模块电路
从图中可以看出, 芯片的 SCL 和 SDA 管脚是连接在单片机的 P2.1 和 P2.0 上,在介绍 IIC 总线的时候我们说过, 为了让 IIC 总线默认为高电平, 通常会在 IIC总线上接上拉电阻, 在图中并没有看到 SCL 和 SDA 管脚有上拉电阻, 这是因为开发板单片机 IO 都外接了 10K 上拉电阻, 当单片机 IO 口连接到芯片的 SCL 和 SDA脚时即相当于它们外接上拉电阻。
软件设计
创建多文件工程
在电脑上创建一个实验文件夹, 为了与教程配套, 这里命名为“I2C-EEPROM实验” , 然后在该文件夹内新建 App、 Obj、 Public、 User 四个文件夹, 如下所示:
- App 文件夹: 用于存放外设驱动文件, 如 LED、 数码管、 定时器等。
- Obj 文件夹: 用于存放编译产生的 c/汇编/链接的列表清单、 调试信息、 hex
- 文件、 预览信息、 封装库等文件。
- Public 文件夹: 用于存放 51 单片机公共的文件, 如延时、 51 头文件、 变量
- 类型重定义等。
- User 文件夹: 用于存放用户主函数文件, 如 main.c。
新建工程
向工程添加文件
按照需要给工程分组并添加对应文件, 这里我们在工程中分 3 组, User、 App、Publi, 至于前面创建的 Obj 文件夹是在工程中无需体现, 因为只是编译器生成的一些中间文件和.hex 执行文件。 通常在工程组的命名与创建的文件夹名保持一致, 方便查找到源文件位置。
然后就是给每个组添加对应的.c 源文件
可以选择到要添加的.c 文件(红色标记 5) , 然后鼠标左键双击也可直接添加进去, 就免去了点击红色标记 6 这步。 添加好后, 在右侧就会显示对应组中已添加的文件。
配置魔术棒选项卡
- Output 选项卡中把输出文件夹定位到我们实验目录下的 Obj 文件夹如果想在编译的过程中生成 hex 文件, 那么 Create HEX File 选项勾上。
- Listing 选项卡中把输出文件夹也定位到我们实验目录下的 Obj 文件夹。 其它设置默认, 配置如下:
- C51 选项卡配置, 此处目的是将我们前面添加到工程组中的文件路径包括进来, 否则程序中调用其他文件夹的头文件则会报错找不到头文件路径, 具体步骤如下:
添加的头文件路径是指, 在 I2C-EEPROM 实验文件夹下里面, 哪些文件夹内含有.h 头文件, 并且需要被调用到的, 通常我们会把只要含有头文件的文件夹都选择进去。 比如本例程中 App 内含有很多子文件夹, 它们里面都含有头文件,因此要分别添加, Public 也含有头文件, 所以也要添加。 添加完成后如下:
仿真器配置
有关 51 仿真器配置可参考前面章节内容, 此处不再重复。然后编译一下工程, 编译后结果 0 错误 0 警告, 表明我们创建的多文件工程没有问题。 如下:
至此, 我们就成功创建好一个多文件工程模板。 在以后的实验中, 凡是涉及到多个外设资源模块的都可以使用该工程模板, 尤其是对重复利用已编写过的外设驱动。
防止头文件被重复包含,避免引起编译错误。
#ifndef _key_H
#define _key_H
//code
#endif
./App/24c02
//24c02.h
#ifndef _24c02_H
#define _24c02_H
#include "public.h"
void at24c02_write_one_byte(u8 addr,u8 dat);//AT24C02指定地址写数据
u8 at24c02_read_one_byte(u8 addr);//AT24C02指定地址读数据
#endif
//24c02.c
#include "24c02.h"
#include "iic.h"
/*******************************************************************************
* 函 数 名 : at24c02_write_one_byte
* 函数功能 : 在AT24CXX指定地址写入一个数据
* 输 入 : addr:写入数据的目的地址
dat:要写入的数据
* 输 出 : 无
*******************************************************************************/
void at24c02_write_one_byte(u8 addr,u8 dat)
{
iic_start();
iic_write_byte(0XA0); //发送写命令
iic_wait_ack();
iic_write_byte(addr); //发送写地址
iic_wait_ack();
iic_write_byte(dat); //发送字节
iic_wait_ack();
iic_stop(); //产生一个停止条件
delay_ms(10);
}
/*******************************************************************************
* 函 数 名 : at24c02_read_one_byte
* 函数功能 : 在AT24CXX指定地址读出一个数据
* 输 入 : addr:开始读数的地址
* 输 出 : 读到的数据
*******************************************************************************/
u8 at24c02_read_one_byte(u8 addr)
{
u8 temp=0;
iic_start();
iic_write_byte(0XA0); //发送写命令
iic_wait_ack();
iic_write_byte(addr); //发送写地址
iic_wait_ack();
iic_start();
iic_write_byte(0XA1); //进入接收模式
iic_wait_ack();
temp=iic_read_byte(0); //读取字节
iic_stop(); //产生一个停止条件
return temp; //返回读取的数据
}
./App/iic
//iic.h
#ifndef _iic_H
#define _iic_H
#include "public.h"
//定义EEPROM控制脚
sbit IIC_SCL=P2^1;//SCL时钟线
sbit IIC_SDA=P2^0;//SDA数据线
//IIC所有操作函数
void iic_start(void); //发送IIC开始信号
void iic_stop(void); //发送IIC停止信号
void iic_write_byte(u8 txd); //IIC发送一个字节
u8 iic_read_byte(u8 ack); //IIC读取一个字节
u8 iic_wait_ack(void); //IIC等待ACK信号
void iic_ack(void); //IIC发送ACK信号
void iic_nack(void); //IIC不发送ACK信号
#endif
//iic.c
#include "iic.h"
/*******************************************************************************
* 函 数 名 : iic_start
* 函数功能 : 产生IIC起始信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_start(void)
{
IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
/*******************************************************************************
* 函 数 名 : iic_stop
* 函数功能 : 产生IIC停止信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_stop(void)
{
IIC_SDA=0;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=1; //当SCL为高电平时,SDA由低变为高
delay_10us(1);
}
/*******************************************************************************
* 函 数 名 : iic_ack
* 函数功能 : 产生ACK应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_ack(void)
{
IIC_SCL=0;
IIC_SDA=0; //SDA为低电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
/*******************************************************************************
* 函 数 名 : iic_nack
* 函数功能 : 产生NACK非应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_nack(void)
{
IIC_SCL=0;
IIC_SDA=1; //SDA为高电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
/*******************************************************************************
* 函 数 名 : iic_wait_ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败
0,接收应答成功
*******************************************************************************/
u8 iic_wait_ack(void)
{
u8 time_temp=0;
IIC_SCL=1;
delay_10us(1);
while(IIC_SDA) //等待SDA为低电平
{
time_temp++;
if(time_temp>100)//超时则强制结束IIC通信
{
iic_stop();
return 1;
}
}
IIC_SCL=0;
return 0;
}
/*******************************************************************************
* 函 数 名 : iic_write_byte
* 函数功能 : IIC发送一个字节
* 输 入 : dat:发送一个字节
* 输 出 : 无
*******************************************************************************/
void iic_write_byte(u8 dat)
{
u8 i=0;
IIC_SCL=0;
for(i=0;i<8;i++) //循环8次将一个字节传出,先传高再传低位
{
if((dat&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
dat<<=1;
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
delay_10us(1);
}
}
/*******************************************************************************
* 函 数 名 : iic_read_byte
* 函数功能 : IIC读一个字节
* 输 入 : ack=1时,发送ACK,ack=0,发送nACK
* 输 出 : 应答或非应答
*******************************************************************************/
u8 iic_read_byte(u8 ack)
{
u8 i=0,receive=0;
for(i=0;i<8;i++ ) //循环8次将一个字节读出,先读高再传低位
{
IIC_SCL=0;
delay_10us(1);
IIC_SCL=1;
receive<<=1;
if(IIC_SDA)receive++;
delay_10us(1);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
./App/key
//key.h
#ifndef _key_H
#define _key_H
#include "public.h"
//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
u8 key_scan(u8 mode);
#endif
//key.c
#include "key.h"
/*******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1按下
KEY2_PRESS:K2按下
KEY3_PRESS:K3按下
KEY4_PRESS:K4按下
KEY_UNPRESS:未有按键按下
*******************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key=1;
if(mode)key=1;//连续扫描按键
if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
{
delay_10us(1000);//消抖
key=0;
if(KEY1==0)
return KEY1_PRESS;
else if(KEY2==0)
return KEY2_PRESS;
else if(KEY3==0)
return KEY3_PRESS;
else if(KEY4==0)
return KEY4_PRESS;
}
else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
{
key=1;
}
return KEY_UNPRESS;
}
./App/smg
//smg.h
#ifndef _smg_H
#define _smg_H
#include "public.h"
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
//定义数码管位选信号控制脚
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;
void smg_display(u8 dat[],u8 pos);
#endif
//smg.c
#include "smg.h"
//共阴极数码管显示0~F的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
/*******************************************************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
* 输 入 : dat:要显示的数据
pos:从左开始第几个位置开始显示,范围1-8
* 输 出 : 无
*******************************************************************************/
void smg_display(u8 dat[],u8 pos)
{
u8 i=0;
u8 pos_temp=pos-1;
for(i=pos_temp;i<8;i++)
{
switch(i)//位选
{
case 0: LSC=1;LSB=1;LSA=1;break;
case 1: LSC=1;LSB=1;LSA=0;break;
case 2: LSC=1;LSB=0;LSA=1;break;
case 3: LSC=1;LSB=0;LSA=0;break;
case 4: LSC=0;LSB=1;LSA=1;break;
case 5: LSC=0;LSB=1;LSA=0;break;
case 6: LSC=0;LSB=0;LSA=1;break;
case 7: LSC=0;LSB=0;LSA=0;break;
}
SMG_A_DP_PORT=gsmg_code[dat[i-pos_temp]];//传送段选数据
delay_10us(100);//延时一段时间,等待显示稳定
SMG_A_DP_PORT=0x00;//消音
}
}
./Public
//public.h
#ifndef _public_H
#define _public_H
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
void delay_10us(u16 ten_us);
void delay_ms(u16 ms);
#endif
//public.c
#include "public.h"
/*******************************************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数,ten_us=1时,大约延时10us
* 输 入 : ten_us
* 输 出 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{
while(ten_us--);
}
/*******************************************************************************
* 函 数 名 : delay_ms
* 函数功能 : ms延时函数,ms=1时,大约延时1ms
* 输 入 : ms:ms延时时间
* 输 出 : 无
*******************************************************************************/
void delay_ms(u16 ms)
{
u16 i,j;
for(i=ms;i>0;i--)
for(j=110;j>0;j--);
}
./User
//main.c
/**************************************************************************************
深圳市普中科技有限公司(PRECHIN 普中)
技术支持:www.prechin.net
PRECHIN
普中
实验名称:I2C-EEPROM实验
接线说明:
实验现象:下载程序后,数码管右4位显示0,按K1键将数据写入到EEPROM内保存,
按K2键读取EEPROM内保存的数据,按K3键显示数据加1,按K4键显示数据清零,
最大能写入的数据是255。
注意事项:
***************************************************************************************/
#include "public.h"
#include "24c02.h"
#include "key.h"
#include "smg.h"
#define EEPROM_ADDRESS 0 //定义数据存入EEPROM的起始地址
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void main()
{
u8 key_temp=0;
u8 save_value=0;
u8 save_buf[3];
while(1)
{
key_temp=key_scan(0);
if(key_temp==KEY1_PRESS)
{
at24c02_write_one_byte(EEPROM_ADDRESS,save_value);
}
else if(key_temp==KEY2_PRESS)
{
save_value=at24c02_read_one_byte(EEPROM_ADDRESS);
}
else if(key_temp==KEY3_PRESS)
{
save_value++;
if(save_value==255)save_value=255;
}
else if(key_temp==KEY4_PRESS)
{
save_value=0;
}
save_buf[0]=save_value/100;
save_buf[1]=save_value%100/10;
save_buf[2]=save_value%100%10;
smg_display(save_buf,6);
}
}