资讯动态

STM32F103 DAC实战:从原理到波形生成与性能优化

发布时间:2026/8/29 18:24:40 来源:尧图企业网站定制
1. 从数字到模拟的桥梁为什么需要DAC在嵌入式开发里我们经常和数字信号打交道比如用STM32的GPIO输出高低电平控制LED或者用PWM模拟一个电压去驱动电机。但有时候我们需要的是一个实实在在的、连续变化的电压比如你想让一个喇叭发出特定频率和音量的声音或者想生成一个标准的正弦波信号给其他电路用。这时候GPIO的0和1、PWM的占空比就显得不够“细腻”了。这就是DAC数模转换器出场的时候。简单来说DAC干的活儿就是把单片机里那一串串冰冷的数字代码变成电路板上真实、平滑的电压。你写个数字4095给DAC它可能就在输出引脚上给你一个3.3V你写个0它就输出0V你写个2048它可能就输出一个1.65V。这个过程是连续的理论上在它的精度范围内你可以输出任意电压值。我最早接触DAC是为了做一个简单的信号发生器。当时想测试一个模拟电路的频率响应手头没有专业的信号源就琢磨着用STM32来产生。一开始用PWM加RC滤波效果很差波形毛刺多频率和幅度调整也不灵活。直到用上了片内的DAC问题才迎刃而解。STM32F103C8T6这款经典的“蓝桥杯”神器就内置了2个12位的DAC通道虽然算不上高精度但对于很多音频生成、波形输出、电压基准的应用来说已经足够好用且成本极低。2. STM32F103的DAC模块架构与核心配置STM32F103的DAC模块相对简洁理解它的工作模式是灵活运用的前提。它不是独立运行的需要配合定时器、DMA等外设才能发挥最大效能。2.1 DAC的核心功能框图与数据通路STM32F103的DAC是12位分辨率电压输出范围一般是0到Vref通常接VDDA即模拟电源典型值3.3V。它有两条输出通道DAC_OUT1 (PA4) 和 DAC_OUT2 (PA5)。这里有个关键点一旦使能了DAC通道对应的GPIO引脚PA4/PA5会自动被配置为模拟模式你不需要也不应该再去手动配置这些引脚的模式寄存器。数据怎么送到DAC呢有三种方式软件触发你直接往DAC_DHRx数据保持寄存器写一个12位的数字然后手动设置一个软件触发位DAC就会开始转换。这种方式最简单但速度慢全靠CPU。定时器触发这是最常用的方式。你可以配置一个定时器如TIM2, TIM4, TIM6, TIM7等产生更新事件这个事件会自动触发DAC进行转换。这样就能以固定的、精确的频率更新DAC输出值从而生成稳定的波形。外部引脚触发用外部引脚EXTI线9的上升沿来触发用得比较少。对于生成连续波形定时器触发DMA是黄金组合。定时器负责定节奏DMA负责自动从内存比如一个存储了正弦波数据的数组搬运数据到DAC的数据寄存器。整个过程无需CPU干预可以输出非常纯净、稳定的模拟信号。2.2 关键配置寄存器详解与代码实现我们以使用DAC通道1PA4由TIM6触发并使用DMA传输数据来生成一个1kHz正弦波为例拆解配置过程。假设系统主频72MHz。第一步开启外设时钟这是最容易忘记的一步。DAC、对应的GPIO、触发用的定时器、DMA它们的时钟都必须开启。RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE); // DAC1的DMA请求在DMA2通道3第二步配置GPIO虽然DAC会使能模拟模式但好的习惯是先初始化引脚为模拟输入这可以降低功耗。GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入模式 GPIO_Init(GPIOA, GPIO_InitStructure);第三步配置定时器TIM6作为触发源我们的目标是1kHz正弦波。假设一个正弦波周期采样100个点那么DAC的更新频率就需要是100kHz。TIM6的时钟来自APB1频率是36MHz72MHz/2。定时器频率TIM6_CLK 36MHz需要产生100kHz的更新事件那么定时器的计数频率就应该是100kHz。因此预分频器PSC设置为36MHz / 100kHz - 1 359自动重装载值ARR设置为0即可因为我们需要的是每次溢出都产生更新事件来触发DAC。更常见的做法是设置ARR0然后通过调整PSC来改变频率。但更精确的控制是设置ARR为一个固定值比如1然后计算PSC。更通用的计算Update_Freq TIM_CLK / ((PSC1)*(ARR1))。如果我们设ARR0那么公式简化为Update_Freq TIM_CLK / (PSC1)。所以PSC TIM_CLK / Update_Freq - 1 36000000 / 100000 - 1 359。TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 0; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler 359; // 预分频值 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); // 设置更新事件为触发输出 TIM_Cmd(TIM6, ENABLE);第四步配置DMADMA负责把内存里的波形数据数组自动搬运到DAC的DHR12R1寄存器12位右对齐数据保持寄存器。外设地址DAC_DHR12R1的地址。内存地址我们定义好的正弦波数组Sine12bit的首地址。数据传输方向内存到外设。数据宽度半字16位因为DAC数据寄存器是12位按16位访问。循环模式必须使能这样波形才能循环播放。外设地址不增量内存地址增量。uint16_t Sine12bit[100]; // 稍后填充这个数组 DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA2_Channel3); // DAC1对应DMA2通道3 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)Sine12bit; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 内存到外设 DMA_InitStructure.DMA_BufferSize 100; // 数组长度 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(DMA2_Channel3, DMA_InitStructure); DMA_Cmd(DMA2_Channel3, ENABLE);第五步配置并启动DAC这是最后一步也是最核心的一步。需要配置DAC通道选择触发源并使能DMA请求。DAC_InitTypeDef DAC_InitStructure; DAC_InitStructure.DAC_Trigger DAC_Trigger_T6_TRGO; // 使用TIM6作为触发源 DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; // 不使用内置波形生成 DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Enable; // 使能输出缓冲驱动能力更强 DAC_Init(DAC_Channel_1, DAC_InitStructure); // 使能DAC通道1的DMA请求 DAC_DMACmd(DAC_Channel_1, ENABLE); // 使能DAC通道1 DAC_Cmd(DAC_Channel_1, ENABLE);注意DAC_OutputBuffer输出缓冲是否使能需要根据负载决定。使能后DAC能输出更大电流典型值几个mA直接驱动轻负载如运放同相端。如果负载很重或对精度要求极高缓冲器有微小偏移可以考虑禁用缓冲但必须外接高输入阻抗的运放做缓冲。第六步生成正弦波数据并启动在程序初始化部分我们需要计算出一个周期的正弦波数据。DAC是12位输出范围0-3.3V。如果我们想得到幅值为1.65V即0V到3.3V的中点的正弦波那么数据应该在2048±2047之间变化因为4095/2≈2048。for(int i0; i100; i) { // 计算0-2π之间的正弦值-1 到 1 float sinVal sin(2 * 3.1415926 * i / 100); // 将正弦值缩放到0-4095范围并偏移到以2048为中心 Sine12bit[i] (uint16_t)(2048 2047 * sinVal); }最后一旦TIM6启动DAC使能DMA就会开始工作PA4引脚上就会出现一个漂亮的1kHz正弦波。3. 精度、噪声与性能提升实战技巧STM32F103的DAC标称12位但实际有效位数ENOB往往达不到。输出精度和噪声水平受电源、PCB布局、软件配置影响很大。下面分享几个实测中提升性能的关键点。3.1 电源与参考电压的净化这是影响DAC精度的头号因素。STM32F103的DAC参考电压Vref内部连接到VDDA模拟电源。VDDA和VSSA模拟地必须与数字电源VDD和VSS分开处理。独立供电与滤波理想情况下VDDA应由一个独立的LDO低压差线性稳压器供电。如果与数字电源共用必须在靠近芯片的VDDA引脚处放置一个磁珠或0欧电阻进行隔离并配合一个10uF的钽电容和一个100nF的陶瓷电容进行去耦。VSSA必须通过最短的路径连接到模拟地平面。参考电压的稳定性即使VDDA很干净如果数字部分负载剧烈变化比如GPIO频繁翻转无线模块发射也会通过芯片衬底耦合噪声到DAC。一个有效的办法是使用外部参考电压芯片如REF1933输出3.3V将其输出连接到Vref引脚如果芯片有独立引出的话。对于STM32F103多数型号Vref内部连VDDA所以提升VDDA质量是唯一途径。我在一个音频项目里就吃过亏。最初VDDA和VDD直接连在一起只用了一个共用的稳压芯片。当CPU全速运行并频繁操作FSMC总线时用示波器能看到DAC输出的直流电平上有几十毫伏的毛刺。后来用了一个简单的LC磁珠电容隔离了模拟电源毛刺幅度立刻降低了70%以上。3.2 输出缓冲与负载匹配DAC内置的输出缓冲放大器不是万能的。它的主要参数输出阻抗使能缓冲后很低通常在几十欧姆级别。驱动能力典型值±5mA。压摆率有限意味着输出快速变化的电压时波形边缘会变圆滑高频分量会衰减。如果你的负载是高阻抗负载10kΩ比如运放的输入端。直接连接即可缓冲使能或禁用影响不大但使能缓冲可以提供短路保护。低阻抗负载比如直接驱动一个耳机32Ω。绝对不要这么做这会严重过载DAC导致输出电压不准、芯片发热甚至损坏。必须外接一个运算放大器作为电压跟随器或功率放大器。容性负载比如长导线带来的寄生电容。这可能导致缓冲器振荡。可以在DAC输出端串联一个小的电阻如10-100Ω再连接到负载这能有效抑制振荡。3.3 软件配置中的“坑”与优化数据对齐问题DAC的数据寄存器有DHR12R112位右对齐、DHR12L112位左对齐、DHR8R18位右对齐。如果你用12位数据务必写入正确的寄存器。使用DMA时外设地址一定要对应你选择的对齐方式。右对齐是最常用的。触发与DMA的启动顺序一个常见的错误是波形输出不连续或卡顿。正确的启动顺序是填充好波形数据数组。配置并使能DMA。配置并使能DAC此时DAC等待触发。最后启动定时器开始产生触发信号。 如果顺序乱了可能导致DMA在DAC准备好之前就开始传输或者DAC触发了几次后DMA才就绪造成波形开头缺失。双DAC同步输出如果你需要两个通道输出同步的波形比如立体声音频要使用同一个定时器作为两个DAC的触发源并且确保两个DAC的配置触发方式、缓冲设置完全一致。然后使用DAC_DualSoftwareTriggerCmd或让定时器同时触发两个通道。4. 超越基础DAC的高级应用场景与方案选型掌握了基础输出后我们可以用DAC做更多有趣的事情。但也要清楚STM32F103片内DAC的局限知道什么时候该寻求外部方案。4.1 波形合成与任意信号发生通过改变内存中的数据数组我们可以生成任意波形而不仅仅是正弦波。比如方波、三角波、锯齿波甚至是复杂的调制波形。关键在于波形表的设计。方波最简单数组里就两个值比如0和4095各占一半长度。三角波数据从0线性增加到4095再线性减少到0。任意波形你可以用数学公式计算也可以用软件如MATLAB、Python生成一个数据表然后导入到工程中作为常量数组。这里有个技巧为了减少CPU计算量尤其是动态改变波形时可以预先计算好多种波形表存放在Flash或RAM中。切换波形时只需改变DMA传输的内存源地址即可。但要注意如果新波形表长度和旧的不同别忘了同时修改DMA的CNDTR数据传输数量寄存器。4.2 音频播放实践用DAC播放WAV或MP3解码后的音频数据是一个经典应用。对于8kHz或16kHz采样率的单声道语音STM32F103的DACDMA完全能胜任。流程如下音频数据准备将音频文件如WAV转换为原始的PCM数据通常是16位有符号整数。数据格式转换STM32F103的DAC是12位无符号。需要将16位有符号的PCM数据缩放到0-4095范围。公式dac_data ((int16_pcm_data 32768) 4)。4相当于除以16将16位映射到12位。双缓冲机制这是流畅播放的关键。准备两个缓冲区BufferA和BufferB。DMA当前正在从BufferA读取数据播放时CPU赶紧把下一段音频数据填充到BufferB。当DMA播完BufferA会自动触发一个DMA半传输/传输完成中断在中断里CPU切换DMA的目标内存地址到BufferB并开始填充BufferA。如此循环实现无缝播放。我做过一个简单的MP3播放器使用VS1053解码芯片输出I2S数据流STM32用I2S接口接收后通过计算或查找表转换成DAC数据再用DMA输出。虽然音质比不上专业音频芯片但作为系统提示音或语音播报效果非常不错。4.3 何时需要外部DAC方案对比STM32F103的片内DAC在简单场合很好用但它有几个硬伤精度有限12位且实际ENOB可能只有10位左右。通道数少最多2路。无音频专用接口不支持I2S直接对接播放音频需要CPU介入格式转换。动态性能一般信噪比SNR、总谐波失真THD指标不高。当你的项目遇到以下需求时就该考虑外部DAC了需求场景推荐方案关键考量高保真音频播放专用音频DAC如VS1053、PCM5102A支持24位/192kHz集成耳机放大信噪比100dB直接处理I2S流。多通道同步输出多通道DAC芯片如AD5668 8通道16位需要精确同步控制多个模拟输出如工业控制。SPI或并行接口。超高精度与稳定性精密DAC如AD5689 16/18位用于仪器仪表、电压基准。关注INL积分非线性、DNL微分非线性、温漂。高速信号生成高速DAC并行接口或专用DDS芯片输出频率超过几百kHz的复杂波形。STM32的DAC更新率受触发定时器和DMA速度限制。关于“R-2R DAC”这是一种用电阻网络搭建的分立元件DAC方案在热词中被提及。它利用GPIO并行输出数字码通过精密电阻网络直接合成电压。它的优点是理论上可以达到很高的速度取决于GPIO翻转速度且成本低。但缺点极其明显精度严重依赖电阻的匹配度需要0.1%甚至更高精度的电阻PCB布局要求高通道数多时占用大量IO。对于大多数应用我不推荐自己搭建R-2R除非是用于教学原理或对成本极度敏感且精度要求不高的特定场景。5. 调试与问题排查从无声到完美波形在实际操作中DAC不出声、波形不对是常事。下面是一个系统的排查链路你可以像查案一样一步步来。现象一DAC引脚完全没有输出一直为0V或一个固定电压检查时钟用调试器或代码检查RCC-APB1ENR寄存器确认DAC、GPIO、触发定时器、DMA的时钟使能位是否真的被置位。这是最最常见的疏忽。检查引脚配置确认PA4/PA5是否被配置成了其他功能比如不小心初始化成了USART或SPI。即使你后面初始化了DAC之前错误的GPIO配置也可能锁死引脚状态。一个保险的做法是在系统初始化最开始就把所有要用到的模拟引脚设为模拟输入。检查DAC使能确认DAC_CR寄存器中对应通道的ENx位是否为1。单步调试跟踪DAC_Cmd()函数是否被正确执行。检查触发源如果你用的是定时器触发用示波器看看定时器的触发输出TRGO有没有信号或者软件仿真查看定时器是否在正常计数和溢出。可以临时改成软件触发在代码里手动调用DAC_SoftwareTriggerCmd()看看DAC是否有输出变化。如果有问题就出在触发定时器上。现象二有输出但电压值不对数据对齐错误如果你写入了20480x800期望得到1.65V但实际输出是0.4V。检查你写入的寄存器地址。你是不是写到了DHR12L1左对齐左对齐时12位数据放在高12位你写入的0x800会被当作0x8000远超出范围DAC可能只取了低12位或产生未定义行为。参考电压问题测量VDDA引脚的实际电压。如果VDDA只有3.0V那么你写入4095输出也就是3.0V而不是3.3V。输出缓冲过载接上负载后再测电压。如果空载时电压正确接上一个1kΩ电阻后电压被拉低说明负载太重需要外接运放缓冲。现象三输出波形有毛刺或台阶电源噪声用示波器探头打到AC耦合直接测量VDDA引脚和VSSA引脚之间的电压看看在DAC输出变化时电源上是否有同步的毛刺。如果有加强电源滤波。数据更新不同步在定时器触发DMA模式下波形应该是平滑的。如果出现明显的台阶可能是DMA在两次触发之间未能及时将新数据送达DAC。检查DMA的传输速度是否跟得上触发频率。触发频率比如100kHz不能超过DMA的最大传输速率取决于系统时钟和DMA优先级。数据数组问题检查你生成波形数据的代码。计算正弦波时浮点运算是否引入了精度问题数组索引是否越界可以尝试输出一个简单的锯齿波数据从0递增到4095再回0来测试如果锯齿波是平滑的那问题就出在波形数据生成算法上。现象四输出波形频率不对定时器计算错误反复核对定时器预分频PSC和重装载值ARR的计算公式。记住Update_Freq TIM_CLK / ((PSC1)*(ARR1))。如果使用高级定时器还要注意重复计数器的设置。DMA传输长度影响波形频率 定时器触发频率 / 波形数组长度。如果你的数组长度是100定时器触发频率是100kHz那么输出波形频率就是1kHz。如果你数组长度误写成了50波形频率就会变成2kHz。调试DAC一台示波器是必不可少的。它不仅能看电压对不对还能通过FFT功能分析输出波形的谐波成分直观地评估DAC的性能。从一个点不亮到输出一条直线再到出现稳定的正弦波最后优化到波形干净平滑这个过程本身就是对单片机外设理解的一次深化。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价