動SX1278 LoRa模塊:代碼整理與移植實戰(zhàn))
簡介一份面向STM32F103的LORA通信精簡代碼包整理自正點原子LORA例程作者去掉了屏顯等與通信無關(guān)的模塊只保留LORA收發(fā)核心代碼并做了重寫讓通信邏輯更加突出適合正在學(xué)習(xí)F103平臺無線通信或者需要快速把指定LORA模塊接入自己項目的開發(fā)者。壓縮包內(nèi)文件不多共5個——2個C源文件、2個H頭文件和1個txt說明文檔整個包僅11KB非常輕量其中C源文件負責(zé)數(shù)據(jù)收發(fā)、寄存器配置和底層驅(qū)動相關(guān)邏輯H頭文件給出對外接口與常用參數(shù)宏txt文檔則對代碼使用方式做了簡要說明整體結(jié)構(gòu)清晰幾乎不用再整理目錄按文件名稱即可快速定位。已有4164人學(xué)習(xí)關(guān)注度較高對有STM32基礎(chǔ)、希望快速完成LORA通信驗證的讀者來說很實用。拿到這份精簡代碼后不需要在大工程中篩選無關(guān)部分可直接對照說明文檔理解通信流程在此基礎(chǔ)上做二次修改同時也能借鑒作者在時鐘初始化、接口配置、收發(fā)狀態(tài)處理上的代碼組織方式縮短嵌入式無線項目的開發(fā)調(diào)試時間。整體來看是一份小而精、可直接上手的入門參考。 做LoRa無線透傳的時候經(jīng)常有朋友翻出正點原子的LoRa例程點開一看里面文件不少注釋也比較“原生態(tài)”直接往自己的F103工程里塞又覺得到處是沖突和冗余。我當(dāng)初整理這份正點原子LoRa代碼時就是沖著“拿來能用、能看懂、能改”這三個目標(biāo)去的。這篇博文就圍繞這份整理過的代碼把F103與LoRa模塊以SX1278為核心的驅(qū)動梳理、移植思路、調(diào)試經(jīng)驗和踩坑記錄都攤開講清楚。如果你手頭有正點原子的LoRa模塊或SX1278兼容模塊用的芯片是STM32F103系列不管是做環(huán)境監(jiān)測、遠程開關(guān)、還是小數(shù)據(jù)量透傳這篇內(nèi)容可以直接照著落地。即使你用的是其他STM32型號只要換一下SPI和GPIO的映射思路完全通用。1. 項目背景與整理思路1.1 LoRa在F103項目里解決什么問題項目里要用LoRa大多數(shù)是看中它兩點遠距離和低功耗。所謂“遠”指的是在同樣的發(fā)射功率下LoRa的靈敏度比FSK、OOK這類傳統(tǒng)調(diào)制方案高很多SX1278的靈敏度能做到-137dBm左右配合合適的擴頻因子和帶寬視距環(huán)境下幾公里很常見。所謂“低功耗”是因為LoRa的接收電流也只有10mA上下對電池供電的采集節(jié)點很友好。至于為什么偏偏是F103原因更簡單這是最普及的Cortex-M3芯片資料多、庫存足、價格便宜。做傳感器采集、繼電器控制這類產(chǎn)品F103的72MHz主頻和豐富外設(shè)綽綽有余搭配LoRa模塊做一個低成本的遠距離鏈路是很多工程師的第一選擇。1.2 原廠代碼為什么要“整理”正點原子提供的LoRa例程核心功能是能跑的但代碼結(jié)構(gòu)偏教學(xué)風(fēng)格有幾個明顯問題所有功能堆在幾個大文件里沒有按驅(qū)動層、應(yīng)用層劃分。寄存器操作和業(yè)務(wù)邏輯混在一起想改成自己的協(xié)議棧得先花時間理清哪些是初始化、哪些是收發(fā)。錯誤處理和狀態(tài)判斷分散發(fā)送和接收的“握手流程”不夠直觀仿照它寫應(yīng)用代碼容易漏掉關(guān)鍵分支。注釋雖然多但更適合“演示”不適合直接裁剪到產(chǎn)品里。整理的目標(biāo)不是重寫驅(qū)動而是把SX1278這條主線抽出來形成清晰的三層結(jié)構(gòu)硬件抽象層SPI和GPIO操作、SX1278驅(qū)動層寄存器讀寫、初始化、收發(fā)狀態(tài)機、應(yīng)用層封裝成Send/Receive事件通知。這樣無論你是想快速驗證通信還是在上面跑自定義協(xié)議都有清晰的切入點。2. 核心器件與通信機制解析2.1 SX1278與F103的連接關(guān)系先看模塊連接。正點原子LoRa模塊把SX1278以及必要的匹配電路、天線座都做好了對外引出SPI接口、復(fù)位引腳、DIO0部分模塊還有DIO1/DIO2和供電引腳。F103這邊用硬件SPI1就能驅(qū)動引腳分配通常是功能F103引腳說明NSSPA4SPI片選軟件控制SCKPA5SPI時鐘MOSIPA7主機輸出連接模塊SIMISOPA6主機輸入連接模塊SORSTPA3模塊復(fù)位低電平有效DIO0PA2發(fā)完成/收完成中斷引腳這里特別提醒一個點正點原子有些例程里片選用的是軟件拉低拉高方式?jīng)]問題但要注意你選的GPIO是否與板子上其他外設(shè)沖突尤其如果工程里還掛了SD卡、LCD等外設(shè)它們可能都要搶占SPI1的引腳。這種情況下要么改用SPI2要么用GPIO模擬SPI驅(qū)動層做好接口分離改起來會容易很多。2.2 寄存器讀寫與時序關(guān)鍵點SX1278是典型的SPI從設(shè)備寄存器地址8位數(shù)據(jù)8位。讀取時把NSS拉低先發(fā)送寄存器地址最高位置1表示讀然后連續(xù)讀數(shù)據(jù)寫入時先發(fā)送地址最高位為0再寫數(shù)據(jù)。代碼里對應(yīng)兩個基礎(chǔ)函數(shù)// 寫寄存器addr為8位寄存器地址value為要寫入的數(shù)據(jù) static void SX1278_WriteReg(uint8_t addr, uint8_t value) { LORA_NSS_LOW(); SPI1_ReadWriteByte(addr 0x7F); // 寫命令最高位為0 SPI1_ReadWriteByte(value); LORA_NSS_HIGH(); } // 讀寄存器返回該地址的當(dāng)前值 static uint8_t SX1278_ReadReg(uint8_t addr) { uint8_t value; LORA_NSS_LOW(); SPI1_ReadWriteByte(addr | 0x80); // 讀命令最高位為1 value SPI1_ReadWriteByte(0x00); LORA_NSS_HIGH(); return value; }實際調(diào)試中很多人遇到“寫進去讀出來不對”的情況大多數(shù)不是函數(shù)問題而是SPI時鐘相位極性和速率設(shè)置不對。SX1278的SPI模式要求CPOL0、CPHA0也就是空閑時時鐘低電平第二個邊沿采樣。如果你的初始化函數(shù)里把SPI配置成Mode 3寄存器讀寫會時好時壞而且很難定位因為初始化那幾步接收狀態(tài)居多問題往往延遲到通信時才暴露。2.3 LoRa配置參數(shù)理解LoRa關(guān)鍵參數(shù)其實就那么幾個載波頻率、擴頻因子SF、信號帶寬BW、編碼率CR、發(fā)射功率、前導(dǎo)碼長度、同步字。整理代碼時我把這些參數(shù)全部集中在lora_config.h里方便一處修改而不是散落在初始化函數(shù)里。參數(shù)常見值對性能影響頻率433MHz / 470MHz / 868MHz不同地區(qū)合法頻段不同買模塊時要注意擴頻因子SF7~12越大靈敏度越高、傳輸更遠但速率降低信號帶寬BW125kHz / 250kHz / 500kHz越大速率越高但靈敏度下降編碼率CR4/5 ~ 4/8越高抗干擾越強但有冗余開銷發(fā)射功率最大20dBm功率越大越耗電不是所有模塊都能跑滿功率展開講一下擴頻因子這個最容易讓人糾結(jié)的參數(shù)。SF7相比SF12有效數(shù)據(jù)率能差出好幾倍但SF12在有干擾和多徑的場景下接收靈敏度更好。拿田間地頭的環(huán)境來說對速率不敏感但希望少用中繼那SF12配合125kHz帶寬是很穩(wěn)的搭配如果是樓宇抄表、需要在幾百米內(nèi)把采集數(shù)據(jù)按時上報那SF9/SF10相對均衡。我平時測試第一版固件固定用SF10、125kHz、CR4/5、發(fā)射功率14dBm先把鏈路跑通再看余量有需要才調(diào)。3. 核心代碼模塊與移植要點3.1 整理后的代碼目錄結(jié)構(gòu)整理后的代碼不再是正點原子那份“堆砌式”結(jié)構(gòu)而是這樣分文件Project/ ├── lora/ │ ├── sx1278.c │ ├── sx1278.h │ └── lora_config.h ├── app/ │ ├── lora_app.c │ └── lora_app.h └── bsp/ ├── spi1.c └── gpio.csx1278.c負責(zé)純驅(qū)動寄存器讀寫、芯片初始化、進入待機/發(fā)射/接收模式、讀取IRQ標(biāo)志。lora_app.c則負責(zé)具體應(yīng)用決策比如啟動一次發(fā)送、收到一幀數(shù)據(jù)后怎么處理、超時重發(fā)策略。bsp目錄里是硬件初始化代碼。這樣拆完以后如果你用的是標(biāo)準(zhǔn)外設(shè)庫那sx1278.c根本不用動換芯片平臺只需要改bsp和配置頭文件。3.2 初始化流程的整理邏輯SX1278初始化流程我建議按下面順序走復(fù)位RST引腳拉低10ms再拉高。讀取RegVersion寄存器確認SPI通信正常芯片型號正確。設(shè)置RF參數(shù)頻率、擴頻因子、帶寬、編碼率等。設(shè)置收發(fā)模式需要的寄存器前導(dǎo)碼、同步字、CRC開關(guān)。配置DIO0映射到發(fā)送完成和接收完成中斷。進入待機模式。這里面第一步和第二步很多人會省略但恰恰是它們能幫你區(qū)分“硬件沒焊好”和“軟件配置不對”。整理代碼時我把版本號讀出來后在串口打印一行類似“SX1278 detected, version 0x12”上電就能看到模塊是否正常響應(yīng)。別小看這一句日志前期聯(lián)調(diào)省的時間比寫整段驅(qū)動還多。初始化代碼核心片段大致是uint8_t SX1278_Init(void) { // 復(fù)位模塊 LORA_RST_LOW(); HAL_Delay(20); LORA_RST_HIGH(); HAL_Delay(20); // 檢查SPI讀寫是否正常 uint8_t version SX1278_ReadReg(SX1278_REG_VERSION); if (version ! 0x12) { return 1; // 返回非0表示初始化失敗 } // 配置LoRa模式 SX1278_WriteReg(SX1278_REG_OP_MODE, SX1278_MODE_SLEEP); SX1278_WriteReg(SX1278_REG_OP_MODE, SX1278_MODE_STANDBY); // 設(shè)置載波頻率以433MHz為例 SX1278_SetFrequency(433000000UL); // 設(shè)置調(diào)制參數(shù) SX1278_WriteReg(SX1278_REG_MODEM_CONFIG_1, (SX1278_BW_125KHZ 4) | (SX1278_CR_4_5 1)); SX1278_WriteReg(SX1278_REG_MODEM_CONFIG_2, (SX1278_SF_10 4) | (SX1278_CRC_ON 2)); // 設(shè)置同步字默認0x12國內(nèi)模塊一般不改 SX1278_WriteReg(SX1278_REG_SYNC_WORD, 0x12); // 設(shè)置發(fā)射功率14dBm比較穩(wěn)妥 SX1278_SetTxPower(14); // 配置DIO0為發(fā)送完成/接收完成中斷 SX1278_WriteReg(SX1278_REG_DIO_MAPPING_1, 0x00); return 0; }3.3 發(fā)送與接收狀態(tài)機LoRa發(fā)送的過程比大家想的要簡單因為SX1278內(nèi)部會自行完成數(shù)據(jù)加前導(dǎo)碼、CRC計算、調(diào)制發(fā)送的流程單片機只需要把數(shù)據(jù)填進FIFO然后觸發(fā)發(fā)送再等DIO0中斷通知即可。發(fā)送路徑整理完基本是這個樣子uint8_t SX1278_SendPacket(uint8_t *buf, uint8_t len) { // 等待上一次發(fā)送或接收動作徹底結(jié)束 SX1278_WriteReg(SX1278_REG_OP_MODE, SX1278_MODE_STANDBY); // 寫FIFO指針 SX1278_WriteReg(SX1278_REG_FIFO_TX_BASE_ADDR, 0x00); SX1278_WriteReg(SX1278_REG_FIFO_ADDR_PTR, 0x00); // 寫入一幀數(shù)據(jù) SX1278_WriteReg(SX1278_REG_PAYLOAD_LENGTH, len); for (uint8_t i 0; i len; i) { SX1278_WriteReg(SX1278_REG_FIFO, buf[i]); } // 切換為發(fā)送模式寫入該寄存器自動觸發(fā)發(fā)送 SX1278_WriteReg(SX1278_REG_OP_MODE, SX1278_MODE_TX); return 0; }發(fā)送完成標(biāo)志和接收完成標(biāo)志都會映射到DIO0引腳上在中斷服務(wù)函數(shù)里讀取IRQ標(biāo)志位來區(qū)分是收發(fā)哪種事件void EXTI_IRQHandler(void) { if (LORA_DIO0_READ()) { uint8_t irq SX1278_ReadReg(SX1278_REG_IRQ_FLAGS); if (irq SX1278_IRQ_TX_DONE) { SX1278_ClearIRQ(SX1278_IRQ_TX_DONE); // 通知應(yīng)用層發(fā)送完成 lora_event | LORA_EVENT_TX_DONE; } else if (irq SX1278_IRQ_RX_DONE) { SX1278_ClearIRQ(SX1278_IRQ_RX_DONE); SX1278_ReadPacket(rx_buf, rx_len); // 通知應(yīng)用層收到數(shù)據(jù) lora_event | LORA_EVENT_RX_DONE; } } }接收側(cè)的FIFO讀取同樣要先把讀指針定位到當(dāng)前接收起始地址void SX1278_ReadPacket(uint8_t *buf, uint8_t *len) { uint8_t current SX1278_ReadReg(SX1278_REG_FIFO_RX_CURRENT_ADDR); SX1278_WriteReg(SX1278_REG_FIFO_ADDR_PTR, current); *len SX1278_ReadReg(SX1278_REG_RX_NB_BYTES); for (uint8_t i 0; i *len; i) { buf[i] SX1278_ReadReg(SX1278_REG_FIFO); } }這里有一個細節(jié)接收完成和發(fā)送完成的判斷不能只靠DIO0引腳的高低電平因為不同模式映射不同。建議在中斷里讀RegIRQFlags用IRQ標(biāo)志位判斷避免DIO0在低功耗模式下出現(xiàn)誤觸發(fā)。4. 實操過程與測試調(diào)優(yōu)4.1 從串口到無線的最小驗證流程拿到整理好的代碼我習(xí)慣先做一個“串口轉(zhuǎn)發(fā)”的最小驗證板子A接串口串口發(fā)什么LoRa就發(fā)什么板子B收到LoRa數(shù)據(jù)后發(fā)到本地串口打印。這個方案能在一小時內(nèi)驗證整條鏈路分為三步兩塊F103都燒錄同一份工程工程里包含LoRa驅(qū)動和串口收發(fā)邏輯。串口波特率配置為115200LoRa參數(shù)配置一致頻率、SF、BW、同步字必須相同。把板子A的串口接到電腦USB轉(zhuǎn)TTL板子B的串口也接到電腦兩個串口助手互發(fā)消息。驗證通過后再開始改自己的應(yīng)用層協(xié)議。比如在發(fā)送的數(shù)據(jù)前加一字節(jié)“目標(biāo)地址”接收端判斷地址匹配才處理這就是最簡單的點對點協(xié)議原型。4.2 距離和丟包率調(diào)優(yōu)實測收到“距離不夠”這類反饋時我第一反應(yīng)是先別動代碼檢查天線。SX1278模塊上用的天線如果是彈簧天線對安裝環(huán)境極其敏感周圍有大面積金屬或者緊貼地平面駐波比會變差距離衰減非常嚴重。把手持模塊離開金屬桌面50cm以上往往距離翻倍。排除天線因素后再討論調(diào)參。拿我一個實測數(shù)據(jù)舉個例子兩塊F103配SX1278天線相同位置固定在一個室內(nèi)樓宇的走廊兩端直線距離約280米中間有兩堵剪力墻。使用SF7、125kHz、發(fā)射功率14dBm時丟包率大約15%把SF從7改為10其他不變丟包率降到接近0。代價是理論空速從約5.5kbps降到約1.4kbps但對溫濕度采集這種低頻上報場景來說完全夠用。整理代碼時我專門在lora_config.h里留下了“調(diào)試模式”開關(guān)。打開后會通過串口把當(dāng)前設(shè)置和每次收發(fā)的空速估算值打印出來方便在現(xiàn)場快速對照參數(shù)調(diào)整效果。4.3 低功耗考慮與配置如果你的節(jié)點是電池供電不能一直開著接收。標(biāo)準(zhǔn)的做法是定期喚醒節(jié)點先進入睡眠模式定時器每隔幾秒喚醒一次切換為接收模式并持續(xù)一段時間沒收到指令就再次入睡。SX1278本身的睡眠電流在0.2uA級別但F103的睡眠電流取決于低功耗模式配置。我在整理代碼時保留了RTC喚醒的接口。節(jié)點默認進入STOP模式RTC定時器到達后喚醒然后LoRa模塊從Sleep切到Standby再切到RX監(jiān)聽一個預(yù)設(shè)的時間窗口比如500ms如果收到指令就處理沒收到就重新睡下去。這套邏輯在lora_app.c里封裝成Lora_NodeSleep()和Lora_NodeListen()做電池類產(chǎn)品時可以直接調(diào)用。低功耗調(diào)試有一個比較容易踩的坑F103進入STOP模式前要把SPI的NSS引腳拉高否則模塊可能誤以為片選拉低在接收模式下白耗電。另外DIO0引腳要配置為外部中斷輸入并且打開上升沿觸發(fā)這樣才能在模塊收到數(shù)據(jù)時把MCU從STOP模式喚醒。5. 常見問題與排查技巧實錄5.1 雙方都收不到數(shù)據(jù)這是最常遇到的情況。按我平時的排查順序優(yōu)先級從高到低依次是排查項操作現(xiàn)象說明SPI通信是否正常讀RegVersion正常應(yīng)為0x12如果讀到0xFF多半是SPI時序、引腳定義或模塊供電異常頻率是否一致雙方打印當(dāng)前頻率偏差哪怕幾十kHz也可能導(dǎo)致收不到同步字是否一致確認雙方RegSyncWord相同很多模塊出廠默認0x12如果被改過而代碼里沒同步必然收不到CRC是否一致確認雙方都開或者都關(guān)發(fā)送端開CRC接收端關(guān)CRC會導(dǎo)致接收中斷不觸發(fā)DIO0是否接對用示波器或邏輯分析儀看DIO0在收發(fā)時有無脈沖如果硬件沒接DIO0接收完成通知不會觸發(fā)其中SPI通信不正常是翻車最多的。常見原因有三個一是SPI用的不是硬件SPI而是GPIO模擬但時序?qū)戝e了二是STM32的SPI初始化配置成了Mode 3三是模塊的MOSI/MISO接反了。Module上標(biāo)注的SI對應(yīng)MCU的MOSISO對應(yīng)MCU的MISO別搞反。5.2 距離短到只有十幾米先懷疑天線再懷疑射頻參數(shù)接著懷疑電源。SX1278的PA供電引腳VCC_RF通常需要單獨供電不要和邏輯供電用同一個LDO還能吃掉瞬態(tài)電流。如果模塊的VCC_RF輸入端沒有加鉭電容或大容量陶瓷電容發(fā)射功率上去后電壓跌落嚴重距離自然上不去。還有一個容易忽略的點發(fā)射功率設(shè)置值可能超出模塊實際承受范圍。正點原子LoRa模塊最大支持20dBm但如果模塊在板上的散熱條件不好連續(xù)長時間大功率發(fā)射芯片溫度升高后發(fā)射功率下降距離也會縮水。我一般測試距離時把發(fā)射功率設(shè)為14~17dBm留出余量也避免頻繁觸發(fā)過溫保護。5.3 數(shù)據(jù)出現(xiàn)亂碼或錯幀LoRa本身帶CRC正常情況下不會出現(xiàn)“隨機錯幾個字節(jié)”的情況。如果頻繁出現(xiàn)亂碼重點查兩件事數(shù)據(jù)長度字段是否匹配。發(fā)送時RegPayloadLength寫的是長的那一包接收端讀RegRxNbBytes得到的是實際接收長度如果兩邊長度處理邏輯不對稱FIFO讀出來自然錯位。FIFO讀指針是否正確。很多問題出在沒把FIFO_ADDR_PTR設(shè)置成當(dāng)前接收地址就開讀直接讀到緩存區(qū)歷史數(shù)據(jù)。另外提醒一下LoRa數(shù)據(jù)包的“最大長度”取決于寄存器配置中的Payload Length設(shè)置和FIFO大小SX1278是256字節(jié)。如果你的一幀業(yè)務(wù)數(shù)據(jù)超過255字節(jié)要么拆包要么用連續(xù)模式分塊傳輸不要把整幀塞進去否則模塊會拒絕發(fā)送或者只發(fā)前一段。5.4 與正點原子其他例程的比照如果你拿到的整理代碼是基于正點原子官方例程改的注意正點原子F103板子上的PA2/PA3通常已經(jīng)焊了串口跳帽或接了CH340芯片而LoRa模塊的DIO0如果恰好也復(fù)用這兩個引腳請先確認板子上的外設(shè)沖突。我見過不止一次有人把DIO0接到PA3上同時又開著一路串口打印結(jié)果模塊接收完成中斷一直被串口引腳電平干擾。整理代碼時最好先查原理圖給LoRa外設(shè)分配一組不和其他板載外設(shè)打架的引腳。6. 寫在最后的幾點體會這套代碼整理完我自己又從里面拆了幾份變體出來一份做成了RTC低功耗節(jié)點一份做成了帶RSSI信號強度回傳的巡檢測試工具。改動最多的反而不是SX1278驅(qū)動而是lora_app.c里的業(yè)務(wù)狀態(tài)機。這恰恰說明驅(qū)動層整理得足夠干凈應(yīng)用層才有條件隨便折騰。如果你打算把這個LoRa鏈路用在產(chǎn)品上我再提三個建議頻率參數(shù)留成可配置項最好在應(yīng)用層通過串口命令修改并存入Flash。同一片板子發(fā)到不同地區(qū)運營商或者客戶對頻段有不同要求硬件不用改改配置文件就行。每一幀都預(yù)留2~4字節(jié)的擴展字段用來放序列號、電源電壓、RSSI等狀態(tài)信息。簡單報站功能可以用后期排查干擾想拉數(shù)據(jù)看的時候這些字段能救急。接收端如果總是最后一個節(jié)點在保持接收模式會一直消耗電流能設(shè)計成ACK確認機制就盡量加。LoRa天然有“發(fā)完才能收”的時分特性把ACK放在發(fā)送完成后的空閑窗口里配合超時重發(fā)鏈路可靠性會有一個明顯提升。整理代碼這件事表面上是在移動文件、拆分函數(shù)實際上是在建立自己對硬件行為的掌控感。SX1278這款芯片成熟穩(wěn)定F103又是多年的老朋友把它們之間的代碼整理順了后面加功能、做變體都會順手很多。本文還有配套的精品資源點擊獲取