射频芯片学习-ExpressLRS 射频芯片配置完整分析

ExpressLRS 射频芯片配置完整分析

一、涉及的射频芯片与文件结构

ELRS 支持四种射频芯片平台,核心文件如下:

芯片 频段 调制支持 核心文件
SX127x (SX1276/1278) 900MHz / 433MHz LoRa SX127x.cpp, SX127xRegs.h
SX1280 2.4GHz LoRa, FLRC SX1280.cpp, SX1280_Regs.h
LR1121 900MHz + 2.4GHz (双频) LoRa, GFSK LR1121Driver

FHSS(跳频)实现在 FHSS.cpp 和 FHSS.h。

配置入口表在 common.cpp 中,按 RADIO_SX127XRADIO_SX128XRADIO_LR1121四个编译宏分支。


二、LoRa 配置参数详解

2.1 SX127x (900MHz) 完整配置表

所有模式共用 BW=500kHz, 隐式包头(Implicit Header), CRC禁用

速率 间隔 SF CR Preamble Payload 灵敏度 跳频间隔
200Hz 5000µs SF6 4/7 8 8字节(OTA4) -112dBm 4包一跳
100Hz (8CH) 10000µs SF6 4/8 8 13字节(OTA8) -112dBm 4包一跳
100Hz 10000µs SF7 4/7 8 8字节(OTA4) -117dBm 4包一跳
50Hz (绑定) 20000µs SF8 4/7 10 8字节(OTA4) -120dBm 4包一跳
25Hz 40000µs SF9 4/7 10 8字节(OTA4) -123dBm 2包一跳
DVDA 50Hz 5000µs SF6 4/7 8 8字节(OTA4) -112dBm 2包一跳 (4次重复发送)

2.2 SX128x (2.4GHz LoRa) 完整配置表

所有模式共用 BW=800kHz, CR_LI_4/6或4/8, 隐式包头

速率 间隔 SF CR Preamble Payload 灵敏度 跳频间隔
500Hz 2000µs SF5 4/6(LI) 12 8字节 -105dBm 4包一跳
333Hz (8CH) 3003µs SF5 4/8(LI) 12 13字节 -105dBm 4包一跳
250Hz 4000µs SF6 4/8(LI) 14 8字节 -108dBm 4包一跳
150Hz 6666µs SF7 4/8(LI) 12 8字节 -112dBm 4包一跳
100Hz (8CH) 10000µs SF7 4/8(LI) 12 13字节 -112dBm 4包一跳
50Hz (绑定) 20000µs SF8 4/8(LI) 12 8字节 -115dBm 2包一跳

2.3 SX128x (2.4GHz FLRC 高速模式)

FLRC 是一种高速窄带调制,用于 500Hz/1000Hz 极致低延迟模式:

速率 间隔 比特率 BW CR Preamble Payload 灵敏度
FLRC 1000Hz 1000µs 0.650 Mbps 0.6 1/2 32 8字节 -104dBm
FLRC 500Hz 2000µs 0.650 Mbps 0.6 1/2 32 8字节 -104dBm
DVDA 500Hz 1000µs 0.650 Mbps 0.6 1/2 32 8字节(2次发送) -104dBm
DVDA 250Hz 1000µs 0.650 Mbps 0.6 1/2 32 8字节(4次发送) -104dBm
FSK DVDA 500Hz 1000µs GFSK 300kbps 467kHz - 16 8字节(2次发送) -103dBm
FSK 1000Hz 1000µs GFSK 300kbps 467kHz - 16 8字节 -101dBm

2.4 SX128x 关键寄存器配置细节

在 SX1280.cpp:277-306 中:

复制代码
// ConfigModParamsLoRa: 发送 SetModulationParams 命令
uint8_t rfparams[3] = {sf, bw, cr};  // 三元组直接发到芯片

// SF 相关的额外配置寄存器 (SX1280_REG_SF_ADDITIONAL_CONFIG = 0x925):
// SF5/SF6 → 写 0x1E
// SF7/SF8 → 写 0x37
// SF9/SF10/SF11/SF12 → 写 0x32

// SetPacketParamsLoRa:
// HeaderType = FIXED_LENGTH (隐式包头, 节省2字节头开销)
// CRC = OFF (SX1280_LORA_CRC_OFF, 不浪费CRC字节)

2.5 设计理念总结

ELRS 的 LoRa 参数设计遵循以下原则:

  1. 速度优先 → 先用最大 BW (500kHz/800kHz) 和最小 SF,随距离增加逐级降速
  2. 隐式包头 → 因为双方预先知道 Payload 长度,不发送显式包头,节省2字节
  3. 无 LoRa CRC → 自己实现了更轻量级的 CRC(OTA4 用8位CRC,OTA8 用16位CRC),因为 LoRa 硬件 CRC 不是标准 CRC 且占用芯片寄存器
  4. 无 Sync Word → LoRa 模式不使用 sync word 寄存器(FLRC 才有硬件 sync word)
  5. LI (Long Interleaver) → SX1280 使用 LI 版本的 CR,在相同码率下提供更好的抗突发干扰能力
  6. Preamble 优化 → 选择刚好能让接收机检测到信号的最小 preamble 长度

三、跳频策略完整分析

3.1 频域划分

900MHz 频段 (SX127x):
起始频率 终止频率 频道数 频道间隔 总带宽
FCC915 903.5 MHz 926.9 MHz 40 ~600 kHz 23.4 MHz
AU915 915.5 MHz 926.9 MHz 20 ~600 kHz 11.4 MHz
EU868 865.275 MHz 869.575 MHz 13 ~358 kHz 4.3 MHz
IN866 865.375 MHz 866.95 MHz 4 ~525 kHz 1.575 MHz
US433W 423.5 MHz 438.0 MHz 20 ~763 kHz 14.5 MHz
2.4GHz 频段 (SX1280):
起始频率 终止频率 频道数 频道间隔 总带宽
ISM2G4 / CE_LBT 2400.4 MHz 2479.4 MHz 80 ~1 MHz 79 MHz

3.2 跳频序列生成算法

这是 ELRS 跳频的核心,实现在 FHSS.cpp:123-163 的 FHSSrandomiseFHSSsequenceBuild()

设计约束:

  1. freq_count 个跳频位置,Sync Channel 出现一次(在区块首位)
  2. 同一区块内无重复频道
  3. 每个频道出现频率尽量均等
  4. 伪随机

算法步骤:

复制代码
Step 1: 初始化 256 长度的 FHSSsequence 数组
  → 将序列按 freq_count 分块,每块内 channel 排列为:
    [sync_channel, 0, 1, 2, ..., sync_channel-1, sync_channel+1, ..., freq_count-1]
  → 同步信道始终在每块的第一个位置

Step 2: 遍历所有非 Sync Channel 的位置
  → 在每个区块内,i % freq_count != 0 的位置:
    → 与同区块内的随机位置交换(PRNG 使用绑定 UID 作为种子)

Step 3: 序列长度 = (256 / freq_count) * freq_count
  → 取 256 内 freq_count 的最大整倍数
  → 保证每个 channel 在序列中出现次数相同

关键参数:

  • FHSS_SEQUENCE_LEN = 256 --- 序列总长
  • sync_channel = freq_count / 2 + 1 --- 同步信道在中心位置
  • FHSShopInterval --- 每次跳频前的包数 (2 或 4)
  • seed --- 绑定 UID 的 macSeed,确保收发双方序列一致
  • primaryBandCount --- 实际使用的序列长度 (= 最大整倍数)

3.3 频率计算公式

复制代码
// 频道间隔计算
freq_spread = (freq_stop - freq_start) * FREQ_SPREAD_SCALE / (freq_count - 1)

// 具体频道频率
freq = freq_start + (freq_spread * FHSSsequence[idx] / FREQ_SPREAD_SCALE)

// 实际使用时还要减去 FreqCorrection (晶振误差补偿)
freq = freq_start + (freq_spread * FHSSsequence[idx] / FREQ_SPREAD_SCALE) - FreqCorrection

3.4 跳频时序

复制代码
TX 侧时序(以 200Hz/FCC915 为例):
┌──────────────────────────────────────────────────────┐
│  每 5ms 发一包,每 4 包换一次频率                      │
│  4 × 5ms = 20ms 驻留时间                              │
│  20ms × 40 频道 = 800ms 遍历全部频道                   │
│                                                       │
│  Block: [SyncCH, CHx, CHy, CHz][SyncCH, CHa, CHb, CHc]│
│  每 freq_count 个 block 发一次 Sync Packet             │
│  Sync Packet 携带: fhssIndex, nonce, rateIndex        │
└──────────────────────────────────────────────────────┘

RX 侧:
  - 断开时: 在每个频率上停留 ~11% 额外时间扫描 Sync Packet
  - 收到 Sync 后: FHSSsetCurrIndex(sync->fhssIndex),锁定跳频指针
  - 连续模式: 与 TX 同步跳频

3.5 Sync 协议与重同步机制

在 OTA.h 中定义的 OTA_Sync_s 结构:

复制代码
typedef struct {
    uint8_t fhssIndex;    // 当前 FHSS 序列指针位置
    uint8_t nonce;        // 当前包计数
    uint8_t switchEncMode:1,
            newTlmRatio:3,
            rateIndex:4;  // 当前速率索引
    uint8_t UID3, UID4, UID5;  // 绑定 UID 的末3字节
} OTA_Sync_s;

RX 收到 Sync 后:

  1. 检查 UID 匹配
  2. 检查 ModelMatch
  3. 设置 FHSSsetCurrIndex(otaSync->fhssIndex) 锁定跳频位置
  4. 设置 OtaNonce = otaSync->nonce 同步包计数

3.6 非 Sync 包的频点恢复机制

即使没收到 Sync,RC Data 包的 CRC 高位 4 bits 也嵌入了 (OtaNonce % FHSShopInterval) + 1,RX 可以据此推断应该在第几次包时跳频。

3.7 Gemini 双天线模式

同一频段下两个射频芯片同时在不同频率上工作:

复制代码
// 偏移频率 = 当前频点 + freq_count/2 个频道的偏移
offsSetIdx = (FHSSsequenceIdx + (numfhss / 2)) % numfhss;
freq = freq_start + (freq_spread * offSetIdx / FREQ_SPREAD_SCALE);

3.8 Dual Band 双频段模式

LR1121 支持 900MHz + 2.4GHz 同时工作:

  • 维护两套独立的 FHSS 序列 (FHSSsequence / FHSSsequence_DualBand)
  • 序列指针共用,取两个 band 序列长度的较小值作为上限
  • 根据当前选择的 radio_type 决定使用哪个频段

四、如何实现相同的跳频策略

步骤 1: 定义频域配置表

复制代码
typedef struct {
    const char *domain;
    uint32_t    freq_start;    // 起始频率 (Hz)
    uint32_t    freq_stop;     // 终止频率 (Hz)
    uint32_t    freq_count;    // 频道数量
} fhss_config_t;

// 例如 FCC915:
const fhss_config_t fcc915 = {
    .domain = "FCC915",
    .freq_start = 903500000,
    .freq_stop  = 926900000,
    .freq_count = 40
};

步骤 2: 生成跳频序列

复制代码
import random

FHSS_SEQUENCE_LEN = 256

def generate_fhss_sequence(seed, freq_count):
    """
    freq_count: 频道数量
    seed: 绑定 UID 衍生的种子
    """
    sync_channel = freq_count // 2 + 1  # ELRS 的 sync 信道在中心
    
    # 实际序列长度 = freq_count 在 256 内的最大整倍数
    seq_len = (FHSS_SEQUENCE_LEN // freq_count) * freq_count
    num_blocks = seq_len // freq_count
    
    # Step 1: 初始化
    sequence = [0] * seq_len
    for i in range(seq_len):
        block_offset = (i // freq_count) * freq_count
        idx_in_block = i % freq_count
        if idx_in_block == 0:
            sequence[i] = sync_channel
        elif idx_in_block == sync_channel:
            sequence[i] = 0
        else:
            sequence[i] = idx_in_block
    
    # Step 2: 随机化 (保持 sync_channel 在位置0不变)
    random.seed(seed)
    for i in range(seq_len):
        idx_in_block = i % freq_count
        if idx_in_block != 0:  # 不是 sync channel 位置
            block_offset = (i // freq_count) * freq_count
            rand_pos = random.randint(1, freq_count - 1)
            sequence[i], sequence[block_offset + rand_pos] = \
                sequence[block_offset + rand_pos], sequence[i]
    
    return sequence, sync_channel

步骤 3: 频率计算

复制代码
def compute_frequencies(config, sequence):
    """计算序列中每个位置的绝对频率"""
    freq_spread = (config.freq_stop - config.freq_start) / (config.freq_count - 1)
    
    frequencies = []
    for idx in sequence:
        freq = config.freq_start + (freq_spread * idx)
        frequencies.append(freq)
    
    return frequencies

步骤 4: TX 侧伪代码

复制代码
class ELRS_TX:
    def __init__(self, bind_uid, fhss_config):
        self.seed = crc32(bind_uid)  # 从 UID 派生种子
        self.config = fhss_config
        self.sequence, self.sync_channel = generate_fhss_sequence(
            self.seed, fhss_config.freq_count
        )
        self.fhss_ptr = 0
        self.nonce = 0
        self.fhss_hop_interval = 4  # 每4包换频
        self.packet_interval_us = 5000  # 200Hz 模式

    def on_timer_tick(self):
        """每次包间隔定时器触发"""
        # 检查是否需要跳频
        if (self.nonce + 1) % self.fhss_hop_interval == 0:
            self.fhss_ptr = (self.fhss_ptr + 1) % len(self.sequence)
        
        # 获取当前频率
        freq = self.config.freq_start + \
               self.config.freq_spread * self.sequence[self.fhss_ptr] / \
               self.config.freq_count
        
        # 设置射频频率
        radio.set_frequency(freq)
        
        # 决定是否发 Sync 包
        if self.sequence[self.fhss_ptr] == self.sync_channel:
            packet = build_sync_packet(self.fhss_ptr, self.nonce)
        else:
            packet = build_rc_data_packet(...)
        
        radio.transmit(packet)
        self.nonce += 1

步骤 5: RX 侧伪代码

复制代码
class ELRS_RX:
    def __init__(self, bind_uid, fhss_config):
        seed = crc32(bind_uid)
        self.config = fhss_config
        self.sequence, self.sync_channel = generate_fhss_sequence(
            seed, fhss_config.freq_count
        )
        self.fhss_ptr = 0
        self.nonce = 0
        self.locked = False  # 是否锁定到 TX
    
    def scanning(self):
        """未锁定时的扫频模式"""
        for freq in self.all_frequencies:
            radio.set_frequency(freq)
            radio.start_rx()
            # 在每个频率上停留足够久 (sync 间隔 + 余量)
            wait(self.scan_dwell_time_ms)
            if radio.packet_received():
                if self.handle_packet(radio.read_packet()):
                    return
    
    def handle_packet(self, packet):
        if packet.type == PACKET_SYNC:
            if packet.uid_matches():
                self.fhss_ptr = packet.fhss_index
                self.nonce = packet.nonce
                self.locked = True
                return True
        return False
    
    def connected_mode(self):
        """锁定后跟随 TX 跳频"""
        if (self.nonce + 1) % self.fhss_hop_interval == 0:
            self.fhss_ptr = (self.fhss_ptr + 1) % len(self.sequence)
        freq = self.config.freq_start + \
               self.config.freq_spread * self.sequence[self.fhss_ptr] / \
               self.config.freq_count
        radio.set_frequency(freq)
        radio.start_rx()
        self.nonce += 1

步骤 6: Sync Packet 结构

复制代码
Byte 0:    [type:2][CRC_high:6]    // type = 0b10 for SYNC
Byte 1:    fhssIndex               // 在 256-entry 序列中的位置
Byte 2:    nonce                   // 当前包计数
Byte 3:    [switchEnc:1][tlmRatio:3][rateIndex:4]
Byte 4-6:  UID[3:5]               // 绑定 UID 的末3字节
Byte 7:    CRC_low

关键设计决策

决策点 ELRS 的做法 理由
Sync Channel 位置 freq_count/2 + 1 (频段中心) 中心频率干扰最小
Sync 包频率 每个 block 一次 (每 freq_count 跳) 覆盖整个频段后才发 sync
跳频粒度 2 或 4 包一跳 平衡驻留时间和快速遍历
序列长度 256 刚好用 uint8_t 索引
序列种子 绑定 UID 的 CRC 相同 bind phrase 产生相同序列
隐式包头 (LoRa) 固定 PayloadLength 不使用显式头 节省2字节空中时间
自定义 CRC OTA4=8位, OTA8=16位 比 LoRa 硬件 CRC 更高效

完整周期时间估算

FCC915, 200Hz + 4包一跳 为例:

复制代码
每包间隔:      5 ms
每个频率驻留:   5ms × 4 = 20 ms
完整遍历:      20ms × 40 ch = 800 ms
Sync 包间隔:   800 ms (正好一个完整遍历一次)
Sync 包占比:   1/40 = 2.5%

ISM2G4, 500Hz + 4包一跳 为例:

复制代码
每包间隔:      2 ms
每个频率驻留:   2ms × 4 = 8 ms
完整遍历:      8ms × 80 ch = 640 ms
Sync 包间隔:   640 ms

这套跳频策略的核心优势是:序列确定性(同 UID 产生相同序列)、Sync Channel 作为连接锚点、快的完整遍历周期(<1秒)、以及隐含在数据包中的跳频信息用于快速恢复同步