【通信协议】I2C总线(一主多从)

目录

I2C简介

硬件电路

软件模拟初始化

基本单元

起始信号

停止信号

发送一个字节

接收一个字节

发送应答

接收应答

I2C基本单元代码

MyI2C.h

MyI2C.c

完整数据帧

学习资料分享


本博客使用软件模拟的代码进行I2C总线​​​​​​(*总线指多个设备共用的信号线)*协议中一个主机和多个从机之间的通信。更多内容见博客末学习资料分享

I2C简介

I2C ,全称为内部集成电路(Inter-Integrated Circuit) 总线*,* 是由Philips公司开发一种串行通信的总线协议,通常用于连接低速 外设,允许将多个设备连接到一条总线上,主要解决了微控制器一对多通信的问题。

特点

  • 同步,半双工
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从)
  • 仅用两根通信线:SCL串行时钟线,SDA串行数据线
  • 仅用于短距离通信
  • 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式,传输的最高速率取决于总线上最慢的设备。

由于芯片内建的电平稳定器(如施密特触发器或者结电容),芯片需要一段时间完成对总线电平的采样,所以IIC的速度不能太快。

I2C总线具有双向传输、系统集成、多设备共享等优点,但传输速度相对较低,时序要求严格且最长电缆长度有限等缺点。

主从机

在一主多从的I2C下,主机为微控制器,从机为其他设备,例如:含有SCL,SDA引脚的OLED,MPU6050模块。

主机在 I2C 总线上启动所有通信,并为所有从设备提供时钟。

从机都有一个独立的(7位或10位)地址,主机可以通过地址选择来确定与谁进行通信

通信线

一个 I2C 总线只使用两条总线,一条双向串行数据线 (SDA) ,一条串行时钟线 (SCL)。

SCL总线:Serial Clock,串行时钟线,为数据收发提供必要的同步时钟

由主机产生,从机被动读取

SCL低电平,SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为SCL高电平时做好准备

SCL高电平,发送端需要保持数据稳定,接收端读取一位数据

SDA总线:Serial Data,串行数据线,用于通讯,可表示通讯的开始,结束和数据的传输

在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答(即主机接收数据和应答)的时候,主机才会转交SDA的控制权给从机

硬件电路

假如I2C总线设备SDA配置为推挽输出,某一时刻,如果有两个设备同时要发送数据,一个发送高电平,一个发送低电平。

单片机I2C通信入门(上):硬件部分有哪些注意点?_哔哩哔哩_bilibili

这条通路短路了,必定会有一个元器件烧毁,为避免这样事情发生,所以I2C总线对于设备的IO口,做了一些阉割,去掉了上面的MOS管,这样就不可能存在短路的情况了。不过这样设备只能输出低电平 ,不能输出高电平,为了输出高电平,采用上拉电阻拉高。

禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构

总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。不管几个同时输出低电平,都是低电平。

I2C通讯设备间的常用连接方式

I2C总线上设备的SCL与SDA

1.上拉电阻阻值一般为4.7KΩ左右。具体取决于总线的电容负载和通信距离

2.SCL虽然在一主多从模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征

3.开漏输出加上拉电阻,所以I2C信号的抗干扰能力是比较弱的,它只适合于,同一块电路板上的芯片之间进行通信,并不适合超过30厘米电路板之间的通信

软件模拟初始化

软件I2C的优势在于不需要特定的硬件支持,可以在任何支持GPIO功能的微控制器上实现。它利用了微控制器的通用IO引脚来实现I2C通信协议。

通过配置寄存器设置微控制器的GPIO为开漏输出模式

本篇博客以STM32为例

cpp 复制代码
void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);  //高阻态,由总线上拉电阻拉高
}

void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

基本单元

I2C 的协议定义了空闲状态,通讯的起始和停止信号,数据传输,响应信号等基本单元

空闲状态:SCL与SDA均由外部上拉电阻拉高

起始信号

起始信号:SCL高电平期间,SDA从高电平切换到低电平

空闲状态时,主机并不想进行通讯,所有I2C总线设备(包括主机)均为输入模式,SCL与SDA均由上拉电阻拉高。主机想要进行通讯,必须要有起始条件告诉从机,通讯的开始。

主机将 SDA(串行数据)拉低并使 SCL(串行时钟)保持高电平以启动地址帧。它提醒所有从机传输即将开始。

起始信号,从机在SCL高电平时接收数据,其接收到主机主导的SDA的下降沿(主机从输入模式下由上拉电阻产生的高电平变成开漏输出的低电平),代表主机为输出模式,要发数据了。之后主机将SCL拉低,准备更改SDA数据(SCL低电平发送端更改数据)。

cpp 复制代码
//主机产生起始信号
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}


/* MyI2C_W_SDA(1);
   MyI2C_W_SCL(1);
这两条代码在起始条件下可以不调用,但这里为了兼容指定位置读数据帧中,从机应答位后的重复起始条件
主机应先将SDA输出1(开漏输出模式时由上拉电阻拉高),再将主机主导的SCL置1(开漏输出模式时由上拉电阻拉高).

从机应答位后给主机应答则SDA为0,不给应答SDA为1
如果先将SCL置1,主机再将SDA输出1,SCL高电平期间,SDA可能由0(从机给应答)置1(主机开漏输出模式时由上拉电阻拉高)误让从机收到结束信号
*/

停止信号

停止信号,从机在SCL高电平时接收数据,其接收到主机主导的SDA的上升沿(主机从开漏输出的低电平变成输入模式下由上拉电阻产生的高电平 ),代表主机为输入模式,不要发数据了,通讯结束。主机在发送停止信号后不能再向从设备发送任何数据,除非再次发送起始信号

停止信号 :SCL高电平期间,SDA从低电平切换到高电平

cpp 复制代码
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}
/*
  MyI2C_W_SDA(0);
  停止信号之前SCL已经为低电平,SCL为低电平时,主机可以改变SDA数据。为停止信号做准备
*/

发送一个字节

SCL低电平期间,主机将数据位依次放到SDA线上(高位先行)

然后释放SCL(高电平),主机保持SDA没有数据变化,从机将在SCL高电平期间读取数据位,依次循环上述过程8次,即可发送一个字节

如果SCL高电平期间,SDA有数据变化,从机就会认为是起始或终止条件

cpp 复制代码
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
         //SCL在发送字节的上个基本单元已经为0
		MyI2C_W_SDA(Byte & (0x80 >> i));
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

接收一个字节

SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后主机释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)

如果SCL高电平期间,SDA有数据变化,主机就会认为是起始或终止条件

cpp 复制代码
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);//主机释放SDA
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

发送应答

I2C还提供了一种称为"ACK/NACK"(应答/非应答)的确认机制。如果一个设备接收到数据,它将通过在SDA线上拉低电平来发送一个应答信号以通知发送方数据已被接收。相反,如果数据被损坏或未接收,接收设备将发送非应答信号。(在SDA上保持高电平)。

每传输8个位,就会留下一个位用于监听,这个位由接受数据的芯片返回"是否应答成功"。

发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

发送应答了,从机就会再发一个字节;不应答,表示主机不想接收数据了,从机不发送,交出SDA控制权

应答信号

非应答信号

cpp 复制代码
void MyI2C_SendAck(uint8_t AckBit)
{
   //SCL在发送应答的上个基本单元已经为0
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

接收应答

主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答

主机在接收之前,需要释放SDA,释放SDA代表主机切换为输入模式

cpp 复制代码
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);//主机释放SDA
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}

I2C基本单元代码

基本单元的模块代码

MyI2C.h

cpp 复制代码
#ifndef __MYI2C_H
#define __MYI2C_H

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif

MyI2C.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}

void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}

完整数据帧

待续。。。

学习资料分享

|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 资料来源 | 链接 |
| 嘉立创(文档) | 1. I2C协议 | 立创开发板技术文档中心 (lckfb.com) |
| myfreax(文档) | I2C 通信协议详解 | myfreax |
| 野火(文档) | 1. I2C --- [野火]STM32模块例程介绍 文档 (embedfire.com) |
| 江协科技(代码) | [10-3] 软件I2C读写MPU6050_哔哩哔哩_bilibili |
| 立功科技(手册) | IIC总线协议105.doc (suda.edu.cn) |

相关推荐
吉大一菜鸡2 小时前
FPGA学习(基于小梅哥Xilinx FPGA)学习笔记
笔记·学习·fpga开发
森旺电子3 小时前
51单片机仿真摇号抽奖机源程序 12864液晶显示
单片机·嵌入式硬件·51单片机
CCSBRIDGE4 小时前
Magento2项目部署笔记
笔记
不过四级不改名6775 小时前
蓝桥杯嵌入式备赛教程(1、led,2、lcd,3、key)
stm32·嵌入式硬件·蓝桥杯
小A1595 小时前
STM32完全学习——SPI接口的FLASH(DMA模式)
stm32·嵌入式硬件·学习
Rorsion5 小时前
各种电机原理介绍
单片机·嵌入式硬件
亦枫Leonlew5 小时前
微积分复习笔记 Calculus Volume 2 - 5.1 Sequences
笔记·数学·微积分
爱码小白6 小时前
网络编程(王铭东老师)笔记
服务器·网络·笔记
LuH11246 小时前
【论文阅读笔记】Learning to sample
论文阅读·笔记·图形渲染·点云
一棵开花的树,枝芽无限靠近你8 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器