(一)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的工作流程,解决了一些遇到的错误,积累了错误处理的经验