[體感遊戲] 一、MPU6050數據採集傳輸與可視化

 

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

一、基於51的MPU6050模塊通訊簡介(入門級)git

二、陀螺儀數據採集與傳輸及幀格式介紹(小技巧)程序員

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

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

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

關鍵詞:MPU6050 藍牙 C#串口 多線程 高速串口 折線圖繪製多線程


 

一、基於51的MPU6050模塊通訊簡介(入門級)socket

由於是入門級,就先最簡單的介紹如何利用51從MPU6050中讀取數據吧(對於想知道卡爾曼濾波、俯角仰角、距離測量、摔倒檢測、記步等算法的可能要在接下來介紹)。既然要和MPU6050通訊,那麼必不可少的是閱讀芯片手冊,若是您以爲親自去看又長又多並且都是英文的手冊很費時,不仿看看我找的簡要版:函數

MPU-60X0是全球首例9軸運動處理器。它集成了3軸MEMS陀螺儀,3軸MEMS加速計,以及1個可擴展的數字運動處理器DMP(Digital Motion Processor),可用I2C接口鏈接一個第三方的數字傳感器,好比磁力計。擴展以後就能夠經過其I2C或SPI接口輸出一個9軸的信號。MPU-60X0也能夠經過其I2C接口鏈接非慣性的數字傳感器,好比壓力傳感器。
post

   

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 }

 

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

上面咱們已經知道單片機如何利用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 }

 

三、基於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,問題獲得解決。


 

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

因爲下位機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 }

 

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

經過上面幾步咱們已經能夠將下位機的陀螺儀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中裝入必定量的值了)


 

六、預告與小結(預知後事如何,請聽下回分解)

上面我只是簡單收集了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 融合算法移植

相關文章
相關標籤/搜索