1.硬件介绍
本期介绍一下智能家居项目中用到的角度传感器MPU6050。此模块在项目中用于获取姿态角度,方便后续添加无按键控制功能以及步数获取功能。

1.1.产品参数
产品名称:MPU-6050模块(三轴陀螺仪+三轴加速度)
使用芯片:MPU-6050
供电电源:3-5V(内部低压差稳压)通信方式:标准IIC通信协议
芯片内置:16bit AD转换器,16位数据输出
陀螺仪范围: ±250 500 1000 2000°/s
加速度范围: ±2±4±8±16g
引脚间距:2.54mm
1.2.产品介绍详解
MPU-6050 这模块挺实用的,里面集成了 3 轴陀螺仪和 3 轴加速器,相当于一下子能测六个方向的运动状态。
更方便的是,它自带一个叫 DMP 的硬件加速引擎,专门处理运动数据。这个引擎能直接算出 9 轴融合的结果(虽然本身是 6 轴,但通过算法能实现类似 9 轴的效果),不用我们自己写复杂的融合代码。
它有两个 I2C 接口,主接口用来传输数据,第二个可以接其他传感器。最关键的是InvenSense 已经做好了现成的运动处理数据库,能帮我们处理那些复杂的运动数据计算,这样一来,单片机或者处理器就不用费劲儿去算这些,能减轻不少负担。
而且它还提供了结构化的 API,咱们做开发的时候,直接调用接口就行,不用从底层一点点写,用起来挺顺手的。
此外,MPU-6050 的角速度测量范围挺灵活的,能调到 ±250、±500、±1000 还有 ±2000 度每秒,不管是快动作还是慢动作都能精准捕捉到。加速度的测量范围也能自己设置,有 ±2g、±4g、±8g 和 ±16g 这几个档,按需调就行。
传数据的话,它支持两种方式:I2C 最快能到 400kHz,SPI 更快,能到 20MHz,看你项目需求选哪种都行。
供电方面也挺方便,主电源 VDD 可以用 2.5V、3.0V 或者 3.3V,误差在 ±5% 以内都没问题;不过逻辑接口的电压得是 1.8V±5%,这点要注意。
另外它的尺寸是真小,4x4x0.9 毫米的 QFN 封装,在业内算是很小巧的了,省空间。还有些实用功能,比如自带温度传感器,里面的振荡器在工作时误差也很小,只有 ±1%,稳定性不错。
2.I2C协议
2.1.I2C协议简介
I2C 协议说白了,就是电子设备之间 “聊天” 的一套规矩,专门用来解决多个小器件(比如传感器、显示屏、单片机)怎么用最少的线互相传数据的问题。
咱们具体说,它就靠两根线干活:一根叫 SDA(数据线),专门用来传实际的数据(比如传感器测到的温度、湿度);另一根叫 SCL(时钟线),负责 “打拍子”—— 所有设备都跟着这根线的节奏走,保证传数据时不抢、不错位,就像大家一起听着同一个节拍唱歌,不会乱。
这协议厉害的地方在于 “一根线能接一串设备”。比如一个单片机,想连温湿度传感器、光照传感器、OLED 屏幕,不用给每个设备单独拉一堆线,只要把它们的 SDA 和 SCL 分别并到一起,再加上电源和地线,就能让单片机挨个跟它们 “聊天”。
每个设备都有自己独一无二的 “地址”,就像每家有个门牌号,单片机喊 “地址 0x48 的设备,把数据发过来”,对应的传感器就会回应,其他设备则乖乖等着,不会瞎掺和。
传数据的时候也有固定流程:比如单片机想从传感器拿数据,先通过 SDA 线发个 “开始信号”(告诉大家要说话了),然后发传感器的地址,再发 “我要读数据” 的指令,传感器收到后就会把数据通过 SDA 线传回来,最后单片机发个 “结束信号”,这次聊天就结束了。整个过程都跟着 SCL 线的节奏走,一步一步来,不容易出错。
平时咱们玩的开发板、智能家居里的小模块(比如 DHT11 温湿度传感器有时候也会用 I2C 版本),很多都靠它来传数据,主要就是因为省线、简单,一堆设备挤在一起也能有条不紊地通信。
2.2.I2C通讯过程
I2C 的通信过程其实就像两个人打电话,有一套固定的 “通话流程”:
首先,主机(比如单片机)得先 “拿起电话”—— 发一个起始信号,告诉总线上的设备 “准备开始通信了”。
接着,主机得说清 “打给谁、要干嘛”—— 发一个字节的数据,里面包含了目标从机(比如传感器)的地址,以及接下来是要 “听数据” 还是 “发数据”(也就是传送方向)。
被点名的从机收到后,得 “喂一声” 回应 —— 发一个应答信号,告诉主机 “我收到了,你说吧”。
然后就进入正题了:发送数据的一方(可能是主机也可能是从机)发一个字节的数据,接收的一方收到后,再发一个应答信号表示 “收到了,继续”。
就这样,发数据、回应答,一步一步循环着来,直到所有要传的内容都发完。
最后,主机 “挂电话”—— 发一个停止信号,告诉大家 “这次通信结束了”,总线就释放出来,其他设备可以用了。
整个过程就像按剧本走,每个步骤都有回应,保证数据传得又准又顺~
2.3.I2C起始与停止信号

2.3.1.起始信号

I2C发送一个起始信号:SDA和SCL线都设置为高电平,然后在SCL为高电平的期间,让SDA线让从高电平到低电平进行一个下降沿。
//产生IIC起始信号
void MPU_IIC_Start(void)
{
MPU_SDA_OUT(); //sda线输出
MPU_IIC_SDA=1;
MPU_IIC_SCL=1;
MPU_IIC_Delay();
MPU_IIC_SDA=0;//START:when CLK is high,DATA change form high to low
MPU_IIC_Delay();
MPU_IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
2.3.2.停止信号

I2C发送一个停止信号:SCL和SDA线拉低,SCL线先拉高,再将SDA拉高,让SDA线在SCL高电平期间,经历一个上升沿。
//产生IIC停止信号
void MPU_IIC_Stop(void)
{
MPU_SDA_OUT();//sda线输出
MPU_IIC_SCL=0;
MPU_IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
MPU_IIC_Delay();
MPU_IIC_SCL=1;
MPU_IIC_SDA=1;//发送I2C总线结束信号
MPU_IIC_Delay();
}
2.4.I2C应答信号
这是 I²C 总线里 “数据格式和应答机制” 的说明,用大白话拆解下:
数据是 8 位一组:不管传啥数据,都拆成一个一个 8 位的字节(比如温度值、设备地址 ),想传多少字节都行,没限制。
每传 1 字节必须跟个 “应答(ACK)”:就像你给朋友发消息,发一条得等对方回个 “收到”,保证数据没丢。

2.4.1.应答信号
发送完一字节数据后,拉低SCL线,SDA线,在SDA为低电平过程中,将SCL线拉高,再拉低为一个应答信号。
//产生ACK应答
void MPU_IIC_Ack(void)
{
MPU_IIC_SCL=0;
MPU_SDA_OUT();
MPU_IIC_SDA=0;
MPU_IIC_Delay();
MPU_IIC_SCL=1;
MPU_IIC_Delay();
MPU_IIC_SCL=0;
}
2.4.1.不产生应答信号
发送完一字节数据后,拉低SCL线,SDA线为高电平,在SDA为低电平过程中,将SCL线拉高,再拉低为代表一个非应答信号。

//不产生ACK应答
void MPU_IIC_NAck(void)
{
MPU_IIC_SCL=0;
MPU_SDA_OUT();
MPU_IIC_SDA=1;
MPU_IIC_Delay();
MPU_IIC_SCL=1;
MPU_IIC_Delay();
MPU_IIC_SCL=0;
}
2.5.I2C写数据函数

SCL线拉低开始数据传输,SDA线每传输一位,SCL从低到高,再由高到低同步时钟一次。循环8次,发送一个字节数据。
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void MPU_IIC_Send_Byte(u8 txd)
{
u8 t;
MPU_SDA_OUT();
MPU_IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
MPU_IIC_SDA=(txd&0x80)>>7;
txd<<=1;
MPU_IIC_SCL=1;
MPU_IIC_Delay();
MPU_IIC_SCL=0;
MPU_IIC_Delay();
}
}
2.6.I2C读数据函数
这是 “读 8 位数据” 的核心循环,逐位读取 SDA 上的数据:拉低SCL,告诉发送方 “准备发下一位数据”;Delay(); → 延时等待,让数据稳定;拉高SCL,此时 SDA 上的数据有效,receive<<=1; → 把 receive 里的二进制位 左移 1 位,给新数据腾位置;if(MPU_READ_SDA)receive++; → 读 SDA 引脚状态:如果 SDA 是高电平(MPU_READ_SDA 为真),receive 最低位加 1;如果是低电平,不加(保持 0)
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 MPU_IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
MPU_SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
MPU_IIC_SCL=0;
MPU_IIC_Delay();
MPU_IIC_SCL=1;
receive<<=1;
if(MPU_READ_SDA)receive++;
MPU_IIC_Delay();
}
if (!ack)
MPU_IIC_NAck();//发送nACK
else
MPU_IIC_Ack(); //发送ACK
return receive;
}
2.7.I2C通讯全过程
这段文字和波形图讲的是 I²C 通信的完整流程,用大白话拆解每一步:
-
主机(例如单片机)先给一个起始信号(S)
-
接着发 7 位从机地址(比如传感器的地址,总线上设备靠这个区分)
-
再跟 1 位 读写位(R/W):读写位是 0 → 主机要 “写数据” 给从机(比如给显示屏发内容);读写位是 1 → 主机要 “读数据” 从从机(比如从温湿度传感器拿数据)。
-
发完地址 + 读写位后,主机松开 SDA 线,等从机应答:
-
从机收到地址后,如果是自己的地址,就把 SDA 拉低,并在 SCL 高电平期间保持低电平 → 这就是 应答(ACK)
-
接下来进入 “传数据阶段”:主机 / 从机发 8 位数据(比如传感器的温度值),每发 1 字节,必须等对方应答(ACK);应答逻辑和之前一样:接收方拉低 SDA,在 SCL 高电平时保持低 → 确认 “收到这字节了”。
-
最后主机想结束通信时,发 “停止信号”:SCL 高电平时,SDA 从低变高 ,相当于说 “不传啦,总线释放,大家可以用了”。

3.MPU6050代码
3.1.I2C通讯应用
以下代码就是对上面I2C通讯全过程的实现,包括读和写的过程。
//IIC写一个字节
//reg:寄存器地址
//data:数据
//返回值:0,正常
// 其他,错误代码
u8 MPU_Write_Byte(u8 reg,u8 data)
{
MPU_IIC_Start();
MPU_IIC_Send_Byte((MPU_ADDR<<1)|0);//发送器件地址+写命令
if(MPU_IIC_Wait_Ack()) //等待应答
{
MPU_IIC_Stop();
return 1;
}
MPU_IIC_Send_Byte(reg); //写寄存器地址
MPU_IIC_Wait_Ack(); //等待应答
MPU_IIC_Send_Byte(data);//发送数据
if(MPU_IIC_Wait_Ack()) //等待ACK
{
MPU_IIC_Stop();
return 1;
}
MPU_IIC_Stop();
return 0;
}
//IIC读一个字节
//reg:寄存器地址
//返回值:读到的数据
u8 MPU_Read_Byte(u8 reg)
{
u8 res;
MPU_IIC_Start();
MPU_IIC_Send_Byte((MPU_ADDR<<1)|0);//发送器件地址+写命令
MPU_IIC_Wait_Ack(); //等待应答
MPU_IIC_Send_Byte(reg); //写寄存器地址
MPU_IIC_Wait_Ack(); //等待应答
MPU_IIC_Start();
MPU_IIC_Send_Byte((MPU_ADDR<<1)|1);//发送器件地址+读命令
MPU_IIC_Wait_Ack(); //等待应答
res=MPU_IIC_Read_Byte(0);//读取数据,发送nACK
MPU_IIC_Stop(); //产生一个停止条件
return res;
}
3.2.初始化配置
以下是对MPU6050寄存器的一系列配置,详细可见其数据手册。

//初始化MPU6050
//返回值:0,成功
//其他,错误代码
u8 MPU_Init(void)
{
u8 res;
MPU_IIC_Init();//初始化IIC总线
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X80); //复位MPU6050
delay_ms(100);
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X00); //唤醒MPU6050
MPU_Set_Gyro_Fsr(3); //陀螺仪传感器,±2000dps
MPU_Set_Accel_Fsr(0); //加速度传感器,±2g
MPU_Set_Rate(200); //设置采样率50Hz
MPU_Write_Byte(MPU_INT_EN_REG,0X00); //关闭所有中断
MPU_Write_Byte(MPU_USER_CTRL_REG,0X00); //I2C主模式关闭
MPU_Write_Byte(MPU_FIFO_EN_REG,0X00); //关闭FIFO
res=MPU_Read_Byte(MPU_DEVICE_ID_REG);
printf("ID: %d\r\n", res);
if(res==MPU_ADDR)//器件ID正确
{
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X01); //设置CLKSEL,PLL X轴为参考
MPU_Write_Byte(MPU_PWR_MGMT2_REG,0X00); //加速度与陀螺仪都工作
MPU_Set_Rate(100); //设置采样率为100Hz,周期为10ms
}else return 1;
return 0;
}
3.2.main.c
调用初始化函数,初始化MPU6050配置和启动专门处理运动数据的DMP硬件加速引擎。
int main()
{
MPU_Init();
mpu_dmp_init();
while(1)
{
MPU_Get_Gyroscope(&g_ngyrox,&g_ngyroy,&g_ngyroz); // 读取角速度
MPU_Get_Accelerometer(&g_naacx,&g_naacy,&g_naacz); // 读取加速度
}
}