先分清本页的产物与边界
本例把板上真实ES7210 MIC1采样送入ESP-SR WebRTC NS/VAD,按串口q/n分别触发安静/背景噪声场景的10秒RAW与AFE单声道WAV、逐fetch VAD CSV。固件没有合成音源;本次未接板,没有产出真实录音。桌面合成样本只测试文件工具,不能填入安静/噪声观察表。
采音、VAD“像说话”、唤醒词和命令识别是不同结果。本项目不开WakeNet/MultiNet,没有文字识别、云端理解或回声消除。与既有小智排错文章不同,这里提供独立可构建的原生IDF采音/AFE/保存工程,不改变小智固件的板配置。
参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义
安静/噪声两组操作与文件提交
先备份卡、关电插入4–32GB FAT32 SDHC;程序挂载失败不会自动格式化。核供电/串口后等待pipeline打印。保持板和说话者位置不变,串口发q再回车,在安静环境按预先写下的同一段话说10秒;等待FILES_SW_COMMITTED。第二轮只增加位置固定的背景噪声源,发n再回车并重复同一文本。记录距离/方向/噪声类型和人为估计与仪表读数的区别;本次两组都没有执行,不提供伪录音。
每轮从命令之后分别缓存160000个RAW MIC1样本和160000个AFE fetch输出,放在PSRAM,采满后才写卡。两条流都有自己的首样本软件时刻,AFE还有内部队列/处理延迟,因此两个WAV不是逐样本时间对齐,也不是同一时刻的“无损前后”。比较时只选共同稳定发声区间;若要SNR/延迟数值,须另做明确对齐和参考测试,不能直接相减这两文件。
文件名如A03_q_000_RAW.WAV、A03_q_000_AFE.WAV、A03_q_000_EVENTS.csv;q/n为场景标签,不是测量结论。只选三种目标/part均不存在的新编号000–999,用O_EXCL创建part。两个WAV为16kHz/PCM16/mono/10秒,每个320044字节。CSV记录输出样本位置、样本数、software_fetch_us、VAD状态和库的数字音量字段;该volume不是dB SPL,时间戳不是ADC采样硬件时间。
WAV按完整RAM数据写头/PCM,检查fwrite、fflush、fsync、fclose,再rename;CSV同样检查。每个文件独立提交,三文件不是一个事务:RAW成功后AFE/CSV失败仍会留下已完成RAW和part,不能把不完整三件套标为整轮成功。FAT/卡缓存也没有断电原子性保证。故障后保存现状、复制读取检查;不覆盖part或删除旧录音来“恢复成功”。运行采满超20秒/读错则锁存,不把缺失样本补零后保存为成功。
参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现
检查真实文件,不替它们编造来源
在读者自己完成现场录音后,关电取卡,用本地工具分别检查两WAV,再由操作者手动用本机播放器回放同一稳定语句区间,记录噪声/语音失真与VAD漏/误触发。本文作者没有播放现实音频;工具不上传、不自动播放。文件合法不能证明录音来自指定MIC,也不能证明降噪变好。
check_recording.py从当前main.c原样抽取le16/le32和完整wav_commit函数,与明确stdio故障包装一起编译运行,验证正常提交、短写、fsync失败、fclose失败和已有PART拒覆盖。用Python wave/struct读真正写出的合成320044B WAV;此夹具没有执行AFE推理、SD硬件或麦克风。源/实际体/产物SHA和原日志用于定位软件故障。
python3 tools/inspect_wav.py A03_q_000_RAW.WAV
python3 tools/inspect_wav.py A03_q_000_AFE.WAV
python3 tools/check_recording.py --output /tmp/a03-commit-check
python3 tools/inspect_models.py build/srmodels/srmodels.bin --expect '' --model-root managed_components/espressif__esp-sr/model参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现
完整应用、板级音频与同源状态代码
以下四个文件与下载目录逐字一致。项目另含正常Registry依赖锁、构建配置、测试工具、许可和空观察表。没有隐藏平台服务、自动上传音频或桌面合成音冒充传感器的分支。
// ---- main/main.c ----
// Copyright2026 hixinwu. SPDX-License-Identifier: MIT
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_afe_sr_iface.h"
#include "esp_afe_sr_models.h"
#include "esp_mn_models.h"
#include "esp_mn_speech_commands.h"
#include "board_audio.h"
#include "speech_state.h"
#define PROJECT_ID "A03"
#define COMMANDS 0
static const char *TAG=PROJECT_ID;
static const esp_afe_sr_iface_t *afe;
static esp_afe_sr_data_t *ad;
static atomic_bool failed;
static int feed_n;
static uint64_t now_us(void){return (uint64_t)esp_timer_get_time();}
static void fail(const char *why){atomic_store(&failed,true);ESP_LOGE(TAG,"FAULT %s; no inferred hardware result; RESET after repair",why);}
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#define SAMPLES 160000u
#define MAX_EVENTS 2000u
static int16_t *raw_capture,*out_capture;
typedef struct{uint32_t sample;uint32_t samples;uint64_t software_us;int vad;float volume;} event_t;
static event_t *events;
static atomic_bool capturing;
static atomic_uint generation,raw_done,out_done,event_done;
static atomic_ullong raw_first_us,out_first_us;
static uint32_t raw_pos,out_pos,event_pos,raw_gen,out_gen;
static void raw_save(const int16_t *data,size_t n){
if(atomic_load(&failed)||!atomic_load(&capturing)){return;}
unsigned gen=atomic_load(&generation);if(raw_gen!=gen){raw_gen=gen;raw_pos=0;}
if(raw_pos==SAMPLES){return;}
if(raw_pos==0)atomic_store(&raw_first_us,now_us());
if(n>SAMPLES-raw_pos){n=SAMPLES-raw_pos;}
memcpy(raw_capture+raw_pos,data,n*2);raw_pos+=n;if(!atomic_load(&failed))atomic_store(&raw_done,raw_pos);
}
static void processed_save(const afe_fetch_result_t *res,uint64_t t){
if(atomic_load(&failed)||!atomic_load(&capturing)){return;}
unsigned gen=atomic_load(&generation);if(out_gen!=gen){out_gen=gen;out_pos=0;event_pos=0;}
if(out_pos==SAMPLES){return;}
if(out_pos==0)atomic_store(&out_first_us,t);
size_t n=res->data_size/2;if(n>SAMPLES-out_pos)n=SAMPLES-out_pos;
if(event_pos==MAX_EVENTS){fail("event capture capacity");return;}
events[event_pos++]=(event_t){.sample=out_pos,.samples=n,.software_us=t,.vad=res->vad_state,.volume=res->data_volume};
memcpy(out_capture+out_pos,res->data,n*2);out_pos+=n;if(!atomic_load(&failed)){atomic_store(&event_done,event_pos);atomic_store(&out_done,out_pos);}
}
static void le16(uint8_t*p,uint16_t n){p[0]=n;p[1]=n>>8;}
static void le32(uint8_t*p,uint32_t n){le16(p,n);le16(p+2,n>>16);}
static bool wav_commit(const char *part,const char *done,const int16_t *data){
int fd=open(part,O_CREAT|O_EXCL|O_WRONLY,0600);if(fd<0)return false;FILE*f=fdopen(fd,"wb");if(!f){close(fd);return false;}
uint8_t h[44]={0};memcpy(h,"RIFF",4);le32(h+4,36+SAMPLES*2);memcpy(h+8,"WAVEfmt ",8);le32(h+16,16);le16(h+20,1);le16(h+22,1);le32(h+24,16000);le32(h+28,32000);le16(h+32,2);le16(h+34,16);memcpy(h+36,"data",4);le32(h+40,SAMPLES*2);
bool ok=fwrite(h,1,44,f)==44&&fwrite(data,2,SAMPLES,f)==SAMPLES;
if(ok){ok=fflush(f)==0;}
if(ok){ok=fsync(fileno(f))==0;}
if(fclose(f)!=0){ok=false;}
if(ok){ok=rename(part,done)==0;}
return ok;
}
static bool file_absent(const char*p){struct stat st;if(stat(p,&st)==0)return false;return errno==ENOENT;}
static bool save_record(char condition){
if(atomic_load(&failed)){return false;}
char rp[64],rw[64],op[64],ow[64],cp[64],cw[64];int seq;
for(seq=0;seq<1000;seq++){snprintf(rp,sizeof(rp),"/sd/A03_%c_%03d_RAW.part",condition,seq);snprintf(rw,sizeof(rw),"/sd/A03_%c_%03d_RAW.WAV",condition,seq);snprintf(op,sizeof(op),"/sd/A03_%c_%03d_AFE.part",condition,seq);snprintf(ow,sizeof(ow),"/sd/A03_%c_%03d_AFE.WAV",condition,seq);snprintf(cp,sizeof(cp),"/sd/A03_%c_%03d_EVENTS.part",condition,seq);snprintf(cw,sizeof(cw),"/sd/A03_%c_%03d_EVENTS.csv",condition,seq);if(file_absent(rp)&&file_absent(rw)&&file_absent(op)&&file_absent(ow)&&file_absent(cp)&&file_absent(cw))break;}
if(seq==1000)return false;
// Each file commits independently. This is NOT a transaction across the three files.
if(atomic_load(&failed)||!wav_commit(rp,rw,raw_capture)){return false;}
if(atomic_load(&failed)||!wav_commit(op,ow,out_capture)){return false;}
if(atomic_load(&failed)){return false;} // completed earlier files are retained, not rolled back.
int fd=open(cp,O_CREAT|O_EXCL|O_WRONLY,0600);if(fd<0)return false;FILE*f=fdopen(fd,"w");if(!f){close(fd);return false;}
bool ok=fprintf(f,"output_sample,output_samples,software_fetch_us,vad_state,volume_library_db\n")>0;
for(unsigned i=0;i<atomic_load(&event_done)&&ok&&!atomic_load(&failed);i++){event_t *a=events+i;ok=fprintf(f,"%u,%u,%llu,%d,%f\n",(unsigned)a->sample,(unsigned)a->samples,(unsigned long long)a->software_us,a->vad,(double)a->volume)>0;}
if(ok){ok=fflush(f)==0;}
if(ok){ok=fsync(fileno(f))==0;}
if(fclose(f)!=0){ok=false;}
if(atomic_load(&failed)){ok=false;}
if(ok){ok=rename(cp,cw)==0;}
if(atomic_load(&failed)){ok=false;}
if(ok)ESP_LOGI(TAG,"FILES_SW_COMMITTED raw=%s afe=%s events=%s raw_first_us=%llu fetch_first_us=%llu; not aligned/acoustic proof",rw,ow,cw,atomic_load(&raw_first_us),atomic_load(&out_first_us));
return ok;
}
static void feed_task(void *arg){
(void)arg;int16_t *stereo=heap_caps_malloc(feed_n*4,MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),*mono=heap_caps_malloc(feed_n*2,MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT);
if(!stereo||!mono){fail("feed allocation");vTaskDelete(NULL);return;}
size_t discarded=0;while(discarded<8000&&!atomic_load(&failed)){if(board_audio_read(stereo,feed_n)!=ESP_OK){fail("startup read/short read");break;}discarded+=feed_n;}
// Driver runs during model creation. Discard startup samples and fence its old overflows.
const uint32_t baseline=board_audio_lost();
while(!atomic_load(&failed)){
if(board_audio_read(stereo,feed_n)!=ESP_OK||board_audio_lost()!=baseline){fail("read/short read/RX queue overflow");break;}
if(atomic_load(&failed)){break;}
for(int i=0;i<feed_n;i++)mono[i]=mic1(stereo,i);
raw_save(mono,feed_n);
if(atomic_load(&failed)){break;}
if(afe->feed(ad,mono)<0){fail("AFE feed");break;}
}
free(stereo);free(mono);vTaskDelete(NULL);
}
static void fetch_task(void *arg){
(void)arg;uint64_t last=now_us();
while(!atomic_load(&failed)){afe_fetch_result_t*res=afe->fetch_with_delay(ad,pdMS_TO_TICKS(100));uint64_t t=now_us();
if(atomic_load(&failed)){break;} // fetch may have waited while feed failed.
if(!res){if(t-last>2000000ULL)fail("AFE no fetch >2s");continue;}
if(res->ret_value!=ESP_OK||!res->data||res->data_size<=0||res->data_size%2){fail("AFE fetch/size");break;}last=t;processed_save(res,t);
}board_leds(true,false);vTaskDelete(NULL);
}
void app_main(void){
ESP_LOGI(TAG,"IDF5.5.5 / SR2.4.7 / Korvo2 MAIN V3.0 N16R8; no WiFi/cloud; PA stays OFF");
ESP_ERROR_CHECK(board_audio_init());srmodel_list_t *models=esp_srmodel_init("model");if(!models){fail("model partition init");return;}
afe_config_t *cfg=afe_config_init("M",models,AFE_TYPE_SR,AFE_MODE_LOW_COST);if(!cfg){fail("AFE config");return;}
cfg->aec_init=false;cfg->se_init=false;cfg->ns_init=true;cfg->vad_init=true;cfg->agc_init=false;cfg->wakenet_init=false;
cfg->afe_ns_mode=AFE_NS_MODE_WEBRTC;cfg->vad_model_name=NULL;cfg->afe_linear_gain=1.0f;cfg->memory_alloc_mode=AFE_MEMORY_ALLOC_MORE_PSRAM;
cfg=afe_config_check(cfg);if(cfg->aec_init||cfg->pcm_config.total_ch_num!=1||cfg->pcm_config.ref_num!=0){fail("unexpected AFE reference format");return;}
afe=esp_afe_handle_from_config(cfg);ad=afe?afe->create_from_config(cfg):NULL;afe_config_free(cfg);if(!ad){fail("AFE create");return;}
feed_n=afe->get_feed_chunksize(ad);if(feed_n<=0||feed_n>4096||afe->get_feed_channel_num(ad)!=1||afe->get_samp_rate(ad)!=16000){fail("AFE shape");return;}
afe->print_pipeline(ad);ESP_LOGI(TAG,"feed samples/chunk=%d; ES7210 MIC1/MIC2 stereo16 → first MIC only M; no R/AEC",feed_n);
ESP_ERROR_CHECK(board_sd_mount());raw_capture=heap_caps_malloc(SAMPLES*2,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);out_capture=heap_caps_malloc(SAMPLES*2,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);events=heap_caps_malloc(MAX_EVENTS*sizeof(event_t),MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);if(!raw_capture||!out_capture||!events){fail("capture PSRAM allocation");return;}fcntl(STDIN_FILENO,F_SETFL,O_NONBLOCK);
if(xTaskCreatePinnedToCore(feed_task,"mic-feed",6144,NULL,5,NULL,0)!=pdPASS||xTaskCreatePinnedToCore(fetch_task,"afe-fetch",8192,models,4,NULL,1)!=pdPASS){fail("task create");return;}
char condition=0;uint64_t began=0;ESP_LOGI(TAG,"type q then Enter for quiet, n then Enter for noise;10s each; never synthetic capture");
while(!atomic_load(&failed)){
int c=getchar();if(c==EOF)clearerr(stdin);
if(atomic_load(&failed)){break;} // no new capture/commit after an observed feed/fetch fault.
if((c=='q'||c=='n')&&!atomic_load(&capturing)){condition=c;atomic_store(&raw_done,0);atomic_store(&out_done,0);atomic_store(&event_done,0);atomic_fetch_add(&generation,1);began=now_us();atomic_store(&capturing,true);ESP_LOGI(TAG,"CAPTURE %c started; independent first-sample fences, not sample aligned",condition);}
if(atomic_load(&capturing)&&atomic_load(&raw_done)==SAMPLES&&atomic_load(&out_done)==SAMPLES){atomic_store(&capturing,false);if(atomic_load(&failed)){break;}if(!save_record(condition)){fail("file commit; keep existing .part/independent completed files; do not retry overwrite");break;}}
if(atomic_load(&capturing)&&now_us()-began>20000000ULL){atomic_store(&capturing,false);fail("capture incomplete20s");break;}vTaskDelay(pdMS_TO_TICKS(20));
}atomic_store(&capturing,false);ESP_LOGE(TAG,"latched fault; partial RAM not saved as a successful recording; RESET after repair");
}
// ---- main/board_audio.c ----
// Copyright 2026 hixinwu. SPDX-License-Identifier: MIT
#include "board_audio.h"
#include "driver/i2c_master.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "driver/sdmmc_host.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "esp_codec_dev.h"
#include "esp_codec_dev_defaults.h"
#include "esp_check.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static i2c_master_bus_handle_t bus;
static i2c_master_dev_handle_t io;
static i2s_chan_handle_t rx;
static uint32_t lost;
static const char *TAG="BOARD_V3_0";
static bool IRAM_ATTR overflow(i2s_chan_handle_t h,i2s_event_data_t *e,void *u){
(void)h;(void)e;(void)u;__atomic_fetch_add(&lost,1,__ATOMIC_RELAXED);return false;
}
static esp_err_t reg_read(uint8_t reg,uint8_t *value){return i2c_master_transmit_receive(io,®,1,value,1,100);}
static esp_err_t reg_write(uint8_t reg,uint8_t value){uint8_t b[]={reg,value};return i2c_master_transmit(io,b,2,100);}
esp_err_t board_leds(bool red,bool blue){
uint8_t out;ESP_RETURN_ON_ERROR(reg_read(1,&out),TAG,"TCA output read");
out=(out&~0xc0)|(red?0x40:0)|(blue?0x80:0);return reg_write(1,out);
}
uint32_t board_audio_lost(void){return __atomic_load_n(&lost,__ATOMIC_RELAXED);}
/* P0 and GPIO48 share PA_CTRL. Only MCU reset does not reset TCA latches.
* First release MCU drive, then release the expander, preload LOW, finally drive.
* Speaker is disconnected in this recipe; no seamless mute/electrical guarantee. */
static esp_err_t board_io_claim(void){
ESP_RETURN_ON_ERROR(gpio_set_direction(GPIO_NUM_48,GPIO_MODE_INPUT),TAG,"PA release MCU");
ESP_RETURN_ON_ERROR(gpio_set_pull_mode(GPIO_NUM_48,GPIO_FLOATING),TAG,"PA no MCU pull");
i2c_master_bus_config_t b={.i2c_port=I2C_NUM_0,.sda_io_num=17,.scl_io_num=18,.clk_source=I2C_CLK_SRC_DEFAULT,.glitch_ignore_cnt=7,.flags.enable_internal_pullup=true};
ESP_RETURN_ON_ERROR(i2c_new_master_bus(&b,&bus),TAG,"I2C");
// V3.0 TCA9554 has A2:A0=000. No blind fallback to different board revisions.
ESP_RETURN_ON_ERROR(i2c_master_probe(bus,0x20,100),TAG,"TCA9554 absent");
i2c_device_config_t dc={.dev_addr_length=I2C_ADDR_BIT_LEN_7,.device_address=0x20,.scl_speed_hz=100000};
ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(bus,&dc,&io),TAG,"TCA add");
uint8_t out,dir;ESP_RETURN_ON_ERROR(reg_read(1,&out),TAG,"TCA latch");
// P5 LOW powers peripherals (SD), P6/P7 initially OFF; preserve other latches.
ESP_RETURN_ON_ERROR(reg_write(1,out&~0xe0),TAG,"TCA initial levels");
ESP_RETURN_ON_ERROR(reg_read(3,&dir),TAG,"TCA direction");
ESP_RETURN_ON_ERROR(reg_write(3,(dir&~0xe0)|1),TAG,"TCA outputs; P0 input");
ESP_RETURN_ON_ERROR(reg_read(3,&dir),TAG,"TCA verify P0 input");
if(!(dir&1)){return ESP_ERR_INVALID_STATE;}
ESP_RETURN_ON_ERROR(gpio_set_level(GPIO_NUM_48,0),TAG,"PA preload OFF");
ESP_RETURN_ON_ERROR(gpio_set_direction(GPIO_NUM_48,GPIO_MODE_OUTPUT),TAG,"PA claim MCU output LOW");
return ESP_OK;
}
esp_err_t board_audio_init(void){
ESP_RETURN_ON_ERROR(board_io_claim(),TAG,"PA/TCA ownership");
ESP_RETURN_ON_ERROR(i2c_master_probe(bus,0x40,100),TAG,"ES7210 absent");
i2s_chan_config_t cc=I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0,I2S_ROLE_MASTER);cc.dma_desc_num=8;cc.dma_frame_num=256;
ESP_RETURN_ON_ERROR(i2s_new_channel(&cc,NULL,&rx),TAG,"I2S channel");
i2s_std_config_t sc={.clk_cfg=I2S_STD_CLK_DEFAULT_CONFIG(16000),.slot_cfg=I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,I2S_SLOT_MODE_STEREO),.gpio_cfg={.mclk=16,.bclk=9,.ws=45,.dout=I2S_GPIO_UNUSED,.din=10}};
ESP_RETURN_ON_ERROR(i2s_channel_init_std_mode(rx,&sc),TAG,"STD stereo16");
i2s_event_callbacks_t cb={.on_recv_q_ovf=overflow};ESP_RETURN_ON_ERROR(i2s_channel_register_event_callback(rx,&cb,NULL),TAG,"overflow callback");
audio_codec_i2s_cfg_t data_cfg={.port=I2S_NUM_0,.rx_handle=rx};
const audio_codec_data_if_t *data=audio_codec_new_i2s_data(&data_cfg);
// Codec API uses 8-bit control address 0x80; raw master probe above uses 7-bit0x40.
audio_codec_i2c_cfg_t ctrl_cfg={.port=I2C_NUM_0,.addr=ES7210_CODEC_DEFAULT_ADDR,.bus_handle=bus};
const audio_codec_ctrl_if_t *ctrl=audio_codec_new_i2c_ctrl(&ctrl_cfg);
es7210_codec_cfg_t adc_cfg={.ctrl_if=ctrl,.mic_selected=ES7210_SEL_MIC1|ES7210_SEL_MIC2,.master_mode=false};
const audio_codec_if_t *adc=es7210_codec_new(&adc_cfg);
if(!data||!ctrl||!adc)return ESP_ERR_NO_MEM;
esp_codec_dev_cfg_t dev_cfg={.dev_type=ESP_CODEC_DEV_TYPE_IN,.codec_if=adc,.data_if=data};
esp_codec_dev_handle_t dev=esp_codec_dev_new(&dev_cfg);if(!dev)return ESP_ERR_NO_MEM;
esp_codec_dev_sample_info_t fs={.sample_rate=16000,.channel=2,.bits_per_sample=16,.channel_mask=3};
if(esp_codec_dev_open(dev,&fs)!=ESP_CODEC_DEV_OK)return ESP_FAIL;
if(esp_codec_dev_set_in_gain(dev,24.0f)!=ESP_CODEC_DEV_OK)return ESP_FAIL;
// Direct channel read below checks actual byte count, not only codec wrapper status.
return ESP_OK;
}
esp_err_t board_audio_read(int16_t *stereo,size_t frames){
size_t bytes=0,want=frames*2*sizeof(int16_t);esp_err_t e=i2s_channel_read(rx,stereo,want,&bytes,500);
return e==ESP_OK&&bytes==want?ESP_OK:(e==ESP_OK?ESP_ERR_INVALID_SIZE:e);
}
esp_err_t board_sd_mount(void){
sdmmc_host_t host=SDMMC_HOST_DEFAULT();host.max_freq_khz=SDMMC_FREQ_DEFAULT;
sdmmc_slot_config_t slot=SDMMC_SLOT_CONFIG_DEFAULT();slot.width=1;slot.clk=15;slot.cmd=7;slot.d0=4;slot.d1=GPIO_NUM_NC;slot.d2=GPIO_NUM_NC;slot.d3=GPIO_NUM_NC;
esp_vfs_fat_sdmmc_mount_config_t cfg={.format_if_mount_failed=false,.max_files=4,.allocation_unit_size=16384};sdmmc_card_t *card=NULL;
return esp_vfs_fat_sdmmc_mount("/sd",&host,&slot,&cfg,&card);
}
// ---- main/board_audio.h ----
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
esp_err_t board_audio_init(void);
esp_err_t board_audio_read(int16_t *stereo,size_t frames);
uint32_t board_audio_lost(void);
esp_err_t board_leds(bool red,bool blue);
esp_err_t board_sd_mount(void);
// ---- main/speech_state.h ----
// Copyright 2026 hixinwu. SPDX-License-Identifier: MIT
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef struct{bool active;bool led_on;uint64_t until;} command_gate_t;
static inline void command_wake(command_gate_t *g,uint64_t now){if(!g->active){g->active=true;g->until=now+8000000ULL;}}
static inline void command_tick(command_gate_t *g,uint64_t now){if(g->active&&now>=g->until)g->active=false;}
static inline bool command_accept(command_gate_t *g,int id,uint64_t now){command_tick(g,now);if(!g->active||!(id==1||id==2))return false;g->led_on=id==1;g->active=false;return true;}
static inline int16_t mic1(const int16_t *stereo,unsigned frame){return stereo[2*frame];}参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[4] BSP5.0.0固定音频实现:ES7210/端点[5] BSP5.0.0固定头:虚拟LED映射和硬件引脚[17] IDF5.5.5标准I2S新channel宏[21] IDF5.5.5 GPIO API:输入/方向/拉电阻[22] IDF5.5.5 GPIO实现:输出锁存与驱动使能分别设置
固定硬件和电源边界
只适用于官方ESP32-S3-Korvo-2主板V3.0,且实际模块丝印为ESP32-S3-WROOM-1-N16R8:16MB Flash、8MB Octal PSRAM。官方主站列Korvo-2为16/8MB,旧V3.0手册只写WROOM-1,没有保证每块后缀;先拍下自己的模块/主板丝印,不能套V3.1、Korvo-1、仿板、N16R8V或A01的DevKitC。本文没有持有这块实板,不能替读者确认出厂装配。
使用板载ES7210四路ADC中的MIC1和MIC2,板图04_AUDIO的U23/U31为MSM381A3729H9BPC模拟麦克风。它们不是A01的ICS43434数字麦克风;不要外接A01杜邦线。ES7210经I2C配置,输出到S3的标准I2S,程序选择MIC1/2、stereo PCM16;仅取交错数组第0路送AFE,input_format固定“M”。MIC2采入但未送算法,MIC3的板载DAC回声参考未选。AEC关闭、SE阵列处理关闭,不用虚构R/全零R冒充真正参考。NS选择WebRTC、VAD选择WebRTC,线性增益1、AGC关闭;ES7210前端增益固定24dB。这里不宣称双麦波束形成、声压级或回声消除性能。
取下LCD/摄像头扩展,电池座留空。关板上电源开关后,按官方手册分别接USB-to-UART数据口与独立USB Power口;后者用稳压5V/2A适配器和合适线材,再打开开关。不把两个Micro-USB口当作原生USB19/20;bridge/UART0用于115200日志。观察供电/温升异常立即关电排线,不靠提高麦克风增益处理供电故障。不接裸市电、外部功放或电池充电实验。
| 端点 | 固定连接/程序选择 | 断电核对 |
|---|---|---|
| I2C SDA/SCL | GPIO17/18;主机100kHz | 板载总线,勿接5V上拉 |
| ES7210控制 | 主机7位0x40;codec API参数0x80为8位地址 | 两种地址写法不能混用 |
| I2S MCLK/BCLK/WS/ADC SDOUT | GPIO16/9/45/10;RX only | 16kHz、2×16bit,名义MCLK4.096MHz/BCLK512kHz;必须用仪表核实实际波形 |
| ES7210 MIC1/MIC2 | 16位有符号交错;每帧选stereo[2*i] | 不把声道编号直接称左右空间位置 |
| PA_CTRL | 先释放GPIO48;TCA P0输入读回后,GPIO48才输出LOW | 不初始化ES8311播放、不接扬声器;软件OFF不是物理断电证明 |
| TCA9554 | 7位0x20,V3.0固定A2:A0=000 | P5=外设电源,LOW使能;P6=红灯,P7=蓝灯,高亮 |
| LED虚拟编号 | BSP的55/56是扩展器映射 | 不是S3 GPIO55/56,项目直接读写TCA寄存器并保留其他位 |
| MicroSD(仅A03使用) | 1bit:CLK15/CMD7/D0=4 | 关机插拔,4–32GB FAT32 SDHC;不自动格式化 |
参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[3] 乐鑫官方S3产品页:Korvo2 16MB/8MB配置[4] BSP5.0.0固定音频实现:ES7210/端点[5] BSP5.0.0固定头:虚拟LED映射和硬件引脚[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏
锁定工程、构建与刷机
使用ESP-IDF 5.5.5,ESP-SR 2.4.7(commit2f8c4b0459db5bbb39abd77adae27962d6d94bcb),esp_codec_dev 1.5.11、esp-dsp 1.8.0、dl_fft 0.8.0、cjson 1.7.19~2。这是一组实际可构建基线,不声称最新版本。main/idf_component.yml与正常Registry service源dependencies.lock逐个锁定;无需Arduino core,也没有第二套IDF。源码直接用IDF新I2S channel与I2C master API,不混装legacy driver。
本次作者从既有、已核Registry component_hash及每文件CHECKSUMS的缓存,用APFS clone预填标准managed_components,随后运行正常resolver,得到可迁移service lock;没有override_path、私有SDK路径写进公共manifest,也没有改上游源。普通机器由组件管理器按锁下载一次即可。离线机器须准备同版本缓存;不能静默换成另一版SR或模型。首次下载占用显著,空间不足先停止构建并核已有缓存,不能删最终固件/模型/配置/日志冒充恢复。
sdkconfig.defaults固定16MB/QIO、80MHz Flash与Octal PSRAM;分区factory=4MiB、model=10MiB,应用和模型的实际大小见作者构建JSON。model子类型写spiffs只是分区标签,本项目由ESP-SR读取其专用打包格式,不要拿SPIFFS格式化它。全部flash参数/偏移以当前build/flasher_args.json为准,SR模型会在0x410000写入;换旧固件也要按那一版的完整分区/模型恢复。错误模型不能仅凭“应用烧成功”排除。
刷机失败先核PORT/数据线/串口占用;关闭监视器,按住BOOT点RESET进入下载,重新flash全部当前输出,松开BOOT再RESET。重启后核项目ID、固定版本、pipeline与模型打印,不拿旧终端内容当新固件结果。PSRAM不识别或模块后缀不同先停,不能删PSRAM设置强行运行大型模型。
idf.py --version
idf.py -D IDF_TARGET=esp32s3 build
idf.py -p PORT flash monitor参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义
软件运行与错误边界
I2S在codec初始化时已经启用;AFE/模型初始化期间的队列可能产生旧溢出。真正采样任务先丢弃至少8000个stereo帧(名义至少0.5秒),之后快照RX overflow计数作为本轮边界;任何后续计数变化、读错或不足字节立即锁存FAULT。启动丢弃是本项目保守处理,不是麦克风有效时间或音质实测。feed chunk由AFE运行时查询,16kHz PCM16、一个M通道;fetch数据长度/状态均检查,不能假设feed与fetch帧数相同。
feed任务与fetch任务分开,AFE可能内部阻塞,100ms fetch等待不是硬实时总延迟保证。2秒无fetch或运行读错会锁存故障,要求修复后RESET;没有断线后自动伪造PCM恢复路径。全部应用计时用esp_timer_get_time的64位微秒,未用32位millis让旧窗口在完整周期后复活。没有Wi-Fi/云服务和音频上传;本项目也没有音频播放路径。
GPIO48与TCA9554 P0在本板共用PA_CTRL。只复位MCU不保证TCA输出锁存被复位:初始化先将GPIO48设INPUT/FLOATING,确认0x20的P0为输入并读回,再预装GPIO48低电平、切OUTPUT;探测/写入/读回失败保持MCU高阻,不强行接管。此前状态可能仍使PA使能,本例不接扬声器,不承诺上电到接管之间无声或无缝静音;这只是按原理图避免相反强驱动的调用顺序,电平/时序仍待实板。
运行路径在阻塞fetch返回后立即重查故障标志;A05还在detect、get_results/时钟返回后以及新的正常LED调用前检查,已观察故障则丢弃本轮结果,收尾尝试故障红灯。A03也在采样发布、开始新录音和逐文件提交边界检查,不把已观察故障后的完整录音标为成功。检查与下一次调用之间仍有跨核调度窗口,已经进入的I2C/文件写入不能撤销;不承诺FAULT日志出现瞬间全核硬实时停止或已写文件自动回滚。
初始化故障的ESP_ERROR_CHECK可能让固件中止/重启,运行故障则打印锁存FAULT;二者不能混称“仍在正常识别”。红灯/日志为诊断输出,未实测前不保证灯真的亮。ADC/I2C探测只证明地址有应答;即使流正常,静音/错误通道也不能靠软件日志证明麦克风和声学路径正常。
参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义
故障输入、恢复与记录
observations.csv、electrical-observations.csv均仅表头。按指定模块实际填供电/电流条件、MCLK/BCLK/WS、固件/模型SHA、现场声音与结果,不能填入构建机的合成值。本交付没有实板、麦克风录音、声学/电气仪表或实际灯光观察;所有硬件状态pending。
| 输入/现象 | 可执行检查与恢复 |
|---|---|
| 0x20或0x40探测失败 | 关电检查V3.0/模块/两口供电和板损伤;不向不同地址盲写寄存器。修好后RESET |
| PSRAM/AFE/model初始化失败 | 核N16R8与当前sdkconfig、应用+model完整烧录、锁定版本;保留原日志后重新build/flash,不格式化model |
| RX overflow/短读/无fetch | 保留首个FAULT和固件SHA,核供电、I2S波形/任务负载;修复后RESET,不能把旧音频拼成连续新数据 |
| 启动或运行静音 | ADC地址应答不等于麦克风有效;按原理图核MIC1支路/增益/实际PCM统计,再定位算法,勿提高增益掩盖问题 |
| LED日志变化但没灯 | 先核TCA P6/P7和板修订;GPIO55/56不是可接实体端点;断电复核后恢复 |
| 断电/RESET | 应用窗口与RAM录音消失,LED初始化OFF;保留卡上已完成文件及.part,不能据旧记录宣称新会话有效 |
参考:[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[15] codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟[16] codec1.5.11固定I2S数据接口:open/reconfigure/read[17] IDF5.5.5标准I2S新channel宏[18] IDF5.5.5 I2S读/等待的实际实现[19] IDF5.5.5 esp_timer:64位微秒语义
桌面检查与许可
同源C11 ASan/UBSan只验证main实际调用的纯逻辑:MIC1交错提取;A05另测命令窗口/有效ID/超时/64位时刻。它们不在桌面运行ESP-SR二进制推理,也不证明真实说话能唤醒/识别。
原创固件/工具/图为MIT,见LICENSE;ESP-IDF与codec等依组件原许可。ESP-SR的LICENSE为ESPRESSIF MIT,授权条件限用于乐鑫产品,不能简写成无限制普通MIT;库和模型由官方组件按原许可提供,未随下载包复制大二进制。第三方资料版权归原厂;固定来源、实际章节和比较记录见正文sources及作者证据,不用URL可达替代阅读。
clang -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -fno-omit-frame-pointer -I main tests/state.c -o /tmp/speech-state
/tmp/speech-state复现故障边界与PA所有权顺序
输出目录必须未存在。前者原样提取当前完整fetch_task,A03另提取完整save_record,以明确外部AFE/MN/时钟/LED或stdio桩注入故障;后者原样提取完整board_io_claim,覆盖有电TCA P0保留高/低、MCU旧锁存与I2C探测/写入/读回失败16组。它们验证生产调用边界和顺序,不运行神经识别、真实RTOS调度、SD/I2C/麦克风或电气实测。LED用例另保留“已进入调用后才发布故障”的允许边界,不能把桩结果当灯已亮/PA已静音。
PA接管使用的固定API与实现另见[IDF5.5.5 gpio.h](https://github.com/espressif/esp-idf/blob/v5.5.5/components/esp_driver_gpio/include/driver/gpio.h)及[gpio.c](https://github.com/espressif/esp-idf/blob/v5.5.5/components/esp_driver_gpio/src/gpio.c),已与实际SDK逐字核对。
python tools/check_fault_boundaries.py --out /tmp/NEW-speech-fault-boundaries
python tools/check_pa_ownership.py --out /tmp/NEW-speech-pa-order参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[4] BSP5.0.0固定音频实现:ES7210/端点[5] BSP5.0.0固定头:虚拟LED映射和硬件引脚[17] IDF5.5.5标准I2S新channel宏[21] IDF5.5.5 GPIO API:输入/方向/拉电阻[22] IDF5.5.5 GPIO实现:输出锁存与驱动使能分别设置
保留软件证据,等待现场结果
本次实际IDF5.5.5构建、源/配置/模型/镜像SHA、固定上游比较和主机/真实文件检查归档于audio-speech-builds.json、audio-speech-host-final.json、audio-speech-model-files.json与audio-speech.md。模型包检查不运行神经推理,编译通过不替代声音、灯光、电源或时钟测量。
下载包中的observations.csv和electrical-observations.csv仅表头。请记录自己的板型、应用/模型SHA与实验条件后再填入现场数据,不能把本文构建机的合成WAV、注入命令ID或数字统计写成录音与识别结果。
- 做到这里,你应该看到
- 能拿出当前源与真实编译/工具证据;未执行的物理项目保持pending。
参考:[1] Korvo2 V3.0固定用户手册:供电、针脚、AEC参考[2] Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO[6] ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则[7] ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待[20] ESP-SR2.4.7原许可:限乐鑫产品的ESPRESSIF MIT
资料来源与版本说明
固定SR/codec源码与校验缓存、IDF5.5.5实际安装源及官方板图已读核;编译/主机/模型字节检查不替代麦克风、灯光或仪表实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- Korvo2 V3.0固定用户手册:供电、针脚、AEC参考
- Korvo2 V3.0原理图20210918:03_CAM_LCD/04_AUDIO
- 乐鑫官方S3产品页:Korvo2 16MB/8MB配置
- BSP5.0.0固定音频实现:ES7210/端点
- BSP5.0.0固定头:虚拟LED映射和硬件引脚
- ESP-SR2.4.7固定AFE配置:M/R、NS/VAD、检查规则
- ESP-SR2.4.7固定AFE接口:feed/fetch数据与等待
- ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en
- ESP-SR2.4.7打包脚本:MultiNet7附带fst
- ESP-SR2.4.7模型布局打包器
- ESP-SR2.4.7模型加载:名称和文件索引
- ESP-SR2.4.7 MultiNet接口:chunk/timeout/结果
- ESP-SR2.4.7命令图:英文g2p/add/update
- ESP-SR2.4.7 wn9_hiesp实际模型信息
- codec1.5.11固定ES7210驱动:MIC选择、位宽和时钟
- codec1.5.11固定I2S数据接口:open/reconfigure/read
- IDF5.5.5标准I2S新channel宏
- IDF5.5.5 I2S读/等待的实际实现
- IDF5.5.5 esp_timer:64位微秒语义
- ESP-SR2.4.7原许可:限乐鑫产品的ESPRESSIF MIT
- IDF5.5.5 GPIO API:输入/方向/拉电阻
- IDF5.5.5 GPIO实现:输出锁存与驱动使能分别设置