一、 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(); // 获取默认配置
常用成员:
cfg.mac_address:ESPNow 逻辑 MAC (字符串格式"XX:XX:XX:XX:XX:XX",用于 ESP-NOW 对等识别,不能修改芯片硬件 MAC)cfg.rate:WiFi 物理速率,WIFI_PHY_RATE_1M / 2M /5_5M /11M /24M,收发两端必须一致cfg.channel:WiFi 信道,默认自动,多设备必须同信道cfg.buffer_tx_size:发送环形缓冲区大小cfg.buffer_rx_size:接收环形缓冲区大小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、 工作原理
now.write()写入数据 → 内部代码把大数据切成 ≤250 字节分片包,每个分片带上序号- 通过原生
esp_now_send()发出 - 接收端 ESP-NOW 回调收到小包,存入 ringbuffer
- 接收端 ESPNowStream 检测分片序号,缓存等待全部分片收齐
- 完整大块重组完成后,才对外暴露给
read()或者 StreamCopy 读取 - 如果任意分片丢包,缓存的这一组分片全部清空,整块数据丢弃(这个是封装带来的代价)
5、 关键注意事项
- 两端 cfg.rate 必须完全一致,否则大量丢包甚至收不到包
- 单播模式:带 ACK,吞吐上限低、可靠性更高;广播 FF:FF:FF:FF:FF:FF,无 ACK,发包速度更快,丢包概率变大
- 分片特性:单次 write 越大,分片越多,丢包对整块数据影响越严重;压测推荐 240 字节以内不分片
- 信道:ESP-NOW 通信设备必须同一 WiFi 信道
- 流模型是轮询:
copier.copy()/read()都要放在 loop 里持续执行;主 loop 阻塞会造成接收缓冲区溢出丢包 - 不直接暴露底层 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 是事件回调模型:
- 必须手动初始化 WiFi,设置信道
- 注册 发送回调、接收回调(中断上下文!回调里不能做复杂操作、不能串口打印)
- 底层硬性限制:单次 payload 最多250 字节 。如果你要发 1024 字节,你自己必须手动做分包、加序号、接收端写重组逻辑
- 自己维护环形缓冲区,处理队列满、丢包、分片不全的情况
- 发送:
esp_now_send(),返回值处理、peer 管理、PMK 加密都要手动写 - 接收数据只能在回调里拿到,必须把数据拷贝到全局 buffer,loop 里再消费,代码割裂(回调和主 loop 两套代码)
原生最小 demo 也要几十行,一旦做大流传输(音频、持续压测),分包重组 + 环形队列自己写非常容易出 bug。
2、ESPNowStream 封装功能
- 包装成 Arduino Stream :直接
now.write()/now.read(),和 Serial、I2SStream 接口完全统一。所以可以直接搭配StreamCopy、MeasuringStream、Throttle这一套 AudioTools 生态组件,一行拷贝就实现流转发。 - 内置分片 & 重组 :你直接 write (1024),库内部自动切成多个≤250 字节 ESP-NOW 包;接收端收到分片后自动缓存、重组,集齐全部分片才输出完整 1024 块。也就是你日志里看到的:
1024 ->1024。 - 内部自带环形 buffer:收发缓冲队列管理,队列溢出的判断库内部处理
- 封装 WiFi 初始化、peer 添加、WiFi PHY 速率设置 :
cfg.rate = WIFI_PHY_RATE_24M一行搞定,原生要手动调用 wifi API - 内置日志:直接打印拷贝字节、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 底层:
-
测试 ESPNowStream 内部分片逻辑(ESP-NOW 底层单包最大 payload 250 字节,所以 1024 会自动拆成多片)
-
观察串口日志,看发送队列满、溢出、丢包报错
-
测试
WIFI_PHY_RATE_24M高速模式下的极限吞吐量 -
配合接收端统计收到字节数、丢包率,算出实际带宽
#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 语音,这也是这个库用来传音频的基础。