源码信息
本文分析基于 ExpressLRS 开源项目,具体信息如下:
-
版本号:3.5.6
说明:本文旨在解读跳频机制的实现原理,所有代码和分析均来自上述开源项目。读者可访问仓库获取最新源码、提交问题和参与开发。
一、引言:为什么要跳频?
在无线通信中,跳频(Frequency Hopping Spread Spectrum,简称FHSS)是一种重要的抗干扰技术。它通过让收发双方按照预定序列在多个频道间快速切换,实现以下目标:
-
抗干扰:避免长期停留在某个被干扰的频道
-
提高可靠性:分散通信风险,单一频道故障不影响整体链路
-
增强安全性:未经授权的设备难以跟踪通信频率
ExpressLRS(ELRS)作为开源高频宽遥控链路系统,其跳频机制的实现既高效又精巧。
二、整体架构:Tx与Rx如何同步跳频?
ELRS的跳频机制基于“时隙同步”原则:Tx(发射端)和Rx(接收端)按照相同的序列和节奏切换频率。
┌─────────────┐ 同步频道 ┌─────────────┐ │ Tx │───────────────>│ Rx │ │ 发射端 │ │ 接收端 │ └─────────────┘ └─────────────┘ │ │ ├── 1. 基于相同种子生成序列 ──┤ ├── 2. 同时从索引0开始计时 ──┤ └── 3. 按相同间隔跳至下一频 ──┘
关键同步要素:
-
相同种子:基于设备UID生成
-
相同序列:256个频道索引的排列
-
相同节奏:每N个数据包跳频一次(FHSShopInterval)
三、Tx侧跳频实现详解
3.1 初始化阶段:奠定跳频基础
Tx的初始化在tx_main.cpp的setup()函数中完成,主要步骤包括:
void setup() {
// 1. 检查硬件配置
if (setupHardwareFromOptions()) {
// 2. 读取设备唯一标识符
setupBindingFromConfig();
// 3. ★核心步骤:初始化跳频序列
// 基于UID生成随机种子,创建伪随机跳频序列
FHSSrandomiseFHSSsequence(uidMacSeedGet());
// 4. 注册无线电回调函数
Radio.RXdoneCallback = &RXdoneISR;
Radio.TXdoneCallback = &TXdoneISR;
// 5. 设置初始频率(同步频道)
Radio.currFreq = FHSSgetInitialFreq();
// 6. 初始化射频模块
Radio.Begin(FHSSgetMinimumFreq(), FHSSgetMaximumFreq());
// 7. 启动定时器控制时隙
hwTimer::init(nullptr, timerCallback);
}
}
3.2 跳频序列生成算法
跳频序列是整套机制的核心。ELRS采用“分块随机置换”算法,确保序列既随机又包含必要的同步点。
void FHSSrandomiseFHSSsequence(const uint32_t seed) {
// 1. 获取频域配置(如2.4GHz ISM频段)
FHSSconfig = &domains[firmwareOptions.domain];
// 2. 计算同步频道位置(通常位于频段中部)
sync_channel = (FHSSconfig->freq_count / 2) + 1;
// 3. 计算频道间隔(带缩放因子保证精度)
freq_spread = (FHSSconfig->freq_stop - FHSSconfig->freq_start)
* FREQ_SPREAD_SCALE / (FHSSconfig->freq_count - 1);
// 4. 构建跳频序列
FHSSrandomiseFHSSsequenceBuild(seed, FHSSconfig->freq_count,
sync_channel, FHSSsequence);
}
序列构建算法的巧妙之处:
-
分块结构:将256个位置分为若干块,每块大小等于频道总数
-
固定同步点:每块的第0位置固定为同步频道,确保定期回归
-
块内随机化:每块内部的其他位置随机交换,实现伪随机跳频
// 简化版算法逻辑示意
for (每个块) {
块[0] = 同步频道; // 固定为同步点
块[同步频道] = 0; // 避免冲突
for (块内其他位置) {
与同块内随机位置交换; // 实现随机化
}
}
3.3 跳频触发时机
Tx在每次发送完成后判断是否需要跳频:
void TXdoneISR() {
if (busyTransmitting) {
// 检查是否为跳频时机
HandleFHSS();
busyTransmitting = false;
}
}
void HandleFHSS() {
// 计算下一个包是否应该跳频
uint8_t modresult = (OtaNonce + 1) % ExpressLRS_currAirRate_Modparams->FHSShopInterval;
// modresult == 0 表示到达跳频点
if (!InBindingMode && modresult == 0) {
// 切换到下一个频率
Radio.SetFrequencyReg(FHSSgetNextFreq());
}
}
关键变量说明:
-
OtaNonce:数据包计数器,每个时隙递增 -
FHSShopInterval:跳频间隔,如设置为47表示每48个包跳频一次 -
FHSSptr:当前在跳频序列中的位置索引
四、Rx侧跳频实现详解
4.1 初始化:与Tx保持同步
Rx的初始化流程与Tx高度一致,确保双方从相同的起点开始:
void setup() {
// 与Tx完全相同的初始化步骤
FHSSrandomiseFHSSsequence(uidMacSeedGet());
setupRadio();
// Rx特有的定时器初始化
hwTimer::init(HWtimerCallbackTick, HWtimerCallbackTock);
}
4.2 双保险跳频触发机制
Rx采用“中断+定时器”双触发机制,确保跳频绝不遗漏:
机制一:数据包接收完成触发(RXdoneISR)
bool RXdoneISR(SX12xxDriverCommon::rx_status const status) {
if (ProcessRFPacket(status)) {
// 尝试处理跳频
didFHSS = HandleFHSS();
return true;
}
return false;
}
机制二:定时器后备触发(HWtimerCallbackTock)
void HWtimerCallbackTock() {
// 如果RXdoneISR没有处理跳频,则在这里补上
if (!didFHSS) {
HandleFHSS();
}
didFHSS = false; // 重置标志
}
这种设计确保了即使某个数据包丢失,Rx仍能通过定时器保持跳频同步。
4.3 定时器的双重职责
Rx定时器有两个回调函数,相位相差180度:
| 回调函数 | 触发时机 | 主要职责 |
|---|---|---|
HWtimerCallbackTick | 数据包接收中期 | 更新时隙计数、发送控制数据 |
HWtimerCallbackTock | 数据包接收间隙 | 跳频处理、周期性任务 |
void HWtimerCallbackTick() {
OtaNonce++; // ★关键:推进时隙计数器
SendRCdataToRF(); // 发送控制数据到射频
// ...其他中间处理
}
void HWtimerCallbackTock() {
if (!didFHSS) {
HandleFHSS(); // 后备跳频触发
}
// ...周期性维护任务
}
五、关键技术细节解析
5.1 设备UID到跳频种子的转换
跳频序列的随机性来源于设备唯一标识符(UID):
uint32_t uidMacSeedGet() {
// 提取UID的最后4字节,并与版本号异或
const uint32_t macSeed = ((uint32_t)UID[2] << 24) +
((uint32_t)UID[3] << 16) +
((uint32_t)UID[4] << 8) +
(UID[5] ^ OTA_VERSION_ID);
return macSeed;
}
设计要点:
-
每个设备有唯一的跳频序列
-
固件版本更新(OTA_VERSION_ID变化)会改变序列,避免版本间干扰
-
种子计算高效,适合在启动时快速执行
5.2 频率计算方法
频道索引到实际频率的转换:
static inline uint32_t FHSSgetNextFreq() {
// 1. 前进到序列中的下一个位置
FHSSptr = (FHSSptr + 1) % FHSSgetSequenceCount();
// 2. 获取频道索引
uint8_t channelIndex = FHSSsequence[FHSSptr];
// 3. 计算实际频率
// 公式:起始频率 + (间隔 × 索引 / 缩放因子) - 频率修正
return FHSSconfig->freq_start +
(freq_spread * channelIndex / FREQ_SPREAD_SCALE) -
FreqCorrection;
}
5.3 伪随机数生成器
ELRS使用线性同余生成器(LCG)产生随机数:
uint16_t rng(void) {
const uint32_t m = 2147483648; // 2^31
const uint32_t a = 214013; // 乘数
const uint32_t c = 2531011; // 增量
seed = (a * seed + c) % m; // LCG公式
return seed >> 16; // 取高16位作为随机数
}
特点:
-
算法简单,计算速度快
-
周期足够长(2^31),满足跳频需求
-
确定性:相同种子产生相同序列,保证Tx/Rx同步
六、跳频机制总结
6.1 设计亮点
-
完全同步:Tx和Rx基于相同算法和种子生成完全一致的跳频序列
-
容错机制:Rx端双重触发确保跳频不遗漏
-
灵活可调:通过
FHSShopInterval参数控制跳频速度 -
资源高效:算法复杂度低,适合嵌入式平台
6.2 工作流程总结
启动阶段: 1. 读取设备UID 2. 生成随机种子 3. 构建跳频序列(256个频道索引) 4. 设置初始频率(同步频道) 运行阶段(Tx): 1. 发送数据包 2. 包计数器+1 3. 检查是否到达跳频点 4. 若到达,切换到序列中的下一个频率 运行阶段(Rx): 1. 接收数据包(触发跳频检查) 2. 定时器到期(后备跳频检查) 3. 保持与Tx完全同步的频率切换
结语
ELRS的跳频机制展示了一个精巧的嵌入式无线通信系统设计范例。它通过简洁的算法、高效的实施和鲁棒的同步机制,在有限的硬件资源上实现了可靠的跳频通信。
这种设计不仅适用于遥控模型领域,也为其他需要可靠无线通信的嵌入式应用提供了参考。通过深入理解这套机制,开发者可以更好地调试ELRS系统,甚至借鉴其设计思想用于自己的项目中。
致谢:感谢所有ExpressLRS开源项目的贡献者,他们的工作使这份技术解读成为可能。
ExpressLRS跳频机制解析
269

被折叠的 条评论
为什么被折叠?



