4. LoRa点对点通信实验:从硬件到空中的完整链路

好,咱们进入第四章。这一章我打算带大家亲手搭建一个LoRa点对点通信链路。说白了,就是让两个ESP32通过SX1278模块互相喊话。你想想看,没有网关、没有服务器,两个节点就能在几公里外通信——这就是LoRa最迷人的地方。

我在做第一个LoRa项目时,就是从这个实验开始的。当时在实验室里,一个模块放在桌上,另一个拿到走廊尽头,看着串口打印出"Received: Hello World!",那种感觉,嗯,真的很爽。

4.1 硬件连接图:别小看这几根线

先看硬件。SX1278模块和ESP32的连接,我习惯用SPI接口。为什么?因为SPI速度快,而且LoRa芯片本身就是SPI从设备。

这里给出我常用的接线方案:

SX1278引脚 ESP32引脚 说明
NSS GPIO5 片选,低电平有效
SCK GPIO18 SPI时钟
MOSI GPIO23 主机输出,从机输入
MISO GPIO19 主机输入,从机输出
RST GPIO14 复位引脚
DIO0 GPIO2 中断引脚,用于接收完成标志
注意: 我曾经因为NSS引脚没接对,折腾了整整一个下午。SX1278的NSS必须由ESP32的GPIO控制,不能直接接地。否则模块永远处于选中状态,SPI通信会乱套。

供电方面,SX1278工作电压3.3V,千万别接5V。ESP32的3.3V输出电流足够驱动它。如果你用电池供电,记得加个100μF的电解电容滤波——我在项目中遇到过因为电源纹波导致通信距离缩短的情况。

4.2 SPI总线配置:时钟频率别贪快

SPI配置其实不难,但有个坑——时钟频率。SX1278的数据手册说最高支持10MHz,但我个人建议用1MHz左右。为什么?

你想想看,LoRa通信本身速率就很低(几kbps),SPI再快也快不到哪去。而且ESP32的SPI在高速下容易受干扰,尤其是飞线连接时。我试过用8MHz,结果偶尔出现寄存器读写错误,降到1MHz后稳如老狗。

代码示例:

#include <SPI.h>

#define SS_PIN   5
#define RST_PIN  14
#define DIO0_PIN 2

void setup() {
  Serial.begin(115200);
  
  // 配置SPI
  SPI.begin(18, 19, 23, 5);  // SCK, MISO, MOSI, SS
  SPI.setFrequency(1000000);  // 1MHz,稳定第一
  SPI.setDataMode(SPI_MODE0); // 模式0:极性0,相位0
  
  pinMode(SS_PIN, OUTPUT);
  digitalWrite(SS_PIN, HIGH); // 片选默认高电平
  
  pinMode(RST_PIN, OUTPUT);
  digitalWrite(RST_PIN, HIGH);
  
  Serial.println("SPI初始化完成");
}
小技巧: 如果你用ESP32的VSPI或HSPI,记得检查引脚映射。VSPI默认是MOSI=23、MISO=19、SCK=18,和上面代码一致。HSPI的引脚不同,别搞混了。

4.3 LoRa寄存器初始化:核心配置三步走

寄存器初始化是LoRa通信的灵魂。SX1278有上百个寄存器,但咱们只需要配置几个关键的。我把它总结为三步:

  1. 复位模块:拉低RST引脚10ms,再拉高。让芯片回到默认状态。
  2. 进入LoRa模式:写寄存器RegOpMode(地址0x01),把bit7置1。
  3. 配置射频参数:频率、扩频因子、带宽、编码率、输出功率。

代码实现:

// 寄存器操作函数
void LoRa_writeReg(uint8_t addr, uint8_t value) {
  digitalWrite(SS_PIN, LOW);
  SPI.transfer(addr | 0x80);  // 写操作,最高位置1
  SPI.transfer(value);
  digitalWrite(SS_PIN, HIGH);
}

uint8_t LoRa_readReg(uint8_t addr) {
  digitalWrite(SS_PIN, LOW);
  SPI.transfer(addr & 0x7F);  // 读操作,最高位清0
  uint8_t value = SPI.transfer(0x00);
  digitalWrite(SS_PIN, HIGH);
  return value;
}

void LoRa_init() {
  // 第一步:复位
  digitalWrite(RST_PIN, LOW);
  delay(10);
  digitalWrite(RST_PIN, HIGH);
  delay(10);
  
  // 第二步:进入LoRa模式
  LoRa_writeReg(0x01, 0x80);  // RegOpMode: LoRa模式,睡眠状态
  delay(100);
  
  // 第三步:配置射频参数
  // 频率:868MHz(欧洲频段,国内用CN470需调整)
  uint64_t freq = 868000000;
  uint64_t frf = (uint64_t)freq * 524288 / 32000000;
  LoRa_writeReg(0x06, (frf >> 16) & 0xFF);  // RegFrMsb
  LoRa_writeReg(0x07, (frf >> 8) & 0xFF);   // RegFrMid
  LoRa_writeReg(0x08, frf & 0xFF);          // RegFrLsb
  
  // 扩频因子SF=7,带宽125kHz,编码率4/5
  LoRa_writeReg(0x1D, 0x72);  // RegModemConfig1: BW=125kHz, CR=4/5
  LoRa_writeReg(0x1E, 0x74);  // RegModemConfig2: SF=7, 使能CRC
  
  // 输出功率:17dBm(约50mW)
  LoRa_writeReg(0x09, 0xFF);  // RegPaConfig: 最大功率
  LoRa_writeReg(0x4D, 0x87);  // RegPaDac: 开启+20dBm模式(慎用)
  
  // 进入待机模式
  LoRa_writeReg(0x01, 0x81);  // RegOpMode: LoRa模式,待机
  
  Serial.println("LoRa初始化完成");
}
核心要点: 扩频因子SF越大,灵敏度越高,但速率越慢。SF7速率约5kbps,SF12只有约300bps。我一般调试用SF7,实际部署用SF9或SF10——平衡速率和距离。

4.4 发送与接收数据:中断驱动更靠谱

发送和接收,我建议用中断方式。轮询虽然简单,但浪费CPU。LoRa模块的DIO0引脚在发送完成或接收到数据时会触发中断。

发送流程:

  1. 清空FIFO,写入要发送的数据
  2. 设置发送模式,启动发送
  3. 等待DIO0中断(或轮询RegIrqFlags)
  4. 检查发送完成标志

接收流程:

  1. 设置接收模式,等待数据
  2. DIO0中断触发,读取FIFO
  3. 检查CRC,提取有效数据

代码示例:

void LoRa_send(uint8_t *data, uint8_t len) {
  // 写入FIFO
  LoRa_writeReg(0x0D, 0x00);  // RegFifoAddrPtr: 从地址0开始写
  for (int i = 0; i < len; i++) {
    LoRa_writeReg(0x00, data[i]);  // RegFifo
  }
  LoRa_writeReg(0x22, len);  // RegPayloadLength
  
  // 启动发送
  LoRa_writeReg(0x01, 0x83);  // RegOpMode: LoRa模式,发送
  // 等待发送完成(实际项目中用中断)
  while (!(LoRa_readReg(0x12) & 0x08));  // RegIrqFlags: TxDone
  LoRa_writeReg(0x12, 0x08);  // 清除标志
  LoRa_writeReg(0x01, 0x81);  // 回到待机
}

void LoRa_receive() {
  LoRa_writeReg(0x01, 0x85);  // RegOpMode: LoRa模式,连续接收
  // 等待接收完成(实际项目中用中断)
  while (!(LoRa_readReg(0x12) & 0x40));  // RegIrqFlags: RxDone
  LoRa_writeReg(0x12, 0x40);  // 清除标志
  
  // 读取数据
  uint8_t len = LoRa_readReg(0x13);  // RegRxNbBytes
  LoRa_writeReg(0x0D, LoRa_readReg(0x10));  // RegFifoAddrPtr: 读取当前指针
  uint8_t buffer[256];
  for (int i = 0; i < len; i++) {
    buffer[i] = LoRa_readReg(0x00);
  }
  
  // 检查CRC
  if (LoRa_readReg(0x12) & 0x20) {  // PayloadCrcError
    Serial.println("CRC错误,数据丢弃");
  } else {
    Serial.print("收到数据:");
    Serial.write(buffer, len);
    Serial.println();
  }
}
避坑指南: 我曾经在发送后忘记清除中断标志,导致下一次发送一直卡在等待循环里。记住:每次中断触发后,一定要手动写RegIrqFlags清除对应位。

4.5 空中速率与距离测试:理论vs实际

好了,硬件通了,代码跑了,接下来就是最激动人心的部分——测试。

先看理论速率。LoRa的空中速率计算公式:

BitRate = SF × BW / (2^SF) × CR

其中SF是扩频因子,BW是带宽(Hz),CR是编码率(4/5=0.8)。

举个例子:SF=7,BW=125kHz,CR=4/5

BitRate = 7 × 125000 / 128 × 0.8 ≈ 5468 bps

嗯,你没看错,只有5kbps。但换来的是-130dBm的灵敏度,比WiFi强了30dB。

扩频因子 速率(bps) 灵敏度(dBm) 典型距离(开阔地)
SF7 5468 -123 2-3公里
SF9 1465 -129 5-8公里
SF12 293 -134 10-15公里

实际测试时,我建议这样操作:

  • 先近距离(10米)验证通信正常
  • 然后逐步拉远,每次增加100米
  • 记录RSSI(接收信号强度)和SNR(信噪比)
  • 当RSSI低于-120dBm时,基本就到极限了
经验之谈: 我在城市环境中测试SF12,实际距离只有理论值的1/3。建筑物遮挡太严重了。如果你在开阔地,比如湖边或楼顶,SF12跑10公里完全没问题。另外,天线高度影响巨大——我试过把天线从1米升到3米,距离直接翻倍。

最后说一句:LoRa点对点通信,核心是理解"速率换距离"这个trade-off。你追求距离,就得忍受低速率。反过来,想要高速率,就别指望传太远。这个道理,在后续的组网实验中会反复用到。

好,这一章就到这里。下一章咱们开始玩真正的LoRaWAN协议栈——那才是重头戏。