1 概述

1.1 实验目的

        本实验旨在通过 STM32 微控制器的 通用定时器输出比较功能,配置并产生 PWM 波形,从而驱动无源蜂鸣器实现乐音的输出与歌曲的演奏。实验展示了定时器 PWM 信号的配置方法、频率与占空比的调节原理,以及将音乐音符转化为对应 PWM 频率的实现过程。通过完整的代码实现与参数设置,帮助读者深入理解 STM32 定时器在 PWM 信号生成中的作用,以及其在外设驱动中的应用方式。

        通过本实验,读者不仅能够掌握 STM32 通用定时器输出比较模式 的基本原理和实际使用方法,还能熟悉 PWM 信号与音频频率的对应关系,理解如何通过占空比与频率控制实现音调和音量的变化。在实践过程中,读者将能够完成无源蜂鸣器的基本驱动,并进一步扩展至 乐曲演奏应用。这对于日后在嵌入式系统中实现 声音提示、音乐播放、报警提醒等功能 提供了技术积累和实践经验,具有重要的教学意义与工程应用价值。

1.2 定时器介绍

以下为STM32F103ZET6芯片的定时器介绍,如果是STM32F103C8T6的定时器有所裁剪

定时器类型 定时器 通道数 功能 典型应用场景
高级控制定时器 TIM1、TIM8 4 + 互补输出 输出比较(含 PWM 模式,带死区/互补) 电机控制(FOC)、逆变器、半桥/全桥驱动、舵机控制
刹车输入 电机过流/过压保护
编码器接口 高精度转速/角度检测
外部触发同步 多定时器联动控制
通用定时器 TIM2、TIM3、TIM4、TIM5 4 输出比较(PWM输出) 蜂鸣器音调、LED 呼吸灯、舵机角度控制、定时脉冲输出、
输入捕获(PWM测量) 超声波测距、信号频率/脉宽测量
单脉冲模式 输出单次脉冲信号
编码器接口 旋转编码器测速/位置检测
外部时钟模式 外部事件计数(转速传感器)
基本定时器 TIM6、TIM7 简单定时 软件定时、延时
DAC 触发 DAC 周期采样、波形输出
定时中断 周期性任务触发
系统定时器 SysTick 1 系统时基 / 递减计数 RTOS 心跳节拍(1ms Tick)、延时函数、任务调度

加粗部分是必须掌握也是最常用的功能。

1.3 输出比较原理

        输出比较是 STM32 定时器的一种工作模式,通过比较计数器值(CNT)与比较寄存器值(CCR),在匹配时改变定时器通道的输出状态,实现精确的脉冲输出和 PWM 波形生成。

CNT(计数器) :定时器内部递增(或递减)的计数值,用于计时。
ARR(自动重装载寄存器) :设定计数器的最大值,计数器到达 ARR 后清零或反向计数,形成周期性计数。
CCR(比较寄存器 ):存储阈值,当 CNT = CCR 时触发事件,改变输出引脚状态。

定时器开始计数,CNT 从 0 增加到 ARR。
当 CNT < CCR 时,输出引脚保持初始状态(高或低电平)。
当 CNT = CCR 时,根据通道模式(PWM1、PWM2 或普通 OC 模式),输出引脚翻转或置高/置低。
当 CNT 达到 ARR,计数器重置,下一周期重新开始。

在 PWM 模式下,输出比较用于生成占空比可调的方波:

  • PWM 占空比:由 CCR/ARR 决定

Duty Cycle = \frac{CCR}{ARR+1}\times 100

  • PWM 频率:由 ARR 和预分频器 PSC 决定

    PWM_Frequency=\frac{TimerClock}{(PSC+1)\cdot (ARR+1)}

        在实际应用中,PWM 波形的生成通常有两类需求:变频定占空比定频变占空比。在 STM32F103 系列单片机的实验中,可以通过合理配置定时器的预分频系数(PSC)和自动重装载寄存器(ARR),达到简化设置与控制的目的。

  1. 变频定占空比

    • 适用于需要频率可变而占空比相对固定的场合。

    • 一般情况下,STM32F103 的系统主频为 72 MHz。若将 PSC 设置为 71,则定时器的计数频率为 1 MHz(即 1 μs 计数一次)。

    • 在此配置下,只需通过修改 ARR 的值即可直接改变 PWM 输出的频率,而无需调整占空比。

    • 实验中通常将占空比固定为 50%,从而输出标准的方波信号,这样能保证输出波形能量稳定,特别适合用于 无源蜂鸣器的音调控制舵机驱动 等场景。

  2. 定频变占空比

    • 适用于需要频率恒定,而占空比可调的场合。

    • 常见的设置方法是将 PSC 配置为 7199,ARR 配置为 99,此时输出 PWM 信号的频率为 100 Hz

    • 在该配置下,只需调节 CCR 的取值范围(0~100),即可方便地实现占空比在 0%~100% 之间的线性变化。

    • 该方式常用于 LED 调光/呼吸灯 以及 直流电机转速控制 等应用。

1.4 蜂鸣器

在本实验中,需要注意 蜂鸣器的驱动特性

  1. 单片机直接驱动限制

    • 不带驱动电路的蜂鸣器,直接连接到单片机 IO 引脚是无法正常工作的,下图为无驱动电路的蜂鸣器。
       

    • 与 LED 灯不同,LED 所需电流较小,单片机 IO 口可以直接驱动;而蜂鸣器所需电流较大,需要借助 驱动电路(如三极管、MOSFET 等)进行电流放大,才能可靠驱动蜂鸣器发声,下图为蜂鸣器模块,集成了驱动电路。

  2. 有源蜂鸣器 vs 无源蜂鸣器

    • 有源蜂鸣器:接通电源即发声,断电即停止,发声频率固定,无法改变音调,因此不适合本实验播放音乐。

    • 无源蜂鸣器:本实验采用无源蜂鸣器,通过 占空比为 50% 的 PWM 波 驱动。改变 PWM 频率即可改变蜂鸣器的音调,从而播放不同音高的音符,实现音乐演奏。

        通过使用无源蜂鸣器和 PWM 控制,本实验能够演示 如何通过定时器输出比较功能调整 PWM 频率,实现音调变化和简单音乐播放。

2 STM32CubeMx配置

2.1 SYS设置

2.2 RCC设置

2.3 TIM3设置

3 VSCode

3.1 驱动代码

#include "beep.h"

#include "oled12864.h" 
#include "ds3231.h"
#include "usart.h"
#include "tim.h"

// 音符频率定义(单位:Hz)
#define NOTE_C6  1046
#define NOTE_D6  1171
#define NOTE_E6  1319
#define NOTE_F6  1396
#define NOTE_G6  1567
#define NOTE_A6  1760
#define NOTE_B6  1975
#define NOTE_C7  2093


// ================= 音符时值(单位:ms) =================
#define WHOLE     1000
#define HALF      500
#define QUARTER   250
#define EIGHTH    125
#define REST      0   // 休止符
 

// 小星星旋律(音符频率数组)
const float melody1[] = {
    NOTE_C6, NOTE_C6, NOTE_G6, NOTE_G6, NOTE_A6, NOTE_A6, NOTE_G6,  // 第一小节
    NOTE_F6, NOTE_F6, NOTE_E6, NOTE_E6, NOTE_D6, NOTE_D6, NOTE_C6,  // 第二小节
    NOTE_C6, NOTE_C6, NOTE_G6, NOTE_G6, NOTE_A6, NOTE_A6, NOTE_G6,  // 第三小节
    NOTE_F6, NOTE_F6, NOTE_E6, NOTE_E6, NOTE_D6, NOTE_D6, NOTE_C6   // 第四小节
};

// 音符时长数组(与 melody 对应)
const int durations1[] = {
    QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, HALF,    // 第一小节
    QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, HALF,    // 第二小节
    QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, HALF,    // 第三小节
    QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, HALF     // 第四小节
};

// 欢乐颂 
const float melody2[] = {
    NOTE_E6, NOTE_E6, NOTE_F6, NOTE_G6,   // E E F G
    NOTE_G6, NOTE_F6, NOTE_E6, NOTE_D6,   // G F E D
    NOTE_C6, NOTE_C6, NOTE_D6, NOTE_E6,   // C C D E
    NOTE_E6, NOTE_D6, NOTE_D6,            // E D D

    NOTE_E6, NOTE_E6, NOTE_F6, NOTE_G6,   // E E F G
    NOTE_G6, NOTE_F6, NOTE_E6, NOTE_D6,   // G F E D
    NOTE_C6, NOTE_C6, NOTE_D6, NOTE_E6,   // C C D E
    NOTE_D6, NOTE_C6, NOTE_C6             // D C C
};

// 每个音符对应的时长(四分音符为主,句尾用半拍)
const int durations2[] = {
    QUARTER, QUARTER, QUARTER, QUARTER,
    QUARTER, QUARTER, QUARTER, QUARTER,
    QUARTER, QUARTER, QUARTER, QUARTER,
    QUARTER, QUARTER, HALF,

    QUARTER, QUARTER, QUARTER, QUARTER,
    QUARTER, QUARTER, QUARTER, QUARTER,
    QUARTER, QUARTER, QUARTER, QUARTER,
    QUARTER, QUARTER, HALF
};




void PlayNote(float freq, int duration) {
    if (freq == 0) {  // 休止符
        HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_3);  // 停止 PWM 信号
    } else {
        TIM3_CH3_SetPWM(freq, 50);   
    }
    HAL_Delay(duration);  // 播放音符持续时间
    // HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_3);  // 停止当前音符
    TIM3_CH3_SetPWM(freq, 0);
    HAL_Delay(50);  // 音符间短暂静音
    // alarm_display_update();
}

void play_music(uint8_t music){
  if(music == 1){
    for (int i = 0; i < 28; i++) {
        PlayNote(melody1[i], durations1[i]);
    }
  }else if (music == 2)
  {
    for (int i = 0; i < 30; i++) {
        PlayNote(melody2[i], durations2[i]);
    }
  }
}
#ifndef __BEEP_H__
#define __BEEP_H__

#include "stm32f1xx_hal.h"  // 根据您的STM32系列修改
#include <string.h>
#include <stdint.h>
#include <stdbool.h>


// 函数声明
void PlayNote(float freq, int duration);
void play_music(uint8_t music);

#endif /* __BEEP_H__ */

3.2 main代码

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "beep.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_USART1_UART_Init();
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */

  //启TIM3
  HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);
  // 播放旋律
  play_music(1);
  play_music(2);
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
     HAL_Delay(500);
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

4 实验结论

        通过以上代码,本实验演示了如何利用 STM32 通用定时器的输出比较功能(Output Compare, OC),生成 PWM 波 驱动无源蜂鸣器,实现音乐播放。

在实验过程中,学生不仅掌握了 PWM 波的频率与占空比设置方法,还能理解定时器的输出比较功能在实际应用中的作用:

  • 通过调整 ARR(自动重装载寄存器) 可以改变 PWM 频率,从而控制音符高低;

  • 通过调整 CCR(比较寄存器) 可以改变占空比,保证蜂鸣器方波信号稳定且音质清晰。

利用这些知识,实验实现了播放简单而优美的音乐,如《小星星》《欢乐颂》《送别》等。同时,这种技术也可以拓展到多种 预警与提醒场景

  • 可用于 闹钟提示,播放自定义音乐提醒时间;

  • 可通过组合不同频率与时长的音符,实现 不同类型的预警,如警车、救护车、消防车等模拟警报。

本实验不仅提升了学生对 PWM 控制、定时器输出比较功能 的理解,还为嵌入式系统中的 声音输出与报警提示设计提供了实践基础,具有较强的工程应用价值和教学意义。

Logo

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

更多推荐