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_SX127X、RADIO_SX128X、RADIO_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 参数设计遵循以下原则:
- 速度优先 → 先用最大 BW (500kHz/800kHz) 和最小 SF,随距离增加逐级降速
- 隐式包头 → 因为双方预先知道 Payload 长度,不发送显式包头,节省2字节
- 无 LoRa CRC → 自己实现了更轻量级的 CRC(OTA4 用8位CRC,OTA8 用16位CRC),因为 LoRa 硬件 CRC 不是标准 CRC 且占用芯片寄存器
- 无 Sync Word → LoRa 模式不使用 sync word 寄存器(FLRC 才有硬件 sync word)
- LI (Long Interleaver) → SX1280 使用 LI 版本的 CR,在相同码率下提供更好的抗突发干扰能力
- 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()。
设计约束:
- 每
freq_count个跳频位置,Sync Channel 出现一次(在区块首位) - 同一区块内无重复频道
- 每个频道出现频率尽量均等
- 伪随机
算法步骤:
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 后:
- 检查 UID 匹配
- 检查 ModelMatch
- 设置
FHSSsetCurrIndex(otaSync->fhssIndex)锁定跳频位置 - 设置
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秒)、以及隐含在数据包中的跳频信息用于快速恢复同步。