先上演示效果

STM32DAC

DAC(Digital-to-Analog Converter)是一种将数字信号转换为模拟信号的器件,能够根据数字量生成相应比例的电压值。STM32系列微控制器通常配备8位或12位DAC模块。

在介绍音乐播放功能前,有必要简要说明几种常见的音频格式:

  1. MP3:采用有损压缩的音频格式
  2. WAV:无损音频格式,标准WAV文件采用与CD相同的参数(44.1kHz采样频率,16位量化)
  3. PCM:直接将模拟音频信号转换为数字脉冲序列的记录方式,由未经编码或压缩的1/0数字信号组成

还需要关注一下采样率位深度

采样率很好理解,把一秒分成很多帧,采样率越高声音越细致。

位深度可以理解为声音的高低大小,深度越大,声音越精准。例如8bit对应256个层级。

前置准备

MP3转WAV - 免费在线将MP3文件转换成WAV

本次使用的是STM32F407VET6 512KB FLASH

我这里使用16KHz 8Bit PCM格式的音频数据,8Bit的数据对应一个Byte,16KHz采样,1秒种占用存储空间就是16K Byte,理论上可以存储512K/16K = 32秒的音频。那本次就截取30s的音频。

然后通过以下python脚本把对应的WAV文件转成PCM。(在线的转换很少能指定8bit和任意采样率,为了高可复制性,直接python去做)

import wave
import numpy as np
import os
import glob


def wav_to_pcm(input_file, output_file, target_sample_rate=16000, target_bits=8, target_channels=1):
    """
    将WAV文件转换为指定格式的PCM文件

    参数:
    input_file: 输入的WAV文件路径
    output_file: 输出的PCM文件路径
    target_sample_rate: 目标采样率 (默认16000Hz)
    target_bits: 目标位深度 (默认8bit)
    target_channels: 目标声道数 (默认1声道)
    """

    try:
        # 打开WAV文件
        with wave.open(input_file, 'rb') as wav_in:
            # 获取原始音频参数
            original_channels = wav_in.getnchannels()
            original_sample_width = wav_in.getsampwidth()
            original_sample_rate = wav_in.getframerate()
            original_frames = wav_in.getnframes()

            print(f"处理文件: {os.path.basename(input_file)}")
            print(f"  原始声道数: {original_channels}")
            print(f"  原始采样宽度: {original_sample_width * 8} bit")
            print(f"  原始采样率: {original_sample_rate} Hz")
            print(f"  原始时长: {original_frames / original_sample_rate:.2f} 秒")

            # 读取音频数据
            audio_data = wav_in.readframes(original_frames)

            # 将字节数据转换为numpy数组
            if original_sample_width == 1:
                # 8bit无符号
                audio_array = np.frombuffer(audio_data, dtype=np.uint8)
            elif original_sample_width == 2:
                # 16bit有符号
                audio_array = np.frombuffer(audio_data, dtype=np.int16)
            elif original_sample_width == 4:
                # 32bit有符号
                audio_array = np.frombuffer(audio_data, dtype=np.int32)
            else:
                raise ValueError(f"不支持的采样宽度: {original_sample_width}")

            # 重塑数组以处理多声道
            if original_channels > 1:
                audio_array = audio_array.reshape(-1, original_channels)
                # 转换为单声道 (取平均值)
                audio_array = np.mean(audio_array, axis=1)

            # 重采样到目标采样率
            if original_sample_rate != target_sample_rate:
                # 简单的线性插值重采样
                ratio = target_sample_rate / original_sample_rate
                new_length = int(len(audio_array) * ratio)
                indices = np.linspace(0, len(audio_array) - 1, new_length)
                audio_array = np.interp(indices, np.arange(len(audio_array)), audio_array)

            # 转换为8bit无符号格式
            if target_bits == 8:
                # 归一化到0-255范围
                if audio_array.max() != audio_array.min():
                    audio_array = ((audio_array - audio_array.min()) / (
                                audio_array.max() - audio_array.min()) * 255).astype(np.uint8)
                else:
                    # 处理静音或恒定值的情况
                    audio_array = np.full(len(audio_array), 128, dtype=np.uint8)

            print(f"  转换后: {target_channels}声道, {target_bits}bit, {target_sample_rate}Hz")
            print(f"  转换后时长: {len(audio_array) / target_sample_rate:.2f} 秒")

            # 写入PCM文件
            with open(output_file, 'wb') as pcm_out:
                pcm_out.write(audio_array.tobytes())

            print(f"  ✓ 转换完成: {os.path.basename(output_file)}")

    except Exception as e:
        print(f"  ✗ 转换失败: {str(e)}")
        return False

    return True


def batch_convert_wav_to_pcm(directory=".", target_sample_rate=16000, target_bits=8, target_channels=1):
    """
    批量转换目录下的所有WAV文件为PCM格式

    参数:
    directory: 工作目录 (默认为当前目录)
    target_sample_rate: 目标采样率
    target_bits: 目标位深度
    target_channels: 目标声道数
    """

    # 获取目录下所有WAV文件
    wav_files = glob.glob(os.path.join(directory, "*.wav"))
    wav_files.extend(glob.glob(os.path.join(directory, "*.WAV")))

    if not wav_files:
        print(f"在目录 '{directory}' 中没有找到WAV文件")
        return

    print(f"找到 {len(wav_files)} 个WAV文件:")
    for wav_file in wav_files:
        print(f"  - {os.path.basename(wav_file)}")

    print(f"\n开始批量转换...")
    print(f"目标格式: {target_channels}声道, {target_bits}bit, {target_sample_rate}Hz")
    print("-" * 50)

    success_count = 0
    total_count = len(wav_files)

    for wav_file in wav_files:
        # 生成输出文件名 (在同一目录下)
        base_name = os.path.splitext(os.path.basename(wav_file))[0]
        output_file = os.path.join(directory, f"{base_name}_16k_8bit_mono.pcm")

        # 执行转换
        if wav_to_pcm(wav_file, output_file, target_sample_rate, target_bits, target_channels):
            success_count += 1

        print()  # 空行分隔

    print("-" * 50)
    print(f"批量转换完成!")
    print(f"成功: {success_count}/{total_count}")

    if success_count < total_count:
        print(f"失败: {total_count - success_count} 个文件")


def main():
    import argparse

    parser = argparse.ArgumentParser(description='批量将WAV文件转换为16KHz 8Bit 单声道PCM格式')
    parser.add_argument('-d', '--directory', default='.', help='工作目录 (默认为当前目录)')
    parser.add_argument('--sample-rate', type=int, default=16000, help='目标采样率 (默认16000Hz)')
    parser.add_argument('--bits', type=int, default=8, help='目标位深度 (默认8bit)')
    parser.add_argument('--channels', type=int, default=1, help='目标声道数 (默认1声道)')

    args = parser.parse_args()

    # 检查目录是否存在
    if not os.path.exists(args.directory):
        print(f"错误: 目录 '{args.directory}' 不存在")
        return

    # 执行批量转换
    batch_convert_wav_to_pcm(
        args.directory,
        args.sample_rate,
        args.bits,
        args.channels
    )


if __name__ == "__main__":
    main()

把WAV文件放在和python一个目录下,然后点击运行即可生成对应的PCM文件。

然后通WinHex软件打开,复制为C源码格式

然后把数组名字改成这个:const unsigned char data,并作为一个.h文件在项目中

环境配置

打开CUBEMX,配置基本的时钟 debug 168MHZ.然后激活DAC,OUT1单声道,并且触发源选择TIM6。

然后DMA配置为Normal,数据宽度选Byte(因为我们传的8bit)

然后配置TIM6,TIM6对应APB1 84MHZ,ARR配置为5249,即为16khz。那么这样就相当于TIM6以16khz的频率更新,无需配置中断,很方便。

就这样生成代码即可。

代码编写

需要注意的STM32DAC一次传的大小只有16位 对应65535。那么音乐肯定没办法一空气放完,那我们就分开来。

/* USER CODE BEGIN PV */
// 分块传输相关变量
static uint32_t current_offset = 0;  // 当前数据偏移
static uint32_t total_length = 480000;  // 总数据长度
static uint32_t block_size = 65535;  // 每块大小(DMA最大传输计数)
static uint32_t block_count = 0;  // 当前块计数
/* USER CODE END PV */
  /* USER CODE BEGIN 2 */
	// 启动定时器,为DAC提供触发信号
	HAL_TIM_Base_Start(&htim6);
	// 启动DAC的DMA传输,播放PCM音频数据
	// 开始第一块数据传输
	HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)(data + current_offset), block_size, DAC_ALIGN_8B_R);
  /* USER CODE END 2 */
/* USER CODE BEGIN 3 */
    // 检查DMA传输是否完成
    if (HAL_DAC_GetState(&hdac) == HAL_DAC_STATE_READY)
    {
      // 计算下一块数据的偏移和大小
      current_offset += block_size;
      block_count++;
      
      // 检查是否还有更多数据需要传输
      if (current_offset < total_length)
      {
        // 计算当前块的实际大小
        uint32_t remaining = total_length - current_offset;
        uint32_t current_block_size = (remaining > block_size) ? block_size : remaining;
        
        // 启动下一块数据传输
        HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)(data + current_offset), current_block_size, DAC_ALIGN_8B_R);
      }
      else
      {
        // 所有数据已传输完成,重新开始
        current_offset = 0;
        block_count = 0;
        HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)data, block_size, DAC_ALIGN_8B_R);
      }
    }

  }
  /* USER CODE END 3 */

另外提一嘴,声音的质感和运放以及喇叭都有关系。编者这次用的是淘宝2块钱的5128D类功放模块,结合8bit的采样,音质只能说就那样吧。下期文章我们直接从SD卡中解析完整的完全的音频进行播放。

Logo

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

更多推荐