基于杰理 AC7911 制作 AI 对话机器人全攻略:硬件选型、电路连接与完整代码


目录

  1. 为什么选择 AC7911?——芯片选型深度论证
  2. 芯片核心规格与架构
  3. 完整 BOM 元器件清单
  4. 硬件电路连接详解
  5. 开发环境搭建
  6. SDK 工程结构与代码框架
  7. 完整代码实现
  8. 云端 AI 大模型对接
  9. 调试与测试
  10. 常见问题与避坑指南

一、为什么选择 AC7911?——芯片选型深度论证

1.1 需求分析

我们要做一个 AI 对话机器人,它的核心工作流是:

麦克风拾音

语音唤醒检测

降噪 + VAD 断句

音频编码

Wi-Fi 发送到云端 AI

接收云端 TTS 音频 / 文本

解码并播放

等待下一次唤醒

拆解到芯片层面的需求:

功能需求 芯片能力要求
实时音频采集 ≥1 路 ADC,≥16bit,≥16kHz 采样率
本地唤醒词检测 本地 NN 推理能力,能跑唤醒模型
回声消除/降噪 DSP 处理能力,多麦克风支持
Wi-Fi 联网 2.4G Wi-Fi b/g/n
音频播放 DAC + 功放驱动,支持 TTS 音频流
BLE 配网 BLE 5.0+ 用于手机配网
电池供电 低功耗待机 (< 10μA)
成本控制 单芯片方案,BOM 成本 < ¥20

1.2 候选方案对比

方案 ESP32-S3 AC7911 RTL8720DN ASR6601
CPU Xtensa LX7 RISC-V 双核 ARM M4 RISC-V
主频 240 MHz 320 MHz 200 MHz 160 MHz
Wi-Fi b/g/n b/g/n b/g/n — (需外挂)
BLE 5.0 5.0 双模 5.0
音频 ADC 无内置(需外挂 Codec) 4路 16-bit 内置 1路
DAC 1路 100dB SNR 内置 1路
AI 加速 向量指令 矩阵向量加速器
MIC 放大器 无(需外挂) 4路内置
外挂 Codec 必须 不需要 需要 需要
BOM 成本 ¥12 + Codec ¥5 ¥1.6 芯片 ¥15 + Codec ¥8 + Wi-Fi
休眠功耗 ~7μA < 3μA ~5μA ~2μA
SDK 成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐

1.3 结论:AC7911 为何是最优解

AC7911 是目前市面上唯一能在 ¥2 以内提供"Wi-Fi + BLE + 完整音频子系统 + AI 加速"的单芯片方案。相比之下:

  • ESP32-S3 虽然生态最好,但必须外挂 Codec(如 ES8388),BOM 成本翻倍,PCB 面积增加
  • RTL8720DN 音频能力弱,缺少多麦克风支持
  • ASR6601 根本不含 Wi-Fi,需外挂模组

AC7911 的"全集成"设计使其特别适合 AI 玩具、智能音箱、语音遥控器 等成本敏感又需要完整音频+无线能力的场景。其内置的 矩阵向量加速器 虽然算力只有 3 亿次/秒(8-bit),但对于唤醒词检测、VAD 等轻量推理任务完全够用。


二、芯片核心规格与架构

2.1 AC7911 内部架构

┌─────────────────────────────────────────────────────────────┐
│                        AC7911 内部架构                        │
│                                                             │
│  ┌──────────────────┐   ┌──────────────────┐               │
│  │  RISC Core 0     │   │  RISC Core 1     │               │
│  │  (应用处理)       │   │  (音频/无线)      │               │
│  │  FPU + DSP       │   │  FPU + 向量加速   │               │
│  └────────┬─────────┘   └────────┬─────────┘               │
│           └───────────┬──────────┘                          │
│                       │                                     │
│  ┌────────────────────┼──────────────────────┐              │
│  │             系统总线 (AHB)                  │              │
│  └────────────────────┼──────────────────────┘              │
│                       │                                     │
│  ┌────┬────┬────┬────┼────┬────┬────┬────┬────┐            │
│  │ADC │DAC │I2S │SPI │UART│I2C │USB │PWM │GPIO│            │
│  │x4  │x2  │x1  │x3  │x3  │x1  │OTG │x8  │x28 │            │
│  └────┴────┴────┴────┴────┴────┴────┴────┴────┘            │
│                                                             │
│  ┌──────────────────────────────────────────┐              │
│  │        无线子系统                          │              │
│  │  Wi-Fi 802.11 b/g/n  +  BLE 5.0 双模     │              │
│  │  (AP + STA + BLE 三模同时工作)             │              │
│  └──────────────────────────────────────────┘              │
│                                                             │
│  ┌──────────────────────────────────────────┐              │
│  │   PMU (电源管理)                           │              │
│  │   DC-DC Buck + 多路 LDO + 电池充电管理     │              │
│  └──────────────────────────────────────────┘              │
└─────────────────────────────────────────────────────────────┘

2.2 关键规格速查

维度 参数
封装 QFN48 (6×6mm)
供电 2.2V ~ 5.5V(支持单节锂电池直供)
GPIO 28 个,5V 容忍
内置 Flash 无(外部 SPI Flash 启动)
内置 SDRAM 8MB(部分型号)
音频 SNR DAC 100dB / ADC 94dB
Wi-Fi 灵敏度 -95dBm (DSSS 1Mbps)
BLE 灵敏度 -93dBm
射频发射功率 Wi-Fi 最高 17dBm / BLE 最高 15dBm

三、完整 BOM 元器件清单

以下是一份 可直接采购 的 BOM 清单,所有元器件均可从立创商城(LCSC)或淘宝购买。总成本约 ¥25-35

3.1 核心芯片与存储

序号 元件 型号 封装 数量 单价(约) 说明
1 主控 AC7911BA QFN48 1 ¥1.6 杰理 AI 语音 SoC
2 NOR Flash W25Q64JVSSIQ SOP8 1 ¥2.5 8MB,固件+资源存储
3 PSRAM(可选) APS6404L-3SQR SOP8 1 ¥3.0 8MB,AC7911B8 版本需外挂

💡 选型提示:AC7911BA 内置 SDRAM,无需外挂 PSRAM;若使用 AC7911B8(外置 RAM 版本),则需要增加 PSRAM。

3.2 音频采集(麦克风)

序号 元件 型号 封装 数量 单价(约) 说明
4 MEMS 麦克风 INMP441 LGA 2 ¥4.0 I2S 数字输出,64dB SNR
5 模拟 MEMS 麦 SGM3760ZTS6212 LGA 2 ¥1.5 模拟输出,利用 AC7911 内置 ADC

方案选择说明

方案 A(推荐):INMP441 数字 I2S 麦克风 × 2
  优点:数字信号抗干扰强、无需 Codec、SNR 高
  连接:I2S 接口 (WS/SCK/SD) → AC7911 I2S 引脚
  用途:实现双麦降噪 + 远场拾音

方案 B(低成本):模拟 MEMS 麦克风 × 2  
  优点:成本极低
  连接:直接接 AC7911 MIC 差分输入引脚 (MIC0P/N, MIC1P/N)
  用途:近距离拾音,利用 AC7911 内置 MIC 放大器

3.3 音频输出(喇叭功放)

序号 元件 型号 封装 数量 单价(约) 说明
6 D 类功放 NS4168 MSOP8 1 ¥2.0 3W,I2S 输入,效率 90%
7 AB 类功放 8002A SOP8 1 ¥0.5 3W,模拟输入,电路简单
8 喇叭 3W / 4Ω 或 5W / 8Ω 1 ¥3.0 直径 28-40mm

功放选型建议

功放 优点 缺点 适用场景
NS4168 I2S 数字输入、效率高、发热小 价格略高 电池供电,追求续航
8002A 极低成本、电路简单 效率低、发热大 插电使用,成本敏感

3.4 电源管理

序号 元件 型号 封装 数量 单价(约) 说明
9 电池充电 IC TP4056 SOP8 1 ¥0.5 单节锂电池充电,1A
10 LDO 3.3V AMS1117-3.3 SOT223 1 ¥0.3 500mA 输出
11 锂电池 603040 (3.7V 800mAh) 1 ¥5.0 带保护板
12 Micro USB 母座 5P 直插 1 ¥0.3 充电+调试

💡 AC7911 内置 PMU 支持 2.2-5.5V 宽压输入,理论上可直接接锂电池。但为了稳定性和保护,建议加一颗 AMS1117-3.3 做稳压。

3.5 射频与时钟

序号 元件 型号 封装 数量 单价(约) 说明
13 2.4G 天线 板载 PCB 天线或陶瓷天线 1 ¥0.5 或预留 IPEX 座子
14 24MHz 晶振 3225 贴片 3225 1 ¥0.5 主时钟,±10ppm
15 32.768kHz 晶振 3215 贴片 3215 1 ¥0.3 RTC 时钟
16 24MHz 负载电容 12pF 或 22pF 0402 2 ¥0.02
17 32.768k 负载电容 12.5pF 0402 2 ¥0.02

3.6 人机交互

序号 元件 型号 封装 数量 单价(约) 说明
18 轻触按键 6×6×5mm DIP 3 ¥0.2 唤醒键 / 音量+ / 音量-
19 RGB LED WS2812B-5050 5050 2 ¥0.5 状态指示(可编程)
20 普通 LED 0805 红/蓝 0805 2 ¥0.05 更简单的状态指示

3.7 配套阻容与连接器

序号 元件 型号 封装 数量 单价(约) 说明
21 电容 10μF / 100nF / 22pF 0402/0603 若干 去耦+滤波
22 电阻 10kΩ / 1kΩ / 4.7kΩ 0402/0603 若干 上拉+限流
23 排针 2.54mm 单排 2 排 ¥0.3 调试/烧录接口

3.8 BOM 成本汇总

类别 成本
核心芯片 (AC7911 + Flash) ¥4.1
音频 (双麦 + 功放 + 喇叭) ¥10.0
电源 (LDO + 充电 + 电池) ¥6.1
射频 (天线 + 晶振 × 2) ¥1.3
交互 (按键 + LED) ¥0.8
其他 (电容电阻排针) ¥3.0
合计 约 ¥25.3

四、硬件电路连接详解

4.1 系统连接总览

                    ┌───────────────────────────────────────┐
                    │              AC7911BA (QFN48)           │
                    │                                        │
    W25Q64 ─────────┤ SPI_FLASH (PB2/PB3/PB4/PB5)            │
                    │                                        │
    INMP441#1 ──────┤ I2S_DO  (PA4)                          │
                    │ I2S_WS  (PA5)     ┌─────┐             │
    INMP441#2 ──────┤ I2S_DO2 (PA6)    │     │             │
                    │ I2S_SCK (PA7)    │ DSP │             │
                    │                   │ 处  │             │
    NS4168 ─────────┤ I2S_DAC0          │ 理  │             │
                    │ I2S_DAC1          └─────┘             │
                    │                                        │
    TP4056 + ───────┤ VDD_IO (3.3V)                          │
    锂电池           │ VDD_CORE (1.2V, 内部 LDO)              │
                    │                                        │
    按键 ───────────┤ PA0 (唤醒), PA1 (音量), PA2 (功能)     │
    LED  ───────────┤ PA8 (状态)                             │
    天线 ───────────┤ RF_ANT                                 │
                    │                                        │
    UART0 ──────────┤ PA10(TX)/PA11(RX) → USB-TTL → PC调试   │
                    └───────────────────────────────────────┘

4.2 Flash 连接(SPI 接口)

AC7911 从外部 SPI Flash 启动,这是最关键的连接,接错则芯片无法启动

W25Q64JV (SOP8)          AC7911 (QFN48)
    ┌───┐
1  ─┤CS │─────────────── PB2  (SPI_CS)
2  ─┤DO │─────────────── PB3  (SPI_DO / MISO)
3  ─┤WP │─────────────── 3.3V (写保护引脚拉高)
4  ─┤GND│─────────────── GND
5  ─┤DI │─────────────── PB4  (SPI_DI / MOSI)
6  ─┤CLK│─────────────── PB5  (SPI_CLK)
7  ─┤HOLD│────────────── 3.3V (HOLD 引脚拉高)
8  ─┤VCC│─────────────── 3.3V
    └───┘

电源去耦:VCC 与 GND 之间并联 100nF + 10μF 电容
信号线:CS/CLK/DI/DO 建议串联 22Ω 终端电阻(放在靠近 Flash 侧)

⚠️ 关键提示:Flash 的 CS 引脚必须接对,不同版本的 AC7911 的 SPI Flash 引脚可能映射到不同 GPIO。请以具体型号的 SDK Pinmux 表为准!

4.3 数字麦克风 INMP441 连接(I2S 接口)

INMP441 #1 (主麦,正面)                    AC7911
    ┌──────┐
1  ─┤L/R  │──── GND (左声道)               —
2  ─┤WS   │──────────────────────────────  PA5  (I2S_WS, 帧时钟)
3  ─┤SCK  │──────────────────────────────  PA7  (I2S_SCK, 位时钟)
4  ─┤SD   │──────────────────────────────  PA4  (I2S_DI, 数据输入)
5  ─┤VDD  │──── 3.3V (加 100nF 去耦)       —
6  ─┤GND  │──── GND                        —
    └──────┘

INMP441 #2 (副麦,背面,用于双麦降噪)       AC7911
    ┌──────┐
1  ─┤L/R  │──── 3.3V (右声道)              —
2  ─┤WS   │──── PA5 (与 #1 共用 WS)        —
3  ─┤SCK  │──── PA7 (与 #1 共用 SCK)       —
4  ─┤SD   │──────────────────────────────  PA6  (I2S_DI2, 副麦数据)
5  ─┤VDD  │──── 3.3V                       —
6  ─┤GND  │──── GND                        —
    └──────┘

I2S 参数配置:
  - 采样率:16kHz(语音识别最优)
  - 位宽:24-bit
  - 格式:I2S (Philips) 标准
  - 两根数据线(主麦+副麦)共享 WS 和 SCK

4.4 功放与喇叭连接

方案 A:NS4168(数字 I2S 输入 D 类功放,推荐)
NS4168 (MSOP8)              AC7911
    ┌───┐
1  ─┤BCK│──────────────── PA5  (I2S_SCK)
2  ─┤WS │──────────────── PA5  (I2S_WS)
3  ─┤DIN│──────────────── PA15 (I2S_DAC0)
4  ─┤GND│──────── GND
5  ─┤VDD│──────── 5V (电池直供)

                        喇叭
6 ─┤OUTP│─────────────── [+]
7 ─┤OUTN│─────────────── [-]
8 ─┤NC  │
    └───┘

VDD 去耦:5V → 100nF + 100μF → GND
输出滤波:OUTP/OUTN 各串磁珠 (FB) + 对地 1nF 电容(可选)
方案 B:8002A(模拟输入 AB 类功放,低成本)
8002A (SOP8)                AC7911
    ┌───┐
1  ─┤SHDN│──────── 3.3V (常使能)
2  ─┤BYP │──── 1μF ── GND (旁路电容)
3  ─┤IN+ │──── 1μF ── AC7911 DAC 输出 (PA12)

                        喇叭
4 ─┤IN- │───── 1μF ── GND
5 ─┤VO1 │──────────────── [Speaker +]
6 ─┤VDD │──── 5V
7 ─┤GND │──── GND
8 ─┤VO2 │──────────────── [Speaker -]
    └───┘

4.5 电源电路

USB 5V ───┬─────── TP4056 ─────── 锂电池 3.7V
           │         │
           │      BAT+ ─┬──── AMS1117-3.3 ──── 3.3V (IO + 外设)
           │            │
           │            └──── 直接供电 AC7911 VDD_IO (2.2~5.5V 宽压)
           │
           └──────────────────── 5V 供 NS4168 功放

TP4056 充电部分:
  5V ──[1]VCC    CE[8]── 悬空 (使能)
  GND ─[2]GND   CHRG[7]── 1kΩ ── LED(红) ── 5V   (充电指示)
  电池 ─[3]BAT  STDBY[6]── 1kΩ ── LED(绿) ── 5V   (充满指示)
  ──[4]TEMP   PROG[5]── 1.2kΩ ── GND             (充电电流 = 1000V/Rprog ≈ 833mA)

  电池反接保护:BAT 引脚串联 0.5Ω 保险电阻
  输入去耦:VCC 对地 10μF + 100nF
  输出去耦:BAT 对地 10μF + 100nF

4.6 按键与 LED

按键 (共 3 个):
  ┌────┐
  │ K1 │──── PA0 ───┐  (唤醒/对讲键,短按唤醒,长按对话)
  │ K2 │──── PA1 ───┤  (音量+)
  │ K3 │──── PA2 ───┘  (音量-)
  └────┘
  
  按键另一端接 GND
  每个 IO 引脚内部使能上拉 (默认高电平,按下为低)

LED (共 2 个):
  PA8 ──── 1kΩ ──── LED(蓝) ──── GND   (Wi-Fi 连接状态)
  PB0 ──── 1kΩ ──── LED(绿) ──── GND   (AI 对话中状态)
    
  或使用 WS2812B (可编程单总线 RGB):
  PA9 ─────────────────────── WS2812B DIN → DOUT → WS2812B#2 DIN

4.7 烧录/调试接口

UART0 烧录 & 调试 (2.54mm 排针 4P):
  ┌──────┐
1 │ 3.3V │──── 可不接(板子自供电)
2 │ TX   │──── PA10 (AC7911 UART0 TX) → USB-TTL RX
3 │ RX   │──── PA11 (AC7911 UART0 RX) → USB-TTL TX
4 │ GND  │──── GND → USB-TTL GND
  └──────┘

烧录时操作:
  1. 断开电池
  2. USB-TTL 插入电脑
  3. 短接 Flash CS(PB2) 到 GND → 插入电池 → 松开 → 芯片进入烧录模式
  4. 使用杰理专用烧录工具下载固件
  5. 重新上电运行

五、开发环境搭建

5.1 获取开发工具链

工具 下载地址 说明
Codeblocks IDE 杰理 SDK 中附带 AC7911 官方推荐的开发环境
杰理编译工具链 doc.zh-jieli.com RISC-V GCC 交叉编译链
AC7911 SDK 联系杰理代理获取 约 3-5GB,含完整源码和文档
烧录工具 SDK 中 dev_tools/ 目录 杰理自研烧录上位机

5.2 环境变量配置

# Windows 环境变量设置 (以实际路径为准)
set JL_TOOLCHAIN=C:\JL\toolchain\riscv-elf-gcc\bin
set JL_SDK=C:\JL\AC7911_SDK_V2.0
set PATH=%JL_TOOLCHAIN%;%PATH%

# 验证
riscv-elf-gcc --version

5.3 SDK 目录结构

AC7911_SDK/
├── apps/                    # 应用层
│   └── ai_speaker/          # 我们的 AI 对话应用
│       ├── app_main.c       # 主入口
│       ├── wifi/            # Wi-Fi 配网与连接
│       ├── audio/           # 音频采集/编码/播放
│       ├── ai/              # AI 协议对接
│       ├── key/             # 按键处理
│       └── led/             # LED 状态
├── cpu/                     # 芯片底层驱动
│   └── ac7911/              
│       ├── include/         # 寄存器定义与外设接口
│       └── lib/             # 预编译库文件
├── libs/                    # 通用库
│   ├── lwip/                # TCP/IP 协议栈
│   ├── mbedtls/             # TLS 加密
│   ├── freertos/            # 实时操作系统
│   └── audio_codec/         # 音频编解码库
├── tools/                   # 辅助工具
├── Makefile                 # 顶层 Makefile
└── build.sh                 # 编译脚本

六、SDK 工程结构与代码框架

6.1 整体软件架构

驱动层

中间件层

AI语音层

应用层

app_main 主控状态机

按键检测任务

LED 指示任务

语音唤醒引擎

音频采集/编码

云端通信模块

音频播放模块

FreeRTOS

LwIP TCP/IP

mbedTLS

音频 Codec OPUS/PCM

Wi-Fi 驱动

BLE 驱动

I2S 音频驱动

SPI Flash 驱动

GPIO/UART 驱动

6.2 主状态机设计

上电

初始化完成

按键/上电自动联网

联网成功

联网失败

重试

语音唤醒检测到关键词

VAD 检测到语音

语音结束(VAD静音)

数据发送完成

收到AI回复

播放完毕,继续监听

超时未检测到语音

超时无回复

INIT

IDLE

WIFI_CONNECTING

WIFI_CONNECTED

WIFI_FAIL

WAKEUP_LISTENING

RECORDING

UPLOADING

WAITING_AI

PLAYING


七、完整代码实现

说明:以下代码基于 AC7911 SDK V2.0 框架编写,展示了 AI 对话机器人的完整实现。由于杰理 SDK 的底层驱动以 .a 静态库形式提供,实际开发时部分底层 API 需查阅 SDK 文档。代码中的函数名以 jl_ 前缀表示杰理 SDK 提供的 API。

7.1 主程序入口 app_main.c

/**
 * @file    app_main.c
 * @brief   AI 对话机器人主程序
 * @author  GD32VW553 学习笔记
 * @date    2025
 * 
 * 功能描述:
 *   1. 系统初始化(时钟、Flash、FreeRTOS)
 *   2. 创建各功能任务
 *   3. 管理全局状态机
 */

#include "includes.h"
#include "app_config.h"
#include "wifi_manager.h"
#include "audio_capture.h"
#include "audio_player.h"
#include "wakeup_engine.h"
#include "ai_cloud.h"
#include "key_driver.h"
#include "led_driver.h"

/* ========== 全局变量 ========== */
static sys_state_t    g_sys_state = STATE_INIT;
static TaskHandle_t   g_main_task_handle = NULL;

/* ========== 日志宏 ========== */
#define TAG "AI-SPEAKER"
#define LOGI(fmt, ...)  printf("[%s][I] " fmt "\r\n", TAG, ##__VA_ARGS__)
#define LOGE(fmt, ...)  printf("[%s][E] " fmt "\r\n", TAG, ##__VA_ARGS__)
#define LOGD(fmt, ...)  printf("[%s][D] " fmt "\r\n", TAG, ##__VA_ARGS__)

/* ========== 系统初始化 ========== */
static void system_init(void)
{
    LOGI("=== AI 对话机器人启动 ===");
    LOGI("芯片: AC7911, SDK Version: %s", SDK_VERSION);

    /* 1. 时钟初始化 (320MHz) */
    jl_clock_init(SYS_CLOCK_320M);

    /* 2. 中断优先级分组 */
    jl_irq_priority_group_set(4);

    /* 3. 看门狗初始化 (3秒超时) */
    jl_wdt_init(3000);

    /* 4. GPIO 默认状态 */
    jl_gpio_init();

    /* 5. Flash 驱动初始化 */
    jl_spi_flash_init();

    /* 6. 启动 FreeRTOS 内核 */
    LOGI("FreeRTOS 内核启动...");
    vTaskStartScheduler();

    /* 不应该运行到这里 */
    while (1);
}

/* ========== 主任务 ========== */
static void main_task(void *param)
{
    LOGI("主任务启动, 堆栈剩余: %u", uxTaskGetStackHighWaterMark(NULL));

    /* 初始化各模块 */
    led_init();                         /* LED 指示 */
    key_init();                         /* 按键检测 */
    audio_capture_init();               /* 麦克风采集 */
    audio_player_init();                /* 喇叭播放 */
    wakeup_engine_init();               /* 语音唤醒引擎 */
    wifi_manager_init();                /* Wi-Fi 管理 */
    ai_cloud_init();                    /* 云端 AI 对接 */

    LOGI("所有模块初始化完成");
    g_sys_state = STATE_IDLE;

    /* 主循环 */
    while (1) {
        /* 喂狗 */
        jl_wdt_feed();

        /* 状态机处理 */
        switch (g_sys_state) {
        case STATE_IDLE:
            /* 空闲:等待按键或唤醒词 */
            break;

        case STATE_WIFI_CONNECTING:
            /* Wi-Fi 连接中 */
            if (wifi_is_connected()) {
                g_sys_state = STATE_WIFI_CONNECTED;
                led_set_pattern(LED_PATTERN_WIFI_OK);
                LOGI("Wi-Fi 已连接");
            }
            break;

        case STATE_WIFI_CONNECTED:
            /* Wi-Fi 已连接:启动唤醒监听 */
            wakeup_engine_start();
            g_sys_state = STATE_WAKEUP_LISTENING;
            LOGI("进入唤醒监听模式");
            break;

        case STATE_WAKEUP_LISTENING:
            /* 等待唤醒词... */
            break;

        case STATE_RECORDING:
            /* 正在录音采集中... */
            break;

        case STATE_UPLOADING:
            /* 音频数据上传中... */
            break;

        case STATE_WAITING_AI:
            /* 等待 AI 回复... */
            break;

        case STATE_PLAYING:
            /* 正在播放 AI 回复... */
            break;

        default:
            break;
        }

        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

/* ========== 状态切换函数 ========== */
void sys_state_change(sys_state_t new_state)
{
    LOGI("状态切换: %d -> %d", g_sys_state, new_state);
    g_sys_state = new_state;
}

sys_state_t sys_state_get(void)
{
    return g_sys_state;
}

/* ========== 程序入口 ========== */
int app_main(void)
{
    /* 创建主任务 */
    if (xTaskCreate(main_task, "main_task",
                    2048,           /* 堆栈 2KB */
                    NULL,
                    3,              /* 优先级 3 */
                    &g_main_task_handle) != pdPASS) {
        LOGE("主任务创建失败!");
        return -1;
    }

    system_init();
    return 0;
}

7.2 Wi-Fi 管理模块 wifi_manager.c

/**
 * @file    wifi_manager.c
 * @brief   Wi-Fi 连接管理
 * 
 * 功能:
 *   - 支持 SmartConfig / 微信 AirKiss 配网
 *   - 支持预设 SSID 直连
 *   - 断线自动重连
 *   - Wi-Fi 状态事件回调
 */

#include "wifi_manager.h"
#include "app_config.h"

#define TAG "WIFI"

/* ========== Wi-Fi 配置(从 app_config.h 读取) ========== */
#ifndef WIFI_SSID
#define WIFI_SSID      "YOUR_WIFI_SSID"       /* 默认 Wi-Fi 名称 */
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD  "YOUR_WIFI_PASSWORD"   /* 默认 Wi-Fi 密码 */
#endif

#define WIFI_RETRY_MAX      5       /* 最大重试次数 */
#define WIFI_RETRY_INTERVAL 5000    /* 重试间隔 (ms) */

/* ========== 内部变量 ========== */
static wifi_state_t g_wifi_state = WIFI_STATE_DISCONNECTED;
static uint8_t      g_retry_count = 0;
static TimerHandle_t g_reconnect_timer = NULL;

/* ========== Wi-Fi 事件回调 ========== */
static void wifi_event_callback(int event, void *arg)
{
    switch (event) {
    case WIFI_EVENT_CONNECTED:
        LOGI("[WIFI] 已连接到 %s", WIFI_SSID);
        g_wifi_state = WIFI_STATE_CONNECTED;
        g_retry_count = 0;

        /* 获取 IP 地址 */
        uint32_t ip = jl_wifi_get_ip();
        LOGI("[WIFI] IP: %d.%d.%d.%d",
             (ip >> 0) & 0xFF, (ip >> 8) & 0xFF,
             (ip >> 16) & 0xFF, (ip >> 24) & 0xFF);
        break;

    case WIFI_EVENT_DISCONNECTED:
        LOGI("[WIFI] 连接断开, 原因: %d", (int)arg);
        g_wifi_state = WIFI_STATE_DISCONNECTED;

        /* 自动重连 */
        if (g_retry_count < WIFI_RETRY_MAX) {
            g_retry_count++;
            LOGI("[WIFI] 第 %d 次重连...", g_retry_count);
            jl_wifi_connect(WIFI_SSID, WIFI_PASSWORD);
        } else {
            LOGE("[WIFI] 重连失败, 已达最大重试次数");
            g_wifi_state = WIFI_STATE_FAILED;
        }
        break;

    case WIFI_EVENT_SCAN_DONE:
        LOGI("[WIFI] AP 扫描完成");
        break;

    case WIFI_EVENT_GOT_IP:
        LOGI("[WIFI] DHCP 获取 IP 成功");
        break;

    default:
        break;
    }
}

/* ========== Wi-Fi 初始化 ========== */
int wifi_manager_init(void)
{
    LOGI("[WIFI] 初始化 Wi-Fi 模块...");

    /* 注册事件回调 */
    jl_wifi_event_cb_register(wifi_event_callback);

    /* 配置 Wi-Fi 模式为 STA */
    jl_wifi_mode_set(WIFI_MODE_STA);

    /* 配置省电模式(DTIM=3,平衡功耗与响应速度) */
    jl_wifi_power_save_set(WIFI_PS_DTIM, 3);

    /* 尝试连接 */
    g_wifi_state = WIFI_STATE_CONNECTING;
    int ret = jl_wifi_connect(WIFI_SSID, WIFI_PASSWORD);

    if (ret != 0) {
        LOGE("[WIFI] 首次连接失败, 错误码: %d", ret);
        g_wifi_state = WIFI_STATE_FAILED;
        return -1;
    }

    return 0;
}

/* ========== 查询 Wi-Fi 状态 ========== */
bool wifi_is_connected(void)
{
    return (g_wifi_state == WIFI_STATE_CONNECTED);
}

wifi_state_t wifi_state_get(void)
{
    return g_wifi_state;
}

/* ========== 强制重连 ========== */
int wifi_reconnect(void)
{
    g_retry_count = 0;
    g_wifi_state = WIFI_STATE_CONNECTING;
    return jl_wifi_connect(WIFI_SSID, WIFI_PASSWORD);
}

7.3 音频采集模块 audio_capture.c

/**
 * @file    audio_capture.c
 * @brief   麦克风音频采集
 * 
 * 采集流程:
 *   1. 配置 I2S 接口,接收 INMP441 数字麦克风数据
 *   2. DMA 双缓冲乒乓采集
 *   3. 每采集满一帧 (40ms) 数据,回调给上层
 *   4. 支持 16kHz / 16bit / 单声道
 */

#include "audio_capture.h"

#define TAG "AUDIO-CAP"

/* ========== 音频参数 ========== */
#define AUDIO_SAMPLE_RATE    16000    /* 16kHz 采样率 */
#define AUDIO_BIT_WIDTH      16       /* 16-bit */
#define AUDIO_CHANNELS       1        /* 单声道(降噪后合并为单声道) */
#define AUDIO_FRAME_MS       40       /* 每帧 40ms */
#define AUDIO_FRAME_SIZE     (AUDIO_SAMPLE_RATE * AUDIO_BIT_WIDTH / 8 * AUDIO_FRAME_MS / 1000)
                                      /* = 16000*2*40/1000 = 1280 bytes */

#define DMA_BUF_COUNT        4        /* DMA 缓冲数 */
#define DMA_BUF_SIZE         AUDIO_FRAME_SIZE

/* ========== 数据结构 ========== */
typedef struct {
    uint8_t  data[DMA_BUF_SIZE];      /* 音频数据 */
    uint16_t len;                      /* 实际长度 */
    uint32_t timestamp_ms;             /* 时间戳 */
} audio_frame_t;

typedef void (*audio_frame_cb_t)(audio_frame_t *frame);

/* ========== 内部变量 ========== */
static audio_frame_t     g_dma_buf[DMA_BUF_COUNT];
static uint8_t           g_active_buf = 0;
static bool              g_capturing = false;
static audio_frame_cb_t  g_frame_callback = NULL;

/* ========== I2S 初始化 ========== */
static int i2s_mic_init(void)
{
    i2s_config_t i2s_cfg;

    /* I2S 配置 */
    i2s_cfg.mode        = I2S_MODE_MASTER_RX;   /* 主模式接收 */
    i2s_cfg.sample_rate = AUDIO_SAMPLE_RATE;
    i2s_cfg.bits        = I2S_BITS_24;          /* INMP441 输出 24-bit */
    i2s_cfg.format      = I2S_FORMAT_STANDARD;  /* 标准 I2S 格式 */
    i2s_cfg.channel     = I2S_CHANNEL_STEREO;   /* 双声道(双麦) */
    i2s_cfg.dma_buf_count = DMA_BUF_COUNT;
    i2s_cfg.dma_buf_len   = DMA_BUF_SIZE;

    /* 引脚映射 */
    i2s_cfg.pin_bck = IO_PORT_GROUP_A, 7;   /* PA7 = I2S_SCK */
    i2s_cfg.pin_ws  = IO_PORT_GROUP_A, 5;   /* PA5 = I2S_WS */
    i2s_cfg.pin_din0 = IO_PORT_GROUP_A, 4;  /* PA4 = I2S_SD (主麦) */
    i2s_cfg.pin_din1 = IO_PORT_GROUP_A, 6;  /* PA6 = I2S_SD2 (副麦) */

    return jl_i2s_init(&i2s_cfg);
}

/* ========== DMA 中断回调 ========== */
static void audio_dma_callback(uint8_t *data, uint32_t len)
{
    if (!g_capturing) return;

    /* 将 DMA 数据拷贝到当前帧缓冲 */
    audio_frame_t *frame = &g_dma_buf[g_active_buf];
    memcpy(frame->data, data, (len < DMA_BUF_SIZE) ? len : DMA_BUF_SIZE);
    frame->len = (len < DMA_BUF_SIZE) ? len : DMA_BUF_SIZE;
    frame->timestamp_ms = jl_get_system_tick_ms();

    /* 切换乒乓缓冲 */
    g_active_buf = (g_active_buf + 1) % DMA_BUF_COUNT;

    /* 回调通知上层 */
    if (g_frame_callback) {
        g_frame_callback(frame);
    }
}

/* ========== 公开接口 ========== */

int audio_capture_init(void)
{
    LOGI("[AUDIO] 初始化音频采集...");

    memset(g_dma_buf, 0, sizeof(g_dma_buf));
    g_active_buf = 0;
    g_capturing = false;

    /* 初始化 I2S */
    if (i2s_mic_init() != 0) {
        LOGE("[AUDIO] I2S 初始化失败!");
        return -1;
    }

    /* 注册 DMA 回调 */
    jl_i2s_dma_callback_register(audio_dma_callback);

    LOGI("[AUDIO] 音频采集初始化成功");
    LOGI("[AUDIO] 参数: %dHz/%dbit/%dch, 帧长 %dms(%dB)",
         AUDIO_SAMPLE_RATE, AUDIO_BIT_WIDTH, AUDIO_CHANNELS,
         AUDIO_FRAME_MS, AUDIO_FRAME_SIZE);

    return 0;
}

int audio_capture_start(audio_frame_cb_t callback)
{
    if (g_capturing) {
        LOGE("[AUDIO] 采集已在进行中");
        return -1;
    }

    g_frame_callback = callback;
    g_capturing = true;

    /* 使能 I2S DMA */
    jl_i2s_start();

    LOGI("[AUDIO] 开始采集");
    return 0;
}

int audio_capture_stop(void)
{
    if (!g_capturing) return 0;

    g_capturing = false;
    jl_i2s_stop();

    LOGI("[AUDIO] 停止采集");
    return 0;
}

bool audio_capture_is_running(void)
{
    return g_capturing;
}

/* ========== 双麦降噪处理(简易 Delay-and-Sum 波束成形) ========== */
void audio_beamforming_2mic(int16_t *mic1, int16_t *mic2, 
                             int16_t *output, int sample_count)
{
    /* Delay-and-Sum 波束成形:
     *   output[i] = (mic1[i] + mic2[i]) / 2
     * 
     * 更高级的方案可以用 AC7911 内置的 DSP 指令做 LMS 自适应滤波
     */
    for (int i = 0; i < sample_count; i++) {
        output[i] = (int16_t)(((int32_t)mic1[i] + (int32_t)mic2[i]) >> 1);
    }
}

7.4 音频播放模块 audio_player.c

/**
 * @file    audio_player.c
 * @brief   喇叭音频播放
 * 
 * 播放流程:
 *   1. 接收 PCM 数据或 MP3/OPUS 压缩数据
 *   2. 解码(如需要)
 *   3. 通过 I2S 发送给 NS4168 D 类功放
 *   4. DMA + 双缓冲平滑播放
 */

#include "audio_player.h"

#define TAG "AUDIO-PLAY"

/* ========== 播放参数 ========== */
#define PLAY_SAMPLE_RATE     16000
#define PLAY_BIT_WIDTH       16
#define PLAY_CHANNELS        1
#define PLAY_BUF_SIZE        2560      /* 80ms 缓冲 */
#define PLAY_DMA_BUF_COUNT   4

/* ========== 内部变量 ========== */
static bool         g_playing = false;
static uint8_t      g_play_buf[PLAY_DMA_BUF_COUNT][PLAY_BUF_SIZE];
static uint8_t      g_buf_idx_wr = 0;   /* 写指针 */
static uint8_t      g_buf_idx_rd = 0;   /* 读指针 */
static uint32_t     g_total_played = 0;  /* 已播放字节数 */
static play_finish_cb_t g_finish_cb = NULL;

/* ========== I2S DAC 初始化 ========== */
static int i2s_dac_init(void)
{
    i2s_config_t i2s_cfg;

    i2s_cfg.mode        = I2S_MODE_MASTER_TX;  /* 主模式发送 */
    i2s_cfg.sample_rate = PLAY_SAMPLE_RATE;
    i2s_cfg.bits        = I2S_BITS_16;
    i2s_cfg.format      = I2S_FORMAT_STANDARD;
    i2s_cfg.channel     = I2S_CHANNEL_MONO;
    i2s_cfg.dma_buf_count = PLAY_DMA_BUF_COUNT;
    i2s_cfg.dma_buf_len   = PLAY_BUF_SIZE;

    /* 引脚映射到 NS4168 */
    i2s_cfg.pin_bck  = IO_PORT_GROUP_A, 7;   /* PA7 = I2S_SCK (复用) */
    i2s_cfg.pin_ws   = IO_PORT_GROUP_A, 5;   /* PA5 = I2S_WS (复用) */
    i2s_cfg.pin_dout = IO_PORT_GROUP_A, 15;  /* PA15 = I2S_DAC */

    return jl_i2s_dac_init(&i2s_cfg);
}

/* ========== DMA 发送完成回调 ========== */
static void audio_dma_tx_done_callback(void)
{
    if (!g_playing) return;

    /* 发送下一帧 */
    if (g_buf_idx_rd != g_buf_idx_wr) {
        jl_i2s_dac_write(g_play_buf[g_buf_idx_rd], PLAY_BUF_SIZE);
        g_total_played += PLAY_BUF_SIZE;
        g_buf_idx_rd = (g_buf_idx_rd + 1) % PLAY_DMA_BUF_COUNT;
    } else {
        /* 缓冲区空,播放结束 */
        g_playing = false;
        jl_i2s_dac_stop();
        LOGI("[AUDIO] 播放完成, 总计: %u bytes", g_total_played);
        if (g_finish_cb) {
            g_finish_cb();
        }
    }
}

/* ========== 公开接口 ========== */

int audio_player_init(void)
{
    LOGI("[AUDIO] 初始化音频播放...");

    memset(g_play_buf, 0, sizeof(g_play_buf));
    g_buf_idx_wr = g_buf_idx_rd = 0;
    g_playing = false;
    g_total_played = 0;

    if (i2s_dac_init() != 0) {
        LOGE("[AUDIO] I2S DAC 初始化失败!");
        return -1;
    }

    jl_i2s_dac_tx_done_register(audio_dma_tx_done_callback);

    LOGI("[AUDIO] 音频播放初始化成功");
    return 0;
}

int audio_player_play(uint8_t *data, uint32_t len, play_finish_cb_t finish_cb)
{
    if (g_playing) {
        LOGD("[AUDIO] 播放忙, 先停止当前播放");
        audio_player_stop();
    }

    g_finish_cb = finish_cb;
    g_playing = true;
    g_total_played = 0;

    /* 填充数据到 DMA 缓冲区 */
    uint32_t offset = 0;
    while (offset < len) {
        uint32_t chunk = (len - offset) > PLAY_BUF_SIZE 
                         ? PLAY_BUF_SIZE : (len - offset);
        
        memcpy(g_play_buf[g_buf_idx_wr], data + offset, chunk);
        /* 不足一帧时填充静音 */
        if (chunk < PLAY_BUF_SIZE) {
            memset(g_play_buf[g_buf_idx_wr] + chunk, 0, PLAY_BUF_SIZE - chunk);
        }
        
        g_buf_idx_wr = (g_buf_idx_wr + 1) % PLAY_DMA_BUF_COUNT;
        offset += chunk;
    }

    /* 启动播放 */
    jl_i2s_dac_write(g_play_buf[g_buf_idx_rd], PLAY_BUF_SIZE);
    g_buf_idx_rd = (g_buf_idx_rd + 1) % PLAY_DMA_BUF_COUNT;
    jl_i2s_dac_start();

    return 0;
}

int audio_player_stop(void)
{
    g_playing = false;
    jl_i2s_dac_stop();
    g_buf_idx_wr = g_buf_idx_rd = 0;
    return 0;
}

bool audio_player_is_playing(void)
{
    return g_playing;
}

int audio_player_volume_set(uint8_t vol)
{
    /* 0-100% → DAC 增益调节 */
    return jl_i2s_dac_volume_set((uint32_t)vol * 255 / 100);
}

7.5 语音唤醒引擎 wakeup_engine.c

/**
 * @file    wakeup_engine.c
 * @brief   本地语音唤醒词检测
 * 
 * 原理:
 *   - 使用 AC7911 内置的矩阵向量加速器运行轻量级 NN 模型
 *   - 模型:基于 DS-CNN (Depthwise Separable CNN) 的关键词检测
 *   - 唤醒词:"小杰小杰"(可在 SDK 中定制)
 *   - 配合 VAD (Voice Activity Detection) 判断语音起始与结束
 */

#include "wakeup_engine.h"
#include "audio_capture.h"

#define TAG "WAKEUP"

/* ========== 唤醒参数 ========== */
#define WAKEUP_MODEL_SIZE     (64 * 1024)   /* 模型大小 64KB */
#define WAKEUP_WINDOW_MS      1500          /* 1.5s 滑动窗口 */
#define VAD_SILENCE_MS        800           /* 800ms 静音判为结束 */
#define VAD_ENERGY_THRESHOLD  500           /* 能量阈值 */

/* ========== 内部状态 ========== */
typedef enum {
    WK_STATE_IDLE = 0,          /* 空闲 */
    WK_STATE_LISTENING,         /* 监听中 */
    WK_STATE_WAKEUP_DETECTED,   /* 检测到唤醒词 */
} wk_state_t;

static wk_state_t      g_wk_state = WK_STATE_IDLE;
static uint8_t        *g_wakeup_model = NULL;
static uint32_t        g_vad_silence_cnt = 0;
static wakeup_cb_t     g_wakeup_cb = NULL;
static vad_cb_t        g_vad_cb = NULL;

/* ========== 加载唤醒模型 ========== */
static int wakeup_model_load(void)
{
    /* 从 Flash 的固定地址加载唤醒词模型 */
    #define WAKEUP_MODEL_FLASH_ADDR  0x600000

    g_wakeup_model = (uint8_t *)malloc(WAKEUP_MODEL_SIZE);
    if (!g_wakeup_model) {
        LOGE("[WAKEUP] 模型内存分配失败!");
        return -1;
    }

    jl_flash_read(WAKEUP_MODEL_FLASH_ADDR, g_wakeup_model, WAKEUP_MODEL_SIZE);

    /* 初始化 AC7911 矩阵向量加速器并加载模型 */
    jl_nn_accelerator_init();
    jl_nn_model_load(g_wakeup_model, WAKEUP_MODEL_SIZE);

    LOGI("[WAKEUP] 模型加载成功, 大小: %d bytes", WAKEUP_MODEL_SIZE);
    return 0;
}

/* ========== 简易 VAD (基于短时能量) ========== */
static bool vad_detect_voice(int16_t *samples, int count)
{
    int64_t energy = 0;
    for (int i = 0; i < count; i++) {
        energy += (int64_t)samples[i] * samples[i];
    }
    energy /= count;  /* 平均能量 */

    return (energy > VAD_ENERGY_THRESHOLD);
}

/* ========== 音频帧回调 ========== */
static void on_audio_frame(audio_frame_t *frame)
{
    int16_t *samples = (int16_t *)frame->data;
    int sample_count = frame->len / sizeof(int16_t);

    bool has_voice = vad_detect_voice(samples, sample_count);

    switch (g_wk_state) {
    case WK_STATE_IDLE:
        break;

    case WK_STATE_LISTENING:
        /* 持续将音频喂给 NN 做唤醒词检测 */
        float score = jl_nn_kws_detect(samples, sample_count);

        if (score > 0.85f) {  /* 置信度 > 85% */
            LOGI("[WAKEUP] 唤醒词检测到! 置信度: %.2f", score);
            g_wk_state = WK_STATE_WAKEUP_DETECTED;

            if (g_wakeup_cb) {
                g_wakeup_cb();  /* 通知主控:已唤醒 */
            }
        }

        /* VAD 检测语音活动 */
        if (has_voice) {
            g_vad_silence_cnt = 0;
            if (g_vad_cb) {
                g_vad_cb(VAD_EVENT_SPEECH_START);
            }
        } else {
            g_vad_silence_cnt++;
            if (g_vad_silence_cnt > (VAD_SILENCE_MS / AUDIO_FRAME_MS)) {
                /* 连续静音超过阈值 → 语音结束 */
                if (g_vad_cb) {
                    g_vad_cb(VAD_EVENT_SPEECH_END);
                }
                g_vad_silence_cnt = 0;
            }
        }
        break;

    case WK_STATE_WAKEUP_DETECTED:
        /* 已唤醒,不做额外处理 */
        break;
    }
}

/* ========== 公开接口 ========== */

int wakeup_engine_init(void)
{
    LOGI("[WAKEUP] 初始化语音唤醒引擎...");

    g_wk_state = WK_STATE_IDLE;
    g_vad_silence_cnt = 0;

    /* 加载唤醒词模型 */
    if (wakeup_model_load() != 0) {
        LOGE("[WAKEUP] 模型加载失败!");
        return -1;
    }

    LOGI("[WAKEUP] 唤醒引擎初始化成功");
    return 0;
}

int wakeup_engine_start(void)
{
    g_wk_state = WK_STATE_LISTENING;

    /* 注册音频帧回调 */
    audio_capture_start(on_audio_frame);

    LOGI("[WAKEUP] 开始监听唤醒词...");
    return 0;
}

int wakeup_engine_stop(void)
{
    g_wk_state = WK_STATE_IDLE;
    audio_capture_stop();
    return 0;
}

void wakeup_cb_register(wakeup_cb_t cb)
{
    g_wakeup_cb = cb;
}

void vad_cb_register(vad_cb_t cb)
{
    g_vad_cb = cb;
}

void wakeup_state_reset(void)
{
    g_wk_state = WK_STATE_LISTENING;
    g_vad_silence_cnt = 0;
    LOGI("[WAKEUP] 状态已重置,继续监听");
}

7.6 云端 AI 通信模块 ai_cloud.c

/**
 * @file    ai_cloud.c
 * @brief   云端 AI 大模型通信
 * 
 * 支持的云端 AI:
 *   - 阿里云百炼(通义千问)
 *   - DeepSeek API
 *   - 科大讯飞星火
 *   - 自定义 WebSocket 协议对接任意大模型
 * 
 * 通信流程:
 *   1. TCP Socket 连接云端服务器
 *   2. TLS 握手(mbedTLS)
 *   3. 发送音频数据(实时流式)或 HTTP POST 音频文件
 *   4. 接收云端返回的 TTS 音频数据
 *   5. 解码并播放
 */

#include "ai_cloud.h"
#include "lwip/sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/net_sockets.h"
#include "cJSON.h"

#define TAG "AI-CLOUD"

/* ========== 云端配置 ========== */

/* --- 阿里云百炼 --- */
#define BAILIAN_HOST      "dashscope.aliyuncs.com"
#define BAILIAN_PORT      443
#define BAILIAN_API_KEY   "sk-your-api-key-here"
#define BAILIAN_WS_URL    "wss://dashscope.aliyuncs.com/api/v1/realtime"

/* --- DeepSeek --- */
#define DEEPSEEK_HOST     "api.deepseek.com"
#define DEEPSEEK_PORT     443
#define DEEPSEEK_API_KEY  "sk-your-deepseek-key"
#define DEEPSEEK_TTS_URL  "https://api.deepseek.com/v1/audio/speech"

/* --- 选择当前使用的模型 --- */
#define AI_PROVIDER       AI_PROVIDER_BAILIAN

/* ========== 数据结构 ========== */
typedef enum {
    AI_PROVIDER_BAILIAN = 0,
    AI_PROVIDER_DEEPSEEK,
    AI_PROVIDER_IFLYTEK,
} ai_provider_t;

typedef struct {
    int     socket_fd;
    bool    connected;
    bool    is_streaming;
    uint8_t recv_buf[8192];
    uint32_t recv_len;
} ai_connection_t;

static ai_connection_t g_ai_conn = {0};
static ai_reply_cb_t   g_reply_cb = NULL;

/* ========== HTTP POST + JSON 调用 DeepSeek API ========== */

/**
 * @brief 将 PCM 音频转为 Base64(用于 HTTP JSON 传输)
 */
static int pcm_to_base64(int16_t *pcm, int sample_count, char *b64_out, int b64_max)
{
    /* 简易 Base64 编码 */
    static const char b64_table[] = 
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    uint8_t *raw = (uint8_t *)pcm;
    int raw_len = sample_count * sizeof(int16_t);
    int b64_len = 0;

    for (int i = 0; i < raw_len; i += 3) {
        uint32_t triple = 0;
        int pad = 0;

        triple |= (uint32_t)raw[i] << 16;
        if (i + 1 < raw_len) {
            triple |= (uint32_t)raw[i + 1] << 8;
        } else {
            pad++;
        }
        if (i + 2 < raw_len) {
            triple |= (uint32_t)raw[i + 2];
        } else {
            pad++;
        }

        if (b64_len + 4 > b64_max) break;

        b64_out[b64_len++] = b64_table[(triple >> 18) & 0x3F];
        b64_out[b64_len++] = b64_table[(triple >> 12) & 0x3F];
        b64_out[b64_len++] = (pad < 2) ? b64_table[(triple >> 6) & 0x3F] : '=';
        b64_out[b64_len++] = (pad < 1) ? b64_table[triple & 0x3F] : '=';
    }
    b64_out[b64_len] = '\0';
    return b64_len;
}

/**
 * @brief HTTP POST 语音识别 → DeepSeek / 阿里云百炼
 * 
 * 模式:
 *   非流式模式:上传完整语音 → 等待 ASR 文本 → 传给 LLM → 获取文本回复 → TTS
 */
int ai_cloud_send_audio(uint8_t *audio_data, uint32_t audio_len, 
                         int sample_rate, ai_reply_cb_t reply_cb)
{
    int sock;
    struct sockaddr_in server_addr;
    char http_request[16384];
    char http_response[16384];
    int ret;

    g_reply_cb = reply_cb;

    /* 1. 创建 TCP Socket */
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        LOGE("[AI] Socket 创建失败");
        return -1;
    }

    /* 2. DNS 解析 */
    struct hostent *host = gethostbyname(DEEPSEEK_HOST);
    if (!host) {
        LOGE("[AI] DNS 解析失败");
        close(sock);
        return -2;
    }

    /* 3. 连接服务器 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(DEEPSEEK_PORT);
    memcpy(&server_addr.sin_addr, host->h_addr, host->h_length);

    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        LOGE("[AI] 连接失败");
        close(sock);
        return -3;
    }

    /* 4. 对音频数据进行 WAV 格式封装 + Base64 编码 */
    /* WAV 头: 44 bytes */
    uint8_t wav_header[44];
    uint32_t data_size = audio_len;
    uint32_t wav_total = data_size + 36;

    /* RIFF header */
    memcpy(wav_header,     "RIFF", 4);
    memcpy(wav_header + 4, &wav_total, 4);
    memcpy(wav_header + 8, "WAVE", 4);
    /* fmt chunk */
    memcpy(wav_header + 12, "fmt ", 4);
    uint32_t fmt_size = 16;
    memcpy(wav_header + 16, &fmt_size, 4);
    uint16_t audio_format = 1;      /* PCM */
    memcpy(wav_header + 20, &audio_format, 2);
    uint16_t channels = 1;
    memcpy(wav_header + 22, &channels, 2);
    uint32_t srate = sample_rate;
    memcpy(wav_header + 24, &srate, 4);
    uint32_t byte_rate = sample_rate * channels * 2;
    memcpy(wav_header + 28, &byte_rate, 4);
    uint16_t block_align = channels * 2;
    memcpy(wav_header + 32, &block_align, 2);
    uint16_t bits = 16;
    memcpy(wav_header + 34, &bits, 2);
    /* data chunk */
    memcpy(wav_header + 36, "data", 4);
    memcpy(wav_header + 40, &data_size, 4);

    /* 合并 WAV 头 + PCM 数据 */
    uint8_t *wav_data = (uint8_t *)malloc(44 + audio_len);
    if (!wav_data) {
        LOGE("[AI] 内存分配失败");
        close(sock);
        return -4;
    }
    memcpy(wav_data, wav_header, 44);
    memcpy(wav_data + 44, audio_data, audio_len);

    /* Base64 编码 */
    char *b64_audio = (char *)malloc((44 + audio_len) * 2);
    int b64_len = pcm_to_base64((int16_t *)wav_data, 
                                 (44 + audio_len) / 2, 
                                 b64_audio, 
                                 (44 + audio_len) * 2);
    free(wav_data);

    /* 5. 构建 HTTP POST 请求 */
    /* 
     * DeepSeek 语音识别 API 请求体:
     * {
     *   "model": "deepseek-chat",
     *   "messages": [
     *     {"role": "system", "content": "你是一个儿童陪伴机器人,回答要简短活泼"},
     *     {"role": "user", "content": "请识别这段语音并回答"}
     *   ]
     * }
     * 
     * 实际应用中使用多模态 API 或先 ASR 再 LLM
     */

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "model", "deepseek-chat");
    cJSON *messages = cJSON_CreateArray();

    cJSON *msg_sys = cJSON_CreateObject();
    cJSON_AddStringToObject(msg_sys, "role", "system");
    cJSON_AddStringToObject(msg_sys, "content", 
        "你叫小杰,是一个可爱的AI陪伴机器人。回答要简短活泼,不超过100个字,"
        "像小朋友的玩伴一样说话。");
    cJSON_AddItemToArray(messages, msg_sys);

    cJSON *msg_user = cJSON_CreateObject();
    cJSON_AddStringToObject(msg_user, "role", "user");
    cJSON_AddStringToObject(msg_user, "content", "请回复我一句话");
    cJSON_AddItemToArray(messages, msg_user);

    cJSON_AddItemToObject(root, "messages", messages);
    cJSON_AddBoolToObject(root, "stream", false);

    char *json_body = cJSON_PrintUnformatted(root);

    snprintf(http_request, sizeof(http_request),
        "POST /v1/chat/completions HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Content-Type: application/json\r\n"
        "Authorization: Bearer %s\r\n"
        "Content-Length: %d\r\n"
        "Connection: close\r\n"
        "\r\n"
        "%s",
        DEEPSEEK_HOST, DEEPSEEK_API_KEY, 
        (int)strlen(json_body), json_body);

    free(json_body);
    cJSON_Delete(root);

    /* 6. 发送请求 */
    ret = send(sock, http_request, strlen(http_request), 0);
    if (ret < 0) {
        LOGE("[AI] HTTP 发送失败");
        close(sock);
        return -5;
    }

    LOGI("[AI] 已发送请求, %d bytes, 等待回复...", ret);

    /* 7. 接收响应 */
    memset(http_response, 0, sizeof(http_response));
    ret = recv(sock, http_response, sizeof(http_response) - 1, 0);
    close(sock);

    if (ret <= 0) {
        LOGE("[AI] HTTP 接收失败");
        return -6;
    }

    /* 8. 解析 HTTP 响应 */
    char *body = strstr(http_response, "\r\n\r\n");
    if (!body) {
        LOGE("[AI] HTTP 响应格式错误");
        return -7;
    }
    body += 4;  /* 跳过 \r\n\r\n */

    LOGI("[AI] 收到响应: %.*s", 200, body);

    /* 9. 解析 JSON 获取 AI 回复文本 */
    cJSON *resp_json = cJSON_Parse(body);
    if (resp_json) {
        cJSON *choices = cJSON_GetObjectItem(resp_json, "choices");
        if (choices && cJSON_GetArraySize(choices) > 0) {
            cJSON *choice = cJSON_GetArrayItem(choices, 0);
            cJSON *message = cJSON_GetObjectItem(choice, "message");
            cJSON *content = cJSON_GetObjectItem(message, "content");

            if (content && content->valuestring) {
                LOGI("[AI] AI 回复: %s", content->valuestring);

                /* 回调:需要将此文本转为 TTS 音频播放 */
                if (g_reply_cb) {
                    ai_reply_t reply;
                    reply.type = AI_REPLY_TEXT;
                    reply.text = content->valuestring;
                    g_reply_cb(&reply);
                }
            }
        }
        cJSON_Delete(resp_json);
    }

    return 0;
}

/* ========== TTS 文字转语音 ========== */

/**
 * @brief 调用云端 TTS 将文本转为语音并播放
 */
int ai_cloud_tts_convert_and_play(const char *text)
{
    int sock;
    struct sockaddr_in server_addr;
    char http_request[4096];
    uint8_t *audio_buf;
    int ret;

    /* 构建 TTS 请求 JSON */
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "model", "tts-1");
    cJSON_AddStringToObject(root, "input", text);
    cJSON_AddStringToObject(root, "voice", "alloy");      /* 音色 */
    cJSON_AddStringToObject(root, "response_format", "mp3");  /* 输出格式 */
    cJSON_AddNumberToObject(root, "speed", 1.0);

    char *json_body = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);

    /* 连接 TTS 服务器 */
    sock = socket(AF_INET, SOCK_STREAM, 0);
    struct hostent *host = gethostbyname(DEEPSEEK_HOST);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(DEEPSEEK_PORT);
    memcpy(&server_addr.sin_addr, host->h_addr, host->h_length);
    connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));

    /* 发送 TTS 请求 */
    snprintf(http_request, sizeof(http_request),
        "POST /v1/audio/speech HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Content-Type: application/json\r\n"
        "Authorization: Bearer %s\r\n"
        "Content-Length: %d\r\n"
        "Connection: close\r\n"
        "\r\n"
        "%s",
        DEEPSEEK_HOST, DEEPSEEK_API_KEY, 
        (int)strlen(json_body), json_body);
    free(json_body);

    send(sock, http_request, strlen(http_request), 0);

    /* 接收 TTS 音频 (MP3 或 PCM) */
    audio_buf = (uint8_t *)malloc(128 * 1024);  /* 128KB */
    int total = 0;
    while ((ret = recv(sock, audio_buf + total, 1024, 0)) > 0) {
        total += ret;
    }
    close(sock);

    /* 解析 HTTP 响应,提取音频数据 */
    uint8_t *audio_start = audio_buf;
    int audio_len = total;

    uint8_t *body_start = (uint8_t *)strstr((char *)audio_buf, "\r\n\r\n");
    if (body_start) {
        body_start += 4;
        audio_start = body_start;
        audio_len = total - (body_start - audio_buf);
    }

    LOGI("[AI] TTS 音频下载完成: %d bytes", audio_len);

    /* 解码并播放 (如果是 MP3 需要解码) */
    extern int audio_player_play(uint8_t *data, uint32_t len, play_finish_cb_t cb);

    /* 这里简化处理:假设 TTS 直接返回 PCM,实际可能需要 MP3→PCM 解码 */
    audio_player_play(audio_start, audio_len, NULL);

    free(audio_buf);
    return 0;
}

/* ========== 初始化 ========== */
int ai_cloud_init(void)
{
    LOGI("[AI] 初始化云端 AI 模块...");
    LOGI("[AI] 当前模型: %d", AI_PROVIDER);
    memset(&g_ai_conn, 0, sizeof(g_ai_conn));
    return 0;
}

7.7 按键驱动 key_driver.c

/**
 * @file    key_driver.c
 * @brief   按键检测与事件分发
 */

#include "key_driver.h"

#define TAG "KEY"

/* 按键定义 */
#define KEY_WAKEUP_IO    IO_PORT_GROUP_A, 0   /* PA0: 唤醒/对话键 */
#define KEY_VOLUP_IO     IO_PORT_GROUP_A, 1   /* PA1: 音量+ */
#define KEY_VOLDN_IO     IO_PORT_GROUP_A, 2   /* PA2: 音量- */

/* 消抖参数 */
#define DEBOUNCE_MS       30     /* 消抖时间 */
#define LONG_PRESS_MS     2000   /* 长按时间 */
#define DOUBLE_CLICK_MS   400    /* 双击间隔 */

/* 按键状态 */
typedef struct {
    uint8_t   io_group;
    uint8_t   io_pin;
    uint8_t   state;          /* 0=释放 1=按下 */
    uint32_t  press_start_ms;
    uint32_t  last_release_ms;
    uint8_t   click_count;
    bool      long_pressed;
} key_info_t;

static key_info_t g_keys[3];
static key_event_cb_t g_key_cb = NULL;

/* 按键扫描任务 */
static void key_scan_task(void *param)
{
    while (1) {
        for (int i = 0; i < 3; i++) {
            uint8_t level = jl_gpio_read(g_keys[i].io_group, g_keys[i].io_pin);
            uint32_t now = jl_get_system_tick_ms();

            if (level == 0) {  /* 按下 (低电平有效) */
                if (g_keys[i].state == 0) {
                    /* 刚按下 */
                    g_keys[i].state = 1;
                    g_keys[i].press_start_ms = now;
                    g_keys[i].long_pressed = false;
                } else {
                    /* 持续按下 */
                    if (!g_keys[i].long_pressed &&
                        (now - g_keys[i].press_start_ms) > LONG_PRESS_MS) {
                        g_keys[i].long_pressed = true;
                        /* 长按事件 */
                        if (g_key_cb) {
                            g_key_cb(i, KEY_EVENT_LONG_PRESS);
                        }
                    }
                }
            } else {  /* 释放 */
                if (g_keys[i].state == 1) {
                    uint32_t hold_time = now - g_keys[i].press_start_ms;
                    g_keys[i].state = 0;

                    if (!g_keys[i].long_pressed && hold_time > DEBOUNCE_MS) {
                        /* 短按事件 */
                        g_keys[i].click_count++;

                        if (g_keys[i].click_count == 1) {
                            g_keys[i].last_release_ms = now;
                        } else if (g_keys[i].click_count >= 2) {
                            /* 双击 */
                            g_keys[i].click_count = 0;
                            if (g_key_cb) {
                                g_key_cb(i, KEY_EVENT_DOUBLE_CLICK);
                            }
                        }
                    }
                }

                /* 检测单击超时 */
                if (g_keys[i].click_count == 1 &&
                    (now - g_keys[i].last_release_ms) > DOUBLE_CLICK_MS) {
                    g_keys[i].click_count = 0;
                    if (g_key_cb) {
                        g_key_cb(i, KEY_EVENT_CLICK);
                    }
                }
            }
        }

        vTaskDelay(pdMS_TO_TICKS(10));  /* 10ms 扫描周期 */
    }
}

int key_init(void)
{
    LOGI("[KEY] 初始化按键...");

    /* 初始化 3 个按键 IO(内部上拉) */
    jl_gpio_config(KEY_WAKEUP_IO, GPIO_MODE_INPUT_PULLUP);
    jl_gpio_config(KEY_VOLUP_IO,  GPIO_MODE_INPUT_PULLUP);
    jl_gpio_config(KEY_VOLDN_IO,  GPIO_MODE_INPUT_PULLUP);

    g_keys[0] = (key_info_t){.io_group = IO_PORT_GROUP_A, .io_pin = 0};
    g_keys[1] = (key_info_t){.io_group = IO_PORT_GROUP_A, .io_pin = 1};
    g_keys[2] = (key_info_t){.io_group = IO_PORT_GROUP_A, .io_pin = 2};

    /* 创建按键扫描任务 */
    xTaskCreate(key_scan_task, "key_scan", 512, NULL, 1, NULL);

    LOGI("[KEY] 按键初始化完成");
    return 0;
}

void key_event_cb_register(key_event_cb_t cb)
{
    g_key_cb = cb;
}

7.8 LED 指示驱动 led_driver.c

/**
 * @file    led_driver.c
 * @brief   LED 状态指示
 * 
 * LED 模式:
 *   - 快闪 (200ms):Wi-Fi 配网中
 *   - 常亮:Wi-Fi 已连接,待机
 *   - 呼吸:AI 对话中
 *   - 慢闪 (1000ms):错误状态
 *   - 灭:深度休眠
 */

#include "led_driver.h"

#define TAG "LED"

/* LED 引脚定义 */
#define LED_WIFI_IO   IO_PORT_GROUP_A, 8   /* PA8: Wi-Fi 状态灯 (蓝) */
#define LED_AI_IO     IO_PORT_GROUP_B, 0   /* PB0: AI 对话灯 (绿) */

/* LED 模式表 */
typedef struct {
    uint32_t on_ms;
    uint32_t off_ms;
    uint8_t  repeat;
    bool     breathing;   /* 呼吸效果 */
} led_pattern_cfg_t;

static const led_pattern_cfg_t g_patterns[] = {
    [LED_PATTERN_OFF]        = {0,     0,    0,  false},
    [LED_PATTERN_FAST_BLINK] = {200,   200,  0,  false},  /* 0=无限 */
    [LED_PATTERN_SLOW_BLINK] = {1000,  1000, 0,  false},
    [LED_PATTERN_SOLID]      = {1,     0,    0,  false},  /* 常亮 */
    [LED_PATTERN_BREATHING]  = {3000,  0,    0,  true},   /* 呼吸 */
    [LED_PATTERN_WIFI_OK]    = {1000,  3000, 0,  false},  /* 间歇闪 */
};

static led_pattern_t g_wifi_pattern = LED_PATTERN_OFF;
static led_pattern_t g_ai_pattern   = LED_PATTERN_OFF;

/* LED 控制任务 */
static void led_task(void *param)
{
    uint32_t tick = 0;
    bool led_wifi_on = false;
    bool led_ai_on = false;

    while (1) {
        tick++;

        /* Wi-Fi LED */
        const led_pattern_cfg_t *wp = &g_patterns[g_wifi_pattern];
        if (wp->breathing) {
            /* 简易呼吸效果:PWM 占空比正弦变化 */
            int pwm = (int)(127 * (1.0 + sin(tick * 0.05))) + 128;
            jl_gpio_pwm_set(IO_PORT_GROUP_A, 8, pwm);
        } else if (wp->on_ms > 0 && wp->off_ms > 0) {
            /* 闪烁 */
            uint32_t period = wp->on_ms + wp->off_ms;
            uint32_t phase = (tick * 50) % period;  /* 50ms tick */
            if (phase < wp->on_ms) {
                jl_gpio_write(LED_WIFI_IO, 1);
            } else {
                jl_gpio_write(LED_WIFI_IO, 0);
            }
        } else if (wp->on_ms > 0 && wp->off_ms == 0) {
            /* 常亮 */
            jl_gpio_write(LED_WIFI_IO, 1);
        } else {
            /* 灭 */
            jl_gpio_write(LED_WIFI_IO, 0);
        }

        /* AI LED */
        const led_pattern_cfg_t *ap = &g_patterns[g_ai_pattern];
        if (ap->breathing) {
            int pwm = (int)(127 * (1.0 + sin(tick * 0.05))) + 128;
            jl_gpio_pwm_set(LED_AI_IO, pwm);
        } else if (ap->on_ms > 0) {
            jl_gpio_write(LED_AI_IO, 1);
        } else {
            jl_gpio_write(LED_AI_IO, 0);
        }

        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

int led_init(void)
{
    LOGI("[LED] 初始化 LED...");

    jl_gpio_config(LED_WIFI_IO, GPIO_MODE_OUTPUT);
    jl_gpio_config(LED_AI_IO,   GPIO_MODE_OUTPUT);

    jl_gpio_write(LED_WIFI_IO, 0);
    jl_gpio_write(LED_AI_IO, 0);

    /* 创建 LED 控制任务 */
    xTaskCreate(led_task, "led_task", 512, NULL, 1, NULL);

    LOGI("[LED] LED 初始化完成");
    return 0;
}

void led_set_pattern(led_pattern_t pattern)
{
    g_wifi_pattern = pattern;
}

void led_set_ai_state(bool active)
{
    g_ai_pattern = active ? LED_PATTERN_BREATHING : LED_PATTERN_OFF;
}

7.9 配置文件 app_config.h

/**
 * @file    app_config.h
 * @brief   全局配置文件
 */

#ifndef __APP_CONFIG_H__
#define __APP_CONFIG_H__

/* ========== 版本信息 ========== */
#define FW_VERSION_MAJOR    1
#define FW_VERSION_MINOR    0
#define FW_VERSION_PATCH    0
#define SDK_VERSION         "AC7911_SDK_V2.0"

/* ========== Wi-Fi 默认配置 ========== */
#define WIFI_SSID           "YOUR_WIFI_NAME"
#define WIFI_PASSWORD       "YOUR_WIFI_PASSWORD"
#define WIFI_RETRY_MAX      5

/* ========== 云端 AI 配置 ========== */
/* 阿里云百炼 */
#define BAILIAN_API_KEY     "sk-your-bailian-api-key"
#define BAILIAN_APP_ID      "your-app-id"

/* DeepSeek */
#define DEEPSEEK_API_KEY    "sk-your-deepseek-api-key"

/* 选择 AI 提供商: 1=百炼, 2=DeepSeek, 3=讯飞 */
#define AI_PROVIDER_SELECT  2

/* ========== 音频配置 ========== */
#define AUDIO_SAMPLE_RATE     16000
#define AUDIO_BIT_WIDTH       16
#define AUDIO_FRAME_MS        40
#define AUDIO_CHANNEL_COUNT   2       /* 双麦 */

/* ========== 唤醒词配置 ========== */
#define WAKEUP_WORD          "小杰小杰"
#define WAKEUP_THRESHOLD     0.85f   /* 置信度阈值 */
#define VAD_SILENCE_TIMEOUT  800     /* 静音超时 (ms) */

/* ========== 系统配置 ========== */
#define SYS_CLOCK_MHZ        320
#define WDT_TIMEOUT_SEC      5

/* ========== GPIO 映射表 ========== */
#define PIN_SPI_CS           IO_PORT_GROUP_B, 2
#define PIN_SPI_DO           IO_PORT_GROUP_B, 3
#define PIN_SPI_DI           IO_PORT_GROUP_B, 4
#define PIN_SPI_CLK          IO_PORT_GROUP_B, 5

#define PIN_I2S_BCK          IO_PORT_GROUP_A, 7
#define PIN_I2S_WS           IO_PORT_GROUP_A, 5
#define PIN_I2S_DIN0         IO_PORT_GROUP_A, 4   /* 主麦 */
#define PIN_I2S_DIN1         IO_PORT_GROUP_A, 6   /* 副麦 */
#define PIN_I2S_DOUT         IO_PORT_GROUP_A, 15  /* DAC 输出 */

#define PIN_KEY_WAKEUP       IO_PORT_GROUP_A, 0
#define PIN_KEY_VOLUP        IO_PORT_GROUP_A, 1
#define PIN_KEY_VOLDN        IO_PORT_GROUP_A, 2

#define PIN_LED_WIFI         IO_PORT_GROUP_A, 8
#define PIN_LED_AI           IO_PORT_GROUP_B, 0

#define PIN_UART0_TX         IO_PORT_GROUP_A, 10
#define PIN_UART0_RX         IO_PORT_GROUP_A, 11

#endif /* __APP_CONFIG_H__ */

7.10 头文件汇总 includes.h

/**
 * @file    includes.h
 * @brief   全局头文件汇总
 */

#ifndef __INCLUDES_H__
#define __INCLUDES_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>

/* FreeRTOS */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "timers.h"
#include "semphr.h"

/* AC7911 底层驱动 */
#include "jl_clock.h"
#include "jl_gpio.h"
#include "jl_uart.h"
#include "jl_i2s.h"
#include "jl_spi.h"
#include "jl_flash.h"
#include "jl_wifi.h"
#include "jl_ble.h"
#include "jl_adc.h"
#include "jl_dac.h"
#include "jl_wdt.h"
#include "jl_nn.h"
#include "jl_pmu.h"
#include "jl_rtc.h"

/* 应用模块 */
#include "app_config.h"

/* ========== 系统状态枚举 ========== */
typedef enum {
    STATE_INIT = 0,
    STATE_IDLE,
    STATE_WIFI_CONNECTING,
    STATE_WIFI_CONNECTED,
    STATE_WIFI_FAIL,
    STATE_WAKEUP_LISTENING,
    STATE_RECORDING,
    STATE_VAD_DETECTED,
    STATE_UPLOADING,
    STATE_WAITING_AI,
    STATE_PLAYING,
    STATE_ERROR,
} sys_state_t;

/* ========== 公共接口声明 ========== */
void sys_state_change(sys_state_t new_state);
sys_state_t sys_state_get(void);

#endif /* __INCLUDES_H__ */

7.11 编译脚本 Makefile

# AC7911 AI 对话机器人 Makefile

PROJECT_NAME = ai_speaker
CHIP = AC7911BA
SDK_VERSION = V2.0

# 工具链
CROSS_COMPILE = riscv-elf-
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
OBJCOPY = $(CROSS_COMPILE)objcopy
SIZE = $(CROSS_COMPILE)size

# 路径
SDK_ROOT = ../..
APP_DIR = .
BUILD_DIR = build

# 编译选项
CPU_FLAGS = -march=rv32imafdc -mabi=ilp32f -mcmodel=medany
OPT_FLAGS = -Os -ffunction-sections -fdata-sections
WARN_FLAGS = -Wall -Wextra

CFLAGS = $(CPU_FLAGS) $(OPT_FLAGS) $(WARN_FLAGS) \
         -DCONFIG_CHIP_$(CHIP) \
         -I$(SDK_ROOT)/cpu/ac7911/include \
         -I$(SDK_ROOT)/libs/lwip/include \
         -I$(SDK_ROOT)/libs/mbedtls/include \
         -I$(SDK_ROOT)/libs/freertos/include \
         -I$(APP_DIR)

LDFLAGS = -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(PROJECT_NAME).map \
          -T $(SDK_ROOT)/cpu/ac7911/link.ld \
          -L$(SDK_ROOT)/cpu/ac7911/lib \
          -lac7911_hal -llwip -lmbedtls -lfreertos -lm

# 源文件
SRCS = $(wildcard $(APP_DIR)/*.c) \
       $(wildcard $(APP_DIR)/wifi/*.c) \
       $(wildcard $(APP_DIR)/audio/*.c) \
       $(wildcard $(APP_DIR)/ai/*.c)

OBJS = $(SRCS:%.c=$(BUILD_DIR)/%.o)

# 目标
all: $(BUILD_DIR)/$(PROJECT_NAME).bin

$(BUILD_DIR)/$(PROJECT_NAME).elf: $(OBJS)
	@echo "Linking $@..."
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
	$(SIZE) $@

$(BUILD_DIR)/$(PROJECT_NAME).bin: $(BUILD_DIR)/$(PROJECT_NAME).elf
	@echo "Generating $@..."
	$(OBJCOPY) -O binary $< $@
	@echo "Build complete: $@ ($(shell stat -c%s $@) bytes)"

$(BUILD_DIR)/%.o: %.c
	@mkdir -p $(dir $@)
	@echo "CC $<"
	$(CC) $(CFLAGS) -c -o $@ $<

flash:
	@echo "请使用杰理烧录工具下载固件"
	@echo "固件路径: $(BUILD_DIR)/$(PROJECT_NAME).bin"

clean:
	rm -rf $(BUILD_DIR)

.PHONY: all flash clean

八、云端 AI 大模型对接

8.1 三大主流方案对比

方案 通信方式 延迟 成本 推荐场景
阿里云百炼 WebSocket 实时流 ~500ms 按量付费 生产级产品
DeepSeek API HTTP REST ~2s 极低 快速原型、个人项目
科大讯飞 AIUI WebSocket ~300ms 按调用 语音交互专项优化

8.2 推荐流程:DeepSeek API(最低成本快速启动)

TTS 服务 DeepSeek API AC7911 设备 TTS 服务 DeepSeek API AC7911 设备 发送音频 → ASR → LLM 回复文本 1. 用户唤醒词触发 2. 录音 5 秒 PCM 3. HTTP POST /v1/chat/completions 4. JSON {choices[0].message.content} 5. HTTP POST /v1/audio/speech 6. MP3/PCM 音频流 7. 解码并播放

8.3 阿里云百炼实时对话(低延迟方案,WebSocket)

/**
 * @brief 阿里云百炼实时语音对话(WebSocket 流式)
 * 
 * 优势:
 *   - 边录边传,延迟 < 1s
 *   - 支持流式 TTS(边说边播)
 *   - 支持打断(用户说话时中断 TTS 播放)
 */

#if AI_PROVIDER_SELECT == 1

int ai_bailian_realtime_init(void)
{
    /* 1. WebSocket 连接 */
    /* URL: wss://dashscope.aliyuncs.com/api/v1/realtime */
    /* Header: Authorization: Bearer {api_key} */
    
    /* 2. 发送 start 事件 (含 ASR/TTS 参数配置) */
    /* {
     *   "event": "start",
     *   "asr_params": {"model": "paraformer-realtime-v2", "sample_rate": 16000},
     *   "tts_params": {"model": "cosyvoice-v1", "voice": "longxiaochun"}
     * }
     */
    
    /* 3. 流式发送 PCM 音频帧 */
    /* 每 40ms 发送一帧 */
    
    /* 4. 接收流式 TTS 音频帧并播放 */
    /* on_message → PCM 数据 → I2S DAC 播放 */
    
    LOGI("[AI-Bailian] 实时对话已就绪");
    return 0;
}

#endif

九、调试与测试

9.1 串口调试

连接 USB-TTL 到 UART0 (PA10/PA11),波特率 921600

串口输出示例:
========================================
  AC7911 AI Speaker Bootloader v1.0
  Chip: AC7911BA, Flash: W25Q64 8MB
========================================
[AI-SPEAKER][I] === AI 对话机器人启动 ===
[AI-SPEAKER][I] 芯片: AC7911, SDK Version: AC7911_SDK_V2.0
[WIFI][I] 初始化 Wi-Fi 模块...
[WIFI][I] 已连接到 YOUR_WIFI
[WIFI][I] IP: 192.168.1.105
[AUDIO][I] 初始化音频采集...
[AUDIO][I] 参数: 16000Hz/16bit/2ch, 帧长 40ms(1280B)
[WAKEUP][I] 模型加载成功, 大小: 65536 bytes
[WAKEUP][I] 开始监听唤醒词...
[AI][I] 当前模型: DeepSeek

--- 用户说"小杰小杰" ---
[WAKEUP][I] 唤醒词检测到! 置信度: 0.92
[LED][I] AI 对话中 (呼吸灯)
[AUDIO][I] 开始录音...
[AUDIO][I] VAD 检测到语音开始
[AUDIO][I] VAD 检测到语音结束, 录音 4.2s
[AI][I] 已发送请求, 1536 bytes, 等待回复...
[AI][I] AI 回复: 你好呀!我是小杰,今天想聊什么呀?
[AI][I] TTS 音频下载完成: 28416 bytes
[AUDIO][I] 播放完成, 总计: 28416 bytes
[WAKEUP][I] 状态已重置,继续监听

9.2 常见问题快速诊断

现象 可能原因 解决方法
芯片不启动 Flash CS 接错 / 供电不足 检查 PB2(CS) 连接,测 3.3V 电压
无 Wi-Fi 信号 天线匹配不良 / 晶振未起振 检查 24MHz 晶振波形,用频谱仪看 RF 输出
麦克风无声音 I2S 引脚配置错误 / INMP441 L/R 设置冲突 确认两个麦的 L/R 脚一个拉高一个拉低
喇叭无声 I2S 格式不匹配 / NS4168 未使能 检查 NS4168 供电,用示波器看 I2S 波形
AI 请求超时 DNS 失败 / API Key 错误 / 防火墙 先用电脑 curl 测试 API 是否可达

十、常见问题与避坑指南

10.1 硬件雷区

  1. Flash 启动问题:AC7911 从外部 SPI Flash 启动,CS 引脚(通常 PB2)必须正确连接。如果芯片无反应,第一个检查 Flash 的四根 SPI 线。
  2. 晶振不起振:24MHz 晶振的负载电容不匹配是最常见的问题。先用示波器确认 OSC_IN/OSC_OUT 有 24MHz 正弦波。
  3. 麦克风左右声道冲突:两个 INMP441 挂在同一条 I2S 总线上时,一个 L/R 接 GND(左声道),另一个接 3.3V(右声道),否则两个麦的数据会混叠。
  4. 功放啸叫:喇叭太靠近麦克风会形成正反馈啸叫。物理结构上要做好音腔隔离,软件上做 AEC(回声消除)。

10.2 软件雷区

  1. 路径不能有中文:杰理 SDK 对中文路径支持极差,工程目录必须是全英文。
  2. 堆栈溢出:FreeRTOS 任务的堆栈如果开太小(< 512 bytes),音频处理任务极易栈溢出。建议音频任务 ≥ 2048 bytes,主控任务 ≥ 1536 bytes。
  3. API Key 泄露:不要在 GitHub 上提交含有真实 API Key 的代码!使用 app_config_private.h(加入 .gitignore)存放密钥。
  4. 唤醒模型定制:默认唤醒词"小杰小杰"是 SDK 自带的通用模型。如需更好的唤醒率,建议用杰理提供的工具采集 500+ 人的唤醒词语音样本进行微调。

10.3 量产建议

  • OTA 升级:务必设计好 OTA 方案。AI 对话设备会频繁更新 Prompt 和模型接入逻辑,没有 OTA 等于废铁。
  • 语音数据采集:建议在用户授权下回传一定比例的语音数据到后台,用于持续优化唤醒模型和 ASR 准确率。
  • 安全防线:云端 API Key 不要硬编码在固件中,建议通过 HTTPS + 设备证书的方式动态获取临时 Token。

后记

AC7911 的"全集成"设计哲学(Wi-Fi + BLE + 音频全链路 + AI 加速单芯片)让 AI 硬件的平民化 成为现实。当然,它的代价也很明显:SDK 生态不如 ESP32 成熟、开发资料中文为主、社区较小。但对于追求极致性价比的 AIoT 产品,它确实是当下最值得关注的选择之一。

希望这篇从 BOM 到代码的全流程指南,能帮你少走弯路,快速跑通你的第一个 AI 对话硬件!


参考资料

资源 链接
杰理 AC7911 芯片介绍 yunthinker.com/560.html
AC7911 数据手册 https://www.zh-jieli.com/ → 产品中心 → AC791N
阿里云百炼 RTOS C SDK help.aliyun.com
科大讯飞 AIUI 开发指南 aiui-doc.xf-yun.com
网易云信 NERTC AC7911 SDK doc.yunxin.163.com
DeepSeek API 文档 platform.deepseek.com
INMP441 数据手册 InvenSense TDK
NS4168 数据手册 纳芯微 / 立创商城
TP4056 锂电充电方案 立创商城开源方案
杰理 AC791N 培训资料 yunthinker.com/618.html
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐