【I2C通讯协议】I2C入门认知篇

【I2C通讯协议】I2C入门认知篇

一、I2C协议简介

1.I2C概述

IIC全称Inter-Integrated Circuit (集成电路总线) 是由PHILIPS公司在80年代开发的两线式串行总线,用于连接微控制器及其外围设备。IIC属于半双工(同一时间只可以单向通信)同步通信方式。

多设备之间的通讯如果使用串口通讯,则每个设备要有足够的独立串口收发组,而采用I2C就能极大简化设备硬件要求和电路

I2C一般采用一主多从,其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控

串口通讯:

在这里插入图片描述

I2C通讯:

在这里插入图片描述

在这里插入图片描述

2.I2C构成

  • IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,其时钟信号是由主控器件产生。
  • 所有接到IIC总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线 的SCL上。
  • 对于并联在一条总线上的每个IC都有唯一的地址。

在这里插入图片描述

3.IIC协议规则

IIC总线在传输数据的过程中一共有三种类型信号,分别为:开始信号、结束信号和应答信号。

起始位,停止位,数据位,速度 这些信号中,起始信号是必需的,结束信号和应答信号

3.1 空闲状态(初始状态)

因为IIC的 SCL 和SDA 都需要接上拉电阻,保证空闲状态的稳定性
所以IIC总线在空闲状态下SCL 和SDA都保持高电平

在这里插入图片描述

3.2 起始信号

SCL保持高电平,SDA由高电平变为低电平后,延时(>4.7us),SCL变为低电平, 比如发送设备唯一地址

在这里插入图片描述

在这里插入图片描述

3.3 终止信号(停止信号)

SCL保持高电平,SDA由低电平变为高电平

在这里插入图片描述

在这里插入图片描述

3.4 应答信号(逻辑“0” “1”)

每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据。

  • 应答信号:主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答。
  • 应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
  • 应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

在这里插入图片描述

在这里插入图片描述

例如:

在这里插入图片描述

  • 每发送一个字节(8个bit), 在一个字节传输的8个时钟后的第九个时钟期间,接收器接收数据后必须回一个ACK应答信号给发送器,这样才能进行数据传输。
  • 应答出现在每一次主机完成8个数据位传输后紧跟着的时钟周期,低电平0表示应答,1表示非应答。

在这里插入图片描述

3.5 数据有效性

IIC信号在数据传输过程中,当SCL=1高电平时,数据线SDA必须保持稳定状态,不允许有电平跳变,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。

SCL=1时 数据线SDA的任何电平变换会看做是总线的起始信号或者停止信号。

也就是在IIC传输数据的过程中,SCL时钟线会频繁的转换电平,以保证数据的传输

在这里插入图片描述

3.6 总线数据发送时序图

在这里插入图片描述

  • 写数据帧

    在这里插入图片描述

  • 读数据帧

    在这里插入图片描述

  • 读 写 数据帧

    在这里插入图片描述

4. I2C扩展:IIC为什么用开漏输出和上拉电阻?

4.1 开漏输出的作用

作用1:防止短路

I2C协议支持多个主设备与多个从设备在一条总线上,如果不用开漏输出,而用推挽输出,会出现主设备之间短路的情况。所以总线一般会使用开漏输出。

  • 在一些情况下(比如总线), 多个GPIO口可能会连接在同一根线上, 存在某个GPIO输出高电平, 另一个GPIO输出低电平的情况. 如果使用推挽输出, 你会发现这个GPIO的VCC和另一个GPIO的GND接在了一起, 也就是短路了(凉凉了).
  • 如果换成开漏输出呢? VCC和GND多了个电阻, 这样电路就是安全的.所以总线一般会使用开漏输出

在这里插入图片描述

作用2:线与

  • 开漏输出还能实现 线与 (自行百度), 减少一个与门, 简化电路
4.2 上拉电阻的作用

接上拉电阻是因为I2C通信需要输出高电平的能力。一般开漏输出无法输出高电平,如果在漏极接上拉电阻,则可以进行电平转换。

I2C由两条总线SDA和SCL组成。连接到总线的器件的输出级必须是漏极开路,都通过上拉电阻连接到电源,这样才能够实现“线与”功能。当总线空闲时,这两条线路都是高电平。


注:上拉电阻选取

上拉电阻阻值怎么确定?

  • 一般IO端口的驱动能力在2mA~4mA量级。

阻值不能过小:

  • 功耗问题。如果上拉阻值过小,VDD灌入端口的电流将较大,功耗会很大,导致端口输出的低电平值增大(I2C协议规定,端口输出低电平的最高允许值为0.4V)。故通常上拉电阻应选取不低于1K的电阻(当VDD=3V时,灌入电流不超过3mA)。

阻值不能过大:

  • 速度问题。它取决于上拉电阻和线上电容形成的RC延时,RC延时越大,波形越偏离方波趋向于正弦波,数据读写正确的概率就越低,所以上拉电阻不能过大。

I2C总线上的负载电容不能超过400pF。当I2C总线上器件逐渐增多时,总线负载电容也相应增加。当总的负载电容大于400pF时,就不能可靠的工作。这也是I2C的局限性。

(建议上拉电阻可选用1.5K,2.2K,4.7K)


4.3 I2C总线基本操作
  • IIC只有两根线(SCL和SDA), 怎么判断哪个主设备占用总线(当然是先来后到了).

  • 假设主设备A需要启动IIC, 他需要在SCL高电平时, 将SDA由高电平转换为低电平作为启动信号. 主设备A在把SDA拉高后, 它需要再检查一下SDA的电平.

  • SDA是高电平, 说明主设备A可以占用总线, 然后主设备A将SDA拉低, 开始通信.

  • SDA是低电平, 说明有人已经捷足先登了, 主设备A不能占用总线, 结束通信.

  • 为什么? 因为线与. 如果主设备A拉高SDA时, 已经有其他主设备将SDA拉低了. 由于 1 & 0 = 0 那么主设备A在检查SDA电平时, 会发现不是高电平, 而是低电平. 说明其他主设备抢占总线的时间比它早, 主设备A只能放弃占用总线. 如果是高电平, 则可以占用.

二、实现软件I2C(直接上手)

1.软硬件I2C概述

I2C有两种实现方式,一种是硬件I2C,另一种是软件I2C

  • 软件I2C一般是用GPIO引脚,用代码控制管脚状态以模拟I2C通讯波形。
  • 硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用,直接调用内部寄存器进行配置通讯波形(类似有封装好的波形通讯函数可直接调用)。

硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活

  • 软件I2C 是通过GPIO,软件模拟寄存器的工作方式,而硬件(固件)I2C是直接调用内部寄存器进行配置。
  • 如果要从具体硬件上来看,可以去看下芯片手册。因为固件I2C的端口是固定的,所以会有所区别

2.代码实现I2C时序

总的来说,软件I2C就是要编写代码封装“实现该通讯波形时序”的函数(根据时序控制GPIO引脚的电平变化),方便后续调用


2.1 起始信号函数

在这里插入图片描述

scl、sda为单片机上两个不同的gpio引脚

//起始信号
void I2C_Start()
{
	scl = 1;
	sda = 1;
	delay5us();
	sda = 0;
	delay5us();
	scl = 0;
}

2.2 终止信号函数

在这里插入图片描述

//停止信号
void I2C_Stop()
{
	scl = 1;
	sda = 0;
	delay5us();
	sda = 1;
	delay5us();
	sda = 0;
}

2.3 应答信号函数(获取从机应答信号:“0”或“1”)

在这里插入图片描述

//获取应答信号,逻辑“0”或“1”
char I2C_ACK()
{
	char flag;   //应答标志
	scl = 0;     //先拉低SCL,使得SDA数据可以发生改变
	sda = 1;     //就在时钟脉冲9期间释放数据线
	delay5us();
	scl = 1;
	delay5us();
	flag = sda;  //获取从机返回的应答信号“0”或“1”
	delay5us();
	scl = 0;
	delay5us();
	
	return flag;
}

2.4 发送字节数据函数(当scl=1时,sda状态不变,则认为sda为传输的数据)

数据传输格式

  • SDA线上的数据在SCL时钟“高”期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的“高”或“低”状态才可以改变

  • 输出到SDA线上的每个字节必须是8位,数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)

  • 当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位ACK, 此时才认为一个字节真正的被传输完成 ,如果一段时间内没有收到从机的应答信号,则自动认为从机已正确接收到数据

// 发送字节数据函数
void I2C_Send_Byte(char dataSend)
{
	int i;
	
	for(i=0; i<8; i++){
		scl = 0;       //scl拉低,让sda做好数据准备
		//1000 0000 获取dataSend的最高位,给sda
		sda = dataSend & 0x80; 
		delay5us();    //发送数据建立时间
		scl = 1;       //scl拉高开始发送
		delay5us();    //数据发送时间
		scl = 0;       //发送完毕拉低
		delay5us();
		dataSend = dataSend << 1;   //数据左移
	}
}

3.I2C实战(暂不作要求,后续可单独学习)

3.1 硬件准备
  • 0.96寸 SSD1306 OLED
  • STC89C52开发板(这里以C51/52为例)
3.2 OLED模块介绍

该模块是IIC接口,引脚分别为VCC,GND,SCL,SDA

在这里插入图片描述

该模块的写入时序,如下图:

可结合后面的代码注释进行理解(在此不作过多介绍)

在这里插入图片描述

写命令/数据的代码:

/*
1. start()
2. 写入 b0111 1000 0x78
3. ACK
4. cotrol byte: (0)(0)000000 写入命令 (0)(1)000000写入数据
5. ACK
6. 写入指令/数据
7. ACK
8. STOP
*/
//写命令
void Oled_Write_Cmd(char dataCmd)
{
    // 1. start()
    I2C_Start();
    // 2. 写入从机地址 b0111 1000 0x78
    I2C_Send_Byte(0x78);
    // 3. ACK
    I2C_ACK();
    // 4. cotrol byte: (0)(0)000000 写入命令 (0)(1)000000写入数据
    I2C_Send_Byte(0x00);
    // 5. ACK
    I2C_ACK();
    //6. 写入指令/数据
    I2C_Send_Byte(dataCmd);
    //7. ACK
    I2C_ACK();
    //8. STOP
    I2C_Stop();
}

//写数据
void Oled_Write_Data(char dataData)
{
    // 1. start()
    I2C_Start();
    // 2. 写入从机地址 b0111 1000 0x78
    I2C_Send_Byte(0x78);
    // 3. ACK
    I2C_ACK();
    // 4. cotrol byte: (0)(0)000000 写入命令 (0)(1)000000写入数据
    I2C_Send_Byte(0x40);
    // 5. ACK
    I2C_ACK();
    ///6. 写入指令/数据
    I2C_Send_Byte(dataData);
    //7. ACK
    I2C_ACK();
    //8. STOP
    I2C_Stop();
}

OLDE模块的初始化和清屏函数代码:

//初始化
void Oled_Init(void){
    Oled_Write_Cmd(0xAE);//--display off
    Oled_Write_Cmd(0x00);//---set low column address
    Oled_Write_Cmd(0x10);//---set high column address
    Oled_Write_Cmd(0x40);//--set start line address
    Oled_Write_Cmd(0xB0);//--set page address
    Oled_Write_Cmd(0x81); // contract control
    Oled_Write_Cmd(0xFF);//--128
    Oled_Write_Cmd(0xA1);//set segment remap
    Oled_Write_Cmd(0xA6);//--normal / reverse
    Oled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)
    Oled_Write_Cmd(0x3F);//--1/32 duty
    Oled_Write_Cmd(0xC8);//Com scan direction
    Oled_Write_Cmd(0xD3);//-set display offset
    Oled_Write_Cmd(0x00);//
    Oled_Write_Cmd(0xD5);//set osc division
    Oled_Write_Cmd(0x80);//
    Oled_Write_Cmd(0xD8);//set area color mode off
    Oled_Write_Cmd(0x05);//
    Oled_Write_Cmd(0xD9);//Set Pre-Charge Period
    Oled_Write_Cmd(0xF1);//
    Oled_Write_Cmd(0xDA);//set com pin configuartion
    Oled_Write_Cmd(0x12);//
    Oled_Write_Cmd(0xDB);//set Vcomh
    Oled_Write_Cmd(0x30);//
    Oled_Write_Cmd(0x8D);//set charge pump enable
    Oled_Write_Cmd(0x14);//
    Oled_Write_Cmd(0xAF);//--turn on oled panel
}

//清屏
void Oled_Clear()
{
    unsigned char i,j; //-128 --- 127
    for(i=0;i<8;i++){
    Oled_Write_Cmd(0xB0 + i);//page0--page7
    //每个page从0列
    Oled_Write_Cmd(0x00);
    Oled_Write_Cmd(0x10);
    //0到127列,依次写入0,每写入数据,列地址自动偏移
    for(j = 0;j<128;j++){
            Oled_Write_Data(0);
        }
    }
}
3.3 使用STC89C52单片机通过模拟I2C让OLED显示一个字符A

部分使用规则需查看模块使用手,如OLED初始化,寻址等

STC89C520.96寸OLED
P2.2SCL
P2.3SDA
5VVCC
GNDGND

源码:

#include "reg52.h"
#include "intrins.h"


sbit scl = P2^2;
sbit sda = P2^3;

void IIC_Start(){

	scl = 0;  //防止雪花
	sda = 1;
	scl = 1;
	_nop_();  //约等于5us
	sda = 0;
	_nop_();
	
}

void IIC_Stop(){
	
	scl = 0;  //防止雪花
	sda = 0;
	scl = 1;
	_nop_();
	sda = 1;
	_nop_();
}

char IIC_ACK(){
	char flag;
	sda = 1;  //就在时钟脉冲9期间释放数据线
	_nop_();
	scl = 1;
	_nop_();
	flag = sda;
	_nop_();
	scl = 0;
	_nop_();
	
	return flag;
}

void IIC_Send_Byte(char dataSend){
	int i;
	for(i = 0;i<8;i++){
		scl = 0;  //scl拉低,让sda做好数据准备,scl低电平时才方便发生数据的翻转
		sda = dataSend & 0x80; //1000 0000获得dataSend的最高位,给sda
		_nop_();  //发送数据建立时间
		scl = 1;	//scl拉高开始发送
		_nop_();	//数据发送时间
		scl = 0;	//发送完毕拉低
		_nop_();
		dataSend = dataSend << 1;		//左移
	}
}

void Oled_Write_Cmd(char dataCmd){
	//1.start
	IIC_Start();
	//2.写入从机地址 b0111 1000 0x78
	IIC_Send_Byte(0x78);
	//3.ACK
	IIC_ACK();
	//4.cotrol byte:(0)(0)000000 写入命令  (0)(1)000000 写入数据
	IIC_Send_Byte(0x00);
	//5.ACK
	IIC_ACK();
	//6.写入指令/数据
	IIC_Send_Byte(dataCmd);
	//7.ACK
	IIC_ACK();
	//8.Stop
	IIC_Stop();
}

void Oled_Write_Data(char dataData){
	//1.start
	IIC_Start();
	//2.写入从机地址 b0111 1000 0x78
	IIC_Send_Byte(0x78);
	//3.ACK
	IIC_ACK();
	//4.cotrol byte:(0)(0)000000 写入命令  (0)(1)000000 写入数据
	IIC_Send_Byte(0x40);
	//5.ACK
	IIC_ACK();
	//6.写入指令/数据
	IIC_Send_Byte(dataData);
	//7.ACK
	IIC_ACK();
	//8.Stop
	IIC_Stop();
}

void Oled_Init(void){
	Oled_Write_Cmd(0xAE);//--display off
	Oled_Write_Cmd(0x00);//---set low column address
	Oled_Write_Cmd(0x10);//---set high column address
	Oled_Write_Cmd(0x40);//--set start line address
	Oled_Write_Cmd(0xB0);//--set page address
	Oled_Write_Cmd(0x81); // contract control
	Oled_Write_Cmd(0xFF);//--128
	Oled_Write_Cmd(0xA1);//set segment remap
	Oled_Write_Cmd(0xA6);//--normal / reverse
	Oled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)
	Oled_Write_Cmd(0x3F);//--1/32 duty
	Oled_Write_Cmd(0xC8);//Com scan direction
	Oled_Write_Cmd(0xD3);//-set display offset
	Oled_Write_Cmd(0x00);//
	Oled_Write_Cmd(0xD5);//set osc division
	Oled_Write_Cmd(0x80);//
	Oled_Write_Cmd(0xD8);//set area color mode off
	Oled_Write_Cmd(0x05);//
	Oled_Write_Cmd(0xD9);//Set Pre-Charge Period
	Oled_Write_Cmd(0xF1);//
	Oled_Write_Cmd(0xDA);//set com pin configuartion
	Oled_Write_Cmd(0x12);//
	Oled_Write_Cmd(0xDB);//set Vcomh
	Oled_Write_Cmd(0x30);//
	Oled_Write_Cmd(0x8D);//set charge pump enable
	Oled_Write_Cmd(0x14);//
	Oled_Write_Cmd(0xAF);//--turn on oled panel
}

void Oled_Clear(){
	int i,j;
	
	for(i=0;i<8;i++){
		Oled_Write_Cmd(0xB0 + i);  //page0 - page7
		//每个page从0列
		Oled_Write_Cmd(0x00);
		Oled_Write_Cmd(0x10);
		//从0到127列,依次写入0,每写入数据,列地址自动偏移
		for(j=0;j<128;j++){
			Oled_Write_Data(0);
		}
	}
}

/*--  文字:  A  --*/
/*--  宋体12;  此字体下对应的点阵为:宽x高=8x16   --*/
//需要用到2个page块  1个page  8位高
char A1[8]={0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00};
char A2[8]={0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20};

void main()
{
	int i;
	//1.OLED初始化
	Oled_Init();
	//2.选择一个位置
	//2.1 确认页寻址模式
	Oled_Write_Cmd(0x20);
	Oled_Write_Cmd(0x02);
	Oled_Clear();
	//2.2 选择page 页码数  1011 0xxx
	Oled_Write_Cmd(0xB0);  //1011 0000
	Oled_Write_Cmd(0x00);
	Oled_Write_Cmd(0x10);
	for(i=0;i<8;i++){
		Oled_Write_Data(A1[i]);
	}
	
	Oled_Write_Cmd(0xB1);  //1011 0000
	Oled_Write_Cmd(0x00);
	Oled_Write_Cmd(0x10);
	for(i=0;i<8;i++){
		Oled_Write_Data(A2[i]);
	}

	
	
	//不让程序退出
	while(1);
}

效果展示:

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

索子也敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值