完整对频流程
触发方式
TX 进入对频:
- 手柄 Lua 脚本选 [Bind] → ACTION_BIND
- 调用 EnterBindingModeSafely() → EnterBindingMode()
RX 进入对频:
- 上电时 UID 为空 → updateBindingMode() 检测到未绑定
- 上电插拔3次 → PowerOnCounter >= 3
- OTA 收到绑定请求 → BindingModeRequest = true
- 按钮触发 → ACTION_BIND
TX 端对频流程
EnterBindingMode() // tx_main.cpp:1009
│
├── 1. 停定时器 + 等 TX 完成
│ hwTimer::stop();
│ while (busyTransmitting);
│
├── 2. 把 Master UID 封装成 MSP 数据包
│ SendUIDOverMSP(); // UID 通过 MSP over CRSF 发给 TX 模块
│
├── 3. 锁定到绑定速率
│ OtaCrcInitializer = 0; // 绑定模式 CRC 初始值 = 0
│ InBindingMode = true; // ← 全局标志位
│ SetRFLinkRate(RATE_BINDING); // 强制切换到 50Hz (SF8)
│ ↓
│ 绑定期间: 不发 RC 数据,只发 SYNC + MSP 包
│ MSP 包里装着 UID,反复发送
│
├── 4. 恢复定时器开始发送
│ hwTimer::resume();
│
└── 5. 每个时隙循环检查
if (BindingSendCount > 25) { // 发了 25 个 MSP 包后
ExitBindingMode(); // 自动退出绑定模式
}
#if RADIO_LR1121
// LR1121 特殊处理: 一半包在 900M 发,一半在 2.4G 发
// 确保无论 RX 在哪个频段都能收到
if (BindingSendCount == 12) {
SetRFLinkRate(RATE_DUALBAND_BINDING); // 切换到 2.4GHz 继续发
}
#endif
ExitBindingMode() // tx_main.cpp:1036
│
├── OtaUpdateCrcInitFromUid(); // 恢复正常的 CRC 初始化值
├── InBindingMode = false; // 退出绑定模式
└── SetRFLinkRate(config.GetRate()); // 恢复到用户设定的速率
RX 端对频流程
EnterBindingMode() // rx_main.cpp:1743
│
├── 1. 锁定到绑定速率
│ OtaCrcInitializer = 0; // CRC=0 (与 TX 匹配)
│ InBindingMode = true;
│ SetRFLinkRate(RATE_BINDING, true); // 强制 50Hz SF8
│ Radio.RXnb(); // 开始接收
│
└── 2. 等待接收 MSP 包
收到 SYNC → 无 UID,先不管
收到 MSP 包 → ProcessRfPacket_MSP()
└→ OnELRSBindMSP(payload)
UID[2..5] = payload[0..3] // 存入 UID
config.SetUID(UID); // 写入 EEPROM
updateBindingMode() 每循环检查:
if (config.IsModified()) { // UID 已写入 EEPROM
ExitBindingMode(); // 退出绑定
}
#if RADIO_LR1121 // 单频 LR1121 需要轮换频段
// 每 125ms 在 900M 和 2.4G 之间切换
// 确保能收到 TX 发来的绑定包(因为 TX 也在两个频段之间发送)
SetRFLinkRate(交替 RATE_BINDING ↔ RATE_DUALBAND_BINDING);
#endif
ExitBindingMode() // rx_main.cpp:1775
│
├── FHSSrandomiseFHSSsequence(uidMacSeedGet()); // ← 用新的 UID 生成跳频序列!
├── InBindingMode = false;
├── scanIndex = RATE_MAX; // 从最快的速率开始扫描
└── LostConnection(false); // 进入 disconnected 状态 → 开始速率遍历
绑定协议的本质
TX 端 (有Master UID) RX 端 (最初无UID)
───────────────────── ─────────────────
① 都锁定在 RATE_BINDING (50Hz SF8)
都设置 CRC_Init = 0
都强制 IQ 不反转 (InvertIQ)
都在 Sync Channel 频点上
跳频暂时停止 (InBindingMode 阻止 FHSS)
② TX 发 MSP 包 (封装 UID)
─────────────────────────→ ③ RX 收到,提取 UID
写入 EEPROM
④ TX 发够 25 包 → 自动退出绑定
⑤ RX 检测到 EEPROM 变更
生成新的 FHSS 序列 (基于 UID)
开始速率遍历找 TX
⑥ TX 也退出绑定,恢复用户速率
正常发 SYNC 包
⑦ RX 遍历到正确速率 → 收到 SYNC
✅ 连接成功!
关键:绑定只交换 UID。有了相同的 UID,双方独立生成完全相同的 FHSS 跳频序列(确定性伪随机),后续就能在各自配置的速率上通信。 绑定速率(50Hz)只是安全网------最可靠、距离最远、双方肯定支持的速率。
已配对后的接收机或和发射机,在不同时间点上电后,分别是怎么跳频操作的?
已配对后,TX/RX 不同时上电的重连流程
场景:TX 先上电,RX 后上电
TX 侧:上电就开始全频段跳
TX 上电
│
├── SetRFLinkRate(用户配置的速率)
│ ├── Radio.Config(SF/BW/CR..., FHSSgetInitialFreq()) // 从 sync channel 开始
│ ├── FHSSsetCurrIndex(0) // FHSS 指针归零
│ └── OtaNonce = 0 // 包计数归零
│
├── connectionState = disconnected
├── hwTimer::resume() // ← 定时器启动!
│
└── timerCallback() 每 20ms 触发 (50Hz):
├── OtaNonce++
├── 每 FHSHopInterval 包: FHSSptr++
├── 跳频: SetFrequencyReg(FHSSgetNextFreq())
├── 不在 sync_channel: 发 RC 数据
└── 在 sync_channel: 发 SYNC 包 (每 600ms 一次)
Sync 包包含: [fhssIndex=5, nonce=127, rateIndex=3, UID3, UID4, UID5]
↑ ↑
"我在序列的第5个位置" "我发了127个包了"
TX 在 40 个频道之间一直跳,不停歇。 无论 RX 什么时候来,TX 都在某个频率上发着包。
RX 侧:上电后死守一个频率,不做任何跳频
RX 上电
│
├── LostConnection(false)
│ ├── connectionState = disconnected
│ ├── hwTimer::stop() // ← 定时器停止! 不做任何定时任务
│ ├── SetRFLinkRate(配置速率)
│ │ └── Radio.Config(相同的SF/BW/CR..., FHSSgetInitialFreq())
│ │ // 频点 = sync_channel 频率 (FCC915 的 915MHz)
│ │ // 注意: FHSSptr 和 OtaNonce 没有重置!
│ └── Radio.RXnb() // 进入连续接收模式
│
└── 死守 sync channel 频率,等 RXdone 中断
不跳频、不定时、只在 sync channel 上持续监听
握手瞬间
TX 在频道间跳来跳去...
时间 →
TX: F0→F1→F2→...→F9→SyncCH→F11→...
↑
TX 终于跳到了 Sync Channel
在这个频率上驻留 4 个时隙 (80ms @50Hz)
发了一个 SYNC 包
RX: SyncCH══════════════════════收到!═══════════════════
死守不动 ↓
ProcessRFPacket_SYNC():
connectionState == disconnected ← 匹配!
→ FHSSsetCurrIndex(sync->fhssIndex) // 跳到TX的序列位置9
→ OtaNonce = sync->nonce // 匹配TX的包计数127
→ TentativeConnection() // 进入 tentative 状态
RXdoneISR 中:
→ HandleFHSS() // 不再返回false了! 开始正常跳频
→ hwTimer::resume() // 启动定时器!
TX: 继续 F11→F12→F13...
RX: ↑ ↑ ↑
已经在相同频率上了!
因为 FHSSptr 和 OtaNonce 完全同步
几个时隙后 → GotConnection() → connectionState = connected ✅
关键代码证明
RX 断开时不跳频: rx_main.cpp:408-413
bool HandleFHSS() {
uint8_t modresultFHSS = (OtaNonce + 1) % FHSHopInterval;
if (... || (connectionState == disconnected) || ...)
return false; // ← 断开时不跳频! 频率永远不变!
RX 定时器停止: rx_main.cpp:852-857
if (!InBindingMode) {
if (hwTimer::running) {
while(micros() - PFDloop.getIntEventTime() > 250);
hwTimer::stop(); // ← 停止定时器, 没有任何周期任务
}
收到 Sync 后启动定时器: rx_main.cpp:1194-1198
if (ProcessRFPacket(status)) {
didFHSS = HandleFHSS(); // 现在 connectionState ≠ disconnected → 会跳频了!
if (doStartTimer) {
doStartTimer = false;
hwTimer::resume(); // ← 启动定时器, RX 开始跟 TX 同步运行
}
}
时间线对比
TX 先上电, RX 后上电:
═══════════════════════════════════════════════════════════
TX: [上电]──跳频中─────────────────────────────────────────→
F0 F1 F2 ... SyncCH F11 ... F23 SyncCH ...
RX: [上电]─死守SyncCH────────────────收到SYNC!──同步跳频→
不跳不动 ↑
双方在SyncCH相遇
RX 最多等待时间: 一个完整 FHSS 遍历周期
FCC915 50Hz: 40ch × 4 × 20ms = 3.2 秒
ISM2G4 500Hz: 80ch × 4 × 2ms = 0.64 秒
极端情况
RX 先上电,TX 后上电:
RX: [上电]─死守SyncCH────────────────────────收到SYNC!──同步→
(可能等几秒) ↑
TX: [上电]─从SyncCH开始发!──────────────┘
FHSSptr=0, Nonce=0
第一站就是 Sync Channel!
几乎立即被 RX 收到
这种情况秒连,因为 TX 上电后的第一个频点就是 Sync Channel。
如果 RX 速率配错了呢?
TX: 50Hz SF8 到处跳
RX: 200Hz SF6 死守 SyncCH
→ 调制参数不匹配, RX 的解调器根本解不出 TX 的信号
→ cycleRfMode() 每隔几秒换一个速率
→ 遍历到 50Hz 时 → 收到 SYNC ✅
这就是之前讨论过的 cycleRfMode() 速率遍历机制在起作用。