[體感游戲] 1、MPU6050數據采集傳輸與可視化


 

最近在研究體感游戲,到目前為止實現了基於51單片機的MPU6050數據采集、利用藍牙模塊將數據傳輸到上位機,並利用C#自制串口數據高速采集軟件,並且將數據通過自制的折線圖繪制模塊可視化地展示出來等功能。本文將主要對實現這意見單系統中遇到的問題做一個小結——其中包括:

1、基於51的MPU6050模塊通信簡介(入門級)

2、陀螺儀數據采集與傳輸及幀格式介紹(小技巧)

3、基於C#的串口接收函數(C#基本知識)

4、多線程數據池解決高速串口實時性問題(難點)

5、折線圖可視化模塊(程序員基本功)

關鍵詞: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總線通信的代碼:

 1 #include <REG52.H>
 2 #include <INTRINS.H>
 3     
 4 typedef unsigned char  uchar;
 5 typedef unsigned short ushort;
 6 typedef unsigned int   uint;
 7 
 8 //-----------------------------------------
 9 // 定義MPU6050內部地址
10 //-----------------------------------------
11 #define    SMPLRT_DIV      0x19    //陀螺儀采樣率,典型值:0x07(125Hz)
12 #define    CONFIG          0x1A    //低通濾波頻率,典型值:0x06(5Hz)
13 #define    GYRO_CONFIG     0x1B    //陀螺儀自檢及測量范圍,典型值:0x18(不自檢,2000deg/s)
14 #define    ACCEL_CONFIG    0x1C    //加速計自檢、測量范圍及高通濾波頻率,典型值:0x01(不自檢,2G,5Hz)
15 #define    ACCEL_XOUT_H    0x3B
16 #define    ACCEL_XOUT_L    0x3C
17 #define    ACCEL_YOUT_H    0x3D
18 #define    ACCEL_YOUT_L    0x3E
19 #define    ACCEL_ZOUT_H    0x3F
20 #define    ACCEL_ZOUT_L    0x40
21 #define    TEMP_OUT_H      0x41
22 #define    TEMP_OUT_L      0x42
23 #define    GYRO_XOUT_H     0x43
24 #define    GYRO_XOUT_L     0x44    
25 #define    GYRO_YOUT_H     0x45
26 #define    GYRO_YOUT_L     0x46
27 #define    GYRO_ZOUT_H     0x47
28 #define    GYRO_ZOUT_L     0x48
29 #define    PWR_MGMT_1      0x6B    //電源管理,典型值:0x00(正常啟用)
30 #define    WHO_AM_I        0x75    //IIC地址寄存器(默認數值0x68,只讀)
31 #define    SlaveAddress    0xD0    //IIC寫入時的地址字節數據,+1為讀取
32 
33 //-----------------------------------------
34 // I2C總線通信函數
35 //-----------------------------------------
36 void  I2C_Start();                  //I2C起始信號
37 void  I2C_Stop();                   //I2C停止信號
38 void  I2C_SendACK(bit ack);         //I2C發送應答信號[入口參數:ack (0:ACK 1:NAK)]
39 bit   I2C_RecvACK();                //I2C接收應答信號
40 void  I2C_SendByte(uchar dat);      //向I2C總線發送一個字節數據
41 uchar I2C_RecvByte();               //從I2C總線接收一個字節數據
42 void  Single_WriteI2C(uchar REG_Address,uchar REG_data);//向I2C設備寫入一個字節數據
43 uchar Single_ReadI2C(uchar REG_Address);                //從I2C設備讀取一個字節數據
44 
45 //-----------------------------------------
46 // 通過I2C和MPU6050通信的函數
47 //-----------------------------------------
48 void InitMPU6050();                //初始化MPU6050
49 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則是傳入想獲得數據項的低地址,然后連續讀取當前地址數據和下一地址數據合成為想要的項目數據(上面講了數據分高低部分)。

 1 //-----------------------------------------
 2 //初始化MPU6050
 3 //-----------------------------------------
 4 void InitMPU6050()
 5 {
 6     Single_WriteI2C(PWR_MGMT_1, 0x00);    //解除休眠狀態
 7     Single_WriteI2C(SMPLRT_DIV, 0x07);
 8     Single_WriteI2C(CONFIG, 0x06);
 9     Single_WriteI2C(GYRO_CONFIG, 0x18);
10     Single_WriteI2C(ACCEL_CONFIG, 0x01);
11 }
12 //-----------------------------------------
13 //合成數據
14 //-----------------------------------------
15 int GetData(uchar REG_Address)
16 {
17     uchar H,L;
18     H=Single_ReadI2C(REG_Address);
19     L=Single_ReadI2C(REG_Address+1);
20     return (H<<8)+L;   //合成數據
21 }

 

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 $

 

 

注:符號位要么為'-',要么為空。

 1 //-----------------------------------------
 2 //主程序
 3 //-----------------------------------------
 4 void main()
 5 { 
 6     delay(500);        //上電延時        
 7     init_uart();
 8     InitMPU6050();    //初始化MPU6050
 9     delay(150);
10     while(1)
11     {
12         SeriPushSend('#');//
13         SendData(GetData(0x3B));    //X軸加速度
14         SendData(GetData(0x3D));    //Y軸加速度
15         SendData(GetData(0x3F));    //Z軸加速度
16         SeriPushSend('$'); //結束
17         delay(20);
18     }
19 }

 

3、基於C#的串口接收函數(C#基本知識)

上面講到下位機通過串口藍牙將數據發送給上位機,那么上位機如何接收藍牙信號呢?其實以我的筆記本為例,因為筆記本內置藍牙模塊,所以無需在上位機上獨立安裝一個USB-藍牙模塊。而上位機操作藍牙模塊和操作串口幾乎一模一樣。如下面的C#程序,當點擊連接按鈕時實例化SerialPort,設置端口號、讀超時、然后實例化一個串口數據接收事件句柄(這里PortDataReceived作為數據接收的回調函數)。

 1 //Create a serial port for Connection
 2 SerialPort Connection = new SerialPort();
 3 private void btn_link_Click(object sender, EventArgs e)
 4 {
 5     if (!Connection.IsOpen)
 6     {
 7         //Start
 8         //Status = "正在連接...";
 9         Connection = new SerialPort();
10         btn_link.Enabled = false;
11         Connection.PortName = PortList.SelectedItem.ToString();
12         Connection.Open();
13         Connection.ReadTimeout = 10000;
14         Connection.DataReceived += new SerialDataReceivedEventHandler(PortDataReceived);
15         //Status = "連接成功";
16         timer1.Start();
17     }
18 }

在PortDataReceived中,只要簡單調用Connection.Read(data, 0, length);就能從串口緩沖區讀取數據到data中。

1 private void PortDataReceived(object o, SerialDataReceivedEventArgs e)
2 {
3     byte[] data = new byte[length];
4     int num=Connection.Read(data, 0, length);
5     datepool.push_back(data,num);//實際接收的不一定是length,之前一直錯
6     Connection.DiscardInBuffer();
7     Connection.DiscardOutBuffer();
8 }

注:本來是每次讀取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指針可以實時取得最新數據。

 1 /// <summary>
 2 /// 詢問當前值
 3 /// </summary>
 4 /// <returns>如果解析到則返回真</returns>
 5 public bool ask()
 6 {
 7     i = 0;//立刻將相應的40個字符復制出來
 8     p_read_from = p_write - 40;
 9     while (i < 40)
10     {
11         str[i] = pool[(p_read_from + pool_size) % pool_size];
12         i++;
13         p_read_from++;
14     }
15     i = 39;
16     while (i > 18 && str[i] != '$') i--;
17     if (i == 18) return false;
18     i--;
19     data_Z = 0;
20     for (int j = 4; j > -1; j--)
21     {
22         data_Z *= 10;
23         data_Z += (str[i - j] - '0');
24     }
25     if (str[i - 5] == '-') data_Z = -data_Z;
26     i -= 6;
27 
28     data_Y = 0;
29     for (int j = 4; j > -1; j--)
30     {
31         data_Y *= 10;
32         data_Y += (str[i - j] - '0');
33     }
34     if (str[i - 5] == '-') data_Y = -data_Y;
35     i -= 6;
36 
37     data_X = 0;
38     for (int j = 4; j > -1; j--)
39     {
40         data_X *= 10;
41         data_X += (str[i - j] - '0');
42     }
43     if (str[i - 5] == '-') data_X = -data_X;
44 
45     X = data_X;
46     Y = data_Y;
47     Z = data_Z;
48     return true;
49 }
50 
51 /// <summary>
52 /// 將數據輸入數據池
53 /// </summary>
54 /// <param name="date">數據</param>
55 /// <param name="length">長度</param>
56 internal void push_back(byte[] date, int length)
57 {
58     for (int i = 0; i < length; i++)
59     {
60         pool[p_write++] = date[i];
61         if (p_write == pool_size) p_write = 0;
62     }
63 }

 

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 融合算法移植

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM