最近在研究體感遊戲,到目前爲止實現了基於51單片機的MPU6050數據採集、利用藍牙模塊將數據傳輸到上位機,並利用C#自制串口數據高速採集軟件,而且將數據經過自制的折線圖繪製模塊可視化地展現出來等功能。本文將主要對實現這意見單系統中遇到的問題作一個小結——其中包括:html
三、基於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 }
上面講到下位機經過串口藍牙將數據發送給上位機,那麼上位機如何接收藍牙信號呢?其實以個人筆記本爲例,由於筆記本內置藍牙模塊,因此無需在上位機上獨立安裝一個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/體感遊戲