【数字图传第三步】整合系统
前言在前两篇文章中我们分别实现了第一步USB UVC ACM 复合设备让接收端能被电脑识别为摄像头和串口第二步ESP32 802.11 原始帧发送实现底层WiFi数据注入本文将把这两部分整合起来搭建一个完整的发射端-接收端FPV系统。发射端负责采集摄像头数据并经过FEC编码后通过WiFi发送接收端负责接收WiFi帧、FEC解码后通过UVC推流到电脑。一、整体架构回顾发射端 (Air) 接收端 (Ground)-------- -------- -------- --------| OV2640 | -- | FEC | -- | 802.11 | -- | 802.11 || Camera | | Encoder| | Raw | | Sniffer |-------- -------- -------- --------|v-------- --------| FEC | -- | UVC | -- USB - 电脑| Decoder| | Push |-------- --------核心设计思想发射端FEC(8,12) 编码容忍最多4个包丢失接收端混杂模式监听指定WiFi信道提取自定义BSSID的帧传输协议自定义Air2Ground_Video_Packet头包含帧索引、分包标记等二、发射端完整代码fpv_sender.cpp发射端在主任务中采集摄像头图像分包后喂给FEC编码器编码器的回调函数直接调用send_raw_data()发送。2.1 头文件与全局定义#include OVCamera.h#include esp_log.h#include esp_mac.h#include esp_rom_sys.h#include esp_wifi.h#include freertos/FreeRTOS.h#include freertos/queue.h#include freertos/task.h#include nvs_flash.h#include stdio.h#include string.h#include fec_codec.h#include packets.hstatic const char *TAG SENDER;static const uint8_t TARGET_MAC[6] {0x14, 0xC1, 0x9F, 0x2E, 0x26, 0xC0};static const uint8_t CUSTOM_BSSID[6] {F, P, V, C, A, R};static const uint8_t WIFI_CHANNEL 6;Fec_Codec my_fec_encoder;2.2 WiFi原始帧发送函数这是整个系统的关键函数。ESP32的WiFi驱动支持发送自定义802.11数据帧我们构造一个Data帧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 Data Frame Header (24 bytes)packet[0] 0x40; // Frame Control: Version 0, Type Data, Subtype Datapacket[1] 0x00;packet[2] 0x00;packet[3] 0x00;// Address 1: Receiver (目标MAC)memcpy(packet[4], target_mac, 6);// Address 2: Transmitter (本机MAC)memcpy(packet[10], my_mac, 6);// Address 3: BSSID (自定义标识)memcpy(packet[16], CUSTOM_BSSID, 6);// Sequence Numberstatic uint16_t seq 0;packet[22] (seq 0x0F) 4;packet[23] (seq 4) 0xFF;seq;if (len 1450) len 1450;memcpy(packet[24], data, len);esp_wifi_80211_tx(WIFI_IF_STA, packet, 24 len, true);}原理esp_wifi_80211_tx()允许我们在STA模式下直接注入原始802.11帧。这里使用自定义BSSID作为广播标识接收端根据这个标识过滤自己想要的包。2.3 FEC编码回调当FEC编码器完成一个块的处理后会调用此回调。我们需要在这里把编码后的数据包发出去void on_fec_encoded(void *data, size_t size) {send_raw_data(TARGET_MAC, (const uint8_t *)data, size);esp_rom_delay_us(500); // 小延时防止撑爆底层DMA队列}2.4 视频帧分包发送这是发射端的核心逻辑。一帧完整的JPEG图像可能很大需要分成多个包发送void send_video_frame(uint8_t *fb_buf, size_t fb_len) {static uint32_t current_frame_id 0;size_t offset 0;// 单包最大有效载荷 FEC MTU - 视频协议头size_t max_video_payload 1330 - sizeof(Air2Ground_Video_Packet);int packets_sent_this_frame 0;while (offset fb_len) {size_t chunk_size (fb_len - offset max_video_payload)? max_video_payload: (fb_len - offset);uint8_t *packet_buffer my_fec_encoder.get_encode_packet_data(true);if (!packet_buffer) break;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-resolution Resolution::VGA;header-last_part (offset chunk_size fb_len) ? 1 : 0;memcpy(packet_buffer sizeof(Air2Ground_Video_Packet),fb_buf offset, chunk_size);my_fec_encoder.flush_encode_packet(true);offset chunk_size;packets_sent_this_frame;}// PaddingFEC要求每块恰好 coding_k 个包不够就补空包int remainder packets_sent_this_frame % 8; // coding_k 8if (remainder ! 0) {int padding_needed 8 - remainder;for (int i 0; i padding_needed; i) {uint8_t *pad_buf my_fec_encoder.get_encode_packet_data(true);if (pad_buf) {Air2Ground_Video_Packet *header (Air2Ground_Video_Packet *)pad_buf;header-type Air2Ground_Header::Type::Video;header-size sizeof(Air2Ground_Video_Packet); // 只有头无数据header-frame_index current_frame_id;header-last_part 0; // 绝对不能设为1my_fec_encoder.flush_encode_packet(true);}}}current_frame_id;}关键点get_encode_packet_data()返回一个用于填充数据的缓冲区指针flush_encode_packet()将该包送入编码队列。FEC编码器会收集够coding_k个包后计算FEC冗余包并通过回调发出去。2.5 主函数extern C void app_main(void) {vTaskPrioritySet(NULL, 10);// 初始化NVS、网络、WiFiESP_ERROR_CHECK(nvs_flash_init());ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(cfg));ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_start());// 锁定信道关键必须与接收端一致ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_11M_S);ESP_ERROR_CHECK(esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE));// 初始化FEC编码器 (8,12): 8个原始包 → 12个传输包Fec_Codec::Descriptor fec_desc;fec_desc.coding_k 8;fec_desc.coding_n 12;fec_desc.mtu 1330;fec_desc.core Fec_Codec::Core::Core_1;if (!my_fec_encoder.init_encoder(fec_desc)) {ESP_LOGE(TAG, FEC Encoder init failed!);}my_fec_encoder.set_data_encoded_cb(on_fec_encoded);// 初始化摄像头OVCamera cam;if (cam.init(esp32cam_config) ! ESP_OK) {ESP_LOGE(TAG, Camera Init Failed!);esp_restart();}// 主循环采帧 → 发送while (1) {cam.run();if (cam.fb ! NULL) {send_video_frame(cam.fb-buf, cam.fb-len);cam.done();vTaskDelay(pdMS_TO_TICKS(5));} else {ESP_LOGW(TAG, Camera capture failed!);vTaskDelay(pdMS_TO_TICKS(10));}}}三、接收端完整代码fpv_receiver.cpp接收端在混杂模式下监听WiFi信道匹配自定义BSSID后提取payload送入FEC解码器解码完成后通过UVC推流到USB。3.1 头文件与全局定义#include esp_log.h#include esp_mac.h#include esp_wifi.h#include nvs_flash.h#include freertos/FreeRTOS.h#include freertos/queue.h#include freertos/task.h#include tusb.h#include uvc.h#include fec_codec.h#include packets.hstatic const char* TAG RECEIVER;static const uint8_t CUSTOM_BSSID[6] {F, P, V, C, A, R};static const uint8_t WIFI_CHANNEL 6;static const size_t MAX_JPEG_SIZE 100000; // 100KB足够存一帧VGA JPEGstatic QueueHandle_t rx_queue;EXT_RAM_BSS_ATTR static uint8_t frame_buffer[MAX_JPEG_SIZE];typedef struct {int8_t rssi;uint16_t length;uint8_t payload[1400];} rx_packet_t;Fec_Codec my_fec_decoder;3.2 WiFi嗅探回调接收端的关键在混杂模式回调中过滤帧只接收目标BSSID的包void wifi_sniffer_cb(void* recv_buf, wifi_promiscuous_pkt_type_t type) {wifi_promiscuous_pkt_t* sniffer (wifi_promiscuous_pkt_t*)recv_buf;uint8_t* frame sniffer-payload;uint16_t rx_len sniffer-rx_ctrl.sig_len;// 检查是否为Data帧且BSSID匹配if (frame[0] 0x40 rx_len 24) {if (memcmp(frame[16], CUSTOM_BSSID, 6) 0) {uint8_t my_mac[6];esp_read_mac(my_mac, ESP_MAC_WIFI_STA);// 只接收发往本机的包if (memcmp(frame[4], my_mac, 6) 0) {rx_packet_t pkt;pkt.rssi sniffer-rx_ctrl.rssi;pkt.length rx_len - 28; // 去掉WiFi头和FCSif (pkt.length sizeof(pkt.payload))pkt.length sizeof(pkt.payload) - 1;memcpy(pkt.payload, frame[24], pkt.length);xQueueSendFromISR(rx_queue, pkt, NULL);}}}}3.3 WiFi接收任务独立任务处理接收队列将收到的payload送入FEC解码器static void wifi_rx_task(void* param) {rx_packet_t received_pkt;while (1) {if (xQueueReceive(rx_queue, received_pkt, portMAX_DELAY) pdTRUE) {if (received_pkt.length Fec_Codec::PACKET_OVERHEAD) {my_fec_decoder.decode_data(received_pkt.payload,received_pkt.length, true);}}}}3.4 FEC解码回调UVC推流当FEC解码器成功恢复出原始数据包时调用此回调。我们需要根据协议头重组完整的JPEG帧void on_fec_decoded(void* data, size_t size) {Air2Ground_Video_Packet* header (Air2Ground_Video_Packet*)data;if (header-type ! Air2Ground_Header::Type::Video)return;// 安全检查if (header-size sizeof(Air2Ground_Video_Packet))return;size_t chunk_len header-size - sizeof(Air2Ground_Video_Packet);uint8_t* video_data (uint8_t*)data sizeof(Air2Ground_Video_Packet);static uint32_t current_recv_frame_id 0;static size_t current_frame_offset 0;// 新帧开始if (header-frame_index ! current_recv_frame_id) {current_recv_frame_id header-frame_index;current_frame_offset 0;}// 拼接数据if (current_frame_offset chunk_len MAX_JPEG_SIZE) {memcpy(frame_buffer current_frame_offset, video_data, chunk_len);current_frame_offset chunk_len;}// 帧结束标志推送到UVCif (header-last_part 1) {uvc_push_jpeg(frame_buffer, current_frame_offset);ESP_LOGD(TAG, Frame %lu complete, size: %zu bytes,header-frame_index, current_frame_offset);}}四、关键参数说明与调优建议4.1 FEC参数选择参数发射端接收端说明coding_k88每块原始包数越大纠错效率越高但延迟越大coding_n1212编码后总包数冗余率 (12-8)/12 ≈ 33%mtu13301330单包载荷大小考虑FEC头(6B)后不超过WiFi最大帧经验值空旷环境k8,n12足够恶劣环境可尝试k4,n6冗余50%。4.2 MTU计算textWiFi最大帧 1500字节 WiFi头部 24字节 FCS 4字节 安全余量 若干字节 实际可用payload ≈ 1450字节 减去FEC头(6B) 1444字节 再减去视频协议头(13B) ≈ 1431字节 我们取1330保证安全留出100字节给可能的WiFi重传开销4.3 信道选择2.4GHz频段建议选择1、6、11这三个互不重叠的信道。如果有其他WiFi干扰可尝试监测后选择最干净的信道。4.4 速率设置发射端使用了WIFI_PHY_RATE_11M_S11Mbps这是一个比较保守的选择。实际可根据距离调整cpp// 更高速率距离近 esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M); // 更远距离速率低但稳定 esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_2M_L);至此一个数字FPV的系统就构建完成了。TODO1. 安装到STM32小车上就是一个fpv智能小车了2. 安装到自己搓的无人机上就是一个穿越机了。3. 整个项目分三步文章是通过deepseek根据代码总结出来的。固件可以从咸鱼上找到请尝试检索“ESP32-S3 超低延时点对点无线图传/数传固件含FECUVC/ACM”