10天学会嵌入式技术之51单片机-day-8
第十七章 IIC总线
17.1 概述
I2C(Inter-Integrated Circuit),通常简称为 IIC,是一种用在集成电路(IC)之间的串
行通信总线。它是由 Philips(现在的 NXP 半导体)在上世纪 80 年代开发的,并在之后广
泛应用于各种电子设备和嵌入式系统中。
17.2 信号线
IIC 为同步串行通信,使用两根线路进行通信,分别是数据线(SDA)和时钟线
(SCL),SDA 线用于数据传输,SCL 线用于数据传输的同步。SCL 的每个时钟周期,
SDA 传输一位数据。
I2C 规定,数据的接收方会在每个时钟周期的高电平期间读取数据,具体来讲就是在
SCL 处于高电平时,读取 SDA 上的数据,如下图所示。
因此,SDA 必须在 SCL 低电平期准备好要发送的下一位数据,然后在 SCL 高电平期
间保持稳定。
17.3 主从架构
IIC 采用主从架构,一个主设备可连接多个从设备。主设备负责发起通信和控制总线,
而从设备负责响应主设备的请求。如下图所示。
IIC 总线中的每个设备都有一个唯一的地址(用 7 位二进制数字表示),用于在总线上
标识自己。主设备可以根据地址选择性的与特定的从设备进行通信。
需要注意的是,SCL 信号线上的时钟信号始终由主设备产生,而 SDA 信号线上的数据
信号既可由主设备产生,也可由从设备产生。当主设备向从设备发送数据时,SDA 信号由
主设备产生,从设备接收信号;当主设备从从设备读取数据时,SDA 信号由从设备产生,
主设备接收信号。
17.4 通信协议
17.4.1 空闲状态
IIC协议规定,当 SDA 和 SCL 均为高电平时,总线为空闲状态。
17.4.2起始和结束信号
主设备和从设备间的每次通信,都需要以一个起始信号开始,以一个结束信号终止。
起始信号和结束信号的定义如下:
起始信号:当 SCL 处于高电平时,SDA 由高变低。
停止信号:当 SCL 处于高电平时,SDA 由低变高。
起始信号和结束信号,都只能由主设备产生。
17.4.3 确认信号
IIC 协议规定,发送方每发送一个字节(8 位)的数据,接收方都要向发送方回复一个
1 位的确认信号,如下图所示。
如果该确认信号为 0 表示接收方已成功接收到该字节,发送方可继续发送下一字节,
这个信号在 IIC 协议中称为 ACK(Acknowledge);如果该信号为 1,则表示接收方未能成
功接收到该字节,或者不希望接收更多数据,该信号在 IIC 协议中称为 NACK(Not
Acknowledge)
17.4.4 从机地址和读写标识
由于一个 I2C 总线上可能有多个从设备,所以开始通信前,主设备需要先与目标设备
取得联系,然后再进行数据传输,除此之外,主机还需要向目标设备明确本次通信的操作
是写数据还是读数据。
上述操作的实现思路如下:
当主设备发送起始信号之后,会向所有设备发送一个字节的数据,这一个字节中,前
7 位为目标设备地址,第 8 位为读/写标识(1 表示读,0 表示写),如下图所示。
当各从设备收到这个字节的数据后,会将 7 位地址与自身进行对比,相同则会向主设
备回复确认信号,不相同则不做任何回应。
当主设备收到目标设备的确认信号后,便会开始与该设备进行通信。
17.4.5 完整通信流程
综上所述,当主设备想要与某个从设备进行通信时,需要经历如下流程。
(1)发送起始信号
(2)发送目标从设备地址+读写标识位
(3)接收从设备回复的确认信号
(4)与从设备进行数据传输(发送/接收)
(5)发送终止信号
17.5 基础驱动编写
17.5.1 功能分析
为方便后序使用 I2C 协议通信,此处先编写一个通用的基础的驱动,该驱动包含的具
体函数如下图所示
17.5.2 发送起始信号
17.5.2.1 实现思路
17.5.2.2 完整代码
void Dri_IIC_Star()
{SCL=1;//高电平SDA=1;//高电平 空闲状态SDA=0;//起始信号SCL=0;//进入发送信息准备阶段
}
17.5.3 发送一个字节
17.5.3.1 实现思路
17.5.3.2 完整代码
void Dri_Sendbyte(u8 byte)
{u8 i;for(i=0;i<8;i++){SDA=(byte & (0x80>>i))==0? 0:1;SCL=1;SCL=0;}
}
17.5.4 接收确认信号
17.5.4.1 实现思路
bit Dri_IIC_ReceiveACK()
{bit ack;SDA=1;//释放SDASCL=1;//拉高点平ack=SDA;//读取SDASCL=0;//拉低电平 准备发送下一字节return ack;
}
17.5.5 接收一个字节
17.5.5.1 实现思路
u8 Dri_IIC_ReceiveByte()
{u8 byte =0;u8 = i;SDA=1;for(i=0;i<8;i++){SCL=1;byte=(byte<<1) | SDA;SCL=0;}return byte;
}
17.5.6 发送确认信号
17.5.6.1 实现思路
void Dri_IIC_SendAck(bit ack)
{SDA=ack;SCL=1;SCL=0;
}
17.5.7 发送停止信号
void Dri_IIC_Stop()
{SDA=0;SCL=1;SDA=1;
}
17.5.8 完整代码
(1)Dri_IIC.h
#ifndef __DRI_IIC_H__
#define __DRI_IIC_H__
#include <STC89C5xRC.H>
#include "Util.h"
#define SCL P17
#define SDA P16
void Dri_IIC_Start();
void Dri_IIC_Stop();
void Dri_IIC_SendByte(u8 byte);
u8 Dri_IIC_ReceiveByte();
void Dri_IIC_SendAck(bit ack);
bit Dri_IIC_ReceiveAck();
#endif /* __DRI_IIC_H__ */
(2)Dri_IIC.c
#include "Dri_IIC.h"
void Dri_IIC_Start()
{SCL = 1;SDA = 1;SDA = 0;SCL = 0;
}
void Dri_IIC_Stop()
{SDA = 0;SCL = 1;SDA = 1;
}
void Dri_IIC_SendByte(u8 byte)
{u8 i;for (i = 0; i < 8; i++) {SDA = (byte & (0x80 >> i)) == 0 ? 0 : 1;SCL = 1;SCL = 0;}
}
u8 Dri_IIC_ReceiveByte()
{u8 byte = 0;u8 i;SDA = 1;for (i = 0; i < 8; i++) {SCL = 1;byte = (byte << 1) | SDA;SCL = 0;}return byte;
}
void Dri_IIC_SendAck(bit ack)
{SDA = ack;SCL = 1;SCL = 0;
}
bit Dri_IIC_ReceiveAck()
{bit ack;SDA = 1;SCL = 1;ack = SDA;SCL = 0;return ack;
}
第 18 章 I2C 协议-EEPROM
18.1 EEPROM 概述
18.1.1 简介
EEPROM(Electrically Erasable Programmable Read-Only Memory,电可擦写可编程只
读存储器)是一种非易失性存储器(断电后仍能保留数据),可以多次写入和擦除数据。
EEPROM 广泛应用于需要永久存储数据的电子设备中, 常用于存储设备工作模式、用户偏
好设置等信息。
虽然叫做只读存储器(ROM),但 EEPROM 是即可读又可写的,这种看似矛盾的命名
源于其历史发展。最早期的 ROM 是在制造过程中被一次性编程,出厂之后便只能读取,
不能再修改,后期的各种可编程(可写)的 ROM,都是基于最早期的 ROM 发展而来的,
所以 ROM 这个名称就被一直沿用下来了。现在的 ROM 基本指非易失性存储器。
本课程使用的 EEPROM 型号是 AT24C02CN,其存储容量为 2K(2048 位,256 字节),
采用 I2C 协议进行读写。
18.1.2 引脚功能
18.1.3 内存结构
18.1.4 写操作
AT24C02CN 提供 Byte Write 和 Page Write 两种写操作模式,下面逐一介绍。
(1)Byte Write
Byte Write 用于写入一个字节,其时序图如下
(2)Page Write
Page Write 用于连续写入一页数据,其时序图如下。
18.1.5 读操作
AT24C02CN 提 供 了 3 种 读 操 作 , 分 别 是 Current Address Read ( 读 当 前 地 址 )、
Random Read(随机读)、Sequential Read(连续读)。下面逐一介绍。
(1)Current Address Read
EEPROM 内部有一个 Address Register(地址寄存器),用于记录当前操作(读/写)的
字节地址,每当完成一个字节的操作后(读/写),该地址会自动指向下一个字节。
Current Address Read 读取的就是 Address Register 中的地址所指向的这一个字节,具体
读时序如下。
(2)Random Read
Random Read 用于读取任意指定地址的一个字节,时序图如下。
(3)Sequential Read
Sequential Read 用于读取连续的多个字节。其起始位置可以是 Address Register 记录的
当前地址,也可以是用户指定的任意位置,指定起始位置的方式仍然是在 Sequential Read
前增加一个 Dummy Write 操作。
下图是从指定位置开始的连续读操作的时序图。
18.2 需求描述
先向 EEPROM 写入点阵 LED 中的“尚硅谷”三个字的字模,再将其读取来并显示在
点阵 LED 上。
18.3 硬件设计
18.3.1 硬件原理图
硬件原理图如下。
18.4 软件设计
(1)复制点阵 LED 驱动代码
将 点 阵 LED 项 目 中 的 Int_MatrixLED.h 、 Int_MatrixLED.c 、 Dir_Timer0.h 和
Dri_Timer0.c 复制当前项目。
(2)Int_EEPROM.h
#ifndef __INT_EEPROM_H__
#define __INT_EEPROM_H__
#include <STC89C5xRC.H>
#define DEV_ADDR 0xA0
#define PAGE_SIZE 16
#include "Dri_IIC.h"
#include "Util.h"
/**
* @brief 向 EEPROM 指定位置写入多个字节
*
* @param addr 起始地址
* @param bytes 要写入的字节
* @param len 要写入的字节个数
* @return bit 0:写入成功 1:写入失败
*/
bit Int_EEPROM_WriteBytes(u8 addr, u8 *bytes, u8 len);
/**
* @brief 从 EEPROM 指定位置读取多个字节
*
* @param addr 起始地址
* @param bytes 存储读取的字节用的数组指针
* @param len 要读取的字节个数
* @return bit 0:读取成功 1:读取失败
*/
bit Int_EEPROM_ReadBytes(u8 addr, u8 *bytes, u8 len);
#endif /* __INT_EEPROM_H__ */
(3)Int_EEPROM.c
#include "Int_EEPROM.h"bit Int_EEPROM_WritePage(u8 addr, u8 *bytes, u8 len)
{u8 i;bit ack = 0;// 发送起始信号Dri_IIC_Start();// 发送设备地址Dri_IIC_SendByte(DEV_ADDR);ack |= Dri_IIC_ReceiveAck();// 发送字地址Dri_IIC_SendByte(addr);ack |= Dri_IIC_ReceiveAck();// 发送数据for (i = 0; i < len; i++) {Dri_IIC_SendByte(bytes[i]);ack |= Dri_IIC_ReceiveAck();
}
// 发送结束信号Dri_IIC_Stop();return ack;
}
bit Int_EEPROM_WriteBytes(u8 addr, u8 *bytes, u8 len)
{
// 当前页剩余空间u8 page_remain;bit ack = 0;while (len > 0) {page_remain = PAGE_SIZE - (addr % PAGE_SIZE);if (len > page_remain) {// 当前页空间不足,先写满当前页ack |= Int_EEPROM_WritePage(addr, bytes, page_remain);// 剩余内容写到下一页len -= page_remain;bytes += page_remain;addr += page_remain;// 等待 EEPROM 内部写入完成Delay1ms(5);} else {// 当前页空间足够,直接写入剩余内容ack |= Int_EEPROM_WritePage(addr, bytes, len);len = 0;}}return ack;
}
bit Int_EEPROM_ReadBytes(u8 addr, u8 *bytes, u8 len)
{bit ack = 0;u8 i;// 发送起始信号Dri_IIC_Start();// 发送设备地址Dri_IIC_SendByte(DEV_ADDR);ack |= Dri_IIC_ReceiveAck();// 发送字地址Dri_IIC_SendByte(addr);ack |= Dri_IIC_ReceiveAck();// 再次发送起始信号Dri_IIC_Start();// 发送设备地址Dri_IIC_SendByte(DEV_ADDR + 1);ack |= Dri_IIC_ReceiveAck();// 读取数据for (i = 0; i < len; i++) {bytes[i] = Dri_IIC_ReceiveByte();Dri_IIC_SendAck(i == len - 1 ? 1 : 0);}// 发送结束信号Dri_IIC_Stop();return ack;
}
(4)Main.c
#include "Dri_MartixLED.h"
#include "Dri_Timer0.h"
#include "Int_EEPROM.h"
void main()
{// 写入 EEPROM// unsigned char i = 0;// unsigned char pic[] = {// 0xF8, 0x0A, 0xEC, 0xAF, 0xEC, 0x8A, 0xF8, 0x00,// 尚// 0x10, 0xF9, 0x97, 0xF1, 0x88, 0xAA, 0xFF, 0xAA, 0x88, 0x00,// 硅// 0x14, 0x0A, 0xF5, 0x92, 0x92, 0xF5, 0x0A, 0x14,0x00// 谷
// };
// Int_EEPROM_WriteBytes(0, pic, sizeof(pic));
// while (1) {}// 读取 EEPROM
unsigned char i = 0;
u8 pic[27];
Int_EEPROM_ReadBytes(0, pic, sizeof(pic));Dri_Timer0_Init();
Dri_MatrixLED_Init();while (1) {if (i >= sizeof(pic)) {i = 0;}Dri_MatrixLed_Shift(pic[i++]);Delay1ms(300);}
}