这次使用自制的STM32WL3R板卡来实现LoRa的收发。
首先参考 STM32Cube FW_WL3_V1.5.0 中的
MRSUBG_BasicGeneric_Tx、MRSUBG_BasicGeneric_Rx 和 802.15.4 Tx 示例,将工程改为 Basic
Packet 模式。
优先启用外部48Mhz高速晶振 使用内部RC会出现hardfault的情况
内部 HSI RC 可以让 CPU 正常运行,但在实际测试中
MRSUBG 发射序列无法完成
| 参数 |
最终采用值 |
说明 |
| 频率 |
868 MHz |
最终正常发射验证使用的频段 |
| 调制 |
2-GFSK 0.5 |
MOD_2GFSK05 |
| 速率 |
1 kbit/s |
255 字节包便于功率计捕获 |
| 频偏 |
20 kHz |
与接收端一致 |
| 带宽 |
100 kHz |
与接收端一致 |
| 前导码 |
16 |
Basic Packet |
| 同步字 |
0x88888888,31 |
|
| bit |
收发两端必须一致 |
|
| CRC |
8 bit |
接收只接受 CRC 正确的数据 |
| 数据白化 |
开启 |
收发两端一致 |
配置代码参考如下
MRSUBG_RadioInitStruct.lFrequencyBase = 868000000U;
MRSUBG_RadioInitStruct.xModulationSelect = MOD_2GFSK05;
MRSUBG_RadioInitStruct.lDatarate = 1000U;
MRSUBG_RadioInitStruct.lFreqDev = 20000U;
MRSUBG_RadioInitStruct.lBandwidth = 100000U;
MRSUBG_RadioInitStruct.outputPower = 20;
MRSUBG_RadioInitStruct.PADrvMode = PA_DRV_TX_TX_HP;
在这个项目中 我们采用AB乒乓通信的方式进行收发 也就是按下A的按键时 B进行动作 按下B的按键时 A进行动作,两端通信类似半双工,这样就有一个收发状态机切换的问题
按键发送前射频仍处于 RX 状态,代码发送 CMD_SABORT 后立即切换 TX,会导致无法响应的问题,我们新增一个中间态 STATE_IDLE,在 RX 到 TX、TX 到 RX 的切换前增加状态机等待:
static void RadioAbortAndWaitIdle(void)
{
uint32_t abortTick;
__HAL_MRSUBG_STROBE_CMD(CMD_SABORT);
abortTick = HAL_GetTick();
while ((MR_SUBG_GLOB_STATUS->RADIO_FSM_INFO & 0x1FU) != STATE_IDLE)
{
if ((uint32_t)(HAL_GetTick() - abortTick) >= 20U)
{
break;
}
}
}
按下按键发送后,等PHY返回IDLE状态再进行状态切换
发送代码我们使用Basic Packet
配置部分如下代码
MRSUBG_PacketSettingsStruct.PreambleLength = 16U;
MRSUBG_PacketSettingsStruct.PostambleLength = 0U;
MRSUBG_PacketSettingsStruct.SyncLength = 31U;
MRSUBG_PacketSettingsStruct.SyncWord = 0x88888888U;
MRSUBG_PacketSettingsStruct.FixVarLength = VARIABLE;
MRSUBG_PacketSettingsStruct.PreambleSequence = PRE_SEQ_0101;
MRSUBG_PacketSettingsStruct.PostambleSequence = POST_SEQ_0101;
MRSUBG_PacketSettingsStruct.CrcMode = PKT_CRC_MODE_8BITS;
MRSUBG_PacketSettingsStruct.Coding = CODING_NONE;
MRSUBG_PacketSettingsStruct.DataWhitening = ENABLE;
MRSUBG_PacketSettingsStruct.LengthWidth = BYTE_LEN_1;
MRSUBG_PacketSettingsStruct.SyncPresent = ENABLE;
发送部分代码建立了一块四字节对齐的RAM
__attribute__((aligned(4))) static uint8_t TxPayload[TX_PAYLOAD_SIZE]; // 32 字节
__attribute__((aligned(4))) volatile uint8_t RxPayload[TX_PAYLOAD_SIZE]; // 32 字节
发送代码实现
static void RadioPrepareTx(void)
{
RadioAbortAndWaitIdle(); // 停掉 RX 并等状态机回到 IDLE
__HAL_MRSUBG_SET_TX_MODE(TX_NORMAL); // 只提供 payload,前导/同步/长度/CRC 由硬件生成
HAL_MRSubG_PktBasicSetPayloadLength(TX_PAYLOAD_SIZE); //写 DATABUFFER_SIZE(容量) 和 PCKTLEN(包长) = 32
__HAL_MRSUBG_SET_DATABUFFER0_POINTER((uint32_t)TxPayload); // 告诉硬件数据在 RAM 哪里
}
发送状态机是很重要的 否则就会导致发送一次后TX卡在某个状态无法恢复
static uint8_t RadioSendPacket(void)
{
uint32_t txStartTick = HAL_GetTick();
uint32_t irq;
RadioPrepareTx();
__HAL_MRSUBG_CLEAR_RFSEQ_IRQ_FLAG(__HAL_MRSUBG_GET_RFSEQ_IRQ_STATUS()); // 清旧标志
__DMB(); // 确保上面的寄存器/缓冲区写在命令前生效
__HAL_MRSUBG_STROBE_CMD(CMD_TX); // 打开发射序列
do { // 轮询等待硬件发完
irq = __HAL_MRSUBG_GET_RFSEQ_IRQ_STATUS();
RadioLastIrq = irq;
if ((irq & (MR_SUBG_GLOB_STATUS_RFSEQ_IRQ_STATUS_TX_DONE_F | TX_ERROR_FLAGS)) != 0U)
break;
} while ((uint32_t)(HAL_GetTick() - txStartTick) < TX_TIMEOUT_MS); // 500ms 超时
RadioLastDurationMs = HAL_GetTick() - txStartTick;
RadioLastFsm = MR_SUBG_GLOB_STATUS->RADIO_FSM_INFO;
if ((irq & TX_DONE_F) && ((irq & TX_ERROR_FLAGS) == 0U)) {
RadioTxCount++; RadioLastTxOk = 1U;
TxPayload[TX_PAYLOAD_SIZE - 1U]++; // 载荷末字节自增,当包序号
__HAL_MRSUBG_CLEAR_RFSEQ_IRQ_FLAG(irq); // 标志是写 1 清零,把读到的值写回即可
return 1U;
}
RadioTxErrorCount++; RadioLastTxOk = 0U;
__HAL_MRSUBG_STROBE_CMD(CMD_SABORT); // 异常则中止
return 0U;
}
收到 CMD_TX 后 FSM 走 SYNTH_SETUP → CALIB_VCO → LOCKONTX → EN_PA → TX,通过自己的 AHB 主机按 DATABUFFER0_PTR 读走 32 字节 payload,自动加前导码/同步字/长度字段/8bit CRC/白化,发完后置 TX_DONE_F。若 AHB 取数出错会置 `AHB_ACCESS_ERROR_F
接收函数的实现和发送一样都是阻塞式轮询的
RadioLastRxIrq = __HAL_MRSUBG_GET_RFSEQ_IRQ_STATUS();
if ((RadioLastRxIrq & MR_SUBG_GLOB_STATUS_RFSEQ_IRQ_STATUS_RX_OK_F) != 0U) // 收到且 CRC 正确
{
RadioRxCount++;
RadioLastRxLength = __HAL_MRSUBG_GET_DATABUFFER_COUNT(); // 硬件实际写入 RxPayload 的字节数
__HAL_MRSUBG_CLEAR_RFSEQ_IRQ_FLAG(RadioLastRxIrq);
SEGGER_RTT_printf(0, "Hello STM32, timestamp=%lu ms, rx_count=%lu, length=%lu\r\n", ...);
{ /* 读链路质量:RSSI_SYNC / RSSI_RUN / AGC / PQI / SQI / AFC */
uint32_t rxIndicator = __HAL_MRSUBG_GET_RX_INDICATOR(); // bit0-8=RSSI_ON_SYNC, bit12-20=RSSI_RUN, bit24-27=AGC
uint32_t qiInfo = MR_SUBG_GLOB_STATUS->QI_INFO; // bit0-7=PQI, bit8-13=SQI, bit16-23=AFC
int32_t rssiDbm = HAL_MRSubG_GetRssidBm();
int32_t rssiOnSyncDbm = __HAL_MRSUBG_CONVERT_RSSI_TO_DBM(rssiOnSyncRaw);
int32_t rssiRunDbm = __HAL_MRSUBG_CONVERT_RSSI_TO_DBM(rssiRunRaw);
SEGGER_RTT_printf(0, "RF RX: RSSI_SYNC=%ld dBm, ... QI=0x%08lX\r\n", ...);
}
/* PB0 拉高 200ms 表示收到一个 CRC 正确包 */
HAL_GPIO_WritePin(RX_ACTIVITY_GPIO_Port, RX_ACTIVITY_Pin, GPIO_PIN_SET);
rxIndicatorOffTick = HAL_GetTick() + RX_INDICATE_MS;
rxIndicatorActive = 1U;
RadioStartRx(); // 等待下一包
}
else if ((RadioLastRxIrq & MR_SUBG_GLOB_STATUS_RFSEQ_IRQ_STATUS_RX_CRC_ERROR_F) != 0U)
{
RadioRxCrcErrorCount++;
__HAL_MRSUBG_CLEAR_RFSEQ_IRQ_FLAG(RadioLastRxIrq);
RadioStartRx(); // CRC 错误
}
接收处理函数,在进入接收后 会停止正在进行的TXRX动作 保证每个包都会有单独的接收窗口 防止粘包
static void RadioAbortAndWaitIdle(void) //
{
uint32_t abortTick;
__HAL_MRSUBG_STROBE_CMD(CMD_SABORT); // 停掉正在进行的 RX/TX
abortTick = HAL_GetTick();
while ((MR_SUBG_GLOB_STATUS->RADIO_FSM_INFO & 0x1FU) != STATE_IDLE) { // 必须等 FSM 真回到 IDLE(0x00)
if ((uint32_t)(HAL_GetTick() - abortTick) >= 20U) break; // 最多等 20ms
}
}
static void RadioPrepareRx(void)
{
RadioAbortAndWaitIdle();
__HAL_MRSUBG_SET_RX_MODE(RX_NORMAL); // payload 存进 RAM,CRC/长度在状态寄存器
HAL_MRSubG_PktBasicSetPayloadLength(TX_PAYLOAD_SIZE); // 缓冲区容量/包长 32(与发送端一致)
__HAL_MRSUBG_SET_DATABUFFER0_POINTER((uint32_t)RxPayload); // 接收数据落地点
}
static void RadioStartRx(void)
{
RadioPrepareRx();
__HAL_MRSUBG_CLEAR_RFSEQ_IRQ_FLAG(__HAL_MRSUBG_GET_RFSEQ_IRQ_STATUS());
__DMB();
__HAL_MRSUBG_STROBE_CMD(CMD_RX); // 打开接收序列
}
这里一直轮询PHY的状态 必须等phy到IDLE状态才能开始下一轮接收 否则就会导致异常出现
在这套工程中我们也实现了SeggerRTT的组件 可以用RTT的printf来实现高效的log输出

如图就是每次按下按键后 A板发送Hello STM32 B板接收并打印出字符串 同时将RF信息也一起打印出来 方便调试人员调参,按键初始化这些过于简单就不在继续讲述了
<iframe src="https://player.bilibili.com/player.html?isOutside=true&aid=117349126703249&bvid=BV1mfaG6PExZ&cid=42282452013&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
<iframe src="https://player.bilibili.com/player.html?isOutside=true&aid=117349126703249&bvid=BV1mfaG6PExZ&cid=42282452013&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
<iframe src="https://player.bilibili.com/player.html?isOutside=true&aid=117349126703249&bvid=BV1mfaG6PExZ&cid=42282452013&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>