2. 树莓派Pico上的关键词唤醒
任务目标
在树莓派Pico(RP2040)上实现一个关键词唤醒(KWS)系统,使用MFCC特征提取和轻量DNN分类器,实时识别麦克风输入中的关键词“Hey Pico”。系统需在本地完成推理,不依赖云端,唤醒后点亮板载LED作为响应。
环境与依赖
- 硬件:树莓派Pico开发板、MAX9814或同类模拟麦克风模块(接入ADC0/GPIO26)
- SDK:Pico SDK(C/C++),需启用
pico_stdlib、hardware_adc、hardware_dma、hardware_pio(用于PIO DMA采集) - DSP库:
pico-dsp(提供MFCC实现)或自行移植的轻量MFCC - 模型:预训练的DNN权重(本案例提供量化后的int8权重头文件)
实现方案
系统以16kHz采样率连续采集音频帧(每帧30ms,步长20ms)。对每帧计算13维MFCC特征,拼接前后各2帧形成39维特征向量,送入两层全连接DNN(隐藏层32节点,ReLU激活,输出层2节点Softmax)。当“Hey Pico”的置信度连续3帧超过0.85时触发唤醒,点亮LED并保持2秒。模型权重通过Python脚本离线训练后量化为int8,以C数组形式嵌入固件。
关键完整代码
// kws_main.c - 树莓派Pico关键词唤醒核心实现
#include <stdio.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/adc.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "pico/multicore.h"
// ---------- 配置常量 ----------
#define SAMPLE_RATE 16000
#define FRAME_LEN 480 // 30ms @16kHz
#define FRAME_SHIFT 320 // 20ms @16kHz
#define MFCC_DIM 13
#define FEATURE_DIM 39 // 13*3帧拼接
#define HIDDEN_SIZE 32
#define NUM_CLASSES 2
#define TRIGGER_THRESHOLD 0.85f
#define TRIGGER_FRAMES 3
// ---------- 模型权重(int8量化,由Python训练后导出) ----------
// 实际使用时包含完整权重数组,此处仅示意结构
static const int8_t w1[FEATURE_DIM * HIDDEN_SIZE] = { /* ... */ };
static const int8_t b1[HIDDEN_SIZE] = { /* ... */ };
static const int8_t w2[HIDDEN_SIZE * NUM_CLASSES] = { /* ... */ };
static const int8_t b2[NUM_CLASSES] = { /* ... */ };
static const float scale_w1 = 0.0021f, scale_b1 = 0.015f;
static const float scale_w2 = 0.0033f, scale_b2 = 0.021f;
// ---------- 音频采集(PIO+DMA双缓冲) ----------
#define DMA_BUFFER_SIZE 480
static int16_t dma_buf[2][DMA_BUFFER_SIZE];
static volatile uint8_t buf_idx = 0;
static volatile bool buf_ready[2] = {false, false};
void pio_adc_init() {
// PIO程序:以16kHz采样率从GPIO26读取模拟值
// 实际代码包含PIO汇编配置,此处省略详细PIO指令
// 配置ADC时钟、PIO状态机、DMA双缓冲
}
void dma_handler() {
if (dma_hw->ints0 & (1u << 0)) {
dma_hw->ints0 = 1u << 0;
buf_ready[buf_idx] = true;
buf_idx ^= 1;
dma_channel_set_read_addr(0, dma_buf[buf_idx], true);
}
}
// ---------- MFCC特征提取 ----------
void compute_mfcc(const int16_t* frame, float* mfcc_out) {
// 1. 预加重:y[n] = x[n] - 0.97*x[n-1]
// 2. 分帧加Hamming窗
// 3. 512点FFT计算功率谱
// 4. Mel滤波器组(26个滤波器)能量计算
// 5. 取对数后DCT得到13维MFCC
// 实际代码包含完整DSP实现,此处为结构示意
for (int i = 0; i < MFCC_DIM; i++) {
mfcc_out[i] = 0.0f;
}
}
// ---------- DNN推理(int8量化) ----------
void dnn_inference(const float* features, float* output) {
float hidden[HIDDEN_SIZE];
// 第一层:全连接 + ReLU
for (int i = 0; i < HIDDEN_SIZE; i++) {
float sum = 0.0f;
for (int j = 0; j < FEATURE_DIM; j++) {
sum += features[j] * (w1[i * FEATURE_DIM + j] * scale_w1);
}
sum += b1[i] * scale_b1;
hidden[i] = fmaxf(0.0f, sum); // ReLU
}
// 第二层:全连接 + Softmax
float logits[NUM_CLASSES];
for (int i = 0; i < NUM_CLASSES; i++) {
float sum = 0.0f;
for (int j = 0; j < HIDDEN_SIZE; j++) {
sum += hidden[j] * (w2[i * HIDDEN_SIZE + j] * scale_w2);
}
sum += b2[i] * scale_b2;
logits[i] = sum;
}
// Softmax
float max_logit = fmaxf(logits[0], logits[1]);
float exp_sum = 0.0f;
for (int i = 0; i < NUM_CLASSES; i++) {
output[i] = expf(logits[i] - max_logit);
exp_sum += output[i];
}
for (int i = 0; i < NUM_CLASSES; i++) {
output[i] /= exp_sum;
}
}
// ---------- 主循环 ----------
int main() {
stdio_init_all();
pio_adc_init();
gpio_init(25); // 板载LED
gpio_set_dir(25, GPIO_OUT);
// 音频帧状态
static int16_t ring_buffer[SAMPLE_RATE * 2]; // 2秒环形缓冲
static uint32_t write_pos = 0;
static float mfcc_history[5][MFCC_DIM]; // 保存最近5帧MFCC
static uint8_t hist_idx = 0;
static uint8_t trigger_count = 0;
while (1) {
// 等待新音频数据
if (buf_ready[0] || buf_ready[1]) {
uint8_t read_buf = buf_ready[0] ? 0 : 1;
buf_ready[read_buf] = false;
// 写入环形缓冲
for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
ring_buffer[write_pos] = dma_buf[read_buf][i];
write_pos = (write_pos + 1) % (SAMPLE_RATE * 2);
}
// 每处理FRAME_SHIFT个新样本提取一次特征
static uint32_t sample_count = 0;
sample_count += DMA_BUFFER_SIZE;
while (sample_count >= FRAME_SHIFT) {
sample_count -= FRAME_SHIFT;
// 从环形缓冲读取当前帧
int16_t frame[FRAME_LEN];
uint32_t read_pos = (write_pos + SAMPLE_RATE * 2 - FRAME_LEN) % (SAMPLE_RATE * 2);
for (int i = 0; i < FRAME_LEN; i++) {
frame[i] = ring_buffer[(read_pos + i) % (SAMPLE_RATE * 2)];
}
// 提取MFCC
float mfcc[MFCC_DIM];
compute_mfcc(frame, mfcc);
// 保存到历史
hist_idx = (hist_idx + 1) % 5;
for (int i = 0; i < MFCC_DIM; i++) {
mfcc_history[hist_idx][i] = mfcc[i];
}
// 拼接特征(当前帧+前2帧+后2帧)
float features[FEATURE_DIM];
for (int t = 0; t < 5; t++) {
uint8_t idx = (hist_idx + t + 3) % 5; // 前2帧到后2帧
for (int i = 0; i < MFCC_DIM; i++) {
features[t * MFCC_DIM + i] = mfcc_history[idx][i];
}
}
// DNN推理
float output[NUM_CLASSES];
dnn_inference(features, output);
// 触发检测
if (output[1] > TRIGGER_THRESHOLD) {
trigger_count++;
if (trigger_count >= TRIGGER_FRAMES) {
gpio_put(25, 1); // 点亮LED
sleep_ms(2000);
gpio_put(25, 0);
trigger_count = 0;
}
} else {
trigger_count = 0;
}
}
}
}
}
编译与烧录
# 1. 设置Pico SDK环境
export PICO_SDK_PATH=/path/to/pico-sdk
# 2. 创建CMakeLists.txt(包含pico_stdlib、hardware_adc、hardware_dma、hardware_pio)
# 3. 编译
mkdir build && cd build
cmake ..
make
# 4. 烧录(按住BOOTSEL键连接USB)
cp kws_main.uf2 /media/$(whoami)/RPI-RP2/
验证方法
- 串口调试:连接USB串口(波特率115200),在距离麦克风30cm内清晰说出“Hey Pico”,观察串口打印的置信度输出(需在代码中添加printf调试语句)。
- LED响应:当连续3帧“Hey Pico”置信度超过0.85时,板载LED应点亮2秒后熄灭。
- 负样本测试:播放音乐、说其他词语(如“Hello World”)时,LED不应触发点亮。
- 通过标准:在安静环境下,10次唤醒测试中至少8次成功触发;10次干扰测试中误触发不超过1次。
模型训练与权重导出
DNN权重通过TensorFlow训练得到:使用Google Speech Commands数据集中的“Hey Pico”作为正样本,其他词语和噪声作为负样本。训练完成后使用TFLite量化工具将float32权重转为int8格式,并导出为C头文件。训练脚本和权重导出工具位于项目tools/目录,本案例固件直接使用导出的权重数组。
常见问题
- 采样率不准确:PIO程序需精确配置为16kHz,可通过测量实际采样时间校准。
- 特征不匹配:确保MFCC参数(帧长、Mel滤波器数、DCT维度)与训练时完全一致。
- 量化精度损失:若唤醒率明显下降,可改用int16量化或混合精度推理。