ESP32音频/语音技术(7)ESP-NOW无线音频方案

一、 Audio-Tools 库 ESP-NOW 无线音频方案

audio-tools 开源库中有 4 类 esp-now 无线音频方案,每一类下面包含发送 / 接收配套工程。

仓库路径:examples/examples-communication/esp-now

复制代码
esp-now
├─ broadcast    # 裸PCM,ESP-NOW广播(一对多)
├─ codec        # 带音频编码压缩(Opus),ESP-NOW单播点对点
├─ pcm          # 裸PCM,ESP-NOW单播点对点(原始未压缩PCM音频)
└─ speed-test   # ESP-NOW 传输速率性能测速,不带音频播放

1、broadcast(裸 PCM 广播方案)

  • communication-broadcast-espnow-send:广播发送端 发送裸 16bit PCM 音频,ESP-NOW 广播模式(目标 FF:FF:FF:FF:FF:FF),use_send_ack=false,一对多,无 ACK 重传。音源默认正弦波,Throttle 限速。
  • communication-broadcast-espnow-receive:广播接收端 监听 ESP-NOW 广播包,接收裸 PCM,Throttle 做流平滑,I2S 输出播放。同信道所有 ESP 设备都可以收到。

✅ 特点:原始 PCM,不压缩;一对多广播 ❌ 缺点:码率高,容易丢包,广播不支持 ACK 重传

2、pcm(裸 PCM 单播点对点方案)

pcm = raw PCM,音频不做任何压缩,直接把 16bit PCM 样本通过 ESP-NOW 点对点传输。

  • communication-espnow-send:单播发送,填写接收端 MAC 作为 peer,开启 ACK 重传。原始 PCM 流 + Throttle。
  • communication-espnow-receive:单播接收,收到裸 PCM,I2S 播放
  • communication-espnow-receive_csv:接收端,把收到的 PCM 样本打印成 CSV 格式到串口 ,用于电脑导出、波形分析,不播放声音
  • communication-espnow-receive_measure:接收端性能测量,统计数据包数量、丢包率、延迟,串口输出统计,不播放音频

✅ 特点:点对点单播,有 ACK 重传,可靠性比广播高;但是PCM 码率大,带宽压力大。适合短距离一对一无线音频。

3、codec(Opus压缩编码单播方案)

这是压缩音频版本,发送端 PCM → Opus 编码器 → ESP-NOW 传输 → 接收端 Opus 解码器 → PCM → I2S 播放

  • communication-codec-espnow-send:发送端,采集 PCM,实时 Opus 压缩,再 ESP-NOW 单播发送
  • communication-codec-espnow-receive:接收端,收到压缩包,Opus 解码还原 PCM,I2S 输出播放
  • communication-codec-espnow-receive_measure:接收 + 性能统计,统计丢包、延迟,同时解码播放

✅ 核心优势:Opus 压缩,大幅降低码率,同样 WiFi 环境下丢包更少,传输距离更远。适合麦克风语音传输

代价:ESP32-S3 会占用 CPU 算力做编解码。

4、speed-test (传输速度压力测试)

  • communication-espnow-send:发送端,持续发送大量测试数据包,不产生音频
  • communication-espnow-receive_measure:接收端,统计 ESP-NOW 最大吞吐量、包速率、丢包率,串口打印测速结果。

用途:评估你的硬件环境下 ESP-NOW 最高能跑多高带宽,用来判断能不能承载 PCM 或者 Opus 音频流,没有音频播放。

5、 四套方案横向对比

|------------|------------|------------|-----|--------------------------|
| 目录 | 音频格式 | ESP-NOW 模式 | ACK | 典型用途 |
| broadcast | Raw PCM 裸流 | 广播(一对多) | ❌关闭 | 一个发,多个喇叭同时收听;原始 PCM |
| pcm | Raw PCM 裸流 | 单播点对点 | ✅开启 | 一对一无线音频,无压缩,简单;带宽压力大 |
| codec | Opus 压缩音频 | 单播点对点 | ✅开启 | 语音传输,压缩降码率,丢包更少(麦克风项目首选) |
| speed-test | 无音频,纯测试数据 | 单播点对点 | ✅开启 | ESP-NOW 带宽、丢包性能评估 |

二、ESPNowStream 解析

ESPNowStream 是 AudioTools 库封装的 ESP‑NOW 流包装类 ,继承 Arduino Stream,底层依然调用官方 esp_now.h。 核心定位:把 ESP-NOW 包装成标准流,和 Serial、I2SStream 统一 API,自动处理分包、重组、环形缓冲区,专门用于连续流式数据(音频、压力测试)。

头文件

复制代码
#include "AudioTools.h"
#include "AudioTools/Communication/ESPNowStream.h"

类:ESPNowStream

1、核心配置结构体 ESPNowConfig

复制代码
auto cfg = now.defaultConfig(); // 获取默认配置

常用成员:

  1. cfg.mac_address:ESPNow 逻辑 MAC (字符串格式"XX:XX:XX:XX:XX:XX",用于 ESP-NOW 对等识别,不能修改芯片硬件 MAC)
  2. cfg.rate:WiFi 物理速率,WIFI_PHY_RATE_1M / 2M /5_5M /11M /24M,收发两端必须一致
  3. cfg.channel:WiFi 信道,默认自动,多设备必须同信道
  4. cfg.buffer_tx_size:发送环形缓冲区大小
  5. cfg.buffer_rx_size:接收环形缓冲区大小
  6. cfg.retry_count:单播重试次数

2、基础公共接口(重点)

1. 对象实例化

复制代码
ESPNowStream now;

2. 初始化

复制代码
bool begin(ESPNowConfig &config);
  • 作用:初始化 WiFi、ESP-NOW 底层、收发环形 buffer;
  • 返回true成功;false失败。

3. Peer 对等设备管理

复制代码
// 添加单个peer
bool addPeer(const char *macStr);
// 添加peer数组(你压测代码在用)
bool addPeers(const char **macArray, int count=-1);
// 删除peer
bool removePeer(const char *macStr);

peer 就是通信对端 MAC;广播模式填 FF:FF:FF:FF:FF:FF ESP-NOW 限制最多 20 个 peer。

4. Stream 标准读写接口(最重要,和 Serial 一模一样)

复制代码
// 写数据:返回实际写入缓冲区字节数,不一定已经无线发出
virtual size_t write(const uint8_t *buffer, size_t size);

// 读取收到的数据
virtual size_t readBytes(uint8_t *buffer, size_t length);
virtual int read();
virtual int available(); // 返回接收缓冲区可读字节

✅ 核心黑科技:write 任意长度,库内部自动切割成≤250 字节 ESP-NOW 小包;接收端自动缓存分片,集齐所有分片重组完整大块再输出 ⚠️ 风险:任意分片丢失,整块数据包直接丢弃。

5. 辅助接口

复制代码
// 停止ESPNow
void end();
// 获取当前状态、错误信息(日志输出)

3、使用范式(两种典型用法)

方式 A:手动 read /write(原始读写,适合压测)

发送端

复制代码
ESPNowStream now;
const char *peers[] = {"A8:48:FA:0B:93:01"};

void setup(){
    auto cfg = now.defaultConfig();
    cfg.mac_address = "A8:48:FA:0B:93:02";
    cfg.rate = WIFI_PHY_RATE_24M;
    now.begin(cfg);
    now.addPeers(peers);
}

void loop(){
    uint8_t buf[1024] = {0};
    now.write(buf,1024); // 自动分包发送
}

接收端

复制代码
ESPNowStream now;
uint8_t buf[1024];
const char *peers[] = {"A8:48:FA:0B:93:02"};

void setup(){
    auto cfg = now.defaultConfig();
    cfg.mac_address = "A8:48:FA:0B:93:01";
    cfg.rate = WIFI_PHY_RATE_24M;
    now.begin(cfg);
    now.addPeers(peers);
}

void loop(){
    if(now.available()>0){
        int len = now.readBytes(buf,1024);
    }
}

方式 B:搭配 StreamCopy(AudioTools 生态标准用法)

StreamCopy自动循环把源流拷贝到目标流,不需要手动 read/write,适合音频 / 连续流。

复制代码
ESPNowStream now;
MeasuringStream out;
StreamCopy copier(out, now); // src=now, dst=out

void setup(){
    //初始化now
    out.begin();
}
void loop(){
    copier.copy(); // 在loop持续搬运数据
}

可以串接 I2SStream、Throttle、MeasuringStream 等 AudioTools 所有流组件。

4、 工作原理

  1. now.write()写入数据 → 内部代码把大数据切成 ≤250 字节分片包,每个分片带上序号
  2. 通过原生esp_now_send()发出
  3. 接收端 ESP-NOW 回调收到小包,存入 ringbuffer
  4. 接收端 ESPNowStream 检测分片序号,缓存等待全部分片收齐
  5. 完整大块重组完成后,才对外暴露给read()或者 StreamCopy 读取
  6. 如果任意分片丢包,缓存的这一组分片全部清空,整块数据丢弃(这个是封装带来的代价)

5、 关键注意事项

  1. 两端 cfg.rate 必须完全一致,否则大量丢包甚至收不到包
  2. 单播模式:带 ACK,吞吐上限低、可靠性更高;广播 FF:FF:FF:FF:FF:FF,无 ACK,发包速度更快,丢包概率变大
  3. 分片特性:单次 write 越大,分片越多,丢包对整块数据影响越严重;压测推荐 240 字节以内不分片
  4. 信道:ESP-NOW 通信设备必须同一 WiFi 信道
  5. 流模型是轮询:copier.copy()/read()都要放在 loop 里持续执行;主 loop 阻塞会造成接收缓冲区溢出丢包
  6. 不直接暴露底层 esp_now 回调、RSSI、每个小包的 ACK 状态,底层信息拿不到(封装的取舍)

6、适用场景 & 不适合场景

✅ 适合

  • 连续流式 PCM 音频传输
  • 带宽压力测试(你现在的场景)
  • 快速开发,不想手写分包重组、环形队列

❌ 不适合

  • 低延迟控制指令(需要底层回调)
  • 需要获取每个无线包 RSSI、ACK 状态
  • 需要自定义丢包重传策略

三、ESPNowStream 与原生 ESPNow 对比

原生 esp_now.h:小包回调驱动,需要手动分包重组;自由度高,代码量大。

ESPNowStream:上层 Stream 封装,自动分片重组;一行 StreamCopy 跑流,开发简单,牺牲部分底层控制权。

ESPNowStream 本质是在原生 esp_now.h****之上做了一层面向 Stream****的高层封装 。 原生 ESP-NOW 是回调驱动、小包(最大 250 字节)底层 API ;ESPNowStream 把它包装成 Arduino 标准Stream(和 Serial 一样,直接 read/write),并且内置了分包、重组、环形缓冲,所以写代码极简,代价是牺牲了一部分底层自由度。

1、ESP-NOW 原理

原生 ESP-NOW 是事件回调模型:

  1. 必须手动初始化 WiFi,设置信道
  2. 注册 发送回调、接收回调(中断上下文!回调里不能做复杂操作、不能串口打印)
  3. 底层硬性限制:单次 payload 最多250 字节 。如果你要发 1024 字节,你自己必须手动做分包、加序号、接收端写重组逻辑
  4. 自己维护环形缓冲区,处理队列满、丢包、分片不全的情况
  5. 发送:esp_now_send(),返回值处理、peer 管理、PMK 加密都要手动写
  6. 接收数据只能在回调里拿到,必须把数据拷贝到全局 buffer,loop 里再消费,代码割裂(回调和主 loop 两套代码)

原生最小 demo 也要几十行,一旦做大流传输(音频、持续压测),分包重组 + 环形队列自己写非常容易出 bug。

2、ESPNowStream 封装功能

  1. 包装成 Arduino Stream :直接 now.write() / now.read(),和 Serial、I2SStream 接口完全统一。所以可以直接搭配StreamCopy、MeasuringStream、Throttle这一套 AudioTools 生态组件,一行拷贝就实现流转发。
  2. 内置分片 & 重组 :你直接 write (1024),库内部自动切成多个≤250 字节 ESP-NOW 包;接收端收到分片后自动缓存、重组,集齐全部分片才输出完整 1024 块。也就是你日志里看到的:1024 ->1024。
  3. 内部自带环形 buffer:收发缓冲队列管理,队列溢出的判断库内部处理
  4. 封装 WiFi 初始化、peer 添加、WiFi PHY 速率设置 :cfg.rate = WIFI_PHY_RATE_24M一行搞定,原生要手动调用 wifi API
  5. 内置日志:直接打印拷贝字节、bps 测速,配套 MeasuringStream 直接统计吞吐

3、 两者选型对比

|--------|-------------------------|-----------------------------|
| 项目 | 原生 esp_now.h | AudioTools ESPNowStream |
| 编程模型 | 回调驱动,事件中断 | 标准 Stream,阻塞 / 轮询 |
| 大包传输 | 必须手写分包重组 | 内置分片重组,直接 write 任意长度 |
| 代码量 | 多,容易踩坑 | 极少,一行 StreamCopy |
| 生态 | 独立 API,和 I2S / 音频流要自己粘合 | 原生配套 AudioTools 整套音频组件 |
| 底层控制权 | 完全可控(ACK、RSSI、信道、加密) | 被封装,底层事件拿不到 |
| 适用场景 | 控制指令、低延迟、需要精细底层控制 | 持续流式数据(音频、你现在的吞吐压测) |

三、ESPNowStream 传输速度测试

1、传输速度压力测试代码

ESP‑NOW 压力 / 吞吐性能测试代码

目标:全速往 ESP-NOW 接口灌固定数据,测试最大发包能力、队列溢出、丢包、延迟,不是音频传输,所以不需要 Throttle、I2S、音频限流。

代码核心作用

循环不间断调用 now.write(buffer,1024),持续输出 1024 字节全 0 缓冲区,用来压测 ESPNowStream 封装层 + ESP-NOW 底层:

  1. 测试 ESPNowStream 内部分片逻辑(ESP-NOW 底层单包最大 payload 250 字节,所以 1024 会自动拆成多片)

  2. 观察串口日志,看发送队列满、溢出、丢包报错

  3. 测试WIFI_PHY_RATE_24M高速模式下的极限吞吐量

  4. 配合接收端统计收到字节数、丢包率,算出实际带宽

    #include "AudioTools.h"
    #include "AudioTools/Communication/ESPNowStream.h"

    ESPNowStream now;
    const char *peers[] = {"3C:0F:02:D8:16:14"};
    uint8_t buffer[1024] = {0};

    void setup() {
    Serial.begin(115200);
    AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Warning);

    auto cfg = now.defaultConfig();
    cfg.mac_address = "10:51:DB:85:0D:88";
    cfg.rate = WIFI_PHY_RATE_24M;
    now.begin(cfg);
    now.addPeers(peers);

    Serial.println("Sender started...");
    }

    void loop() {
    now.write(buffer, 1024);
    }

接收端:

复制代码
#include "AudioTools.h"
#include "AudioTools/Communication/ESPNowStream.h"

ESPNowStream now;
MeasuringStream out;
StreamCopy copier(out, now);     
const char *peers[] = {"10:51:DB:85:0D:88"};

void setup() {
  Serial.begin(115200);
  AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Info);

  auto cfg = now.defaultConfig();
  cfg.mac_address = "3C:0F:02:D8:16:14";
  cfg.rate = WIFI_PHY_RATE_24M;
  now.begin(cfg);
  now.addPeers(peers);

  // start I2S
  Serial.println("starting Out...");
  out.begin();

  Serial.println("Receiver started...");
}

void loop() { 
  copier.copy();
}

2、传输速度压力测试分析

(1)测试结果
复制代码
[I] StreamCopy.h : 185 - StreamCopy::copy  1024 -> 1024 -> 1024 bytes - in 1 hops 
[I] MeasuringStream.h : 190 -  ==> Bytes per second: 131282
  • 1024 -> 1024 -> 1024 bytes:从 ESPNowStream 读出 1024 字节,完整送入 MeasuringStream,没有丢包、没有截断
  • in 1 hops:本次拷贝一次就读满整块数据
  • Bytes per second: 131282:接收端实测速率~131KB/s,下一轮降到~129.6KB/s

换算比特率: 131282 B/s × 8 ≈ 1,050,256 bps ≈ 1.05 Mbps

(2)结果评估

发送端一次 write 1024 字节(ESPNowStream 内部分片传输),24M PHY 速率,单播 ESP-NOW,实测稳定 ~130KB/s。 这个数值符合 ESP32 ESP-NOW 单播的典型上限:

  • 单播带 ACK,每一包无线报文都需要接收端回 ACK,会严重压低吞吐;
  • 虽然 WiFi 物理层设 24Mbps,但 ESP-NOW 单播实际应用层能跑到 1Mbps 左右是正常水平;
  • 你这里没有出现丢包(日志里每块 1024 全部收齐,没有部分读取),说明近距离、干扰小,链路质量很好。

✅ 当前状态:链路无丢包,稳定跑在~130KB/s

✅ ESPNowStream 的分片重组工作正常:1024 的大块被拆成多个 ESP-NOW 底层包,接收侧成功重组回完整 1024 字节块

❇️ 注意:这个是应用层有效数据速率,不含 WiFi 包头、ESP-NOW 协议头、ACK 报文开销。

(3)带宽换算
  • 130KB/s = 1040kbps
  • 16kHz 16bit 单声道 PCM 音频:16000 × 16 ×1 /8 = 32000 B/s = 32KB/s
  • 👉 也就是说当前 ESP-NOW 链路带宽足够同时传输多路 PCM 语音,这也是这个库用来传音频的基础。
相关推荐
AI视频工具指南4 小时前
怎么用 AI 做一两分钟的小故事视频?
人工智能·aigc·音视频
ACP广源盛139246256735 小时前
项目评估记录:4K 远距离视频 Serdes 传输方案调研,GSV5800@ACP参数与工程问题记录
笔记·硬件架构·音视频·视频·国产芯片
碧海银沙音频科技研究院7 小时前
面向长尾音频分类的知识蒸馏研究
人工智能·算法·音视频
BeiGuTing_code7 小时前
C链表基础(二)有视频模拟!!!
c语言·链表·音视频
燕落南枝7 小时前
HTML 视频标签(video)详解笔记
笔记·html·音视频
霖霖7149 小时前
【ESP32笔记】I2S的音频录制与播放
笔记·单片机·mcu·音视频
码上观世界12 小时前
Agent 对话里的图片与视频处理
音视频
lingyubb15 小时前
影视解说配音工具的技术选型与对比(行业观察)
音视频·ai配音·多语种
科技小E21 小时前
监控设备品牌杂乱无法互通?EasyGBS打通孤岛,统一汇聚视频
服务器·网络·音视频