先分清本页的产物与边界
本例让Korvo-2的MIC1经NS/VAD进入WakeNet9固定wn9_hiesp,识别到“Hi ESP”后给蓝灯一个约1秒的软件反馈窗口。没有网络、文本转写、自动对话或开机播音。编译和模型包字节已检查,但没有实板/手机/音箱唤醒结果,误触发与漏检表保持空白。
本页的产物是本地唤醒反馈与可执行测试方法。A03负责看RAW/AFE及VAD;本页不把VAD触发当唤醒,也不把改角色名当自定义唤醒词。该锁定模型不包括任意自定义中文词,定制支持应另按上游当前方案核查。
参考:[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位微秒语义
模型、反馈与现场实验
sdkconfig仅选wn9_hiesp,启动时对实际esp_srmodel_filter结果再检查完整名称;不是从模型目录随意挑第一个。AFE获取结果中只有WAKENET_DETECTED才打印WAKE并更新蓝灯until,VAD_SPEECH不会点亮唤醒反馈。输出日志含软件fetch时刻、模型/词索引与trigger_channel,不能把它们当麦克风采样时间或距离测量。反馈截止由64位微秒比较,任务阻塞时实际灯时长可更长,不是硬实时1秒保证;连续唤醒可续期。
先在安静条件、正对MIC1约固定近距处连续说同一句“Hi ESP”10次,每次留至少3秒;记录尝试数/成功反馈数。再只改变一个条件:更远距离、偏转角度、固定风扇背景或非唤醒语句,均记录相同次数。不同人/音量/口音分组,不把距离变化和噪声同时改动后归因给某一个因素。误触发观察可设固定10分钟不说唤醒词,保留背景类型和意外事件时间。
漏检率要以实际明确尝试次数作分母;误触发每小时要以真正观察时长作分母,不能把10分钟零次写成“零误触发保证”。蓝灯可能坏、也可能软件检测但GPIO失败,要连同WAKE/FAULT日志核对。原始空表不含任何距离/成功率结果。自定义唤醒词/另一语言不在本项目已构建范围,换模型要重新核配置、model包和声学测试。
参考:[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[9] ESP-SR2.4.7打包脚本:MultiNet7附带fst[10] ESP-SR2.4.7模型布局打包器[11] ESP-SR2.4.7模型加载:名称和文件索引[12] ESP-SR2.4.7 MultiNet接口:chunk/timeout/结果[13] ESP-SR2.4.7命令图:英文g2p/add/update[14] ESP-SR2.4.7 wn9_hiesp实际模型信息
在电脑核实际模型包
inspect_models.py按固定pack_model.py/model_path.c布局检查条目数、名称、边界/重叠/总长度,并把每个实际打包条目的字节和受校验的managed模型源比较。不是只看目录名。截断复制品应Exit3拒绝,恢复原文件再验证Exit0;保留原文件SHA。该工具不运行WakeNet推理,也不是模型签名或供应链认证工具。记录实际应用与srmodels.bin SHA后完整刷机,不能拿另一次同名bin替代。
python3 tools/inspect_models.py build/srmodels/srmodels.bin --expect wn9_hiesp --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数据与等待[8] ESP-SR2.4.7固定Kconfig:WebRTC/wn9_hiesp/mn7_en[9] ESP-SR2.4.7打包脚本:MultiNet7附带fst[10] ESP-SR2.4.7模型布局打包器[11] ESP-SR2.4.7模型加载:名称和文件索引[12] ESP-SR2.4.7 MultiNet接口:chunk/timeout/结果[13] ESP-SR2.4.7命令图:英文g2p/add/update[14] ESP-SR2.4.7 wn9_hiesp实际模型信息
完整应用、板级音频与同源状态代码
以下四个文件与下载目录逐字一致。项目另含正常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 "A04"
#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);}
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);
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(),blue_until=0;command_gate_t gate={0};
#if COMMANDS
srmodel_list_t *models=arg;char *mn_name=esp_srmodel_filter(models,ESP_MN_PREFIX,ESP_MN_ENGLISH);
if(!mn_name||strcmp(mn_name,"mn7_en")){fail("expected mn7_en");vTaskDelete(NULL);return;}
const esp_mn_iface_t *mn=esp_mn_handle_from_name(mn_name);model_iface_data_t *md=mn?mn->create(mn_name,5000):NULL;
if(!md||mn->get_samp_rate(md)!=16000){fail("MultiNet create/rate");vTaskDelete(NULL);return;}
int chunks=mn->get_samp_chunksize(md),fill=0;int16_t *block=heap_caps_malloc(chunks*2,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);
if(chunks<=0||chunks>4096||!block||esp_mn_commands_alloc(mn,md)!=ESP_OK||esp_mn_commands_add(1,"turn on the light")!=ESP_OK||esp_mn_commands_add(2,"turn off the light")!=ESP_OK||esp_mn_commands_update()!=NULL){fail("MultiNet command graph/allocation");vTaskDelete(NULL);return;}
mn->print_active_speech_commands(md);ESP_LOGI(TAG,"MODEL=%s; id1=turn on the light; id2=turn off the light; local LED only",mn_name);
#endif
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;
if(res->wakeup_state==WAKENET_DETECTED){
if(atomic_load(&failed)){break;}
ESP_LOGI(TAG,"WAKE software_fetch_us=%llu model_index=%d word=%d trigger_channel=%d; physical=UNTESTED",(unsigned long long)t,res->wakenet_model_index,res->wake_word_index,res->trigger_channel_id);
#if COMMANDS
if(!gate.active){command_wake(&gate,t);fill=0;mn->clean(md);}
#else
blue_until=t+1000000ULL;
#endif
}
#if COMMANDS
bool was_active=gate.active;command_tick(&gate,t);
if(was_active&&!gate.active){mn->clean(md);fill=0;ESP_LOGI(TAG,"COMMAND_WINDOW_TIMEOUT wall8s; LED state unchanged");}
if(gate.active){
for(int i=0;i<res->data_size/2&&gate.active&&!atomic_load(&failed);i++){
block[fill++]=res->data[i];if(fill!=chunks)continue;fill=0;
command_tick(&gate,now_us());if(!gate.active){mn->clean(md);break;}
esp_mn_state_t state=mn->detect(md,block);
if(atomic_load(&failed)){break;} // detect may have blocked across a feed fault.
if(state==ESP_MN_STATE_DETECTED){esp_mn_results_t *result=mn->get_results(md);bool accepted=false;
uint64_t decision_us=now_us();
if(atomic_load(&failed)){break;} // includes get_results and clock observation.
if(result&&result->num>0&&isfinite(result->prob[0])&&result->prob[0]>=0.7f)accepted=command_accept(&gate,result->command_id[0],decision_us);
if(atomic_load(&failed)){break;}
ESP_LOGI(TAG,"COMMAND detected accepted=%d id=%d probability=%f; not physical LED proof",accepted,result&&result->num?result->command_id[0]:-1,result&&result->num?result->prob[0]:0.0);
gate.active=false;mn->clean(md);
}else if(state==ESP_MN_STATE_TIMEOUT){gate.active=false;mn->clean(md);ESP_LOGI(TAG,"COMMAND model timeout; no action");}
}
}
if(atomic_load(&failed)){break;} // final normal-output gate; an in-flight I2C call cannot be revoked.
if(board_leds(gate.led_on,gate.active)!=ESP_OK){fail("LED expander transaction");break;}
#else
if(atomic_load(&failed)){break;} // no new normal LED call after observed fault.
(void)gate;if(board_leds(false,t<blue_until)!=ESP_OK){fail("LED expander transaction");break;}
#endif
}
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=true;
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->wakenet_model_name=esp_srmodel_filter(models,ESP_WN_PREFIX,"hiesp");if(!cfg->wakenet_model_name||strcmp(cfg->wakenet_model_name,"wn9_hiesp")){fail("expected wn9_hiesp");return;}
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);
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;}
while(!atomic_load(&failed)){vTaskDelay(pdMS_TO_TICKS(1000));} ESP_LOGE(TAG,"latched fault; RESET after hardware/config 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实现:输出锁存与驱动使能分别设置