AC7911制作AI对话机器人全攻略
基于杰理 AC7911 制作 AI 对话机器人全攻略:硬件选型、电路连接与完整代码
目录
- 为什么选择 AC7911?——芯片选型深度论证
- 芯片核心规格与架构
- 完整 BOM 元器件清单
- 硬件电路连接详解
- 开发环境搭建
- SDK 工程结构与代码框架
- 完整代码实现
- 云端 AI 大模型对接
- 调试与测试
- 常见问题与避坑指南
一、为什么选择 AC7911?——芯片选型深度论证
1.1 需求分析
我们要做一个 AI 对话机器人,它的核心工作流是:
拆解到芯片层面的需求:
| 功能需求 | 芯片能力要求 |
|---|---|
| 实时音频采集 | ≥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 麦 | SGM3760 或 ZTS6212 | 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 整体软件架构
6.2 主状态机设计
七、完整代码实现
说明:以下代码基于 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(最低成本快速启动)
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 硬件雷区
- Flash 启动问题:AC7911 从外部 SPI Flash 启动,CS 引脚(通常 PB2)必须正确连接。如果芯片无反应,第一个检查 Flash 的四根 SPI 线。
- 晶振不起振:24MHz 晶振的负载电容不匹配是最常见的问题。先用示波器确认 OSC_IN/OSC_OUT 有 24MHz 正弦波。
- 麦克风左右声道冲突:两个 INMP441 挂在同一条 I2S 总线上时,一个 L/R 接 GND(左声道),另一个接 3.3V(右声道),否则两个麦的数据会混叠。
- 功放啸叫:喇叭太靠近麦克风会形成正反馈啸叫。物理结构上要做好音腔隔离,软件上做 AEC(回声消除)。
10.2 软件雷区
- 路径不能有中文:杰理 SDK 对中文路径支持极差,工程目录必须是全英文。
- 堆栈溢出:FreeRTOS 任务的堆栈如果开太小(< 512 bytes),音频处理任务极易栈溢出。建议音频任务 ≥ 2048 bytes,主控任务 ≥ 1536 bytes。
- API Key 泄露:不要在 GitHub 上提交含有真实 API Key 的代码!使用
app_config_private.h(加入 .gitignore)存放密钥。 - 唤醒模型定制:默认唤醒词"小杰小杰"是 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 |
更多推荐



所有评论(0)