stm32入门学习10-软件I2C和陀螺仪模块

(一)I2C通信

(1)通信方式

I2C是一种同步半双工的通信方式,同步指的是通信双方时钟为一个时钟,半双工指的是在同一时间只能进行接收数据或发送数据,其有一条时钟线(SCL)和一条数据线(SDA),使用I2C通信要遵守一定的规定

其收发数据有几种模式

(1)开启传输:在时钟线(SCL)为1时将数据线(SDA)由1转为0;

(2)结束传输:在时钟线(SCL)为1时将数据线(SDA)由0转为1;

(3)传输数据:在时钟线(SCL)为0时在数据线(SDA)中写入数据,将时钟线(SCL)由0变为1发送;

(4)接收数据:先释放数据线(SDA置1),后将时钟线(SCL)由0变为1后读取数据线(SDA);

(5)接收响应:I2C在主机传输8位数据时会给主机响应,为1则接收失败,为0则接收成功,主机需要接收响应,接收方式和接收数据相同;

(6)发送响应:I2C在主机接收8位数据时要给从机响应,给1则从机停止继续向主机发送,给0则继续向主机发送数据

这里可以注意到,I2C通信中只有开始和结束时在时钟线(SCL)为1时操作的数据线(SDA),其余都是在时钟线为0时操作数据线;

通信过程中其有一定的协议

(1)发送数据:(1)开启传输;(2)写入外设地址写模式;(3)接收从机响应;(4)写入外设寄存器的地址;(5)接收响应;(6)写入数据;(7)接收响应;(8)结束传输

(2)接收数据:(1)开启传输;(2)写入外设地址写模式;(3)接收从机响应;(4)写入外设寄存器的地址;(5)接收响应;(6)重新开启传输;(7)写入外设地址读模式;(8)接收响应;(9)读取数据;(10)发送响应;(11)结束传输

(2)软件模拟

这样我们就可以用代码来模拟I2C通信,我们选择时钟线(SCL)接在PA0口上,数据线(SDA)接在PA1口上

#define SCL GPIO_Pin_0
#define SDA GPIO_Pin_1

(1)时钟打开和初始化

这里要注意的是I2C是采用开漏输出的,即默认为高电平,我们这里端口输出模式也要选择开漏输出

void i2c_init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef gpio_init;
	gpio_init.GPIO_Mode = GPIO_Mode_Out_OD;
	gpio_init.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &gpio_init);
	
	GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1);
}

(2)置高低电平和读取数据

为了方便给SCL和SDA高电平或低电平,封装几个函数方便后面调用,分别为置某个端口高电平、置某个端口低电平、读取某个端口的值

void set(uint16_t io)
{
	GPIO_SetBits(GPIOA, io);
	Delay_us(10);
}

void reset(uint16_t io)
{
	GPIO_ResetBits(GPIOA, io);
	Delay_us(10);
}

unsigned char read(uint16_t io)
{
	unsigned char result;
	result = GPIO_ReadInputDataBit(GPIOA, io);
	return result;
}

(3)开启传输

和前面讲的一样,我们要在SCL高电平的情况下把SDA由高电平拉到低电平,然后拉低SCL,为后面的传输数据做准备

void i2c_start()
{
	set(SDA);
	set(SCL);
	reset(SDA);
	reset(SCL);
}

由于我们不知道在开始前数据线和时钟线是否一定为高电平,因此我们先把两者置高电平后再拉低

(4)结束传输

我们要在SCL为高电平的情况下把SDA由低电平上拉为高电平

void i2c_end()
{
	reset(SDA);
	set(SCL);
	set(SDA);
}

我们不知道在要结束的时候SDA是否为低电平,因此我们先拉低SDA,为后面的上拉做准备,至于我们的时钟线SCL,我们确保其在除了结束传输这一步外每一步结束时都为低电平,可以注意一下其他步骤的代码

(5)传输一个字节

我们在时钟线SCL为低电平的时候把数据放在数据线SDA上,然后把SCL拉高为高电平即完成传输,循环8次,传输一个字节

void i2c_send_byte(unsigned char message)
{
	unsigned char i;
	for(i = 0; i < 8; i++)
	{
		if ((message & (0x80>>i)) == 0)
			reset(SDA);
		else
			set(SDA);
		set(SCL);
		reset(SCL);
	}
}

这里的数据传输为高位先行,先传输高位,我们使用"与"的方法依次提取从高位到低位的八位bit,将数据message和1000 0000 的右移i位相与;

(6)接收一个字节

先释放数据线SDA(将其置1),后在SCL置1后读取SDA,循环八次,读取一个字节

unsigned char i2c_receive_byte()
{
	unsigned char i;
	unsigned char message = 0x00;
	set(SDA);
	for (i = 0; i < 8; i++)
	{
		set(SCL);
		if (read(SDA) == 1)
			message |= (0x80>>i);
		reset(SCL);
	}
	return message;
}

这里使用"或"的方式来接收数据,如果接收到i位数据为1,则将message与1000 0000 右移i位相或,第i位置1,其余位保持不变;

(7)发送应答

发送应答和发送单个bit做法相同,需要在SCL低电平期间将应答置于SDA中,再SCL上拉发送

void i2c_send_ack(unsigned char ack)
{
	if (ack == 0)
		reset(SDA);
	else 
		set(SDA);
	set(SCL);
	reset(SCL);
}

(8)接收应答

接收应答和接收单个bit做法相同,需要先释放数据线SDA(置1),在SCL置高电平时读取数据线的值

unsigned char i2c_receive_ack()
{
	unsigned char ack;
	set(SDA);
	set(SCL);
	ack = read(SDA);
	reset(SCL);
	return ack;
}

(3)封装

这样I2C的几种基本通信方式就写好了,我们只需要把其封装,再按照发送接收的规定,就可以读取改写外设寄存器,最后.c和.h文件可以这样

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


#define SCL GPIO_Pin_0
#define SDA GPIO_Pin_1

void i2c_init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef gpio_init;
	gpio_init.GPIO_Mode = GPIO_Mode_Out_OD;
	gpio_init.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &gpio_init);
	
	GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1);
}

void set(uint16_t io)
{
	GPIO_SetBits(GPIOA, io);
	Delay_us(10);
}

void reset(uint16_t io)
{
	GPIO_ResetBits(GPIOA, io);
	Delay_us(10);
}

unsigned char read(uint16_t io)
{
	unsigned char result;
	result = GPIO_ReadInputDataBit(GPIOA, io);
	return result;
}

void i2c_start()
{
	set(SDA);
	set(SCL);
	reset(SDA);
	reset(SCL);
}

void i2c_end()
{
	reset(SDA);
	set(SCL);
	set(SDA);
}

void i2c_send_byte(unsigned char message)
{
	unsigned char i;
	for(i = 0; i < 8; i++)
	{
		if ((message & (0x80>>i)) == 0)
			reset(SDA);
		else
			set(SDA);
		set(SCL);
		reset(SCL);
	}
}

unsigned char i2c_receive_byte()
{
	unsigned char i;
	unsigned char message = 0x00;
	set(SDA);
	for (i = 0; i < 8; i++)
	{
		set(SCL);
		if (read(SDA) == 1)
			message |= (0x80>>i);
		reset(SCL);
	}
	return message;
}

void i2c_send_ack(unsigned char ack)
{
	if (ack == 0)
		reset(SDA);
	else 
		set(SDA);
	set(SCL);
	reset(SCL);
}

unsigned char i2c_receive_ack()
{
	unsigned char ack;
	set(SDA);
	set(SCL);
	ack = read(SDA);
	reset(SCL);
	return ack;
}

#ifndef __I2C_H__
#define __I2C_H__

void i2c_init(void);
void i2c_start(void);
void i2c_end(void);
void i2c_send_byte(unsigned char message);
unsigned char i2c_receive_byte(void);
void i2c_send_ack(unsigned char ack);
unsigned char i2c_receive_ack(void);

#endif

(二)MPU-6050

MPU-6050是一个可以测量加速度和角速度的陀螺仪加速度计,其外设写地址为0xD0,外设的读地址为0xD1,其测量的加速度和角速度存在其内部寄存器中,我们通过I2C访问其内部寄存器来读取测量值

经过我们前面的程序,我们已经有了这几个函数:(1)开启传输函数;(2)结束传输函数;(3)发送一个字节函数;(4)接收一个字节函数;(5)发送应答函数;(6)接收应答函数;通过这些函数我们就可以操作寄存器了

(1)写某个位置的一个字节

按照我们之前的说法,我们要写某个寄存器,我们先要开启传输,发送外设写地址,接收应答,发送寄存器地址,接收应答,写入数据,接收应答,结束传输,对应下面的每一行代码

void mpu_write(unsigned char address, unsigned char message)
{
	i2c_start();
	i2c_send_byte(mpu6050_address);
	ack = i2c_receive_ack();
	i2c_send_byte(address);
	ack = i2c_receive_ack();
	i2c_send_byte(message);
	ack = i2c_receive_ack();
	i2c_end();
	Delay_us(10);
}

(2)读某个位置的一个字节

和前面说的一样,要读取某个外设的寄存器,我们需要开启传输,发送外设写地址,接收应答,发送寄存器地址,接收应答,重新开启传输,发送外设读地址,接收应答,读取数据,发送应答,结束传输,对应代码为

uint8_t mpu_read(unsigned char address)
{
	uint8_t message = 0x00;
	i2c_start();
	i2c_send_byte(mpu6050_address);
	ack = i2c_receive_ack();
	i2c_send_byte(address);
	ack = i2c_receive_ack();
	
	i2c_start();
	i2c_send_byte(mpu6050_address | 0x01);
	ack = i2c_receive_ack();
	message = i2c_receive_byte();
	i2c_send_ack(1);
	i2c_end();

	return message;
}

(3)初始化MPU-6050

MPU-6050默认为睡眠模式,如果不初始化不会进行数据转换,这里直接操作寄存器转换如下,主要为停止睡眠模式、选择时钟等,顺便把I2C也在此初始化

void mpu_init()
{
	i2c_init();
	
	mpu_write(0x6B, 0x01);	//PWR_MGMT_1 -> 0000 0001
	mpu_write(0x6C, 0x00);	//PWR_MGMT_2 -> 0000 0000
	mpu_write(0x19, 0x09);	//SMPLRT_DIV -> 0000 1001
	mpu_write(0x1A, 0x06);	//CONFIG -> 0000 0110
	mpu_write(0x1B, 0x18);	//GYRO_CONFIG -> 0001 1000
	mpu_write(0x1C, 0x18);	//ACCEL_CONFIG -> 0001 1000
}

(4)传输数据

MPU-6050中有六个数据,分别为x、y、z轴加速度,x、y、z轴的角速度,我们需要一次返回六个变量,可以用数组,但这里用一个结构体来返回六个变量

typedef struct inf
{
	int x_acceleration;
	int y_acceleration;
	int z_acceleration;
	
	int x_angular_velocity;
	int y_angular_velocity;
	int z_angular_velocity;
} information;

这六个变量都是16位数据,其高八位和低八位存在不同的寄存器中

我们可以通过把高位左移8位"或"低位的方式来读取其16位寄存器

++这里记录下常见的错误:如果你在读取某些有符号数据时其逼近但不超过最大值,但是却从来没有为负数,这可能是因为在位数小的数据强行转换为位数大的数据中出现的错误,其会在位数小数据的前面自动补0,而众所周知我们负数的补码是要在前面补1的,比如8位有符号数据1111 1101为-3,若将其强行转化为16位有符号数据则默认为0000 0000 1111 1101,这就是一个很大的正数了,我们要的16位-3应该为1111 1111 1111 1101++

information mpu_get_inf()
{
	uint8_t inf_L;
	uint8_t inf_H;
	information infor;
	inf_H = mpu_read(0x3B);
	inf_L = mpu_read(0x3C);
	infor.x_acceleration = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x3D);
	inf_L = mpu_read(0x3E);
	infor.y_acceleration = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x3F);
	inf_L = mpu_read(0x40);
	infor.z_acceleration = (inf_H<<8) | inf_L;
	
	inf_H = mpu_read(0x43);
	inf_L = mpu_read(0x44);
	infor.x_angular_velocity = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x45);               
	inf_L = mpu_read(0x46);               
	infor.y_angular_velocity = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x47);               
	inf_L = mpu_read(0x48);               
	infor.z_angular_velocity = (inf_H<<8) | inf_L;
	
	if (infor.x_acceleration & 0x8000)
	{
		infor.x_acceleration |= 0xFFFF0000;
	}
	if (infor.y_acceleration & 0x8000)
	{
		infor.y_acceleration |= 0xFFFF0000;
	}
	if (infor.z_acceleration & 0x8000)
	{
		infor.z_acceleration |= 0xFFFF0000;
	}
	if (infor.x_angular_velocity & 0x8000)
	{
		infor.x_angular_velocity |= 0xFFFF0000;
	}
	if (infor.y_angular_velocity & 0x8000)
	{
		infor.y_angular_velocity |= 0xFFFF0000;
	}
	if (infor.z_angular_velocity & 0x8000)
	{
		infor.z_angular_velocity |= 0xFFFF0000;
	}
	
	return infor;
}

最后的一连串if就是来解决类型转化间的错误的

这样我们就成功读到了寄存器内的数据并返回一个包含所有数据的结构体

(5)封装与声明

最后的.c 和 .h代码如下

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

#define mpu6050_address 0xD0
unsigned char ack;

void mpu_write(unsigned char address, unsigned char message)
{
	i2c_start();
	i2c_send_byte(mpu6050_address);
	ack = i2c_receive_ack();
	i2c_send_byte(address);
	ack = i2c_receive_ack();
	i2c_send_byte(message);
	ack = i2c_receive_ack();
	i2c_end();
	Delay_us(10);
}

uint8_t mpu_read(unsigned char address)
{
	uint8_t message = 0x00;
	i2c_start();
	i2c_send_byte(mpu6050_address);
	ack = i2c_receive_ack();
	i2c_send_byte(address);
	ack = i2c_receive_ack();
	
	i2c_start();
	i2c_send_byte(mpu6050_address | 0x01);
	ack = i2c_receive_ack();
	message = i2c_receive_byte();
	i2c_send_ack(1);
	i2c_end();

	return message;
}

void mpu_init()
{
	i2c_init();
	
	mpu_write(0x6B, 0x01);	//PWR_MGMT_1 -> 0000 0001
	mpu_write(0x6C, 0x00);	//PWR_MGMT_2 -> 0000 0000
	mpu_write(0x19, 0x09);	//SMPLRT_DIV -> 0000 1001
	mpu_write(0x1A, 0x06);	//CONFIG -> 0000 0110
	mpu_write(0x1B, 0x18);	//GYRO_CONFIG -> 0001 1000
	mpu_write(0x1C, 0x18);	//ACCEL_CONFIG -> 0001 1000
}

typedef struct inf
{
	int x_acceleration;
	int y_acceleration;
	int z_acceleration;
	
	int x_angular_velocity;
	int y_angular_velocity;
	int z_angular_velocity;
} information;

information mpu_get_inf()
{
	uint8_t inf_L;
	uint8_t inf_H;
	information infor;
	inf_H = mpu_read(0x3B);
	inf_L = mpu_read(0x3C);
	infor.x_acceleration = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x3D);
	inf_L = mpu_read(0x3E);
	infor.y_acceleration = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x3F);
	inf_L = mpu_read(0x40);
	infor.z_acceleration = (inf_H<<8) | inf_L;
	
	inf_H = mpu_read(0x43);
	inf_L = mpu_read(0x44);
	infor.x_angular_velocity = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x45);               
	inf_L = mpu_read(0x46);               
	infor.y_angular_velocity = (inf_H<<8) | inf_L;
	inf_H = mpu_read(0x47);               
	inf_L = mpu_read(0x48);               
	infor.z_angular_velocity = (inf_H<<8) | inf_L;
	
	if (infor.x_acceleration & 0x8000)
	{
		infor.x_acceleration |= 0xFFFF0000;
	}
	if (infor.y_acceleration & 0x8000)
	{
		infor.y_acceleration |= 0xFFFF0000;
	}
	if (infor.z_acceleration & 0x8000)
	{
		infor.z_acceleration |= 0xFFFF0000;
	}
	if (infor.x_angular_velocity & 0x8000)
	{
		infor.x_angular_velocity |= 0xFFFF0000;
	}
	if (infor.y_angular_velocity & 0x8000)
	{
		infor.y_angular_velocity |= 0xFFFF0000;
	}
	if (infor.z_angular_velocity & 0x8000)
	{
		infor.z_angular_velocity |= 0xFFFF0000;
	}
	
	return infor;
}

#ifndef __MPU_H__
#define __MPU_H__

typedef struct inf
{
	int x_acceleration;
	int y_acceleration;
	int z_acceleration;

	int x_angular_velocity;
	int y_angular_velocity;
	int z_angular_velocity;
} information;

//extern struct information;
void mpu_init(void);
information mpu_get_inf(void);

#endif

(三)主函数调用

由于我们的MPU-6050初始化中已经包含了I2C的初始化,我们只要引用MPU头文件即可,我们在第1列显示加速度,在第9列显示角速度

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

int main()
{
	information num;
	mpu_init();
	OLED_Init();
	while(1)
	{
		num = mpu_get_inf();
		OLED_ShowSignedNum(1, 1, num.x_acceleration, 5);
		OLED_ShowSignedNum(2, 1, num.y_acceleration, 5);
		OLED_ShowSignedNum(3, 1, num.z_acceleration, 5);
		
		OLED_ShowSignedNum(1, 9, num.x_angular_velocity, 5);
		OLED_ShowSignedNum(2, 9, num.y_angular_velocity, 5);
		OLED_ShowSignedNum(3, 9, num.z_angular_velocity, 5);
		
		Delay_ms(500);
	}
	return 0;
}

(三)总结

通过读取加速度和角速度,我们通过I2C通信协议读写MPU-6050,了解了I2C的工作原理和手动模拟I2C的工作流程,解决了一些遇到的错误,积累了错误处理的经验

相关推荐
@小博的博客1 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
南宫生2 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
scan13 小时前
单片机串口接收状态机STM32
stm32·单片机·串口·51·串口接收
懒惰才能让科技进步3 小时前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
love_and_hope3 小时前
Pytorch学习--神经网络--搭建小实战(手撕CIFAR 10 model structure)和 Sequential 的使用
人工智能·pytorch·python·深度学习·学习
Chef_Chen3 小时前
从0开始学习机器学习--Day14--如何优化神经网络的代价函数
神经网络·学习·机器学习
芊寻(嵌入式)3 小时前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
Qingniu013 小时前
【青牛科技】应用方案 | RTC实时时钟芯片D8563和D1302
科技·单片机·嵌入式硬件·实时音视频·安防·工控·储能
hong1616884 小时前
跨模态对齐与跨领域学习
学习
Mortal_hhh4 小时前
VScode的C/C++点击转到定义,不是跳转定义而是跳转声明怎么办?(内附详细做法)
ide·vscode·stm32·编辑器