首先恭喜你們挺過了測試二!爲何說「挺」呢?由於測試二的難度和測試一相比有一個比較大的跳躍:首先測試一僅僅利用現有硬件模塊稍加改造而DIY一個藍牙防丟器,而測試二則要求你們具備從腦殼裏的一個想法到一個全新的小設備的實現的所有能力,顯然該過程不是連幾根線那麼簡單;其次測試一對藍牙的使用僅限於信號搜索層面,而測試二一會兒深刻到可靠通訊的層面了,其難度可想而知;最後在測試二中客戶端的設計中複雜的狀態轉換過程,以及嵌入式編程時須要對所使用的硬件做細緻的分析,都構成了對前期基礎沒打牢的同窗一種挑戰。不過好消息是你們挺過來了,接下來的模式大體相同。java
如今,讓咱們回到本次的正題!可能細心的讀者會發現上次的遙控小風扇雖然利用了藍牙的通訊功能,可是僅限於客戶端向硬件發送命令(在上次並未使用更嚴謹的應答模式,即客戶端發送控制命令->硬件響應命令->硬件返回響應完畢命令)。爲了彌補上次的缺陷,那麼本次會將重點放在數據收集方面,樓主想來想去以爲仍是一個簡單的記步手環最適合了。[正版請搜索:beautifulzzzz(看樓主博客園官方博客,享高質量生活)]git
1 智能手環簡介算法
智能手環是一種穿戴式智能設備。經過該設備,用戶能夠記錄平常生活中的鍛鍊、睡眠等實時數據,並將這些數據與手機、平板同步,起到經過數據指導健康生活的做用。另外,智能手環還具備社交功能,可以將鍛鍊狀況和睡眠質量發送到社交網絡進行分享。編程
圖 1_1某款智能手環api
一個智能手環最小系統通常包括:可充電的電源模塊、控制模塊(圖1_2中左邊芯片)、藍牙模塊(右邊芯片)、存儲模塊和加速計模塊(上面芯片)。其中加速計是爲了得到佩戴者在運動或睡眠過程當中的加速度數據,經過分析這些數據則可以判斷佩戴者的運動狀況和睡眠質量;存儲模塊主要負責將實時數據暫存,接着在適當的時刻藉助藍牙模塊將數據同步到手機端。方便起見本次要自制的記步手環將不採用存儲器暫存,而是將數據實時地傳送到手機端。同時爲了便於你們對記步算法的理解,客戶端將採用一個折線圖的形式實時展現記步手環收集的數據。數組
圖 1_2某款智能手環核心電路板服務器
2 如何實現記步網絡
看了上面的分析你們可能會疑惑——僅僅用一個加速計怎麼能實現記步和睡眠質量檢測呢?其實確實能夠!由於加速計能夠實時獲取自身的X\Y\Z三個軸向的加速度。當其靜止時合加速度會在重力加速度附近波動;當佩戴者處於深度睡眠過程當中時,其合加速度將呈現出長時間的穩定於重力加速度附近;當其隨着運動的佩戴者手臂而作週期性擺動時,其數據也是有必定規律可循的。這樣,設計時只要經過分析從加速計獲的數據就能實現對運動或睡眠質量的記錄。數據結構
3 預期效果構思多線程
上面已經提到:爲了方便,咱們並未採用存儲器實現記步手環的離線記錄,而是實時地將數據發送到客戶端由一個可視化的折線圖動態繪製結果。如圖3_1所示系統中記步手環部分包含單片機模塊、藍牙模塊、加速計模塊和電源模塊,這樣經過單片機的協調能夠實現將加速計模塊的數據經過藍牙實時地傳送給客戶端程序。在客戶端部分則負責將收集到的實時數據以折線圖的形式動態地展現出來,此外客戶端中也加入一個滑動條來控制記步閾值來真正讓你們明白其設計思想(真正商業化的智能手環多數採用的是先將有效數據保存在手環的小型存儲器中,上位機週期性地將數據收集並同步到服務器端)。
圖 3_1 預期效果圖
4 硬件總體設計
如圖4_1,相比於上一個無線小風扇該硬件構成反而比較簡單:藍牙模塊依然採用咱們比較熟悉的HC-06模塊,對於加速度的測量採用四周飛行器上常採用的MPU6050模塊。該模塊不只含有加速計的功能,還具備陀螺儀的功能,其在汽車防側翻、相機雲臺穩定、機器人平衡、空中鼠標、姿態識別等衆多領域都有應用,這裏咱們只是利用了它的加速計功能。此外要注意:圖4_1所示的單片機模塊的電源引腳被隱藏了,在真正設計鏈接時必定不要忽略這兩個引腳!
圖 4_1 硬件電路圖
5 MPU6050介紹
MPU-60X0是全球首例9軸運動處理器。它集成了3軸MEMS陀螺儀,3軸MEMS加速計,以及1個可擴展的數字運動處理器DMP(Digital Motion Processor)。如圖5_1所示軸向是相對於加速計說的,當芯片水平靜止放置時x軸和y軸的加速度份量幾乎爲0,z軸的加速度份量約爲當地的重力加速度;而旋轉極性則是對陀螺儀來講的,本次先不介紹。
圖 5_1 MPU-60X0軸向和旋轉的極性(來自MPU6050數據手冊)
爲什麼上面說9軸信號呢?由於MPU-60X0可用I2C接口鏈接一個第三方的數字傳感器,好比磁力計。擴展以後就能夠經過其I2C或SPI接口輸出一個9軸的信號。也能夠經過其I2C接口鏈接非慣性的數字傳感器,好比壓力傳感器。(爲何特別提磁力計和壓力傳感器呢?由於在飛控方面,利用陀螺儀和加速計能夠計算飛行器的傾角,從而調節飛行器平衡。可是隻是調節平衡對方向沒有概念也不能執行復雜任務,所以須要配備磁力計(也即電子羅盤傳感器)。此外,因爲飛行器在不一樣高度做業時,其周圍的重力加速度也不一樣,這樣會影響傾角的準確性,所以經過氣壓計計算所處高度而後計算實時加速度達到精確控制的效果。)
圖 5_2 MPU-60X0典型工做電路(來自MPU6050數據手冊)
MPU-60X0對陀螺儀和加速計分別用了三個16位的ADC,將其測量的模擬量轉化爲可輸出的數字量。爲了精確跟蹤快速和慢速運動,傳感器的測量範圍是可控的,陀螺儀可測範圍爲±250,±500,±1000,±2000°/秒(dps),加速計可測範圍爲±2,±4,±8,±16g(重力加速度)。如圖5_3是直接從16位ADC中讀出的6軸的數據(從左到右依次爲加速計X軸數據、Y軸數據、Z軸數據、陀螺儀X極數據、Y極數據、Z極數據):
圖 5_3 MPU6050輸出加速計和陀螺儀6軸的原始數據
可是這裏的輸出值並非真正的加速度和角速度的值,上面說過,MPU是一個16位AD量程可程控的設備,這裏設置的加速度傳感器的測量量程爲正負2g(這裏的g爲重力加速度),陀螺儀的量程爲正負2000°/s。因此要用下面的公式進行轉化:
圖5_4 實際值計算公式
最後給你們推薦一款比較容易買到的MPU6050,如圖5_5該模塊將核心芯片和外圍電路集成到一個模塊上並留出八個引腳,本次使用只需用到上面四個便可(具體鏈接參考圖4_1)。
圖5_5 MPU6050模塊
6 一個簡單的記步算法設計
第二小節講到當MPU6050隨着運動的佩戴者手臂而作週期性擺動時,其數據也是有必定規律可循的。簡單起見咱們只分析合加速度:一個擺臂週期其合加速度會在重力加速度上下波動,如圖6_1只要選取合適的閾值(黑線表明閾值),每次檢測出合加速度大於該閾值則認爲是一次擺臂,從而能夠實現記步的功能。這裏要特別說明下:若是想把你的手環推向市場,就要經過大量分析擺臂數據創建一套更好的記步算法,若是偷懶只用樓主的簡單算法,當心產品推出後被用戶的口水淹死(哈哈)!
圖 6_1 擺臂時合加速度變化圖
7 I2C總線介紹
上次咱們在使用藍牙串口模塊時使用過串口通訊,因爲51系列單片機將串口通訊不少細節都封裝到芯片內部,因此咱們即便設計了串口驅動模塊,也並無真正瞭解串口通訊的核心思想。其實串口協議的出現是爲了構成一個總線線路,這樣單片機只要使用比較少的引腳就能和比較多的設備進行通訊了,這裏要用到的I2C總線也具備相同的效果但又有些不一樣。
圖 7_1I2C總線掛接多個設備圖
I2C(Inter-Integrated Circuit)總線是由PHILIPS公司開發的兩線式串行總線,用於鏈接微控制器及其外圍設備。是微電子通訊控制領域普遍採用的一種總線標準。它是同步通訊的一種特殊形式,具備接口線少,控制方式簡單,器件封裝形式小,通訊速率較高等優勢。如圖7_1採用I2C總線後CPU只要使用2個引腳即可和多個設備進行通訊(其實每一個採用I2C通訊方式的設備都具備惟一的地址碼,這樣在總線中便可以被惟一識別),從而大大減小了引腳的使用。
在I2C總線中使用的兩線爲時鐘線SCL和數據線SDA。全部的I2C主從設備都是隻被這兩根線鏈接起來的。每個設備既能夠做爲發送方,也能夠做爲接收方,或者既能夠做爲發送發也能夠做爲接收方。在總線中的主設備通常起產生時鐘信號和初始化通訊的做用,從設備則負責響應主設備發出的命令。爲了在總線上區分每個設備,每個從設備必須有一個惟一的地址。主設備通常不須要地址(通常爲微處理器),由於從設備不能發送命令給主設備。
圖 7_2 I2C總線中主從設備
這裏要先介紹I2C總線中幾個專有名詞:
l 發送者:將數據發送到總線的設備
l 接收者:從總線接收數據的設備
l 主設備:產生時鐘信號、啓動通訊、發送I2C命令和終止通訊的設備
l 從設備:監聽總線、能被主設備尋址的設備
l 多主設備:I2C可以擁有多個主設備,並且每一個主設備都可以發送命令
l 仲裁:當多個主設備請求使用總線時,決定哪個主設備能夠佔用的一個過程
l 同步:同步多個設備時鐘信號的一個過程
上面是從宏觀上對I2C總線介紹了下,接下來將深刻細節研究其通訊過程:
n 串行數據傳送:
在總線備用時SDA和SCL都必須保持高電平狀態,只有關閉I2C總線時才能使SCL鉗位在低電平。在I2C總線數據傳輸時,在時鐘線高電平期間,數據線上必須保持有穩定的邏輯電平(也就是說在數據傳輸期間只有時鐘線低電平期間,才容許數據線上的電平發生變化)。
圖 7_3 串行數據發送
所以在如圖7_3中對於每個時鐘脈衝期間一比特的數據將會被傳送,SDA只能在時鐘信號爲低電平時才能改變。下面是代碼中發送一字節的函數:在循環體內每次將dat內的最高位移出到CY中,進而賦值給SDA(這時SCL爲低,SDA可改變)。接着拉高SCL並保持5us,最後再拉低SCL實現一個時鐘脈衝將dat中最高位送出。依此循環8次實現將dat所有傳出。
1 //------------------------------------------------ 2 //向I2C總線發送一個字節數據 3 //------------------------------------------------ 4 void I2C_SendByte(uchar dat) 5 { 6 uchar i; 7 for (i=0; i<8; i++) //8位計數器 8 { 9 dat <<= 1; //移出數據的最高位 10 SDA = CY; //送數據口 11 SCL = 1; //拉高時鐘線 12 Delay5us(); //延時 13 SCL = 0; //拉低時鐘線 14 Delay5us(); //延時 15 } 16 I2C_RecvACK(); 17 }
n 開始和結束條件:
命令不會沒有任何預兆直接發送的,每個I2C命令的發送老是開始於開始條件並結束於終止條件。這裏所謂的開始條件和終止條件起始也是由SCL和SDA組合造成的(如圖7_4)。
圖 7_4 開始和結束條件
若是時鐘線保持高電平期間,數據線出現由高到低的電平變化,則會啓動I2C總線,此時爲I2C的起始信號:
1 //------------------------------------------------ 2 //I2C起始信號 3 //------------------------------------------------ 4 void I2C_Start() 5 { 6 SDA = 1; //拉高數據線 7 SCL = 1; //拉高時鐘線 8 Delay5us(); //延時 9 SDA = 0; //產生降低沿 10 Delay5us(); //延時 11 SCL = 0; //拉低時鐘線 12 }
若在時鐘線保持高電平期間,數據線出現由低到高的電平變化,則會中止I2C總線的數據傳輸,此時爲I2C的終止信號:
1 //------------------------------------------------ 2 //I2C中止信號 3 //------------------------------------------------ 4 void I2C_Stop() 5 { 6 SDA = 0; //拉低數據線 7 SCL = 1; //拉高時鐘線 8 Delay5us(); //延時 9 SDA = 1; //產生上升沿 10 Delay5us(); //延時 11 }
開始條件以後I2C總線被認爲是忙狀態,只有當中止信號以後其餘主設備才能使用該總線。此外,當開始條件以後主設備可以屢次發出開始信號。這些開始信號和第一次發出的開始信號相似,他們後面常常會跟從設備的地址。這樣能夠方便實如今I2C總線忙期間,當前佔線的主設備能夠和不一樣的從設備進行通訊。
n I2C數據傳送:
I2C總線上傳送的每個字節均爲8位,可是每啓動一次I2C總線,其後的數據傳送字節數是沒有限制的。同時每傳送一字節的數據後面都要跟隨一個接收者回應的應答位(低電平爲應答信號,高電平爲非應答信號),當所有數據發送完畢後主設備發送終止信號。
圖 7_5 數據傳送圖
因此在上面向I2C總線發送一字節的數據的代碼的最後有一個I2C_RecvACK()函數。(以下)該函數負責接收接收者發送過來的應答信號,也即圖7_5中的第9個時鐘脈衝的期間的相應操做。
1 //------------------------------------------------ 2 //I2C接收應答信號 3 //------------------------------------------------ 4 bit I2C_RecvACK() 5 { 6 SCL = 1; //拉高時鐘線 7 Delay5us(); //延時 8 CY = SDA; //讀應答信號 9 SCL = 0; //拉低時鐘線 10 Delay5us(); //延時 11 return CY; 12 }
要特別說明下:全部的數據位包括應答位都須要主設備產生時鐘脈衝。若是從設備沒有應答意味着將沒有更多的數據要傳送或者設備沒有準備好傳送。這時,主設備要麼產生中止信號,要麼從新發出開始條件。
圖 7_6 應答信號
n I2C的7-bit地址:
上面說過每個從設備都應該具備惟一的地址,這樣主設備才能準確的尋址到每個設備,而這些地址被統一規定爲7比特。可是上面講過I2C總線傳輸數據都是8比特傳送,地址7比特豈不是少一位!其實緊跟地址還有一位用來表示是讀操做仍是寫操做的標誌位。若是該位爲0表示主設備將要向從設備寫數據,不然表示主設備將要從從設備讀數據。在這8比特被髮送後主設備可以持續地進行讀或者寫。若是主設備想和其餘從設備進行通訊,只要再次發送一個新的開始信號就能夠而沒必要發送終止信號。
圖 7_7 一個完整的數據讀寫操做
8 MPU6050驅動設計
至此,咱們基本上已經將I2C的知識學完了,下面將結合MPU6050的驅動進一步講解其原理(該部分的代碼參見工程的mpu6050.c部分)。咱們首先來看一下它的頭文件mpu6050.h:從第6到25行上來就是一大串內部地址的定義,對於初學者可能一頭霧水!若是樓主再引入寄存器等數字電路的知識可能又要說幾頁了,因而這裏準備只用一個簡單的例子闡述下這些地址的做用。
1 #include"i2c.h" 2 3 //----------------------------------------- 4 // 定義MPU6050內部地址 5 //----------------------------------------- 6 #define SMPLRT_DIV 0x19 //陀螺儀採樣率,典型值:0x07(125Hz) 7 #define CONFIG 0x1A //低通濾波頻率,典型值:0x06(5Hz) 8 #define GYRO_CONFIG 0x1B //陀螺儀自檢及測量範圍,典型值:0x18(不自檢,2000deg/s) 9 #define ACCEL_CONFIG 0x1C //加速計自檢、測量範圍及高通濾波頻率,典型值:0x01(不自檢,2G,5Hz) 10 #define ACCEL_XOUT_H 0x3B 11 #define ACCEL_XOUT_L 0x3C 12 #define ACCEL_YOUT_H 0x3D 13 #define ACCEL_YOUT_L 0x3E 14 #define ACCEL_ZOUT_H 0x3F 15 #define ACCEL_ZOUT_L 0x40 16 #define TEMP_OUT_H 0x41 17 #define TEMP_OUT_L 0x42 18 #define GYRO_XOUT_H 0x43 19 #define GYRO_XOUT_L 0x44 20 #define GYRO_YOUT_H 0x45 21 #define GYRO_YOUT_L 0x46 22 #define GYRO_ZOUT_H 0x47 23 #define GYRO_ZOUT_L 0x48 24 #define PWR_MGMT_1 0x6B //電源管理,典型值:0x00(正常啓用) 25 #define WHO_AM_I 0x75 //IIC地址寄存器(默認數值0x68,只讀) 26 #define SlaveAddress 0xD0 //IIC寫入時的地址字節數據,+1爲讀取 27 28 //----------------------------------------- 29 // 經過I2C和MPU6050通訊的函數 30 //----------------------------------------- 31 void Single_WriteI2C(uchar REG_Address,uchar REG_data);//向I2C設備寫入一個字節數據 32 uchar Single_ReadI2C(uchar REG_Address); //從I2C設備讀取一個字節數據 33 void InitMPU6050(); //初始化MPU6050 34 int GetData(uchar REG_Address); //合成數據
上面講到在I2C總線中主設備能夠經過固定的7-bit地址尋找到相應的從設備(這裏的7-bit地址爲第26行的SlaveAddress,想必你們也可以理解後面註釋的意義了吧~不加1表示緊跟着地址的一位爲0,表示向該設備寫數據;加1則表示緊跟着的一位爲1,表示主設備從從設備讀數據)。雖然採用這種方式可以準確找到從設備,可是從設備裏面又有比較多的寄存器。這就比如你知道了某個要找的東西在具體的某個大櫃子裏,可是來到大櫃子前又發現有許多小抽屜。這裏的7-bit地址就好像指明瞭哪一個櫃子,而從第6到25行的內部地址就像櫃子上的抽屜編號,而不同之處是位於mpu6050內的「小抽屜」一部分存放着其採集的實時數據,另外一部分等着外部放一些數據來設置其採樣屬性。
這樣,如上面的第6行的SMPLRT_DIV(0x19)是用來設置陀螺儀採樣率的寄存器地址,只要向該地址所指的寄存器寫入相應的值則能夠設置陀螺儀採樣率。所以下面MPU6050初始化函數就是調用封裝的I2C寫函數向相應的小抽屜內寫屬性數據,設置MPU6050採樣屬性。
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 }
再如第10~11行的ACCEL_XOUT_H、ACCEL_XOUT_L是用來存放最新的陀螺儀X極的數值,由於採用16位ADC因此這裏須要用兩個寄存器。因此下面合成數據函數負責連續讀取REG_Address開始的兩字節數據組成一個16位數據。當函數的參數爲ACCEL_XOUT_H時,則獲取的是實時的陀螺儀X極的數值,一樣地能夠得到實時的6軸數據。
1 //------------------------------------------------ 2 //合成數據 3 //------------------------------------------------ 4 int GetData(uchar REG_Address) 5 { 6 uchar H,L; 7 H=Single_ReadI2C(REG_Address); 8 L=Single_ReadI2C(REG_Address+1); 9 return (H<<8)+L; //合成數據 10 }
最後要說明下:關於MPU6050內部的「小抽屜」的地址和功能須要閱讀其官方的MPU6050寄存器手冊。(注意是寄存器手冊!)
9 硬件工程總體介紹
9.一、打開Keil uVision2,點擊Project下的Open Project,打開記步手環.Uv2加載工程。
圖 9_1 打開工程
9.二、待工程加載完畢,你們會在工程窗口中看到圖9_2所示文件結構。其中FUNC組下面包含數i2c驅動、mpu6050和串口驅動文件, USER組下是最上層應用程序文件。
圖 9_2 文件結構
9.三、上一章已經把uart.c講解了,前幾節也把i2c.c和mpu6050,c介紹了。這裏直接從main.c對整個工程的流程進行分析:主函數中先初始化串口和MPU6050,接着進入無限循環。循環中每隔必定的時間發送一幀的數據——該幀以‘#’開始以‘$’結束,中間依次是X軸加速度值、Y軸加速度值和Z軸加速度值。
1 //------------------------------------------------ 2 //主函數 3 //------------------------------------------------ 4 void main (void) 5 { 6 delay(500); //上電延時 7 InitUART(); //初始化串口 8 InitMPU6050(); //初始化MPU6050 9 10 while (1) //主循環 11 { 12 SendByte('#'); //起始標誌 13 SendData(GetData(0x3B)); //X軸加速度 14 SendData(GetData(0x3D)); //Y軸加速度 15 SendData(GetData(0x3F)); //Z軸加速度 16 SendByte('$'); //標誌 17 delay(20); 18 } 19 }
其中調用了串口驅動中的void InitUART(void)串口初始化函數、 void SendByte(unsigned char dat)串口發送一字節函數和 void SendStr(unsigned char *s)串口發送一個字符串函數,以及調用了mpu6050驅動中的void InitMPU6050()初始化函數和int GetData(uchar REG_Address)獲取6軸數據函數。
1 //外部函數 2 extern void InitUART(void); 3 extern void SendByte(unsigned char dat); 4 extern void SendStr(unsigned char *s); 5 extern void InitMPU6050(); 6 extern int GetData(uchar REG_Address);
這裏惟一要特別說明的函數是:void SendData(int value)函數。咱們知道直接調用MPU6050的函數int GetData(uchar REG_Address)返回的是int類型的數據,而串口每次只能發送一個8bit的數據,因而這裏的SendData則是負責將該int類型的數值轉換爲串口容易發送的數據再進行發送。
1 //----------------------------------------- 2 //整數轉字符串 3 //----------------------------------------- 4 void enCode(uchar *s,int temp_data) 5 { 6 if(temp_data<0) 7 { 8 temp_data=-temp_data; 9 *s='-'; 10 } 11 else *s=' '; 12 *++s =temp_data/10000+0x30; 13 temp_data=temp_data%10000; //取餘運算 14 *++s =temp_data/1000+0x30; 15 temp_data=temp_data%1000; //取餘運算 16 *++s =temp_data/100+0x30; 17 temp_data=temp_data%100; //取餘運算 18 *++s =temp_data/10+0x30; 19 temp_data=temp_data%10; //取餘運算 20 *++s =temp_data+0x30; 21 *++s ='\0'; //字符串結束標誌 22 } 23 24 //----------------------------------------- 25 //編碼+發送到串口 26 //----------------------------------------- 27 void SendData(int value) 28 { 29 enCode(temp, value); //轉換數據顯示 30 SendStr(temp); 31 }
上面的enCode函數是將輸入的int類型的數據轉換爲第一位爲符號(正用空格代替,負用負號代替),後5位爲數值的字符串,即便不足五位數前面也要填充0。這樣便不難理解SendData的功能:將value編碼並經過串口發送。
這樣整個工程的做用則是週期性讀取MPU6050三軸的加速度並用下面的幀格式經過藍牙發送出去:
10 客戶端軟件構成模塊
10.一、打開Eclipse點擊File菜單欄下的Import按鈕準備導入second_test工程(如圖10_1所示)。
圖 10_1 導入工程
10.二、接着在彈出的Select窗口中選擇Android文件夾下的Existing Android Code Into Workspace點擊next(如圖10_2所示)。
圖 10_2 選擇導入類型
10.三、接着在彈出的框中點擊右上角的Browse按鈕,找到要導入的third_test所在路徑,而且須要勾選Copy projects into workspace(如圖10_3所示)。
圖 10_3 選擇工程
10.四、最終效果如圖10_4所示在src文件夾下有四個包:其中第一個是和藍牙相關的類(從下到上依次爲藍牙設備搜索相關類、藍牙通訊鏈接相關類和藍牙通訊相關類);第二個是繪製折線圖表相關的類(這裏採用開源圖表繪製引擎achartengine,因此在libs裏要添加相應的包);第三個是數據池相關的類,用於實現藍牙數據實時高速處理;另外一個包是UI相關類,也是整個工程最核心的部分。若是讀者導入過程當中出現錯誤,也能夠採用第三章的方法新建一個工程,而後把src下的文件、layout下的文件和AndroidManifest.xml文件作相應的新建或修改,同時還要注意引入libs的包以及values裏的strings.xml。
圖 10_4 工程文件結構
11 軟件最終效果預覽
上面是從模塊構成的角度介紹工程的主要文件,爲了更好的方便分析其內部邏輯,筆者準備先帶領你們預覽下本次應用的最終效果(如圖11_1所示):
n 第一幅圖:是初始打開界面,若是本地藍牙沒有打開最左邊的按鈕將會顯示「打開藍牙設備」;
n 第二幅圖:是點擊「鏈接個人小手環」後進入藍牙搜索階段;
n 第三幅圖:是自動搜索到記步手環後進入的鏈接藍牙階段;
n 第四幅圖:是鏈接完成後,應用把從手環收集的實時數據(X\Y\Z軸加速度以及合加速度)繪製出;
n 第五幅圖:是經過滑動條調大記步閾值,並選擇CheckBox只顯示合加速度值的實時折線;
n 第六幅圖:是放大折線圖,並點擊某個點顯示具體信息圖。
其中前三個階段和上一章中的小風扇的控制很相似,都是點擊鏈接到進入搜索再到進行鏈接。只不過一個是鏈接後經過應用向硬件發送命令幀來控制小風扇轉速;一個是不斷從記步手環讀取實時的X\Y\Z三軸的加速度,計算合加速度同時記步,而且將數據實時以折線圖的形式展現出來。
圖 11_1 軟件最終效果預覽圖(從左到右從上到下編號1-6)
12 一個高效處理數據的數據池設計
當提到爲何須要高效處理的數據池時,其實要從藍牙搜索講起。因爲上一章的最後對藍牙搜索、鏈接、通訊的三個過程作了詳細的講解,本次則只從總體上進行梳理一下。
如圖12_1,當點擊鏈接小手環按鈕後則執行藍牙搜索類的doDiscovery()函數進行搜索藍牙設備,在其搜索過程當中搜索的設備名和設備地址分別存儲在BlueToothSearch的公有成員變量mNameVector和mAddrVector中,而後在本次搜索結束後會向Activity發送一個類型爲0x01的Handler消息,而該消息會被Activity中的handleMessage接收到。
當Activity中的handleMessage接收類型爲0x01的消息後,程序會遍歷本次藍牙搜索到的周邊設備的名稱找到符合咱們的手環的藍牙設備。而後調用藍牙鏈接的setDevice()函數獲取遠程藍牙通訊socket,接着在handleMessage內再觸發藍牙鏈接的線程進行藍牙鏈接。當藍牙鏈接完畢,則會發送0x02類型的消息反饋給Activity中的handleMessage。
一樣的當Activity中的handleMessage接收類型爲0x02的消息後,程序會調用藍牙通訊類的setSocket()函數來獲取標準輸入輸出流。此後,若是想從軟件向硬件發送消息則直接能夠調用藍牙通訊類的write()函數,而接收數據則是採用啓動一個接收線程來實現實時接收的。
圖 12_1 從點擊鏈接小手環到完成藍牙鏈接全過程流程圖
如今咱們的思惟已經跟着轉到了上圖中最後一個無限輪訓收數據階段。同時咱們知道從小手環發來的數據是比較高速的(硬件工程中寫的是每次發送完畢delay(20),應該算是比較短的時間了)。那麼問題就來了:若是咱們不能及時地將手環傳來的數據進行處理,頗有可能致使大量的數據滯留在緩衝區。這樣進一步會致使每次得到的數據都不是最新的數據,而表現出動態繪製折線圖滯後糟糕的效果。
綜上因爲下位機10ms發送一次20byte的數據,上位機一方面要作好接收工做,保證數據不擁擠在串口接收緩衝區;另外一方面也要實時獲取當前從串口讀到的最新數據。若是採用傳統多線程+鎖的機制是能夠的,可是當多線程中加入鎖勢必會影響程序執行效率,經過綜合分析該問題筆者最終抽象出一個特殊的數據模型——自動更新的環形棧(樓主自造的詞,見諒哈哈):
圖 12_2 自動更新的環形棧
如圖12_2所謂自動更新的環形棧本質上是一個基於環形數組的特殊數據結構。圖中環形表明數據池,也是一個環形數組(普通數組,採用必定技巧將首尾鏈接),p_write指示當前數據插入位置,每次插入一個數據p_write順時針移動一格,從而實現新數據覆蓋老數據的自動更新功能。而這裏最精妙的地方在於每次取數據的方式:從p_write所指的位置逆時針取40個數據(由於有效幀包含的數據長度爲20,一次取40保證至少有一個有效幀),而後從這40個數據中找出有效信息,賦值給公有成員X,Y,Z。這樣經過適當調節環的容量,保證取數據時該段數據不被覆蓋的前提下,又能根據p_write指示獲取最新的下位機發來的有效幀,將存和取有效地分離從而完美達到了咱們的需求。
具體在程序中UI_Main.java的onCreate函數中聲明並實例化一個大小爲20000的數據池mDataPool = new DataPool(20000)。接着在BlueToothCommunicate的輪詢接收數據的線程中(也即圖12_1的最後一環節的read中)對於每次新收到的數據調用mDataPool的push_back(buffer, bytes)函數將其存儲在數據池中。當每次須要取最新數據時只要先調用mDataPool的ask()函數,接着即可直接經過訪問DataPool的公有成員X\Y\Z獲取最新三軸加速度的值了。
1 // 利用線程一直收數據 2 public void run() { 3 byte[] buffer = new byte[1024]; 4 int bytes; 5 // 循環一直接收 6 while (state) { 7 try { 8 // bytes是返回讀取的字符數量,其中數據存在buffer中 9 bytes = mmInStream.read(buffer); 10 String readMessage = new String(buffer, 0, bytes); 11 Log.i("beautifulzzzz", "read: " + bytes + " mes: " 12 + readMessage); 13 UI_Main.mDataPool.push_back(buffer, bytes); 14 } catch (IOException e) { 15 break; 16 } 17 } 18 }
13 一個開源的折線圖繪製方案
在第10節客戶端軟件構成模塊中曾提到本項目中採用了開源圖表繪製引擎AChartEngine。它是一個安卓系統上製做圖表的框架,支持折線圖、面積圖、分區圖、對比圖、散點圖、柱狀圖、餅圖等(如圖13_1所示)。
圖13_1 AChartEngine繪製的圖標Demo
此外其全部支持的圖表類型,均可以包含多個系列,都支持水平(默認)或垂直方式展現圖表。而且支持許多其餘的自定義功能。全部圖表均可以創建爲一個view,也能夠創建爲一個用於啓動activity的intent(顯然上面前兩幅圖是採用view的形式,其餘幾個是採用intent啓動的)。
通常忽然提到某某開源包或者調用別的接口初學者可能會頭大,並且這裏更讓多數人頭痛的是筆者竟忽然亮出了這麼多炫酷的UI,豈不是更加難以使用!因而可能會有不少人準備本身DIY折線圖了(哈哈)。然而事實倒是這個開源的框架用起來十分方便:你們能夠把全部的chart都想象成由兩層組成,一部分是Renderer(如XYMultipleSeriesRenderer,用於對圖表樣框架樣式的說明),另外一部分是Dataset(如XYMultipleSeriesDataset,用於對視圖數值的處理)。因此在ChartLine.java類的開始就定義並聲明這兩種類型的私有成員:【第一步:數據層和顯示層定義並實例化】
1 private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset(); 2 private XYMultipleSeriesRenderer mRenderer = new XYMultipleSeriesRenderer();
由於mRenderer用於對圖表框架樣式的說明,因此在setChartSettings函數裏調用了多個其成員函數用來對圖表總體樣式屬性進行設置。例如七、8兩行是設置X軸和Y軸的標題,9到12行設置初始X軸和Y軸所表示的範圍,22到24行用來設置放大縮小的控件和屬性(就像地圖控件裏的放大縮小按鈕)。這樣下層的X軸、Y軸等就都設置好了。【第二步:設置顯示層顯示樣式】
1 public void setChartSettings(String xTitle, String yTitle, double xMin, 2 double xMax, double yMin, double yMax, int axesColor, 3 int labelsColor) { 4 // 有關對圖表的渲染可參看api文檔 5 mRenderer.setXTitle(xTitle);// 名字 6 mRenderer.setYTitle(yTitle); 7 mRenderer.setXAxisMin(xMin);// 最小最大值 8 mRenderer.setXAxisMax(xMax); 9 mRenderer.setYAxisMin(yMin); 10 mRenderer.setYAxisMax(yMax); 11 mRenderer.setAxesColor(axesColor);// 座標軸顏色 12 mRenderer.setLabelsColor(labelsColor);// 標號顏色 13 mRenderer.setShowGrid(true);// 顯示網格 14 mRenderer.setGridColor(Color.GRAY); 15 mRenderer.setXLabels(16); 16 mRenderer.setYLabels(20); 17 mRenderer.setYLabelsAlign(Align.RIGHT);// 設置標籤居Y軸的方向 18 mRenderer.setPointSize((float) 2); 19 mRenderer.setShowLegend(true);// 下面的標註 20 // mRenderer.setZoomButtonsVisible(true);// 放大縮小按鈕 21 mRenderer.setZoomEnabled(true, false);// 設置縮放,這邊是橫向能夠縮放,豎向不能縮放 22 mRenderer.setPanEnabled(true, false);// 設置滑動,這邊是橫向能夠滑動,豎向不可滑動 23 }
當表格框架設置好以後,接下來就是向框架內填充折線,而且在此過程當中把每一條折線的數據層放入總的數據層中。以下setLineSettings函數循環4次,每次首先實例化一個標題爲titles[i]的座標序列,而後將該序列放入總的數據層mDataset中。一樣的每次實例化一個XYSeriesRenderer(由於每一個折線也有本身的樣式),並將其加入總的圖標層mRenderer中。這樣就可以將4條分別表示X軸加速度、Y軸加速度、Z軸加速度和合加速度的折線圖設置好。【第三步:設置4個折線數據序列並加入數據層,設置4個折線層並加入顯示層】
public void setLineSettings() { for (int i = 0; i < titles.length; i++) { // create a new series of data mCurrentSeries[i] = new XYSeries(titles[i]); mDataset.addSeries(mCurrentSeries[i]); // create a new renderer for the new series renderer[i] = new XYSeriesRenderer(); mRenderer.addSeriesRenderer(renderer[i]); // set some renderer properties renderer[i].setPointStyle(styles[i]); renderer[i].setColor(colors[i]); renderer[i].setFillPoints(true);// 實心仍是空心 renderer[i].setDisplayChartValues(false);// 不顯示值 renderer[i].setDisplayChartValuesDistance(10); } }
此時mDataset裏存放着當前要顯示的折線的全部XYSeries,每一個折線XY序列存放在mCurrentSeries[i]中,若是想在該折線圖上增長一個數據只要調用mCurrentSeries[i].add(x, y)便可;若是想顯示或隱藏某個折線圖只要調用圖表層的mRenderer和數據層mDataset移出對應的折線和折線序列便可。【提早一步(5):如何往對應的折線中增長數據,以及如何顯示隱藏某條折線】
1 // 顯示第i個折線圖 2 public void showLine(int i) { 3 mDataset.addSeries(mCurrentSeries[i]); 4 mRenderer.addSeriesRenderer(renderer[i]); 5 } 6 7 // 隱藏第i個折線圖 8 public void hideLine(int i) { 9 mDataset.removeSeries(mCurrentSeries[i]); 10 mRenderer.removeSeriesRenderer(renderer[i]); 11 } 12 13 // 向第i個折線圖中添加(x,y)數據 14 public void addData(int i, double x, double y) { 15 mCurrentSeries[i].add(x, y); 16 }
上面說過全部圖表均可以創建爲一個view,也能夠創建爲一個用於啓動activity的intent。這裏因爲咱們須要在ui_main.xml中添加其餘控件,因此採用view的方式新建圖表。以下setChartViewSetting函數負責當圖表沒有創建時分別實例化layout和mChartView,並將新建的mChartView加入ui_main.xml中圖表所在的layout中,這樣咱們就能夠看到基本的圖表了。此外,第10行是給圖表加的點擊監聽,用於顯示點擊點的詳細信息(圖11_1軟件最終效果的第6張圖)。【第四步:將數據層和顯示層合成爲圖表加入UI中】
1 public void setChartViewSetting(final Activity activity) { 2 if (mChartView == null) { 3 LinearLayout layout = (LinearLayout) activity 4 .findViewById(R.id.chart); 5 mChartView = ChartFactory.getLineChartView(activity, mDataset, 6 mRenderer); 7 // enable the chart click events 8 mRenderer.setClickEnabled(true); 9 mRenderer.setSelectableBuffer(10); 10 mChartView.setOnClickListener(new View.OnClickListener() { 11 public void onClick(View v) { 12 ……(略) 13 } 14 }); 15 layout.addView(mChartView, new LayoutParams( 16 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 17 } else { 18 mChartView.repaint(); 19 } 20 }
14 總體邏輯梳理
其實仔細觀察的讀者會發現:本次的UI_Main.java和上次的大體相同。前一階段都是點擊按鈕來鏈接遠程藍牙設備。而不一樣之處在於上一章是經過加減按鈕向小風扇發送速度控制命令來控制速度,這一章是不斷讀取手環的實時數據並用折線圖繪製出來。總體業務邏輯仍是在控件的點擊事件和handleMessage之間有序進行,下面將着重說明數據的實時顯示及一些用於優化操做的細節。
在onCreate中首先實例化藍牙三劍客,接着實例化數據池和折線圖表,而後調用折線圖類的成員函數對摺線圖作前期設置,最後啓動ChartThread線程。
1 // 實例化藍牙三劍客(搜索、鏈接、通訊) 2 // myHandler是用來反饋信息的 3 mBlueToothSearch = new BlueToothSearch(this, myHandler); 4 mBlueToothConnect = new BlueToothConnect(myHandler); 5 mBlueToothCommunicate = new BlueToothCommunicate(myHandler); 6 7 mDataPool = new DataPool(20000); 8 9 mChartLine = new ChartLine(); 10 // 設置圖標顯示的基本屬性 11 mChartLine.setChartSettings("Time", "", 0, 100, -20000, 20000, 12 Color.WHITE, Color.WHITE); 13 // 設置四個折線圖的屬性 14 mChartLine.setLineSettings(); 15 16 ChartThread.start();// 啓動圖標更新線程
在此以後即是對鏈接手環按鈕作的相關設置,這裏和上一章中的鏈接風扇幾乎同樣,關鍵在於理解藍牙三劍客經過線程啓動並經過handler將消息反饋的機制。
圖 14_1 點擊鏈接設備到通訊創建
這樣當點擊鏈接手環的按鈕以後,而後在handler的溝通下上位機和下位機最終實現可通訊。此時下位機一旦有數據傳送上來,上位機便快速的將其放入數據池內。那麼程序是在成麼時候取數據並更新UI的呢?祕密就在於ChartThread.start()!
1 private Thread ChartThread = new Thread() { 2 public void run() { 3 while (true) { 4 try { 5 sleep(100); 6 // 週期性發送更新Chart的消息(由於UI不能放在這個裏面更新) 7 Message msg = new Message(); 8 msg.what = 0x04; 9 myHandler.sendMessage(msg); 10 } catch (InterruptedException e) { 11 } 12 } 13 } 14 };
從上面的能夠看出ChartThread主要負責週期性發送類別爲0x04的消息,而在handleMessage的case 0x04中則是負責獲取實時數據並更新UI的。之因此這樣繞個彎是由於UI更新一旦放在ChartThread中就會致使程序運行異常。這裏的數據獲取和更新也比較容易理解:首先調用數據池的ask函數從p_write向後找40個數據尋找並解析有效幀,若是成功則最新的X\Y\Z三軸的加速度已經保存在mDataPool的公有成員X\Y\Z中。下面第3行是計算合加速度(減去16000是爲了方便顯示),接着6到9行負責分別將三軸加速度及其合速度值加入折線圖。第10到13行即是咱們簡單的記步算法了,即當合加速值超過設定的記步閾值時記步數加一。第1五、16行是控制折線圖滾動到最新的位置並刷新ChartView。
1 case 0x04: 2 if (mDataPool.ask() == true) { 3 int all = (int) Math.sqrt(mDataPool.X * mDataPool.X 4 + mDataPool.Y * mDataPool.Y + mDataPool.Z 5 * mDataPool.Z) - 16000; 6 mChartLine.addData(0, mTime, mDataPool.X); 7 mChartLine.addData(1, mTime, mDataPool.Y); 8 mChartLine.addData(2, mTime, mDataPool.Z); 9 mChartLine.addData(3, mTime, all); 10 if (all > mUpperLimit) {// 記步-和加速度超過設定上限則記步 11 mNum++; 12 mTextView2.setText("當前記步數爲: " + mNum); 13 } 14 mTime += 1; 15 mChartLine.letChartMove(mTime);// 控制圖形滾動 16 mChartLine.mChartView.repaint(); 17 } 18 break;
綜上,當創建藍牙通訊後,整個應用程序中主要有三個線程:①用於不斷讀取串口數據並將其存入數據池的數據線程;②用於週期性發送0x04消息的信號線程;③隱蔽而重要的主線程(UI更新等操做)。如圖14_2所示:一方面數據線程不斷讀取數據存入數據池,另外一方面信號線程週期性發送0x04消息觸發handleMessage的case 0x04執行ask讀數據函數,當成功解析到有效數據時會在主線程中記步並更新UI。
圖 14_2 三線程運做
此外,還有一些其餘的控件用於提升交互性,如表14_1所示:開始/中止按鈕用於控制折線圖是否動態滾動,當中止折線圖動態滾動時折線圖的數據增長並未被停止,此時能夠方便用戶拖動折線圖查看歷史或觀察細節。四個CheckBox用於控制顯示哪個折線圖,這樣便於單獨分析。滾動條是用來動態設置記步閾值的,這樣便於你們深刻理解咱們的簡單的記步算法。
表 14_1 其餘用於優化交互的控件
15 最終成果檢查
若是說遙控小風扇是硬件上要費工夫的一個測試,那麼本章的記步手環無疑須要在軟件上費很大工夫的一個測試。這裏所說的軟件不只包括引進一個開源圖表繪製框架、延續了上一章中的藍牙通訊三劍客、嘗試了稍微繁瑣的佈局和多控件UI,還包括多線程中狀態轉換控制和爲高速實時而設計的數據池數據結構。若是再廣一點,還有I2C通訊協議的驅動設計和數據幀的設計等。
你們也不要被這幾篇測試的難度嚇倒,由於無數的事實證實備戰的強度要比真正的戰鬥要艱難的多(哈哈,不然怎麼會有勢如破竹的戰績呢?)。下面讓咱們看看本身本次備戰的成績如何——
△ 知道一個簡單記步手環的構成模塊(+ 10分)
△ 懂得如何利用加速計的數據實現簡單的記步和睡眠質量檢查(+ 10分)
△ 掌握MPU6050的功能並可以讀懂MPU6050輸出數據的意義(+ 10分)
△ 能根據講解讀懂I2C驅動代碼的意義(+ 20分)
△ 理解MPU6050的驅動設計(+ 20分)
△ 成功設計出記步手環硬件部分(+ 40分)
△ 回顧藍牙三劍客(+ 5分)
△ 理解數據池數據結構的由來緣由和設計思想(+ 20分)
△ 知道利用開源資源豐富本身的項目(+ 5分)
△ 讀懂利用AChartEngine設計的折線圖繪製類(+ 20分)
△ 腦殼裏走通了整個客戶端軟件的狀態轉換圖(+ 30分)
△ 加入睡眠質量檢測功能(+20分)
△ 加入存儲器實現記步手環離線功能(+ 30分)……
——及格線70分,良好線150分,優秀200分。本身還滿意吧?
[搜索:beautifulzzzz(看樓主博客園官方博客,享高質量生活)嘻嘻!!!]
[若是您也喜歡智能硬件的東西,能夠交個朋友~]
[若是您胸懷大志,能團結各路豪傑的亂世英雄,也能夠留下一下信息~]
若是您以爲不錯,別忘點個贊讓更多的小夥伴看到\(^o^)/~