简介:这是一份基于LoRa模块探索网状网络组网方法的开源源码包,适合物联网开发者、嵌入式爱好者和从事无线自组网研究的技术人员。资源中包含多个用于测试T-Beam硬件功能的Arduino工程文件,既有发送与接收的基础示例,也有网关节点双工通信、随机数据上报、GPS坐标采集等进阶代码。同时提供Socket服务器与数据库对接的脚本,便于理解LoRa节点采集到的位置、跳数、时间等信息如何经过网关转发至后台服务。压缩包共34个文件,主要类型为.ino工程、Markdown说明文档以及少量C语言与Socket服务器代码,整体体积仅43KB,结构清晰,易于查阅。该资源已有1385人学习下载。借助这份源码,读者可以很快搭建最多8个T-Beam节点的实验网络,观察LoRa多跳路由与数据回传的完整流程,也能够在此基础上修改或改进路由算法,实际体验从物理层采集到服务端落地的系统闭环。
1. lora-mesh:在窄带无线下自建网状网络
在山区、矿区或农业大棚里,运营商网络覆盖不到,但你又需要把分散的温湿度、位移传感器数据汇到同一个网关。传统做法是每一颗节点拉一根线,或者用 4G 路由,但成本和功耗都受不了。LoRa 正好补上这块:单点能传 2 到 5 公里(空旷视距),接收电流可低到几百微安,缺点是速率只能到几十 kbps。如果节点数量再多一点,光靠星形结构就不够灵活了。lora-mesh 指的是用 LoRa 模块在没有中心网关的情况下,让每个节点既能收发数据,也能转发邻居的数据,形成一个多跳的网状网。这个项目源码的核心就是给出这套方案的实现骨架,里面怎么组包、怎么中继、怎么防广播风暴,都有最省代码的落地方式。不管你是做嵌入式、边缘网关还是物联网平台,都能直接拉下来改参数用。
2. LoRa 网状网原理与选型:为什么用扩频还得算清楚
2.1 LoRa 物理层能做什么,不能做什么
LoRa 调制采用 CSS 啁啾扩频,把低速比特扩展到整个信道带宽。常用带宽是 125kHz / 250kHz / 500kHz,扩频因子 SF 从 7 到 12。SF 每提高 1,比特率和灵敏度都变差变好:比如带宽 125kHz 下,SF7 的空中速率约 5.47 kbps,SF12 只有 0.29 kbps,但接收灵敏度会从 -123 dBm 降到 -137 dBm,也就是把信号从噪声里抠出来的能力更强。所以做 mesh 时,SF 是第一个要定的参数:如果节点之间距离近(百米级),优先 SF7 减小空口时间;如果中继距离想拉到 1 公里以上,就得换 SF10 或 SF12。
同时 LoRa 半双工、同频收发会碰撞。LoRa 支持 CAD(信道活动检测)和可变前导码,但不像 WiFi 那样有完整的 RTS/CTS 退避机制。网状网里多跳转发,必然要自己控制时分或跳数。
2.2 星形 vs 网状:选型看节点密度和链路预算
LoRaWAN 是承载在 LoRa 上的标准协议,它是星形结构:所有节点只盯网关。网关和节点之间可以隔 8 公里,但节点和节点之间不直接通信。如果要节点之间互传数据,比如设备到设备、边缘自组网,LoRaWAN 的设备也要通过网关中转,延迟变大。lora-mesh 选择网状,是因为它可以在没有基站的环境里工作。但网状网也有代价:中继意味着同一条消息会发多次,吞吐量被跳数成倍压缩。所以 mesh 的适用场景不是高速数据流,而是控制命令、周期状态、报警事件。
如果你有几十个节点且分布在 3 跳以内,mesh 是合理的。如果节点超过 200 个,建议把网络拆成几个簇,用簇头走主干中继。
2.3 硬件选型:SX1276 还是 LLCC68,以及带 MCU 的最小组合
常见的 LoRa 无线电芯片包括 SX1272 / SX1276 / LLCC68 和 SX1262。SX1276 便宜,SX1262 更省电、支持前导码检测,LLCC68 只支持 LoRa 但速率低。mesh 对信号质量要求高,我一般推荐 SX1276 配整个 PCB 天线,或者 SX1262 加外部天线座。真正决定系统复杂度的是 MCU:ESP32 自带 WiFi/蓝牙、RAM 大,调试方便;STM32L0 电流更低,但写路由逻辑要小心内存。lora-mesh 源码里用的是 ESP32 + SX1276,理由是串口调试方便、Flash 够大。
| 配置项 | 用途 | 推荐值 | 说明 |
|---|---|---|---|
| 带宽 | 信道带宽 | 125kHz | 越窄灵敏度越高,但空中时间变长 |
| 扩频因子 SF | 抗噪/速率权衡 | 7~10 | 近距用 7,远距用 10 |
| 编码率 CR | 前向纠错 | 4/5 | 提升抗干扰,减小吞吐 |
| 工作频率 | 频段 | 470MHz 或 868/915MHz | 按本地频谱法规选择 |
| 发射功率 | 发射功率 | +14 dBm 到 +20 dBm | 高功率会增加功耗 |
其中要注意:带宽和 SF 不能随意独立调,很多 LoRa 芯片要求 SF 与带宽匹配。LoRa 与 FSK 混合模式在单芯片中可以用,但 mesh 场景下我建议固定 LoRa 模式,减少兼容性排查。
2.4 网状网络的路由算法:泛洪和按需路由
网状网最常用的算法是泛洪(flooding)和动态源路由(DSR)。泛洪的优点是实现简单、拓扑变化自适应;缺点是每个包乘跳数放大。在跳数上限为 5 的网络里,泛洪放大倍率可达十几倍,所以必须配合去重表和跳数限制。DSR 则先发探测包找到路径再传数据,但 LoRa 的低速率让“先探测再传”变得非常慢,探测过程本身就可能被干扰。我一般的取舍是:节点少于 30 个用泛洪,多于 30 个或要求低时延的用分簇。
还可以用时间槽(TDMA)避免碰撞。但 TDMA 需要全网时钟同步,LoRa 没有内置同步机制,硬做同步会提高功耗,所以 lora-mesh 的源码优先选择载波侦听加随机退避。
3. 从零跑通 lora-mesh 的最小代码:发送、接收与转发
3.1 需要的基础环境和引脚连接
假设你已经准备两块 ESP32 开发板和一个 SX1276 模块,模块通过 SPI 连接。推荐引脚分配:
- CS -> GPIO5
- RST -> GPIO14
- DIO0 -> GPIO2
- MOSI/MISO/SCK 接硬件 SPI
接线完成后,在 Arduino IDE 里安装 RadioLib 库,也可以直接用原始 SX127x 驱动。这里以 RadioLib 为例,因为它把 CAD、setFrequency、send 等封装得更简洁。
3.2 最小发送端代码
#include <Arduino.h> #include <RadioLib.h> SX1276 radio = new Module(5, 14, 2); // CS, RST, DIO0 void setup() { Serial.begin(115200); int state = radio.begin(470.0); // 470 MHz if (state != RADIOLIB_ERR_NONE) { Serial.println(F("radio init failed")); while (true) {} } radio.setBandwidth(125.0); radio.setFrequency(478.0); // 工作频率 478 MHz radio.setSpreadingFactor(10); // 远距离 radio.setCodingRate(5); // 4/5 radio.setOutputPower(17); // 17 dBm } void loop() { uint8_t msg[16] = "hello-mesh"; int r = radio.transmit(msg, strlen((char*)msg)); if (r == RADIOLIB_ERR_NONE) { Serial.println("TX ok"); } else { Serial.println(r); } delay(10000); }这段代码把hello-mesh每 10 秒发一次。注意radio.begin(470.0)只是初始化,真正工作频率在下一行单独设置;RadioLib 允许中心频率和偏移分开,但为了直观,我这里直接用setFrequency覆盖。setSpreadingFactor(10)对应前面表格里的远距配置,代价是单包传输时间约 65ms(负载 16 字节)。
参数说明:带宽 125kHz,编码率 4/5,输出功率 17dBm。这里把 SF 设为 10,是为了在中等距离下得到更可靠的链路;如果你测试时发现丢包,再往上调 SF,不要无脑用 SF12。
3.3 接收端代码:加上 CAD 和转发逻辑
接收端不只收,还要转发,这里先写一个最简单的“收到就原样再发一次”的中继:
#include <RadioLib.h> SX1276 radio = new Module(5, 14, 2); void setup() { Serial.begin(115200); int state = radio.begin(470.0); radio.setFrequency(478.0); radio.setBandwidth(125.0); radio.setSpreadingFactor(10); radio.setCodingRate(5); radio.setOutputPower(17); state = radio.startReceive(); if (state != RADIOLIB_ERR_NONE) Serial.println(state); } void loop() { if (radio.available()) { uint8_t buf[64]; int len = radio.getPacketLength(); int state = radio.readData(buf, len); if (state == RADIOLIB_ERR_NONE) { Serial.printf("RX rssi=%d snr=%d\n", radio.getRSSI(), radio.getSNR()); radio.transmit(buf, len); // 转发,注意这里会导致自收 } } }这段代码已经具备接收和转发的雏形,但暴露了 mesh 的经典问题:转发后又会被其它节点接收,形成无限循环。所以真正的 mesh 必须在包结构里加跳数限制。
3.4 最小帧格式与路径代价
我建议在源码里用这样的帧头:目标地址(1 字节)、源地址(1 字节)、跳数计数字段(1 字节)、类型字段(1 字节)、载荷。类型字段可以区分数据帧、ACK、路由探测帧。转发前将 hop 字段加一,如果 hop 超过最大值(比如 5)就丢弃。
利用这个格式,3.3 节的转发函数要改成这样:
void route(uint8_t *buf, int len) { if (buf[2] >= MAX_HOP) { Serial.println("drop: hop exceed"); return; } buf[2]++; radio.transmit(buf, len); }注意此时要防止同一节点重复收到同一跳数的包,最简单的办法是维护一个最近收到的包序号表。在低速率下,这样的软件过滤足够应对 20 个节点的小型网络。
3.5 去重表与转发风暴抑制
为了防止同一帧被多跳重复转发,每节点维护一个最近包序号的环形表:
#define DEDUP_SIZE 256 uint16_t seen_idx = 0; uint32_t seen_seq[DEDUP_SIZE]; bool isDuplicate(uint16_t src, uint16_t seq) { // 用 src 和 seq 计算 hash uint32_t key = (src << 16) | seq; for (uint8_t i = 0; i < 16; i++) { int ix = (seen_idx - i + DEDUP_SIZE) % DEDUP_SIZE; if (seen_seq[ix] == key) return true; if (seen_seq[ix] == 0) break; } // 更新 seen_seq[seen_idx] = key; seen_idx = (seen_idx + 1) % DEDUP_SIZE; return false; }这段代码用环形表缓存最近 256 个包序号,检查重复时只看最后 16 条记录,很快。注意环形表把最老的记录覆盖,适合节点间包速率不高的情况。收到包后,在转发前调用isDuplicate(src, seq),返回true说明已经处理过,直接丢弃,否则进入路由。
4. 实战:让 lora-mesh 跑通一条多跳链路的两套配置
4.1 设备数量、信道和跳数上限怎么定
如果网络是 10 个节点以内、单跳覆盖 500 米,我建议先不要做复杂路由,用最简单的中继转发:约定某个节点为汇聚根,其他节点往里发。汇聚根不转发普通数据,只负责收上报。这样可避免大部分广播风暴。
如果节点数超过 10 且要互相通信,就要定跳数上限。跳数上限直接决定时延和可靠性:一跳大约 0.5 秒(在 SF10、64 字节负载下约 0.3 秒的空口时间),三跳就是 1.5 秒量级。所以 mesh 链路跳数不要超过 5,否则秒级延迟谁也受不了。
4.2 使用 lora-mesh 源码中预置的泛洪路由参数
项目源码有一个config.h,里面定义频道号、扩频因子、功率、最大跳数等。我一般会按下列模板调整:
#define MESH_FREQUENCY 478.0 #define MESH_BANDWIDTH 125.0 #define MESH_SPREADING_FACTOR 10 #define MESH_CODING_RATE 5 #define MESH_OUTPUT_POWER 17 #define MESH_MAX_HOP 5 #define MESH_NODE_ID 0x01代码里每个节点有唯一 ID,发送时把自己的 ID 填进源地址。接收时只处理目标地址为广播0xFF或等于自身节点的包;如果目标不是自己且 hop 没超,则转发。这个配置在 10 节点的测试网里,端到端投递率能到 90% 以上。
4.3 在源码基础上加 ACK 与重传的一种实现
数据包到达最终目标后应回一个 ACK。因为 LoRa 是半双工,ACK 必须延迟一段时间发送,比如接收后等待 200ms 再回。发送方设置超时 2 秒,没收到 ACK 就重传,最多重传 3 次。
void sendWithAck(uint8_t *msg, int len, uint8_t target) { for (int i = 0; i < 3; i++) { uint8_t frame[32] = {target, MESH_NODE_ID, 0, 0x01}; memcpy(frame + 4, msg, len); int r = radio.transmit(frame, len + 4); if (r != RADIOLIB_ERR_NONE) { Serial.println("tx fail"); return; } // 等待 ACK,pollRead 在收包后返回 ACK 类型 unsigned long t = millis(); while (millis() - t < 2000) { if (radio.available()) { uint8_t ack[16]; int n = radio.readData(ack, 16); if (n > 3 && ack[3] == 0x02 && ack[0] == MESH_NODE_ID) { Serial.println("ack ok"); return; } } delay(10); } delay(200 * (i + 1)); // 退避 } }这段代码增加了 ACK 和重传。关键一点:ACK 帧类型0x02、源地址写自己、目标地址写原发送方;跳数计数同样参与,所以 ACK 也能被中继,只是类型不同。当网络规模变大后,ACK 占用大量信道,可以通过批确认(多个数据帧共用一个 ACK)来降低负载。
4.4 使用中会遇到的三个坑
第一,开发板上电后先用串口看radio.begin()是否成功。经常是 SPI 引脚接错或 DIO0 中断未配置。第二,LoRa 模块对地平面敏感,天线旁边不要走电源线。第三,如果你同时测试多块板子,要把频率、SF、带宽统一,否则收不到包。
另外,LoRa 和 FSK 混合技术虽然芯片支持,但 mesh 源码里一般只开 LoRa,这样便于用一个调制参数覆盖所有包。
5. 调试 lora-mesh 的三个具体技巧,最后能把全网状态串起来
5.1 用 RSSI 和 SNR 判断中继是否该加
接收节点可以在 readData 后立刻打印这两个值。RSSI 低于 -110 dBm 时,要么调大功率,要么把链路断掉。SNR 为负值时说明信号在噪声底以下,LoRa 能解调但余量小,慎用。
5.2 用固定信道分离控制面和数据面
把控制包(探测、ACK)放在奇数号信道,数据包放偶数号信道,两个信道之间用跳频交叉。这听起来奢侈,但源码里只需要为每类包调setFrequency,一张表就能维护。代价是收发需要同步,所以我在实际使用中只对汇聚根做这个策略。
5.3 验证 mesh 收敛性的一分钟手测法
把三个节点按“A-B-C”直线摆放,A 发数据,C 收。用串口打印 A 的跳数计数,正常应等于 2;然后手动关掉 B,C 上应该出现超时或重传日志。这个测试能快速判断路由有没有缓存失效问题。对于“上次转发后不再继续广播”的 bug,检查源地址和序列号去重表是否在转发后清空。
最后一个小小的技巧:把跳数计数字段放进负载数组的第一个字节之前,可以直接在现有缓冲区上修改而不用整包翻转,减少一次拷贝。这样在低速率环境下也能节省几十微秒的 CPU 时间。如果你写的是纯 C 代码,还可以用一个volatile数组记录每个邻居最后收到的包序号,避免重复转发时报告假的丢包。
本文还有配套的精品资源,点击获取