AI 语音与工具 / 音频实践发布 内容更新

ESP32-S3录一段WAV:把真实I2S采集变成可检查的文件

固定ICS43434与SD,完整采集、24/32/16位转换、WAV提交和复制修复;用文件验证定位静音/饱和,不以合成音冒充实录。

读完能做什么
得到完整可构建工程、明确的音频数据格式和可复现文件校验/故障恢复方法,保留真实物理结果待填。
开始之前
能通过桥接USB烧录Arduino3.x程序,断电追线并核对板版本;先完成所列前置。
所需器材
官方S3 DevKitC1 v1.1 N8R8、桥USB数据线、短线/按钮与10k上拉、Adafruit PID254(排针已可靠焊接)、4–32GB FAT32 SDHC与读卡器、Adafruit PID6049 ICS43434(排针已可靠焊接;限具体模块/停产存量),SEL接GND
版本组合
Arduino core / CLI:3.3.12 / 1.5.1随包IDF / I2S:5.5.5 / driver/i2s_std.h新channel APISPI / SD / FS:均随core3.3.12,无第三方库桌面文件工具:Python3.12.13标准库;clang C++17 ASan/UBSan麦克风资料:ICS43434 DS-000069 v1.2 / PID6049 PCB cde79dde…
适用范围

仅官方S3 DevKitC1 v1.1 / WROOM-1-N8R8;Arduino3.3.12/CLI1.5.1随包IDF5.5.5标准I2S;PID6049 ICS43434 + PID254。芯片停产范围明确,未实录。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/audio-io-sources.json及audio-io.md:固定源/实际编译SDK比较与厂商具体章节。版本:Arduino3.3.12 / IDF5.5.5验证日期:
构建 / 配置已通过
docs/esp32-evidence/audio-io-builds.json:真实最终构建、生成CPP/配置/镜像/日志SHA;旧失败与恢复另保留。对象:esp32s3 / v1.1 N8R8版本:Arduino3.3.12 / CLI1.5.1验证日期:
仿真已通过
docs/esp32-evidence/audio-io-host.json:同源符号/slot/DSP/格式/按键模型与真实文件校验、预期拒绝和复制修复;非声学/电气仿真。对象:same-source C++ ASan/UBSan + real synthetic WAV files验证日期:
本站实板待验证
无指定实板/麦克风/扬声器/示波器或声压计运行,observations.csv仅表头;未录取/上传/播放现实音频。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 音频实践

本页先完成什么

这个工程只从I2S RX取得数据,不在固件里生成测试音。输出为32,000Hz、单声道、16位有符号小端PCM RIFF/WAVE,320000样本/640000数据字节/640044总字节。10秒按采样帧计,是标称时钟下的时长;按键到完成还包括250ms启动丢弃、SD写入和同步,没有实测时钟/声学结果。

参考:[2] Arduino-ESP32 3.3.12 release:底层IDF5.5.5[3] IDF5.5.5 I2S STD:Philips slot与clock宏[4] IDF5.5.5 I2S channel:DMA/溢出/读写回调签名[5] IDF5.5.5 I2S指南:传输、slot排列与状态机[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化

02 / 音频实践

确切硬件与范围

转到A02时先断电、移除本页麦克风和GPIO7录音按钮,再按其独立接线图重接。

不接外部功放,不把GPIO35–37、板载flash/PSRAM脚或USB19/20混进接线。麦克风和S3同一3.3V电源;禁止麦克风断电时继续向它送BCLK/WS。

  • 官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,一根桥接USB数据线。
  • Adafruit PID6049 ICS-43434,排针先可靠焊接;麦克风底部声孔留空。厂商页面明确芯片已停产,本例限定这个具体模块/存量器材,不把SPH0645、INMP441、PDM或模拟麦克风冒称同样可套。购置前核实型号与供货,本页不承诺未来供货。
  • Adafruit PID254 microSD breakout,排针先焊接,FAT32的4–32GB SDHC卡和电脑读卡器。板有稳压/电平缓冲,不能把裸卡或任意5V电阻分压卡板当同型号。
  • 常开按钮一只、10kΩ上拉一只、短杜邦线;所有信号3.3V逻辑。断开USB再接线/换卡,不热插录音卡。
DevKitC-1 v1.1连接电平/说明
J1-1 3V3PID6049 3V麦克风供电3.3V,绝不5V
J1-22 GND麦克风GND/SEL、SD GND、按键地SEL接地选左;PID6049自身已含数据下拉/去耦
J1-5 GPIO5麦克风BCLKS3主机输出2.048MHz标称时钟
J1-4 GPIO4麦克风LRCLK/WS32kHz;LOW左,标准I2S延迟1bit
J1-6 GPIO6麦克风DOUT麦克风输出→S3输入
J1-7 GPIO7按钮→GND,同时10k→3V3启动按住必须先释放50ms
J1-21 5VPID254 5V只由本板桥USB供电;模块3V输出不接
J1-18 GPIO12PID254 CLKSPI SCK
J1-17 GPIO11PID254 DI主机MOSI
J1-19 GPIO13PID254 DO主机MISO,3.3V
J1-16 GPIO10PID254 CS片选;CD不接,程序不做在线拔卡检测

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[8] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动[9] PID6049固定PCB:3V、SEL与47k数据下拉[10] PID6049产品页:具体型号与停产边界[11] PID254固定PCB:5V输入、稳压和SPI端点

03 / 音频实践

线上24位、DMA32位与文件16位不是同一件事

ICS-43434 DS-000069 v1.2规定24bit二补码、MSB-first、WS边沿后延迟一BCLK,每个左右帧必须64 SCK。32kHz处于23–51.6kHz高性能采样区间。芯片在SCK下降沿后给出SD数据,主机默认上升沿采样、不翻转BCLK/WS。固件用32bit×2slot stereo Philips RX,BCLK=32000×64=2.048MHz;不把“单声道文件”误配置成只有32个BCLK的线上帧。

DMA交错left32,right32;SEL=GND时只取raw[2*n]的bits31..8为24位有符号样本,右slot未使用。mic24()显式符号扩展。CaptureDSP默认额外去DC:Q16 EMA的alpha=1/1024(32k标称下约5Hz拐点),然后unity gainQ8=256,除256降到PCM16并饱和/计clip。它会改变极低频,不是校准过的AFE/声压计;24bit容器也不等于24bit有效声学分辨率。

手册启动<20ms;程序开始送时钟后实际读走并丢弃8000帧,标称250ms,不靠delay留下旧DMA数据。12×256帧DMA标称约96ms容量,不保证SD一定这么快;接收队列溢出回调仅作计数,主循环发现后拒绝完成文件。无DMA溢出也不证明实际时钟/麦克风一定正确。

参考:[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化[12] Microsoft RIFF:chunk大小与WORD padding[13] Microsoft WAVEFORMATEX:声道、位宽与blockAlign

04 / 音频实践

运行与读取文件

构建/烧录见下文。先让按键松开至少50ms,确认SD=READY,再按一次;持续按住只录一次。等WAV_COMMITTED再断电取卡。自动选择A01_000..999.wav,同时避开同编号.part;不覆盖旧文件,不自动格式化卡。编号耗尽或卡故障会CREATE_ABORT。

卡复制到电脑后先保存原副本,再运行:

确认rate32000/channel1/frames320000/data_bytes640000、RIFF总长一致,查看mean、peak、RMS、near_rail_samples。peak24来自固件转换前,clips是数字转换饱和数;RMS/零交叉都不是dB SPL。用python3 tools/waveform.py /path/to/A01_000.wav first100ms.svg导出前100ms固定纵轴波形,再用本地浏览器查看;它只画文件样本,不推断来源。选择本地安静/说话或轻敲不同条件,各录一次并记空表;只有自己实际采得的文件才可标“麦克风实录”。取得许可后可在电脑本地播放器低音量打开副本、用波形工具看时间轴/直流/平顶;本交付没有录取、上传或播放任何现实音频,也没有以合成音代替该步骤。

代码 / 配置
python3 tools/audio_tool.py inspect /path/to/A01_000.wav
/tmp/audioio-check /path/to/A01_000.wav

参考:[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化[12] Microsoft RIFF:chunk大小与WORD padding[13] Microsoft WAVEFORMATEX:声道、位宽与blockAlign

05 / 音频实践

完整固件与同源处理代码

以下代码与下载目录逐字一致;工具、同源主机检查、依赖锁、许可和空观察表都在同一个下载包。标准I2S新channel API来自Arduino core随包IDF,不混装legacy驱动。固件没有通过串口/网络上传录音,也没有后台自动播放。

代码 / 配置
// ---- esp32-i2s-mic-wav.ino ----
// A01: real microphone only. No synthetic PCM path exists in this firmware.
#include <Arduino.h>
#include <SPI.h>
#include <SD.h>
#include <driver/i2s_std.h>
#include <esp_timer.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include "audio_core.h"
using namespace audioio;
constexpr int Bclk=5, Ws=4, Din=6, Button=7, Sck=12, Miso=13, Mosi=11, Cs=10;
static i2s_chan_handle_t rx=nullptr;
static volatile uint32_t overflows=0;
static bool mounted=false;
static Press recordPress;
static uint32_t raw[512]; // 256 stereo frames, two32bit slots per frame.
static uint8_t pcm[512];
static bool IRAM_ATTR overflow(i2s_chan_handle_t,i2s_event_data_t*,void*){
 __atomic_fetch_add(&overflows,1u,__ATOMIC_RELAXED);return false;
}
static uint32_t lost(){return __atomic_load_n(&overflows,__ATOMIC_RELAXED);}
static bool startRX(){
 i2s_chan_config_t c=I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO,I2S_ROLE_MASTER);
 c.dma_desc_num=12;c.dma_frame_num=256;
 if(i2s_new_channel(&c,nullptr,&rx)!=ESP_OK)return false;
 i2s_std_config_t s={};s.clk_cfg=I2S_STD_CLK_DEFAULT_CONFIG(Rate);
 s.slot_cfg=I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT,I2S_SLOT_MODE_STEREO);
 s.gpio_cfg.mclk=I2S_GPIO_UNUSED;s.gpio_cfg.bclk=gpio_num_t(Bclk);s.gpio_cfg.ws=gpio_num_t(Ws);s.gpio_cfg.dout=I2S_GPIO_UNUSED;s.gpio_cfg.din=gpio_num_t(Din);
 if(i2s_channel_init_std_mode(rx,&s)!=ESP_OK)return false;
 i2s_event_callbacks_t cb={};cb.on_recv_q_ovf=overflow;
 if(i2s_channel_register_event_callback(rx,&cb,nullptr)!=ESP_OK)return false;
 __atomic_store_n(&overflows,0u,__ATOMIC_RELAXED);
 return i2s_channel_enable(rx)==ESP_OK;
}
static void stopRX(){
 if(!rx)return;
 // disable can fail if init never reached RUNNING; delete still releases a READY channel.
 i2s_channel_disable(rx);i2s_del_channel(rx);rx=nullptr;
 pinMode(Bclk,OUTPUT);digitalWrite(Bclk,LOW);pinMode(Ws,OUTPUT);digitalWrite(Ws,LOW);
}
static bool readFrames(size_t frames){
 size_t got=0,want=frames*8;
 esp_err_t e=i2s_channel_read(rx,raw,want,&got,500);
 if(e!=ESP_OK||got!=want||lost()){
  Serial.printf("RX_ABORT err=%s got=%u want=%u overflows=%lu\n",esp_err_to_name(e),unsigned(got),unsigned(want),(unsigned long)lost());return false;
 }
 return true;
}
static bool absent(const char* name){struct stat s;if(stat(name,&s)==0)return false;return errno==ENOENT;}
static FILE* newFile(char* part,char* good){
 for(unsigned n=0;n<1000;n++){
  snprintf(part,64,"/sd/A01_%03u.part",n);snprintf(good,64,"/sd/A01_%03u.wav",n);
  if(!absent(good)||!absent(part))continue;
  int fd=open(part,O_CREAT|O_EXCL|O_RDWR,0600);if(fd<0)return nullptr;
  FILE* f=fdopen(fd,"w+b");if(!f){close(fd);return nullptr;}return f;
 }
 return nullptr;
}
static void recordWav(){
 char part[64],good[64];FILE* f=newFile(part,good);
 if(!f){Serial.printf("CREATE_ABORT errno=%d; existing files retained\n",errno);return;}
 uint8_t h[44];header(h,Rate,1,0);bool ok=fwrite(h,1,44,f)==44;
 uint32_t frames=0;CaptureDSP dsp;
 if(ok)ok=startRX();
 // Discard 8000 *received* stereo frames (250ms atnominal32k), not a delay leaving old DMA data queued.
 for(uint32_t n=0;ok&&n<Rate/4;){size_t k=min(size_t(256),size_t(Rate/4-n));ok=readFrames(k);n+=uint32_t(k);}
 while(ok&&frames<CaptureFrames){
  size_t k=min(size_t(256),size_t(CaptureFrames-frames));ok=readFrames(k);if(!ok)break;
  for(size_t n=0;n<k;n++)put16(pcm+2*n,uint16_t(dsp.sample(raw[2*n],256,true)));
  ok=fwrite(pcm,1,k*2,f)==k*2;if(ok)frames+=uint32_t(k);
 }
 // Conservative: any overflow before the completed capture is rejected, never silently compact a gap.
 if(lost())ok=false;stopRX();
 if(ok){header(h,Rate,1,frames*2);ok=fseek(f,0,SEEK_SET)==0&&fwrite(h,1,44,f)==44;}
 if(ok)ok=fflush(f)==0;
 if(ok)ok=fsync(fileno(f))==0;
 int closeResult=fclose(f);if(closeResult!=0)ok=false;
 if(ok)ok=rename(part,good)==0;
 Serial.printf("%s path=%s frames=%lu data_bytes=%lu peak24=%ld clips=%lu overflows=%lu optical_acoustic=UNTESTED\n",ok?"WAV_COMMITTED":"PART_RETAINED",ok?good:part,(unsigned long)frames,(unsigned long)(frames*2),(long)dsp.peak24,(unsigned long)dsp.clipped,(unsigned long)lost());
 // FAT sync/rename cannot promise power-fail atomicity. Inspect the copied file before listening.
}
void setup(){
 Serial.begin(115200);pinMode(Button,INPUT); // External10k to3V3; button toGND.
 SPI.begin(Sck,Miso,Mosi,Cs);mounted=SD.begin(Cs,SPI,4000000,"/sd",5,false);
 Serial.printf("A01 Arduino3.3.12 IDF%s SD=%s; release button, then press for real10s capture\n",esp_get_idf_version(),mounted?"READY":"FAILED");
}
void loop(){
 bool press=recordPress.update(digitalRead(Button)==LOW,uint64_t(esp_timer_get_time())/1000);
 if(press&&mounted)recordWav();delay(2);
}

// ---- audio_core.h ----
#pragma once
// Original MIT code. Same header is compiled on ESP32 and in the desktop fixtures.
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
namespace audioio {
constexpr uint32_t Rate=32000, CaptureFrames=Rate*10, MaxFile=8*1024*1024;
inline uint16_t le16(const uint8_t* p){return uint16_t(p[0])|(uint16_t(p[1])<<8);}
inline uint32_t le32(const uint8_t* p){return uint32_t(p[0])|(uint32_t(p[1])<<8)|(uint32_t(p[2])<<16)|(uint32_t(p[3])<<24);}
inline void put16(uint8_t* p,uint16_t n){p[0]=uint8_t(n);p[1]=uint8_t(n>>8);}
inline void put32(uint8_t* p,uint32_t n){for(unsigned i=0;i<4;i++)p[i]=uint8_t(n>>(8*i));}
inline bool rateOK(uint32_t n){return n==16000||n==32000||n==48000;}
inline void header(uint8_t* h,uint32_t rate,uint16_t channels,uint32_t bytes){
 memset(h,0,44);memcpy(h,"RIFF",4);put32(h+4,36+bytes);memcpy(h+8,"WAVEfmt ",8);put32(h+16,16);
 put16(h+20,1);put16(h+22,channels);put32(h+24,rate);put32(h+28,rate*channels*2);put16(h+32,channels*2);put16(h+34,16);memcpy(h+36,"data",4);put32(h+40,bytes);
}
struct Wav {uint32_t rate=0,offset=0,bytes=0,frames=0;uint16_t channels=0;};
// Bounded, little-endian RIFF/WAVE PCM16 subset. Unknown chunks are skipped with padding.
inline const char* parse(FILE* f,Wav& w){
 w=Wav{};if(!f||fseek(f,0,SEEK_END))return "SEEK";long end=ftell(f);if(end<44||uint64_t(end)>MaxFile)return "FILE_SIZE";
 if(fseek(f,0,SEEK_SET))return "SEEK";uint8_t h[12];if(fread(h,1,12,f)!=12)return "READ";
 if(memcmp(h,"RIFF",4)||memcmp(h+8,"WAVE",4))return "RIFF_WAVE";
 if(uint64_t(le32(h+4))+8!=uint64_t(end))return "RIFF_SIZE";
 bool fmt=false,data=false;uint32_t pos=12;
 while(pos<uint32_t(end)){
  if(uint32_t(end)-pos<8||fseek(f,long(pos),SEEK_SET))return "CHUNK_HEADER";
  uint8_t c[8];if(fread(c,1,8,f)!=8)return "READ";uint32_t n=le32(c+4);uint64_t next=uint64_t(pos)+8+n+(n&1u);
  if(next>uint64_t(end))return "CHUNK_BOUNDS";
  if(!memcmp(c,"fmt ",4)){
   if(fmt||data||n!=16)return "FMT_SUBSET";uint8_t v[16];if(fread(v,1,16,f)!=16)return "READ";
   uint16_t ch=le16(v+2),align=le16(v+12);uint32_t rate=le32(v+4);
   if(le16(v)!=1||le16(v+14)!=16||(ch!=1&&ch!=2)||!rateOK(rate)||align!=ch*2||le32(v+8)!=rate*align)return "PCM_SUBSET";
   w.rate=rate;w.channels=ch;fmt=true;
  }else if(!memcmp(c,"data",4)){
   if(!fmt||data||!n||n%(w.channels*2))return "DATA_SUBSET";
   w.offset=pos+8;w.bytes=n;w.frames=n/(w.channels*2);data=true;
   if(w.frames>15*w.rate)return "DURATION_LIMIT";
  }
  pos=uint32_t(next);
 }
 if(!fmt||!data)return "MISSING_CHUNK";if(fseek(f,long(w.offset),SEEK_SET))return "SEEK";return nullptr;
}
// A 24-bit signed microphone word occupies bits31..8 of the captured left 32-bit slot.
inline int32_t mic24(uint32_t slot){uint32_t n=slot>>8;return n&0x800000u?int32_t(n)-16777216:int32_t(n);}
struct CaptureDSP {
 int64_t dcQ16=0;uint32_t clipped=0;int32_t peak24=0;
 // DC EMA alpha=1/1024 at32kHz (~5Hz corner); gainQ8=256 means unity.
 int16_t sample(uint32_t slot,uint16_t gainQ8=256,bool removeDC=true){
  int32_t x=mic24(slot);int32_t a=x<0?-x:x;if(a>peak24)peak24=a;
  if(removeDC){dcQ16+=(int64_t(x)*65536-dcQ16)/1024;x-=int32_t(dcQ16/65536);}
  int64_t y=int64_t(x)*gainQ8/65536;
  if(y>32767){y=32767;clipped++;}if(y< -32768){y=-32768;clipped++;}return int16_t(y);
 }
};
// Fixed -24.08dB attenuation, then hard limit. This is not acoustic-volume calibration.
inline int16_t output(int16_t sample){int32_t n=int32_t(sample)/16;if(n>2048)n=2048;if(n< -2048)n=-2048;return int16_t(n);}
inline int16_t route(int16_t left,int16_t right,unsigned mode){return mode==0?left:mode==1?right:int16_t((int32_t(left)+right)/2);}
struct Press {
 bool raw=false,stable=false,released=false,initialized=false;uint64_t at=0;
 bool update(bool down,uint64_t now){
  if(!initialized){initialized=true;raw=stable=down;at=now;return false;}
  if(down!=raw){raw=down;at=now;}
  if(now-at<50)return false;
  if(!raw)released=true;
  if(raw==stable)return false;stable=raw;
  if(stable&&released){released=false;return true;}return false;
 }
};
}

参考:[2] Arduino-ESP32 3.3.12 release:底层IDF5.5.5[3] IDF5.5.5 I2S STD:Philips slot与clock宏[4] IDF5.5.5 I2S channel:DMA/溢出/读写回调签名[5] IDF5.5.5 I2S指南:传输、slot排列与状态机[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化[7] IDF5.5.5 I2S实际读写:等待参数非总deadline

06 / 音频实践

文件完成、断电与恢复

先以O_EXCL创建.part、写44B零数据长头,再逐帧写PCM;成功后回填RIFF/data长度,检查fseek/fwrite/fflush/fsync/fclose,最后rename成.wav。写入短、读超时、溢出或同步失败都保留.part。FAT的fsync/rename不能承诺断电原子性,卡控制器缓存、目录和整卡仍可受损,日志不是落盘仪表证明。

读卡器上备份原卡后,工具只支持本工程44B规范头/单声道32k PCM16的PART,不会猜任意损坏文件,也不修改源:

它把实际可读取的完整2B样本前缀复制到新文件,回填长度、丢弃最多1个尾字节;目标必须不存在。修复只能恢复可读前缀,不证明没有漏帧/卡损坏。先备份后查卡/重格式化,再复位重新录;不要把PART直接改扩展名。

代码 / 配置
python3 tools/audio_tool.py salvage /copy/A01_000.part /copy/recovered-copy.wav
python3 tools/audio_tool.py inspect /copy/recovered-copy.wav
/tmp/audioio-check /copy/recovered-copy.wav

参考:[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化[12] Microsoft RIFF:chunk大小与WORD padding[13] Microsoft WAVEFORMATEX:声道、位宽与blockAlign

07 / 音频实践

故障输入与空观察

observations.csv及独立音频/声学、electrical-observations.csv均仅表头。应记录供电/板与模块批次、时钟测量、安静/声源条件、文件SHA/长度、clip/overflow、断电前后目录与可读性;本包没有实板结果。

输入/现象可执行定位和恢复
SD FAILED/CREATE_ABORT断电查PID254供电/CS/DI/DO、FAT32卡与空间;电脑备份后修复卡,重新上电;不启用自动格式化
RX_ABORT/overflows>0保留PART;断电换合格卡/短线、检查供电和SPI,重录。不要把短录补零称连续10秒
WAV合法但全零/极小I2S无器件身份ACK;拔掉DOUT也可能是合法静音。断电核3V、SEL左、DOUT6与声孔,仪表查WS/BCLK/SD;先保留gain=1
平顶/clips增加降声源强度/拉开距离,核位移和电平;不要先提高增益掩盖slot错误
文件速度/音高异常对照头rate与实际WS/BCLK;文件头正确不等于晶振实际正确;不要改头“修”未知时钟
启动按住无动作这是防意外录音;松开50ms再按,EN重启也遵循同规则

参考:[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化[12] Microsoft RIFF:chunk大小与WORD padding[13] Microsoft WAVEFORMATEX:声道、位宽与blockAlign

08 / 音频实践

锁定、构建与烧录

本包为Arduino工程:CLI 1.5.1、esp32:esp32 3.3.12,底层ESP-IDF 5.5.5。使用随core提供的driver/i2s_std.h新channel API;不是旧driver/i2s.h,不需另装原生IDF工程或第三方音频库。SPI/SD也随core。sketch.yaml没有default_profile;versions.json给出本次实际FQBN。复用已安装core,用显式FQBN编译不会因默认profile另装重复SDK。

PORT必须通过插拔确认USB-to-UART桥口,烧录前关闭旧串口监视器。用桥接USB/UART0做烧录和115200日志;不接原生USB19/20。USB Mode/CDC参数与实际构建一致。若上传失败,检查线/端口/占用,按住BOOT点EN进入下载,再刷后松开BOOT、点EN;不要把旧日志当新固件。首次运行先打印工程ID/版本/SD状态。不要给N16R8或别家“ESP32-S3”直接套该FQBN/针脚。

代码 / 配置
arduino-cli version
arduino-cli core list
FQBN='esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc'
arduino-cli compile --fqbn "$FQBN" --build-path build --warnings all .
arduino-cli upload --fqbn "$FQBN" --input-dir build --port PORT .
arduino-cli monitor --port PORT --config baudrate=115200

参考:[1] S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB[2] Arduino-ESP32 3.3.12 release:底层IDF5.5.5[14] Arduino ctags5.8-arduino11精确源:本次macOS原生构建

09 / 音频实践

桌面验证(不录音、不播放、不联网上传)

Python实际验证版本3.12.13,仅标准库;C++测试为clang C++17的ASan/UBSan。命令在本项目根执行。tests/host.cpp编译同一个生产audio_core.h:24位符号/缩放/DC/限幅、按键、RIFF边界和格式拒绝;不模拟射频、电气或声音。Python真实写出合成WAV,以标准库wave和生产解析器分别读取,检查440/880Hz零交叉、预期格式失败、复制修复与拒绝覆盖。合成样本不是麦克风实录。

生成器用xb创建,不覆盖既有文件;目标已存在时换空目录,不要为通过命令删除真实录音。WAV可证明格式、样本和字节数,不能自动推断来自哪个传感器。实板/录音/声压/光学/仪表数据都没有执行,observations.csv只留表头,须由读者按自己的设备填写。

代码 / 配置
clang++ -std=c++17 -Wall -Wextra -Werror -fsanitize=address,undefined -fno-omit-frame-pointer tests/host.cpp -o /tmp/audioio-check
/tmp/audioio-check
python3 tools/audio_tool.py self-test --checker /tmp/audioio-check
python3 tools/audio_tool.py tone SYNTH.WAV --rate 32000
python3 tools/audio_tool.py inspect SYNTH.WAV
python3 tools/waveform.py SYNTH.WAV synthetic-first100ms.svg
/tmp/audioio-check SYNTH.WAV

参考:[2] Arduino-ESP32 3.3.12 release:底层IDF5.5.5[3] IDF5.5.5 I2S STD:Philips slot与clock宏[4] IDF5.5.5 I2S channel:DMA/溢出/读写回调签名[5] IDF5.5.5 I2S指南:传输、slot排列与状态机[6] Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化

10 / 音频实践

本次macOS ARM构建工具例外(仅遇到同样ctags启动阻塞时)

作者机的官方Intel ctags5.8-arduino11停在启动阶段,未进入项目编译。没有替换系统ctags或全局工具,而是用同一精确tag/commit在隔离目录构建arm64版本;其--version自报Exuberant Ctags Development,精确版本以commit为准。当前macOS SDK的dirent.h与旧代码__unused__宏冲突,强制预包含系统头即可,未修改上游parser源。正常机器无需此步骤。ctags为独立GPL-2.0构建工具,不链接进MIT固件。

在本次verbose原日志里核实际执行的隔离ctags路径,再核当前源/生成CPP/最终固件SHA。首次阻塞、第一次本地ctags编译宏冲突、I2S回调签名编译失败和最终恢复日志都保留在作者evidence;这些失败未被当作固件成功或实板通过。

代码 / 配置
git clone https://github.com/arduino/ctags.git .ctags-src
git -C .ctags-src checkout abc8fca7499f44c725122881cd380a88c37abe0e
cd .ctags-src
./configure CC=clang CFLAGS='-O2 -arch arm64' --prefix="$PWD/../.ctags-native"
make -j2 CFLAGS='-O2 -arch arm64 -include dirent.h'
make install
cd ..
.ctags-native/bin/ctags --version
arduino-cli compile --fqbn "$FQBN" --build-path build --verbose --build-property "runtime.tools.ctags.path=$PWD/.ctags-native/bin" --build-property "runtime.tools.ctags-5.8-arduino11.path=$PWD/.ctags-native/bin" .

参考:[3] IDF5.5.5 I2S STD:Philips slot与clock宏[4] IDF5.5.5 I2S channel:DMA/溢出/读写回调签名[5] IDF5.5.5 I2S指南:传输、slot排列与状态机[7] IDF5.5.5 I2S实际读写:等待参数非总deadline[8] ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动

11 / 音频实践

把软件证据与声音观察分开记录

本交付的真实Arduino编译、源/生成CPP/配置/固件SHA、同源ASan/UBSan与真实文件失败恢复记录见audio-io-builds.json、audio-io-host.json和audio-io.md。合成测试文件只验证工具/格式/算法,不能作为麦克风录音、扬声器出声或电源功率证据。

编译证据必须对应当前源和工具。若macOS的旧Intel ctags启动阻塞,README给出同一精确tag的原生构建及本次CLI属性覆盖步骤,不用系统不同实现的ctags冒充同一版本。原始失败与恢复记录单独归档,最终只采用与当前源对应的成功产物。

没有连接指定实板、麦克风、功放或任何声学/电气仪表。运行步骤与空observations.csv用于下一步实际验收,不能填入估算的测量值。

做到这里,你应该看到
文件格式、样本统计、软件日志和物理观察分别有来源;没有实测的字段保持空白。

参考:[2] Arduino-ESP32 3.3.12 release:底层IDF5.5.5[4] IDF5.5.5 I2S channel:DMA/溢出/读写回调签名[14] Arduino ctags5.8-arduino11精确源:本次macOS原生构建

BACK TO THE SOURCE

资料来源与版本说明

固定core/SDK的I2S与SD实现、指定PCB和版本PDF已实际阅读核对;主机/编译证据不替代麦克风、扬声器、电源或仪表实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. S3 DevKitC1 v1.1固定文档:J1/J3、电源与USB
  2. Arduino-ESP32 3.3.12 release:底层IDF5.5.5
  3. IDF5.5.5 I2S STD:Philips slot与clock宏
  4. IDF5.5.5 I2S channel:DMA/溢出/读写回调签名
  5. IDF5.5.5 I2S指南:传输、slot排列与状态机
  6. Arduino3.3.12 SD.cpp:mountpoint与禁止自动格式化
  7. IDF5.5.5 I2S实际读写:等待参数非总deadline
  8. ICS43434 DS-000069 v1.2:电气/边沿/24bit/启动
  9. PID6049固定PCB:3V、SEL与47k数据下拉
  10. PID6049产品页:具体型号与停产边界
  11. PID254固定PCB:5V输入、稳压和SPI端点
  12. Microsoft RIFF:chunk大小与WORD padding
  13. Microsoft WAVEFORMATEX:声道、位宽与blockAlign
  14. Arduino ctags5.8-arduino11精确源:本次macOS原生构建