使用 STM32WL 系列 Sub-GHz 无线驱动程序的应用示例

介绍

STM32WL 系列器件包括内置的低于1GHz无线外设 ( Sub-GHz 指的是低于 1GHz 的无线电频段 ),能够支持LoRa(仅限STM32WLE5/55器件)、(G)FSK、(G)MSK和BPSK调制方案。与此无线外设的通信是通过使用设备参考手册第5.8节中概述的命令的内部SPI接口完成的。虽然该RF接口的抽象层是在低于1GHz Phy中间件中定义的(在STM32CubeWL MCU Package中可得),但将该中间件添加到使用STM32CubeMX的项目中需要在其他几个外设和库之间进行高级配置。这会导致一个更大、更复杂的项目,消耗更多的设备内存,并导致抽象层低效率。对于要求功耗低的简单应用,将RF接口驱动程序与低于1GHzPhy中间件隔离并直接利用它可能是有益的。

低于1GHz Phy中间件由高层(radio.c) 和低层(radio_driver.c) 组成。高级驱动程序提供了许多有用的函数,这些函数抽象了低层无线功能,例如RadioInit()RadioSetTxConfig()RadioSend() 。然而,尽管这些函数很方便,但它们的代价是效率低下,比如冗余的函数调用和过度依赖诸如音序器和定时器服务器之类的实用程序。低层驱动程序简单地实现参考手册中概述的 SUBGHZSPI 命令,并提供低于1GHz无线寄存器的定义。以牺牲一些质量属性(如可维护性和可移植性)为代价,使用该驱动程序进行编码直接允许程序员对其应用程序进行更大的控制。在本教程中,将演示如何将这个低层与低于 1GHz Phy 中间件隔离开来,并直接添加到 STM32CubeIDE 项目中。

要求

要准确地跟随教程,需要以下项目。

操作过程

创建和配置项目

第一步是建立一个可以添加驱动程序的项目。在这里,将从头创建一个新的 STM32 项目,并启用 SubGHz 外设。也可以使用已正确配置的现有项目。

  1. 启动STM32CubeIDE应用程序并打开(或创建)所需的工作空间。要创建一个新项目,请通过选择 File > New > STM32 Project,启动 STM32 项目 向导。

  2. 使用Board Selector 选择器选择 NUCLEO-WL55JC1 评估板。单击 Next

  3. 给项目起一个合适的名字(例如,“radioDriverExample”),取消选中 Enable Multi Cpus Configuration 选项。单击 Finish

  4. 将出现一个弹出窗口,询问是否应该将外设初始化为其默认模式。单击 Yes

  5. 向导将生成一个扩展名为“.ioc”的设备配置文件。该文件可以在 STM32CubeIDE 中打开。在 Pinout & Configuration 选择器中,在Connectivity 类别下选择SUBGHZ外设。选中 “Activated

  6. 在 SUBGHZ 配置部分中,在 Parameter Settings 选择器下将波特率预调量值 ( Baudrate Prescaler Value ) 更改为 “4”。

NVIC 设置 选择器中,选中复选框以启用 SUBGHZ Radio Interrupt

  1. 切换到 “Clock Configuration ”选择器。将 MSI RC 值改为“48000”。

  2. 最后,切换到 Project Manager 选择器并打开 Code Generator 选项。勾选 “Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”

  3. 保存修改后的.ioc文件,并在询问 “Do you want to generate code?” 时单击 Yes 。如果提示是否要打开 C/ c++ 透视图,再点击 Yes

添加SubGHz Radio Driver

  1. 作为第一步,从ST的网站下载STM32CubeWL MCU Package,并将其内容提取到你选择的位置。

添加BSP驱动程序

由于低于1GHz中间件与硬件无关,因此需要一个Board Support Package (BSP) 来允许驱动程序控制RF开关。

  1. 在项目资源管理器中,右键单击“驱动程序”目录,选择Import . … 在出现的向导中,在General 类别中选择File System ,如 下所示。单击Next。

  2. 用STM32CubeWL MCU包中驱动程序目录的位置填充From 目录字段(举例, /STM32Cube_FW_WL_V1.1.0/Drivers)。使用文件资源管理器窗口选择Drivers/BSP/STM32WLxx_Nucleo目录下的所有.h和.c文件。单击Finish

  3. 在项目资源管理器中找到名为stm32wlxx_nucleo_conf_template.h 的导入文件,并将其重命名为stm32wlxx_nucleo_conf.h 。项目结构现在应该出现如图所示。

  4. 为了让编译器定位这些新的头文件,必须将包含它们的目录添加到include目录列表中。右键单击“STM32WLxx_Nucleo”目录,选择Add/remove include directory 。将弹出一个窗口,询问应该修改哪些配置。确保选中DebugRelease 选项,然后单击OK

添加所需的实用程序

就目前而言,低层无线驱动程序仍然对实用程序函数有一些较小的依赖。这些依赖关系可以很容易地通过对驱动程序文件的一些修改来消除,但是在项目中包含这些文件也同样容易。

  1. 选择 File > New > Source Folder。将文件夹命名为“实用程序”并单击Finish

  2. 右键单击项目资源管理器中的实用程序文件夹并选择Import. …在出现的向导中,在General 类别中选择File System 。单击 N ext

  3. 用STM32CubeWL MCU包中实用程序目录的位置填充From 目录字段(举例, /STM32Cube_FW_WL_V1.1.0/Utilities)。使用文件资源管理器窗口选择以下文件:

  • Utilities/conf/utilities_conf_template.h
  • Utiliites/misc/stm32_mem.c
  • Utilities/misc/stm32_mem.h

如下面的两张图所示。

  1. 在项目浏览器中找到导入的名为“utilites_conf_template .h 的文件,将其重命名为“utilites_conf .h 。项目结构现在应该出现如图所示。

  2. Utilities/conf and Utilites/misc 目录重复步骤4,将它们添加到包含路径列表中。

无线驱动程序

处理好依赖关系后,就可以添加无线驱动程序本身了!

  1. 在驱动程序目录中创建一个名为“Radio”的新文件夹(或你喜欢的任何名称)。在这个文件夹中,从STM32CubeWL MCU包中复制以下文件:
  • <extraction_location>/STM32Cube_FW_WL_V1.1.0/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c
  • <extraction_location>/STM32Cube_FW_WL_V1.1.0/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.h
  • <extraction_location>/STM32Cube_FW_WL_V1.1.0/Middlewares/Third_Party/SubGHz_Phy/Conf/radio_conf_template.h
  • <extraction_location>/STM32Cube_FW_WL_V1.1.0/Projects/NUCLEO-WL55JC/Applications/SubGHz_Phy/SubGHz_Phy_PingPong/SubGHz_Phy/Target/radio_board_if.c
  • <extraction_location>/STM32Cube_FW_WL_V1.1.0/Projects/NUCLEO-WL55JC/Applications/SubGHz_Phy/SubGHz_Phy_PingPong/SubGHz_Phy/Target/radio_board_if.h
  1. 将文件radio_conf_template.h 重命名为radio_conf.h 。项目结构现在应该如图所示。

  2. 打开radio_conf.h 文件,注释掉mw_log_conf.hutilites_def .hsys_debug.h include 指令。为subghz.h 添加一个 include 指令。

  3. 类似地,打开文件radio_driver.c 并注释掉mw_log_conf.h 的include指令。

  4. Drivers/Radio 目录重复步骤4,将其添加到包含路径列表中。

  5. 将文件 <extraction_location>/STM32Cube_FW_WL_V1.1.0/Projects/NUCLEO-WL55JC/Applications/SubGHz_Phy/SubGHz_Phy_PingPong/Core/Inc/platform.h 复制到Core/Inc 目录下。

  6. 打开platform.h ,注释掉stm32wlxx_ll_gpio.h include 指令。

  7. 最后,要开始使用无线驱动程序,请在适当的用户代码部分的 main.c 顶部添加 #include radio_driver.h 行。

应用程序示例

作为以独立方式使用低于1GHz Phy驱动程序的示例,我们创建了两个示例程序(可在GitHub Repository上获得)。这些示例复制了STM32CubeWL MCU Package中SubGHz_Phy_PingPong示例的高级功能。也就是说,它们都实现了图1所示的状态机。这两个示例之间的唯一区别是一个使用LoRa调制解调器,而另一个使用FSK调制解调器。

1: 低层无线驱动乒乓样例项目有限状态机

两个NUCLEO-WL55JC1板需要运行这些示例,其中一个将充当主机,而另一个将充当从机。最初,两个板都处于主状态,以随机间隔发送“PING”消息并等待响应。最终,两个板同步,因此只有一个设备发送“PING”消息,另一个设备发送“PONG”消息作为响应。要执行该应用程序,请按照前一节提供的步骤创建一个项目,该项目包含低于1GHz 无线驱动程序。然后,只需将项目的main.c 文件的内容替换为GitHub Repository中的一个文件的内容,具体取决于你希望在示例中使用哪种调制方案。最后,构建项目并使用它对两个Nucleo板进行编程。



注意,这些示例与SubGHz_Phy_PingPong示例兼容。也就是说,一块板可以用上述应用程序编程,另一块板可以用SubGHz_Phy_PingPong应用程序编程,它们将按预期一起工作。然而,为了利用GFSK调制,必须首先对SubGHz_Phy_PingPong示例进行稍微修改。打开subghz_phy_app.h 文件,修改第一个define指令如下:

#define USE_MODEM_LORA  0 //1
#define USE_MODEM_FSK   1 //0

#define REGION_US915 //REGION_EU868

然后,在radio.c 中找到RadioRandom() 函数,注释掉RadioSetModem(MODEM_LORA); 这一行不仅不需要获得随机数,还会擦除之前初始化步骤中设置的无线配置。因此,在这种情况下,它被认为是一个bug,不应该被包括在内。SubGHz_Phy_PingPong示例现在准备编译并烧写到NUCLEO-WL55JC1板之一。另一个板应该根据上述说明使用GitHub Repository中的main_gfsk.c 文件的内容进行编程。



在初始化和执行图1所示的有限状态机之前,通过调用清单1中定义的radioInit() 函数来初始化无线。该函数使用与SubGHz_Phy_PingPong示例相同的无线配置,但有一个例外。在参考手册第6.1节的末尾,它说:

SMPS需要时钟才能正常工作。如果由于任何原因这个时钟停止,设备可能会被破坏。为了避免这种情况,使用时钟检测,当出现时钟故障时,关闭SMPS并启用LDO。SMPS时钟检测通过低于1GHz无线SUBGHZ_SMPSC0R.CLKDE使能。缺省情况下,SMPS时钟检测功能处于关闭状态,开启SMPS前必须开启时钟检测功能。

尽管有这个警告,低于1GHz Phy中间件的高层和低层都没有启用SMPS时钟检测。因为DCDC_ENABLE 是在radio_config.h 中定义的,所以SUBGRF_SetRegulatorMode() 函数将启用SMPS降压转换器。因此,在此函数调用之前,手动启用SMPS时钟检测。

清单 1: main_gfsk.c 中的 radioInit() 定义

void radioInit(void)
{
  // Initialize the hardware (SPI bus, TCXO control, RF switch)
  SUBGRF_Init(RadioOnDioIrq);

  // Use DCDC converter if `DCDC_ENABLE` is defined in radio_conf.h
  // "By default, the SMPS clock detection is disabled and must be enabled before enabling the SMPS." (6.1 in RM0453)
  SUBGRF_WriteRegister(SUBGHZ_SMPSC0R, (SUBGRF_ReadRegister(SUBGHZ_SMPSC0R) | SMPS_CLK_DET_ENABLE));
  SUBGRF_SetRegulatorMode();

  // Use the whole 256-byte buffer for both TX and RX
  SUBGRF_SetBufferBaseAddress(0x00, 0x00);

  SUBGRF_SetRfFrequency(RF_FREQUENCY);
  SUBGRF_SetRfTxPower(TX_OUTPUT_POWER);
  SUBGRF_SetStopRxTimerOnPreambleDetect(false);

  SUBGRF_SetPacketType(PACKET_TYPE_GFSK);

  ModulationParams_t modulationParams;
  modulationParams.PacketType = PACKET_TYPE_GFSK;
  modulationParams.Params.Gfsk.Bandwidth = SUBGRF_GetFskBandwidthRegValue(FSK_BANDWIDTH);
  modulationParams.Params.Gfsk.BitRate = FSK_DATARATE;
  modulationParams.Params.Gfsk.Fdev = FSK_FDEV;
  modulationParams.Params.Gfsk.ModulationShaping = MOD_SHAPING_G_BT_1;
  SUBGRF_SetModulationParams(&modulationParams);

  packetParams.PacketType = PACKET_TYPE_GFSK;
  packetParams.Params.Gfsk.AddrComp = RADIO_ADDRESSCOMP_FILT_OFF;
  packetParams.Params.Gfsk.CrcLength = RADIO_CRC_2_BYTES_CCIT;
  packetParams.Params.Gfsk.DcFree = RADIO_DC_FREEWHITENING;
  packetParams.Params.Gfsk.HeaderType = RADIO_PACKET_VARIABLE_LENGTH;
  packetParams.Params.Gfsk.PayloadLength = 0xFF;
  packetParams.Params.Gfsk.PreambleLength = (FSK_PREAMBLE_LENGTH << 3); // bytes to bits
  packetParams.Params.Gfsk.PreambleMinDetect = RADIO_PREAMBLE_DETECTOR_08_BITS;
  packetParams.Params.Gfsk.SyncWordLength = (FSK_SYNCWORD_LENGTH << 3); // bytes to bits
  SUBGRF_SetPacketParams(&packetParams);

  SUBGRF_SetSyncWord((uint8_t[]){0xC1, 0x94, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00});
  SUBGRF_SetWhiteningSeed(0x01FF);
}

其他利用 Sub-GHz 无线驱动程序的函数是状态输入函数。作为使用这些驱动程序函数接收和发送数据包的示例,清单2和3分别提供了enterMasterRx() 和enterMasterTx()函数。 有关SUBGRF_SetDioIrqParams() 函数使用的详细说明,请参见 STM32WL 系列中的 Sub-GHz 无线电中断

清单 2: main_gfsk.c 中的enterMasterRx() 定义

void enterMasterRx(pingPongFSM_t *const fsm)
{
  HAL_UART_Transmit(&huart2, "Master Rx start\r\n", 17, HAL_MAX_DELAY);
  SUBGRF_SetDioIrqParams( IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR,
                          IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR,
                          IRQ_RADIO_NONE,
                          IRQ_RADIO_NONE );
  SUBGRF_SetSwitch(RFO_LP, RFSWITCH_RX);
  packetParams.Params.Gfsk.PayloadLength = 0xFF;
  SUBGRF_SetPacketParams(&packetParams);
  SUBGRF_SetRx(fsm->rxTimeout << 6);
}

清单 3: main_gfsk.c 中的enterMasterTx() 定义


void enterMasterTx(pingPongFSM_t *const fsm)
{
  HAL_Delay(fsm->rxMargin);

  HAL_UART_Transmit(&huart2, "...PING\r\n", 9, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart2, "Master Tx start\r\n", 17, HAL_MAX_DELAY);
  SUBGRF_SetDioIrqParams( IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,
                          IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,
                          IRQ_RADIO_NONE,
                          IRQ_RADIO_NONE );
  SUBGRF_SetSwitch(RFO_LP, RFSWITCH_TX);
  // Workaround 5.1 in DS.SX1261-2.W.APP (before each packet transmission)
  SUBGRF_WriteRegister(0x0889, (SUBGRF_ReadRegister(0x0889) | 0x04));
  packetParams.Params.Gfsk.PayloadLength = 0x4;
  SUBGRF_SetPacketParams(&packetParams);
  SUBGRF_SendPayload((uint8_t *)"PING", 4, 0);
}

同样,请参阅GitHub Repository上的LoRa和GFSK调制方案的完整示例代码。

1 个赞

这个教程蛮实用的,直接剥离低层 Sub-GHz 驱动,省去中间件的冗余,对低功耗简单应用很友好了。 步骤也写得详细,从建项目到加驱动、配依赖都讲清了。

想问下实际用的时候,LoRa 和 FSK 调制的传输距离差得多吗?有没有遇到过寄存器配置后没正常通信的情况?

LoRa 与 FSK 的传输距离差得多吗?
结论:差别很大,尤其是在复杂的电磁环境下。

LoRa vs Narrowband FSK 对比优势

更大的灵敏度

LoRa 在典型应用下可实现相对于 FSK 更高的接收灵敏度。

更强的链路距离

Semtech 的环境测试表明 LoRa 在城市环境下典型范围约为 FSK 的多倍。

传播距离估算

一般而言,更高的灵敏度意味着无线链路距离可以呈现指数增长:一般按每 6 dB 灵敏度改进距离约翻倍。LoRa 可凭借其扩频增益,在同等传输功率下达到远距离覆盖。

更多内容,请看下面这篇Semtech的LoRa应用笔记:

这篇教程实用性有点强奥!怎么把 SubGHz_Phy 那套中间件从“巨型工程”拆出来,只保留 radio_driver.c/.h 这种底层 SUBGHZSPI 命令驱动,换取更可控的资源占用和功耗呢?这篇教程给了我们如何做的思路。
我有几个问题想问一下作者:
1.这里只开了 RX_DONE / TIMEOUT / CRC_ERROR(或 TX_DONE),如果现场干扰大,是否建议把某些错误中断也打开用于统计链路质量,或者说如何减少这些干扰?我之前做过一个带无线通信的项目,使用的nrf24l01,和lora很像,实际用下来不是特别理想,很容易受到外界干扰。
2.您文中提到 US915/EU868 等区域配置:如果做成产品,参数(频点、带宽、功率、占空比/跳频策略)最好做成“可配置表”。我想问我们实际项目里一般怎么组织更不容易踩法规坑?

这是一个非常专业且实际的问题。

帮您找了一个原厂应用笔记: 如何使用STM32CubeWL 构建 LoRa®应用程序

1. 如何拆离中间件并只保留底层驱动?
在 STM32CubeWL 的架构中,无线电驱动是分层定义的 。要实现最小化占用,你可以跳过上层的 LoRaWAN 和 SubGHz_Phy 中间件,直接操作以下两个核心部分:

  • 保留核心文件: 你只需要 radio_driver.c/.h(实现底层指令)和 radio_board_if.c/.h(实现射频开关、TCXO 和 DC/DC 硬件控制接口) 。

  • 硬件接口解耦: 上面的应用笔记建议,如果不需要完整的中间件,可以通过在 platform.h 中禁用 USE_BSP_DRIVER,然后在 radio_board_if.c 中直接实现射频开关(如控制 PC3/PC4/PC5 引脚)和电源管理(如开启 SMPS/DCDC)的功能 。

  • 直接调用底层 API: 弃用复杂的 RadioSetTxConfig 等中间件 API,改用底层 SUBGRF_SetRegulatorMode()、SUBGRF_SetRfFrequency() 等直接读写寄存器或发送 SPI 指令的函数,这能显著降低 Flash 和 RAM 的占用 。

2. 针对干扰大的环境,是否应开启更多中断?

nRF24L01 工作在 2.4GHz,干扰极多;LoRa 虽然工作在更干净的 Sub-GHz 频段且具有很强的抗噪能力,但在极端环境下仍需更精细的链路质量统计。

建议开启的中断:
HEADER_ERROR: 在 LoRa 模式下,如果该中断频繁触发,说明干扰已经严重到破坏了同步头。

链路质量统计(不一定要靠中断): 虽然你只开了 RX_DONE 等核心中断,但建议在每次 RX_DONE 后,通过底层 API 读取 RSSI(接收信号强度指示)和 SNR(信噪比) 。

如何减少干扰:

  • 利用 LBT(Listen Before Talk): 文档提到该技术可用于规避信道冲突,即“先听后发” 。

  • 调整扩频因子(SF): 增加 SF 会提高灵敏度和抗干扰能力,但会增加功耗和空中时间。

  • 开启 CRC: 确保 packetParams.Params.Gfsk.CrcLength 设置正确,以剔除坏包。

教程非常详细,感谢分享。

个人觉得Cube等软件都太大了,要装太多软件太麻烦了,

要是能直接提供MDK,IAR的Demo程序,

直接打开Demo程序进行开发更简单。

不知道测试性能怎么样?

1. 采用“驱动隔离”策略提升效率

隔离低层驱动:建议将底层无线驱动程序(radio_driver.c)与中间件(radio.c)隔离开来,直接在应用中调用底层命令12

手动配置项目结构:按照文中步骤,手动将 BSP 驱动、必要的实用程序(如 stm32_mem)以及 radio_driver 文件导入 STM32CubeIDE,以构建一个更轻量、高效的工程5…

2. 强化硬件安全防护

开启 SMPS 时钟检测:在初始化无线外设(radioInit)时,必须显式开启 SUBGHZ_SMPSC0R 寄存器中的 CLKDE 位,确保在时钟故障时系统能自动切换到 LDO 模式,防止硬件烧毁38

3. 修复与优化代码逻辑

修正随机数函数:在参考官方示例时,应检查并注释掉 RadioRandom() 中重新设置调制解调器的代码行,以防无线配置被意外擦除4

简化依赖关系:在独立使用底层驱动时,可以通过修改 radio_conf.hradio_driver.c,注释掉不需要的日志模块(mw_log_conf.h)和调试模块(sys_debug.h),进一步精简代码9

4. 灵活配置调制参数

直接操作系统参数结构体:通过 ModulationParams_t 等结构体直接配置 GFSK 或 LoRa 的带宽、比特率和频率偏差,而不是通过多层抽象函数转发,以获得更快的响应速度10

优化中断处理:利用 SUBGRF_SetDioIrqParams() 精确配置所需的无线电中断(如 IRQ_RX_DONE),从而实现更高效的状态机切换

收到,学习了,又涨见识了。