目录
1、I2C通信(分为软件I2C通信、硬件I2C通信)
1.1 I2C通信的特点
1.2 时钟线和数据线
1.3 硬件电路
2、时序设计
2.1 起始条件 & 终止条件
2.2 发送一个字节 & 接收一个字节
2.3 应答机制:发送应答 & 接收应答
2.4 指定地址写 & 当前地址读 & 指定地址读
2.4.1 指定地址写
2.4.2 当前地址读
2.4.3 指定地址读
3、代码实现
3.1 软件模拟的I2C通信
3.1.1 I2C软件模拟通信(协议)层
3.1.2 MPU6050设备操作层
3.1.3 主函数逻辑层
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线。
案例程序目的:通过软件I2C通信,对MPU6050芯片内部的寄存器进行读写,
写入到配置寄存器,就可以对外挂的模块进行配置,读出数据寄存器,就可以获取外挂模块的数据,这就是I2C通信的目的。
1、同步、半双工
2、带数据应答
3、支持总线挂载多设备(一主多从、多主多从)
4、可以是软件IC和硬件IC
5、两根通信线:SCL(Serial Clock)、SDA(Serial Data)
下面图是I2C典型电路模型(一主多从的模型)
CPU是单片机,作为总线的主机,权力很大,对SCL线的完全控制,任何时候都是主机完全掌握SCL总线;在空闲状态下,主机还可以主动发起对SDA的控制,只有从机在发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这是主机的权利。
下面是一系列“被控IC”(挂载在I2C总线上的从机)
“被控IC”这些从机可以是姿态传感器、OLED、存储器、时钟模块等。
从机的权利比较小,对于SCL时钟线,在任何时刻都只能被动的读取,
从机不允许控制SCL线,对于SDA线数据线,从机不允许主动发起对SDA的控制。
只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能暂短的取得SDA的控制权。这就是一主多从的规定。
下面图是“被控IC”的内部电路
左边绿色部分是SCL,右边是SDA。
起始条件:
在I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备区碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态。
当主机需要收发数据时,首先就要打破总线的宁静,产生一个起始条件,这个起始条件就是SCL处于高电平不去动它,然后把SDA拉下来,会产生一个下降沿,当从机捕获到这个SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤,接下来在SDA下降沿之后,主机要再把SCL拉下来。
模拟起始信号:
/**
* @brief 软件I2C的起始信号
* @param 无
* @retval 无
*/
void MyI2C_Start(void)
{
// 这里先释放SDA,再释放SCL的原因是为了使Start信号兼容重复起始信号RS
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
终止条件:
SCL先放手,回弹到高电平,SDA再放手,回弹到高电平,产生一个上升沿,
这个上升沿触发终止条件,同时终止条件后,SCL和SDA都是高电平,回归平静状态。
起始和终止都是主机产生的,从机是没有这个权利的,所以在总线空闲状态时,从机必须始终双手放开,不允许主动去接触总线,如果允许,那就是多主机模型了。
模拟终止信号:
/**
* @brief 软件I2C的结束信号
* @param 无
* @retval 无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
通俗理解:SCL低电平的时候,主机把数据放到SDA线上,过了一会SCL回到高电平,
此时从机趁SCL高电平的时候,立马读取SDA线上的数据。
举个例子,玩123木头人的游戏,数数字的人是主机,其他人是从机。
当主机不动的时候,相当于SCL处于高电平状态,此时其他人趁数数字人不动的期间(SCL高电平期间)就快速动起来,这些人的行为就相当于从机在读取SDA数据,直到数数字的人转头喊123木头人的时候,其他人就得保持静止状态,此时SCL又是低电平状态,主机也是在这个时候把数据放到SDA线上,等到转身不动时(SCL高电平期间),其他人又开始动起来(从机读取数据)。依次类推,循环8次,一个字节就发送成功了。
省流:主机拉低SCL,把数据放到SDA上,主机松开SCL回到高电平,从机读取SDA的数据。
模拟发送一个字节:
/**
* @brief 发送一个字节
* @param Byte 发送的字节数据
* @retval 无
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));
// SCL产生一个正脉冲,让从机读取数据
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
主机在接收数据之前,需要释放SDA。释放SDA相当于切换成输入模式,
当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,此时从机取得了SDA的控制权,
从机发送0,就把SDA拉低;从机需要发送1,就放手,SDA回弹高电平。
实线部分表示主机控制的电平,虚线表示从机控制的电平,SCL全程由主机控制。
模拟接收一个字节:
/**
* @brief 接收一个字节
* @param 无
* @retval 接收的字节数据
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1); // 主机释放SDA,交出SDA控制权
for (i = 0; i < 8; i ++)
{
// 在SCL高电平期间读取SDA,如果SDA为1,则将Byte对应位置1(高位先行)
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1)
{
Byte |= (0x80 >> i);
}
MyI2C_W_SCL(0);
}
return Byte;
}
模拟发送应答:
/**
* @brief 发送应答,以通知从机数据是是否发送结束
* @param AckBit 0为发送未结束,1为发送已结束
* @retval 无
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
// SCL产生一个正脉冲,让从机读取数据
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
模拟接收应答:
**
* @brief 接受应答,主机读取该信号以确认从机是否接受到数据
* @param 无
* @retval 应答信号,0为已收到(从机受到后下拉SDA),1为未收到
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1); // 主机释放SDA,交出SDA控制权
// 在SCL高电平期间读取SDA,如果SDA为1,则将AckBit置1
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
(1)读写数据位:读数据置1,写数据置0
(2)第一个应答信号:信号时由从机发送给主机,如果从机收到之前的信息,回复0,没有收到或者(主机)读取接收完成回复1
(3)第二个应答信号:单片机需要存储器返回一个应答信号
(4)第三个应答信号:发送完数据后,需要再给主机发送应答信号0,告诉主机写入成功
(5)最后写入停止位:SCL为高电平,SDA为上升沿
案例:对于指定从机地址为1101000的设备,在其内部0x19地址的寄存器中,写入数据0xAA,下图是指定地址写的时序。
首先,SCL高电平期间,拉低SDA,产生起始条件(Start,S),在起始条件之后,紧跟着的时序,必须是发送一个字节的时序(Send Byte:0xD0),字节的内容必须是从机地址和读写位(Slave Address +R/W),刚好从机地址是7位,读写位是1位,总共8位,刚好一个字节。发送从机地址(Slave Address),就是确定通信的对象,发送读写位,就是确认接下来要写入还是要读出。
如下图所示:
根据上图波形,如何理解应答和非应答?
应答:如果主机释放SDA之后,回弹到高电平,此时没有从机拉下SDA,主机依旧处于高电平,就代表从机没作出应答,即非应答;非应答的波形如下图所示:
非应答:如果主机释放SDA之后,从机立马拉下SDA,此时又处于低电平状态,就代表从机产生了应答。应答的波形如下图所示:
如果主机要读取从机的数据,就可以执行这个时序。
首先,SCL高电平期间,拉低SDA,产生起始条件,然后主机调用发送一个字节,
来进行从机的寻址和指定读写标志位。
比如图示的波形,表示本次寻址的目标是1101000的设备,最后一位读写标志为1, 表示主机接下来想要读取从机的数据,
紧跟着,发送一个字节之后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节。
在从机应答之后,数据的传输方向得放过来了,因为主机发出了读的命令,所以主机就不能继续发送了,同时主机要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作。
从机在SCL低电平期间写入SDA,主机在SCL高电平期间读取数据,最后,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据,0000 1111,也就是0x0F。
那问题来了,这个0x0F是从机的哪个寄存器的数据呢?
此时就用“当前地址指针”来说明这个情况了。在从机中,所有的寄存器被分配到了一个线性区域中,且会有一个单独的指针变量,指示着其中一个寄存器,指针上默认一般指向0地址,并且每写入一个字节和读出一个字节之后,这个指针就会自动自增一次,移动到下一个位置,在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,假设主机当前接收的数据是来自从机地址0x1A的,那下一次接收的地址就是0x1B,以此类推。
不过当前地址读这个时序不常用。
指定地址读,其实就是指定地址写和当前地址读的复合格式。
前半部分是指定地址写(但没来得及写),后半部分是当前地址读,两者加在一起就是指定地址读。
首先写入设备地址,然后发送一个字节,进行寻址,指定从机地址是1101 0000,读写标志位是0,代表主机要写的操作,经过从机应答之后,
再发送一个字节,用来指定地址,这个数据就写入到了从机的地址指针里了。它的寄存器指针指向0x19,
再发送一遍设备地址,主机接收数据,该数据就是寄存器地址0x19下的数据。
最后一部分的数据可以多来几个,就可以写多个数据,地址指针在读后会自增,就可以连续读出一片区域的寄存器,效率也会变高。
主机给应答:从机就会继续发,主机给非应答,从机不会再法发,交出SDA的控制权,从机控制SDA发送一个字节的权力,开始于读写标志位1,结束于主机给应答位为1
异步时序和同步时序的优缺点
异步时序:
1、好处:省一根时钟线,节省资源;
2、坏处:对时钟要求严格,发送方和接收方时钟不能由过大的偏差;
传输过程中,单片机进中断,发送方时序暂停,接受方仍会按照约定的速率读取,传输出错;
故异步时序的缺点:非常依赖硬件外设的支持,必须有USART电路才能方便的使用,否则很难用软件模拟。
同步时序(时钟要求不严格,对电路依赖度低)
1、设计时钟线,则对传输的时间要求变低;
2、在单方面暂停传输时,时钟线也暂停,传输双方都能定格在暂停的时刻,可过段时间再来继续;
3、极大的降低单片机对硬件电路的依赖,没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信。
MyI2C.h
#ifndef __MYI2C_H_
#define __MYI2C_H_
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
#endif
MyI2C.c
#include "stm32f10x.h" // Device header
// 更改GPIO和引脚时,只需要更改以下的宏定义即可
// 需要注意:请检查使用的GPIO是否是APB2总线的外设,如果不是,则需要更改MyI2C_Init函数中的RCC_APB2PeriphClockCmd函数名称
#define SCL_GPIO_Port_CLK RCC_APB2Periph_GPIOA
#define SDA_GPIO_Port_CLK RCC_APB2Periph_GPIOA
#define SCL_GPIO_Port GPIOA
#define SDA_GPIO_Port GPIOA
#define SCL_Pin GPIO_Pin_6
#define SDA_Pin GPIO_Pin_7
/**
* @brief 软件I2C的GPIO端口初始化函数
* @param 无
* @retval 无
*/
void MyI2C_Init(void)
{
// 开启SCL和SDA对应GPIO的时钟
RCC_APB2PeriphClockCmd(SCL_GPIO_Port_CLK, ENABLE);
RCC_APB2PeriphClockCmd(SDA_GPIO_Port_CLK, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = SCL_Pin;
GPIO_Init(SCL_GPIO_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = SDA_Pin;
GPIO_Init(SDA_GPIO_Port, &GPIO_InitStructure);
GPIO_SetBits(SCL_GPIO_Port, SCL_Pin);
GPIO_SetBits(SDA_GPIO_Port, SDA_Pin);
}
/**
* @brief 控制SCL线的下拉与释放
* @param BitValue 其值可以是0或1,0为下拉,1为释放
* @retval
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(SCL_GPIO_Port, SCL_Pin, (BitAction)BitValue);
// Delay_us(10);
}
/**
* @brief 控制SDA线的下拉与释放
* @param BitValue 其值可以是0或1,0为下拉,1为释放
* @retval 无
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(SDA_GPIO_Port, SDA_Pin, (BitAction)BitValue);
// Delay_us(10);
}
/**
* @brief 读取SDA
* @param 无
* @retval 读取到SDA的高低电平值
*/
uint8_t MyI2C_R_SDA(void)
{
return GPIO_ReadInputDataBit(SDA_GPIO_Port, SDA_Pin);
// Delay_us(10);
}
/**
* @brief 软件I2C的起始信号
* @param 无
* @retval 无
*/
void MyI2C_Start(void)
{
// 这里先释放SDA,再释放SCL的原因是为了使Start信号兼容重复起始信号RS
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
/**
* @brief 软件I2C的结束信号
* @param 无
* @retval 无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
/**
* @brief 发送一个字节
* @param Byte 发送的字节数据
* @retval 无
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));
// SCL产生一个正脉冲,让从机读取数据
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
/**
* @brief 接收一个字节
* @param 无
* @retval 接收的字节数据
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1); // 主机释放SDA,交出SDA控制权
for (i = 0; i < 8; i ++)
{
// 在SCL高电平期间读取SDA,如果SDA为1,则将Byte对应位置1(高位先行)
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1)
{
Byte |= (0x80 >> i);
}
MyI2C_W_SCL(0);
}
return Byte;
}
/**
* @brief 发送应答,以通知从机数据是是否发送结束
* @param AckBit 0为发送未结束,1为发送已结束
* @retval 无
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
// SCL产生一个正脉冲,让从机读取数据
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
/**
* @brief 接受应答,主机读取该信号以确认从机是否接受到数据
* @param 无
* @retval 应答信号,0为已收到(从机受到后下拉SDA),1为未收到
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1); // 主机释放SDA,交出SDA控制权
// 在SCL高电平期间读取SDA,如果SDA为1,则将AckBit置1
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
// I2C地址应答测试程序,在main函数中可以循环以下过程来遍历I2C地址,以检查从机是否能正常应答
uint8_t ACK;
MyI2C_Start();
MyI2C_SendByte(0xD0); // 0xD0为MPU6050的I2C地址和读写操作复合而成的地址
ACK = MyI2C_ReceiveAck();
MyI2C_Stop();
MPU6050.h
#ifndef __MPU6050_H_
#define __MPU6050_H_
void MPU6050_Init(void);
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
uint8_t MPU6050_ReadID(void);
void MPU6050_ReadData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);
#endif
MPU6050_Reg.h
#ifndef __MPU6050_REG_H_
#define __MPU6050_REG_H_
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B //电源管理1
#define MPU6050_PWR_MGMT_2 0x6C //电源管理2
#define MPU6050_WHO_AM_I 0x75 //ID寄存器(默认数值0x68,只读)
#endif
MPU6050.c
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
#define MPU6050_ADDRESS_W 0xD0
#define MPU6050_ADDRESS_R 0xD1
/**
* @brief MPU6050 向芯片内部的指定地址写
* @param RegAddress 芯片内部的寄存器地址
* @param Data 要写入的数据
* @retval 无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS_W);
MyI2C_ReceiveAck(); // 这里可以对获取到的回应信号进行处理
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
MyI2C_SendByte(Data);
MyI2C_ReceiveAck();
MyI2C_Stop();
}
/**
* @brief MPU6050 向芯片内部的指定地址读
* @param RegAddress 要读取的寄存器在芯片内部的地址
* @retval 读取的数据
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
// 向MPU6050发送将要读的地址
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS_W);
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
MyI2C_Start(); // 重复启动信号
MyI2C_SendByte(MPU6050_ADDRESS_R); // 发送读地址,让出SDA控制权
MyI2C_ReceiveAck();
Data = MyI2C_ReceiveByte();
MyI2C_SendAck(1); // 向从机发送应答信号(不响应),从机终止数据发送
MyI2C_Stop();
return Data;
}
/**
* @brief MPU6050初始化函数
* @param 无
* @retval 无
*/
void MPU6050_Init(void)
{
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); // 不复位,关闭睡眠模式,不循环,使能温度传感器,选择X轴陀螺仪的内部震荡电路作为系统时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); // 不需要设置循环模式的唤醒频率, 六个轴都不需要待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); // 设置采样分频, 这里选择10分频
MPU6050_WriteReg(MPU6050_CONFIG, 0x06); // 不需要外部同步, 数字低通滤波设置为最高(最平滑)
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); // 角速度计配置:不自测(高三位为自测使能, 手册有遗漏), 设计为最大量程
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); // 加速度计配置:不自测,选择为最大量程,不使用高通滤波器
}
/**
* @brief 获取MPU6050的ID值,可根据ID值检查STM32和MPU6050之间是否正常通信
* @param 无
* @retval ID值
*/
uint8_t MPU6050_ReadID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
/**
* @brief 获取并返回MPU6050六轴传感器的返回值
* @param 无
* @retval 通过参数指针操作(返回)6个返回值
*/
void MPU6050_ReadData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
*AccX = (MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccY = (MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccZ = (MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*GyroX = (MPU6050_ReadReg(MPU6050_GYRO_XOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroY = (MPU6050_ReadReg(MPU6050_GYRO_YOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroZ = (MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H) << 8)
| MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
}
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
int main(void)
{
OLED_Init();
MPU6050_Init();
OLED_ShowString(1, 1, "ID:0x");
ID = MPU6050_ReadID();
OLED_ShowHexNum(1, 6, ID, 2);
while (1)
{
MPU6050_ReadData(&AX, &AY, &AZ, &GX, &GY, &GZ);
OLED_ShowSignedNum(2, 1, AX, 5);
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
OLED_ShowSignedNum(2, 8, GX, 5);
OLED_ShowSignedNum(3, 8, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
}
}