LoRa自学笔记02:双机驱动 SX1278(安信可 Ra-01)实战
硬件:两块 STM32 开发板 + 两个安信可 Ra-01 模块(SX1278,433MHz),我手头只有一块103和407
工程:标准外设库、Keil MDK 可直接打开编译
实现:双机互发互收、ACK 应答、串口打印 RSSI/SNR 信号质量本文从 LoRa 调制原理讲起,一直到寄存器级驱动实现和一个真实踩过的时序 bug,适合想彻底搞懂 SX1278 驱动的读者。
一、最终效果
两块板子上电后,两个串口助手分别看到:

发送和应答严格交替、一包不丢——双向链路完全打通。
二、先搞懂原理:LoRa 到底是什么
2.1 LoRa 与 SX1278 的关系
很多人会把这两个词混用,其实它们是两个层面的东西:
-
LoRa 是一种物理层调制技术(Chirp Spread Spectrum,线性调频扩频,简称 CSS),由 Semtech 公司持有专利;
-
SX1278 是 Semtech 生产的射频收发芯片,支持 LoRa 调制,也兼容传统的 FSK/GFSK/OOK 调制;
-
Ra-01 是安信可(Ai-Thinker)用 SX1278 做的邮票孔贴片模块,把芯片、晶振(32MHz TCXO)、射频匹配电路、天线接口都集成好了,只对外引出 SPI 和电源引脚。
所以我们的分工是:STM32 通过 SPI 读写 SX1278 的寄存器来配置和收发数据,无线电部分完全由模块自己完成。
2.2 CSS 扩频:LoRa 为什么传得远
传统 FSK 调制靠"频率高低"表示 0 和 1,信号能量集中在一个很窄的频点上,噪声稍大就解调不出来。
LoRa 的 CSS 调制则不同:它把每一个符号(Symbol)的能量摊开到一个较宽的频带上,用"频率随时间线性扫动"的 Chirp(啁啾)信号来编码信息。接收端用相关运算把散布在宽带内的能量重新"聚拢",因此即使信号淹没在噪声之下(SNR 为负)也能解调出来。
带来的直接好处:
| 指标 | 传统 FSK 模块 | LoRa |
|---|---|---|
| 接收灵敏度 | 约 -110 dBm | 最低约 -148 dBm |
| 可解调信噪比 | SNR > 0 dB | SNR 低至 -20 dB |
| 空旷传输距离 | 几百米 | 2~10 公里 |
2.3 三个关键射频参数:SF、BW、CR
这是玩 LoRa 必须理解的三个旋钮,它们之间存在"距离与速度"的权衡:
① 扩频因子 SF(Spreading Factor,6~12)
-
每个符号携带 SF 个 bit。SF 越大,符号时间越长,接收灵敏度越高、传得越远,但速率越低;
-
SF 每加 1,符号时间翻倍,大约增加 2.5dB 链路预算;
-
不同 SF 的信号在空中近似正交,SF7 和 SF9 的设备互相收不到(实际有一定互扰,配置时必须一致)。
② 带宽 BW(7.8k ~ 500kHz,常用 125kHz)
-
Chirp 扫频占用的频带宽度。BW 越小,灵敏度越高、速率越低;
-
常用 125kHz 是距离与速率的平衡点。
③ 编码率 CR(4/5 ~ 4/8)
-
前向纠错冗余比例。CR 越大抗突发干扰越强,但有效速率越低;
-
一般用 4/5 即可。
符号时间与空中耗时(以本工程的 SF7 + BW125kHz 为例):
符号时间 Tsym = 2^SF / BW = 2^7 / 125000 = 1.024 ms
一包 25 字节的数据(含 8 符号前导码)空中耗时约 60~100 ms
直观对比几种典型配置(每包 25 字节):
| 配置 | 速率约 | 空中耗时约 | 特点 |
|---|---|---|---|
| SF7 + 500kHz | 22 kbps | 15 ms | 最快,距离最短 |
| SF7 + 125kHz(本工程) | 5.5 kbps | 70 ms | 平衡之选 |
| SF10 + 125kHz | 0.98 kbps | 400 ms | 距离明显增加 |
| SF12 + 125kHz | 0.29 kbps | 1.5 s | 极限距离 |
2.4 LoRa 设备之间"对上话"的条件
两个 SX1278 设备要互相收到,以下参数必须完全一致:
- 载波频率(433MHz)
- 扩频因子 SF
- 带宽 BW
- 编码率 CR
- 同步字 SyncWord(0x12 私有网络 / 0x34 公有 LoRaWAN 网络)
- CRC 开关状态
- IQ 极性(默认非反转)
注意:LoRa 没有"配对/绑定"概念,参数一致就能互相收到,这也是它可以灵活组网的原因。
三、硬件:Ra-01 模块与接线
3.1 Ra-01 模块关键指标
| 项目 | 参数 |
|---|---|
| 芯片 | Semtech SX1278 |
| 频段 | 410~525 MHz(433MHz 版本) |
| 接口 | SPI(最高 10MHz) |
| 供电 | 3.3V(严禁 5V) |
| 最大发射功率 | +20 dBm(PA_BOOST 引脚) |
| 接收灵敏度 | 最低约 -139 dBm |
| FIFO | 256 字节 |
3.2 引脚说明
| 引脚 | 作用 | 是否必须 |
|---|---|---|
| 3.3V / GND | 电源 | 必须 |
| SCK / MOSI / MISO | SPI 三线 | 必须 |
| NSS | SPI 片选(低有效) | 必须 |
| RST | 硬件复位(低有效) | 必须 |
| DIO0 | 收发完成标志输出 | 必须(本方案用) |
| DIO1~DIO5 | 其他中断映射(CAD、FHSS 等) | 本方案不用 |
3.3 接线表(两套板子引脚完全相同)
| Ra-01 | STM32F103C8T6 | STM32F407ZGT6 | 说明 |
|---|---|---|---|
| 3.3V | 3.3V | 3.3V | 严禁接 5V |
| GND | GND | GND | 共地 |
| NSS | PA4 | PA4 | 软件片选,推挽输出 |
| SCK | PA5 | PA5(AF5) | SPI1_SCK |
| MOSI | PA7 | PA7(AF5) | SPI1_MOSI |
| MISO | PA6 | PA6(AF5) | SPI1_MISO |
| RST | PB0 | PB0 | 复位,推挽输出 |
| DIO0 | PB1 | PB1 | 上拉输入,查询方式 |
调试串口(两套板子相同):USART1,PA9(TX) → USB 转 TTL 的 RX,115200 8N1。
为什么两套板子用同样的引脚? SPI1 在 F103 和 F407 上都是 PA4~PA7 这组引脚,只是 F407 需要额外配置复用功能 AF5。这样移植时驱动逻辑完全不变,只改底层初始化。
天线必装:不装天线时两个模块即使贴着也可能收不到;天线频段必须是 433MHz(用 868/915MHz 天线会严重失配)。另外两个模块不要贴在一起(<20cm 信号过强可能接收异常),测试距离 1 米以上为宜。
四、工程配置详解
4.1 目录结构
STM32F103LoRaTest0816 / STM32F407LoRaRecieve0816
├── Project\
│ └── STM32F103_LoRa.uvprojx ← Keil 工程,双击打开
├── User\
│ ├── main.c ← 应用逻辑
│ ├── stm32f10x_it.c/h ← 中断服务函数(f4xx 同理)
│ └── stm32f10x_conf.h ← 标准库裁剪配置
├── SX1278\
│ ├── sx1278.c ← 驱动实现
│ └── sx1278.h ← 寄存器定义与接口
└── Libraries\
├── CMSIS\ ← 内核/器件支持
└── STM32F10x_StdPeriph_Driver\ ← ST 标准外设库(F4 为 STM32F4xx)
4.2 Keil 工程关键配置对照
| 配置项 | F103 工程 | F407 工程 |
|---|---|---|
| Device | STM32F103C8 | STM32F407ZG |
| 器件包 | Keil.STM32F1xx_DFP 2.4.1 | Keil.STM32F4xx_DFP 3.0.0 |
| 全局宏 | USE_STDPERIPH_DRIVER,STM32F10X_MD |
USE_STDPERIPH_DRIVER,STM32F40_41xxx,HSE_VALUE=8000000 |
| ROM/RAM | 64K / 20K | 1024K / 128K |
| 编译器 | ARMCC V5(Compiler 5) | 同左 |
| 烧录算法 | STM32F10x_64.FLM | STM32F4xx_1024.FLM |
| 启动文件 | startup_stm32f10x_md.s | startup_stm32f40_41xxx.s |
F407 时钟的一个坑:ST 标准库 system_stm32f4xx.c 默认按 25MHz 外部晶振配置 PLL(PLL_M=25)。而国内绝大多数 F407 开发板(正点原子探索者等)用的是 8MHz 晶振。如果不管它,系统时钟就不是 168MHz,串口波特率会整体偏移,打印出来全是乱码。本工程的做法:
// system_stm32f4xx.c 中,把 STM32F40_41xxx 分支的
#define PLL_M 25 → 改为 8
// 配合工程宏 HSE_VALUE=8000000
// 时钟树:8MHz / 8(PLL_M) × 336(PLL_N) / 2(PLL_P) = 168MHz
改完可用一个简单方法验证:串口能正常打印,说明 168MHz 对了(因为波特率分频依赖准确的外设时钟)。
4.3 F103 与 F407 驱动层的差异
驱动的寄存器操作逻辑 100% 相同,差异只在底层外设初始化:
| 项目 | F103 写法 | F407 写法 |
|---|---|---|
| GPIO 时钟 | RCC_APB2PeriphClockCmd |
RCC_AHB1PeriphClockCmd |
| SPI1 时钟 | APB2(72MHz) | APB2(84MHz) |
| 复用配置 | 选 GPIO_Mode_AF_PP 即可 |
需 GPIO_PinAFConfig(..., GPIO_AF_SPI1) |
| SPI 分频 | /8 → 9MHz | /16 → 5.25MHz |
| USART1 复用 | 同上 | AF7 |
| SysTick 1ms | LOAD=72000-1 | LOAD=168000-1 |
SX1278 的 SCK 上限是 10MHz,两边都留有余量。
五、驱动实现:逐层拆解 sx1278.c
5.1 第一层:SPI 字节收发
SX1278 的 SPI 是标准的模式 0(CPOL=0 空闲低电平,CPHA=0 第一个边沿采样),MSB 先发:
static uint8_t SX1278_SpiTransfer(uint8_t data)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, data);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1); // 全双工:发的同时也在收
}
5.2 第二层:寄存器读写协议
SX1278 的 SPI 协议规则:
-
第一个字节的最高位决定读/写:写=1,读=0;低 7 位是寄存器地址;
-
第二个字节起是数据。读寄存器时要发一个 dummy 字节(0x00)来"换"回数据;
-
NSS 拉低开始一帧,拉高结束。
void SX1278_WriteReg(uint8_t addr, uint8_t value)
{
LORA_NSS_LOW();
SX1278_SpiTransfer(addr | 0x80); // 写:最高位置 1
SX1278_SpiTransfer(value);
LORA_NSS_HIGH();
}
uint8_t SX1278_ReadReg(uint8_t addr)
{
uint8_t value;
LORA_NSS_LOW();
SX1278_SpiTransfer(addr & 0x7F); // 读:最高位清 0
value = SX1278_SpiTransfer(0x00); // dummy 字节换回数据
LORA_NSS_HIGH();
return value;
}
FIFO 突发读写(地址 0x00)同理,NSS 拉低期间连续传多个字节即可,芯片内部自动递增 FIFO 指针。
5.3 第三层:初始化流程(SX1278_Init)
初始化的顺序是有讲究的,数据手册有硬性要求:
uint8_t SX1278_Init(void)
{
SX1278_HwInit(); // GPIO + SPI1
SX1278_Reset(); // RST 拉低 10ms 再拉高,硬件复位
// ① 必须先 Sleep,才能写 LongRangeMode 位切换到 LoRa 模式
SX1278_WriteReg(REG_OP_MODE, MODE_SLEEP);
SX1278_WriteReg(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_SLEEP);
SX1278_SetMode(MODE_STDBY);
// ② 版本校验:读 RegVersion,SX1278 固定返回 0x12
// 这一步同时验证了 SPI 通信和供电是否正常
if (SX1278_ReadReg(REG_VERSION) != 0x12) return 0;
// ③ 载波频率 433MHz
// Frf = Fdesired / Fstep, Fstep = 32MHz / 2^19 = 61.035 Hz
// 433MHz → Frf = 7094272 = 0x6C4000,写入 0x06/0x07/0x08
SX1278_SetFrequency(433000000UL);
// ④ FIFO 基地址:TX=0x80, RX=0x00(256 字节 FIFO 分两半)
// ⑤ LNA 增益最大 + Boost
// ⑥ PA 配置:PA_BOOST 引脚,Pout = 17 - (15 - OutputPower)
// 17dBm → OutputPower=15 → 0x8F
// ⑦ ModemConfig1 = 0x72:BW=125kHz, CR=4/5, 显式头模式
// ⑧ ModemConfig2 = SF<<4 | 0x04:SF7 + CRC 开启
// ⑨ 前导码 8 符号、同步字 0x12(私有网络)
// ⑩ DIO0 映射:Rx 模式映射 RxDone,Tx 模式映射 TxDone
// ⑪ 清空中断标志,进 Standby
return 1;
}
显式头模式 vs 隐式头模式:本工程用显式头(Explicit Header),每包自带长度/CRC 信息,包长可变,最灵活;隐式头省 2 字节开销但双方必须事先约定包长,一般用不到。
5.4 发送流程
void SX1278_SendPacket(const uint8_t *data, uint8_t len)
{
SX1278_SetMode(MODE_STDBY);
SX1278_WriteReg(REG_DIO_MAPPING1, 0x40); // DIO0 ← TxDone
SX1278_WriteReg(REG_FIFO_ADDR_PTR, 0x80); // 指针指向 TX 基地址
SX1278_WriteFifo(data, len); // 数据写入 FIFO
SX1278_WriteReg(REG_PAYLOAD_LENGTH, len); // 告诉芯片发多少
SX1278_WriteReg(REG_IRQ_FLAGS, 0xFF); // 清标志
SX1278_SetMode(MODE_TX); // 开始发射!
while (LORA_DIO0_READ() == RESET); // 等 DIO0 拉高 = 发完
SX1278_WriteReg(REG_IRQ_FLAGS, IRQ_TX_DONE);
SX1278_SetMode(MODE_STDBY);
}
发送完成后芯片自动回到 Standby,DIO0 拉高——这就是串口能看到连续 TX 打印的前提:如果 DIO0 没接对,程序会卡死在这里。
5.5 接收流程
void SX1278_EnterRx(void)
{
SX1278_SetMode(MODE_STDBY);
SX1278_WriteReg(REG_DIO_MAPPING1, 0x00); // DIO0 ← RxDone
SX1278_WriteReg(REG_FIFO_ADDR_PTR, 0x00);
SX1278_WriteReg(REG_IRQ_FLAGS, 0xFF);
SX1278_SetMode(MODE_RX_CONTINUOUS); // 连续接收,收完一包不退出
}
收到一包后 DIO0 拉高,主循环调用 SX1278_GetRxPacket():
irqFlags = SX1278_ReadReg(REG_IRQ_FLAGS);
if (!(irqFlags & IRQ_RX_DONE)) return 0;
SX1278_WriteReg(REG_IRQ_FLAGS, 0xFF); // 清标志
if (irqFlags & IRQ_PAYLOAD_CRC_ERROR) return 0; // CRC 错则丢弃
nbBytes = SX1278_ReadReg(REG_RX_NB_BYTES); // 这包多少字节
currentAddr = SX1278_ReadReg(REG_FIFO_RX_CURRENT_ADDR); // 在 FIFO 哪里
SX1278_WriteReg(REG_FIFO_ADDR_PTR, currentAddr);
SX1278_ReadFifo(buf, nbBytes); // 读出来
// 信号质量换算(433MHz 低频段公式):
*rssi = -157 + SX1278_ReadReg(REG_PKT_RSSI_VALUE);
*snr = (int8_t)SX1278_ReadReg(REG_PKT_SNR_VALUE) / 4;
注意 RSSI 换算系数和频段有关:低频段(<525MHz,Ra-01 所在)是 -157,高频段(868/915MHz 模块)是 -164。SNR 寄存器是有符号数,单位 0.25dB,所以要除以 4。
六、双机应用逻辑
6.1 角色分配
F103 端 = 主动方:每 2 秒发一包 Hello LoRa from STM32 #n,发完回接收模式等 ACK。
F407 端 = 应答方:常处于连续接收,收到包后打印并回发 ACK from F407 #n:
// F407 main 主循环
while (1) {
if (SX1278_Dio0IsSet()) {
if (SX1278_GetRxPacket(rxBuf, &rxLen, &rssi, &snr)) {
printf("RX#%lu [%d bytes]: %s\r\n RSSI=%d dBm, SNR=%d dB\r\n", ...);
DelayMs(500); // 等对方回到 RX 模式
SX1278_SendPacket(ack, len); // 回发应答
SX1278_EnterRx(); // 继续接收
}
}
}
6.2 一个真实踩过的时序 bug(值得细看)
现象:F407 端能连续收到 F103 的包并回发 ACK(TX: ACK from F407 #n 正常打印),但 F103 端只有 TX、永远收不到 ACK。
最初的 F103 主循环:
while (1) {
if (SX1278_Dio0IsSet()) { ...收包... } // ← 只在延时前检查一次
DelayMs(2000); // ← ACK 在这期间到达!
SX1278_SendPacket(...); // ← 发送流程清空中断标志
SX1278_EnterRx();
}
时序分析:
t=0 F103 发包完成,进入 RX 模式
t=0 F407 收到包
t=500ms F407 延时结束,回发 ACK
t=600ms ACK 到达 F103,DIO0 拉高锁存 ← 但此时 F103 正在 DelayMs(2000) 里睡觉
t=2000ms F103 延时结束,直接 SendPacket
→ SendPacket 里 WriteReg(REG_IRQ_FLAGS, 0xFF)
→ RxDone 标志连同 FIFO 里的 ACK 一起被清掉,丢包!
根因:检查接收的动作在"等待"之前,而 ACK 恰好落在等待期间;下一轮发送又把等待期间锁存的中断标志清掉了。
修复:把 2 秒等待改成"边等边查"的轮询:
while (1) {
SX1278_SendPacket(txBuf, len); // 1. 发送
SX1278_EnterRx(); // 2. 立即回接收模式
for (i = 0; i < 200; i++) { // 3. 2 秒等待期间每 10ms 查一次 DIO0
if (SX1278_Dio0IsSet()) {
if (SX1278_GetRxPacket(...)) { ...打印... }
}
DelayMs(10);
}
}
修复后 F103 端 TX/RX 严格交替,零丢包。
经验总结:用"查询"方式处理中断脚时,一定要保证从事件发起到下一次可能清除标志的操作之间,至少有一次检查。更稳妥的工程做法是把 DIO0 接到 EXTI 外部中断引脚,中断里置标志位,主循环消费——这样无论主循环在干什么都不会漏。
七、测试现象与指标解读
7.1 串口输出说明什么
| 现象 | 说明 |
|---|---|
SX1278 init FAILED |
SPI 不通:检查接线、供电(必须 3.3V)、NSS/RST |
RegVersion = 0x12 |
SPI 读写正常、芯片型号确认是 SX1278 |
| TX 计数连续增长 | 发送链路 + DIO0 接线正常(否则程序卡死) |
收到 RX# / ACK |
射频参数两端匹配,双向链路通 |
7.2 RSSI / SNR 怎么读
本次实测(两板距离约 1 米):
-
RSSI ≈ -21 ~ -25 dBm:信号极强。LoRa 接收极限约 -130 dBm(SF7),中间有 100dB 以上的余量——意味着这套配置理论上可以拉距很远;
-
SNR ≈ 5~7 dB:信噪比健康。LoRa 的厉害之处是 SNR 为负数也能解调(SF7 极限约 -7.5dB,SF12 可到 -20dB),所以日常看到正数都属于轻松工况。
7.3 想传得更远?调这两个参数
把两端 sx1278.h 里的宏同步改掉即可:
#define LORA_SPREADING_FACTOR 10 // SF7 → SF10,灵敏度提升约 7.5dB
// ModemConfig3 里 SF≥11 会自动开 LowDataRateOptimize(驱动已处理)
代价是速率下降(SF10 空中耗时是 SF7 的约 5 倍)。两端必须改得一致,否则互相收不到。
八、常见问题 FAQ
Q1:串口打印 SX1278 init FAILED!?
SPI 通信失败。按顺序查:① 模块供电是不是 3.3V;② NSS/SCK/MOSI/MISO 有没有接错或虚焊;③ RST 有没有接;④ F407 工程的晶振配置对不对(串口乱码是时钟问题,init FAILED 是接线问题,两者别混淆)。
Q2:两端都 init OK,但互相收不到?
① 天线装了吗?频段对不对(433MHz);② 距离拉开到 1 米以上;③ 两端 sx1278.h 里的频率/SF/同步字是否一致;④ 周围是否有同频段强干扰。
Q3:能收到但 RSSI 很低、偶尔丢包?
正常现象,拉距测试时越远离丢包越多。可以增大 SF、减小 BW、或提高发射功率(Ra-01 最大 +20dBm,需把 REG_PA_DAC 写 0x87 并配 17dBm 以上的 OutputPower)。
Q4:能不能两个以上节点组网?
可以。LoRa 本身没有地址概念,常见做法是在应用层协议里加"地址字节 + 命令字",比如 [目标地址][源地址][数据...],各节点收到后判断目标地址是否是自己。这就是最简单的私有组网协议。
Q5:手机/小程序能直接收 LoRa 吗?
不能。手机没有 Sub-GHz 射频。要让手机看数据,路径是:节点 → LoRa 网关(或接收板接电脑/ESP32)→ 互联网/云服务器 → APP/小程序。
九、扩展方向
这套代码已经具备了一个"无线传感网络"的雏形骨架,往应用方向走一步很容易:
- 无线测温节点:F103 端加 DS18B20/DHT11,把发送内容换成温度数据;F407 端就是接收网关;
- 远程控制:F103 端发开关指令,F407 端解析后控制继电器/电磁阀(农业灌溉场景);
- 多点组网:应用层加地址字节,一个接收端收多个发射节点;
- 低功耗:SX1278 支持 Sleep 模式(电流 <1μA),配合 STM32 的 Stop 模式和 RTC 定时唤醒,一节电池可以工作数年——这也是 LoRa 在抄表行业称霸的原因;
- 上云:F407 端通过串口/以太网把数据转发到电脑或服务器,实现远程查看。
更多推荐

所有评论(0)