简介
看到ST新出了一款低功耗无线MCU STM32WL3R很感兴趣,于是果断下单样品准备尝鲜,先前有购买过WLE55的模组 准备先熟悉下sub-Ghz的软件框架。
在此之前还是先对比这两款MCU
STM32WL3R && STM32WLE55
| 特性 |
STM32WL3R |
STM32WLE55 |
| 核心架构 |
单核 Arm® Cortex®-M0+[] |
双核 Arm® Cortex®-M4 + Cortex®-M0+ |
| 主频 |
最高 64 MHz |
最高 48 MHz |
| Flash 最大容量 |
128 KB |
256 KB |
| SRAM 最大容量 |
16 KB |
64 KB |
| 主要无线协议 |
2/4(G)FSK, OOK, ASK, D-BPSK, DSSS[] |
LoRa® |
| TX 最大输出功率 |
+20 dBm |
+22 dBm |
|
|
|
| 10dBm TX 电流 |
8 mA |
15 mA |
| 超深度停止模式 |
450 nA (RAM 数据保持) |
1.07 µA (Stop2 模式) |
| 关断模式电流 |
14 nA (6个唤醒引脚) |
31 nA |
WL3R更偏向的是低功耗终端 WLE55偏向高性能复杂物联网设计 最主要的是WLE55支持LoRa
代码框架设计
cubemx配置
tiao调试串口 方便后续跟踪log

使能sub-Ghz收发器,以及收发中断

重点

射频参数按照如上配置 使能串口log模块 禁用低功耗模式 调制使用LoRa 频段暂定868Mhz 后续可以更改
带宽125Khz

设定好平台参数 指定调试串口
然后就生成代码
框架实现思路与功能
代码参考了官方的pingpong例程实现 抽象了发送函数,实现一行调用即可发送,另外实现了功率调试 最高22dbm输出,软件层面实现了CRC16校验+ACK应答+自动重传极致,大大提高了稳定性
项目结构
├─ Core/ # CubeMX 生成的内核层
│ ├─ Inc/
│ └─ Src/
│ ├─ main.c # 入口:时钟/外设初始化 +
│ ├─ subghz.c # SUBGHZ外设初始化
│ ├─ usart.c / usart_if.c # 调试串口
│ ├─ sys_app.c # APP_LOG / 日志系统
│ └─ stm32wlxx_it.c #
├─ SubGHz_Phy/
│ ├─ App/
│ │ ├─ subghz_phy_app.c # 收发状态机 +传输协议
│ │ ├─ subghz_phy_app.h # 频率/功率/调制参数
│ │ └─ app_subghz_phy.c # MX_SubGHz_Phy_Init/Process
│ └─ Target/
│ ├─ radio_conf.h # 射频中间件配置
│ └─ radio_board_if.c # 射频开关/PA 等板级适配
├─ Middlewares/Third_Party/SubGHz_Phy/
│ ├─ radio.h # Radio 驱动 API(Radio.Send/Rx/Standby...)
│ └─ stm32_radio_driver/ # STM32WL 底层驱动
├─ Utilities/ # sequencer(任务调度)、timer(软定时器)、trace ...
├─ Drivers/ # CMSIS + STM32WLxx HAL
├─ BSP/STM32WLxx_LM401/ # 板级支持(LED / 按键 / 射频开关)
└─ MDK-ARM/
射频事件(收完/发完/超时)在 中断上下文 回调里只做两件事 :更新状态、用 UTIL_SEQ_SetTask() 排一个后台任务。射频操作(发送、重进接收、协议处理)全部放在后台任务 SubghzApp_Process() 里,在主循环上下文执行。这样中断里不操作SPI接口 。
│ 仅:State = ... ; UTIL_SEQ_SetTask(...)
▼
[主循环] UTIL_SEQ_Run() → SubghzApp_Process()
│ 按 State 分发:
│ RX_DONE → Proto_HandleRxFrame() 解析/校验CRC/回ACK或NACK/上报
│ TX_DONE → 若刚发的是 DATA,启动 ACK 等待定时器
│ RX_ERROR → 记录
│ ACK_TIMEOUT → Proto_Retransmit() 重传或判失败
└─ 收尾:有待发帧则 Radio.Send(),否则(且不在发送中)Radio.Rx() 继续监听
校验机制和自动重传触发
收发校验和自动重传参考以下流程
发送端 A 发送端 B
──────── ────────
SubghzApp_Send()
TX DATA(seq=N) ─────────────────── 收到 → 计算CRC并与帧内CRC比较
启动 ACK 定时器(1s) ├─ 一致 → 上报数据, 回 ACK(seq=N)
└─ 不一致 → 回 NACK(seq=N)
收到 ACK(seq=N) ───────────────────┘
→ transfer OK, seq++── 若收到 NACK 或 ACK 超时 ──
重传 DATA(seq=N) (最多 MAX_RETRIES=3 次)
仍失败 → transfer FAILED
依靠这个机制我们就可以实现数据可靠的发送与接收 防止对面接收到坏帧的情况
抽像发送函数
为了方便收发 我们直接将发送函数进行二次封装
bool SubghzApp_Send(const uint8_t *data, uint8_t size)
{
if (!data || size == 0 || size > MAX_PAYLOAD_LEN) return false; /* 参数检查 */
if (awaitingAck) return false; /* 停等:同一时刻仅一帧在途 */
lastFrameLen = Proto_BuildFrame(lastFrame, FRAME_TYPE_DATA, txSeq, data, size);
retryCount = 0; awaitingAck = 1;
Proto_QueueTx(lastFrame, lastFrameLen, FRAME_TYPE_DATA); /* 入队 + 唤醒任务 */
return true;
}
使用队列确保同一时刻只有1个帧在使用硬件,组好 DATA 帧存入 lastFrame(供重传),置 awaitingAck,交给任务发送;真正的 Radio.Send 在任务里完成
后台主任务subghzApp_Process
static void SubghzApp_Process(void)
{
switch (State) { /* 1) 按事件分发 */
case RX_DONE: BSP_LED_Toggle(LED_GREEN); Proto_HandleRxFrame(); break;
case TX_DONE: BSP_LED_Toggle(LED_RED);
if (txFrameType == FRAME_TYPE_DATA && awaitingAck)
UTIL_TIMER_Start(&ackTimer); /* 刚发的是 DATA → 开始等 ACK */
break;
case RX_ERROR: APP_LOG(...,"RxError\n\r"); break;
case ACK_TIMEOUT: if (awaitingAck) { APP_LOG(...,"ACK timeout..."); Proto_Retransmit(); } break;
default: break;
}
if (TxPending != 0) { /* 2) 有待发帧:先发 */
TxPending = 0; State = TX;
Radio.Standby(); /* 发送前置于干净状态 */
Radio.Send(BufferTx, TxSize);
} else if (State != TX) { /* 否则继续监听 —— */
State = RX; /* "State!=TX" 守卫见 §9.2:*/
Radio.Rx(RX_TIMEOUT_VALUE); /* 防止重入时打断在途发送 */
}
}
简单实现了一个状态机 依靠不同的状态来实现相应行为
实际操作演示

如图 上电后会自动打印PHY状态与初始化参数 按下按键后 会触发发送函数,对端接收到数据后 会计算CRC数据并进行校验 校验通过后会回ACK包 这是一帧完整的收发流程,如果发生撞包或者坏包 就会触发自动重传流程
功率测试

上图为降功率发送的效果 因为频谱仪只有15dbm最大输入 没有配置衰减器 实测22dbm可以稳定输出 电流120ma@5V