射频芯片学习-ExpressLRS 对频流程分析 完整对频流程触发方式TX 进入对频: - 手柄 Lua 脚本选 [Bind] → ACTION_BIND - 调用 EnterBindingModeSafely() → EnterBindingMode() RX 进入对频: - 上电时 UID 为空 → updateBindingMode() 检测到未绑定 - 上电插拔3次 → PowerOnCounter 3 - OTA 收到绑定请求 → BindingModeRequest true - 按钮触发 → ACTION_BINDTX 端对频流程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; // CRC0 (与 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 包包含: [fhssIndex5, nonce127, rateIndex3, 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-413bool HandleFHSS() { uint8_t modresultFHSS (OtaNonce 1) % FHSHopInterval; if (... || (connectionState disconnected) || ...) return false; // ← 断开时不跳频! 频率永远不变!RX 定时器停止rx_main.cpp:852-857if (!InBindingMode) { if (hwTimer::running) { while(micros() - PFDloop.getIntEventTime() 250); hwTimer::stop(); // ← 停止定时器, 没有任何周期任务 }收到 Sync 后启动定时器rx_main.cpp:1194-1198if (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开始发!──────────────┘ FHSSptr0, Nonce0 第一站就是 Sync Channel! 几乎立即被 RX 收到这种情况秒连因为 TX 上电后的第一个频点就是 Sync Channel。如果 RX 速率配错了呢TX: 50Hz SF8 到处跳 RX: 200Hz SF6 死守 SyncCH → 调制参数不匹配, RX 的解调器根本解不出 TX 的信号 → cycleRfMode() 每隔几秒换一个速率 → 遍历到 50Hz 时 → 收到 SYNC ✅这就是之前讨论过的cycleRfMode()速率遍历机制在起作用。