前言
在无人机FPV(第一人称视角)飞行中,图传系统的延迟和距离直接决定了飞行体验。传统的WiFi UDP图传虽然简单,但存在协议栈开销大、重传机制导致延迟抖动等问题。本文将带你从零实现一个基于802.11 RAW帧的FPV图传系统,通过绕过TCP/IP协议栈,结合FEC前向纠错编码,实现低延迟、高可靠性的视频传输。
为什么选择802.11 RAW帧?
标准WiFi传输使用TCP/IP协议栈,数据需要经过多层封装,带来以下问题:
-
协议开销大:UDP/IP头就占42字节
-
重传机制:丢包后重传会引入不可预测的延迟
-
竞争接入:与普通网络流量争抢信道
而802.11 RAW帧直接操作MAC层,我们可以:
-
自定义数据格式,去除无用头部
-
主动丢包,不等待重传,用FEC修复丢失数据
-
独占信道,获得稳定延迟
系统整体架构
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ OV2640 │───▶│ JPEG │───▶│ FEC │───▶│ 802.11 │
│ 摄像头 │ │ 编码 │ │ 编码器 │ │ RAW帧 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 显示 │◀───│ JPEG │◀───│ FEC │◀───│ 802.11 │
│ │ │ 解码 │ │ 解码器 │ │ RAW帧 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
核心代码实现
1. 摄像头初始化
首先配置OV2640摄像头,使用JPEG格式输出以节省带宽:
camera_config_t esp32cam_config = {
.pin_pwdn = CAMERA_PIN_PWDN,
.pin_reset = CAMERA_PIN_RESET,
.pin_xclk = CAMERA_PIN_XCLK,
.pin_sscb_sda = CAMERA_PIN_SIOD,
.pin_sscb_scl = CAMERA_PIN_SIOC,
// ... 省略引脚配置
.pixel_format = PIXFORMAT_JPEG, // JPEG硬件编码
.frame_size = FRAMESIZE_XGA, // 1024x768
.jpeg_quality = 30, // 画质与压缩比平衡
.fb_count = 2, // 双缓冲
.fb_location = CAMERA_FB_IN_PSRAM, // 使用PSRAM
};
esp_err_t err = esp_camera_init(&config);
2. 自定义数据包格式
为了高效传输,我们设计紧凑的数据包结构:
#pragma pack(push, 1)
struct Air2Ground_Video_Packet {
enum class Type : uint8_t {
Video, // 视频数据
Telemetry // 遥测数据
};
Type type; // 包类型 (1字节)
uint32_t size; // 包大小 (4字节)
uint8_t pong; // 延迟测量 (1字节)
uint8_t crc; // 校验 (1字节)
Resolution resolution; // 分辨率 (1字节)
uint8_t part_index : 7; // 分片索引 (7位)
uint8_t last_part : 1; // 最后一包标识 (1位)
uint32_t frame_index; // 帧序号 (4字节)
// 后面跟实际视频数据
};
#pragma pack(pop)
static_assert(sizeof(Air2Ground_Video_Packet) == 13, "精确控制包大小");
3. 802.11 RAW帧发送
这是整个系统最核心的部分------构造并发送自定义WiFi帧:
void send_raw_data(const uint8_t *target_mac, const uint8_t *data, uint16_t len) {
static uint8_t packet[1500];
uint8_t my_mac[6];
esp_read_mac(my_mac, ESP_MAC_WIFI_STA);
// 构造802.11数据帧头
packet[0] = 0x40; // 帧控制:Version=0, Type=Data, Subtype=Data
packet[1] = 0x00; // 标志位
packet[2] = 0x00; // Duration
packet[3] = 0x00;
memcpy(&packet[4], target_mac, 6); // 目标MAC地址 (Receiver Address)
memcpy(&packet[10], my_mac, 6); // 源MAC地址 (Transmitter Address)
memcpy(&packet[16], CUSTOM_BSSID, 6);// BSSID,自定义标识
// 序列号,用于区分帧
static uint16_t seq = 0;
packet[22] = (seq & 0x0F) << 4;
packet[23] = (seq >> 4) & 0xFF;
seq++;
// 复制数据负载
memcpy(&packet[24], data, len);
// 发送RAW帧
esp_wifi_80211_tx(WIFI_IF_STA, packet, 24 + len, true);
}
关键技术点:
-
esp_wifi_80211_tx是ESP-IDF提供的底层发送接口 -
帧头只占24字节,比UDP/IP的42字节节省近一半
-
不需要ARP、IP、UDP层层封装
4. FEC前向纠错编码
为解决无线丢包问题,我们引入Reed-Solomon风格的FEC编码:
class Fec_Codec {
public:
static const uint8_t MAX_CODING_K = 16;
static const uint8_t MAX_CODING_N = 32;
struct Descriptor {
uint8_t coding_k = 8; // 原始数据包数量
uint8_t coding_n = 12; // 总包数(原始+冗余)
size_t mtu = 1330; // 单包大小
uint8_t priority = configMAX_PRIORITIES - 1;
};
bool init_encoder(const Descriptor& descriptor);
bool encode_data(const void* data, size_t size, bool block);
bool flush_encode_packet(bool block);
void set_data_encoded_cb(void (*cb)(void* data, size_t size));
};
FEC工作流程:
-
收集8个原始视频包(coding_k=8)
-
生成4个冗余包(coding_n - coding_k = 4)
-
共12个包发送出去
-
接收端只需收到任意8个包即可恢复全部数据
这样即使丢失33%的包,依然可以完整恢复视频帧!
5. 视频帧分片与发送
将摄像头采集的JPEG帧分片后送入FEC编码器:
void send_video_frame(uint8_t *fb_buf, size_t fb_len) {
static uint32_t current_frame_id = 0;
size_t offset = 0;
int packets_sent_this_frame = 0;
// 单包有效载荷 = MTU - 包头
size_t max_video_payload = 1330 - sizeof(Air2Ground_Video_Packet);
while (offset < fb_len) {
size_t chunk_size = min(max_video_payload, fb_len - offset);
// 从FEC编码器获取数据缓冲区
uint8_t *packet_buffer = my_fec_encoder.get_encode_packet_data(true);
// 填充视频包头
Air2Ground_Video_Packet *header = (Air2Ground_Video_Packet*)packet_buffer;
header->type = Air2Ground_Header::Type::Video;
header->size = chunk_size + sizeof(Air2Ground_Video_Packet);
header->frame_index = current_frame_id;
header->last_part = (offset + chunk_size >= fb_len) ? 1 : 0;
// 复制视频数据
memcpy(packet_buffer + sizeof(Air2Ground_Video_Packet),
fb_buf + offset, chunk_size);
// 提交到FEC编码器
my_fec_encoder.flush_encode_packet(true);
offset += chunk_size;
packets_sent_this_frame++;
}
current_frame_id++;
}
6. 完整主程序
extern "C" void app_main(void) {
// 提高任务优先级
vTaskPrioritySet(NULL, 10);
// 初始化WiFi
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
// 关键:锁定WiFi信道并开启混杂模式
esp_wifi_set_promiscuous(true);
esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_11M_S);
esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
// 初始化FEC编码器
Fec_Codec::Descriptor fec_desc;
fec_desc.coding_k = 8;
fec_desc.coding_n = 12;
fec_desc.mtu = 1330;
my_fec_encoder.init_encoder(fec_desc);
my_fec_encoder.set_data_encoded_cb(on_fec_encoded);
// 初始化摄像头
OVCamera cam;
cam.init(esp32cam_config);
// 主循环
while (1) {
cam.run();
if (cam.fb != NULL) {
send_video_frame(cam.fb->buf, cam.fb->len);
cam.done();
vTaskDelay(pdMS_TO_TICKS(5)); // 喂狗
}
}
}
项目文件结构
fpv_sender/
├── CMakeLists.txt # 编译配置
├── idf_component.yml # 依赖管理
├── main/
│ ├── fpv_sender.cpp # 主程序
│ ├── fec_codec.cpp # FEC编解码
│ ├── fec.cpp # FEC数学实现
│ ├── crc.cpp # CRC校验
│ ├── OVCamera.cpp # 摄像头驱动
│ ├── packets.h # 数据包定义
│ └── structures.h # 通用结构