STM32 软件模拟IIC

通信协议学习(复习用)–IIC

  • IIC用2根信号线通信:串行数据线 SDA、串行时钟线 SCL;
  • IIC总线上所有器件的SDA、SCL引脚输出驱动都为 开漏(OD) 结构,通过外接上拉电阻实现总线上所有节点SDA、SCL信号的线与逻辑关系;
  • 总线上的所有设备通过软件寻址且具有唯一的地址(7位或10位)。7位“从机专用地址码”,其高4位为由生产厂家制定的设备类型地址,低3位为器件引脚定义地址(由使用者定义);10位地址不常见;
  • 任何时刻都只存在简单的主从关系,按数据传输的方向,主机可以是主发送器或主接收器;
  • 支持多主机。在总线上存在多个主机时,通过冲突检测和仲裁机制防止多个主机同时发起数据传输时存在的冲突;
  • IIC总线上所有器件都具有“自动应答”功能,保证数据传输的正确性; 主机和从机的区别在于对SCL的发送权,只有主机才能发送SCL;

时序图

指定地址读写,应该先发送从机ID(+写位)再发送寄存器地址
然后再根据读写发送从机ID(读/写)

起始信号

发送字节

接收字节

收发应答

收发应答

指定地址写


指定地址读


I2C外设实现的序列图

代码(带注释)

MYI2C.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#define SCL_PORT GPIOB // 宏定义方便更改(可选)
#define SCL_PIN GPIO_Pin_12
#define SDA_PIN GPIO_Pin_13

/*
I2C 默认有一个上拉电阻存在,所以SCL和SDA默认均为高电平
因此下文中拉高与释放是等价的
*/

void MYI2C_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //打开I2C设备时钟

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //选择推挽输出,这种情况下,可以通过读取输入寄存器输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13;
GPIO_Init(GPIOB,&GPIO_InitStructure);

GPIO_SetBits(GPIOB,GPIO_Pin_12 | GPIO_Pin_13); //配置好SCL和SDA的GPIO端口
}

void MYI2C_W_SCL(uint8_t BitValue) //封装SCL写函数,SCL功能为向外输出时钟信号
{
GPIO_WriteBit(GPIOB,GPIO_Pin_12,(BitAction)BitValue); //可以通过BitValue选择高低电平,BitAction是枚举类型
}
void MYI2C_W_SDA(uint8_t BitValue) //同理封装SDA写函数
{
GPIO_WriteBit(GPIOB,GPIO_Pin_13,(BitAction)BitValue);
}
uint8_t MYI2C_R_SDA(void) //封装SDA读函数,读取SDA电平的变化
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13); //电平只有0/1,所以ReadInputDataBit即可
return BitValue;
}
/* MYI2C_Start开始函数

I2C协议规定的开始信号为:在SCL高电平的情况下,SDA产生下降沿电平,紧接着,SCL置为低电平
此处为了防止SCL高电平时,SDA产生上升沿信号(即I2C协议规定的通信终止),先让SDA置高电平来规避这种情况

*/
void MYI2C_Start()
{
MYI2C_W_SDA(1); //当SCL与SDA均为高电平时,表示为空闲状态
MYI2C_W_SCL(1); //
MYI2C_W_SDA(0); //产生SDA下降沿信号
MYI2C_W_SCL(0); //置SCL低电平,此时I2C通信正式开启
}
/* MYI2C_Stop停止函数

I2C协议规定的停止信号为:在SCL高电平的情况下,SDA产生上升沿电平,顺势进入空闲状态
此处为了放置SCL高电平时,SDA已经在高电平,所以先将SDA置为低电平以规避这种情况
*/
void MYI2C_Stop()
{
MYI2C_W_SDA(0); //先置为低电平以确保可以产生上升沿
MYI2C_W_SCL(1); //SCL置为高电平准备关闭I2C通信
MYI2C_W_SDA(1); //SDA产生上升沿,此时I2C通信正式关闭
}

void MYI2C_SendByte(uint8_t byte) //I2C发送字节,I2C通信发送数据的方式是高位先行
{ //将字节数据Byte&0x80 (即1000 0000)求与,即可得到最高位是1还是0
uint8_t i; //值得注意的是,上式获得的结果为1000 0000或0000 0000,并非1/0
for(i = 0;i < 8;i++) //但我们注意到,每一位具有非0即1的特性,因此我们可以判断结果是否为0,如果不是,则说明为1
{ //
if((byte&(0x80>>i)) == 0) //这里采取循环读取的方式,将0x80依次右移,高位补0,实现字节的逐位读取
{ //1000 0000 -> 0100 0000 -> 0010 0000 -> 0001 0000 -> ...
MYI2C_W_SDA(0); //非0即1
}else MYI2C_W_SDA(1); //如果芯片主频过高,这里可以加一条Delay,确保SCL拉起前,SDA有足够的时间稳定电平
MYI2C_W_SCL(1); //在上述写入SDA后,将SCL拉起,表示发送数据
MYI2C_W_SCL(0); //再将SCL拉低,进入I2C空闲
}
}
uint8_t MYI2C_ReceiveByte() //I2C接受字节,I2C通信接受字节同样是高位先行
{ //
uint8_t Byte = 0x00; //先准备一个0000 0000准备接收数据
uint8_t i; //
MYI2C_W_SDA(1); //SDA置1 即主机主动释放SDA,避免干扰从机的数据
for(i = 0;i < 8;i++)
{
MYI2C_W_SCL(1); //SCL拉高,表示读取数据
if(MYI2C_R_SDA() == 1) Byte |=(0x80 >> i); //先判断读取到的电平是否为1,再将Byte或0x80(移位) 以读取各位数据
MYI2C_W_SCL(0); //SCL拉低,进入I2C空闲
}
return Byte;
}
// I2C无论是发送数据还是接受数据,都要通过应答位来确保通信的正常进行

void MYI2C_SendAck(uint8_t Ack) //发送应答,Ack取0或1,0表示应答,1表示无应答
{
MYI2C_W_SDA(Ack); //将Ack置于SDA线上
MYI2C_W_SCL(1); //释放SCL,即发送数据
MYI2C_W_SCL(0); //下拉SCL,进入空闲状态
}
uint8_t MYI2C_ReceiveAck() //接收应答
{
uint8_t Ack; //定义变量接受数据
MYI2C_W_SDA(1); //主机释放SDA,避免干扰从机数据
MYI2C_W_SCL(1); //释放拉高SCL,表示读取数据
Ack = MYI2C_R_SDA(); //读取Ack,写入变量
MYI2C_W_SCL(0); //拉低SCL,进入空闲
return Ack;
}
MPU6050.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include "stm32f10x.h"                  // Device header
#include "MYI2C.h"
#define MPU6050_ADDRESS 0xD0 //此为MPU6050芯片的设备ID地址
#include "MPU6050_REG.h"


//I2C协议已完成,此处为硬件的实现部分


void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
MYI2C_Start();
MYI2C_SendByte(MPU6050_ADDRESS);
MYI2C_ReceiveAck();
MYI2C_SendByte(RegAddress);
MYI2C_ReceiveAck();
MYI2C_SendByte(Data);
MYI2C_ReceiveAck();
MYI2C_Stop();
}

void MPU6050_Init() //初始化函数
{
MYI2C_Init(); //首先调用I2C的初始化,端口的设置也在I2C文件内
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //芯片寄存器的设置,这里详细的可以查看手册
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); //寄存器地址采用宏定义写在MPU6050_REG中
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
}



uint8_t MPU6050_ReadReg(uint8_t RegAddress) //读取寄存器内容,I2C读取寄存器的方式为主机先输出从机ID,再输出寄存器ID
{ //最后重新输出从机ID并选择读模式
uint8_t Data;
MYI2C_Start(); //打开I2C通信
MYI2C_SendByte(MPU6050_ADDRESS); //发送传感器ID地址
MYI2C_ReceiveAck(); //接受应答,这里可以判断一下是否有从机回复
MYI2C_SendByte(RegAddress); //发送寄存器地址
MYI2C_ReceiveAck(); //接受应答

MYI2C_Start(); //重新开始
MYI2C_SendByte(MPU6050_ADDRESS | 0x01); //硬件ID均为七位数据,将最后一位修改为1,则为读模式
MYI2C_ReceiveAck(); //接收应答
Data = MYI2C_ReceiveByte(); //调用函数接收数据
MYI2C_SendAck(1); //发送应答,Ack为1表示无应答,即不继续进行通信,如果想要读取多个数据
MYI2C_Stop(); //可以循环n次,期间应答0,最后一次应答1(无应答表示故障或不想通信),然后关闭通信

return Data;
}


typedef struct
{
int16_t X;
int16_t XA;
int16_t Y;
int16_t YA;
int16_t Z;
int16_t ZA;
} Data;


Data MPU6050_GetData() //传出数据,我这里采用了结构体的方法
{
Data result;
uint8_t DataH,DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
result.X = (DataH<<8)|DataL; //将高位左移八位进行拼接,这里可以将uint16 -> int16
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //因为传感器中的数据采用补码存储
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
result.Y = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
result.Z = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
result.XA = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
result.YA = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
result.ZA = (DataH<<8)|DataL;
return result;
}

MPU6050.c (硬件外设实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include "stm32f10x.h"                  // Device header
#include "MYI2C.h"
#define MPU6050_ADDRESS 0xD0 //此为MPU6050芯片的设备ID地址
#include "MPU6050_REG.h"


//I2C协议已完成,此处为硬件的实现部分


void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
//MYI2C_Start();
//MYI2C_SendByte(MPU6050_ADDRESS);
//MYI2C_ReceiveAck();
//MYI2C_SendByte(RegAddress);
//MYI2C_ReceiveAck();
//MYI2C_SendByte(Data);
//MYI2C_ReceiveAck();
//MYI2C_Stop();

I2C_GenerateSTART(I2C1,ENABLE); //硬件I2C是非阻塞的,因此需要有标志位来确保程序的正常运行,检测标志位是必须的
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C1,MPU6050_ADDRESS,I2C_Direction_Transmitter);//库函数中,收发数据都带应答,不需要额外处理
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
I2C_SendData(I2C1,RegAddress);
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//当有连续数据写入时,检测EV8事件
I2C_SendData(I2C1,Data);
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);//发送最后一个数据,检测EV8_2事件
I2C_GenerateSTOP(I2C1,ENABLE);
}

void MPU6050_Init() //初始化函数
{
//MYI2C_Init(); //首先调用I2C的初始化,端口的设置也在I2C文件内
/*
下面改成硬件I2C的实现

*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //首先打开时钟

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化端口,这里选择I2C1的端口

I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 10000;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C1,&I2C_InitStructure);
I2C_Cmd(I2C1,ENABLE); //根据配置初始化I2C后使能

MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //芯片寄存器的设置,这里详细的可以查看手册
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); //寄存器地址采用宏定义写在MPU6050_REG中
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
}



uint8_t MPU6050_ReadReg(uint8_t RegAddress) //读取寄存器内容,I2C读取寄存器的方式为主机先输出从机ID,再输出寄存器ID
{ //最后重新输出从机ID并选择读模式
uint8_t Data;
//MYI2C_Start(); //打开I2C通信
//MYI2C_SendByte(MPU6050_ADDRESS); //发送传感器ID地址
//MYI2C_ReceiveAck(); //接受应答,这里可以判断一下是否有从机回复
//MYI2C_SendByte(RegAddress); //发送寄存器地址
//MYI2C_ReceiveAck(); //接受应答
//
//MYI2C_Start(); //重新开始
//MYI2C_SendByte(MPU6050_ADDRESS | 0x01); //硬件ID均为七位数据,将最后一位修改为1,则为读模式
//MYI2C_ReceiveAck(); //接收应答
//Data = MYI2C_ReceiveByte(); //调用函数接收数据
//MYI2C_SendAck(1); //发送应答,Ack为1表示无应答,即不继续进行通信,如果想要读取多个数据
//MYI2C_Stop(); //可以循环n次,期间应答0,最后一次应答1(无应答表示故障或不想通信),然后关闭通信

I2C_GenerateSTART(I2C1,ENABLE); //发送初始信号
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//等待事件
I2C_Send7bitAddress(I2C1,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送七位地址
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//等待事件
I2C_SendData(I2C1,RegAddress); //发送寄存器地址
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);//等待事件

I2C_GenerateSTART(I2C1,ENABLE); //发送初始信号
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//等待事件
I2C_Send7bitAddress(I2C1,MPU6050_ADDRESS,I2C_Direction_Receiver); //发送七位地址,选择读模式
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);//接受数据比较特殊,当收取到最后一组数据时
I2C_AcknowledgeConfig(I2C1,DISABLE); //需要提前关闭应答,并请求STOP
I2C_GenerateSTOP(I2C1,ENABLE); //也就是循环接收多组数据,最后一组再关闭应答请求STOP
while(I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);//等待事件
Data = I2C_ReceiveData(I2C1); //收取DR寄存器数据
I2C_AcknowledgeConfig(I2C1,ENABLE); //启动应答,进入空闲模式
return Data;
}


typedef struct
{
int16_t X;
int16_t XA;
int16_t Y;
int16_t YA;
int16_t Z;
int16_t ZA;
} Data;


Data MPU6050_GetData() //传出数据,我这里采用了结构体的方法
{
Data result;
uint8_t DataH,DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
result.X = (DataH<<8)|DataL; //将高位左移八位进行拼接,这里可以将uint16 -> int16
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //因为传感器中的数据采用补码存储
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
result.Y = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
result.Z = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
result.XA = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
result.YA = (DataH<<8)|DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
result.ZA = (DataH<<8)|DataL;
return result;
}


STM32 软件模拟IIC
https://akixeon.github.io/2025/03/13/qrs-iic/
作者
Uranus
发布于
2025年3月13日
许可协议