本文以 Q&A 形式整理了关于 ExpressLRS 项目中 LR1121 射频芯片通信机制的技术讨论,涵盖半双工通信、时隙同步、速率与距离权衡等核心话题。
1. LR1121 半双工通信的底层机制
Q: LR1121 是半双工的,底层双向通信的技术细节是怎样的?
LR1121 作为半双工收发器,在同一时刻只能处于发送或接收状态之一。ExpressLRS 通过中断驱动的状态机实现双向通信。
工作模式
LR1121 支持以下关键模式:
| 模式 | 值 | 说明 |
|---|---|---|
LR1121_MODE_SLEEP |
0x00 | 睡眠模式 |
LR1121_MODE_STDBY_RC |
0x01 | RC 振荡器待机 |
LR1121_MODE_STDBY_XOSC |
0x02 | 晶振待机 |
LR1121_MODE_FS |
0x03 | 频率合成模式(空闲态) |
LR1121_MODE_RX_CONT |
0x04 | 连续接收模式 |
LR1121_MODE_TX |
0x05 | 发送模式 |
模式切换实现
// src/lib/LR1121Driver/LR1121.cpp:451-498
void LR1121Driver::SetMode(lr11xx_RadioOperatingModes_t OPmode, SX12XX_Radio_Number_t radioNumber)
{
switch (OPmode)
{
case LR1121_MODE_RX_CONT:
// 进入连续接收模式,timeout = 0xFFFFFF
hal.WriteCommand(LR11XX_RADIO_SET_RX_OC, buf, 3, radioNumber);
break;
case LR1121_MODE_TX:
// 进入发送模式
hal.WriteCommand(LR11XX_RADIO_SET_TX_OC, buf, 3, radioNumber);
break;
case LR1121_MODE_FS:
// 进入频率合成模式(快速切换的中间态)
hal.WriteCommand(LR11XX_SYSTEM_SET_FS_OC, radioNumber);
break;
}
}
中断驱动的收发流程
┌─────────────────────────────────────────────────┐
│ CONTINUOUS RX STATE │
│ SetMode(LR1121_MODE_RX_CONT, SX12XX_Radio_All)│
└────────────┬────────────────────────────────────┘
│ RX_DONE IRQ (DIO1 上升沿)
▼
┌─────────────────────────────────────────────────┐
│ RX ISR HANDLER │
│ - 从缓冲区读取数据包 │
│ - 解码 RSSI/SNR │
│ - 调用 RXdoneCallback() │
└────────────┬────────────────────────────────────┘
│ 应用层决定是否发送遥测
▼
┌─────────────────────────────────────────────────┐
│ TRANSMIT (TXnb) │
│ 1. 编码 payload │
│ 2. 写入 TX 缓冲区 │
│ 3. 命令: WRITE_BUFFER8_SET_TX (0x0704) │
└────────────┬────────────────────────────────────┘
│ TX_DONE IRQ
▼
┌─────────────────────────────────────────────────┐
│ TX ISR → TXdoneCallback() │
│ 应用层调用 RXnb() 返回接收状态 │
└─────────────────────────────────────────────────┘
关键 SPI 命令
| 命令 | Opcode | 说明 |
|---|---|---|
| SetRx | 0x0209 | 进入接收模式 |
| SetTx | 0x020A | 进入发送模式 |
| SetFS | 0x011D | 进入频率合成模式 |
| WriteBuffer+SetTx | 0x0704 | 写缓冲区并发送 |
| SetRxTxFallbackMode | 0x0213 | 设置自动回落模式 |
2. FS 模式是什么?
Q: FS 是一种可以随时进入 TX 的 RX 模式吗?
不是的。FS (Frequency Synthesis) 模式既不收也不发,它是一种"预热空闲"状态。
FS 模式的实际含义
| 组件 | 状态 | 说明 |
|---|---|---|
| PLL(锁相环) | ✅ 已锁定 | 频率已稳定 |
| 晶振 | ✅ 运行中 | 时钟正常 |
| LNA(低噪声放大器) | ❌ 未激活 | 不能接收 |
| PA(功率放大器) | ❌ 未激活 | 不能发送 |
简单理解:引擎已发动,但车还在空挡。
为什么用 FS 而不是其他模式?
| 模式 | PLL 状态 | 切换到 TX/RX 速度 | 功耗 |
|---|---|---|---|
| SLEEP | 关闭 | 最慢(需重启 PLL) | 最低 |
| STDBY_RC | 关闭 | 较慢 | 低 |
| FS | 已锁定 | 最快 | 中等 |
| RX_CONT | 锁定+LNA开 | 快 | 较高 |
FS 的优势:从 FS 切换到 TX/RX 只需激活 PA/LNA,省去 PLL 锁定时间(通常几十到几百微秒)。
在 ExpressLRS 中的实际用途
主要用于双射频分集时隔离另一个射频:
// src/lib/LR1121Driver/LR1121.cpp:633-644
// 双射频模式下,如果射频1要发送
if (radioNumber == SX12XX_Radio_1)
{
SetMode(LR1121_MODE_FS, SX12XX_Radio_2); // 射频2进入FS,不接收自己的TX信号
}
状态切换示意
┌─────────┐
│ SLEEP │ ← 最省电,但唤醒慢
└────┬────┘
│ 唤醒
▼
┌─────────┐
│ STDBY │ ← 待机,PLL 未锁定
└────┬────┘
│ 启动 PLL
▼
┌─────────┐
│ FS │ ← PLL 已锁定,空闲态(不收不发)
└────┬────┘
╱ ╲
激活LNA 激活PA
╱ ╲
▼ ▼
┌────────┐ ┌────────┐
│ RX │ │ TX │
└────────┘ └────────┘
3. TX/RX 切换是手动还是自动的?
Q: 在单个 LR1121 模块下,发射和接收都是手动切换的吗?还是模块提供了特殊功能?
答案是:半自动。
芯片提供的硬件自动功能
RxTxFallbackMode(已使用)
在初始化时配置:
// src/lib/LR1121Driver/LR1121.cpp:132-135
// 7.2.5 SetRxTxFallbackMode
uint8_t FBbuf[1] = {LR11XX_RADIO_FALLBACK_FS};
hal.WriteCommand(LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_OC, FBbuf, ...);
效果:TX 完成或 RX 超时后,芯片硬件自动回落到 FS 模式。
实际的切换流程
┌────────────────────────────────────────────────────────────┐
│ 软件控制 │
│ RXnb() ──────────────────────────► 进入 RX_CONT 模式 │
│ TXnb() ──────────────────────────► 进入 TX 模式 │
└────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ 硬件自动 (Fallback) │
│ TX 完成 ─────────────────────────► 自动进入 FS 模式 │
│ RX 超时/完成 ────────────────────► 自动进入 FS 模式 │
└────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ 软件控制 │
│ ISR 回调通知应用层 ──────────────► 应用决定下一步操作 │
│ 应用调用 RXnb()/TXnb() ──────────► 进入下一个模式 │
└────────────────────────────────────────────────────────────┘
总结
| 操作 | 由谁控制 |
|---|---|
| 进入 RX 模式 | 软件手动 调用 RXnb() |
| 进入 TX 模式 | 软件手动 调用 TXnb() |
| TX/RX 完成后回到 FS | 硬件自动 (Fallback 机制) |
| 从 FS 进入下一个状态 | 软件手动 根据协议逻辑决定 |
4. K1000 为什么 MAVLink 参数下载这么快?
Q: K1000 为什么可以这么快的 MAVLink 参数下载速率?单纯因为给的 RX 时间大吗?
不仅仅是 RX 时间大,而是遥测总吞吐量高。
绝对遥测包频率
| 模式 | 包速率 | TLM Ratio | 遥测频率 |
|---|---|---|---|
| F1000 | 1000 Hz | 1:128 | 7.8 包/秒 |
| 500Hz | 500 Hz | 1:128 | 3.9 包/秒 |
| 100Hz | 100 Hz | 1:32 | 3.1 包/秒 |
| 50Hz | 50 Hz | 1:16 | 3.1 包/秒 |
虽然 F1000 的 TLM ratio 看起来很稀疏 (1:128),但因为基础包速率是 1000Hz,绝对遥测频率反而更高。
遥测突发 (Telemetry Burst) 机制
// src/src/common.cpp:196-214
// 每 512ms 内可以连续发送多少个数据包
uint8_t TLMBurstMaxForRateRatio(uint16_t rateHz, uint8_t ratioDiv)
{
constexpr uint32_t TELEM_MIN_LINK_INTERVAL_MS = 512U;
unsigned retVal = TELEM_MIN_LINK_INTERVAL_MS * rateHz / ratioDiv / 1000U;
if (retVal > 1) --retVal;
return retVal;
}
F1000 计算:
burst = 512 * 1000 / 128 / 1000 = 4 - 1 = 3
100Hz 1:32 计算:
burst = 512 * 100 / 32 / 1000 = 1.6 → 1
实际下行带宽
// src/lib/tx-crsf/TXModuleParameters.cpp:434-436
uint32_t bandwidthValue = bytesPerCall * 8 * burst * hz / ratiodiv / (burst + 1);
F1000 带宽(假设 OTA4 每包 5 字节):
= 5 * 8 * 3 * 1000 / 128 / 4 = 234 bps ≈ 29 字节/秒
100Hz 带宽:
= 5 * 8 * 1 * 100 / 32 / 2 = 62 bps ≈ 7.8 字节/秒
总结
| 因素 | 说明 |
|---|---|
| 高包速率 | 1000Hz 基础速率,即使 TLM ratio 稀疏,绝对遥测频率也高 |
| 更大的 Burst | 每 512ms 周期内可连续发 3 个数据包 |
| FSK/FLRC 调制 | 空中时间短,每包间隔只需 1ms |
5. TX 和 RX 如何实现时隙对齐?
Q: 发射机和接收机是如何协调发送和接收时隙对齐的?
这是 ExpressLRS 最核心的时序同步机制,采用 PFD (Phase Frequency Detector) 实现相位锁定。
整体架构:Tick-Tock 定时器
TX 和 RX 都有一个硬件定时器,产生交替的 Tick 和 Tock 事件:
TX 定时器: ──┬──Tock──┬──Tick──┬──Tock──┬──Tick──┬──
│ TX包 │ │ TX包 │ │
▼ │ ▼ │ │
发送数据 │ 发送数据 │ │
RX 定时器: ──┬──Tock──┬──Tick──┬──Tock──┬──Tick──┬──
│ │ RX │ │ RX │
│ │ 处理 │ │ 处理 │
关键:RX 需要将自己的 Tock 与 TX 数据包的到达时间对齐。
PFD 相位检测器
// src/lib/PFD/PFD.h
class PFD {
uint32_t intEventTime; // RX 本地 Tock 触发时间
uint32_t extEventTime; // 收到 TX 数据包的时间
int32_t calcResult() {
return extEventTime - intEventTime; // 相位差
}
};
相位锁定流程
1. RX 收到数据包 → PFDloop.extEvent(收包时间 + slack)
2. RX Tock 中断 → PFDloop.intEvent(当前时间)
3. 计算相位差 → RawOffset = extEvent - intEvent
4. 低通滤波 → Offset = LPF_Offset.update(RawOffset)
5. 调整定时器 → hwTimer::phaseShift() 或 FreqOffset++/--
频率和相位校正
// src/src/rx_main.cpp:607-645
void updatePhaseLock() {
int32_t RawOffset = PFDloop.calcResult();
int32_t Offset = LPF_Offset.update(RawOffset); // 相位偏移
int32_t OffsetDx = LPF_OffsetDx.update(变化率); // 频率漂移
if (RXtimerState == tim_locked) {
// 已锁定:微调频率
if (Offset > 0) hwTimer::incFreqOffset(); // 本地时钟太快
if (Offset < 0) hwTimer::decFreqOffset(); // 本地时钟太慢
} else {
// 未锁定:大幅相位调整
hwTimer::phaseShift(Offset >> 1);
}
}
两种校正方式
| 状态 | 校正方式 | 说明 |
|---|---|---|
| 未锁定 | phaseShift() |
一次性大幅调整相位(最大 1/4 周期) |
| 已锁定 | FreqOffset++/-- |
每周期微调 1μs 补偿晶振偏差 |
完整时序图
时间 →
┌────────┐ ┌────────┐ ┌────────┐
TX: │ TX 包1 │ │ TX 包2 │ │ TX TLM │
└────┬───┘ └────┬───┘ └────┬───┘
│ 空中传播 │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
RX: │ RX收包 │ │ RX收包 │ │ TX遥测 │◄── RX发送
└────┬───┘ └────┬───┘ └────────┘
▼ ▼
PFD.extEvent PFD.extEvent
│ │
┌────┴────┐ ┌────┴────┐
RX │ Tock │ │ Tock │
Timer: │ intEvent│ │ intEvent│
└─────────┘ └─────────┘
│ │
▼ ▼
updatePhaseLock() - 调整定时器使 Tock 对齐包到达
遥测时隙协调
TX 和 RX 共享相同的 OtaNonce 计数器(通过 SYNC 包同步):
// TX 端 - src/src/tx_main.cpp:857
const bool nextIsTLM = (OtaNonce + 1) % ExpressLRS_currTlmDenom == 0;
if (nextIsTLM) {
TelemetryRcvPhase = ttrpPreReceiveGap; // 准备接收遥测
Radio.RXnb(); // 切换到 RX 模式
}
// RX 端 - src/src/rx_main.cpp:454
uint8_t modresult = OtaNonce % ExpressLRS_currTlmDenom;
if (modresult == 0) {
// 发送遥测数据包
}
总结
| 机制 | 作用 |
|---|---|
| PFD | 测量 RX 本地时钟与 TX 数据包到达的相位差 |
| LPF 滤波 | 平滑相位测量,减少抖动 |
| phaseShift | 初始连接时的粗调(一次性相位跳变) |
| FreqOffset | 锁定后的细调(补偿晶振 ppm 偏差) |
| OtaNonce | TX/RX 共享的计数器,决定哪个时隙是遥测 |
| SYNC 包 | TX 周期性发送,携带 OtaNonce 确保同步 |
6. 为什么 ELRS 不使用 AutoTxRx?
Q: ELRS 没有使用 AutoTxRx 的原因是这个特殊的遥控场景需要的上下行带宽不对称吗?
是的,但还有其他原因。
1. 上下行带宽不对称
上行 (TX→RX): 每个时隙都发包 → 1000 包/秒
下行 (RX→TX): 1:128 才发遥测 → 7.8 包/秒
如果用 AutoTxRx(TX 完自动进 RX 等响应),那 127/128 的时隙都在无意义地等待。
2. 时隙类型多样
ELRS 的上行包有多种类型:
时隙0: RC 数据
时隙1: RC 数据
时隙2: SYNC 包(可能)
...
时隙127: RC 数据
时隙128: RC 数据 → 然后等遥测
AutoTxRx 是硬件自动的,无法区分"这个时隙要等响应"还是"这个时隙不用等"。
3. 双射频分集需要精确控制
// 发送时:一个射频 TX,另一个进入 FS 防止自干扰
if (radioNumber == SX12XX_Radio_1) {
SetMode(LR1121_MODE_FS, SX12XX_Radio_2);
}
AutoTxRx 会让两个射频同时进入 RX,造成混乱。
4. FHSS 跳频时机
TX包 → 跳频 → TX包 → 跳频 → ... → TX包 → 等遥测(不跳频)
跳频发生在 TX 完成后、下一包开始前。AutoTxRx 自动进入 RX 就没有合适的时机切换频率。
5. LBT(部分法规区域)
在 EU CE 等法规区域,需要先"听"再"发":
听(RX) → 信道空闲? → 发(TX) → 等遥测(RX)
6. RxTxFallbackMode 已经够用
hal.WriteCommand(LR11XX_RADIO_SET_RX_TX_FALLBACK_MODE_OC,
LR11XX_RADIO_FALLBACK_FS); // TX/RX 完成后自动回到 FS
这比 AutoTxRx 更灵活:软件决定下一步是进 RX 还是直接发下一包。
总结
| 因素 | AutoTxRx 的问题 |
|---|---|
| 带宽不对称 | 大量时隙无意义等待 |
| 包类型多样 | 无法区分是否需要等响应 |
| 双射频分集 | 无法单独控制每个射频 |
| FHSS 跳频 | 没有时机执行跳频 |
| LBT 法规 | 无法实现"先听后发" |
| PFD 同步 | 需要精确控制定时器触发时机 |
7. SYNC 包会占用正常的数据时隙吗?
Q: SYNC 包会占用包频率吗?占用情况如何?
是的,SYNC 包会占用正常的 RC 数据时隙,但影响很小。
发送条件(三重限制)
// src/src/tx_main.cpp:547-558
// 条件1: 必须在"同步频率"上
FHSSonSyncChannel()
// 条件2: 距离上次 SYNC 要满足时间间隔
(now - SyncPacketLastSent > SyncInterval)
// 条件3: syncSlot 轮转机制
(syncSlot / 2) <= NonceFHSSresult
具体占用率
| 模式 | 连接后 SyncInterval | 未连接 SyncInterval |
|---|---|---|
| F1000 | 5000ms | 3ms |
| 100Hz LoRa | 5000ms | 600ms |
连接后:每 5 秒才发一个 SYNC 包,对 RC 数据几乎无影响。
未连接时:频繁发送 SYNC 以快速建立连接。
Sync Spam 机制(特殊情况)
在配置变更或连接建立时,会强制密集发送 SYNC:
#define syncSpamAmount 3 // 普通 spam
#define syncSpamCounterAfterRateChange 10 // 速率变更后 spam
总结
| 状态 | SYNC 对 RC 数据的影响 |
|---|---|
| 已连接稳定 | 极小 (~1包/5秒) |
| 刚建立连接 | 短暂增加 (spam 3-10包) |
| 未连接扫描 | 很大 (密集发送找 RX) |
| 配置变更时 | 短暂增加 (spam 10包) |
8. AutoTxRx 的工作原理是什么?
Q: AutoTxRx 是什么策略?如果两边都使用 AutoTxRx,会有自动避让吗?
AutoTxRx 的两种模式
不是"不发送就接收",而是两种有限状态机模式:
AutoRX 模式(TX 后自动 RX)
TX 发送 → 可配置延迟 → 进入 RX → 收到1个包或超时 → 回到 Standby
AutoTx 模式(RX 后自动 TX)
RX 接收 → 收到包 → 可配置延迟 → 进入 TX 发送预装数据 → 回到 Standby
两边都使用 AutoTxRx 会怎样?
没有自动避让机制,会靠"碰撞+重试"
场景1: A 和 B 都配置为 AutoRX(TX 后等响应)
时间 →
A: [TX]──delay──[RX等待]──────────[超时]
B: [TX]──delay──[RX等待]──────────[超时]
↑
两边同时发,谁也收不到对方
场景2: A 配置 AutoRX,B 配置 AutoTx(主从模式)
时间 →
A: [TX]──delay──[RX等待]──────[收到B响应]
B: ────[RX]────[收到A]──delay──[TX响应]
这种模式可以工作
高频发送的碰撞问题
| 情况 | 结果 |
|---|---|
| 两边都 AutoRX | 死锁:都在发,没人听 |
| 两边都 AutoTx | 死锁:都在等,没人发 |
| A: AutoRX, B: AutoTx | 主从模式:可以工作 |
| 双方都无协调高频发送 | 碰撞率 ≈ 发送占空比 |
解决方案:需要上层协议
AutoTxRx 只是硬件便利功能,不提供冲突避免:
| 协议层 | 机制 |
|---|---|
| TDMA | 时分多址,分配固定时隙(ELRS 的做法) |
| LoRaWAN | RX1/RX2 窗口,网关调度 |
| CSMA/CA | 发送前监听,随机退避 |
| 主从轮询 | 主机发,从机只响应 |
9. 速率为什么会影响通信距离?
Q: 底层是配置了模块不同的调制方法还是校验方法?
核心是调制参数的综合变化,不仅仅是校验。
不同速率的底层配置对比
900MHz LoRa 模式参数变化
| 速率 | SF | BW | CR | 灵敏度 | TOA (μs) |
|---|---|---|---|---|---|
| 250Hz | SF5 | 500kHz | 4/8 | -111 dBm | 3216 |
| 100Hz | SF7 | 500kHz | 4/7 | -117 dBm | 8770 |
| 50Hz | SF8 | 500kHz | 4/7 | -120 dBm | 18560 |
| 25Hz | SF9 | 500kHz | 4/7 | -123 dBm | 29950 |
FSK 高速模式对比
| 速率 | 调制 | 比特率 | BW | 灵敏度 | TOA (μs) |
|---|---|---|---|---|---|
| F1000 | GFSK | 300 kbps | 467kHz | -101 dBm | 658 |
核心参数解析
SF (Spreading Factor) - 扩频因子
这是影响距离最关键的参数
SF = 每个符号携带的 chirp 数量 = 2^SF chips/symbol
SF5: 2^5 = 32 chips → 快速但需要强信号
SF9: 2^9 = 512 chips → 慢速但能解调极弱信号
原理:
- SF 越大,同样的信息被"拉伸"得越长
- 能量分散在更长的时间上
- 接收端有更多时间积分能量,从噪声中提取信号
CR (Coding Rate) - 编码率
CR 4/5: 每 4 位有效数据加 1 位校验 (20% 冗余)
CR 4/7: 每 4 位有效数据加 3 位校验 (75% 冗余)
CR 4/8: 每 4 位有效数据加 4 位校验 (100% 冗余)
信号处理增益
Processing Gain (dB) = 10 × log10(BW / BitRate)
SF5 (250Hz): PG ≈ 21 dB
SF9 (25Hz): PG ≈ 33 dB
增益差 = 12 dB ≈ 4 倍距离
灵敏度与距离的关系
Friis 传输公式:
链路预算 = 发射功率 - 接收灵敏度 - 衰落余量
25Hz (SF9): Tx 100mW (20dBm), Rx -123dBm
链路预算 = 20 - (-123) - 10 = 133 dB
对应距离 ≈ 20-30 km (空旷环境)
F1000 (FSK): Tx 100mW (20dBm), Rx -101dBm
链路预算 = 20 - (-101) - 10 = 111 dB
对应距离 ≈ 1-2 km (空旷环境)
差距:22 dB 的灵敏度差异 ≈ 10-15 倍距离差
总结:速率 vs 距离的权衡
| 参数 | 高速模式 (F1000) | 低速模式 (25Hz) |
|---|---|---|
| 调制方式 | FSK | LoRa CSS |
| SF | N/A (FSK) | SF9 |
| 符号时间 | 3.3 μs | 1024 μs |
| 灵敏度 | -101 dBm | -123 dBm |
| TOA | 658 μs | 29950 μs |
| 距离 | ~2 km | ~30 km |
| 延迟 | 1 ms | 40 ms |
核心机制:
- ✅ 调制方式改变:FSK vs LoRa
- ✅ 扩频因子增大:SF5→SF9 (LoRa 内部)
- ✅ 编码冗余增加:CR 4/8 vs 4/7
- ✅ 空中时间延长:658μs → 29950μs (45倍)
不是简单的校验增加,而是整个物理层调制方案的根本性改变。
参考资料
本文基于 ExpressLRS 源代码分析,代码版本基于 2024/2025 年的 master 分支。