目錄
1. 簡介
1.1 創客實用電台
1.1.1 頻率
1.1.2 天線
1.1.3 電源
1.1.4 空間
1.2 您將會學到什麼
2. Arduino車庫開關和通用無線電介面
2.1 Arduino車庫開關,基地台
2.1.1 BOM
2.1.2 啟動!
2.2 Arduino車庫開關,手持單元
2.2.1 BOM
2.2.2 啟動!
無線電波無處不在。來自大爆炸的深空回波在微波頻譜中最為明顯,而來自家庭內部電線和附近地鐵系統的局域波在50-60Hz頻段內最為顯著。當您觸摸3.5mm TRS插孔並聽到令人討厭的嗡嗡聲時,您自身就已經成為了接收偶發模擬無線電波的巨型可聽電容器。
在本文中,我們將介紹一個非常適合創客的實用且抗干擾的無線電系統。
它適用於類比和數位的傳輸與接收,且具有CRC校驗和。該系統在ISM/SRD頻段(美國常用433和902-928MHz)上的位置非常靈活,並且允許對其進行程式設計,所以您可以選擇適合自己的以及符合國家要求的頻率。有關合法性的問題,請參考低功率、未經許可的發射機FCC規則檔中的第15.231部分,間歇控制信號。
就範圍而言,它相比於WiFi有很大的進步,但還遠不及LoRa。在兩個網站上都有定向天線的視距(LOS)場景中,範圍大約為400米。
在CC1101資料表中每當提到kBaud(千波特)時,可將其認為0.1250 kByte(千位元組)。實際上,只需要把串列傳輸速率視為位元即可(每位元組8位元)。它們之間存在差異,尤其是曼徹斯特編碼和4-FSK編碼,但這與我們所要講的Arduino車庫開關和通用無線電介面沒什麼關係。
無線電應用中最重要的因素是頻率(以赫茲為單位,Hz)、功率(以瓦特為單位,W)天線和空間。
現在我們來簡單介紹一下。
1.1.1 Frequency頻率
頻率,赫茲(千赫、兆赫、千兆赫)描述了特定事物的週期性間隔或每秒的週期數。如果擺鐘在一秒鐘內完成了一個完整的擺動,它的頻率為1 Hz。
如果需要兩秒鐘,那麼頻率為0.5Hz。而如果每秒擺動兩次,頻率則為2Hz。我們這裡使用的是電磁輻射,該理論也適用於電磁波。
光速“c”是299.792.458m/s,為了簡單起見,光速被近似為300.000km/s。在知道光速和頻率的情況下,我們直接用光速除以頻率“f”(以兆赫為單位),就可以推導出波長。如下所示:
1 2 |
c / f = l 300 / 433 = 0.692 |
將結果“I”乘以100,得到以釐米為單位的波長,乘以(100/2.54)就可以得到以英寸為單位的波長。
如果知道光速和波長,我們將“c”除以“I”,就可以得到“f”的值。因此:
1 2 |
c / l = f 300 / 0.6928 = 433.02 |
1.1.2 天線
這就是我們所需要的所有數學知識了。這很容易記住,接下來您只需要使用各種頻率和DIY天線就可以了。
我們的CC1101模組將設置為在433MHz運行,並帶有板載SMA連接器。請注意,CC1101可以在300-348 MHz、387-464 MHz和779-928 MHz範圍內工作。奇怪的是,附帶的天線只有4cm/1.57 in長。雖然這可能是絕緣層包裹的線圈天線,但是我還是稱這些天線為十六分之一波長天線,並且對它們沒有太多期望。
幸運的是,使用上面的公式,我們可以構造出四分之一波長鞭形天線。對於SMA這樣的天線,可以去除橡膠絕緣層,並焊接四分之一波長線圈天線。實際上,與433MHz信號適配良好的天線長度為:
1 |
300/433/4 = 0.173 |
數值為17.3cm/6.18in。四分之一波長天線的長度同樣容易計算,只需要將波長的結果除以4即可。對於天線,使用絞合線或實芯線都可以,只要是絕緣的就行,但是實芯線更容易做成線圈。而且電氣設備長度也很重要,在一個小小的手持車庫開關上裝配17.3cm/6.18in的天線會很不便利。
1.1.3電源
信號強度也很重要,它被描述為每米dB毫伏(dBmV/m),或每米dB-微伏(dBuV/m),或超過一毫瓦的分貝(dBm)。您需要將頻率計數器直接放置在天線旁邊才能準確讀取—我通常只關心mW/W值,看看我能與多遠的節點之間進行ping-pong信號傳遞,就像潛艇那樣!
我們的CC1101無線電模組在我的頻率計數器上讀數約為30mV,這很不錯(可以推測它們為3.3×0.03=100mW)。將發射器設置為連續傳輸,並在室外進行範圍測試,我在~60m/197ft處才開始丟失信號,這很棒!
如果您有興趣瞭解關於dBm(這是得到無線電信號強度的正確方式)的更多資訊,請參閱有關dBm的維琪百科。這會很有幫助。
1.1.4 空間
用外行的話來說,空間是距離,以及兩點之間空間中的任何物質。
在空間中,可能有一棵樹、一片森林、一所房子,或者知識微小的大氣顆粒,比如水分子。如果空間裡什麼都沒有,那麼無線電波就會從A電直線傳播到B點。如果空間裡到處都是金屬碎片,那麼電波就會被破壞和分散。
可以預料到,我們的無線電信號僅僅在穿越距離的過程中就會消散和散射,而如果碰到障礙物(即使只是樹上的樹葉也會成為遠端WiFi PtP連接無法克服的障礙)只會使ping-pong轉換更加困難。
令人高興的是,CC1101無線電解決了這些問題。如今,433MHz頻段很擁擠,因此對於充滿干擾和障礙物的現代城市環境來說,可以在~60m/197ft的範圍內實現非常可靠的數位傳輸已經很出色了。
閱讀本文後,您將會知道如何同時運行Arduino。
CC1101無線電通過SPI連接。
從Arduino 5V到3V3邏輯電平的SPI需要進行電平轉換,如果您不學會這一操作,硬體將會被燒毀。我打賭您一定會去學習的。
您還將需要學會一些實用的無線電公式,例如用於計算波長(用於天線)和波長頻率的相關公式。這使您可以輕鬆製作出所需的任何類型的天線。生活中普遍使用的天線是價格很高的銅線,這對您來說並不適合。
您還需要學習如何通過適度的置信度來消除輸入信號(來自觸覺開關/按鈕)的抖動。然而,這樣寫出來的代碼可能會非常複雜,一個非常好的替代方案是在一個ADC引腳上使用10K電位器。這非常快速(每10 uS讀取一次),而且也不需要用代碼去執行—如果您之前沒有瞭解過,現在就應該學會了。
當您的構建(有兩個)完成後,您將能夠在比WiFi所能覆蓋區域更大的範圍內傳輸和接收控制/遙測資料,以及對位元組流執行相關操作。“ArduinoGarageOpener_CC1101.ino”中的“magic_token”變數可以被安全地增加到32或64位元組,這似乎並沒有真正影響到傳輸錯誤率。
當您在代碼中擴展車庫基地台段時,您將會學習如何非常高效使用記憶體。
如果您的代碼因為定義的內容而停止運行,就說明您使用了太多的SRAM(atmega328p只有2kB),只需要通過Arduino串列監視器發送一個“foo!”就可以查看所剩記憶體。如果沒有得到任何資訊,請撤銷您最近的更改。
之後還將會提到H橋(用於改變直流馬達的極性/方向),如果您想要自己構建一個,就可以學習更多的相關內容去完成。為了激發您的這一想法,我將會在這裡簡單地展示並介紹一個H橋。它一直在我家裡的某個地方,但是我從來都沒關注過它。不過為了你,我會把它找出來。
如果您喜歡對Arduino埠寄存器進行快速、直接的操作,H橋將會非常有用。更多內容請參看第2部分!
2. Arduino車庫開關和通用無線電介面
車庫開關通過一個大且重的馬達進行操作。它們有各種形狀和尺寸,從很小的12V到近乎高壓的48V,甚至可以達到更高電壓。有趣的是,您打算如何與之交互?這取決於您自己。您需要打開盒子,看一下裡面的東西。
可以在兩個方向上運行的雙極馬達很容易獲取。只需卸下驅動電路,直到只剩下馬達、電源和盒子。
Arduino馬達驅動板(Rev3)不能用於這項任務(事實上應避免使用所有L298N系列)—您需要更強大的馬達驅動器,例如額定電壓為7-30V、10A的SHIELD-MD10。
或者構建您自己的H橋,並直接通過Arduino埠操作進行驅動。看起來可能會如下圖示意圖所示:
請參閱下面的埠操作說明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Atmega328p Ports register reference: Port D 1 2 3 4 5 6 7 8 -- -- -- -- -- -- -- -- D7 D6 D5 D4 D3 D2 D1 D0 Port B 1 2 3 4 5 6 7 8 --- --- --- --- --- --- --- --- D13 D12 D11 D10 D9 D8 Port C 1 2 3 4 5 6 7 8 -- -- -- -- -- -- -- -- A5 A4 A3 A2 A1 A0 |
以及我的一些示例代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// setup() // Set D8+D9+D10+D11+D13 as OUTPUTs DDRB = 0b00101111 ; // Set all bits in register LOW PORTB = 0b00000000 ; // ... // Functions that belong elsewhere void bridgeOFF( void ) { // Set all output pins LOW PORTB = 0b00000000 ; // Motor Spin-down grace period delay( 300 ) ; } void bridgeForward( void ) { bridgeOFF() ; // Forward, D8+D9+D13 HIGH, D13 is the onboard LED. PORTB = 0b00100011 ; } void bridgeReverse( void ) { bridgeOFF() ; // Reverse, D10+D10 HIGH, D13 is set LOW here. PORTB = 0b00001100 ; } |
這完全可行,而且非常便宜。但是,其中有一點搞錯了,純*oomph*的單次擊穿將會燒毀一條或兩條電氣路徑。H橋必需精確計時。
圖中的H喬如下圖所示,儘管它使用過時的BJT,但是性能完美無缺。本來已經弄丟了,但是我又找到了它。
馬達驅動器可以由您自己來選擇(提示:SHIELD-MD10!),而且馬達的選擇會因車庫設置而異。現在來完成Arduino車庫開關!
首先,如果您不熟悉Arduino IDE,請先閱讀這篇文章。
從Arduino官網下載Arduino IDE,並點擊此處瞭解如何安裝該庫。
我們需要的庫包含在這個zip檔中:ArduinoGarageOpener_CC1101。
您也可以在Github上流覽/下載該庫,或者直接使用“git”將其直接安裝到您的Arduino庫資料夾中,如下所示:
繼續!
我們的庫Arduino_CC1101包含兩個用於發送和接收指令的示例。
如果您發現主程序過於複雜,請參考這些示例,這些例子只是簡單地放在了一起。
我們有一個程式檔,ArduinoGarageOpener_CC1101.ino,它包含車庫開關和車庫基地台的代碼。這些行決定了您編譯的Arduino固件的功能。
如果您希望為手持設備編譯,將常數“IS_GARAGE_OPENE”設置為1,“IS_GARAGE_STATION”設置為0。
如果編譯車庫基地台,將“IS_GARAGE_STATION”設置為1,“IS_GARAGE_OPENER”設置為0。
按下CTRL+R t進行編譯,按CTRL+U進行(編譯和)上傳。程式和相關庫都可以在zip檔“ArduinoGarageOpener_CC1101”中找到。
1 2 3 4 |
// Handheld unit? #define IS_GARAGE_OPENER 0 // Garage base station? #define IS_GARAGE_STATION 1 |
Arduino Nano | https://www.newark.com/arduino/a000005/dev-board-atmega328-arduino-nano/dp/13T9275 |
CC1101 RF 模組 | https://www.elecrow.com/433mhz-rf-transceiver-cc1101-module-p-374.html |
ROHM SLR343BC4TT32 3mm LED | https://www.digikey.com/product-detail/en/rohm-semiconductor/SLR343BC4TT32/SLR343BC4TT32-ND/2337159 |
330ohm 電阻 | https://www.newark.com/multicomp-pro/mccfr0w4j0331a50/carbon-film-resistor-330-ohm-250mw/dp/58K5042 |
杜邦電線 | https://www.newark.com/multicomp-pro/mccfr0w4j0331a50/carbon-film-resistor-330-ohm-250mw/dp/58K5042 |
麵包板 | https://www.newark.com/multicomp-pro/mccfr0w4j0331a50/carbon-film-resistor-330-ohm-250mw/dp/58K5042 |
3.3+5V 電源 | https://www.newark.com/bud-industries/bbp-32701/breadboard-power-supply-5v-3-3v/dp/56AC7832 |
電平轉換器 | https://www.newark.com/adafruit/395/logic-level-converter-8ch-arm/dp/53W5916 |
按照下圖進行接線。
可以看出Fritzing圖示軟體的強大功能!雖然CC1101 8P接頭畫得很差(至少從圖像上看如此),但您還是一定要進行電平轉換。如果不這樣做,CC1101還是可以正常運行,但只能運行很短的時間。
代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
/* Constants from Arduino_CC1101/ELECHOUSE_CC1101.h: Name Pin Comment SCK_PIN 13 MUST LEVELSHIFT FROM Arduino 5V to CC1101 SPI 3V3! MISO_PIN 12 MOSI_PIN 11 MUST LEVELSHIFT FROM Arduino 5V to CC1101 SPI 3V3! SS_PIN 10 CSN/SS; MUST LEVELSHIFT FROM Arduino 5V to CC1101 SPI 3V3! GDO0 2 GDO2 9 */ // Handheld unit? #define IS_GARAGE_OPENER 0 // Garage base station #define IS_GARAGE_STATION 1 #if IS_GARAGE_OPENER // Interrupt/sleep related libraries, not used. //#include <avr/wdt.h> //#include <avr/interrupt.h> //#include <avr/sleep.h> //#include <avr/power.h> //const byte wakePin = 3 ; // INT1 #elif IS_GARAGE_STATION // Motor open/close drive times usually differ, // time both separately with a stopwatch app. #define motor_open_drive_time 100 // ms, change this #define motor_close_drive_time 100 // ms, change this #endif #include const byte buttonPin = 6 ; bool buttonState = LOW ; bool previousButtonState = LOW; unsigned long previousDebounceTime = 0 ; //unsigned long debounceDelay = 50; unsigned long debounceDelay = 100 ; const int magic_token_len = 6 ; // PING LIKE A SUBMARINE! byte magic_token[ magic_token_len ] = { "PING!" } ; const int input_buffer_len = 6 ; const byte ledPin = 4 ; bool is_garage_open = false ; // 0: No Serial.printing, except in serialEvent() // 1: Lots of useful information, such as received data, events etc. const bool debug = 1 ; void setup( void ) { Serial.begin( 115200 ) ; pinMode( ledPin, OUTPUT ) ; ELECHOUSE_cc1101.Init( F_433 ); // Frequency: 433MHz //ELECHOUSE_cc1101.Init( F_868 ) ; // Frequency: 868MHZ //ELECHOUSE_cc1101.Init( F_915 ) ; // Frequency: 915MHz if ( IS_GARAGE_STATION ) ELECHOUSE_cc1101.SetReceive() ; // Do listen else if ( IS_GARAGE_OPENER ) { // 10K pull-down from D6->GND, then D6->SW1->5V pinMode( buttonPin, INPUT ) ; } } void loop( void ) { // Begin Garage Opener if ( IS_GARAGE_OPENER ) { int read = digitalRead( buttonPin ) ; if( read != previousButtonState ) { // Reset the debouncing timer previousDebounceTime = millis() ; } if ( (millis() - previousDebounceTime ) > debounceDelay ) { if ( read != buttonState ) { buttonState = read ; // Still HIGH? if (buttonState == HIGH) { digitalWrite( ledPin, HIGH ) ; Serial.println( "[!] Sending magic_token three times ..." ) ; // Now send magic_token with moderate confidence for ( int it = 0 ; it < 3 ; it++ ) { ELECHOUSE_cc1101.SendData( magic_token, magic_token_len ) ; } delay( 50 ) ; digitalWrite( ledPin, LOW ) ; } } } previousButtonState = read ; } // End Garage Opener // Begin Garage Station else if ( IS_GARAGE_STATION ) { byte input_buffer[ input_buffer_len ] = { 0 } ; bool token_matched = false ; if ( ELECHOUSE_cc1101.CheckReceiveFlag() ) { int len = 0 ; len = ELECHOUSE_cc1101.ReceiveData( input_buffer ) ; if ( debug ) { Serial.print( F( "[!] RX data => " ) ) ; Serial.println( (const char *)input_buffer ) ; Serial.print( F( "[!] RX data length => " ) ) ; Serial.println( len, DEC ) ; } delay( 100 ) ; // Are magic_token and input_buffer identical? if( strncmp( (const char *)magic_token, (const char*)input_buffer, 2 ) == 0 ) { token_matched = true ; // If open, close // If closed, open // set is_garage_open accordingly } else token_matched = false ; // End token_match // Begin open/close actions if ( token_matched ) { if ( is_garage_open ) { if( debug ) Serial.println( F( "[!] Garage door CLOSE action!" ) ) ; // Drive closed ... is_garage_open = false ; digitalWrite( ledPin, LOW ) ; delay( motor_close_drive_time ) ; } else if ( ! is_garage_open ) { if ( debug ) Serial.println( F( "[!] Garage door OPEN action!" ) ) ; // Drive open ... is_garage_open = true ; digitalWrite( ledPin, HIGH ) ; delay( motor_open_drive_time ) ; } } // End open/close actions } ELECHOUSE_cc1101.SetReceive() ; // Do continue listening } } // Called if Arduino receives data over serial link void serialEvent( void ) { printFreeRAM() ; while ( Serial.available() > 0 ) Serial.read() ; } // Print free SRAM in bytes void printFreeRAM( void ) { extern int __heap_start, *__brkval ; int v ; v = (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval ) ; Serial.print( F( "[!] Free RAM => " ) ) ; Serial.println( v, DEC ) ; } |
通電後,基地台將開始監聽“magic_token”,即“PING!”。這可以是任何內容,這由您來決定。
當它接收到正確的位元組序列(目前由“P”、“I”、“N”、“G”、“!”的ASCII碼組成,由ASK調製傳輸)時,會觸發您所設定任何事件的“OPEN(打開)”/ “CLOSE(關閉)”,如SHIELD-MD10,一個H橋,或者繼電器。
“OPEN”和“CLOSE”操作存在延遲。可以假設打開和關閉車庫門分別需要兩個不同的時間段,而這就是造成延遲的原因。
改變常量“motor_open_drive_time”和“motor_close_drive_time”,直到找到合適的值為止。另一種方法是使用霍爾效應感測器,但是本文沒有涉及。
1 2 |
#define motor_open_drive_time 5000 // ms #define motor_close_drive_time 5000 // ms |
我們使用變數“is_garage_open”追蹤當前狀態,因此可以在車庫門打開時對其關閉,反之亦然。
它僅存儲在SRAM中,而不存儲在EEPROM中。
對於基地台來說特別重要的一點是,當您添加馬達驅動代碼時,一定要通過發送“foo!”到Arduino來關注串列監視器中的剩餘SRAM(CTRL+SHIFT+M)資訊。當前設置中有1823個位元組可用,但是如果您引用了一些功能比較強大的庫,這可能會變化得很快。
當觸發“OPEN”動作時,ROHM 3mm LED將會被點亮。如果變數“debug”為真,那麼您就可以通過Arduino串列監視器追動當前狀態,如下所示。
2.2 Arduino車庫開關,手持單元
按下按鈕,發送“magic_token”
2.2.1 BOM
Arduino Nano | https://www.newark.com/arduino/a000005/dev-board-atmega328-arduino-nano/dp/13T9275 |
CC1101 RF 模組 | https://www.elecrow.com/433mhz-rf-transceiver-cc1101-module-p-374.html |
ROHM SLR343BC4TT32 3mm LED | https://www.digikey.com/product-detail/en/rohm-semiconductor/SLR343BC4TT32/SLR343BC4TT32-ND/2337159 |
330ohm 電阻 | https://www.newark.com/multicomp-pro/mccfr0w4j0331a50/carbon-film-resistor-330-ohm-250mw/dp/58K5042 |
杜邦電線 | https://www.newark.com/multicomp-pro/mccfr0w4j0331a50/carbon-film-resistor-330-ohm-250mw/dp/58K5042 |
麵包板 | https://www.newark.com/multicomp-pro/mccfr0w4j0331a50/carbon-film-resistor-330-ohm-250mw/dp/58K5042 |
3.3+5V 電源 | https://www.newark.com/bud-industries/bbp-32701/breadboard-power-supply-5v-3-3v/dp/56AC7832 |
電平轉換器 | https://www.newark.com/adafruit/395/logic-level-converter-8ch-arm/dp/53W5916 |
10kOhm 電阻 | https://www.newark.com/arcol/mra0207-10k-b-15ppm-ta/res-10k-0-10-250mw-axial/dp/79Y4556 |
按鈕 | https://www.newark.com/adafruit/1119/tactile-switch-pcb-breadboard/dp/84X1201 |
我們使用與基地台相同的代碼,只是將常量“IS_GARAGE_OPENER”更改為1,將“IS_GARAGE_STATION”更改為0。
再一次,按照下圖接線。
暫態開關需要消除抖動。這在一定程度上取決於您的所處環境:如果電源放在一個經常振動的辦公桌上,那麼需要的就不僅僅是掌上型浮動電池供電單元。嘗試將變數“debounceDelay”設置為40-50ms。我們連接引腳D6 -> 按鈕 -> 5V,但在D6附近需要一個下拉電阻,像這樣:D6 -> 10kOhm -> GND。
將手持單元作為通用車庫開關(雖然它的功能更加強大)。如果使用Arduino Nano,您選擇的電池組可以很方便地作為USB移動電源。
如果您使用了Arduino Pro Mini 3V3(atmega168),那麼可以使用TP4056(+DW01)電池模組和3.7V鋰電池。如此一來就不需要用電平轉換器了。為了延長電池壽命,最好將Arduino睡眠模式設置為“LEEP_MODE_PWR_DOWN”。這取決於您。
當按下按鈕並且去抖延遲已經確認了按鈕在非抖動影響下確實被按下時,它將繼續通過CC1101無線電發送“magic_token”變數的內容。
如果您的基地台處在範圍內,並已通電,它將捕獲傳輸資訊,並執行“OPEN”/“CLOSE”操作。
最後,您的桌面上會有這些東西。
不要感到絕望,縮短跳線或者將麵包板分開會看起來好很多。但重點是測試其是否有效,以及效果是否非常好。
當然,範圍測試是使用一大串電線、手上的Arduino和CC1101以及由3V3導軌降壓的USB電池組供電來完成的,閃爍兩次,確認收到了“PING!”。
請記住,這兩個版本可以用作範本來通過無線電控制任何類型的電子設備—它不僅限於操作車庫門。如果您需要一種簡單的方法來發送遙測資料而不會弄亂您的Raspbian裝置,則可以在任一端使用其他硬體,如Raspberry Pis。該程式易於使用到任何類型的應用中。
現在去介面操作吧!