[体感游戏] 1、MPU6050数据采集传输与可视化
最近在研究体感游戏,到目前为止实现了基于51单片机的MPU6050数据采集、利用蓝牙模块将数据传输到上位机,并利用C#自制串口数据高速采集软件,并且将数据通过自制的折线图绘制模块可视化地展示出来等功能。本文将主要对实现这意见单系统中遇到的问题做一个小结——其中包括:
关键词:MPU6050 蓝牙 C#串口 多线程 高速串口 折线图绘制
1、基于51的MPU6050模块通信简介(入门级)
因为是入门级,就先最简单的介绍如何利用51从MPU6050中读取数据吧(对于想知道卡尔曼滤波、俯角仰角、距离测量、摔倒检测、记步等算法的可能要在接下来介绍)。既然要和MPU6050通信,那么必不可少的是阅读芯片手册,如果您觉得亲自去看又长又多而且都是英文的手册很费时,不仿看看我找的简要版:
MPU-60X0是全球首例9轴运动处理器。它集成了3轴MEMS陀螺仪,3轴MEMS加速计,以及1个可扩展的数字运动处理器DMP(Digital Motion Processor),可用I2C接口连接一个第三方的数字传感器,比如磁力计。扩展之后就可以通过其I2C或SPI接口输出一个9轴的信号。MPU-60X0也可以通过其I2C接口连接非惯性的数字传感器,比如压力传感器。
MPU-60X0对陀螺仪和加速计分别用了三个16位的ADC,将其测量的模拟量转化为可输出的数字量。为了精确跟踪快速和慢速运动,传感器的测量范围是可控的,陀螺仪可测范围为±250,±500,±1000,±2000°/秒(dps),加速计可测范围为±2,±4,±8,±16g(重力加速度)。
注:下图是采用串口助手将MPU6050采集的数据显示在上位机上,其中前三列输出为三维的加速度(这里的加速度包括地球本身的重力加速度),后三列为三维的角速度。
但是这里的输出值并不是真正的加速度和角速度的值,上面说过,MPU是一个16位AD量程可程控的设备,这里设置的加速度传感器的测量量程为正负2g(这里的g为重力加速度),陀螺仪的量程为正负2000°/s。所以要用下面的公式进行转化:
好了,有了上面的基础知识之后咱们就能尝试用51的I2C总线从MPU6050读取实时的3轴加速度和3轴角速度了。由于51本身不带有I2C总线通信协议,所以我们要自己实现一个I2C通信协议,下面是我从网上找的并稍加修改的一个I2C总线通信的代码:
#include <REG52.H>
#include <INTRINS.H> typedef unsigned char uchar;
typedef unsigned short ushort;
typedef unsigned int uint; //-----------------------------------------
// 定义MPU6050内部地址
//-----------------------------------------
#define SMPLRT_DIV 0x19 //陀螺仪采样率,典型值:0x07(125Hz)
#define CONFIG 0x1A //低通滤波频率,典型值:0x06(5Hz)
#define GYRO_CONFIG 0x1B //陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
#define ACCEL_CONFIG 0x1C //加速计自检、测量范围及高通滤波频率,典型值:0x01(不自检,2G,5Hz)
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B //电源管理,典型值:0x00(正常启用)
#define WHO_AM_I 0x75 //IIC地址寄存器(默认数值0x68,只读)
#define SlaveAddress 0xD0 //IIC写入时的地址字节数据,+1为读取 //-----------------------------------------
// I2C总线通信函数
//-----------------------------------------
void I2C_Start(); //I2C起始信号
void I2C_Stop(); //I2C停止信号
void I2C_SendACK(bit ack); //I2C发送应答信号[入口参数:ack (0:ACK 1:NAK)]
bit I2C_RecvACK(); //I2C接收应答信号
void I2C_SendByte(uchar dat); //向I2C总线发送一个字节数据
uchar I2C_RecvByte(); //从I2C总线接收一个字节数据
void Single_WriteI2C(uchar REG_Address,uchar REG_data);//向I2C设备写入一个字节数据
uchar Single_ReadI2C(uchar REG_Address); //从I2C设备读取一个字节数据 //-----------------------------------------
// 通过I2C和MPU6050通信的函数
//-----------------------------------------
void InitMPU6050(); //初始化MPU6050
int GetData(uchar REG_Address); //合成数据
如果你没搞过硬件又从未听说过I2C,那么想想socket的握手再看看上面36~43行的有关ACK、Send、Write的函数大概能明白I2C的功能。当我们实现I2C的通信函数之后就可以与带有I2C通信接口的芯片进行通信,那么怎样通信呢?其实很简单——你可以把每个芯片比做为一个巨大的储物柜,储物柜里每个抽屉里存着相应的东西,你想让佣人帮你去拿个东西,只要告诉佣人对应的抽屉号就行了。这里I2C总线相当于这个佣人,每个抽屉相当于芯片中的寄存器,抽屉号相当于寄存器地址。当你想设置芯片的某些属性时是向对应的寄存器内写数据,当想从芯片内获取相关数据时,就要通过I2C向对应的地址写数据然后接收芯片返回的数据。这里的8~31行为MPU-6050芯片内几个常用的寄存器地址,前四个常用来作为设置芯片工作属性,15~28共14个寄存器地址用来获取传感器的3轴加速度、3轴角速度和温度的数据(这里每一种信息都包括H和L两位,是由于8位表示不完该数据,于是分高低两部分)
这样我们便不难理解InitMPU6050()和GetData(uchar REG_Address)函数:初始化函数是向相应的地址写初始化配置数据(关于0x00\0x07等意思请参看MPU6050寄存器版说明书),而GetData则是传入想获得数据项的低地址,然后连续读取当前地址数据和下一地址数据合成为想要的项目数据(上面讲了数据分高低部分)。
//-----------------------------------------
//初始化MPU6050
//-----------------------------------------
void InitMPU6050()
{
Single_WriteI2C(PWR_MGMT_1, 0x00); //解除休眠状态
Single_WriteI2C(SMPLRT_DIV, 0x07);
Single_WriteI2C(CONFIG, 0x06);
Single_WriteI2C(GYRO_CONFIG, 0x18);
Single_WriteI2C(ACCEL_CONFIG, 0x01);
}
//-----------------------------------------
//合成数据
//-----------------------------------------
int GetData(uchar REG_Address)
{
uchar H,L;
H=Single_ReadI2C(REG_Address);
L=Single_ReadI2C(REG_Address+);
return (H<<)+L; //合成数据
}
2、陀螺仪数据采集与传输及帧格式介绍(小技巧)
上面我们已经知道单片机如何利用I2C设置MPU6050的工作属性,以及从MPU6050获得3轴加速度和3轴角速度的数据。那么接下来将介绍单片机是如何将数据通过蓝牙发送给上位机的。如下图左半部分,下位机部分包括一个MPU6050、一个单片机、一个电源模块,以及一个蓝牙模块。对于蓝牙模块我不想做过多的讲解(我记得我已经写了不下于3次关于手机、PC等和下位机通信的教程了:(如果是想用安卓手机和蓝牙模块通信来实现遥控功能的话,可以参考:http://www.cnblogs.com/zjutlitao/p/4231635.html;想用笔记本和蓝牙模块通信来实现遥控功能的话可以参考:http://www.cnblogs.com/zjutlitao/p/3886826.html)
其实,利用串口蓝牙模块单片机要做的工作和对串口进行的操作一样,对串口写数据则送至蓝牙模块将数据发出,当外部有数据传送过来时,单片机可以用相应的中断捕获该事件,然后接收消息。因此主函数中初始化串口和MPU6050之后就进入循环数据发送状态,在循环中GetData是上面介绍的获得3轴加速度、3轴角速度或温度的值的函数,SendData则是将int类型的值转换为字符串然后一位一位的发送出去,而最开始和最后分别发送一个#和$作为该帧的开始和结束标志位,具体格式如下:
# | 1 | 2 | 3 | 5 | 4 | - | 2 | 1 | 3 | 3 | 2 | - | 2 | 1 | 1 | 2 | 5 | $ |
注:符号位要么为'-',要么为空。
//-----------------------------------------
//主程序
//-----------------------------------------
void main()
{
delay(); //上电延时
init_uart();
InitMPU6050(); //初始化MPU6050
delay();
while()
{
SeriPushSend('#');//
SendData(GetData(0x3B)); //X轴加速度
SendData(GetData(0x3D)); //Y轴加速度
SendData(GetData(0x3F)); //Z轴加速度
SeriPushSend('$'); //结束
delay();
}
}
3、基于C#的串口接收函数(C#基本知识)
上面讲到下位机通过串口蓝牙将数据发送给上位机,那么上位机如何接收蓝牙信号呢?其实以我的笔记本为例,因为笔记本内置蓝牙模块,所以无需在上位机上独立安装一个USB-蓝牙模块。而上位机操作蓝牙模块和操作串口几乎一模一样。如下面的C#程序,当点击连接按钮时实例化SerialPort,设置端口号、读超时、然后实例化一个串口数据接收事件句柄(这里PortDataReceived作为数据接收的回调函数)。
//Create a serial port for Connection
SerialPort Connection = new SerialPort();
private void btn_link_Click(object sender, EventArgs e)
{
if (!Connection.IsOpen)
{
//Start
//Status = "正在连接...";
Connection = new SerialPort();
btn_link.Enabled = false;
Connection.PortName = PortList.SelectedItem.ToString();
Connection.Open();
Connection.ReadTimeout = ;
Connection.DataReceived += new SerialDataReceivedEventHandler(PortDataReceived);
//Status = "连接成功";
timer1.Start();
}
}
在PortDataReceived中,只要简单调用Connection.Read(data, 0, length);就能从串口缓冲区读取数据到data中。
private void PortDataReceived(object o, SerialDataReceivedEventArgs e)
{
byte[] data = new byte[length];
int num=Connection.Read(data, , length);
datepool.push_back(data,num);//实际接收的不一定是length,之前一直错
Connection.DiscardInBuffer();
Connection.DiscardOutBuffer();
}
注:本来是每次读取1byte放入数据池,结果出现程序运行速度越来越慢,本以为是上面的数据池设计的有问题,结果把数据池里的线程注释掉改为ask函数来每次需要数据时才获得,但是问题并不在于此;于是想到可能是绘制折线图的函数有问题,但是重查了一遍发现问题不在于此;于是仔细测量每个过程耗时,发现每个模块耗时正常,最后发现是由于串口缓冲区数据积累造成程序变慢,(因为下位机每20ms发送一次20byte的数据给上位机,上位机若一次不接收完所有数据,将会造成每次都有剩余而逐渐变慢),于是直接改成每次接收20byte,问题得到解决。
4、多线程数据池解决高速串口实时性问题(难点)
由于下位机10ms发送一次20byte的数据,上位机一方面要做好接收工作,保证数据不拥挤在串口接收缓冲区;另一方面也要实时获取当前从串口读到的最新数据。如果采用传统多线程+锁的机制是可以的,但是当多线程中加入锁势必会影响程序执行效率,通过综合分析该问题最终抽象出一个特殊的数据模型——自动更新的环形栈:
这样,当采用多线程时,用一个类似于栈的环状栈结构体(实时从串口读数据放入数据池,数据池用p_write标记最新数据存储位置,当外部程序想得到最新数据时,调用ask程序,ask程序从当前p_write向前取40个数据(因为有效数据长度为20,一次取40保证至少有一个有效数据),然后从这40个数据中找出有效信息,赋值给X,Y,Z;然后外部程序可以直接用对象访问X,Y,Z),通过适当调节环的容量达到自我覆盖的效果,同时根据p_write指针可以实时取得最新数据。
/// <summary>
/// 询问当前值
/// </summary>
/// <returns>如果解析到则返回真</returns>
public bool ask()
{
i = ;//立刻将相应的40个字符复制出来
p_read_from = p_write - ;
while (i < )
{
str[i] = pool[(p_read_from + pool_size) % pool_size];
i++;
p_read_from++;
}
i = ;
while (i > && str[i] != '$') i--;
if (i == ) return false;
i--;
data_Z = ;
for (int j = ; j > -; j--)
{
data_Z *= ;
data_Z += (str[i - j] - '');
}
if (str[i - ] == '-') data_Z = -data_Z;
i -= ; data_Y = ;
for (int j = ; j > -; j--)
{
data_Y *= ;
data_Y += (str[i - j] - '');
}
if (str[i - ] == '-') data_Y = -data_Y;
i -= ; data_X = ;
for (int j = ; j > -; j--)
{
data_X *= ;
data_X += (str[i - j] - '');
}
if (str[i - ] == '-') data_X = -data_X; X = data_X;
Y = data_Y;
Z = data_Z;
return true;
} /// <summary>
/// 将数据输入数据池
/// </summary>
/// <param name="date">数据</param>
/// <param name="length">长度</param>
internal void push_back(byte[] date, int length)
{
for (int i = ; i < length; i++)
{
pool[p_write++] = date[i];
if (p_write == pool_size) p_write = ;
}
}
5、折线图可视化模块(程序员基本功)
通过上面几步我们已经可以将下位机的陀螺仪3轴的加速度收集过来了,但是如果先将数据收集好,然后再用matlab绘制,我们很难知道哪个动作对应哪个数据,不利于我们观察效果(虽然matlab上自带串口接口,但是LZ就是任性!有一张好看的脸,还是想着靠实力赢得地位,哈哈哈~)。
如本节小标题括号内所示,在C#里写一个绘制折线图的程序应该属于我们的基本功(我可不是调用相应的绘图接口哦!),其大致思想就是用一个List存储num个数据,当list中的数据少于num个时则不断添加,当list内的数据大于num个时,则从尾部进来一个的同时从头部删除一个(这样才能实现perfect的效果)。
注:其实中间还出现了一个逻辑错误性小插曲:原初写好之后,本以为能够实现高效数据采集显示,但是仔细观察发现还是有很大延时,但是旁边的数据显示却非常实时。这是为什么呢?查找了一会最终发现问题出在折线图绘制上——本来采用固定的模式(一张图能存放多少数据点就用vector<int>P/Q/R在初始化的时候存放这么多点,然后每次有一个新的数据过来时就会将新数据加到vector后面,同时删除最前面的一个数据,这样做是为了方便初始vector里没有数据绘制折线图错误的问题),可是问题就出在这!咋一看这种思路很好,初始化vector中放num个点,每次新的来到将最前面一个数据冲掉,这样这个vector始终保持着num个点,且最新的在最后面,整个折线图能反应实时情况。但是由于我为了“安全”起见,在vector初始化时多Add几个数据,这样导致vector中的数据量N>折线图一次能呈现的数据量num,所以最新的数据总会在之后出现!当时没有想到是这个原因,就直接改了下DateLineChar函数,实现根据vector大小自动绘制的算法(这样就不用预先在vector中装入一定量的值了)
6、预告与小结(预知后事如何,请听下回分解)
上面我只是简单收集了MPU6050的3轴加速度数值,当MPU6050位置固定好之后,我们就能根据数据推测其具体的姿态。例如:
绿色的z轴方向的加速度先高后低,红色y轴方向加速度先低后高,蓝色x轴方向加速度和y轴类似,但是比y轴幅度变化小,而后半周期数值正负正好相反。那么MPU6050运动过程大致为:在y轴方向上做往返运动,同时在x轴和z轴方向有稍微的偏转。(水平静止放置时z轴为重力加速度,x,y为0)
绿色的z轴变化不大,红色的y和蓝色的x同步类正弦变化。呵呵,这个运动状态分析起来就不太容易了~不过没关系,接下来我们要进一步获取并计算MPU6050的倾角,甚至是利用卡尔曼滤波计算MPU6050的运动距离,最终达到perfect的运动跟踪效果~
链接
51MPU6050采集代码:http://pan.baidu.com/s/1c0yE7Ws
4月2号总工程:http://pan.baidu.com/s/1hqzSt7Y (我用)
4月7号总工程:http://pan.baidu.com/s/1pJwq6qZ (我用)
github:https://github.com/beautifulzzzz/C4plus/tree/master/体感游戏
预习用1:[芯片][MPU6050] MPU60X0的DMP相关链接
预习用2:[stm32] MPU6050 HMC5883 Kalman 融合算法移植
[体感游戏] 1、MPU6050数据采集传输与可视化的更多相关文章
- 使用HTML5开发Kinect体感游戏
一.简介 我们要做的是怎样一款游戏? 在前不久成都TGC2016展会上,我们开发了一款<火影忍者手游>的体感游戏,主要模拟手游章节<九尾袭来 >,用户化身四代,与九尾进行对决, ...
- 【CityHunter】基于LBS的AR体感游戏设计理念
本人目前还不是游戏行业的圈内人士,并不懂得,游戏行业的生态圈,也不懂得,所谓的什么“中国市场环境”.所以不敢发表关于这方面的见解,不过我在这里想要插一句话,就是我认为啊,行业内,人与人之间还是有分层次 ...
- OpenNI结合Unity3D Kinect进行体感游戏开发(转)
OpenNI结合Unity3D Kinect进行体感游戏开发(转) 楼主# 更多 发布于:2012-07-17 16:42 1. 下载安装Unity3D(目前版本为3.4)2. 下载OpenN ...
- 使用unity3d和tensorflow实现基于姿态估计的体感游戏
使用unity3d和tensorflow实现基于姿态估计的体感游戏 前言 之前做姿态识别,梦想着以后可以自己做出一款体感游戏,然而后来才发现too young.但是梦想还是要有的,万一实现了呢.趁着p ...
- scratch编程体感游戏
体感游戏有很多种,最常见的就是摄像头和声控了,今天我们要用scratch编写一系列的体感游戏!!!是不是很激动呢? 首先我们来编摄像头类的: No.1拳头打幽灵 挥动头就能打到幽灵了哟! 具体程序如下 ...
- C#开发体感游戏 Kinect应用知识
Kinect首先是一个XBox 360外接体感设备,通过无线方式捕捉动作感知.由PrimeSense提供Range Camera技术,同类产品如任天堂Wii.Play Station Move,必须让 ...
- 使用IntelRealScene设备结合Cocos引擎实现体感游戏开发
英特尔开发人员专区原文地址 Cocos游戏开发引擎对于广大开发者来说都比较熟悉,Intel RealScene是什么呢,简单理解是一种特殊的摄像头,可以捕捉用户的手势,面部表情等,进而实现AR,VR的 ...
- cocos2dx+KinectV2 体感游戏之微信打飞机
https://download.csdn.net/download/qq_34609108/10038417 https://blog.csdn.net/qq_34609108/article/de ...
- 历时一周,unity3d+xtion打造我的第一个休闲体感小游戏《空降奇兵》
1.游戏介绍 本游戏属于休闲小游戏,主要操作如下: 菜单控制:举起左手或右手,点击左边或者右边的菜单:挥动左手或右手,选择关卡: 操作方式:玩家跳跃,游戏中的伞兵从飞机开始降落:玩家通过控制伞兵的左右 ...
随机推荐
- error C2065: “CDatabase”: 未声明的标识符
使用vc++与access的接口时出现错误“error C2065: “CDatabase”: 未声明的标识符” 解决方法: 添加 #include "afxdb.h"
- Mono for android 如何动态添加View,线程内部如何更新UI.
貌似所有设计到UI的程序原理都是一样的,子线程是不能够更新UI状态的,所以就必须使用UI自身或者第三方来更新UI. 如 在WinForm 中 就可以使用Control.Invoke(Action ac ...
- discuz MVC结构分析
Discuz软件经解压后产生的三个文件夹中的一个叫upload的成为网站的根目录.里面的内容可以在某些网站上在线阅读,如用好库编程网.也可以离线在本地阅读,如用VS.Php for Visual St ...
- XAMARIN +VS2015 ANDROID 开发判断gps 是否打开。
在获取位置的时候首先要判断gps是否打开,如果没有打开就要提示打开,当然最友好的就是直接调转到打开界面. LocationManager alm = (LocationManager)this.Get ...
- PHP使用XHProf进行性能分析
1. 编译安装 wget http://pecl.php.net/get/xhprof-0.9.3.tgz tar zxvf xhprof-0.9.3.tgz cd xhprof-0.9.3/exte ...
- flashftp连接虚拟机centos报错的解决方法
flashftp连接虚拟机centos报错,一般情况可能是因为端口(22)的权限没有开放 先在centos中检查并开放22端口,执行:iptables -I INPUT -p tcp --dport ...
- 移动端字体缩放问题解决方案-摘自《html5移动web开发实践》
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name ...
- Don't Block on Async Code【转】
http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html This is a problem that is brough ...
- Unsupported major.minor version 51.0解决方法
jdk1.6工程中使用外部jar包中类出现:Unsupported major.minor version 51.0 问题原因:外部jar包使用jdk1.7(jdk7)编译,而使用此jar包的工程jd ...
- 在 Mac OS X 上创建的 .NET 命令行程序访问数据库 (使用Entity Framework 7 )
var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...