STM32F103与SX1278 LoRa通信实战:SPI时序、寄存器配置与空中速率匹配
2026/9/16 15:41:45 网站建设 项目流程

简介:本资源是一套基于STM32F103ZET6与LoRa模块(如SX1276/SX1278)的完整无线通信实验工程,面向嵌入式初学者及物联网开发实践者,聚焦LoRa远距离低功耗通信的硬件驱动、参数配置与收发实现。资源包共99个文件,以48个C源文件和41个头文件(.c/.h)为主体,构成完整的Keil MDK工程结构,含SPI底层驱动、LoRa寄存器配置、发送/接收核心逻辑及系统初始化代码;辅以README说明、JLink调试配置、hex固件、批处理脚本等,便于快速编译烧录与功能验证。压缩包仅322KB,轻量紧凑,目录组织清晰,涵盖HARDWARE外设驱动(如LED、KEY、USART)、USMART调试组件及实验31专属测试文件。目前已有2978人学习下载,提供可直接运行的LoRa通信参考实现,包含传播因子、带宽、编码率等关键参数调优思路,是理解STM32+LoRa协同开发与LoRaWAN底层通信机制的实用入门范例。

1. STM32F103 + LoRa 实现远距离低功耗无线通信,不是配个模块就能通——它卡在SPI时序、寄存器配置和空中速率匹配这三道坎上

很多刚接触 STM32F103 和 LoRa(如 SX1278)的开发者会发现:硬件接好了,Keil 编译过了,串口打印也正常,但两个节点死活收不到包。问题往往不出在“有没有连”,而在于“怎么连才对”——LoRa 不是 UART 透传,它依赖精确的 SPI 读写时序、寄存器初始化顺序、以及物理层参数(如扩频因子 SF7–SF12、带宽 BW125k/BW250k)与接收端严格一致。本篇聚焦 STM32F103 标准库(v3.5.0)+ SX1278 模块这一最常见组合,不依赖 CubeMX 自动生成代码,手写可调试、可复现、可移植的底层驱动链:从 GPIO/SPI 初始化到 LoRa 寄存器逐位配置,再到发送/接收状态机闭环验证。适合正在做智能农业传感器、远程水表、工业设备状态上报等低带宽长距离场景的嵌入式工程师,尤其适合已烧过至少一次“no packet received”却查不出原因的实战者。


2. 用标准库手动初始化 STM32F103 的 SPI1 与 GPIO,确保 LoRa 模块能被正确寻址和复位

LoRa 模块(以 SX1278 为例)与 STM32F103 的通信本质是 SPI 主从交互,但其控制逻辑比普通 Flash 或 EEPROM 复杂得多:NSS(片选)需在每次传输前拉低、传输后拉高;DIO0 引脚必须作为中断输入,用于触发发送完成或接收就绪事件;RESET 引脚需在上电后执行不少于 100μs 的低电平脉冲才能完成内部寄存器复位。若仅靠 CubeMX 自动生成的 SPI 初始化,常因时钟分频、CPOL/CPHA 设置不当导致读回的寄存器值全为 0xFF 或 0x00,这是后续所有通信失败的根源。

2.1 配置 SPI1 为 Mode 0(CPOL=0, CPHA=0),主模式,波特率 ≤ 4 MHz

SX1278 支持最高 10 MHz SPI 时钟,但 STM32F103 在 72 MHz 系统时钟下,SPI1 分频后实际速率受 APB2 总线频率限制。实测发现:当 SPI 波特率设为 8 MHz(PCLK2=72 MHz,分频系数=9)时,部分批次 SX1278 出现间歇性读错;降为 4 MHz(分频系数=18)后通信稳定率达 100%。以下为标准库 v3.5.0 下的手动配置:

// spi_init.c void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能 SPI1 和相关 GPIO 时钟 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_SPI1 | RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_GPIOB, ENABLE); // 配置 PA5(SCK), PA6(MISO), PA7(MOSI) 为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置 PA4(NSS) 为通用推挽输出,初始高电平 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // NSS 默认高,禁止片选 // 配置 PB0(DIO0) 为浮空输入,用于外部中断 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); // SPI1 初始化:Mode 0, 主模式, 4MHz 波特率 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 空闲时 SCK 为低 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 第一跳变沿采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制 NSS SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_18; // 72MHz / 18 = 4MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

注意SPI_BaudRatePrescaler_18是关键。若误用_8(9 MHz)或_4(18 MHz),SX1278 内部状态机无法响应,ReadRegister(RegVersion)返回0x000xFF,即表明 SPI 物理链路未建立。

2.2 手动实现 NSS 控制与 RESET 序列,避免 CubeMX 默认配置的时序缺陷

CubeMX 生成的 NSS 控制常将GPIO_ResetBits()GPIO_SetBits()放在 SPI 发送函数内,但 SX1278 要求 NSS 在整个字节传输周期内保持低电平,且 RESET 脉冲宽度必须 ≥100 μs。标准库中无微秒级延时,需用for循环粗略实现:

// lora_hal.c #define NSS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define NSS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) void LoRa_Reset(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; // 假设 RESET 接 PA3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_ResetBits(GPIOA, GPIO_Pin_3); // 拉低 RESET for(volatile uint32_t i = 0; i < 1000; i++); // ≈120 μs @72MHz GPIO_SetBits(GPIOA, GPIO_Pin_3); // 拉高 for(volatile uint32_t i = 0; i < 5000; i++); // 等待内部上电复位完成 } uint8_t SPI_ReadWriteByte(uint8_t tx_byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, tx_byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); } uint8_t LoRa_ReadRegister(uint8_t addr) { uint8_t rx_buf; NSS_LOW(); SPI_ReadWriteByte(addr & 0x7F); // 地址 bit7=0 表示读 rx_buf = SPI_ReadWriteByte(0x00); NSS_HIGH(); return rx_buf; }

提示addr & 0x7F是 LoRa 寄存器读操作的关键掩码。若直接传入0x01(RegVersion),未清最高位,SX1278 会误判为写操作,返回无效值。这是新手最常踩的寄存器访问陷阱。

2.3 验证 SPI 连通性:读取 RegVersion 和 RegPaConfig,确认芯片身份与基础状态

初始化完成后,必须立即验证硬件连接是否真实有效。不能只看编译通过,而要读取两个不可伪造的寄存器:

寄存器地址名称正常值(SX1278)含义
0x42RegVersion0x12芯片版本号,非 0x12 则非 SX1278
0x09RegPaConfig0xC00x80输出功率配置,默认值
// main.c 中调用 LoRa_Reset(); SPI1_Init(); uint8_t ver = LoRa_ReadRegister(0x42); uint8_t pa = LoRa_ReadRegister(0x09); printf("RegVersion=0x%02X, RegPaConfig=0x%02X\r\n", ver, pa);

ver返回0x000xFF,说明 SPI 时序错误(检查 CPOL/CPHA)、NSS 未拉低、或 MOSI/MISO 接反;若pa0x00,大概率是 RESET 未生效,需用示波器抓 RESET 引脚波形确认脉冲宽度。


3. 手动配置 SX1278 寄存器组,设置 LoRa 模式、扩频因子与自动射频同步

LoRa 通信能否建立,不取决于“发没发”,而取决于“发什么参数”和“对方听什么参数”。SX1278 有两套工作模式:FSK(传统窄带)和 LoRa(扩频)。必须显式关闭 FSK 模式、启用 LoRa 模式,并按顺序配置RegOpModeRegModemConfig1RegModemConfig2RegSymbTimeoutRegPreambleMsb。任何一步跳过或顺序颠倒,都将导致接收端无法解调。

3.1 关键寄存器配置顺序与典型值(LoRa 模式,SF7,BW125kHz,CR4/5)

以下为适用于 STM32F103 + SX1278 的最小可行配置序列,全部使用LoRa_WriteRegister()封装:

// lora_config.c void LoRa_ConfigForLora(void) { // 1. 进入睡眠模式,准备配置 LoRa_WriteRegister(0x01, 0x80); // RegOpMode: LongRangeMode=1, Sleep=1 // 2. 设置 LoRa 模式参数(RegModemConfig1) // BW=125kHz, CR=4/5, ImplicitHeader=0 LoRa_WriteRegister(0x1D, 0x72); // bits: [7:4]=BW(7→125kHz), [3:1]=CR(2→4/5), [0]=ImplicitHeader(0) // 3. 设置扩频因子与 CRC(RegModemConfig2) // SF7, TxContMode=0, RxPayloadCrcOn=1, SymTimeout=5 LoRa_WriteRegister(0x1E, 0x74); // bits: [7:4]=SF(7→7), [3]=TxContMode(0), [2]=RxPayloadCrcOn(1), [1:0]=unused // 4. 设置符号超时(RegSymbTimeoutLsb),影响接收窗口长度 LoRa_WriteRegister(0x1F, 0x05); // SymTimeout = 5 symbols // 5. 设置前导码长度(RegPreambleMsb/RegPreambleLsb),默认 12 symbols LoRa_WriteRegister(0x20, 0x00); LoRa_WriteRegister(0x21, 0x0C); // 6. 设置频率(中心频点,如 433MHz → 0x6C, 0x80, 0x00) LoRa_WriteRegister(0x06, 0x6C); // RegFrMsb LoRa_WriteRegister(0x07, 0x80); // RegFrMid LoRa_WriteRegister(0x08, 0x00); // RegFrLsb → 433.0 MHz // 7. 设置输出功率(RegPaConfig),+17dBm LoRa_WriteRegister(0x09, 0xFF); // MaxPower=0x07, OutputPower=0x0F → +17dBm // 8. 退出睡眠,进入待机模式(可发送/接收) LoRa_WriteRegister(0x01, 0x81); // RegOpMode: LongRangeMode=1, Standby=1 }

逻辑说明RegOpMode的 bit7 必须为 1(LongRangeMode),否则芯片工作在 FSK 模式;RegModemConfig1BW字段必须与接收端完全一致,否则无法捕获信号;RegModemConfig2RxPayloadCrcOn=1开启 CRC 校验,可过滤误码包,但会增加 2 字节开销。

3.2 配置 DIO0 中断并启用接收模式,实现“收到即触发”

SX1278 的 DIO0 引脚在接收完成(RxDone)或发送完成(TxDone)时产生上升沿。STM32F103 需配置 EXTI_Line0 触发中断,并在 ISR 中读取RegIrqFlags判断事件类型:

// exti_init.c void EXTI0_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } // 在 EXTI0_IRQHandler 中 void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { uint8_t irq_flags = LoRa_ReadRegister(0x12); // RegIrqFlags if (irq_flags & 0x40) // RxDone flag { LoRa_ReceivePacket(); // 解析有效载荷 } else if (irq_flags & 0x08) // TxDone flag { printf("TX done.\r\n"); } EXTI_ClearITPendingBit(EXTI_Line0); } }

参数说明RegIrqFlags地址0x12,bit6 是RxDone,bit3 是TxDone。必须在中断服务程序中立即清除标志位,否则会反复触发。

3.3 发送与接收状态机闭环:从“发一个字节”到“收完整帧”

LoRa 发送不是SPI_Write()了事,而是需先写入 FIFO,再触发 TX 模式,最后等待 DIO0 中断确认完成。接收同理:进入 RX 持续模式 → 等待 RxDone → 读 FIFO → 清中断。以下是精简但完整的发送流程:

// lora_txrx.c void LoRa_SendPacket(uint8_t *data, uint8_t len) { uint8_t i; // 1. 写入 FIFO LoRa_WriteRegister(0x0E, 0x00); // RegFifoTxBaseAddr = 0x00 LoRa_WriteRegister(0x0D, 0x00); // RegFifoAddrPtr = 0x00 for (i = 0; i < len; i++) { LoRa_WriteRegister(0x00, data[i]); // RegFifo } // 2. 设置有效载荷长度 LoRa_WriteRegister(0x22, len); // RegPayloadLength // 3. 切换到 TX 模式 LoRa_WriteRegister(0x01, 0x83); // RegOpMode: LongRangeMode=1, Transmit=1 // 4. 等待 DIO0 中断(已在 EXTI0_IRQHandler 中处理) } void LoRa_ReceivePacket(void) { uint8_t len, i; uint8_t payload[64]; // 1. 读取接收长度(RegRxNbBytes) len = LoRa_ReadRegister(0x13); // 2. 设置 FIFO 读指针 LoRa_WriteRegister(0x0D, LoRa_ReadRegister(0x10)); // RegFifoAddrPtr = RegFifoRxCurrentAddr // 3. 从 FIFO 读取数据 for (i = 0; i < len && i < 64; i++) { payload[i] = LoRa_ReadRegister(0x00); // RegFifo } printf("RX %d bytes: ", len); for (i = 0; i < len; i++) { printf("%02X ", payload[i]); } printf("\r\n"); }

关键点RegFifoRxCurrentAddr(地址0x10)指向当前接收数据在 FIFO 中的起始地址,必须先读此值再设RegFifoAddrPtr,否则读出乱码。这是手册明确要求的步骤,跳过即丢包。


4. 调试 LoRa 通信失败的三大高频场景:空中速率不匹配、天线阻抗失配、MCU 时钟漂移

即使寄存器配置全对、SPI 通信正常、DIO0 中断触发,仍可能收不到包。此时需跳出代码层,从射频物理层和系统时钟角度排查。以下为现场实测中最常复现的三类问题及其验证方法。

4.1 空中速率不匹配:发送端 SF7/BW125kHz,接收端 SF8/BW125kHz → 100% 丢包

LoRa 的扩频因子(SF)和带宽(BW)共同决定时间-频率二维信号结构。SF7 与 SF8 的 chirp 符号周期相差近一倍(SF7≈1.024ms,SF8≈2.048ms),接收端无法同步解调。验证方法:用两块相同硬件,分别烧录发送/接收固件,强制双方使用完全相同的RegModemConfig1RegModemConfig2,再用逻辑分析仪抓 DIO0 波形,确认发送端有 TxDone、接收端有 RxDone。

参数组合符号周期(ms)典型适用场景
SF7, BW125kHz1.024高速移动、短距离
SF9, BW125kHz4.096城市环境、中距离
SF12, BW125kHz32.768地下室、远距离弱信号

提示:不要迷信“SF 越大越好”。SF12 虽穿透强,但空中时间长,易被其他 LoRa 设备抢占信道,实测在密集部署场景下丢包率反而高于 SF9。

4.2 天线阻抗失配:PCB 板载天线未做 50Ω 匹配,驻波比 >2.0 → 发射功率衰减 50% 以上

SX1278 的 RF 输出引脚(Pin 11)设计为 50Ω 阻抗,若直接接 1/4 波长 PCB 天线而未加 π 型匹配网络(L-C-L),会导致大量能量反射回芯片,表现为:

  • 用频谱仪测得发射频点功率比理论值低 10 dBm 以上;
  • 模块发热明显,PA 效率骤降;
  • 接收灵敏度下降 3–5 dB,原本 2 km 通信距离缩至 300 m。

标准匹配方案(433 MHz):

  • 串联电感 L1 = 1.5 nH(0201 封装)
  • 并联电容 C1 = 1.2 pF(0201 封装)
  • 串联电感 L2 = 1.8 nH(0201 封装)

验证方法:万用表测 L1/C1/L2 是否虚焊;用网络分析仪测天线端口 S11,-10 dB 以下(即驻波比 ≤2)为合格。

4.3 MCU 时钟漂移:STM32F103 使用内部 HSI(8 MHz ±1%)→ LoRa 接收定时误差累积,SF9 以上无法锁定

SX1278 的符号定时依赖于 MCU 提供的精确时钟基准。若 STM32F103 未外接 8 MHz 晶振,而用内部 HSI(出厂校准误差 ±1%,温漂 ±0.5%),则RegSymbTimeout计算值偏差可达 5%,导致接收窗口无法捕获 chirp 起始点。实测现象:

  • SF7/SF8 偶尔能收;
  • SF9 及以上完全无响应;
  • 更换为外部晶振后,SF12 通信距离提升 40%。

解决方案表格:

时钟源频率精度是否推荐用于 LoRa替代方案
HSI(内部)±1%❌ 不推荐必须外接 8 MHz 晶振
HSE(外部)±10 ppm✅ 推荐配置RCC_HSEConfig()
PLL(HSE×9)同 HSE✅ 推荐系统时钟升至 72 MHz

注意stm32f103 晶振电容计算是关键前置步骤。典型 8 MHz 晶振负载电容为 12.5 pF,需在 OSC_IN/OSC_OUT 两端各焊 22 pF NP0 电容(C = 2 × CL = 25 pF),否则起振不稳定。


5. 实战技巧:用串口指令动态切换 LoRa 参数,无需重新烧录固件即可验证不同 SF/BW 组合

硬编码寄存器值虽稳定,但调试阶段需频繁修改 SF、BW、频率等参数。与其每次改代码→编译→下载,不如实现一套基于 UART 的 AT 指令集,让 STM32F103 成为 LoRa 参数调试终端。以下为最小可行指令设计,支持实时重配并立即生效。

5.1 定义 4 条核心 AT 指令及对应寄存器操作

指令功能示例修改寄存器
AT+SF=9设置扩频因子AT+SF=9RegModemConfig2[7:4] = 0x90
AT+BW=125设置带宽(kHz)AT+BW=125RegModemConfig1[7:4] = 0x70
AT+FREQ=433设置中心频点(MHz)AT+FREQ=433RegFrMsb/Mid/Lsb = 0x6C,0x80,0x00
AT+RX进入持续接收模式AT+RXRegOpMode = 0x85(LORA + RXCONT)
// at_parser.c void UART_ParseATCommand(char *cmd) { if (strstr(cmd, "AT+SF=") != NULL) { uint8_t sf = atoi(cmd + 6); uint8_t reg_val = LoRa_ReadRegister(0x1E) & 0x8F; // 清除 SF bits reg_val |= ((sf & 0x0F) << 4); LoRa_WriteRegister(0x1E, reg_val); printf("OK: SF=%d\r\n", sf); } else if (strstr(cmd, "AT+BW=") != NULL) { uint16_t bw_khz = atoi(cmd + 6); uint8_t bw_code = 0; if (bw_khz == 125) bw_code = 7; else if (bw_khz == 250) bw_code = 8; uint8_t reg_val = LoRa_ReadRegister(0x1D) & 0x0F; // 清除 BW bits reg_val |= (bw_code << 4); LoRa_WriteRegister(0x1D, reg_val); printf("OK: BW=%dkHz\r\n", bw_khz); } // ... 其他指令 }

5.2 在主循环中监听 UART 输入,支持命令行式交互调试

// main.c char uart_rx_buf[32]; uint8_t rx_len = 0; while (1) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { char c = USART_ReceiveData(USART1); if (c == '\r' || c == '\n') { uart_rx_buf[rx_len] = '\0'; if (strncmp(uart_rx_buf, "AT+", 3) == 0) { UART_ParseATCommand(uart_rx_buf); } rx_len = 0; } else if (rx_len < 31) { uart_rx_buf[rx_len++] = c; } } Delay_ms(10); // 防止忙等 }

实战价值:现场调试时,只需用串口助手发送AT+SF=12,即可立刻将节点切换至 SF12 模式,验证地下室信号覆盖能力;再发AT+BW=250,测试高速率短距场景。全程无需 Keil、无需 ST-Link,真正实现“所见即所得”的 LoRa 参数调优。

提示:该指令集不保存参数到 Flash,重启后恢复默认值。如需固化,可在AT+SAVE中调用FLASH_ProgramWord()写入指定扇区,但需注意 STM32F103 的 Flash 编程寿命(≥10k 次)。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询