據(jù)采集與遠程控制實現(xiàn))
簡介面向嵌入式與Android開發(fā)者的CC2530溫度、紅外傳感器控制上位機項目整合Zigbee節(jié)點與手機端遠程監(jiān)控適合學(xué)習(xí)無線傳感器網(wǎng)絡(luò)和物聯(lián)網(wǎng)應(yīng)用開發(fā)的工程師。壓縮包共78個文件約4.45MB包含Android工程源碼、XML布局、PNG圖標(biāo)、JAR依賴庫及多個APK安裝包結(jié)構(gòu)完整可直接導(dǎo)入編譯或安裝體驗。已有944人學(xué)習(xí)項目展示了從串口通信建立、協(xié)議設(shè)計、傳感器數(shù)據(jù)采集到UI實時更新的全鏈路實現(xiàn)能幫助讀者理解CC2530硬件控制、Android USB串口通信以及前后端數(shù)據(jù)交互。參考其中協(xié)議定義和應(yīng)用層設(shè)計可快速移植DS18B20、HC-SR501等傳感器邏輯構(gòu)建自己的遠程監(jiān)測方案。1. 項目概述與整體方案選型1.1 需求拆解這個項目到底要做什么這個項目是我?guī)鸵粋€做環(huán)境監(jiān)測的朋友做的核心需求很明確用CC2530作為下位機節(jié)點外接DS18B20采集環(huán)境溫度外接熱釋電紅外傳感器檢測人體活動兩個傳感器數(shù)據(jù)要通過串口實時上報給Android手機端App不僅要顯示數(shù)據(jù)還要能下發(fā)指令控制繼電器和蜂鳴器。一句話總結(jié)就是傳感器采集加執(zhí)行控制Android手機當(dāng)遠程面板。別看功能簡單真正做起來需要打通的東西不少CC2530端要有穩(wěn)定的傳感器驅(qū)動和組幀邏輯Android端要有可靠的串口通信和數(shù)據(jù)解析兩端之間還要約定一套不會出錯的通信協(xié)議。這篇文章把從下位機到上位機的完整鏈路拆開講不光是貼代碼還會說明每一步為什么這么做。適合做課程設(shè)計、電子競賽、智能家居類項目的同學(xué)參考也適合剛?cè)腴T嵌入式、想搞清楚設(shè)備和App怎么對話的開發(fā)者。1.2 方案選型為什么用CC2530又為什么不用Z-Stack用CC2530做主控很多人第一反應(yīng)是這不就是個Zigbee芯片嗎是不是要用Z-Stack協(xié)議棧組網(wǎng)。這恰恰是新手最容易走偏的地方。CC2530本質(zhì)是一顆8051內(nèi)核的SoC自帶2.4GHz射頻和豐富的外設(shè)它既可以跑Zigbee協(xié)議棧也完全可以當(dāng)一顆普通單片機裸奔使用。這個項目里只有一個采集節(jié)點要直接連手機不存在多節(jié)點組網(wǎng)需求所以我選擇了裸機編程。原因很實際Z-Stack的OSAL事件調(diào)度機制對新手來說學(xué)習(xí)曲線陡而且協(xié)議棧初始化會占掉不少系統(tǒng)資源單純?yōu)榱薌PIO采集和串口發(fā)送引入一套協(xié)議棧完全是殺雞用牛刀。如果后續(xù)要擴展成多個CC2530節(jié)點采集匯聚到協(xié)調(diào)器再轉(zhuǎn)發(fā)給手機那時候再切到Z-Stack重新設(shè)計架構(gòu)也不遲。硬件上我用的是一塊CC2530最小系統(tǒng)板加底板擴展因為板載集成了USB轉(zhuǎn)串口芯片調(diào)試和供電都方便。外接DS18B20在P1.0口紅外傳感器接P1.1口繼電器控制引腳放在P1.2預(yù)留一個蜂鳴器在P1.3IO分配盡量錯開避免后來的PCB布線和程序擴展互相干擾。2. 下位機CC2530的采集、控制與通信實現(xiàn)2.1 我使用的開發(fā)環(huán)境與時鐘配置CC2530的開發(fā)環(huán)境我用的是IAR for 8051這個IDE是TI官方主推的工程配置里有一點必須注意芯片型號選CC2530F256鏈接器配置里要用到對應(yīng)的配置文件不然燒錄后程序跑不起來。時鐘選擇是個容易踩坑的點。CC2530內(nèi)部有16MHz RC振蕩器但RC振蕩器的頻率精度受溫度和電壓影響較大如果直接用內(nèi)部時鐘做串口波特率長時間通信后累計誤差會導(dǎo)致數(shù)據(jù)錯位。我的做法是外部32MHz晶振通過寄存器配置將系統(tǒng)主時鐘切到32MHz外部晶振這樣115200波特率才能保證足夠的精度。void system_clock_init(void) { // 切換到32MHz外部晶振確保串口波特率穩(wěn)定 SET_MAIN_CLOCK_SOURCE(0); // 選擇外部32MHz晶振 CLKCONCMD ~0x40; // 設(shè)置系統(tǒng)時鐘源 while (CLKCONSTA 0x40); // 等待切換完成 CLKCONCMD ~0x07; // 系統(tǒng)時鐘設(shè)為32MHz while (CLKCONSTA 0x07); }2.2 DS18B20溫度采集單總線時序與代碼實現(xiàn)DS18B20是Dallas出的單總線數(shù)字溫度傳感器一根數(shù)據(jù)線就能完成供電和通信精度能做到12位分辨率0.0625攝氏度。但它對時序要求很苛刻讀時序和寫時序的最小時間差是微秒級的8051在這個地方特別容易被中斷打擾導(dǎo)致時序被拉長。我的做法是在單總線操作的臨界區(qū)前關(guān)中斷操作完再開。代碼里所有延時函數(shù)必須用邏輯分析儀實測校準(zhǔn)過不能想當(dāng)然填幾個空循環(huán)就完事。最容易被忽略的是上電后的轉(zhuǎn)換時間DS18B20在12位分辨率下的轉(zhuǎn)換時間最長750ms如果你發(fā)完啟動轉(zhuǎn)換指令就立刻讀溫度讀回來的永遠是上一次的舊值。uint16_t ds18b20_read_temperature(void) { uint8_t temp_l, temp_h; int16_t raw; uint16_t result; EA 0; // 關(guān)中斷保護單總線時序 ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳過ROM匹配 ds18b20_write_byte(0x44); // 啟動溫度轉(zhuǎn)換 delay_ms(750); // 等待轉(zhuǎn)換完成必須等夠 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 讀取暫存器內(nèi)容 temp_l ds18b20_read_byte(); temp_h ds18b20_read_byte(); ds18b20_reset(); EA 1; // 恢復(fù)中斷 raw (temp_h 8) | temp_l; result (uint16_t)(raw * 0.0625 * 10); // 乘10保留一位小數(shù) return result; }上拉電阻一定不能省DS18B20數(shù)據(jù)線上必須接4.7k歐姆上拉。我之前圖省事用過板載弱上拉結(jié)果是溫度偶爾正常偶爾顯示85度這個85度是DS18B20上電后的默認(rèn)值說明芯片經(jīng)常復(fù)位排查了半天才發(fā)現(xiàn)是上拉強度不夠。2.3 紅外傳感器接入與控制輸出紅外部分我選用的是HC-SR501熱釋電紅外模塊它檢測的是人體輻射的紅外線變化模塊上自帶菲涅爾透鏡和信號處理電路輸出直接就是TTL高電平單片機只需要讀GPIO狀態(tài)。這里有兩個使用細(xì)節(jié)要強調(diào)。第一HC-SR501上電后需要大約一分鐘預(yù)熱穩(wěn)定期這段時間內(nèi)輸出會頻繁誤觸發(fā)首次上電要在程序里做個初始化延時邏輯或者干脆在校驗階段忽略前60秒的數(shù)據(jù)。第二模塊背面有兩個可調(diào)電位器一個調(diào)感應(yīng)距離大概3到7米一個調(diào)輸出延時大概5秒到5分鐘實際部署時要用小螺絲刀反復(fù)調(diào)到合適的位置不要指望軟件能完全彌補硬件的誤判。采集到紅外狀態(tài)后我的程序邏輯是檢測到有人且溫度超過設(shè)定閾值則置位繼電器開并觸發(fā)蜂鳴器報警同時把狀態(tài)打包發(fā)送給Android端。養(yǎng)成一個習(xí)慣控制輸出前加軟件延時去抖紅外信號持續(xù)20ms以上才認(rèn)為是有效觸發(fā)這樣能過濾掉大部分脈寬很窄的干擾信號。2.4 串口數(shù)據(jù)幀協(xié)議給上下位機定好規(guī)矩下位機采集到的數(shù)據(jù)要發(fā)給Android端通信協(xié)議是我在整個項目里最看重的一部分。串口通信本質(zhì)是面向字節(jié)流的上電后從第一個字節(jié)開始接收如果沒有協(xié)議約束接收端根本無法判斷一個溫度數(shù)據(jù)從哪里開始到哪里結(jié)束。我定義了一個簡單的幀結(jié)構(gòu)所有字段都用單字節(jié)便于單片機處理。幀頭固定為0xAA 0x55設(shè)備地址用于將來擴展多設(shè)備幀類型0x01表示傳感器數(shù)據(jù)上行0x02表示控制指令下行后面跟數(shù)據(jù)長度和數(shù)據(jù)區(qū)最后一字節(jié)是前面所有字節(jié)累加和的低八位。字段字節(jié)數(shù)說明幀頭11固定0xAA幀頭21固定0x55設(shè)備地址10x01代表1號節(jié)點幀類型10x01上行數(shù)據(jù)0x02下行控制數(shù)據(jù)長度1數(shù)據(jù)區(qū)字節(jié)數(shù)數(shù)據(jù)區(qū)N按幀類型定義校驗和1前面所有字節(jié)累加取低8位溫度上行幀的數(shù)據(jù)區(qū)固定5字節(jié)0x01表示溫度數(shù)據(jù)類型隨后兩個字節(jié)是溫度整數(shù)部分和小數(shù)部分再一個字節(jié)是紅外狀態(tài)最后一個字節(jié)是繼電器當(dāng)前狀態(tài)。這樣Android端解析邏輯就能寫得很簡單不需要考慮變長數(shù)據(jù)。校驗和必須加串口在無屏蔽環(huán)境很容易被電機、繼電器開關(guān)的瞬間干擾打亂數(shù)據(jù)位沒有校驗就沒法發(fā)現(xiàn)壞幀。3. Android上位機串口通信鏈路搭建與界面聯(lián)動3.1 通信方式選型USB串口還是藍牙Android端和下位機通信最常見的兩條路是USB轉(zhuǎn)串口和藍牙串口模塊。前者用OTG線直接連設(shè)備穩(wěn)定可靠插上就通后者走HC-05之類的藍牙模塊免布線但配對和連接狀態(tài)管理會多出一堆邏輯。我最終選了USB串口方案因為項目里CC2530底板已經(jīng)集成了CH340芯片直接用OTG線連接手機就能當(dāng)串口用開發(fā)調(diào)試階段少一層無線干擾。如果你手頭的板子沒有USB轉(zhuǎn)串口或者你想做無線部署可以考慮把設(shè)備端換成藍牙模塊Android側(cè)用官方藍牙API做SPP連接整體架構(gòu)不變只是把數(shù)據(jù)鏈路層換一下。需要注意一個前置條件手機必須有OTG功能Android系統(tǒng)版本建議6.0以上。CH340這類芯片不像標(biāo)準(zhǔn)USB免驅(qū)設(shè)備那樣系統(tǒng)默認(rèn)識別需要在App里集成對應(yīng)的串口驅(qū)動庫通過USB權(quán)限申請才能訪問。3.2 串口庫集成與打開串口的正確姿勢Android端做USB串口通信我直接用了一個非常好用的開源庫usb-serial-for-android它內(nèi)部適配了FTDI、CP210x、CH34x以及標(biāo)準(zhǔn)CDC設(shè)備CH340這種國產(chǎn)芯片也在支持列表里省掉了很多自己寫驅(qū)動的痛苦。集成方式很簡單在Gradle里加一行依賴然后在Manifest里聲明USB設(shè)備廣播過濾和權(quán)限uses-feature android:nameandroid.hardware.usb.host / uses-permission android:nameandroid.permission.USB_PERMISSION_ATTACHED / activity android:name.MainActivity intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /activitydevice_filter.xml里指定廠商ID和產(chǎn)品IDCH340的vendorId通常是0x1A86。App檢測到設(shè)備插入后調(diào)用requestPermission彈窗申請權(quán)限拿到權(quán)限再open設(shè)備設(shè)置波特率115200、8數(shù)據(jù)位、1停止位、無校驗參數(shù)。這里有個經(jīng)驗打開串口之前先檢查設(shè)備是否已被占用用兩個Thread同時讀一個串口設(shè)備是必然崩潰的。3.3 數(shù)據(jù)解析與界面刷新別卡主線程串口數(shù)據(jù)讀取必須放在后臺線程Android的主線程負(fù)責(zé)UI如果直接在UI線程做阻塞讀App秒變ANR。我的做法是開一個專門的工作線程用循環(huán)從串口讀取數(shù)據(jù)讀到的字節(jié)先放到一個緩沖區(qū)里解析出完整幀后再把結(jié)果通過Handler或者runOnUiThread發(fā)送到主線程刷新界面。粘包和半包是串口通信里的老問題下位機一次發(fā)送的數(shù)據(jù)可能被拆成多段到達也可能兩次數(shù)據(jù)連在一起到達。我的解析思路是把每次讀到的字節(jié)追加到緩沖區(qū)尾部然后循環(huán)查找?guī)^0xAA 0x55找到后根據(jù)數(shù)據(jù)長度字段判斷幀是否完整完整就取幀校驗不完整就等待下一次讀數(shù)據(jù)。private void parseBuffer() { int index; while ((index findHeader(buffer)) 0) { if (buffer.size() - index 4) { break; // 幀頭不完整等待更多數(shù)據(jù) } int len buffer.get(index 4) 0xFF; if (buffer.size() - index len 6) { byte[] frame copyFrame(index, len 6); if (checkSum(frame)) { handleFrame(frame); } removeProcessed(index, len 6); } else { break; // 幀數(shù)據(jù)不完整等待 } } }這種邊收邊解析的寫法比等滿一包再處理靠譜得多尤其在下位機高頻上報的場景緩沖區(qū)滿了也不怕頭部處理完的數(shù)據(jù)會及時清掉。界面刷新我用了最簡單的Handler機制溫度是一個大的TextView實時更新紅外狀態(tài)用一個圓形指示燈View切換紅綠顏色。讀取線程和UI線程之間只傳遞解析好的數(shù)據(jù)對象不要把原始字節(jié)直接拋給UI線程去解析。3.4 控制指令下發(fā)與反饋閉環(huán)Android端不僅收數(shù)據(jù)還要發(fā)控制指令。界面上放了一個ToggleButton控制繼電器開關(guān)點擊事件里組裝下行幀幀頭0xAA 0x55、設(shè)備地址0x01、幀類型0x02、數(shù)據(jù)長度0x01、數(shù)據(jù)區(qū)0x01或者0x00、最后計算校驗和通過串口write方法寫入。單純發(fā)送了不管是不夠的我在下位機收到控制指令后會立即給Android端回一幀狀態(tài)確認(rèn)數(shù)據(jù)區(qū)帶上繼電器實際狀態(tài)的反饋。Android端在發(fā)送指令后的500毫秒內(nèi)如果沒有收到對應(yīng)反饋就提示用戶指令發(fā)送失敗讓你能區(qū)分是串口斷開了還是下位機沒執(zhí)行。別小看這個閉環(huán)設(shè)計演示的時候它能幫你省掉大量到底發(fā)出去沒有的扯皮。4. 聯(lián)調(diào)階段的坑與排查經(jīng)驗4.1 串口亂碼問題多半出在時鐘和電平第一次把CC2530接上Android App最常見的問題就是收到的數(shù)據(jù)全是亂碼。我先排除了Android側(cè)波特率配置錯誤然后才鎖定了CC2530的時鐘問題上面已經(jīng)說過用內(nèi)部RC振蕩器跑115200波特率誤差會大到直接亂碼。切換到外部晶振后問題立刻消失。另一個隱藏問題是電平不匹配。CC2530是3.3V供電它的串口IO輸出是3.3V電平大部分USB轉(zhuǎn)串口模塊兼容3.3V和5V兩檔。如果你的模塊跳線帽撥到了5V檔或者用了老式RS232電平電路數(shù)據(jù)位會被拉高到錯誤電平表現(xiàn)出來也是亂碼。建議直接用3.3V檔并確認(rèn)連接線沒有交叉錯位CC2530的TX接對端RX別接成直通。4.2 數(shù)據(jù)錯幀、粘包協(xié)議容錯要提前做聯(lián)調(diào)中遇到過一個很典型的錯幀問題Android端偶爾顯示溫度變成80多度或者紅外狀態(tài)莫名其妙反轉(zhuǎn)。起初以為是傳感器壞了后來加日志才發(fā)現(xiàn)是幀同步出了問題。如果下位機上電瞬間發(fā)送了半個幀Android端從錯誤位置開始找?guī)^運氣好能自動同步回來運氣不好會把錯位數(shù)據(jù)當(dāng)有效數(shù)據(jù)處理。解決思路有兩層。上層是嚴(yán)格校驗校驗和不匹配的幀直接丟棄不丟棄也不能把里面的數(shù)據(jù)拿來刷新界面。底層是每幀加幀頭并且?guī)^連續(xù)兩個字節(jié)單字節(jié)幀頭在數(shù)據(jù)區(qū)隨機出現(xiàn)0xAA時容易誤判雙幀頭加上長度和校驗?zāi)馨颜`判概率壓到極低。實測這個方案跑了三天再沒出現(xiàn)過一次錯幀展示。4.3 紅外傳感器誤報觸發(fā)邏輯和設(shè)備調(diào)參HC-SR501在空調(diào)出風(fēng)口附近會頻繁誤報這個不是模塊壞了是熱釋電紅外對溫度擾動本身就很敏感。我的處理策略是軟件層面對相鄰兩次狀態(tài)變化做時間間隔判斷如果紅外信號在小于500毫秒內(nèi)反復(fù)翻轉(zhuǎn)認(rèn)定是抖動維持上一次狀態(tài)不更新。這類傳感器更適合做區(qū)域有人無人判斷不適合做精確的觸發(fā)計時產(chǎn)品設(shè)計時要有預(yù)期。另外提醒一點HC-SR501的塑料透鏡很容易積灰別用濕布擦輕輕用干布或者氣吹清理。室內(nèi)項目用了半年多輸出狀態(tài)越來越不穩(wěn)定最后發(fā)現(xiàn)就是透鏡臟了導(dǎo)致感應(yīng)距離縮短清理后恢復(fù)正常。4.4 供電與穩(wěn)定性一些容易忽略的硬件細(xì)節(jié)CC2530在繼電器吸合瞬間會有比較大的電流沖擊如果供電走的是劣質(zhì)USB線電壓跌落會讓單片機直接復(fù)位溫度數(shù)據(jù)瞬間清零。我的解決辦法是在繼電器控制引腳加三極管驅(qū)動和續(xù)流二極管并且在下位機供電處并聯(lián)一個470uF電解電容加一個0.1uF瓷片電容把瞬態(tài)壓降吃掉。還有一個小細(xì)節(jié)每次繼電器切換指令發(fā)出后Android端看到的狀態(tài)反饋大約有幾十到一百毫秒的延遲這是正常的。不要在下位機程序里做發(fā)送完立即讀取傳感器這種動作傳感器模塊需要幾百微秒的穩(wěn)定時間這個用條件編譯加一點空延時就能規(guī)避。整套系統(tǒng)穩(wěn)定運行下來我對簡單項目這個詞有了新的理解鏈路越短越要把每一環(huán)的細(xì)節(jié)砸實任何一環(huán)松了整體就都松了。本文還有配套的精品資源點擊獲取