1. PwmSpeaker 库概述

PwmSpeaker 是一个轻量级、无依赖的嵌入式音频驱动库,专为资源受限的微控制器设计,其核心目标是 仅通过硬件 PWM 外设驱动无源蜂鸣器或小型动圈扬声器,实现音调生成、简单旋律播放与基础音频控制 。它不依赖 DAC、I²S 或音频编解码器,也不引入 RTOS、标准 C++ STL 或浮点运算库,完全基于裸机(Bare-metal)或 FreeRTOS 环境下的定时器/高级控制定时器(TIM)PWM 输出能力构建。

该库的设计哲学是“ 用最确定的硬件资源做最可控的声音输出 ”。在工业人机界面(HMI)、医疗设备提示音、IoT 节点状态反馈、教育开发板发声模块等场景中,开发者往往不需要高保真音频,而更关注:

  • 音调频率精度(±0.5% 内可接受)
  • 启停响应时间(< 100 µs)
  • 占空比可调性(支持 10%–90% 线性调节以控制响度)
  • 低 CPU 占用(PWM 由硬件自动翻转,CPU 仅配置寄存器)
  • 可预测的时序行为(无动态内存分配、无中断嵌套风险)

PwmSpeaker 不提供音频文件解析(如 WAV/MP3)、混音、滤波或采样率转换功能。它本质上是一个 频率-占空比-时长三元组的精确执行引擎 ,将抽象的“播放 Do 音持续 200ms”转化为对 TIMx->ARR、TIMx->CCRy 和 HAL_TIM_PWM_Start() 的原子级调用。

其典型部署形态如下:

  • MCU:STM32F0/F1/F3/F4/H7、NXP Kinetis、RISC-V GD32VF103、ESP32(使用 LEDC 或 MCPWM)
  • 外设:高级控制定时器(如 STM32 的 TIM1/TIM8)或通用定时器(TIM2–TIM5),支持互补通道与死区插入(可选)
  • 驱动电路:NPN/PNP 推挽、MOSFET 半桥或专用音频驱动芯片(如 TPA2005D1),支持直流偏置消除
  • 发声单元:8Ω/16Ω 无源扬声器(直径 ≤ 25mm)、压电蜂鸣器(需并联阻尼电阻)、或带内置振荡器的有源蜂鸣器(此时仅作开关使能,非本库主要目标)

⚠️ 注意:本库 明确不支持有源蜂鸣器的“直接驱动”模式 。有源蜂鸣器内部已集成振荡电路,仅需直流电压即可发声;而 PwmSpeaker 的设计前提——即通过外部 PWM 控制频率——对其无效。若误用于此类器件,将仅产生固定频率的单一音调(取决于其内置振荡器),且无法通过库 API 改变。

2. 硬件原理与信号链分析

2.1 PWM 驱动扬声器的物理基础

扬声器本质是电-力-声换能器。当电流流过音圈时,根据洛伦兹力定律 $ F = B \cdot I \cdot L $,音圈受力带动振膜振动,从而推动空气形成声波。对于无源扬声器,输入信号的 基频决定音调(pitch),幅值决定响度(loudness),波形决定音色(timbre)

PwmSpeaker 采用 方波 PWM 作为激励源 ,其优势在于:

  • 硬件实现极简:MCU 定时器原生支持,无需额外 DAC 或外设
  • 功率效率高:MOSFET 工作于饱和/截止区,导通损耗低
  • 频率控制精准:ARR(自动重装载值)与 CK_PSC(时钟预分频)共同决定 PWM 周期 $ T_{PWM} = (ARR + 1) \times (PSC + 1) / f_{CLK} $,误差仅来源于时钟晶振精度(通常 ±20 ppm)

但方波含丰富奇次谐波($ f, 3f, 5f, \dots $),直接驱动扬声器会产生刺耳高频噪声。因此,PwmSpeaker 在设计中强制要求 硬件低通滤波

MCU PWM Pin → Series Resistor (R = 10–100 Ω) → Parallel RC Filter (Rf = 100 Ω, Cf = 100 nF) → Speaker

该 RC 滤波器截止频率 $ f_c = \frac{1}{2\pi R_f C_f} \approx 16,\text{kHz} $,可有效衰减 3f 及以上谐波(例如 1 kHz 音调的 3 kHz 分量被衰减约 -10 dB),同时保留基频能量。实测表明,未加滤波时 1 kHz 方波驱动 8Ω 扬声器产生明显“嘶嘶”声;加入上述滤波后,音色显著圆润,接近正弦波效果。

2.2 关键时序约束与定时器选型

PwmSpeaker 对定时器资源提出三项硬性要求:

  1. 分辨率足够 :需覆盖人耳可听范围 20 Hz – 20 kHz。以 20 kHz 为例,若系统主频 $ f_{CLK} = 72,\text{MHz} $,则最大 ARR 值需满足 $ (ARR+1) \geq f_{CLK}/f_{min} = 72,\text{MHz}/20,\text{Hz} = 3.6 \times 10^6 $。16 位定时器(ARR 最大 65535)无法直接满足,必须启用预分频(PSC)。合理配置为 PSC = 0x00FF (255),则有效计数频率为 $ 72,\text{MHz}/256 \approx 281.25,\text{kHz} $,此时 20 Hz 对应 ARR = 14062,完全可行。
  2. 更新事件可控 :频率切换必须在 PWM 周期边界发生,避免相位跳变导致“咔嗒”声(pop noise)。因此必须使用定时器的 影子寄存器(shadow register)机制 ,即修改 ARR/CCR 后,等待 UEV(Update Event)触发才生效。HAL 库中通过 __HAL_TIM_SET_AUTORELOAD() + __HAL_TIM_SET_COMPARE() 配合 HAL_TIM_GenerateEvent() 实现。
  3. 通道独立性 :单音播放仅需 1 路 PWM;双音和声(如和弦)需至少 2 路独立可调频率的 PWM 通道,并确保它们的时钟源同步(共用同一 TIMx,而非 TIM1+TIM2)。STM32 高级控制定时器(TIM1/TIM8)支持 4 路互补通道,是理想选择。

下表列出常见 MCU 平台的推荐定时器配置:

MCU 系列 推荐定时器 时钟源 典型 PSC 设置 ARR 计算公式 备注
STM32F407 TIM1 APB2 @ 84 MHz 0x0000 ARR = (84000000 / freq) - 1 使用 HAL_TIMEx_PWMN_Start() 驱动互补通道
GD32VF103 TIMER0 APB1 @ 108 MHz 0x0001 ARR = (108000000 / 2 / freq) - 1 RISC-V 架构,需查手册确认时钟树
ESP32 (LEDC) LEDC_TIMER_0 REF_TICK @ 1 MHz N/A(LEDC 自管理) ledc_timer_config_t.freq_hz = freq 使用 ESP-IDF LEDC 驱动层封装

2.3 电气安全与驱动能力匹配

MCU GPIO 直接驱动扬声器存在严重风险:

  • STM32 GPIO 最大灌电流约 25 mA,而 8Ω 扬声器在 3.3 V 下理论电流达 412 mA($ I = V/R $),远超限值
  • 瞬态反电动势(back-EMF)可达 ±20 V,易击穿 IO 口

因此,PwmSpeaker 强制要求外部功率级 。典型方案包括:

方案 A:NPN+PNP 推挽(低成本,适合 ≤ 100 mW)

MCU PWM → 1kΩ → Q1(NPN, e.g. 2N2222) Base  
                      ↓ Collector → Speaker Top  
Q2(PNP, e.g. 2N2907) Base ← 1kΩ ← MCU PWM  
                      ↑ Emitter → Speaker Bottom  
Speaker Bottom → GND  

优点:元件少、成本低;缺点:存在交越失真,低频响应差。

方案 B:N-MOSFET 半桥(推荐,高效可靠)

MCU PWM → Gate Driver (e.g. TC4420) → N-MOS (e.g. IRLZ44N) Gate  
MOS Drain → Speaker Top  
Speaker Bottom → GND  
MOS Source → GND  

需注意:MOSFET 必须选用逻辑电平驱动型($ V_{GS(th)} < 2.5,\text{V} $),并添加 10kΩ 下拉电阻确保关断。

方案 C:专用音频功放(高保真,如 TPA2005D1)

  • 输入兼容 3.3 V TTL 电平
  • 内置滤波与过热保护
  • 支持 1.4 W @ 8Ω,THD+N < 1%
  • 仅需连接 PWM 引脚至 IN+,IN- 接地,VDD 接 5 V

无论何种方案, 必须在 PWM 输出引脚与驱动电路间串联 10–100 Ω 限流电阻 ,防止高频振铃损坏 MCU。

3. 核心 API 设计与实现逻辑

PwmSpeaker 提供一组精简但完备的 C 函数接口,全部声明于 pwmspeaker.h ,实现位于 pwmspeaker.c 。所有函数均以 pwmspeaker_ 为前缀,避免命名冲突。关键 API 按功能分组如下:

3.1 初始化与硬件绑定

typedef struct {
    TIM_HandleTypeDef *htim;   // 指向 HAL TIM 句柄(必需)
    uint32_t channel;        // PWM 通道:TIM_CHANNEL_1/2/3/4(必需)
    uint32_t pwm_gpio_pin;   // GPIO 引脚号(如 GPIO_PIN_6,必需)
    GPIO_TypeDef *pwm_gpio_port; // GPIO 端口(如 GPIOA,必需)
    uint8_t volume_level;    // 初始占空比(0–100,对应 0%–100%,默认 50)
} PwmSpeakerConfig_t;

/**
 * @brief 初始化 PwmSpeaker 实例
 * @param hspk: PwmSpeaker 句柄指针(用户定义的 static PwmSpeakerHandle_t 变量)
 * @param config: 硬件配置结构体
 * @retval HAL_StatusTypeDef: HAL_OK 表示成功,HAL_ERROR 表示参数非法或硬件错误
 */
HAL_StatusTypeDef pwmspeaker_init(PwmSpeakerHandle_t *hspk,
                                   const PwmSpeakerConfig_t *config);

pwmspeaker_init() 执行以下关键操作:

  1. 校验 htim 是否已由 MX_TIMx_Init() 初始化(检查 htim->Instance != NULL
  2. 配置 GPIO 引脚为复用推挽输出( GPIO_MODE_AF_PP ),设置速度为高速( GPIO_SPEED_FREQ_HIGH
  3. 将指定通道配置为 PWM 模式( TIM_OCMODE_PWM1 ),启用预装载( TIM_OC_PRELOAD_ENABLE
  4. 设置初始占空比: __HAL_TIM_SET_COMPARE(htim, config->channel, (config->volume_level * (htim->Init.Period + 1)) / 100)
  5. 启动 PWM 输出: HAL_TIM_PWM_Start(htim, config->channel)

📌 注: htim->Init.Period 即 ARR 值。库不修改定时器周期,仅调整 CCR(比较寄存器),因此 频率由用户初始化定时器时设定,PwmSpeaker 仅负责音调切换

3.2 音调控制 API

/**
 * @brief 设置当前播放频率(Hz),立即生效
 * @param hspk: PwmSpeaker 句柄
 * @param freq_hz: 目标频率,范围 20–20000 Hz
 * @retval HAL_StatusTypeDef
 */
HAL_StatusTypeDef pwmspeaker_set_frequency(PwmSpeakerHandle_t *hspk, uint32_t freq_hz);

/**
 * @brief 播放指定频率持续指定毫秒数(阻塞式)
 * @param hspk: PwmSpeaker 句柄
 * @param freq_hz: 频率(Hz)
 * @param duration_ms: 持续时间(毫秒),0 表示无限长(需手动停止)
 * @retval HAL_StatusTypeDef
 */
HAL_StatusTypeDef pwmspeaker_play_tone(PwmSpeakerHandle_t *hspk,
                                       uint32_t freq_hz,
                                       uint32_t duration_ms);

/**
 * @brief 停止当前播放(关闭 PWM 输出)
 * @param hspk: PwmSpeaker 句柄
 * @retval HAL_StatusTypeDef
 */
HAL_StatusTypeDef pwmspeaker_stop(PwmSpeakerHandle_t *hspk);

pwmspeaker_set_frequency() 是核心函数,其实现逻辑严格遵循定时器更新事件规范:

HAL_StatusTypeDef pwmspeaker_set_frequency(PwmSpeakerHandle_t *hspk, uint32_t freq_hz) {
    if (freq_hz < 20 || freq_hz > 20000) return HAL_ERROR;

    // 1. 计算新 ARR 值(假设时钟已知,此处以 84 MHz 为例)
    uint32_t new_arr = (84000000U / freq_hz) - 1U;
    if (new_arr > 0xFFFFU) new_arr = 0xFFFFU; // 限幅

    // 2. 禁用更新事件中断(若已启用)
    __HAL_TIM_DISABLE_IT(hspk->htim, TIM_IT_UPDATE);

    // 3. 写入影子寄存器
    __HAL_TIM_SET_AUTORELOAD(hspk->htim, new_arr);
    __HAL_TIM_SET_COMPARE(hspk->htim, hspk->channel, 
                          (hspk->volume_level * (new_arr + 1U)) / 100U);

    // 4. 生成更新事件,使新值生效
    HAL_TIM_GenerateEvent(hspk->htim, TIM_EVENTSOURCE_UPDATE);

    // 5. 重新使能更新中断(保持原有状态)
    __HAL_TIM_ENABLE_IT(hspk->htim, TIM_IT_UPDATE);

    return HAL_OK;
}

pwmspeaker_play_tone() 在裸机环境下使用 HAL_Delay() 实现阻塞;在 FreeRTOS 下则创建临时任务或使用 vTaskDelay() ,避免阻塞调度器。其伪代码为:

// FreeRTOS 版本
void play_task(void *pvParameters) {
    PwmSpeakerHandle_t *hspk = (PwmSpeakerHandle_t*)pvParameters;
    uint32_t freq = *(uint32_t*)pvParameters;
    uint32_t ms = *(uint32_t*)((uint8_t*)pvParameters + sizeof(uint32_t));

    pwmspeaker_set_frequency(hspk, freq);
    vTaskDelay(pdMS_TO_TICKS(ms));
    pwmspeaker_stop(hspk);
    vTaskDelete(NULL);
}

HAL_StatusTypeDef pwmspeaker_play_tone(...) {
    xTaskCreate(play_task, "SPK_PLAY", 128, &params, 1, NULL);
    return HAL_OK;
}

3.3 高级功能:音阶映射与旋律播放

为简化音乐编程,库内置国际标准音高(A4 = 440 Hz)十二平均律计算:

// 音符枚举(C4 = 261.63 Hz)
typedef enum {
    NOTE_C4  = 0,  NOTE_CS4 = 1,  NOTE_D4  = 2,  NOTE_DS4 = 3,
    NOTE_E4  = 4,  NOTE_F4  = 5,  NOTE_FS4 = 6,  NOTE_G4  = 7,
    NOTE_GS4 = 8,  NOTE_A4  = 9,  NOTE_AS4 = 10, NOTE_B4  = 11,
    // ... 可扩展至 C8
} Note_t;

/**
 * @brief 根据音符编号和八度计算频率
 * @param note: 音符(NOTE_C4 等)
 * @param octave: 八度(4 表示中央 C 所在八度)
 * @retval uint32_t: 计算出的频率(Hz,四舍五入取整)
 */
uint32_t pwmspeaker_note_to_freq(Note_t note, uint8_t octave);

计算公式为:
$$ f = 440 \times 2^{\frac{(note - 9) + 12 \times (octave - 4)}{12}} $$
其中 note - 9 是相对于 A4 的半音数(A4 编号为 9)。

用户可构建旋律数组:

typedef struct {
    uint32_t frequency;  // Hz
    uint16_t duration_ms; // 毫秒
} Tone_t;

const Tone_t melody[] = {
    {pwmspeaker_note_to_freq(NOTE_C4, 4), 500},
    {pwmspeaker_note_to_freq(NOTE_E4, 4), 500},
    {pwmspeaker_note_to_freq(NOTE_G4, 4), 500},
    {pwmspeaker_note_to_freq(NOTE_C5, 5), 1000},
};

// 播放旋律(FreeRTOS 环境)
void play_melody_task(void *pvParameters) {
    for (int i = 0; i < sizeof(melody)/sizeof(Tone_t); i++) {
        pwmspeaker_play_tone(&hspk, melody[i].frequency, melody[i].duration_ms);
        vTaskDelay(pdMS_TO_TICKS(100)); // 音符间隔
    }
    vTaskDelete(NULL);
}

4. 典型应用工程实践

4.1 STM32CubeMX 配置指南

以 STM32F407VG 为例,使用 CubeMX 生成初始化代码:

  1. RCC :HSE = 8 MHz,PLL 配置为 168 MHz(APB1 = 42 MHz,APB2 = 84 MHz)
  2. TIM1
    • Clock Source → Internal Clock
    • Counter Settings → Prescaler = 0, Counter Period = 8399(对应 10 kHz 基准,便于计算)
    • Channel 1 → PWM Generation CH1,Polarity = High
    • Master Configuration → Update Event Request → Enable
  3. GPIOA Pin 8
    • GPIO Mode → Alternate Function Push-Pull
    • GPIO Pull-up/Pull-down → No Pull-up and No Pull-down
    • Maximum Output Speed → High
    • Channel 1 Remap → Full Remap(若使用 PA8)
  4. 生成代码 ,在 main.c 中添加:
#include "pwmspeaker.h"

PwmSpeakerHandle_t hspk;
PwmSpeakerConfig_t spk_config = {
    .htim = &htim1,
    .channel = TIM_CHANNEL_1,
    .pwm_gpio_pin = GPIO_PIN_8,
    .pwm_gpio_port = GPIOA,
    .volume_level = 60 // 60% 占空比
};

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM1_Init(); // 此函数由 CubeMX 生成

    if (pwmspeaker_init(&hspk, &spk_config) != HAL_OK) {
        Error_Handler(); // 初始化失败处理
    }

    // 播放中央 C 音(262 Hz)持续 1 秒
    pwmspeaker_play_tone(&hspk, 262, 1000);

    while (1) {
        // 主循环可处理其他任务
    }
}

4.2 FreeRTOS 集成与多任务协同

在 FreeRTOS 环境中,需确保 PWM 控制不干扰高优先级任务。推荐做法:

  • pwmspeaker_play_tone() 封装为独立低优先级任务(如 tskIDLE_PRIORITY + 1
  • 若需在中断中触发声音(如按键按下),使用 xQueueSendFromISR() 将音调指令发往播放任务队列
  • 为避免多个任务同时调用 pwmspeaker_set_frequency() 导致竞争,可在 pwmspeaker.c 中添加互斥锁:
static SemaphoreHandle_t xSemaphore = NULL;

HAL_StatusTypeDef pwmspeaker_init(...) {
    // ... 原有初始化代码
    xSemaphore = xSemaphoreCreateMutex();
    return HAL_OK;
}

HAL_StatusTypeDef pwmspeaker_set_frequency(...) {
    if (xSemaphore == NULL) return HAL_ERROR;
    if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
        // 执行频率设置...
        xSemaphoreGive(xSemaphore);
        return HAL_OK;
    }
    return HAL_ERROR;
}

4.3 故障排查与性能优化

现象 可能原因 解决方案
完全无声 GPIO 未配置为复用模式;PWM 未启动;驱动电路断路 用示波器测 PA8 波形;检查 HAL_TIM_PWM_Start() 返回值
声音微弱/失真 占空比过低(<20%);RC 滤波参数不当;电源不足 调高 volume_level ;增大 Cf 至 470 nF;检查 VDD 纹波
频率不准(偏低) 定时器时钟源配置错误;ARR 计算溢出 检查 htim->Init.ClockDivision ;启用 __HAL_TIM_GET_COUNTER() 实时监控
播放中出现“咔嗒”声 频率切换未在周期边界;占空比突变 确保调用 HAL_TIM_GenerateEvent(..., TIM_EVENTSOURCE_UPDATE) ;避免从 0% 突变到 80%
CPU 占用率异常高 错误使用 pwmspeaker_play_tone() 在裸机中阻塞主循环 改用非阻塞 API 或迁移到 FreeRTOS 任务中

性能关键点

  • pwmspeaker_set_frequency() 执行时间 < 1.5 µs(Cortex-M4 @ 168 MHz),对实时性无影响
  • 内存占用:静态分配句柄仅 24 字节,无堆内存申请
  • 最大支持并发音调数 = 硬件 PWM 通道数(如 TIM1 支持 4 通道,即可同时播放 4 个不同频率)

5. 与其他音频方案的对比评估

特性 PwmSpeaker DAC + Timer(正弦查表) I²S + Codec(如 CS43L22)
硬件需求 1 个 TIM + GPIO + 简单驱动 1 个 DAC + 1 个 TIM + SRAM I²S 外设 + Codec + 时钟树
音频质量(THD+N) ~5%(方波滤波后) ~1%(12-bit DAC) <0.005%(24-bit,192 kHz)
CPU 占用 极低(仅配置寄存器) 中(每 20 µs 中断填充 DAC) 低(DMA 自动传输)
内存占用 < 100 字节 ≥ 2 KB(正弦表) ≥ 4 KB(缓冲区)
开发复杂度 极简(5 行代码可发声) 中(需理解采样定理、查表) 高(需配置 I²S、Codec 寄存器)
典型应用场景 提示音、警报、教育实验 语音合成、测试音发生器 高保真音乐播放、语音助手

结论: 当项目需求聚焦于“低成本、低功耗、快速响应、确定性时序”的提示音场景时,PwmSpeaker 是不可替代的最优解 。它用最朴素的硬件资源,实现了嵌入式音频中最本质的功能——让机器发出可识别、可控制的声音。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐