
简介这份资源面向嵌入式开发初学者与STM32学习者围绕STM32F103ZET6内置12位DAC实现音频输出这一实验展开帮助读者理解数字信号到模拟电压的转换过程并完成从WAV音频数据到扬声器发声的完整链路。压缩包共181个文件约5.54MB以c与h源码、o与d编译中间文件、crf与lst等工程记录为主另含wav音频素材、txt说明、Keil工程文件及hex、axf等烧录产物结构完整可直接用于复现实验。目前已有4862人学习下载。资源涵盖DAC初始化、时钟与GPIO配置、定时器触发采样、DMA搬运数据以及外接功放驱动扬声器等关键环节读者可据此掌握音频采样率匹配、缓冲输出与噪声排查思路为后续开发更复杂的嵌入式音频系统打下基础。1. 从“能响就行”到“波形可控”STM32F103 DAC音频输出实验到底要解决什么很多人第一次碰 STM32F103 的 DAC目标都很朴素——让蜂鸣器或者耳机里出点声音证明这块芯片“能响”。但真到要交一份完整的课程设计或者实验报告时问题就来了声音是出来了可波形长什么样频率准不准换首歌还能不能放采样率一改为什么全是噪声这套 DAC 音频输出实验要解决的恰恰不是“能不能响”而是“波形可控、参数可算、结果可复现”。它适合正在做嵌入式课设的学生、需要一份完整音频输出链路的从业者以及想把 DAC 从“玄学调参”变成“按公式算参数”的人。下面这套方案从需求、选型、设计到验收按能直接抄作业的粒度展开。2. 需求拆解与技术选型为什么是 STM32F103 的 DAC 而不是 PWM 凑合2.1 先明确音频输出的三个硬指标做音频输出实验最怕一上来就写代码。我一般会先把指标定死否则后面调参全靠猜。这套实验的核心指标有三个第一是输出波形类型至少要覆盖正弦波、方波、三角波最好能扩展到任意波形表第二是频率范围人耳可听范围是 20Hz 到 20kHz但受限于 STM32F103 的 DAC 建立时间和定时器触发频率实际能稳定输出的正弦波上限通常在 10kHz 左右方波可以更高第三是采样率与更新率DAC 每输出一个点都需要一次触发触发频率决定了波形的“细腻程度”。把这三个指标写进需求文档后面选型才有依据。很多同学翻车就翻在没定指标直接抄一段 DMA 代码结果频率对不上改一个参数全乱套。2.2 为什么不用 PWM 加滤波而用片内 DACSTM32F103 的 PWM 加 RC 滤波确实能出模拟波形成本也低但它的致命伤是波形质量高度依赖滤波器的截止频率和负载阻抗换个耳机、改个音量波形就变形。而 STM32F103 自带的 12 位 DAC 是真正的数模转换输出阻抗稳定配合 DMA 可以做到“算好一张表硬件自动播”。选型结论很明确需要波形保真度和可重复性就用片内 DAC只是做个报警提示音PWM 足够。这套实验既然叫“DAC 音频输出”就老老实实走 DAC 通道。2.3 关键外设组合DAC DMA 定时器STM32F103 的 DAC 有两个通道通道 1 对应 PA4通道 2 对应 PA5。要让 DAC 连续输出不同电压必须解决“谁来喂数据”的问题。CPU 轮询写 DAC 寄存器频率一高就顾不上别的用中断写中断太频繁会拖垮系统。标准做法是 DAC DMA 定时器触发定时器常用 TIM2 或 TIM6产生固定频率的触发信号每次触发DAC 从 DMA 搬运一个数据到数据保持寄存器DMA 循环模式数据搬完自动从头开始。这样 CPU 只在初始化时配置一次之后完全不参与波形输出。下面这张表把三个外设的职责和关键参数列清楚外设职责关键参数典型配置DAC数模转换分辨率、触发源、输出缓冲12 位TIM6 触发开启输出缓冲DMA数据搬运传输方向、循环模式、数据宽度外设到 DAC循环半字定时器触发节拍预分频、自动重装值72MHz 下分频到目标采样率提示DAC 的输出缓冲建议开启否则带负载能力很弱接耳机几乎听不到声音。3. 总体设计波形表生成、DMA 搬运与定时器触发的三段式结构3.1 波形数据表的生成方式DAC 输出正弦波的本质是把一个周期的正弦函数离散成 N 个点每个点算出一个 0 到 4095 之间的整数12 位 DAC然后让 DMA 按顺序把这些整数喂给 DAC。N 越大波形越细腻但占用内存也越多。我一般取 128 或 256 个点兼顾精度和内存。生成正弦表有两种方式一是在代码里用sin()函数实时算二是提前算好存成常量数组。实时算会引入浮点运算STM32F103 没有硬件 FPU跑起来很慢所以推荐提前算好。下面是一段生成正弦表的 Python 脚本用来生成 C 语言数组import math POINTS 256 # 一个周期的采样点数 AMPLITUDE 2047 # 12位DAC满量程4095取一半偏置 OFFSET 2048 # 直流偏置保证波形在0~4095之间 print(const uint16_t sine_table[%d] { % POINTS) for i in range(POINTS): # 正弦值映射到0~4095并做四舍五入 value int(round(OFFSET AMPLITUDE * math.sin(2 * math.pi * i / POINTS))) print( %d, % value, end) if (i 1) % 8 0: print() print(};)这段脚本的关键参数是POINTS和AMPLITUDE。POINTS决定频率分辨率AMPLITUDE决定输出幅度。注意OFFSET取 2048 是为了让正弦波有正有负因为 DAC 只能输出 0 到 3.3V没有负电压。如果直接输出负值DAC 会截断成 0波形就废了。3.2 定时器触发频率与输出频率的换算这是整个实验里最容易算错的地方。DAC 每收到一次触发就输出一个点。如果波形表有 256 个点定时器触发频率是 f_trigger那么输出波形的频率就是f_out f_trigger / POINTS反过来想要输出 1kHz 的正弦波波形表 256 个点定时器触发频率就得是 256kHz。STM32F103 的 TIM6 挂在 APB1 总线上时钟通常是 72MHz如果系统时钟配置为 72MHz。要得到 256kHz 的触发频率预分频器和自动重装值这样算定时器计数频率 72MHz / (PSC 1)触发频率 计数频率 / (ARR 1)取 PSC 0则计数频率 72MHzARR 72MHz / 256kHz - 1 280。所以 PSC0ARR280就能得到约 256kHz 的触发频率对应 1kHz 正弦波输出。下面是一段初始化 TIM6 的代码示例// 假设系统时钟72MHzAPB1定时器时钟72MHz void TIM6_Init(uint16_t arr, uint16_t psc) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period arr; // 自动重装值 TIM_TimeBaseStructure.TIM_Prescaler psc; // 预分频 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, TIM_TimeBaseStructure); // 使能TIM6的触发输出用于触发DAC TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_Cmd(TIM6, ENABLE); }arr和psc就是上面算出来的值。改频率只需要改这两个参数不用动波形表。这里有个血泪经验改完arr和psc后一定要重新使能 DAC 的触发否则波形会停在最后一个值不动。3.3 DMA 配置与数据搬运的细节DMA 的配置决定了数据能不能顺畅地从内存搬到 DAC。STM32F103 的 DAC 通道 1 对应 DMA1 通道 3通道 2 对应 DMA1 通道 4。配置时要注意几个点外设地址是DAC-DHR12R1这是 DAC 通道 1 的 12 位右对齐数据寄存器内存地址是波形表数组的首地址传输方向是内存到外设数据宽度是半字16 位因为 DAC 数据寄存器是 16 位循环模式必须开启否则播完一遍就停了。void DAC_DMA_Init(uint16_t *wave_table, uint16_t table_size) { // 使能DMA1时钟和DAC时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(DAC-DHR12R1); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)wave_table; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 内存到外设 DMA_InitStructure.DMA_BufferSize table_size; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); // DAC配置使能通道1使用TIM6触发开启输出缓冲 DAC_InitTypeDef DAC_InitStructure; DAC_InitStructure.DAC_Trigger DAC_Trigger_T6_TRGO; DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); // 关键使能DAC的DMA请求 }这段代码里最容易漏的是最后一行DAC_DMACmd。没有它DMA 搬数据DAC 不认波形就是一条直线。另外DMA_BufferSize必须和波形表长度一致写错了要么只播一部分要么读越界。4. 模块实现从正弦波到任意波形的代码落地4.1 正弦波输出的完整调用链把前面的模块串起来主函数里只需要做三件事初始化波形表、初始化 DMA 和 DAC、启动定时器。下面是一个最小可运行的主函数框架extern const uint16_t sine_table[256]; // 由Python脚本生成 int main(void) { // 系统时钟初始化配置为72MHz SystemInit(); // 初始化DMA和DAC传入正弦表 DAC_DMA_Init((uint16_t *)sine_table, 256); // 初始化TIM6触发频率256kHz对应1kHz正弦波 // PSC0, ARR280 TIM6_Init(280, 0); while (1) { // 主循环空转波形由DACDMA定时器自动输出 } }这段代码跑起来后用示波器接 PA4应该能看到一个 1kHz 的正弦波峰峰值约 3.3V实际会略低因为输出缓冲有压降。如果波形频率不对先查 TIM6 的arr和psc如果波形幅度不对查正弦表的AMPLITUDE和OFFSET如果波形是一条直线查DAC_DMACmd有没有调用。4.2 方波和三角波的实现差异方波和三角波不需要单独写驱动只需要换一张波形表。方波表很简单前一半是 4095后一半是 0。三角波表是线性递增再线性递减。用同样的 Python 脚本生成即可POINTS 256 # 方波表 print(const uint16_t square_table[%d] { % POINTS) for i in range(POINTS): value 4095 if i POINTS // 2 else 0 print( %d, % value, end) if (i 1) % 8 0: print() print(};) # 三角波表 print(const uint16_t triangle_table[%d] { % POINTS) for i in range(POINTS): if i POINTS // 2: value int(4095 * i / (POINTS // 2)) else: value int(4095 * (POINTS - i) / (POINTS // 2)) print( %d, % value, end) if (i 1) % 8 0: print() print(};)换波形时只需要把DAC_DMA_Init的第二个参数换成对应的表并重新初始化 DMA 的DMA_BufferSize。注意DMA 传输过程中不能直接改内存地址必须先DMA_Cmd(DISABLE)改完再使能。4.3 频率动态调整的两种做法实验做到后面通常要求“按键调频率”或者“串口发指令改频率”。有两种做法第一种是改定时器的ARR和PSC。这种方法响应快但频率分辨率受限于定时器位数。比如要输出 1kHz 到 10kHzARR从 280 变到 28步进比较大低频段调不准。第二种是改波形表的点数。比如固定定时器触发频率 256kHz波形表 256 点对应 1kHz128 点对应 2kHz64 点对应 4kHz。这种方法频率是成倍变化的适合做倍频切换不适合连续调。我一般会结合两种粗调改点数细调改ARR。下面是一个改ARR的函数示例// 根据目标频率计算ARR值假设PSC0系统时钟72MHz波形表256点 void Set_Frequency(uint32_t target_freq) { // 触发频率 目标频率 * 波形表点数 uint32_t trigger_freq target_freq * 256; // ARR 72MHz / 触发频率 - 1 uint32_t arr 72000000 / trigger_freq - 1; TIM6-ARR arr; // 直接写寄存器立即生效 TIM6-EGR TIM_EGR_UG; // 产生更新事件让新值生效 }这里TIM6-EGR TIM_EGR_UG是必须的否则新写入的ARR要等到下一次溢出才生效调频率时会有延迟感。5. 避坑与常见问题排查DAC 音频输出最容易翻车的五个点5.1 波形是一条直线示波器什么都不显示现象代码编译下载后PA4 引脚电压恒定没有波形。原因最常见的是DAC_DMACmd没有调用DAC 没有发出 DMA 请求DMA 搬了数据但 DAC 不接收。其次是 DMA 通道选错DAC 通道 1 必须用 DMA1 通道 3通道 2 必须用 DMA1 通道 4。解决检查DAC_DMACmd(DAC_Channel_1, ENABLE)是否存在检查 DMA 初始化里用的是不是DMA1_Channel3用调试器看 DMA 的CNDTR寄存器是否在递减如果不递减说明 DMA 没启动。5.2 波形频率对不上差一倍或者差几倍现象想要 1kHz实际出来 2kHz 或者 500Hz。原因定时器时钟算错了。STM32F103 的 APB1 总线时钟在系统时钟 72MHz 时是 36MHz但定时器时钟会倍频到 72MHz。如果误以为 APB1 是 72MHz 再分频就会差一倍。另外波形表点数写错也会导致频率偏差。解决确认RCC_ClocksTypeDef里PCLK1_Frequency的值定时器时钟是它的两倍当 APB1 分频系数不为 1 时。用示波器测实际频率反推ARR和PSC是否正确。5.3 输出声音有“滋滋”噪声波形上有毛刺现象正弦波看起来还行但接耳机能听到明显的高频噪声。原因DAC 输出没有加低通滤波。DAC 输出的是阶梯波含有大量高频谐波人耳听到的就是“滋滋”声。另外如果 DMA 传输和定时器触发不同步也会产生周期性毛刺。解决在 PA4 和地之间加一个简单的 RC 低通滤波器截止频率取 20kHz 左右。电阻取 1kΩ电容取 10nF截止频率约 15.9kHz。如果还不行检查定时器触发源是否配置为TIM_TRGOSource_Update。5.4 改频率后波形卡死不动现象调用Set_Frequency后波形停在某个电压值不变。原因直接写TIM6-ARR后没有产生更新事件定时器还在用旧的ARR值计数但 DAC 的触发可能已经乱了。另外如果ARR设得太小触发频率超过 DAC 的建立时间DAC 输出会跟不上。解决写完ARR后立即写TIM6-EGR TIM_EGR_UG。同时确认ARR不小于 10否则触发频率超过 7MHzDAC 根本反应不过来。5.5 接耳机声音极小或者完全没声现象示波器看波形正常但接耳机几乎听不到。原因DAC 输出缓冲的驱动能力有限直接驱动 32Ω 耳机电压会被拉低。另外如果耳机是立体声插头只接了单声道可能只听到一边。解决加一个射极跟随器或者运放缓冲级提高驱动能力。实验阶段可以用一个 100Ω 电阻串联耳机虽然声音小但能听清。如果要驱动扬声器必须加功放模块。6. 验收与答辩怎么证明这套 DAC 音频输出实验真的做完了验收不是把代码跑起来就完事。我一般会按下面这张表逐项过每一项都有明确的通过标准验收项通过标准检查方法正弦波输出1kHz 正弦波峰峰值≥3V无明显失真示波器接 PA4测频率和幅度方波输出1kHz 方波上升沿10μs示波器测上升时间三角波输出1kHz 三角波线性度良好示波器观察斜率是否均匀频率可调通过按键或串口频率在 100Hz~5kHz 可调改频率后示波器读数跟随变化波形切换三种波形可切换切换后无卡死切换后示波器波形正确音频输出接耳机能听到对应频率的声音人耳听无明显噪声答辩时导师最常问的三个问题是定时器触发频率怎么算的DMA 为什么用循环模式波形表点数改了频率怎么变这三个问题分别对应第 3.2 节、第 3.3 节和第 4.3 节提前把公式和代码准备好现场直接画图解释。还有一个容易被忽略的点答辩演示时最好把示波器画面和代码同屏展示。一边改ARR一边看波形频率变化比单纯讲代码有说服力得多。我习惯在演示前把几个关键频率对应的ARR值写在纸上现场直接改不翻代码。从那以后我每次做 DAC 实验都强制先算一遍触发频率和ARR再写代码。这个习惯帮我省掉了至少三次“波形不对但不知道哪错了”的返工。希望这套方案能帮到你。本文还有配套的精品资源点击获取