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