Bootstrap

STM32标准库学习笔记(九)I2C

前言


学习永无止境!本篇是嵌入式开发之片上外设I2C,了解基本硬件原理以及通信协议,实现软件模拟通信与硬件外设通信。
注:本文章为学习笔记,部分图片与文字来源于网络/江协科技课程/手册,如侵权请联系!谢谢!


一、I2C通信概述


1.1 I2C基本概念

        I2C总线是Philips公司在八十年代初推出的一种串行同步、半双工的总线,主要用于近距离、低速的芯片之间的通信。

        I2C总线有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步。

        I2C通讯总线中可以连接多个设备(有一主多从、多主多从模式),每个设备都有一个独立地址,主机可以利用地址进行不同设备通信。

1.2 I2C物理层

  • 信号线:共两条信号线,分别是SCL(时钟线)、SDA(数据线),设备挂在两条总线上;
  • 电平:总线通过上拉电阻(一般4.7K),连接到电源,当I2C设备空闲时,会输出高组态,所有设备都空闲时,由上拉电阻将总线拉成高电平。

1.3 I2C协议层

 ① 基本时序: I2C的协议定义了通信的起始位、数据有效性、响应、仲裁、时钟同步与地址广播等环节。

  • 起始条件:SCL处于高电平,SDA由高电平变为低电平(产生下降沿);
  • 发送数据:SCL低电平时,主机将数据放在SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间不允许SDA变化,依次循环8次,可发送一个字节数据;
  • 接收应答:主机在发送完一个字节后,释放SDA,在下个时钟接收一个数据,判断从机是否应答(0表示应答,1表示非应答);
  • 接收数据:SCL低电平时,从机将数据放在SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间不允许SDA变化,依次循环8次,可接收一个字节数据;
  • 发送应答:主机接收完一个字节之后,在下个时钟发送一位数据,0表示应答,1表示非应答;
  • 终止条件:SCL处于高电平,SDA由低电平变为高电平(产生上升沿)。

② I2C完整时序:主要有指定地址写、当前地址读、指定地址读三种。

注:S(起始信号),RA(接收应答),P(停止信号),R/W(R=1,W=0)。

  • 指定地址写:指定设备(Slave Address+W)+指定地址(Reg Address)+写入数据(Data);

  • 当前地址读:指定设备(Slave Address+R)+读取数据(Data);

  • 指定地址读:指定设备(Slave Address+W)+指定地址(Reg Address)+指定设备(Slave Address+R)+读取数据(Data),RS为重复起始信号;


二、STM32的I2C外设


2.1 基本简介

  • 内部集成硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU负担;
  • 支持多主机模型,支持7/10地址寻址模式;
  • 支持不同通讯速度,标准速度(高达100khz),快速(高达400khz);
  • 支持DMA。

2.2 硬件基本结构框图

  • ①SDA数据控制;
  • ②SCL时钟控制;
  • ③数据寄存器及移位寄存器:高位先行(MSB);
  • ④中断及DMA:发送时数据寄存器变空或接收时数据寄存器变满,则产生DMA请求。DMA请求必须在当前字节传输结束之前被响应。当为相应DMA通道设置的数据传输量已经完成时,DMA控制器发送传输结束信号ETO到I2C接口,并且在中断允许时产生一个传输完成中断。

2.3主机发送与接收时序 

  • 主发送器:

  • 主接收器:


三、应用


3.1 软件模拟I2C

①本次示例说明:使用软件模拟SDA以及SCL,利用I/O高低电平翻转实现I2C时序。

②配置步骤:

  • 开启RCC时钟:开启GPIOB外设时钟;
  • 配置GPIO:SCL(GPIOB_Pin10)、SDA(GPIOB_Pin11),默认开漏输出;
  • 起始信号;
  • 停止信号;
  • 发送字节;
  • 接收字节;
  • 发送应答;
  • 接收应答。

③代码实战:

MI2C.c:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
/*SCL置高低电平*/
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}
/*SDA置高低电平*/
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}
/*SDA读取高低电平*/
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}
/*初始化GPIOB,PB10/11为开漏输出*/
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 Byte = 0x00;
	uint8_t i;
	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_ReceiveACKBit(void)
{
	uint8_t ACKBit;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	ACKBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return ACKBit;
}

3.2 硬件I2C

①本次示例说明:调用标准库函数,根据时序进行读写从机。

②配置步骤:

  • RCC时钟:开启GPIOB、I2C2外设时钟;
  • 配置GPIO:SCL(GPIOB_Pin10)、SDA(GPIOB_Pin11),默认开漏输出;
  • 配置I2C:配置模式、时钟频率等;
  • I2C使能:使能;
  • 时序编写:调用I2C标准库函数,实现相关起始停止信号,发送接收数据、发送接收应答的逻辑实现。

③代码实战:

MI2C.c:

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

#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C从机地址

/**
  * 函    数:MPU6050等待事件
  * 参    数:同I2C_CheckEvent
  * 返 回 值:无
  */
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t Timeout;
	Timeout = 10000;									//给定超时计数时间
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)	//循环等待指定事件
	{
		Timeout --;										//等待时,计数值自减
		if (Timeout == 0)								//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;										//跳出等待,不等了
		}
	}
}

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8
	
	I2C_SendData(I2C2, Data);												//硬件I2C发送数据
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTOP(I2C2, ENABLE);											//硬件I2C生成终止条件
}

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重复起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C发送从机地址,方向为接收
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一个字节之前提前将应答失能
	I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一个字节之前提前申请停止条件
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7
	Data = I2C_ReceiveData(I2C2);											//接收数据寄存器
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);									//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
	
	return Data;
}

/**
  * 函    数:MPU6050初始化
  * 参    数:无
  * 返 回 值:无
  */
void MPU6050_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);		//开启I2C2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将PB10和PB11引脚初始化为复用开漏输出
	
	/*I2C初始化*/
	I2C_InitTypeDef I2C_InitStructure;						//定义结构体变量
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;				//模式,选择为I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;				//时钟速度,选择为50KHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;		//时钟占空比,选择Tlow/Thigh = 2
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;				//应答,选择使能
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//应答地址,选择7位,从机模式下才有效
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;				//自身地址,从机模式下才有效
	I2C_Init(I2C2, &I2C_InitStructure);						//将结构体变量交给I2C_Init,配置I2C2
	
	/*I2C使能*/
	I2C_Cmd(I2C2, ENABLE);									//使能I2C2,开始运行
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);				//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);				//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);				//采样率分频寄存器,配置采样率
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);					//配置寄存器,配置DLPF
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);			//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);			//加速度计配置寄存器,选择满量程为±16g
}

待续...

;