当前位置: 首页 > article >正文

I²C通信协议

I²C通信协议

  • I²C通信协议
    • i2c简介
    • i2c硬件电路
  • i2c基本时序单元
    • 起始和终止条件
    • 发送数据
    • 接收数据
    • 发送和接收应答
  • i2c时序
    • 指定地址写
    • 当前地址读
  • MPU6050六轴姿态传感器
    • MPU6050简介
    • 加速度计
    • 陀螺仪传感器
    • 姿态角
    • MPU6050参数
    • 硬件电路
    • 内部框图
  • 案例:软件i2c读写MPU6050
    • 接线图
    • i2c代码封装
      • i2c.c
      • i2c.h
    • 封装MPU6050
      • MPU6050.c
    • MPU6050_Reg.h
      • MPU6050寄存器的地址
      • MPU6050.h
    • 主函数
  • i2c外设
    • i2c外设简介
    • 内部框图
    • i2c的基本结构
    • 主模式数据的收发
      • 发送数据,指定地址写
      • 接收数据,当前地址读

I²C通信协议

i2c简介

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
两根通信线:SCL(SerialClock)、SDA(Serial Data)
同步,半双工
带数据应答 支持总线挂载多设备(一主多从、多主多从)

i2c硬件电路

所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
在这里插入图片描述
主机和从机都采用开漏输出,也就是高电平无效,由外部的弱上拉电阻拉高电平

i2c基本时序单元

起始和终止条件

起始条件:SCL高电平期间,SDA从高电平切换到低电平
终止条件:SCL高电平期间,SDA从低电平切换到高电平
在这里插入图片描述

发送数据

在SCL低电平期间,主机把数据位按从高位到低位的顺序依次放到SDA的线上,然后将SCL切换到高电平,从机开始根据SDA的电平读数据,当SDA为高电平时读出来的数据为1,当SDA为低电平的的时候读出来的数据为0,读完一个位再把SCL切换到低电平

在SCL高电平期间SDA不允许有数据变化,依次循环上面的过程8次,即可发送一个字节
在这里插入图片描述

接收数据

在SCL低电平期间,从机把数据位按从高位到低位的顺序依次放到SDA的线上,然后将SCL切换到高电平,主机开始开始根据SDA的电平读数据,当SDA为高电平时读出来的数据为1,当SDA为低电平的的时候读出来的数据为0,读完一个位再把SCL切换到低电平

在SCL高电平期间SDA不允许有数据变化,依次循环上面的过程8次,即可接受一个字节(注意:主机在接受数据前要释放SDA)
在这里插入图片描述

发送和接收应答

发送应答:在接受完一个字节后,主机下一个时钟发送一位数据,判断主机是否应答,数据0表示应答,数据1表示没有应答
接收应答:在发送完一个字节后,主机下一个时钟再接收一位数据,判断从机是否应答,数据0表示应答,数据1表示没有应答(主机在接受之前,需要释放SDA)
因为上拉电阻的存在,当主机释放SDA后,SDA默认为高电平,所以用数据1表示没有应答 可以把这个当作第九位数据,类似校验位

在这里插入图片描述

i2c时序

指定地址写

对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

时序单元拼接:起始条件—>从机地址+写指令—>接收从机应答位(应答)—>主机指定从机的寄存器地址—>接收从机应答位(应答)—>主机写入的数据—>接收从机应答位(应答)—>停止条件
在这里插入图片描述

当前地址读

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

当前地址指针:

  1. 在从机中,所有的寄存器都被分配到一个线性区域中,其中会有个单独的指针变量指向其中的一个寄存器,该指针复位默认指向0地址,并且每写入一个字节和读出一个字节后都会自动自增一次。

  2. 在调用当前地址读时序时,主机没有指定要读取的寄存器地址,从机就会发送当前地址指针指向的寄存器的值

当前地址指针的作用:

  1. i2c规定,一旦读写标志位给1,下一个字节则立刻转为读的时序,主机无法指定要读取的寄存器,所以没有指定从机寄存器的时序。因此从机会发送当前地址指针指向的寄存器中的数据

时序单元拼接:

起始条件—>从机地址+读指令—>接收从机应答位(应答)—>主机读取从机发送的数据—>主机发送应答位(不应答)—>停止条件

发送应答时,如果主机应答从机,则从机继续发送下一个地址的寄存器中的数据

一般不使用该时序
在这里插入图片描述

MPU6050六轴姿态传感器

MPU6050简介

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度
在这里插入图片描述
9轴姿态传感器:

3轴加速度计,3轴陀螺仪传感器

3轴磁场传感器:测量X、Y、Z轴的磁场强度

10轴姿态传感器:

3轴加速度计,3轴陀螺仪传感器,3轴磁场传感器

气压传感器:根据气压强度测量垂直地面的高度信息

加速度计

加速度

加速度是速度对于时间的变化率,用于描述物体速度变化的快慢

加速度分为重力加速度和运动加速度,如果此时芯片运动起来了,测量的角度就会受运动加速度的影响。

总结:加速度计具有静态稳定性,不具有动态稳定性

陀螺仪传感器

一般来说,陀螺仪可以测量角速度和旋转角度,但是该芯片无法测量旋转角度,只能测量角速度

角速度

角速度是描述物体绕轴旋转的速度的物理量,通常用符号ω表示。它的单位是弧度/秒,表示物体每秒绕轴旋转的弧度数。

总结:当物体静止时,角速度值会因为噪声无法完全归零 , 所以陀螺仪具有动态稳定性,不具有静态稳定性

姿态角

姿态角,或者叫做欧拉角。以飞机为例,欧拉角就是飞机机身相对于初始 3 个轴的夹角,飞机机头下倾或者上仰,这个轴的夹角叫做俯仰,Pitch;飞机机身左翻滚或者右翻滚,这个轴的夹角叫做滚转,Roll;飞机机身保持水平,机头向左转向或者向右转向,这个轴的夹角叫做偏航,Yaw,简单来说,欧拉角就表达了飞机此时的姿态。飞机是上仰了还是下倾了,飞机向左倾斜还是向右倾斜,通过欧拉角都能清晰地表示出来。

陀螺仪是动态稳定,静态不稳定,加速度计是静态稳定,动态不稳定。这两种传感器的特性正好互补,所以我们取长补短,进行一下互补滤波,就能融合得到静态和动态都稳定的姿态角了。

MPU6050参数

16位ADC采集传感器,量化范围:-32768~32767

加速度计满量程:-2 ~ +2、-4 ~ +4、-8 ~ +8、-16 ~ +16(g)

陀螺仪满量程:-250 ~ +250、-500 ~ +500、-1000 ~ +1000、-2000 ~ +2000( °/sec )

这里满量程的意思就相当于ADC外设的VREF参考电压,当AD值达到最大时对应的电压是3.3V还是5V

可配置的数字低通滤波器

可以配置寄存器来选择对输出数据进行低通滤波;如果你觉得输出数据抖动太厉害,就可以加一点低通滤波,这样输出数据就会平缓一些。

可配置的时钟源

可配置的采样分频

这两个参数是配合使用,时钟源经过这个分频器的分频,可以为 AD 转换和内部其他电路提供时钟,控制分频系数,就可以控制 AD 转换的快慢

i2c从机地址

1101 000(AD0=0)

1101 001(AD0=1)

AD0:是芯片引出来的一个引脚,可以控制7位从机地址的最低位,防止在挂载多个设备时出现相同地址的从机

要对从机进行读写就需要将从机地址整体向左移一位,此时最低位,也就是第0位是读写位,第1位是AD0

也可以直接将读写位都算到从机地址中,也就是0xD1是对该从机读操作,0xD0是对该从机写操作

硬件电路

从图中可以看到,MUP6050的SDA和SCL都接了弱上拉电阻,就不需要我们再接电阻了,直接将SDA和SCL接到GPIO口即可
在这里插入图片描述

XCL、XDA:MPU6050只是一个6轴的姿态传感器,如果想进行9轴或者10轴的姿态测量,只需要将磁力计和气压计当作MPU6050的从机,XCL和XDA就是MPU6050作为主机时的引脚

INT: 可以配置芯片内部的一些事件,来触发中断引脚的输出,比如数据准备好了、I2C
主机错误等,另外芯片内部还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等。这些信号都可以出发 INT 引脚产生电平跳变

AD0:MPU6050已经给我们接了一个弱下拉电阻,当引脚悬空时,默认输出低电平

内部框图

在这里插入图片描述
时钟源

  1. 内部晶振,可以作为系统时钟。
  2. XYZ 轴的陀螺仪,它们也都会有个晶振,因为陀螺仪内部需要高精度时钟的支持,所以陀螺仪内部也有独立的时钟,这 3 个时钟也可以输出,作为系统时钟。
  3. 通过外部的 CLKIN 引脚,输入 32.768 KHz 的方波,或者 19.2 MHz 的方波,作为系统时钟。(不过这个外部时钟还需要额外的电路,比较麻烦,所以如果不是特别要求的话,一般用内部晶振或者内部陀螺仪的晶振,作为系统时钟,这个是时钟源的选择问题)

自测单元

这部分是用来验证芯片好坏的,当启动自测后,芯片内部会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些,那如何进行自测呢?
我们可以先使能自测,读取数据,再失能自测,读取数据,两个数据一相减,得到的数据叫自测响应,这个自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内,就说明芯片没问题,如果不在,就说明芯片可能坏了,使用的话就要小心点,这个是自测的功能

电荷泵

或者叫充电泵,CPOUT 引脚需要外接一个电容,是一种升压电路

电荷泵的升压原理(简单描述,了解一下即可):

  1. 比如我有个电池,电压是 5V,然后再来个电容,首先电池和电容并联,电池给电容充电,充满之后,电容是不是也相当于一个 5V 的电池了。然后呢,关键部分来了,我再修改电路的接法,把电池和电容串联起来,电池 5V,电容也是 5V,这样输出就是 10V 的电压了,就把电池电压升高至两倍了
  2. 不过由于这个电容电荷比较少,用一下就不行了,所以这个并联、串联的切换速度要快,趁电容还没放电完,就要及时并联充电,这样一直持续,并联充电,串联放电,并联充电,串联放电,然后后续再加一个电源滤波,就能进行平稳的升压了,这就是电荷泵的升压原理。
  3. 那这里,由于陀螺仪内部是需要一个高电压支持的,所以这里设计了一个电荷泵进行升压,当然这个升压过程是自动的,不需要我们管,了解一下即可。
    中断状态寄存器:可以控制内部的哪些事件到中断引脚的输出

先入先出寄存器:对数据流进行缓存

配置寄存器:对内部的各个电路进行配置

数据寄存器:也就是传感器寄存器,存储了各个传感器的数据

工厂校准:这个意思就是内部的传感器都进行了校准

数字运动处理器:简称 DMP,是芯片内部自带的一个姿态解算的硬件算法,配合官方的 DMP 库,可以进行姿态解算

接口旁路选择器

就是一个开关,如果拨到上面,辅助的 I2C 引脚就和正常的 I2C 引脚接到一起,这样两路总线就合在一起了,STM32 可以控制所有设备,这时 STM32 就是主机,MPU6050 和它的扩展设备都是 STM32 的从机;
如果拨到下面,辅助的 I2C 引脚就由 MPU6050 控制,两条 I2C 总线独立分开,这时 STM32 是 MPU6050 的主机,MPU6050 又是它的扩展设备的主机

案例:软件i2c读写MPU6050

接线图

在这里插入图片描述

i2c代码封装

i2c.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

#define i2c_Port GPIOB			// i2c通信的GPIO端口
#define i2c_SCL GPIO_Pin_10		// SCL对应的引脚
#define i2c_SDA GPIO_Pin_11		// SDA对应的引脚

// 操作SCL
void i2c_W_SCL(uint8_t state)
{
	// BitAction:可以将1、0强转为Bit_SET、Bit_RESET
	GPIO_WriteBit(i2c_Port, i2c_SCL, (BitAction)state);
	Delay_us(10);	// 防止单片机主频过快,从机反应不过来
}
// 操作SDA
void i2c_W_SDA(uint8_t state)
{
	// BitAction:可以将1、0强转为Bit_SET、Bit_RESET
	GPIO_WriteBit(i2c_Port, i2c_SDA, (BitAction)state);
	Delay_us(10);	// 防止单片机主频过快,从机反应不过来
}
// 读取SDA
uint8_t i2c_R_SDA()
{
	uint8_t BitVale = GPIO_ReadInputDataBit(i2c_Port, i2c_SDA);
	Delay_us(10);
	return BitVale;
}

// 初始化用于i2c通信的GPIO口
void i2c_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;	// 开漏输出,i2c通信规定高电平无效,由弱上拉置1
	GPIO_InitStruct.GPIO_Pin = i2c_SCL | i2c_SDA;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(i2c_Port, &GPIO_InitStruct);
}

// 起始条件
void i2c_Start()
{
	i2c_W_SDA(1);
	i2c_W_SCL(1);
	i2c_W_SDA(0);
	i2c_W_SCL(0);
}

// 结束条件
void i2c_End()
{
	i2c_W_SDA(0);
	i2c_W_SCL(1);
	i2c_W_SDA(1);
}

// 主机发送一个字节
void i2c_Write_Byte(uint8_t Byte)
{
	for(uint8_t i = 0; i < 8; i++)
	{
		i2c_W_SDA(Byte & (0x80 >> i));
		i2c_W_SCL(1);
		i2c_W_SCL(0);
	}
}

// 主机读取一个字节
uint8_t i2c_Read_Byte()
{
	uint8_t Byte = 0x00;
	i2c_W_SDA(1);
	for(uint8_t i = 0;i < 8; i++)
	{
		i2c_W_SCL(1);
		if(i2c_R_SDA() == 1){Byte |= (0x80 >> i);}
		i2c_W_SCL(0);
	}
	return Byte;
}

// 接收应答
uint8_t i2c_Read_ACK()
{
	i2c_W_SDA(1);
	i2c_W_SCL(1);
	uint8_t ACK = i2c_R_SDA();
	i2c_W_SCL(0);
	return ACK;
}

// 发送应答
void i2c_Write_ACK(uint8_t ACK)
{
	i2c_W_SDA(ACK);
	i2c_W_SCL(1);
	i2c_W_SCL(0);
}

i2c.h

#ifndef __i2c_H
#define __i2c_H

// 初始化用于i2c通信的GPIO口
void i2c_Init(void);
// 起始条件
void i2c_Start(void);
// 结束条件
void i2c_End(void);
// 主机读取一个字节
uint8_t i2c_Read_Byte(void);
// 主机发送一个字节
void i2c_Write_Byte(uint8_t Byte);
// 接收应答
uint8_t i2c_Read_ACK(void);
// 发送应答
void i2c_Write_ACK(uint8_t ACK);

#endif

封装MPU6050

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"
#include "i2c.h"

#define MPU_Addr 0xD0

/**
  * 函    数:写一个字节的数据到寄存器中
  * 参    数:Reg_Addr 寄存器地址
  * 参    数:Byte 要写入的字节
  * 返 回 值:无
  */
void MPU6050_Write_Reg(uint8_t Reg_Addr, uint8_t Byte)
{
	i2c_Start();				// 开始条件
	i2c_Write_Byte(MPU_Addr);	// MPU写地址
	i2c_Read_ACK();				// 接收应答
	i2c_Write_Byte(Reg_Addr);	// MPU寄存器地址
	i2c_Read_ACK();				// 接收应答
	i2c_Write_Byte(Byte);		// 写入数据
	i2c_Read_ACK();				// 接收应答
	i2c_End();					// 结束条件
}

/**
  * 函    数:从指定的寄存器中读一个字节的数据
  * 参    数:Reg_Addr 寄存器地址
  * 返 回 值:从寄存器中读出的数据(0~255)
  */
uint8_t MPU6050_Read_Reg(uint8_t Reg_Addr)
{
	uint8_t Data;						// 存放读出的数据
	
	i2c_Start();						// 开始条件
	i2c_Write_Byte(MPU_Addr);			// MPU写地址
	i2c_Read_ACK();						// 接收应答
	i2c_Write_Byte(Reg_Addr);			// MPU寄存器地址
	i2c_Read_ACK();						// 接收应答
	
	i2c_Start();						// 开始条件
	i2c_Write_Byte(MPU_Addr | 0x01);	// MPU读地址
	i2c_Read_ACK();						// 接收应答
	Data = i2c_Read_Byte();				// 读一个字节
	i2c_Write_ACK(1);					// 发送应答不应答
	i2c_End();							// 结束条件
	
	return Data;
}

// 存放数据寄存器中的值从0~7分别为:X、Y、Z轴的加速度、温度、X、Y、Z轴的角速度
int16_t MPU6050_Data[7];

int16_t *MPU6050_Read_ALL_Data()
{
	// MPU6050将数据的高位和低位分开放的,一共7个可测量的数据,有14个数据寄存器
	uint16_t Byte[14];
	
	i2c_Start();							// 起始条件
	i2c_Write_Byte(MPU_Addr);				// 从机地址,写指令
	i2c_Read_ACK();							// 接收应答
	i2c_Write_Byte(MPU6050_ACCEL_XOUT_H);	// 第一个数据寄存器的地址,每读一次数据,当前地址指针会自增一次
	i2c_Read_ACK();							// 接收应答
	
	i2c_Start();							// 重复起始
	i2c_Write_Byte(MPU_Addr | 0x01);		// 从机地址,读指令
	i2c_Read_ACK();							// 接收应答
	
	// 连续接收14个数据寄存器的数据,将读出来的数据存放到Byte数组中
	for(uint8_t i = 0; i < 14; i++)
	{
		Byte[i] = i2c_Read_Byte();			// 读一个字节
		if(i == 13){i2c_Write_ACK(1);}		// 没有读到14次时主机给发送应答,表示我还要读数据
		else{i2c_Write_ACK(0);}				// 当14个数据寄存器的数据读完后主机不给发送应答,表示数据接收完毕
	}
	
	i2c_End();								// 结束条件
	
	// 将高8位的数和低8位的数据组合起来放到MPU6050数组中返回出去
	for(uint8_t i = 0; i < 7; i++)
	{
		// 高位数据和低位数据是相邻的,高位在低位的前一个地址
		MPU6050_Data[i] = (int16_t)(Byte[i*2] << 8) | (Byte[i*2+1]);
	}
	return MPU6050_Data;
}

void MPU6050_Init()
{
	i2c_Init();
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_Write_Reg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_Write_Reg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_Write_Reg(MPU6050_GYRO_CONFIG, 0x18);		//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_Write_Reg(MPU6050_ACCEL_CONFIG, 0x18);		//加速度计配置寄存器,选择满量程为±16g
}

MPU6050_Reg.h

MPU6050寄存器的地址

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#define	MPU6050_SMPLRT_DIV		0x19	// 采样分频寄存器
#define	MPU6050_CONFIG			0x1A	// 配置寄存器
#define	MPU6050_GYRO_CONFIG		0x1B	// 陀螺仪配置寄存器
#define	MPU6050_ACCEL_CONFIG	0x1C	// 加速度计配置寄存器

#define	MPU6050_ACCEL_XOUT_H	0x3B	// X轴的加速度高8位数据
#define	MPU6050_ACCEL_XOUT_L	0x3C	// X轴的加速度低8位数据
#define	MPU6050_ACCEL_YOUT_H	0x3D	// Y轴的加速度高8位数据
#define	MPU6050_ACCEL_YOUT_L	0x3E	// Y轴的加速度低8位数据
#define	MPU6050_ACCEL_ZOUT_H	0x3F	// Z轴的加速度高8位数据
#define	MPU6050_ACCEL_ZOUT_L	0x40	// Z轴的加速度低8位数据
#define	MPU6050_TEMP_OUT_H		0x41	// 温度数据的高8位数据
#define	MPU6050_TEMP_OUT_L		0x42	// 温度数据的低8位数据
#define	MPU6050_GYRO_XOUT_H		0x43	// X轴的角速度高8位数据
#define	MPU6050_GYRO_XOUT_L		0x44	// X轴的角速度低8位数据
#define	MPU6050_GYRO_YOUT_H		0x45	// Y轴的角速度高8位数据
#define	MPU6050_GYRO_YOUT_L		0x46	// Y轴的角速度低8位数据
#define	MPU6050_GYRO_ZOUT_H		0x47	// Z轴的角速度高8位数据
#define	MPU6050_GYRO_ZOUT_L		0x48	// Z轴的角速度低8位数据

#define	MPU6050_PWR_MGMT_1		0x6B	// 电源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C	// 电源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75	// 器件ID号

#endif

MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

// 初始化
void MPU6050_Init(void);

// 写单个寄存器
void MPU6050_Write_Reg(uint8_t Reg_Addr, uint8_t Byte);

// 读取全部数据寄存器
int16_t *MPU6050_Read_ALL_Data(void);

// 读单个寄存器
uint8_t MPU6050_Read_Reg(uint8_t Reg_Addr);

#endif

主函数

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "MPU6050.h"

int main()
{
	OLED_Init();
	MPU6050_Init();
	
	// 接收返回的数组
	int16_t *MPU_Data;
	while(1)
	{
		// 读取全部数据寄存器
		MPU_Data = MPU6050_Read_ALL_Data();
		
		OLED_ShowSignedNum(1,1,MPU_Data[0],5);
		OLED_ShowSignedNum(2,1,MPU_Data[1],5);
		OLED_ShowSignedNum(3,1,MPU_Data[2],5);
		
		OLED_ShowSignedNum(1,8,MPU_Data[4],5);
		OLED_ShowSignedNum(2,8,MPU_Data[5],5);
		OLED_ShowSignedNum(3,8,MPU_Data[6],5);
		
		OLED_ShowSignedNum(4,3,MPU_Data[3],5);
	}
}

i2c外设

i2c外设简介

STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担

支持多主机模型

  1. 固定多主机
    总线上有两个及以上固定的主机,从机和主机设备都是固定的,不可变化

  2. 可变多主机
    总线上没有固定的主机和从机,任何一个设备都可以在总线空闲时跳出来当主机,指定其他任何一个设备进项通信,通信完成后主机就又要回到从机的位置。当多个设备要成为主机时就需要进行仲裁,仲裁失败的让出总线控制权。STM32的i2c外设使用的就是可变多主机模型

支持7位/10位地址模式

10位地址

如果总线上的设备超过128后7位地址就不够了,所以有10位地址的模式。

7位地址的设备寻址时只需要发送一个字节即可,10位地址的设备寻址就需要两个字节
第一个字节的高五位必须是11110,也就是10位地址寻址的标志位,第一个字节的最低位依然是读写位,第一个字节剩下的2个位加上第二个字节的8个位就组成10位地址设备的地址
而11110作为10位地址模式的标志位,在7位地址模式下的高五位是不会出现

示例: 例如有一个10位地址的设备地址为:1001 1100 11写模式。那么i2c的前两个字节就要发送:11110 10 0
01110011

15位~11位:11110,标志位

10位~9位:10,设备地址的高两位

8位:0,写指令

7位~0位:01110011,设备地址的低8位

支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)

支持DMA

在多字节传输的时候可以提高传输效率

兼容SMBus协议

这个 SMBus(System Management Bus),是系统管理总线,SMBus 是基于 I2C 总线改进而来的,主要用于电源管理系统中,SMBus 和 I2C 非常像,所以 STM32 的 I2C 外设就顺便兼容了一下 SMBus

STM32F103C8T6 硬件I2C资源:I2C1、I2C2

内部框图

在这里插入图片描述
数据收发的部分

数据控制部分,这里数据收发的核心部分,是这里的数据寄存器和数据移位寄存器。

发送数据:可以把一个字节数据写到数据寄存器 DR;当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步转到移位寄存器里,在移位的过程中,我们就可以直接把下一个数据放到数据寄存器里等着了,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的 TXE 位为 1,表示发送寄存器为空,这就是发送的流程
接收数据: 也是这一路,输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位 RXNE,表示接收寄存器非空,这时我们就可以把数据从数据寄存器读出来了
与串口的区别:串口是全双工,发送数据和接收数据可以同时进行,所以串口有四个寄存器:数据接收寄存器、数据接收移位寄存器、数据发送寄存器、数据发送移位寄存器

地址比较的部分

比较器和地址寄存器这是从机模式使用的, STM32 的 I2C 是基于可变多主机模型设计的,STM32 不进行通信的时候,就是从机,既然作为从机,它就应该可以被别人召唤。

我们可以自定一个从机地址,写到这个寄存器,当 STM32 作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那 STM32 就作为从机,响应外部主机的召唤

并且这个 STM32 支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器

数据校验部分

这是 STM32 设计的一个数据校验模块,当我们发送一个多字节的数据帧时,在这里硬件可以自动执行 CRC 校验计算

CRC 是一种很常见的数据校验算法,它会根据前面这些数据,进行各种数据运算,然后会得到一个字节的校验位附加在这个数据帧后面,在接收到这一帧数据后,STM32 的硬件也可以自动执行校验的判定,如果数据在传输过程中出错了,CRC 校验算法就通不过,硬件就会置校验错误标志位,告诉你数据错了

i2c的基本结构

在这里插入图片描述
移位寄存器和数据寄存器 DR 的配合是通信的核心部分,这里因为 I2C 是高位先行,所以移位寄存器是向左移位,在发送的时候,最高位先移出去,然后是次高位,等等。一个 SCL 时钟,移位一次,移位 8 次,这样就能把一个字节,由高位到低位,依次放到 SDA 线上了,那在接收的时候呢,数据通过 GPIO 口从右边依次移进来,最终移 8 次,一个字节就接收完成了

注意: 这两个对应的 GPIO 口,都要配置成复用开漏输出的模式

主模式数据的收发

也就是STM32作为主机

发送数据,指定地址写

在这里插入图片描述

以7位主发送为例的详细流程解释

  1. 起始条件S:在控制寄存器 CR1 中,有个 START 位,在这一位写 1,就可以产生起始条件了,当起始条件发出后,这一位可以由硬件清除,所以,只要在这一位写 1,STM32
    就自动产生起始条件了。之后,STM32 由从模式转为主模式,也就是多主机模型下,STM32 有数据要发,就要跳出来

  2. EV5: 起始条件之后,会发生 EV5 事件,可以把它当成是标志位。 EV5 事件就是 SB(Start Bit)标志位为 1,SB 是状态寄存器SR1的一个位,这一位置 1,代表起始条件已发送,软件读取 SR1
    寄存器后,也就是查看了这一位,然后写数据寄存器的操作,将清除该位,写数据寄存器
    DR,就是我们接下来的操作,所以按照正常的流程来,这个状态寄存器是不需要手动清除的。

  3. 发送从机地址(寻址):当我们检测起始条件已发送时,就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器 DR 中,写入 DR 之后,硬件电路就会自动把这一个字节,转到移位寄存器里,再把这一个字节发送到 I2C
    总线上,之后硬件会自动接收应答位并判断,如果没有应答,硬件就会置应答失败的标志位,然后这个标志位可以申请中断来提醒我们

  4. EV6:在寻址完成之后,会发生 EV6 事件,下面解释了 EV6 事件,就是 ADDR 标志位为 1,在手册中可以找到,ADDR 标志位在主模式状态下就代表地址发送结束。

  5. EV8_1:EV6 事件结束后,是 EV8_1 事件,下面解释,EV8_1 事件就是 TxE 标志位 = 1,移位寄存器空,数据寄存器空,这时需要我们写入数据寄存器 DR 进行数据发送了

  6. EV8:一旦写入 DR数据寄存之后,因为移位寄存器也是空,所以 DR 会立刻转到移位寄存器进行发送,这时就是 EV8 事件,移位寄存器非空,数据寄存器空,这时就是
    移位寄存器正在发数据的状态。当数据1发送完成后,接收到从机的应答前,在数据寄存器中写入数据2,此时EV8事件消失。然后接收到了从机的应答,数据寄存器DR转移数据到移位寄存器中进行发送,此时EV8事件又出现。在移位寄存器发送的过程中,数据3写入DR数据寄存器中,EV8事件消失。如此循环连续的发送数据

  7. EV8_2:最后,当我们想要发送的数据写完之后,这时就没有新的数据可以写入到数据寄存器了。当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这个事件就是这里的
    EV8_2,下面解释,EV8_2 是 TxE = 1,也就是数据寄存器空,BTF(Byte Transfer
    Finished),这个是字节发送结束标志位,手册里可以看一下解释:在发送时,当一个新数据将被发送且数据寄存器还未被写入新的数据时,BTF
    标志位置
    1,这个意思就是,当前的移位寄存器已经移完了,该找数据寄存器要下一个数据了,但是一看,数据寄存器没有数据,这就说明主机不想发了,这时就代表字节发送结束,是时候停止了。所以当检测到
    EV8_2 时,就可以产生终止条件了

停止条件P: 控制寄存器 CR1 中有一位 STOP,写 1,就会在当前字节传输,或在当前起始条件发出后产生停止条件

接收数据,当前地址读

想要指定地址读的时序需要自己拼接
在这里插入图片描述
这里有 7 位主接收和 10 位主接收,从这个 7 位主接收的时序看,这里时序的流程是:起始,从机地址+读,接收应答,然后就是,接收数据,发送应答,接收数据,发送应答,最后一个数据,给非应答,之后终止。可以看出,这个时序应该是当前地址读的时序,指定地址读的复合格式,这里没有给,得需要我们自己组合一下,然后下面 10 位地址的当前地址读就复杂一些了。这里是,起始,发送帧头,这个帧头里的读写位,应该还是写的,因为后面还要跟着发送第二个字节的地址,之后继续发送第二个字节的 8 位地址,这样才能进行寻址,然后要想转入读的时序,必须再发送重复起始条件,发送帧头,这次帧头的读写位就是读的了,因为发送读的指令之后,必须要立刻转入读的时序,所以这第二个字节的地址就没有了,直接转入接收数据的时序,这是 10 位地址的操作流程,稍微复杂一些,当然我们主要还是看 7 位地址的就行。

首先写入控制寄存器的 START 位,产生起始条件,然后等待 EV5 事件,下面解释和刚才一样,EV5 事件就代表起始条件已发送,之后是寻址,接收应答,结束后产生 EV6 事件,下面的解释也和刚才一样,EV6 事件代表,寻址已完成,之后数据 1 这一块,代表数据正在通过移位寄存器进行输入,EV6_1 事件,下面解释是没有对应的事件标志,只适于接收 1 个字节的情况,这个 EV6_1,可以看到,数据 1 其实还正在移位,还没收到呢,所以这个事件就没有标志位。之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去,如何配置是否要给应答呢?也是看手册,控制寄存器 CR1 里,这里有一位 ACK,应答使能,如果写 1,在接收到一个字节后就返回一个应答,写 0,就是不给应答,这就是应答位的配置。

之后继续,当这个时序单元结束后,就说明移位寄存器就已经成功移入一个字节的数据 1 了,这时,移入的一个字节就整体转移到数据寄存器,同时置 RxNE 标志位,表示数据寄存器非空,也就是收到了一个字节的数据,这个状态就是 EV7 事件。下面解释是,RxNE = 1,数据寄存器非空,读 DR 寄存器清除该事件,也就是,收到数据了,当我们把数据读走之后,这个事件就没有了,上面这里,EV7 事件没有了,说明此时数据 1 被读走了,当然数据 1 还没读走的时候,数据 2 就可以字节移入移位寄存器了,之后数据 2 移位完成,收到数据 2,产生 EV7 事件,读走数据 2,EV7 事件没有了。

然后按照这个流程,就可以一直接收数据了,最后,当我们不需要继续接收时,需要在最后一个时序单元发生时,提前把刚才说的应答位控制寄存器 ACK 置 0,并且设置终止条件请求,这就是 EV7_1 事件,下面解释,和 EV7 一样,后面加了一句,设置 ACK = 0 和 STOP 请求,也就是我们想要结束了。之后,在这个时序完成后,由于设置了 ACK = 0,所以这里就会给出非应答,最后由于设置 STOP 位,所以产生终止条件。

SCL占空比
i2c的输出模式规定开漏输出,所以输出低电平时是强下拉,一瞬间就降到了低电平。输出高电平时是通过弱上拉电阻拉上去的,会需要一点点时间才能升到高电平

在i2c协议下,设备是在低电平期间变化数据,高电平读取数据。而数据变化需要一定时间来翻转SDA的波形,尤其在数据上升沿期间变化比较慢,所以在SCL频率较高的情况下需要给SCL的低电平更多的时间来保证SDA的波形变化完成

i2c规定SCL的频率超过100KHz就算快速传输,需要调节占空比了


http://www.kler.cn/news/307828.html

相关文章:

  • 基于SpringBoot的考研助手系统+LW参考示例
  • 模拟实现通用型排序
  • Rust练手项目,写个有趣的小工具定时从一言网获取一段有趣的话并推送通知
  • STM32—I2C
  • OpenAI o1真的那么强吗
  • 天地一体化物联网:挑战与机遇
  • 移动订货小程序哪个好 批发订货系统源码哪个好
  • 【Elasticsearch系列八】高阶使用
  • 您的计算机已被.lcrypt勒索病毒感染?恢复您的数据的方法在这里!
  • 春秋云境靶场之CVE-2022-29464
  • element-plus弹窗内分页表格保留勾选项
  • 大数据-134 - ClickHouse 集群三节点 安装配置启动
  • 【2023年】云计算金砖牛刀小试4
  • 机器学习文献|基于循环细胞因子特征,通过机器学习算法预测NSCLC免疫治疗结局
  • 24.9.16数据结构|平衡二叉树
  • 如何切换淘宝最新镜像源npm
  • C++菜鸟教程 - 从入门到精通 第二节
  • Bxbshsbsh
  • 联合条件概率 以及在语言模型中的应用
  • 2、vectorCast集成测试常用功能
  • Flask中的蓝图如何进行模块化
  • ELK在Linux服务器下使用docker快速部署(超详细)
  • 苍穹外卖 修改nginx的端口后websocket连接失败解决
  • Datawhale------Tiny-universe学习笔记——Qwen(1)
  • C#:强大编程语言的多面魅力
  • 如何写数学建模竞赛论文
  • 实用调试技巧
  • golang学习笔记20——golang微服务负载均衡的问题与解决方案
  • MyBatis系统学习(四)——MyBatis的关联映射和缓存机制
  • Redis面试---缓存问题