射频芯片学习-ExpressLRS 对频流程分析

完整对频流程

触发方式

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

相关推荐
心中有国也有家1 小时前
E-Brufen 技术选型全景:Flutter + 鸿蒙 + Hive CE
hive·hadoop·学习·flutter·华为·harmonyos
Tom@敲代码2 小时前
js学习笔记-01
javascript·笔记·学习
辞旧 lekkk3 小时前
【Qt】 系统相关:事件与定时器
开发语言·qt·学习·萌新
zzzll11113 小时前
如何学习AI Agent?
java·人工智能·学习·程序员·大模型
默默敲代码的徐哥儿4 小时前
八股文整理——后端
java·开发语言·spring boot·后端·学习
巴巴媛6664 小时前
STM32学习笔记【32.BKP + RTC】
笔记·stm32·学习
网络与设备以及操作系统学习使用者6 小时前
通信的物理与逻辑双维度解析
学习·深度优先
浩瀚地学6 小时前
【Java基础复习】IO流(二)
java·开发语言·经验分享·笔记·学习