
简介这份资源面向嵌入式开发初学者与STM32学习者围绕STM32F103ZET6内置12位DAC实现音频输出这一典型实验展开帮助读者理解数字信号到模拟电压的转换流程掌握定时器触发、DMA搬运与音频数据播放的完整思路。压缩包共181个文件约5.54MB以c与h源码、o与d编译中间文件、crf与lst链接记录为主另含wav音频素材、txt说明、hex与axf固件、uvprojx工程及sct分散加载文件覆盖从源码到可烧录固件的完整工程结构。目前已有4862人学习下载热度较高。资源包含可直接编译的Keil工程与音频转换素材读者可据此复现DAC播放实验学习WAV转C数组、采样率匹配、外接功放驱动扬声器等关键环节并借助工程文件排查时钟配置与数据搬运问题为后续嵌入式音频系统开发打下基础。1. 从需求切入为什么用 STM32F103 的 DAC 直接输出音频很多人第一次听到“用 STM32F103 DAC 输出音频”第一反应是DAC 不是用来输出直流基准电压的吗怎么还能放声音这正是这个实验最值得做的地方——它把“数模转换”这个抽象概念直接变成耳朵能听到的东西。STM32F103 系列内部集成了 2 路 12 位 DAC配合定时器触发和 DMA 搬运就能把一段预先算好的波形数据连续不断地送出去在 PA4 或 PA5 引脚上还原出模拟波形接上耳机或者功放就能听到声音。这套方案适合两类人一类是正在做嵌入式课程设计、需要交一个“看得见波形、听得见声音”的完整实验另一类是想搞明白 DAC 定时器 DMA 这条数据链路到底怎么协同工作的开发者。它不依赖外部音频编解码芯片成本极低但麻雀虽小五脏俱全采样率、波形表、触发源、缓冲策略这些音频播放的核心问题一个都绕不开。下面按“需求 → 设计 → 实现 → 验收”的顺序把整套方案拆开讲清楚。2. 总体方案设计DAC 定时器 DMA 三件套怎么分工2.1 数据链路与模块职责划分整个音频输出的核心链路是波形数据表内存→ DMA 通道 → DAC 数据寄存器 → DAC 模拟输出 → 外部功放/耳机。这条链路上有三个角色缺一不可。DAC 负责把数字量转成模拟电压STM32F103 的 DAC 是 12 位分辨率输出范围 03.3V对应数字量 04095。它本身不会自己“动”必须有人按固定节奏给它喂新数据这个节奏就是采样率。定时器负责提供这个节奏。比如要播放 8kHz 采样率的音频就让定时器每 125 微秒产生一次更新事件用这个事件去触发 DAC 转换。定时器的频率决定了采样率采样率决定了能还原多高的频率成分——根据奈奎斯特采样定理8kHz 采样率理论上最高还原 4kHz 的信号实际留余量能听清语音但听不了高音质音乐。DMA 负责“搬运”这件事。如果让 CPU 每次定时器中断都手动写 DAC 寄存器CPU 会被完全占死而且中断响应有抖动输出波形会失真。DMA 的好处是定时器触发一次DMA 自动从内存搬一个数据到 DAC全程不需要 CPU 干预。CPU 只负责在开头配置好之后就可以去干别的事。三者的分工可以这样理解定时器是节拍器DMA 是传送带DAC 是喇叭波形表是仓库里的货。节拍器一响传送带就送一件货到喇叭喇叭立刻放出来。2.2 关键参数计算采样率、定时器频率与波形表长度参数之间是联动的改一个要跟着改另一个。核心公式定时器触发频率 72MHz / ((PSC1) × (ARR1))其中 72MHz 是 STM32F103 定时器时钟APB1 定时器倍频后PSC 是预分频系数ARR 是自动重装载值。这个频率就是 DAC 的采样率。假设要 8kHz 采样率可以取 PSC0ARR8999则 72MHz / 9000 8kHz。也可以取 PSC8ARR999效果一样。选哪个看你对分辨率的需求一般让 ARR 大一点频率调节更细。波形表长度决定了一个周期波形的采样点数。比如要输出 1kHz 正弦波采样率 8kHz那一个周期就是 8 个点。8 个点还原正弦波比较粗糙会有明显量化噪声。通常的做法是波形表存多个周期或者存一个周期但点数取多通过调整定时器频率来改变输出频率。这里有个容易绕晕的地方输出频率 采样率 / 波形表一个周期的点数。如果波形表存的是完整一个周期的 100 个点采样率 8kHz那输出频率就是 80Hz。想输出 1kHz要么把采样率提到 80kHz要么把波形表一个周期的点数降到 8 个。实际项目中我一般会固定波形表为 256 点一个周期然后通过改定时器频率来调输出频率这样波形质量稳定。2.3 内存与缓冲策略单缓冲还是双缓冲DMA 传输模式有两种循环模式Circular和普通模式Normal。循环模式适合连续播放固定波形。DMA 把波形表从头搬到尾然后自动回到开头继续搬周而复始。播放正弦波测试音就用这个模式配置简单不需要 CPU 干预。普通模式适合播放一段有限长度的音频。DMA 搬完指定数量就停止需要 CPU 重新配置才能播下一段。如果要播放一段 WAV 音频通常用双缓冲DMA 搬缓冲区 A 的时候CPU 往缓冲区 B 填下一段数据A 搬完触发中断DMA 切到 BCPU 再往 A 填。这样交替进行实现不间断播放。对于课程设计级别的实验循环模式播正弦波、三角波、锯齿波已经足够展示 DAC 音频输出的完整原理。如果要做“播放一段语音”那就必须上双缓冲否则会有明显的断音。3. 代码实现从波形表生成到 DMA 触发配置3.1 正弦波波形表生成与存储波形表就是一组 12 位数字量范围 04095对应 03.3V。正弦波的中心点在 2048幅度最大 2047。生成 256 点正弦表的 Python 脚本如下用来在电脑上算好然后粘贴进 C 数组import math POINTS 256 AMPLITUDE 2047 OFFSET 2048 sine_table [] for i in range(POINTS): value int(OFFSET AMPLITUDE * math.sin(2 * math.pi * i / POINTS)) # 限制在 12 位范围内防止溢出 value max(0, min(4095, value)) sine_table.append(value) # 按每行 16 个格式化输出方便粘贴到 C 文件 for i in range(0, POINTS, 16): print(, .join(f0x{v:03X} for v in sine_table[i:i16]) ,)这段脚本的关键点AMPLITUDE取 2047 而不是 2048是为了保证OFFSET AMPLITUDE不超过 4095max/min是保险防止浮点误差导致越界。生成的表直接放进 C 文件const uint16_t sine_wave[256] { 0x800, 0x832, 0x864, 0x896, /* ... 省略中间 ... */ 0x7CE, 0x7CE };提示波形表必须用const修饰并放在全局编译器会把它放到 Flash 里不占 RAM。STM32F103 的 RAM 只有 20KB如果表放 RAM 里256 个 uint16_t 就是 512 字节虽然不大但能省则省。3.2 DAC 与 DMA 初始化代码初始化顺序很重要先开时钟再配 GPIO然后配 DAC最后配 DMA 和定时器。顺序错了会出现 DAC 没输出或者 DMA 不触发的问题。#include stm32f10x.h void DAC_DMA_Init(void) { // 1. 开时钟GPIOA、DAC、DMA1、TIM2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM2, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置 PA4 为模拟输入DAC 输出引脚必须配成模拟模式 GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_4; gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); // 3. 配置 DMA1 通道 3DAC 通道 1 对应 DMA1 通道 3 DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; // DAC 12 位右对齐寄存器 dma.DMA_MemoryBaseAddr (uint32_t)sine_wave; // 波形表地址 dma.DMA_DIR DMA_DIR_PeripheralDST; // 内存 - 外设 dma.DMA_BufferSize 256; // 传输 256 个点 dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不变 dma.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; // 循环模式连续播放 dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, dma); DMA_Cmd(DMA1_Channel3, ENABLE); // 4. 配置 DAC开启通道 1使能 DMA用 TIM2 触发 DAC_InitTypeDef dac; dac.DAC_Trigger DAC_Trigger_T2_TRGO; // TIM2 更新事件触发 dac.DAC_WaveGeneration DAC_WaveGeneration_None; dac.DAC_OutputBuffer DAC_OutputBuffer_Enable; // 开启输出缓冲驱动能力更强 DAC_Init(DAC_Channel_1, dac); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); // 允许 DAC 发出 DMA 请求 // 5. 配置 TIM28kHz 触发频率 TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler 0; tim.TIM_Period 8999; // 72MHz / 9000 8kHz tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); // 更新事件作为 TRGO TIM_Cmd(TIM2, ENABLE); }几个关键参数说明DMA_PeripheralBaseAddr指向DAC-DHR12R1这是 DAC 通道 1 的 12 位右对齐数据寄存器。写这个寄存器就会更新 DAC 输出。DMA_Mode_Circular让 DMA 搬完 256 个点后自动回到开头实现循环播放。DAC_Trigger_T2_TRGO表示用 TIM2 的触发输出作为 DAC 转换启动信号。TIM2 每次更新事件产生一个脉冲DAC 就转换一次。DAC_OutputBuffer_Enable开启内部输出缓冲输出阻抗更低可以直接驱动耳机但音量很小建议接功放。3.3 主函数与输出验证主函数只需要调用初始化然后空循环即可。DMA 和定时器会自动工作。int main(void) { DAC_DMA_Init(); while (1) { // 主循环什么都不用做DMA 定时器自动搬运数据 } }烧录后用示波器接 PA4 引脚应该能看到 8kHz 采样率还原出的正弦波。如果波形表是 256 点一个周期输出频率 8000 / 256 ≈ 31.25Hz。想听到 1kHz需要把波形表改成 8 点一个周期或者把定时器频率提到 256kHz。实际调试时我一般先用示波器确认波形正确再接功放听声音避免直接接耳机听到的是噪声却不知道问题出在哪儿。4. 避坑与常见问题排查那些让波形“翻车”的细节4.1 现象DAC 无输出示波器一条直线原因最常见的是 GPIO 模式配错。PA4 必须配成GPIO_Mode_AIN模拟输入如果配成推挽输出DAC 内部输出会被 GPIO 输出级短路导致无输出或输出异常。另一个原因是 DAC 通道没使能或者 DMA 没使能。解决检查GPIO_Init里 PA4 的模式是否为GPIO_Mode_AIN检查DAC_Cmd(DAC_Channel_1, ENABLE)和DAC_DMACmd是否都调用了用万用表测 PA4 电压如果是 0V 或 3.3V 固定不变基本可以确定是配置问题。4.2 现象输出波形频率不对比预期高或低很多原因定时器频率算错。很多人忘了 STM32F103 的 APB1 定时器时钟是 72MHzAPB1 预分频不为 1 时定时器时钟是 APB1 时钟的 2 倍。如果按 36MHz 算输出频率会差一倍。另一个原因是波形表长度和预期不一致比如实际存了 128 个点但以为存了 256 个。解决用示波器测 PA4 波形周期反推实际采样率。公式实际采样率 1 / 波形周期 × 波形表点数。如果对不上检查RCC_ClocksTypeDef里的PCLK1和定时器时钟配置。4.3 现象声音有“滋滋”噪声或断断续续原因DMA 缓冲欠载。如果用的是普通模式而不是循环模式DMA 搬完就停DAC 输出保持最后一个值听起来就是断音。另外如果波形表放在 RAM 里且被其他代码修改也会导致波形跳变产生噪声。解决确认 DMA 配置为DMA_Mode_Circular波形表加const放到 Flash如果播放的是动态音频必须用双缓冲并在 DMA 传输完成中断里及时填充下一段数据。4.4 现象接耳机声音极小或失真原因DAC 输出缓冲驱动能力有限直接接 32Ω 耳机负载太重电压被拉低。另外DAC 输出是 03.3V 单极性信号而耳机需要的是交流信号直流分量会导致耳机振膜偏置声音失真。解决在 PA4 和耳机之间加一个隔直电容比如 10μF和一个功放模块比如 PAM8403。隔直电容去掉直流分量功放提供足够的驱动电流。如果只是做实验听个响可以接一个有源音箱输入阻抗高DAC 直接驱动也能出声。4.5 现象改变波形表后输出没变化原因DMA 的MemoryBaseAddr在初始化时已经锁定如果运行时换了波形表地址但没重新配置 DMADMA 还是搬旧表。另外如果用了const表但编译器优化把表放到了别的地方地址对不上也会出问题。解决换波形表时先DMA_Cmd(DISABLE)改DMA_MemoryBaseAddr再DMA_Cmd(ENABLE)。或者用两个波形表通过修改DMA_MemoryBaseAddr切换。调试时可以在 DMA 传输完成中断里翻转一个 GPIO用示波器看中断有没有正常触发。5. 验收与答辩怎么证明这套 DAC 音频输出真的跑通了验收不是“能出声就行”而是要有可复现的证据链。我一般会按下面这个清单走一遍每一项都有明确的通过标准。验收项测试方法通过标准DAC 引脚输出示波器接 PA4探头衰减 1X能看到清晰的正弦波峰峰值约 3.3V采样率准确性测波形周期反推采样率与设定值误差小于 1%输出频率准确性测波形一个周期的时长与理论值误差小于 2%波形质量观察波形是否平滑无明显台阶或毛刺量化噪声在合理范围音频听感接功放和音箱播放 1kHz 正弦波能听到单一纯音无明显杂音长时间稳定性连续运行 10 分钟波形不漂移DMA 不停止答辩时评委最常问的三个问题为什么用 DMA 而不是中断采样率怎么定的波形表为什么取 256 点这三个问题在前面章节里都有答案但答辩时要能用自己的话讲清楚。我的习惯是提前把示波器截图、代码关键段、参数计算过程整理成一页 A4 纸答辩时直接展示比口头解释有说服力。还有一个容易被忽略的点电源噪声。STM32F103 的 DAC 参考电压就是 VDDA如果 VDDA 上有纹波输出波形也会叠加噪声。验收时如果发现波形上有高频毛刺先检查 VDDA 滤波电容是否足够通常并一个 100nF 和一个 10μF再检查地线布局。这个坑我在第一次做 DAC 实验时就踩过波形上一直有 50Hz 工频干扰后来发现是电源地线环路太大重新布线后就好了。从那以后我每次做模拟输出相关的实验都强制先用示波器看电源纹波再调代码。希望这套方案能帮你把 DAC 音频输出这个实验一次跑通少走点弯路。本文还有配套的精品资源点击获取