硬件:两块 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 设备要互相收到,以下参数必须完全一致

  1. 载波频率(433MHz)
  2. 扩频因子 SF
  3. 带宽 BW
  4. 编码率 CR
  5. 同步字 SyncWord(0x12 私有网络 / 0x34 公有 LoRaWAN 网络)
  6. CRC 开关状态
  7. 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/小程序。


九、扩展方向

这套代码已经具备了一个"无线传感网络"的雏形骨架,往应用方向走一步很容易:

  1. 无线测温节点:F103 端加 DS18B20/DHT11,把发送内容换成温度数据;F407 端就是接收网关;
  2. 远程控制:F103 端发开关指令,F407 端解析后控制继电器/电磁阀(农业灌溉场景);
  3. 多点组网:应用层加地址字节,一个接收端收多个发射节点;
  4. 低功耗:SX1278 支持 Sleep 模式(电流 <1μA),配合 STM32 的 Stop 模式和 RTC 定时唤醒,一节电池可以工作数年——这也是 LoRa 在抄表行业称霸的原因;
  5. 上云:F407 端通过串口/以太网把数据转发到电脑或服务器,实现远程查看。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐