前言
学习永无止境!本篇是嵌入式开发之片上外设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
}
待续...