ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ggwave 在 Arduino Nano RP2040 Connect 上的声音数据传输实战:内置麦克风接收 + OLED 显示 + 蜂鸣器回传

ggwave 在 Arduino Nano RP2040 Connect 上的声音数据传输实战:内置麦克风接收 + OLED 显示 + 蜂鸣器回传 通信物联网嵌入式【免费下载链接】ggwaveTiny>项目地址https://gitcode.com/GitHub_Trending/gg/ggwave点击查看免费下载本文围绕 ggwave 仓库中的 arduino-rx 示例 展开讲解如何利用 Arduino Nano RP2040 Connect 开发板内置的 PDM 麦克风实现 data-over-sound声音传数据接收并可选配 SSD1306 OLED 显示屏实时呈现解码结果、通过按键与蜂鸣器将收到的最新数据回传。读完本文你将掌握该示例的硬件接线、Arduino 草图的核心实现音频采样、GGWave 实例配置、协议选择、内存预算、与 Waver 应用联调的方法以及底层 FSK 调制与 Reed-Solomon 纠错的原理依据。一、示例概览为什么选 Arduino Nano RP2040 Connectggwave 是一个 Tiny>const int kPinLED0 2; // 板载 RGB LED 的红色通道 const int kPinButton0 5; // D5 按键按下时回传最后收到的数据 const int kPinSpeaker 10; // D10 蜂鸣器输出其中 LED0引脚 2在收到新数据后点亮用于无屏场景下的视觉反馈。接线全貌可参考示例目录中的面包板接线图三、从源码看懂采集链路PDM 麦克风与环形缓冲区接收端的数据流是PDM 麦克风 → ISR 中断回调 → 环形缓冲区 → 主循环逐帧送入ggwave.decode()。这是理解整个示例的关键。音频采集参数草图顶部定义了采集参数arduino-rx.inousing TSample int16_t; // 采样位深16-bit 有符号整数 const size_t kSampleSize_bytes sizeof(TSample); // 2 字节/采样 const char channels 1; // 单声道 const int sampleRate 6000; // 采样率 6 kHz const int samplesPerFrame 128; // 每帧 128 个采样采样率 6000 Hz 与samplesPerFrame 128的组合意味着FFT 频率分辨率hzPerSample为 6000/128 46.875 Hz/bin这恰好对应项目 README 中描述的全协议通用频率间隔dF 46.875 HzREADME.md是 ggwave 协议的原生分辨率每帧 128 个采样、采样率 6000 Hz一帧音频时长为128/6000 ≈ 21.3 ms这是解码器一次处理的最小时间片。环形缓冲区音频数据在中断中写入、主循环中读取因此使用无锁环形缓冲区arduino-rx.inoconst int qpow 9; const int qmax 1 qpow; // 512 个采样 volatile int qhead 0; // 读取指针主循环推进 volatile int qtail 0; // 写入指针ISR 推进 volatile int qsize 0; // 有效采样数 TSample sampleBuffer[qmax];PDM 中断回调onPDMdataonPDMdata()由 PDM 库在中断上下文ISR中调用因此不能在其中使用Serial打印草图注释明确提示了这一点见 arduino-rx.inovoid onPDMdata() { const int bytesAvailable PDM.available(); const int nSamples bytesAvailable/kSampleSize_bytes; if (qsize nSamples qmax) { // 缓冲区溢出说明主循环来不及消费可增大 qmax err 10; qhead 0; qtail 0; qsize 0; // 丢弃全部数据 } PDM.read(sampleBuffer qtail, bytesAvailable); qtail nSamples; qsize nSamples; if (qtail qmax) { err 1; // qmax 不是采样块大小的整数倍时会触发 } if (qtail qmax) { qtail - qmax; // 环形回绕 } }这个回调还承担了看门狗职责err变量累计错误码主循环发现err 0时打印ERROR并清零arduino-rx.ino。这通常意味着微控制器无法实时处理采集到的数据——如果出现应优先增大qmax或减小解码负载。四、GGWave 实例配置详解初始化发生在setup()中先取默认参数再按需覆盖最后调用prepare()分配内存。核心参数ggwave.setLogFile(nullptr); // 关闭内部日志 auto p GGWave::getDefaultParameters(); // 获取默认参数 #ifdef LONG_RANGE p.payloadLength 8; // 长距离模式载荷 8 字节 #else p.payloadLength 16; // 默认固定载荷 16 字节 #endif p.sampleRateInp sampleRate; // 6000麦克风采集率 p.sampleRateOut sampleRate; // 6000扬声器播放率 p.sampleRate sampleRate; // 6000工作采样率 p.samplesPerFrame samplesPerFrame; // 128 p.sampleFormatInp GGWAVE_SAMPLE_FORMAT_I16; // 输入 16-bit 整数 p.sampleFormatOut GGWAVE_SAMPLE_FORMAT_U8; // 输出 8-bit 无符号蜂鸣器只需频率不关心幅值 p.operatingMode GGWAVE_OPERATING_MODE_RX | GGWAVE_OPERATING_MODE_TX | GGWAVE_OPERATING_MODE_USE_DSS | GGWAVE_OPERATING_MODE_TX_ONLY_TONES;各字段含义依据 include/ggwave/ggwave.h 的注释参数本示例取值说明payloadLength16默认/ 8LONG_RANGE固定载荷字节数。 0时启用固定长度解码不发射声音标记源码m_nMarkerFrames parameters.payloadLength 0 ? 0 : kDefaultMarkerFrames见 src/ggwave.cpp解码更快且更省带宽sampleRateInp6000采集采样率与sampleRate不同时内部自动重采样sampleRateOut6000播放采样率同理可自动重采样sampleRate6000工作采样率范围在kSampleRateMin 1000到kSampleRateMax 96000之间samplesPerFrame128FFT 帧大小决定频谱 bin 数默认值是 1024sampleFormatInpI16麦克风数据格式16-bit 有符号sampleFormatOutU8输出波形格式operatingModeRXTXUSE_DSSTX_ONLY_TONES见下文操作模式位掩码操作模式来自 ggwave.h 的位定义GGWAVE_OPERATING_MODE_RX 1 1, // 允许接收 GGWAVE_OPERATING_MODE_TX 1 2, // 允许发送 GGWAVE_OPERATING_MODE_RX_AND_TX RX | TX, GGWAVE_OPERATING_MODE_TX_ONLY_TONES 1 3, // 只生成音调列表而非完整波形低内存设备专用 GGWAVE_OPERATING_MODE_USE_DSS 1 4, // 启用内置直序扩频DSS算法TX_ONLY_TONES是本示例的精髓蜂鸣器只能发单音无法播放完整波形。开启该模式后encode()只产生一系列Tone每个元素是 FFT bin 索引主循环据此逐个调用 Arduino 的tone()函数发声。协议定义中每个音调的频率与时长由以下公式给出ggwave.hfreq_hz (protocol.freqStart Tone) * hzPerSample duration_ms protocol.txDuration_ms(samplesPerFrame, sampleRate)hzPerSample在源码中计算为m_sampleRate/m_samplesPerFramesrc/ggwave.cpp即本示例中的 46.875 Hz/bin。协议选择内存与鲁棒性的权衡草图使用disableAll()toggle()逐项开启所需协议arduino-rx.ino// TX只保留 MT_FASTEST单音、最快协议 GGWave::Protocols::tx().disableAll(); GGWave::Protocols::tx().toggle(GGWAVE_PROTOCOL_MT_FASTEST, true); // RX默认只开 DT_FASTEST 与 MT_FASTEST GGWave::Protocols::rx().disableAll(); #ifdef LONG_RANGE GGWave::Protocols::rx().toggle(GGWAVE_PROTOCOL_DT_FAST, true); #endif GGWave::Protocols::rx().toggle(GGWAVE_PROTOCOL_DT_FASTEST, true); #ifdef LONG_RANGE GGWave::Protocols::rx().toggle(GGWAVE_PROTOCOL_MT_FAST, true); #endif GGWave::Protocols::rx().toggle(GGWAVE_PROTOCOL_MT_FASTEST, true);协议族定义在 ggwave.h本示例只用到单音MTmono-tone与双音DTdual-tone系列其结构字段为struct Protocol { const char * name; // 协议名如 [MT] Fastest int16_t freqStart; // 最低频率的 FFT bin 索引 int8_t framesPerTx; // 传输一个数据块所需帧数 int8_t bytesPerTx; // 一个数据块的字节数 int8_t extra; // 2 表示单音协议1 表示双音协议 bool enabled; };MT 系列extra2每个时刻只发一个音调每个音调携带 4 bit一个数据块nTones 2*bytesPerTx/2 1个音调适合低功耗蜂鸣器抗干扰性更好DT 系列extra1同时发两个音调6 个 4-bit chunk 中的 3 字节吞吐更高framesPerTx越小Fastest3传输越快但每帧携带的比特更多鲁棒性相对降低LONG_RANGE模式下启用DT_FAST/MT_FAST这类更慢但更稳的协议代价是解码内存与时长增加。协议表里还有注释掉的可选项包括可听域AUDIBLE与超声ULTRASOUND系列若要在 Arduino UNO 等资源更受限的板子上使用可在编译时定义GGWAVE_CONFIG_FEW_PROTOCOLS见 ggwave.h裁剪掉这些协议以节省内存。调优提示若蜂鸣器无法良好发声在 1–2 kHz 单音范围可像 arduino-tx 示例 那样把freqStart整体上移例如48个 bin48 × 46.875 Hz ≈ 2250 Hz。此时接收端如 Waver 应用必须做相同偏移否则无法解调。内存预算先查询后分配草图的巧妙之处在于两阶段初始化arduino-rx.inoggwave.prepare(p, false); // allocatefalse只计算所需内存不分配 Serial.print(F(Required memory by the ggwave instance: )); Serial.print(ggwave.heapSize()); // 打印所需字节数 Serial.println(F( bytes)); ggwave.prepare(p, true); // 真正分配并初始化prepare(parameters, allocatefalse)的语义在 ggwave.h 中有明确说明所有内存缓冲都在prepare()中一次性分配之后不再发生任何动态分配。先以allocatefalse查询heapSize()再正式初始化方便在串口监视器中评估内存余量——这是嵌入式场景下的标准实践。源码中缓冲区大小由payloadLength、启用的协议数量等共同决定src/ggwave.cpp因此只启用需要的协议能显著降低内存占用。五、主循环解码、取数、显示与回传逐帧解码与实时性监控主循环以samplesPerFrame为单位消费缓冲区arduino-rx.inowhile (qsize samplesPerFrame) { auto tStart millis(); ggwave.decode(sampleBuffer qhead, samplesPerFrame*kSampleSize_bytes); qsize - samplesPerFrame; qhead samplesPerFrame; if (qhead qmax) qhead 0; // 环形回绕 auto tEnd millis(); if (niter % 10 0) { Serial.println(tEnd - tStart); // 每次 decode() 耗时 if (tEnd - tStart 1000*(float(samplesPerFrame)/sampleRate)) { Serial.println(F(Warning: decode() took too long to execute!)); } } ... }实时性判据decode()单次耗时必须小于一帧音频时长。本示例中128/6000 ≈ 21.3 ms若耗时超限主循环将跟不上采集速度缓冲区会持续溢出ISR 中err累加此时应降低协议数量或提高性能。草图每 10 次解码打印一次耗时方便在串口绘图器中观察。取出解码数据解码结果通过消费式API 取出arduino-rx.inoGGWave::TxRxData result; nr ggwave.rxTakeData(result); // 返回解码字节数 if (nr 0) { Serial.print(F(Received data with length )); Serial.print(nr); // 应等于 p.payloadLength Serial.println(F( bytes:)); Serial.println((char *) result.data()); strcpy(resultLast, (char *) result.data()); // 保存最近一次结果 tLastReceive tEnd; }rxTakeData()与rxData()/rxSpectrum()等 Rx 方法在 ggwave.h 中定义rxTakeData消费并返回已解码数据返回长度即载荷字节数固定长度模式下应等于payloadLength。解码是流式的——每次decode()只分析传入的一帧内部维护帧历史spectrumHistoryFixed、amplitudeHistory等见 ggwave.h跨帧累积足够数据后才产出结果这正是主循环需要持续喂帧的原因。OLED 频谱可视化可选开启DISPLAY_OUTPUT宏后每次成功取到频谱就刷新屏幕arduino-rx.inostatic GGWave::Spectrum rxSpectrum; if (ggwave.rxTakeSpectrum(rxSpectrum) t 2000) { const bool isNew t - tLastReceive 2000; // 2 秒内收到新数据视为新 digitalWrite(kPinLED0, isNew ? HIGH : LOW); // 新数据点亮板载 LED display.clearDisplay(); display.setTextSize(isNew ? 2 : 1); // 新数据用大字号 display.println(resultLast); // 顶部显示最近接收的文本 // 绘制 64 个 bin 的实时频谱柱状图 const int nBin0 16, nBins 64, dX SCREEN_WIDTH/nBins; float smax 0.0f; for (int x 0; x nBins; x) smax std::max(smax, rxSpectrum[nBin0 x]); smax smax 0.0f ? 1.0f : 1.0f/smax; for (int x 0; x nBins; x) { const int x0 x*dX, x1 x0 dX; const int y (int)(0.75f*SCREEN_HEIGHT*(rxSpectrum[nBin0 x]*smax)); display.fillRect(x0, SCREEN_HEIGHT - y, dX, y, SSD1306_WHITE); } display.display(); }这段代码展示了 ggwave 的实时频谱能力rxTakeSpectrum()返回 FFT 幅度谱配合fillRect即可在 128x32 的 OLED 上画出数据音频的频域形态——既是调试利器也能直观验证声音里真的有数据。按键触发回传主循环还处理按键去抖与回传arduino-rx.inoint but0 digitalRead(kPinButton0); if (but0 LOW but0Prev HIGH) { // 下降沿触发 PDM.end(); // 发送前先停掉麦克风 delay(500); // 留出余音衰减时间 send_text(ggwave, kPinSpeaker, resultLast, GGWAVE_PROTOCOL_MT_FASTEST); if (!PDM.begin(channels, sampleRate)) { // 恢复采集 Serial.println(F(Failed to start PDM!)); while (1); } but0Prev LOW; }send_text()是回传的核心arduino-rx.inovoid send_text(GGWave ggwave, uint8_t pin, const char * text, GGWave::TxProtocolId protocolId) { ggwave.init(text, protocolId); // 设置待发送文本与协议 ggwave.encode(); // 生成音调序列TX_ONLY_TONES 模式 const auto protocol GGWave::Protocols::tx()[protocolId]; const auto tones ggwave.txTones(); // 音调列表bin 索引 const auto duration_ms protocol.txDuration_ms(ggwave.samplesPerFrame(), ggwave.sampleRateOut()); for (auto curTone : tones) { const auto freq_hz (protocol.freqStart curTone)*ggwave.hzPerSample(); tone(pin, freq_hz); // Arduino tone() 产生方波单音 delay(duration_ms); } noTone(pin); digitalWrite(pin, LOW); }注意发送与接收使用同一工作采样率 6000 HztxTones()返回的每个 bin 索引经hzPerSample换算成实际频率由tone()驱动蜂鸣器txDuration_ms()计算每帧的时长framesPerTx × 1000 × samplesPerFrame / sampleRate见 ggwave.h。发送期间必须暂停 PDM 采集PDM.end()否则蜂鸣器声音会被自身麦克风采进去形成回授。六、宏开关DISPLAY_OUTPUT 与 LONG_RANGE草图通过两个编译期宏提供功能裁剪位于文件头部arduino-rx.ino//#define DISPLAY_OUTPUT 1 // 启用 SSD1306 显示输出需接线 I2C 屏 //#define LONG_RANGE 1 // 启用长距离传输协议更慢更稳但更耗内存DISPLAY_OUTPUT启用后包含Adafruit_GFX/Adafruit_SSD1306库并初始化 128x32 屏无屏场景下靠串口监视器即可观察解码结果草图的注释明确说明这一点LONG_RANGE启用后payloadLength降为 8 字节、RX 增加DT_FAST与MT_FAST两个较慢协议。原因在草图注释中给出——FAST 系列协议解码所需内存约为 Fastest 的 2 倍因此必须缩小载荷以腾出内存getECCBytesForLength(len)的实现为len 4 ? 2 : max(4, 2*(len/5))见 src/ggwave.cpp载荷变短时 ECC 开销也随之减少。更慢的协议意味着每个音调持续更长时间、能量更集中抗噪与抗衰减能力更强适合室内远距离或嘈杂环境。七、与 Waver 应用联调测试草图注释提供了现成的快速验证方案arduino-rx.ino使用免费的Waverggwave 官方演示应用仓库内示例见 examples/waver从手机端发送数据在 Waver 设置中开启Fixed-length固定长度选项把固定字节数设置为与草图payloadLength一致默认 16LONG_RANGE 模式为 8选择当前草图 RX 已启用的协议默认DT_FASTEST/MT_FASTESTLONG_RANGE 时还包括DT_FAST/MT_FAST。联调时注意若在 arduino-tx 中对freqStart做过偏移接收端也必须做相同偏移保证收发协议参数一致。原示例的演示视频展示了手机发、板子收、OLED 显示、再按键回传的完整链路读者可据此复现。八、底层原理印证从源码看 FSK 与纠错整个示例背后是 ggwave 的 FSK 调制方案详见 README.md 的 Technical details调制数据先按 4-bit 切块每个时刻用 6 个音调承载 3 字节每音调对应一个 4-bit chunk96 个等间隔频率分布在 4.5 kHz 范围内间隔dF 46.875 Hz非超声协议基频F0 1875 Hz——这正好解释了本示例把工作采样率设为 6000 Hz、帧长 128 的原因6000/128 46.875 Hz/bin纠错原始数据先经 Reed-Solomon 编码源码位于 src/reed-solomonECC 字节数由载荷长度按2*(len/5)规则决定接收端对解调出的比特做 RS 解码还原解码流程变长模式下接收端先侦听起始/结束声音标记kDefaultMarkerFrames 16再对中间音频做 FFT 提取频率、映射回比特而固定长度模式payloadLength 0跳过标记、直接跨帧累积频谱历史解码decode_fixed()见 ggwave.h这也是为什么固定长度模式更快、也更适合微控制器的原因。九、扩展参考纯发送端示例 arduino-tx基于 Arduino Uno 蜂鸣器展示了相同的send_text思路与freqStart偏移调优是理解本示例回传逻辑的对照物同类的 rp2040-rx基于 Raspberry Pi PicoRP2040采用不同的模拟麦克风方案见其mic-analog.cpp可作为了解多种采集路径的参考esp32-rxESP32 上的收发示例适合需要无线与声音混合通信的场景。十、常见问题速查现象可能原因与对策串口打印ERROR与错误码PDM 回调来不及消费err10或qmax非块大小整数倍err1。增大qmax、减少启用的协议数Warning: decode() took too longdecode()超过一帧时长约 21 ms。只保留必要协议或改用samplesPerFrame更小的配置收不到数据确认 Waver 开了 Fixed-length 且字节数等于payloadLength确认协议与草图 RX 启用的协议一致检查 OLED 地址0x3C/0x3D蜂鸣器回传对端收不到蜂鸣器频响不佳时按freqStart 48上移基频并确保接收端同步偏移发送前确认已PDM.end()暂停采集内存不足启用GGWAVE_CONFIG_FEW_PROTOCOLS裁剪协议或缩小payloadLengthLONG_RANGE 即此思路赞分享通信物联网嵌入式【免费下载链接】ggwaveTiny>项目地址https://gitcode.com/GitHub_Trending/gg/ggwave点击查看免费下载相关推荐ggwave 数据声波传输实战Arduino Uno 蜂鸣器发射端 arduino-tx 示例深度解析ggwave 数据声波传输实战Arduino Uno 蜂鸣器发射端 arduino tx 示例深度解析 ggwave 是一款数据通过声音传输data o通信物联网嵌入式ggwave arduino-rx-web用 Web 页面实时接收 Arduino 蜂鸣器声音数据的完整实战ggwave arduino rx web用 Web 页面实时接收 Arduino 蜂鸣器声音数据的完整实战 导读 arduino rx web 是 ggwa通信物联网嵌入式Shopify 渠道 Webhook 实战用 Flue 将订单安全分发给本地 Agent 实例Shopify 渠道 Webhook 实战用 Flue 将订单安全分发给本地 Agent 实例 导读 本文以 Flue 仓库中的 examples/shopi通信物联网嵌入式上一篇PathOfBuilding CalcDefence模块解析EHP与生存模拟下一篇Qwen3大语言模型重磅发布双模式切换引领下一代AI交互体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表