射频芯片学习-ExpressLRS 对频流程分析
2026/7/8 6:36:50 网站建设 项目流程

完整对频流程

触发方式

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()速率遍历机制在起作用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询