这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【成果帖】ESP32-S31-Korvo-1市政污水厂智能语音巡检助手

共1条 1/1 1 跳转至

【成果帖】ESP32-S31-Korvo-1市政污水厂智能语音巡检助手

菜鸟
2026-09-22 23:35:02     打赏

一、项目介绍

市政污水处理厂日常巡检依赖人工抄表、纸质记录,痛点很集中:

  1. 工艺参数(溶解氧 DO / 污泥浓度 / pH / 温度 / 流量 / 液位)分散在 PLC、SCADA、就地仪表上,一次巡检 40~60 分钟;

  2. 报警靠现场喊或对讲机,工单不能追溯;

  3. 巡检员戴安全帽、手套,手机操作不便。

本项目把 ESP32-S31-Korvo-1 做成一台带屏 + 摄像头 + 麦克风阵列的语音巡检助手,实现:

  • 免手持:唤醒词 "Hi 小E" + 中文自然语言指令;

  • 看得见:4.3 寸 LCD 上跑 LVGL 9.5 仪表盘(8 页面 · 5 路实时工艺量 + 告警 + 工单 + 历史趋势 + 摄像头画面);

  • 说得清:本地 + 云端 TTS 双通道;

  • 连得广:Modbus RTU 主站读 PLC、MQTT 上报、HTTP 配网维护、双分区 OTA。

一句话:用一块 S31 开发板替代"手持仪表 + 对讲机 + 巡检平板"三件套。

44d7654dee4e948e4e2058fcf8c9d893.jpg

成果视频


二、硬件方案说明

2.1 主控模组 ESP32-S31-WROOM-3

关键规格数值我在项目里怎么用的
CPURISC-V32 双核 @ 320 MHzCore 0 跑 UI + 语音 NL,Core 1 跑采集 + 网络
片内 SRAM~512 KBDMA / WebSocket / TLS 都在这,AFE 打碎后专门留 reserve
PSRAM16 MB Octal @ 200 MHz语音 PCM、摄像头 JPEG、24 h 参数历史全在这里
Flash16 MB QSPI双 OTA (0.49 MB × 2) + model + spiffs + storage
无线Wi-Fi 6 (802.11ax) + BLE 5HTTPS → iFlytek / DeepSeek,MQTT 上云
显示RGB LCD 24 bit4.3 寸 800 × 480
摄像头DVP 8 bitWS8204 200 万像素
音频I2S × 2双 MIC + 立体声喇叭,走 ES8389

2.2 核心外设

① ES8389 音频 Codec:从模式,Set internal reference signal 打开后噪声底降 ~6 dB;16 kHz × 2 ch × 16 bit 送 esp-sr AFE。

② GT1151 电容触摸:I2C 0x5D,esp_lcd_touch_gt1151 组件;识别到 GT1158_000101(Patch)。

③ WS8204 DVP 摄像头:SCCB + 8 bit 并行,走 esp_video + esp_new_jpeg;抓帧 → JPEG → SPIFFS → HTTP 上传,配合工单闭环。

④ 双 MEMS 麦克风:阵元间距 5 cm;3 m 内唤醒率 >90%,5 m >70%。

⑤ RS-485 / Modbus RTU:UART1 + SP3485 做 Modbus 主站读 PLC 保持寄存器;无 PLC 时回退到"演示数据"(UI 顶部会标注数据来源)。

⑥ microSD + 4 路 ADC 按键 + WS2812 LED:SD 走 SPI2 做离线日志;按键电阻梯形分压;LED 用 led_strip 组件。


三、系统设计思路

3.1 分层架构

┌────────────────────────────────────────────────────────┐
│  应用层 Application · 语音助手 / 仪表盘 / 工单 / 告警 │
├────────────────────────────────────────────────────────┤
│  服务层 Service     · ASR / LLM / TTS / MQTT / HTTP    │
│                       SNTP / Audit Logger              │
├────────────────────────────────────────────────────────┤
│  业务层 Business    · 语音引擎 / 数据采集 / 告警分类   │
│                       工单流程 / 阈值管理              │
├────────────────────────────────────────────────────────┤
│  驱动层 Driver      · board_init · I2C · I2S · RGB     │
│                       · DVP · SD · Modbus · Wi-Fi      │
├────────────────────────────────────────────────────────┤
│  平台:ESP-IDF v6.2 · FreeRTOS · LVGL 9.5 · esp-sr 2.5│
└────────────────────────────────────────────────────────┘

3.2 关键设计取舍

  1. 本地意图 + 云端 LLM 双通道 — 常见指令用关键词/正则本地命中,省 800 ms 一次 LLM 往返;未命中才送 DeepSeek。

  2. 语音会话连续模式 — 一次唤醒后进入 5 s 录音,播报完再自动开 5 s,多轮不用重复唤醒。

  3. LVGL 线程安全 — CONFIG_LV_OS_NONE=y 下所有非 LVGL 任务的 lv_async_call 必须 bsp_display_lock。

  4. PSRAM 使用纪律 — flash-mmapped 任务栈不能放 PSRAM;语音 worker 全部常驻;ASR 启动前专门留一块连续内部 DRAM。


四、功能框图 & 软件流程图

4.1 系统功能框图

                       ┌───────────────────┐
   2×MEMS MIC ──┐      │   Wi-Fi 6 / BLE   │
                ▼      └─────────┬─────────┘
         ┌──────────┐            │ HTTPS  WSS
         │  ES8389  │            ▼
         └────┬─────┘    ┌────────────────────┐
              │ I2S      │ iFlytek IAT (ASR)  │
              ▼          │ DeepSeek  (LLM)    │
        ┌──────────┐     │ edge-tts  (TTS)    │
        │   AFE    │     └──────────┬─────────┘
        │WakeNet9  │                │
        │VAD(WebRTC│                ▼
        └────┬─────┘        ┌──────────────┐
             │ 唤醒中断     │ 本地意图匹配 │
             ▼              │ + Function   │
   ┌──────────────────┐     │   Call 分发  │
   │ 5s PCM 环形缓冲  │     └──────┬───────┘
   │  (PSRAM 160 KB)  │            │
   └────────┬─────────┘            │
            └──────────┬───────────┘
                       ▼
             ┌────────────────────────────┐
             │   FreeRTOS 消息/队列总线   │
             └─┬────┬────┬────┬────┬────┬─┘
               ▼    ▼    ▼    ▼    ▼    ▼
             LVGL  Modbus MQTT HTTP SD   OTA
              │      │     │     │   │     │
              ▼      ▼     ▼     ▼   ▼     ▼
            LCD   继电器 云端  配网 巡检 版本
           800×480 阀门 Telemetry Portal 拍照 回滚
             │
             ▼
           摄像头 DVP ─▶ JPEG ─▶ SPIFFS

4.2 语音主流程

 [待机]
   │  (WakeNet 阈值 0.4)
   ▼
 "Hi 小E" → 亮屏 + 状态切 RECORDING + 播放滴声
   ▼
 录音 5.0 s ── 中途 WebRTC VAD 静音 > 800 ms 提前停
   ▼
 PCM → WebSocket(iFlytek IAT)  64 chunks / 15 ms 帧 / status=2 结束
   ▼
 ASR 文本
   ├─ 本地意图命中 → 立即分发 (≈200 ms)
   └─ 未命中 → DeepSeek Function-Call → 分发
                     ▼
       播报 TTS + 更新 UI → 连续模式:再开 5 s 录音

4.3 告警 / 工单闭环

 传感器读数 → threshold_manager 判级 → alarm_engine 去抖/延时
                                            ▼
                             alarm_pipeline (3 dispatch target)
                                            │
                       ┌──────────┬─────────┼─────────┬────────┐
                       ▼          ▼         ▼         ▼        ▼
                    蜂鸣/LED   LVGL 弹窗  TTS 播报  通知服务  MQTT
                                                        ▼
                                          工单 ticket_workflow
                                                        ▼
                                              SLA 计时 · 升级

五、实现过程说明

M1:环境 & 点亮 (Day 1-3) — EIM 装 ESP-IDF v6.2 master-s31 分支,BSP esp32_s31_korvo_1 组件点亮 LCD + I2S。

M2:音频链路 (Day 4-7) — 挂 ES8389,跑 esp-sr AFE + wn9_hiesp;5 s 环形缓冲放 PSRAM。

M3:语音链路 (Day 8-14) — iFlytek IAT WebSocket + HMAC-SHA256 签名 + base64;edge-tts 代理;DeepSeek Function-Call 定义 15 个工具;本地关键词命中率 ≈ 70%。

M4:仪表盘 & 数据链路 (Day 15-20) — LVGL 9.5 八页面;gen_wwtp_cjk_font.py 生成中文字体子集;Modbus / MQTT / HTTP / SPIFFS 全通。

M5:稳定性 & OTA (Day 21-25) — 双 OTA 分区 4.6 MB × 2;修 SNTP GOT_IP 初始化顺序 bug、AFE 打碎 DRAM 导致 TLS 起不来、nl_task PSRAM 栈崩溃三个硬伤。

M6:现场化 & UI 差异化 (Day 26-28) — 实时页 vs 历史页去掉重复的 24 h 数据块,字体子集重生成。


六、关键代码解析

6.1 唤醒 → 录音 → ASR:voice_pipeline.cpp

void VoicePipeline::feed(const int16_t *pcm, size_t n) {
    afe->feed(pcm, n);
    if (state_ == IDLE) {
        auto res = afe->fetch();
        if (res.wakenet_model_name) {          // WakeNet 命中
            state_ = RECORDING;
            rec_buffer_.clear();
        }
    } else if (state_ == RECORDING) {
        rec_buffer_.insert(rec_buffer_.end(), pcm, pcm + n);
        if (++rec_frames_ >= max_rec_frames_)  // 5 s 上限
            finish_recording();
    }
}

vad_eos 从 500 ms 提到 800 ms 显著减少"说话中间被截"。

6.2 iFlytek 签名 URL:asr_client.c

static char *build_signed_url(void) {
    char date_str[64];
    format_rfc1123(date_str, sizeof(date_str));   // GMT 秒级
    char sig_origin[256];
    snprintf(sig_origin, sizeof(sig_origin),
             "host: %s\ndate: %s\nGET %s HTTP/1.1",
             ASR_HOST, date_str, ASR_PATH);
    uint8_t hmac_out[32];
    crypto_engine_hmac_sha256((uint8_t*)sig_origin, strlen(sig_origin),
                              (uint8_t*)s_xf_api_secret,
                              strlen(s_xf_api_secret), hmac_out);
    // base64 → url-encode → 拼 authorization/date/host
    ...
}

date 必须是当前 UTC,服务器允许 ±5 分钟。SNTP 不同步 = 直接 403。

6.3 连续识别 & 意图分发:voice_assistant.cpp

static void nl_worker_task(void *) {
    nl_task_arg_t msg;
    for (;;) {
        xQueueReceive(s_nl_queue, &msg, portMAX_DELAY);
        char transcript[256] = {0};
        if (asr_client_recognize(msg.pcm, msg.pcm_bytes,
                                 transcript, sizeof(transcript),
                                 12000) != ESP_OK) {
            voice_say("没听清,请再说");
            continue;
        }
        llm_function_call_t local_fcs[LLM_MAX_FUNCTION_CALLS] = {};
        int local_n = local_intent_match_all(transcript, local_fcs,
                                             LLM_MAX_FUNCTION_CALLS);
        if (local_n > 0) {
            for (int i = 0; i < local_n; i++)
                dispatch_function_call(&local_fcs[i], ...);
        } else {
            llm_response_t resp;
            llm_client_query(transcript, &resp, 12000);
            for (int i = 0; i < resp.function_call_count; i++)
                dispatch_function_call(&resp.function_calls[i], ...);
        }
        voice_continue_listening();
    }
}

Worker 常驻、栈静态分配在内部 DRAM(xTaskCreateStaticPinnedToCore),队列深度 1 天然限流。

6.4 SNTP 由 Wi-Fi GOT_IP 直接触发(本次修复)

// time_sync.h
void time_sync_notify_got_ip(void);   // 幂等入口

// wifi_manager.c · IP_EVENT_STA_GOT_IP 分支
s_connected = true;
xEventGroupSetBits(s_wifi_events, WIFI_CONNECTED_BIT);
time_sync_notify_got_ip();            // ← 关键:直接推 SNTP 起来

esp_event_handler_instance_register 在 esp_event_loop_create_default() 之前调用会静默失败(ESP_ERR_INVALID_STATE)。

6.5 中文字体子集生成:scripts/gen_wwtp_cjk_font.py

def collect_chars() -> str:
    chars: set[str] = set(EXTRA)
    for p in MAIN.rglob("*"):
        if p.suffix.lower() not in {".c", ".h", ".cpp", ".hpp"}:
            continue
        text = p.read_text(encoding="utf-8")
        for ch in text:
            o = ord(ch)
            if 0x4E00 <= o <= 0x9FFF:
                chars.add(ch)
    return "".join(sorted(chars, key=ord))

新增 UI 中文文本后必须先跑这个脚本,否则上电就是方块。本次修复实时/历史页文案后子集从 418 → 443 字符。


七、技术难点与解决方案

难点 1:iFlytek 403 → "没听清"死循环

现象:唤醒后 ASR 一直返回 没听清。串口:

W asr: Time not synced, brief SNTP wait (max 2s)...
W asr: Current epoch: 39
I asr: Signing with date: Thu, 01 Jan 1970 00:00:41 GMT
E transport_ws: esp_ws_handshake_status_code=403
E asr: WebSocket connect timeout/error

根因:time_sync_init() 在 app_main 里比 esp_event_loop_create_default() 更早(default loop 藏在 wifi_manager.c 的 ensure_netif_init() 里)。esp_event_handler_instance_register 返回 ESP_ERR_INVALID_STATE 被吞了,GOT_IP 处理器根本没挂上,SNTP 从未启动。

修复:SNTP 启动改由 wifi_manager.c 的 GOT_IP 分支直接函数调用 time_sync_notify_got_ip(),幂等,不依赖 event bus 注册时机。同时把 ASR 里"拿 1970 时间硬签"改成"SNTP 4 秒仍未同步就直接 ESP_ERR_INVALID_STATE 返回"。

验证:重刷后:

I (6872) wifi_mgr: Got IP: 192.168.31.33
I (6872) time_sync: SNTP started
I (11431) time_sync: NTP synced, local time: 2026-09-22 22:32:47
I (220375) asr: Signing with date: Tue, 22 Sep 2026 14:36:16 GMT
I (221325) asr: WebSocket CONNECTED
I (225569) asr: ASR result: 帮我查询溶解氧。
I (226450) tts_mgr: Local TTS: 溶解氧 6.23 毫克每升

难点 2:AFE 打碎内部 DRAM,TLS 建不起来

现象:开机正常,唤醒一次之后,第二次 ASR 就 esp_websocket_client_init 返回 NULL。

排查:Free internal heap: 14019 (largest 4864) — 总剩余够,最大块不够 TLS (~8 KB 连续)。

方案:asr_dram_reserve_hold/release 一对函数:语音 pipeline 一就绪立刻抢 4~12 KB 连续内部 DRAM 存起来,每次 ASR 前释放、握手结束再抢回来。

难点 3:nl_task 崩溃

现象:Guru Meditation Error: Core 0 panic'ed (LoadProhibited) 在 ASR 完成后。

根因:临时 spawn 任务栈放在 PSRAM,但访问了 flash-mmapped 代码。

方案:xTaskCreateStaticPinnedToCore + 静态内部 DRAM 栈 10 KB + 常驻 for-loop。任务永远不死,只吃一次栈开销。

难点 4:LVGL 中文方块

根因:字体子集是扫 main/** 得到的;新增页面文案没跑脚本就少字形。

方案:把 gen_wwtp_cjk_font.py 加入 pre-build 钩子;每次 UI 改动前后各跑一次。

难点 5:现场无 PLC 时的可演示性

data_acquisition.c 双源:Modbus 有响应走真数据 + UI 打"PLC 实时";超时回退仿真 + UI 打"演示数据"。没有现场也能演完整链路。


八、项目总结 & 后续计划

8.1 用下来这块板给我的直观感受

优点

  • RISC-V32 · 320 MHz · Wi-Fi 6 · 16 MB Octal PSRAM 这套组合在"带语音 + 屏幕"的项目里明显够用,我全部塞进这一块 S31,没外接协处理器;

  • ES8389 + 双 MIC + RGB LCD + DVP 摄像头 一板到位,硬件不用另做转接;

  • esp-sr WakeNet9 精度惊人,3 m 内唤醒率 >90%,误唤醒很低;

  • IDF v6.2 对 S31 支持完整,esp32_s31_korvo_1 BSP 开箱即用。

踩坑点

  • 内部 SRAM 真的很紧张,Wi-Fi DMA + LVGL + TLS + FreeRTOS 一起抢,要精心安排"什么放 PSRAM / 什么必须留 DRAM";

  • 事件循环初始化顺序敏感,一旦顺序错,静默失败极难发现;

  • RISC-V 上任务栈放 PSRAM + flash-mmapped 代码的组合仍然会炸,关键任务全用内部 DRAM 栈。

8.2 已达成指标

指标目标实测
唤醒响应<500 ms~320 ms
唤醒 → TTS 首字<6 s~4.8 s
一次会话 ASR 字准率>90%93%
连续无重启24 h36 h 未死
单页 UI 帧率≥15 fps20~25 fps

8.3 后续迭代方向

  1. 双麦波束成形:esp-sr SE 通道打开,噪声现场 5 m 唤醒率提到 85%+;

  2. 本地命令词:multinet 加自定义命令集(停止/继续/重复),断网也能操作;

  3. Modbus 从站回归测试:等真 PLC 到位;

  4. 摄像头 AI:esp-dl / esp-who 做人脸识别或表计读数 OCR;

  5. 多设备组网:MQTT + Matter 桥接,走 S31 的 Thread 支持。



共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]