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/

验证方法

  1. 串口调试:连接USB串口(波特率115200),在距离麦克风30cm内清晰说出“Hey Pico”,观察串口打印的置信度输出(需在代码中添加printf调试语句)。
  2. LED响应:当连续3帧“Hey Pico”置信度超过0.85时,板载LED应点亮2秒后熄灭。
  3. 负样本测试:播放音乐、说其他词语(如“Hello World”)时,LED不应触发点亮。
  4. 通过标准:在安静环境下,10次唤醒测试中至少8次成功触发;10次干扰测试中误触发不超过1次。

模型训练与权重导出

DNN权重通过TensorFlow训练得到:使用Google Speech Commands数据集中的“Hey Pico”作为正样本,其他词语和噪声作为负样本。训练完成后使用TFLite量化工具将float32权重转为int8格式,并导出为C头文件。训练脚本和权重导出工具位于项目tools/目录,本案例固件直接使用导出的权重数组。

常见问题

  • 采样率不准确:PIO程序需精确配置为16kHz,可通过测量实际采样时间校准。
  • 特征不匹配:确保MFCC参数(帧长、Mel滤波器数、DCT维度)与训练时完全一致。
  • 量化精度损失:若唤醒率明显下降,可改用int16量化或混合精度推理。