首先恭喜大家挺過了測試二!為什么說“挺”呢,?因?yàn)闇y試二的難度和測試一相比有一個(gè)比較大的跳躍:首先測試一僅僅利用現(xiàn)有硬件模塊稍加改造而DIY一個(gè)藍(lán)牙防丟器,,而測試二則要求大家具有從腦袋里的一個(gè)想法到一個(gè)全新的小設(shè)備的實(shí)現(xiàn)的全部能力,顯然該過程不是連幾根線那么簡單,;其次測試一對藍(lán)牙的使用僅限于信號搜索層面,,而測試二一下子深入到可靠通信的層面了,其難度可想而知,;最后在測試二中客戶端的設(shè)計(jì)中復(fù)雜的狀態(tài)轉(zhuǎn)換過程,,以及嵌入式編程時(shí)需要對所使用的硬件作細(xì)致的分析,都構(gòu)成了對前期基礎(chǔ)沒打牢的同學(xué)一種挑戰(zhàn),。不過好消息是大家挺過來了,,接下來的模式大致相同。 現(xiàn)在,,讓我們回到本次的正題,!可能細(xì)心的讀者會發(fā)現(xiàn)上次的遙控小風(fēng)扇雖然利用了藍(lán)牙的通信功能,但是僅限于客戶端向硬件發(fā)送命令(在上次并未使用更嚴(yán)謹(jǐn)?shù)膽?yīng)答模式,,即客戶端發(fā)送控制命令->硬件響應(yīng)命令->硬件返回響應(yīng)完畢命令),。為了彌補(bǔ)上次的缺陷,那么本次會將重點(diǎn)放在數(shù)據(jù)收集方面,樓主想來想去覺得還是一個(gè)簡單的記步手環(huán)最適合了,。[正版請搜索:beautifulzzzz(看樓主博客園官方博客,,享高質(zhì)量生活)]
1 智能手環(huán)簡介 智能手環(huán)是一種穿戴式智能設(shè)備。通過該設(shè)備,,用戶可以記錄日常生活中的鍛煉、睡眠等實(shí)時(shí)數(shù)據(jù),,并將這些數(shù)據(jù)與手機(jī),、平板同步,起到通過數(shù)據(jù)指導(dǎo)健康生活的作用,。另外,,智能手環(huán)還具有社交功能,能夠?qū)㈠憻捛闆r和睡眠質(zhì)量發(fā)送到社交網(wǎng)絡(luò)進(jìn)行分享,。 圖 1_1某款智能手環(huán) 一個(gè)智能手環(huán)最小系統(tǒng)一般包括:可充電的電源模塊,、控制模塊(圖1_2中左邊芯片)、藍(lán)牙模塊(右邊芯片),、存儲模塊和加速計(jì)模塊(上面芯片),。其中加速計(jì)是為了獲得佩戴者在運(yùn)動(dòng)或睡眠過程中的加速度數(shù)據(jù),通過分析這些數(shù)據(jù)則能夠判斷佩戴者的運(yùn)動(dòng)情況和睡眠質(zhì)量,;存儲模塊主要負(fù)責(zé)將實(shí)時(shí)數(shù)據(jù)暫存,,接著在適當(dāng)?shù)臅r(shí)刻借助藍(lán)牙模塊將數(shù)據(jù)同步到手機(jī)端。方便起見本次要自制的記步手環(huán)將不采用存儲器暫存,,而是將數(shù)據(jù)實(shí)時(shí)地傳送到手機(jī)端,。同時(shí)為了便于大家對記步算法的理解,客戶端將采用一個(gè)折線圖的形式實(shí)時(shí)展示記步手環(huán)收集的數(shù)據(jù),。 圖 1_2某款智能手環(huán)核心電路板
2 如何實(shí)現(xiàn)記步 看了上面的分析大家可能會疑惑——僅僅用一個(gè)加速計(jì)怎么能實(shí)現(xiàn)記步和睡眠質(zhì)量檢測呢,?其實(shí)確實(shí)可以!因?yàn)榧铀儆?jì)可以實(shí)時(shí)獲取自身的X\Y\Z三個(gè)軸向的加速度,。當(dāng)其靜止時(shí)合加速度會在重力加速度附近波動(dòng),;當(dāng)佩戴者處于深度睡眠過程中時(shí),其合加速度將呈現(xiàn)出長時(shí)間的穩(wěn)定于重力加速度附近,;當(dāng)其隨著運(yùn)動(dòng)的佩戴者手臂而做周期性擺動(dòng)時(shí),,其數(shù)據(jù)也是有一定規(guī)律可循的。這樣,,設(shè)計(jì)時(shí)只要通過分析從加速計(jì)獲的數(shù)據(jù)就能實(shí)現(xiàn)對運(yùn)動(dòng)或睡眠質(zhì)量的記錄,。
3 預(yù)期效果構(gòu)思 上面已經(jīng)提到:為了方便,我們并未采用存儲器實(shí)現(xiàn)記步手環(huán)的離線記錄,,而是實(shí)時(shí)地將數(shù)據(jù)發(fā)送到客戶端由一個(gè)可視化的折線圖動(dòng)態(tài)繪制結(jié)果,。如圖3_1所示系統(tǒng)中記步手環(huán)部分包含單片機(jī)模塊、藍(lán)牙模塊,、加速計(jì)模塊和電源模塊,,這樣通過單片機(jī)的協(xié)調(diào)可以實(shí)現(xiàn)將加速計(jì)模塊的數(shù)據(jù)通過藍(lán)牙實(shí)時(shí)地傳送給客戶端程序,。在客戶端部分則負(fù)責(zé)將收集到的實(shí)時(shí)數(shù)據(jù)以折線圖的形式動(dòng)態(tài)地展示出來,此外客戶端中也加入一個(gè)滑動(dòng)條來控制記步閾值來真正讓大家明白其設(shè)計(jì)思想(真正商業(yè)化的智能手環(huán)多數(shù)采用的是先將有效數(shù)據(jù)保存在手環(huán)的小型存儲器中,,上位機(jī)周期性地將數(shù)據(jù)收集并同步到服務(wù)器端),。 圖 3_1 預(yù)期效果圖
4 硬件整體設(shè)計(jì) 如圖4_1,相比于上一個(gè)無線小風(fēng)扇該硬件構(gòu)成反而比較簡單:藍(lán)牙模塊依然采用我們比較熟悉的HC-06模塊,對于加速度的測量采用四周飛行器上常采用的MPU6050模塊,。該模塊不僅含有加速計(jì)的功能,,還具有陀螺儀的功能,其在汽車防側(cè)翻,、相機(jī)云臺穩(wěn)定,、機(jī)器人平衡、空中鼠標(biāo),、姿態(tài)識別等眾多領(lǐng)域都有應(yīng)用,,這里我們只是利用了它的加速計(jì)功能。此外要注意:圖4_1所示的單片機(jī)模塊的電源引腳被隱藏了,,在真正設(shè)計(jì)連接時(shí)一定不要忽略這兩個(gè)引腳,! 圖 4_1 硬件電路圖
5 MPU6050介紹 MPU-60X0是全球首例9軸運(yùn)動(dòng)處理器。它集成了3軸MEMS陀螺儀,,3軸MEMS加速計(jì),,以及1個(gè)可擴(kuò)展的數(shù)字運(yùn)動(dòng)處理器DMP(Digital Motion Processor)。如圖5_1所示軸向是相對于加速計(jì)說的,,當(dāng)芯片水平靜止放置時(shí)x軸和y軸的加速度分量幾乎為0,,z軸的加速度分量約為當(dāng)?shù)氐闹亓铀俣龋欢D(zhuǎn)極性則是對陀螺儀來說的,,本次先不介紹,。 圖 5_1 MPU-60X0軸向和旋轉(zhuǎn)的極性(來自MPU6050數(shù)據(jù)手冊) 為何上面說9軸信號呢?因?yàn)镸PU-60X0可用I2C接口連接一個(gè)第三方的數(shù)字傳感器,,比如磁力計(jì),。擴(kuò)展之后就可以通過其I2C或SPI接口輸出一個(gè)9軸的信號。也可以通過其I2C接口連接非慣性的數(shù)字傳感器,,比如壓力傳感器,。(為什么特別提磁力計(jì)和壓力傳感器呢?因?yàn)樵陲w控方面,,利用陀螺儀和加速計(jì)可以計(jì)算飛行器的傾角,,從而調(diào)節(jié)飛行器平衡。但是只是調(diào)節(jié)平衡對方向沒有概念也不能執(zhí)行復(fù)雜任務(wù),,因此需要配備磁力計(jì)(也即電子羅盤傳感器),。此外,由于飛行器在不同高度作業(yè)時(shí),其周圍的重力加速度也不同,,這樣會影響傾角的準(zhǔn)確性,,因此通過氣壓計(jì)計(jì)算所處高度然后計(jì)算實(shí)時(shí)加速度達(dá)到精確控制的效果。) 圖 5_2 MPU-60X0典型工作電路(來自MPU6050數(shù)據(jù)手冊) MPU-60X0對陀螺儀和加速計(jì)分別用了三個(gè)16位的ADC,,將其測量的模擬量轉(zhuǎn)化為可輸出的數(shù)字量,。為了精確跟蹤快速和慢速運(yùn)動(dòng),傳感器的測量范圍是可控的,,陀螺儀可測范圍為±250,,±500,±1000,,±2000°/秒(dps),加速計(jì)可測范圍為±2,,±4,,±8,±16g(重力加速度),。如圖5_3是直接從16位ADC中讀出的6軸的數(shù)據(jù)(從左到右依次為加速計(jì)X軸數(shù)據(jù),、Y軸數(shù)據(jù)、Z軸數(shù)據(jù),、陀螺儀X極數(shù)據(jù),、Y極數(shù)據(jù)、Z極數(shù)據(jù)): 圖 5_3 MPU6050輸出加速計(jì)和陀螺儀6軸的原始數(shù)據(jù) 但是這里的輸出值并不是真正的加速度和角速度的值,,上面說過,,MPU是一個(gè)16位AD量程可程控的設(shè)備,這里設(shè)置的加速度傳感器的測量量程為正負(fù)2g(這里的g為重力加速度),,陀螺儀的量程為正負(fù)2000°/s,。所以要用下面的公式進(jìn)行轉(zhuǎn)化: 圖5_4 實(shí)際值計(jì)算公式 最后給大家推薦一款比較容易買到的MPU6050,如圖5_5該模塊將核心芯片和外圍電路集成到一個(gè)模塊上并留出八個(gè)引腳,,本次使用只需用到上面四個(gè)即可(具體連接參考圖4_1),。 圖5_5 MPU6050模塊
6 一個(gè)簡單的記步算法設(shè)計(jì) 第二小節(jié)講到當(dāng)MPU6050隨著運(yùn)動(dòng)的佩戴者手臂而做周期性擺動(dòng)時(shí),其數(shù)據(jù)也是有一定規(guī)律可循的,。簡單起見我們只分析合加速度:一個(gè)擺臂周期其合加速度會在重力加速度上下波動(dòng),,如圖6_1只要選取合適的閾值(黑線代表閾值),每次檢測出合加速度大于該閾值則認(rèn)為是一次擺臂,,從而可以實(shí)現(xiàn)記步的功能,。這里要特別說明下:如果想把你的手環(huán)推向市場,就要通過大量分析擺臂數(shù)據(jù)建立一套更好的記步算法,,如果偷懶只用樓主的簡單算法,,小心產(chǎn)品推出后被用戶的口水淹死(哈哈)! 圖 6_1 擺臂時(shí)合加速度變化圖
7 I2C總線介紹 上次我們在使用藍(lán)牙串口模塊時(shí)使用過串口通信,由于51系列單片機(jī)將串口通信很多細(xì)節(jié)都封裝到芯片內(nèi)部,,所以我們即使設(shè)計(jì)了串口驅(qū)動(dòng)模塊,,也并沒有真正了解串口通信的核心思想。其實(shí)串口協(xié)議的出現(xiàn)是為了構(gòu)成一個(gè)總線線路,,這樣單片機(jī)只要使用比較少的引腳就能和比較多的設(shè)備進(jìn)行通信了,,這里要用到的I2C總線也具有相同的效果但又有些不同。 圖 7_1I2C總線掛接多個(gè)設(shè)備圖 I2C(Inter-Integrated Circuit)總線是由PHILIPS公司開發(fā)的兩線式串行總線,,用于連接微控制器及其外圍設(shè)備,。是微電子通信控制領(lǐng)域廣泛采用的一種總線標(biāo)準(zhǔn)。它是同步通信的一種特殊形式,,具有接口線少,,控制方式簡單,器件封裝形式小,,通信速率較高等優(yōu)點(diǎn),。如圖7_1采用I2C總線后CPU只要使用2個(gè)引腳便可和多個(gè)設(shè)備進(jìn)行通信(其實(shí)每個(gè)采用I2C通信方式的設(shè)備都具有唯一的地址碼,這樣在總線中便能夠被唯一識別),,從而大大減少了引腳的使用,。 在I2C總線中使用的兩線為時(shí)鐘線SCL和數(shù)據(jù)線SDA。所有的I2C主從設(shè)備都是只被這兩根線連接起來的,。每一個(gè)設(shè)備既可以作為發(fā)送方,,也可以作為接收方,或者既可以作為發(fā)送發(fā)也可以作為接收方,。在總線中的主設(shè)備一般起產(chǎn)生時(shí)鐘信號和初始化通信的作用,,從設(shè)備則負(fù)責(zé)響應(yīng)主設(shè)備發(fā)出的命令。為了在總線上區(qū)分每一個(gè)設(shè)備,,每一個(gè)從設(shè)備必須有一個(gè)唯一的地址,。主設(shè)備一般不需要地址(一般為微處理器),因?yàn)閺脑O(shè)備不能發(fā)送命令給主設(shè)備,。 圖 7_2 I2C總線中主從設(shè)備 這里要先介紹I2C總線中幾個(gè)專有名詞: l 發(fā)送者:將數(shù)據(jù)發(fā)送到總線的設(shè)備 上面是從宏觀上對I2C總線介紹了下,,接下來將深入細(xì)節(jié)研究其通信過程: n 串行數(shù)據(jù)傳送: 在總線備用時(shí)SDA和SCL都必須保持高電平狀態(tài),,只有關(guān)閉I2C總線時(shí)才能使SCL鉗位在低電平。在I2C總線數(shù)據(jù)傳輸時(shí),,在時(shí)鐘線高電平期間,,數(shù)據(jù)線上必須保持有穩(wěn)定的邏輯電平(也就是說在數(shù)據(jù)傳輸期間只有時(shí)鐘線低電平期間,,才允許數(shù)據(jù)線上的電平發(fā)生變化)。 圖 7_3 串行數(shù)據(jù)發(fā)送 因此在如圖7_3中對于每一個(gè)時(shí)鐘脈沖期間一比特的數(shù)據(jù)將會被傳送,,SDA只能在時(shí)鐘信號為低電平時(shí)才能改變,。下面是代碼中發(fā)送一字節(jié)的函數(shù):在循環(huán)體內(nèi)每次將dat內(nèi)的最高位移出到CY中,進(jìn)而賦值給SDA(這時(shí)SCL為低,,SDA可改變),。接著拉高SCL并保持5us,最后再拉低SCL實(shí)現(xiàn)一個(gè)時(shí)鐘脈沖將dat中最高位送出,。依此循環(huán)8次實(shí)現(xiàn)將dat全部傳出,。 1 //------------------------------------------------ 2 //向I2C總線發(fā)送一個(gè)字節(jié)數(shù)據(jù) 3 //------------------------------------------------ 4 void I2C_SendByte(uchar dat) 5 { 6 uchar i; 7 for (i=0; i<8; i++) //8位計(jì)數(shù)器 8 { 9 dat <<= 1; //移出數(shù)據(jù)的最高位 10 SDA = CY; //送數(shù)據(jù)口 11 SCL = 1; //拉高時(shí)鐘線 12 Delay5us(); //延時(shí) 13 SCL = 0; //拉低時(shí)鐘線 14 Delay5us(); //延時(shí) 15 } 16 I2C_RecvACK(); 17 } n 開始和結(jié)束條件: 命令不會沒有任何預(yù)兆直接發(fā)送的,每一個(gè)I2C命令的發(fā)送總是開始于開始條件并結(jié)束于終止條件,。這里所謂的開始條件和終止條件起始也是由SCL和SDA組合形成的(如圖7_4),。 圖 7_4 開始和結(jié)束條件 如果時(shí)鐘線保持高電平期間,數(shù)據(jù)線出現(xiàn)由高到低的電平變化,,則會啟動(dòng)I2C總線,,此時(shí)為I2C的起始信號: 1 //------------------------------------------------ 2 //I2C起始信號 3 //------------------------------------------------ 4 void I2C_Start() 5 { 6 SDA = 1; //拉高數(shù)據(jù)線 7 SCL = 1; //拉高時(shí)鐘線 8 Delay5us(); //延時(shí) 9 SDA = 0; //產(chǎn)生下降沿 10 Delay5us(); //延時(shí) 11 SCL = 0; //拉低時(shí)鐘線 12 } 若在時(shí)鐘線保持高電平期間,數(shù)據(jù)線出現(xiàn)由低到高的電平變化,,則會停止I2C總線的數(shù)據(jù)傳輸,此時(shí)為I2C的終止信號: 1 //------------------------------------------------ 2 //I2C停止信號 3 //------------------------------------------------ 4 void I2C_Stop() 5 { 6 SDA = 0; //拉低數(shù)據(jù)線 7 SCL = 1; //拉高時(shí)鐘線 8 Delay5us(); //延時(shí) 9 SDA = 1; //產(chǎn)生上升沿 10 Delay5us(); //延時(shí) 11 } 開始條件之后I2C總線被認(rèn)為是忙狀態(tài),,只有當(dāng)停止信號之后其他主設(shè)備才能使用該總線,。此外,當(dāng)開始條件之后主設(shè)備能夠多次發(fā)出開始信號,。這些開始信號和第一次發(fā)出的開始信號類似,,他們后面經(jīng)常會跟從設(shè)備的地址。這樣可以方便實(shí)現(xiàn)在I2C總線忙期間,,當(dāng)前占線的主設(shè)備可以和不同的從設(shè)備進(jìn)行通信,。 n I2C數(shù)據(jù)傳送: I2C總線上傳送的每一個(gè)字節(jié)均為8位,但是每啟動(dòng)一次I2C總線,,其后的數(shù)據(jù)傳送字節(jié)數(shù)是沒有限制的,。同時(shí)每傳送一字節(jié)的數(shù)據(jù)后面都要跟隨一個(gè)接收者回應(yīng)的應(yīng)答位(低電平為應(yīng)答信號,高電平為非應(yīng)答信號),,當(dāng)全部數(shù)據(jù)發(fā)送完畢后主設(shè)備發(fā)送終止信號,。 圖 7_5 數(shù)據(jù)傳送圖 所以在上面向I2C總線發(fā)送一字節(jié)的數(shù)據(jù)的代碼的最后有一個(gè)I2C_RecvACK()函數(shù)。(如下)該函數(shù)負(fù)責(zé)接收接收者發(fā)送過來的應(yīng)答信號,,也即圖7_5中的第9個(gè)時(shí)鐘脈沖的期間的相應(yīng)操作,。 1 //------------------------------------------------ 2 //I2C接收應(yīng)答信號 3 //------------------------------------------------ 4 bit I2C_RecvACK() 5 { 6 SCL = 1; //拉高時(shí)鐘線 7 Delay5us(); //延時(shí) 8 CY = SDA; //讀應(yīng)答信號 9 SCL = 0; //拉低時(shí)鐘線 10 Delay5us(); //延時(shí) 11 return CY; 12 } 要特別說明下:所有的數(shù)據(jù)位包括應(yīng)答位都需要主設(shè)備產(chǎn)生時(shí)鐘脈沖。如果從設(shè)備沒有應(yīng)答意味著將沒有更多的數(shù)據(jù)要傳送或者設(shè)備沒有準(zhǔn)備好傳送,。這時(shí),,主設(shè)備要么產(chǎn)生停止信號,,要么重新發(fā)出開始條件。 圖 7_6 應(yīng)答信號 n I2C的7-bit地址: 上面說過每一個(gè)從設(shè)備都應(yīng)該具有唯一的地址,,這樣主設(shè)備才能準(zhǔn)確的尋址到每一個(gè)設(shè)備,,而這些地址被統(tǒng)一規(guī)定為7比特。但是上面講過I2C總線傳輸數(shù)據(jù)都是8比特傳送,,地址7比特豈不是少一位,!其實(shí)緊跟地址還有一位用來表示是讀操作還是寫操作的標(biāo)志位。如果該位為0表示主設(shè)備將要向從設(shè)備寫數(shù)據(jù),,否則表示主設(shè)備將要從從設(shè)備讀數(shù)據(jù),。在這8比特被發(fā)送后主設(shè)備能夠持續(xù)地進(jìn)行讀或者寫。如果主設(shè)備想和其他從設(shè)備進(jìn)行通信,,只要再次發(fā)送一個(gè)新的開始信號就可以而不必發(fā)送終止信號,。 圖 7_7 一個(gè)完整的數(shù)據(jù)讀寫操作
8 MPU6050驅(qū)動(dòng)設(shè)計(jì) 至此,我們基本上已經(jīng)將I2C的知識學(xué)完了,,下面將結(jié)合MPU6050的驅(qū)動(dòng)進(jìn)一步講解其原理(該部分的代碼參見工程的mpu6050.c部分),。我們首先來看一下它的頭文件mpu6050.h:從第6到25行上來就是一大串內(nèi)部地址的定義,對于初學(xué)者可能一頭霧水,!如果樓主再引入寄存器等數(shù)字電路的知識可能又要說幾頁了,,于是這里準(zhǔn)備只用一個(gè)簡單的例子闡述下這些地址的作用。 1 #include"i2c.h" 2 3 //----------------------------------------- 4 // 定義MPU6050內(nèi)部地址 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 //加速計(jì)自檢,、測量范圍及高通濾波頻率,典型值: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地址寄存器(默認(rèn)數(shù)值0x68,,只讀) 26 #define SlaveAddress 0xD0 //IIC寫入時(shí)的地址字節(jié)數(shù)據(jù),,+1為讀取 27 28 //----------------------------------------- 29 // 通過I2C和MPU6050通信的函數(shù) 30 //----------------------------------------- 31 void Single_WriteI2C(uchar REG_Address,uchar REG_data);//向I2C設(shè)備寫入一個(gè)字節(jié)數(shù)據(jù) 32 uchar Single_ReadI2C(uchar REG_Address); //從I2C設(shè)備讀取一個(gè)字節(jié)數(shù)據(jù) 33 void InitMPU6050(); //初始化MPU6050 34 int GetData(uchar REG_Address); //合成數(shù)據(jù)
上面講到在I2C總線中主設(shè)備可以通過固定的7-bit地址尋找到相應(yīng)的從設(shè)備(這里的7-bit地址為第26行的SlaveAddress,想必大家也能夠理解后面注釋的意義了吧~不加1表示緊跟著地址的一位為0,,表示向該設(shè)備寫數(shù)據(jù),;加1則表示緊跟著的一位為1,表示主設(shè)備從從設(shè)備讀數(shù)據(jù)),。雖然采用這種方式能夠準(zhǔn)確找到從設(shè)備,,但是從設(shè)備里面又有比較多的寄存器。這就好比你知道了某個(gè)要找的東西在具體的某個(gè)大柜子里,,但是來到大柜子前又發(fā)現(xiàn)有許多小抽屜,。這里的7-bit地址就好像指明了哪個(gè)柜子,,而從第6到25行的內(nèi)部地址就像柜子上的抽屜編號,而不一樣之處是位于mpu6050內(nèi)的“小抽屜”一部分存放著其采集的實(shí)時(shí)數(shù)據(jù),,另一部分等著外部放一些數(shù)據(jù)來設(shè)置其采樣屬性,。 這樣,如上面的第6行的SMPLRT_DIV(0x19)是用來設(shè)置陀螺儀采樣率的寄存器地址,,只要向該地址所指的寄存器寫入相應(yīng)的值則可以設(shè)置陀螺儀采樣率,。因此下面MPU6050初始化函數(shù)就是調(diào)用封裝的I2C寫函數(shù)向相應(yīng)的小抽屜內(nèi)寫屬性數(shù)據(jù),設(shè)置MPU6050采樣屬性,。 1 //------------------------------------------------ 2 //初始化MPU6050 3 //------------------------------------------------ 4 void InitMPU6050() 5 { 6 Single_WriteI2C(PWR_MGMT_1, 0x00); //解除休眠狀態(tài) 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極的數(shù)值,因?yàn)椴捎?6位ADC所以這里需要用兩個(gè)寄存器,。所以下面合成數(shù)據(jù)函數(shù)負(fù)責(zé)連續(xù)讀取REG_Address開始的兩字節(jié)數(shù)據(jù)組成一個(gè)16位數(shù)據(jù),。當(dāng)函數(shù)的參數(shù)為ACCEL_XOUT_H時(shí),則獲取的是實(shí)時(shí)的陀螺儀X極的數(shù)值,,同樣地可以獲得實(shí)時(shí)的6軸數(shù)據(jù),。 1 //------------------------------------------------ 2 //合成數(shù)據(jù) 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; //合成數(shù)據(jù) 10 } 最后要說明下:關(guān)于MPU6050內(nèi)部的“小抽屜”的地址和功能需要閱讀其官方的MPU6050寄存器手冊。(注意是寄存器手冊?。?/p>
9 硬件工程整體介紹 9.1,、打開Keil uVision2,點(diǎn)擊Project下的Open Project,,打開記步手環(huán).Uv2加載工程,。 圖 9_1 打開工程 9.2、待工程加載完畢,,大家會在工程窗口中看到圖9_2所示文件結(jié)構(gòu),。其中FUNC組下面包含數(shù)i2c驅(qū)動(dòng),、mpu6050和串口驅(qū)動(dòng)文件,, USER組下是最上層應(yīng)用程序文件。 圖 9_2 文件結(jié)構(gòu) 9.3,、上一章已經(jīng)把uart.c講解了,,前幾節(jié)也把i2c.c和mpu6050,c介紹了。這里直接從main.c對整個(gè)工程的流程進(jìn)行分析:主函數(shù)中先初始化串口和MPU6050,,接著進(jìn)入無限循環(huán),。循環(huán)中每隔一定的時(shí)間發(fā)送一幀的數(shù)據(jù)——該幀以‘#’開始以‘$’結(jié)束,中間依次是X軸加速度值,、Y軸加速度值和Z軸加速度值,。 1 //------------------------------------------------ 2 //主函數(shù) 3 //------------------------------------------------ 4 void main (void) 5 { 6 delay(500); //上電延時(shí) 7 InitUART(); //初始化串口 8 InitMPU6050(); //初始化MPU6050 9 10 while (1) //主循環(huán) 11 { 12 SendByte('#'); //起始標(biāo)志 13 SendData(GetData(0x3B)); //X軸加速度 14 SendData(GetData(0x3D)); //Y軸加速度 15 SendData(GetData(0x3F)); //Z軸加速度 16 SendByte('$'); //標(biāo)志 17 delay(20); 18 } 19 } 其中調(diào)用了串口驅(qū)動(dòng)中的void InitUART(void)串口初始化函數(shù)、 void SendByte(unsigned char dat)串口發(fā)送一字節(jié)函數(shù)和 void SendStr(unsigned char *s)串口發(fā)送一個(gè)字符串函數(shù),,以及調(diào)用了mpu6050驅(qū)動(dòng)中的void InitMPU6050()初始化函數(shù)和int GetData(uchar REG_Address)獲取6軸數(shù)據(jù)函數(shù),。 1 //外部函數(shù) 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); 這里唯一要特別說明的函數(shù)是:void SendData(int value)函數(shù),。我們知道直接調(diào)用MPU6050的函數(shù)int GetData(uchar REG_Address)返回的是int類型的數(shù)據(jù),而串口每次只能發(fā)送一個(gè)8bit的數(shù)據(jù),,于是這里的SendData則是負(fù)責(zé)將該int類型的數(shù)值轉(zhuǎn)換為串口容易發(fā)送的數(shù)據(jù)再進(jìn)行發(fā)送,。 1 //----------------------------------------- 2 //整數(shù)轉(zhuǎn)字符串 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; //取余運(yùn)算 14 *++s =temp_data/1000+0x30; 15 temp_data=temp_data%1000; //取余運(yùn)算 16 *++s =temp_data/100+0x30; 17 temp_data=temp_data%100; //取余運(yùn)算 18 *++s =temp_data/10+0x30; 19 temp_data=temp_data%10; //取余運(yùn)算 20 *++s =temp_data+0x30; 21 *++s ='\0'; //字符串結(jié)束標(biāo)志 22 } 23 24 //----------------------------------------- 25 //編碼+發(fā)送到串口 26 //----------------------------------------- 27 void SendData(int value) 28 { 29 enCode(temp, value); //轉(zhuǎn)換數(shù)據(jù)顯示 30 SendStr(temp); 31 } 上面的enCode函數(shù)是將輸入的int類型的數(shù)據(jù)轉(zhuǎn)換為第一位為符號(正用空格代替,負(fù)用負(fù)號代替),,后5位為數(shù)值的字符串,,即使不足五位數(shù)前面也要填充0。這樣便不難理解SendData的功能:將value編碼并通過串口發(fā)送,。 這樣整個(gè)工程的作用則是周期性讀取MPU6050三軸的加速度并用下面的幀格式通過藍(lán)牙發(fā)送出去:
10 客戶端軟件構(gòu)成模塊 10.1,、打開Eclipse點(diǎn)擊File菜單欄下的Import按鈕準(zhǔn)備導(dǎo)入second_test工程(如圖10_1所示)。 圖 10_1 導(dǎo)入工程 10.2,、接著在彈出的Select窗口中選擇Android文件夾下的Existing Android Code Into Workspace點(diǎn)擊next(如圖10_2所示),。 圖 10_2 選擇導(dǎo)入類型 10.3、接著在彈出的框中點(diǎn)擊右上角的Browse按鈕,,找到要導(dǎo)入的third_test所在路徑,,并且需要勾選Copy projects into workspace(如圖10_3所示)。 圖 10_3 選擇工程 10.4,、最終效果如圖10_4所示在src文件夾下有四個(gè)包:其中第一個(gè)是和藍(lán)牙相關(guān)的類(從下到上依次為藍(lán)牙設(shè)備搜索相關(guān)類,、藍(lán)牙通信連接相關(guān)類和藍(lán)牙通信相關(guān)類);第二個(gè)是繪制折線圖表相關(guān)的類(這里采用開源圖表繪制引擎achartengine,,所以在libs里要添加相應(yīng)的包),;第三個(gè)是數(shù)據(jù)池相關(guān)的類,用于實(shí)現(xiàn)藍(lán)牙數(shù)據(jù)實(shí)時(shí)高速處理,;另一個(gè)包是UI相關(guān)類,,也是整個(gè)工程最核心的部分。如果讀者導(dǎo)入過程中出現(xiàn)錯(cuò)誤,,也可以采用第三章的方法新建一個(gè)工程,,然后把src下的文件、layout下的文件和AndroidManifest.xml文件做相應(yīng)的新建或修改,,同時(shí)還要注意引入libs的包以及values里的strings.xml,。 圖 10_4 工程文件結(jié)構(gòu)
11 軟件最終效果預(yù)覽 上面是從模塊構(gòu)成的角度介紹工程的主要文件,為了更好的方便分析其內(nèi)部邏輯,,筆者準(zhǔn)備先帶領(lǐng)大家預(yù)覽下本次應(yīng)用的最終效果(如圖11_1所示): n 第一幅圖:是初始打開界面,,如果本地藍(lán)牙沒有打開最左邊的按鈕將會顯示“打開藍(lán)牙設(shè)備”; 其中前三個(gè)階段和上一章中的小風(fēng)扇的控制很類似,,都是點(diǎn)擊連接到進(jìn)入搜索再到進(jìn)行連接。只不過一個(gè)是連接后通過應(yīng)用向硬件發(fā)送命令幀來控制小風(fēng)扇轉(zhuǎn)速,;一個(gè)是不斷從記步手環(huán)讀取實(shí)時(shí)的X\Y\Z三軸的加速度,,計(jì)算合加速度同時(shí)記步,并且將數(shù)據(jù)實(shí)時(shí)以折線圖的形式展示出來,。 圖 11_1 軟件最終效果預(yù)覽圖(從左到右從上到下編號1-6)
12 一個(gè)高效處理數(shù)據(jù)的數(shù)據(jù)池設(shè)計(jì) 當(dāng)提到為什么需要高效處理的數(shù)據(jù)池時(shí),,其實(shí)要從藍(lán)牙搜索講起。由于上一章的最后對藍(lán)牙搜索,、連接,、通信的三個(gè)過程做了詳細(xì)的講解,本次則只從整體上進(jìn)行梳理一下,。 如圖12_1,,當(dāng)點(diǎn)擊連接小手環(huán)按鈕后則執(zhí)行藍(lán)牙搜索類的doDiscovery()函數(shù)進(jìn)行搜索藍(lán)牙設(shè)備,在其搜索過程中搜索的設(shè)備名和設(shè)備地址分別存儲在BlueToothSearch的公有成員變量mNameVector和mAddrVector中,,然后在本次搜索結(jié)束后會向Activity發(fā)送一個(gè)類型為0x01的Handler消息,,而該消息會被Activity中的handleMessage接收到。 當(dāng)Activity中的handleMessage接收類型為0x01的消息后,,程序會遍歷本次藍(lán)牙搜索到的周邊設(shè)備的名稱找到符合我們的手環(huán)的藍(lán)牙設(shè)備,。然后調(diào)用藍(lán)牙連接的setDevice()函數(shù)獲取遠(yuǎn)程藍(lán)牙通信socket,接著在handleMessage內(nèi)再觸發(fā)藍(lán)牙連接的線程進(jìn)行藍(lán)牙連接,。當(dāng)藍(lán)牙連接完畢,,則會發(fā)送0x02類型的消息反饋給Activity中的handleMessage。 同樣的當(dāng)Activity中的handleMessage接收類型為0x02的消息后,,程序會調(diào)用藍(lán)牙通信類的setSocket()函數(shù)來獲取標(biāo)準(zhǔn)輸入輸出流,。此后,如果想從軟件向硬件發(fā)送消息則直接可以調(diào)用藍(lán)牙通信類的write()函數(shù),,而接收數(shù)據(jù)則是采用啟動(dòng)一個(gè)接收線程來實(shí)現(xiàn)實(shí)時(shí)接收的,。 圖 12_1 從點(diǎn)擊連接小手環(huán)到完成藍(lán)牙連接全過程流程圖 現(xiàn)在我們的思維已經(jīng)跟著轉(zhuǎn)到了上圖中最后一個(gè)無限輪訓(xùn)收數(shù)據(jù)階段,。同時(shí)我們知道從小手環(huán)發(fā)來的數(shù)據(jù)是比較高速的(硬件工程中寫的是每次發(fā)送完畢delay(20),,應(yīng)該算是比較短的時(shí)間了)。那么問題就來了:如果我們不能及時(shí)地將手環(huán)傳來的數(shù)據(jù)進(jìn)行處理,,很有可能導(dǎo)致大量的數(shù)據(jù)滯留在緩沖區(qū),。這樣進(jìn)一步會導(dǎo)致每次獲得的數(shù)據(jù)都不是最新的數(shù)據(jù),而表現(xiàn)出動(dòng)態(tài)繪制折線圖滯后糟糕的效果,。 綜上由于下位機(jī)10ms發(fā)送一次20byte的數(shù)據(jù),,上位機(jī)一方面要做好接收工作,,保證數(shù)據(jù)不擁擠在串口接收緩沖區(qū);另一方面也要實(shí)時(shí)獲取當(dāng)前從串口讀到的最新數(shù)據(jù),。如果采用傳統(tǒng)多線程+鎖的機(jī)制是可以的,,但是當(dāng)多線程中加入鎖勢必會影響程序執(zhí)行效率,通過綜合分析該問題筆者最終抽象出一個(gè)特殊的數(shù)據(jù)模型——自動(dòng)更新的環(huán)形棧(樓主自造的詞,,見諒哈哈): 圖 12_2 自動(dòng)更新的環(huán)形棧 如圖12_2所謂自動(dòng)更新的環(huán)形棧本質(zhì)上是一個(gè)基于環(huán)形數(shù)組的特殊數(shù)據(jù)結(jié)構(gòu),。圖中環(huán)形代表數(shù)據(jù)池,也是一個(gè)環(huán)形數(shù)組(普通數(shù)組,,采用一定技巧將首尾連接),,p_write指示當(dāng)前數(shù)據(jù)插入位置,每次插入一個(gè)數(shù)據(jù)p_write順時(shí)針移動(dòng)一格,,從而實(shí)現(xiàn)新數(shù)據(jù)覆蓋老數(shù)據(jù)的自動(dòng)更新功能,。而這里最精妙的地方在于每次取數(shù)據(jù)的方式:從p_write所指的位置逆時(shí)針取40個(gè)數(shù)據(jù)(因?yàn)橛行臄?shù)據(jù)長度為20,一次取40保證至少有一個(gè)有效幀),,然后從這40個(gè)數(shù)據(jù)中找出有效信息,,賦值給公有成員X,Y,Z。這樣通過適當(dāng)調(diào)節(jié)環(huán)的容量,,保證取數(shù)據(jù)時(shí)該段數(shù)據(jù)不被覆蓋的前提下,,又能根據(jù)p_write指示獲取最新的下位機(jī)發(fā)來的有效幀,將存和取有效地分離從而完美達(dá)到了我們的需求,。 具體在程序中UI_Main.java的onCreate函數(shù)中聲明并實(shí)例化一個(gè)大小為20000的數(shù)據(jù)池mDataPool = new DataPool(20000),。接著在BlueToothCommunicate的輪詢接收數(shù)據(jù)的線程中(也即圖12_1的最后一環(huán)節(jié)的read中)對于每次新收到的數(shù)據(jù)調(diào)用mDataPool的push_back(buffer, bytes)函數(shù)將其存儲在數(shù)據(jù)池中。當(dāng)每次需要取最新數(shù)據(jù)時(shí)只要先調(diào)用mDataPool的ask()函數(shù),,接著便可直接通過訪問DataPool的公有成員X\Y\Z獲取最新三軸加速度的值了,。 1 // 利用線程一直收數(shù)據(jù) 2 public void run() { 3 byte[] buffer = new byte[1024]; 4 int bytes; 5 // 循環(huán)一直接收 6 while (state) { 7 try { 8 // bytes是返回讀取的字符數(shù)量,其中數(shù)據(jù)存在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 一個(gè)開源的折線圖繪制方案 在第10節(jié)客戶端軟件構(gòu)成模塊中曾提到本項(xiàng)目中采用了開源圖表繪制引擎AChartEngine,。它是一個(gè)安卓系統(tǒng)上制作圖表的框架,,支持折線圖、面積圖,、分區(qū)圖,、對比圖、散點(diǎn)圖,、柱狀圖,、餅圖等(如圖13_1所示)。 圖13_1 AChartEngine繪制的圖標(biāo)Demo 此外其所有支持的圖表類型,,都可以包含多個(gè)系列,,都支持水平(默認(rèn))或垂直方式展示圖表。并且支持許多其他的自定義功能。所有圖表都可以建立為一個(gè)view,,也可以建立為一個(gè)用于啟動(dòng)activity的intent(顯然上面前兩幅圖是采用view的形式,,其他幾個(gè)是采用intent啟動(dòng)的)。 一般突然提到某某開源包或者調(diào)用別的接口初學(xué)者可能會頭大,,而且這里更讓多數(shù)人頭痛的是筆者竟突然亮出了這么多炫酷的UI,,豈不是更加難以使用!于是可能會有很多人準(zhǔn)備自己DIY折線圖了(哈哈),。然而事實(shí)卻是這個(gè)開源的框架用起來十分方便:大家可以把所有的chart都想象成由兩層組成,,一部分是Renderer(如XYMultipleSeriesRenderer,用于對圖表樣框架樣式的說明),,另一部分是Dataset(如XYMultipleSeriesDataset,,用于對視圖數(shù)值的處理)。所以在ChartLine.java類的開始就定義并聲明這兩種類型的私有成員:【第一步:數(shù)據(jù)層和顯示層定義并實(shí)例化】 1 private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset(); 2 private XYMultipleSeriesRenderer mRenderer = new XYMultipleSeriesRenderer(); 因?yàn)閙Renderer用于對圖表框架樣式的說明,,所以在setChartSettings函數(shù)里調(diào)用了多個(gè)其成員函數(shù)用來對圖表整體樣式屬性進(jìn)行設(shè)置,。例如7、8兩行是設(shè)置X軸和Y軸的標(biāo)題,,9到12行設(shè)置初始X軸和Y軸所表示的范圍,,22到24行用來設(shè)置放大縮小的控件和屬性(就像地圖控件里的放大縮小按鈕)。這樣下層的X軸,、Y軸等就都設(shè)置好了,。【第二步:設(shè)置顯示層顯示樣式】 1 public void setChartSettings(String xTitle, String yTitle, double xMin, 2 double xMax, double yMin, double yMax, int axesColor, 3 int labelsColor) { 4 // 有關(guān)對圖表的渲染可參看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);// 坐標(biāo)軸顏色 12 mRenderer.setLabelsColor(labelsColor);// 標(biāo)號顏色 13 mRenderer.setShowGrid(true);// 顯示網(wǎng)格 14 mRenderer.setGridColor(Color.GRAY); 15 mRenderer.setXLabels(16); 16 mRenderer.setYLabels(20); 17 mRenderer.setYLabelsAlign(Align.RIGHT);// 設(shè)置標(biāo)簽居Y軸的方向 18 mRenderer.setPointSize((float) 2); 19 mRenderer.setShowLegend(true);// 下面的標(biāo)注 20 // mRenderer.setZoomButtonsVisible(true);// 放大縮小按鈕 21 mRenderer.setZoomEnabled(true, false);// 設(shè)置縮放,這邊是橫向可以縮放,豎向不能縮放 22 mRenderer.setPanEnabled(true, false);// 設(shè)置滑動(dòng),這邊是橫向可以滑動(dòng),豎向不可滑動(dòng) 23 } 當(dāng)表格框架設(shè)置好之后,,接下來就是向框架內(nèi)填充折線,,并且在此過程中把每一條折線的數(shù)據(jù)層放入總的數(shù)據(jù)層中。如下setLineSettings函數(shù)循環(huán)4次,,每次首先實(shí)例化一個(gè)標(biāo)題為titles[i]的坐標(biāo)序列,,然后將該序列放入總的數(shù)據(jù)層mDataset中。同樣的每次實(shí)例化一個(gè)XYSeriesRenderer(因?yàn)槊總€(gè)折線也有自己的樣式),,并將其加入總的圖標(biāo)層mRenderer中,。這樣就能夠?qū)?條分別表示X軸加速度、Y軸加速度,、Z軸加速度和合加速度的折線圖設(shè)置好,。【第三步:設(shè)置4個(gè)折線數(shù)據(jù)序列并加入數(shù)據(jù)層,,設(shè)置4個(gè)折線層并加入顯示層】 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);// 實(shí)心還是空心 renderer[i].setDisplayChartValues(false);// 不顯示值 renderer[i].setDisplayChartValuesDistance(10); } } 此時(shí)mDataset里存放著當(dāng)前要顯示的折線的所有XYSeries,,每個(gè)折線XY序列存放在mCurrentSeries[i]中,如果想在該折線圖上增加一個(gè)數(shù)據(jù)只要調(diào)用mCurrentSeries[i].add(x, y)即可,;如果想顯示或隱藏某個(gè)折線圖只要調(diào)用圖表層的mRenderer和數(shù)據(jù)層mDataset移出對應(yīng)的折線和折線序列即可,?!咎崆耙徊剑?):如何往對應(yīng)的折線中增加數(shù)據(jù),,以及如何顯示隱藏某條折線】 1 // 顯示第i個(gè)折線圖 2 public void showLine(int i) { 3 mDataset.addSeries(mCurrentSeries[i]); 4 mRenderer.addSeriesRenderer(renderer[i]); 5 } 6 7 // 隱藏第i個(gè)折線圖 8 public void hideLine(int i) { 9 mDataset.removeSeries(mCurrentSeries[i]); 10 mRenderer.removeSeriesRenderer(renderer[i]); 11 } 12 13 // 向第i個(gè)折線圖中添加(x,y)數(shù)據(jù) 14 public void addData(int i, double x, double y) { 15 mCurrentSeries[i].add(x, y); 16 } 上面說過所有圖表都可以建立為一個(gè)view,,也可以建立為一個(gè)用于啟動(dòng)activity的intent。這里由于我們需要在ui_main.xml中添加其他控件,,所以采用view的方式新建圖表,。如下setChartViewSetting函數(shù)負(fù)責(zé)當(dāng)圖表沒有建立時(shí)分別實(shí)例化layout和mChartView,并將新建的mChartView加入ui_main.xml中圖表所在的layout中,,這樣我們就可以看到基本的圖表了,。此外,第10行是給圖表加的點(diǎn)擊監(jiān)聽,,用于顯示點(diǎn)擊點(diǎn)的詳細(xì)信息(圖11_1軟件最終效果的第6張圖),。【第四步:將數(shù)據(jù)層和顯示層合成為圖表加入U(xiǎn)I中】 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 整體邏輯梳理 其實(shí)仔細(xì)觀察的讀者會發(fā)現(xiàn):本次的UI_Main.java和上次的大致相同,。前一階段都是點(diǎn)擊按鈕來連接遠(yuǎn)程藍(lán)牙設(shè)備,。而不同之處在于上一章是通過加減按鈕向小風(fēng)扇發(fā)送速度控制命令來控制速度,這一章是不斷讀取手環(huán)的實(shí)時(shí)數(shù)據(jù)并用折線圖繪制出來,。整體業(yè)務(wù)邏輯還是在控件的點(diǎn)擊事件和handleMessage之間有序進(jìn)行,,下面將著重說明數(shù)據(jù)的實(shí)時(shí)顯示及一些用于優(yōu)化操作的細(xì)節(jié)。 在onCreate中首先實(shí)例化藍(lán)牙三劍客,,接著實(shí)例化數(shù)據(jù)池和折線圖表,,然后調(diào)用折線圖類的成員函數(shù)對折線圖做前期設(shè)置,最后啟動(dòng)ChartThread線程,。 1 // 實(shí)例化藍(lán)牙三劍客(搜索,、連接、通信) 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 // 設(shè)置圖標(biāo)顯示的基本屬性 11 mChartLine.setChartSettings("Time", "", 0, 100, -20000, 20000, 12 Color.WHITE, Color.WHITE); 13 // 設(shè)置四個(gè)折線圖的屬性 14 mChartLine.setLineSettings(); 15 16 ChartThread.start();// 啟動(dòng)圖標(biāo)更新線程 在此之后便是對連接手環(huán)按鈕做的相關(guān)設(shè)置,,這里和上一章中的連接風(fēng)扇幾乎一樣,,關(guān)鍵在于理解藍(lán)牙三劍客通過線程啟動(dòng)并通過handler將消息反饋的機(jī)制。 圖 14_1 點(diǎn)擊連接設(shè)備到通信建立 這樣當(dāng)點(diǎn)擊連接手環(huán)的按鈕之后,,然后在handler的溝通下上位機(jī)和下位機(jī)最終實(shí)現(xiàn)可通信,。此時(shí)下位機(jī)一旦有數(shù)據(jù)傳送上來,上位機(jī)便快速的將其放入數(shù)據(jù)池內(nèi),。那么程序是在成么時(shí)候取數(shù)據(jù)并更新UI的呢,?秘密就在于ChartThread.start()! 1 private Thread ChartThread = new Thread() { 2 public void run() { 3 while (true) { 4 try { 5 sleep(100); 6 // 周期性發(fā)送更新Chart的消息(因?yàn)閁I不能放在這個(gè)里面更新) 7 Message msg = new Message(); 8 msg.what = 0x04; 9 myHandler.sendMessage(msg); 10 } catch (InterruptedException e) { 11 } 12 } 13 } 14 };
從上面的可以看出ChartThread主要負(fù)責(zé)周期性發(fā)送類別為0x04的消息,,而在handleMessage的case 0x04中則是負(fù)責(zé)獲取實(shí)時(shí)數(shù)據(jù)并更新UI的,。之所以這樣繞個(gè)彎是因?yàn)閁I更新一旦放在ChartThread中就會導(dǎo)致程序運(yùn)行異常。這里的數(shù)據(jù)獲取和更新也比較容易理解:首先調(diào)用數(shù)據(jù)池的ask函數(shù)從p_write向后找40個(gè)數(shù)據(jù)尋找并解析有效幀,,如果成功則最新的X\Y\Z三軸的加速度已經(jīng)保存在mDataPool的公有成員X\Y\Z中,。下面第3行是計(jì)算合加速度(減去16000是為了方便顯示),接著6到9行負(fù)責(zé)分別將三軸加速度及其合速度值加入折線圖。第10到13行便是我們簡單的記步算法了,,即當(dāng)合加速值超過設(shè)定的記步閾值時(shí)記步數(shù)加一,。第15、16行是控制折線圖滾動(dòng)到最新的位置并刷新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) {// 記步-和加速度超過設(shè)定上限則記步 11 mNum++; 12 mTextView2.setText("當(dāng)前記步數(shù)為: " + mNum); 13 } 14 mTime += 1; 15 mChartLine.letChartMove(mTime);// 控制圖形滾動(dòng) 16 mChartLine.mChartView.repaint(); 17 } 18 break; 綜上,,當(dāng)建立藍(lán)牙通信后,整個(gè)應(yīng)用程序中主要有三個(gè)線程:①用于不斷讀取串口數(shù)據(jù)并將其存入數(shù)據(jù)池的數(shù)據(jù)線程,;②用于周期性發(fā)送0x04消息的信號線程,;③隱蔽而重要的主線程(UI更新等操作)。如圖14_2所示:一方面數(shù)據(jù)線程不斷讀取數(shù)據(jù)存入數(shù)據(jù)池,,另一方面信號線程周期性發(fā)送0x04消息觸發(fā)handleMessage的case 0x04執(zhí)行ask讀數(shù)據(jù)函數(shù),,當(dāng)成功解析到有效數(shù)據(jù)時(shí)會在主線程中記步并更新UI。 圖 14_2 三線程運(yùn)作 此外,,還有一些其他的控件用于提高交互性,,如表14_1所示:開始/停止按鈕用于控制折線圖是否動(dòng)態(tài)滾動(dòng),當(dāng)停止折線圖動(dòng)態(tài)滾動(dòng)時(shí)折線圖的數(shù)據(jù)增加并未被中止,,此時(shí)可以方便用戶拖動(dòng)折線圖查看歷史或觀察細(xì)節(jié),。四個(gè)CheckBox用于控制顯示哪一個(gè)折線圖,這樣便于單獨(dú)分析,。滾動(dòng)條是用來動(dòng)態(tài)設(shè)置記步閾值的,,這樣便于大家深入理解我們的簡單的記步算法。 表 14_1 其他用于優(yōu)化交互的控件
15 最終成果檢查 如果說遙控小風(fēng)扇是硬件上要費(fèi)工夫的一個(gè)測試,,那么本章的記步手環(huán)無疑需要在軟件上費(fèi)很大工夫的一個(gè)測試,。這里所說的軟件不僅包括引進(jìn)一個(gè)開源圖表繪制框架、延續(xù)了上一章中的藍(lán)牙通信三劍客,、嘗試了稍微繁瑣的布局和多控件UI,,還包括多線程中狀態(tài)轉(zhuǎn)換控制和為高速實(shí)時(shí)而設(shè)計(jì)的數(shù)據(jù)池?cái)?shù)據(jù)結(jié)構(gòu)。如果再廣一點(diǎn),,還有I2C通信協(xié)議的驅(qū)動(dòng)設(shè)計(jì)和數(shù)據(jù)幀的設(shè)計(jì)等,。 大家也不要被這幾篇測試的難度嚇倒,因?yàn)闊o數(shù)的事實(shí)證明備戰(zhàn)的強(qiáng)度要比真正的戰(zhàn)斗要艱難的多(哈哈,,否則怎么會有勢如破竹的戰(zhàn)績呢,?)。下面讓我們看看自己本次備戰(zhàn)的成績?nèi)绾巍?/p> △ 知道一個(gè)簡單記步手環(huán)的構(gòu)成模塊(+ 10分) ——及格線70分,,良好線150分,,優(yōu)秀200分。自己還滿意吧,? [搜索:beautifulzzzz(看樓主博客園官方博客,,享高質(zhì)量生活)嘻嘻?。?!] [如果您也喜歡智能硬件的東西,,可以交個(gè)朋友~] [如果您胸懷大志,能團(tuán)結(jié)各路豪杰的亂世英雄,,也可以留下一下信息~] 如果您覺得不錯(cuò),,別忘點(diǎn)個(gè)贊讓更多的小伙伴看到\(^o^)/~
|
|