资讯动态

STM32内部DAC实战:利用PA4引脚输出可调电压与波形

发布时间:2026/9/9 17:36:07 来源:尧图企业网站定制
简介STM32内部DACPA4输出完整工程包面向嵌入式开发与STM32初学者重点解决片上DAC通道1经PA4引脚输出模拟电压的配置与实现问题。资源共166个文件压缩包3.98MB主要包含C/C源码、H头文件、Keil工程配置、编译生成的hex/axf文件以及说明文档和工程备份便于直接打开工程查看、编译与烧录。工程代码采用标准库编写完整覆盖GPIO模拟模式切换、DAC时钟使能、通道初始化、参考电压设定、数据写入及转换启动等关键环节逻辑清晰并预留定时器与DMA接口可扩展生成任意波形。同时保留uvproj工程文件和map/lst等编译辅助文件适合对照学习MDK编译流程。目前已有1355人学习下载适合希望通过标准库快速上手STM32 DAC外设或需要在课程设计、毕业设计中实现模拟输出模块的开发者。1. 项目思路PA4这根引脚背后的数模转换潜力STM32内部DAC模块是个很实用但经常被忽视的外设。很多人在做波形发生器、音频播放、可调电压源时第一反应是外挂一块DAC芯片比如MCP4822或者DAC8552。但实际上STM32不少型号内部就集成了DAC比如F103系列里的DAC模块支持两个独立通道分别对应PA4和PA5引脚12位分辨率最高转换速率能到1MHz左右完全满足大部分低频信号生成和电压输出的需求。这次分享的是最基础的玩法用STM32内部DAC的通道1也就是PA4这个引脚直接输出一路可调的模拟电压。别小看这个基础操作它是后面所有DAC高级玩法的基础——无论是输出正弦波、三角波还是做简易音频播放器本质都是在DAC输出连续变化电压这件事上做文章。把PA4输出这条链路彻底搞懂你能省掉一块外置DAC芯片的钱也能更深刻理解MCU内部数模转换的底层逻辑。适合谁读这篇如果你正要开始用STM32做波形生成类项目或者手里有一块带DAC的STM32开发板却不知道怎么下手又或者想对比一下内部DAC和外置DAC在实际使用中的差距这篇文章应该能给你一个完整的答案。注意不是所有STM32型号都带DAC比如最常见的F103C8T6那颗料内部其实没有DAC模块。选型时务必查数据手册别在C8T6上折腾一晚上最后发现硬件不支持。2. 动手前的关键决策引脚、时钟与开发方式2.1 PA4引脚的功能边界PA4在STM32内部对应的是DAC_OUT1也就是DAC通道1的模拟输出引脚。它和普通GPIO的区别在于GPIO只能输出高电平或低电平两个离散状态而PA4能够输出0到VREF之间任意电压值精度取决于DAC分辨率。以12位DAC为例参考电压3.3V时最小电压步进是3.3V / 4096大约0.8mV。这里有一个非常容易踩的坑PA4同时复用为SPI1_NSS、ADC_IN4等引脚。如果你之前用这个引脚做过SPI从机片选或者ADC采集配置DAC之前要先把对应的外设禁用然后把引脚模式设为模拟输入。DAC输出引脚在CubeMX里要配成Analog模式这是很多人第一步就出错的地方。2.2 标准库还是HAL库这个问题几乎每个玩STM32的人都纠结过。我的建议是分情况如果你在做快速原型验证、用CubeMX生成工程、或者项目以模块化开发为主HAL库更省心特别是涉及中断、DMA、多外设联动时。如果追求精细控制、对代码体积和时序敏感、或者从旧项目迁移标准库更直接。这次用HAL库走完整流程也会把标准库的关键代码放出来对照。因为DAC外设本身并不复杂核心就是设置几个寄存器无论哪个库最后硬件行为完全一致。2.3 开启DAC之前先搞定时钟DAC模块挂在APB1总线上时钟源默认由PCLK1提供。在F1系列上APB1最高频率36MHzDAC的转换时钟就是这个APB1时钟。时钟树配置错了DAC模块可能根本没被使能。HAL库中时钟在MX_DAC_Init()里通过__HAL_RCC_DAC_CLK_ENABLE()自动搞定。但如果你在标准库裸写工程里忘了开时钟DAC寄存器读写就会卡死或读出来全是零。这个问题非常隐蔽我第一次调DAC时就栽在这上面捣鼓了半天最后发现只是时钟没开。3. 核心实操让PA4真正输出模拟电压3.1 HAL库版的完整配置流程先放代码再解释每一处为什么这么写。// 初始化DAC通道1输出引脚PA4 void MX_DAC_Init(void) { DAC_ChannelConfTypeDef sConfig {0}; /** DAC Initialization */ hdac.Instance DAC; HAL_DAC_Init(hdac); /** DAC channel OUT1 config */ sConfig.DAC_SampleAndHold DAC_SAMPLEANDHOLD_DISABLE; sConfig.DAC_Trigger DAC_TRIGGER_NONE; sConfig.DAC_OutputBuffer DAC_OUTPUTBUFFER_ENABLE; if (HAL_DAC_ConfigChannel(hdac, sConfig, DAC_CHANNEL_1) ! HAL_OK) { Error_Handler(); } } // 设置输出电压值的函数 void DAC_SetVoltage(uint16_t value) { // value范围0~4095对应0~3.3V假设VREF接3.3V HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, value); HAL_DAC_Start(hdac, DAC_CHANNEL_1); }代码不长但三个参数值得你仔细品DAC_Trigger DAC_TRIGGER_NONEDAC输出由软件直接触发不需要定时器或外部事件打拍子。对于静态电压输出这是最佳选择。如果打算生成连续波形后面会说要用定时器触发让DAC按固定频率刷新。DAC_OutputBuffer DAC_OUTPUTBUFFER_ENABLE使能内部缓冲器这样DAC能驱动更重的负载但代价是输出电压范围会被压缩一点。不使能缓冲时带载能力很弱接个10k电阻就能明显看到电压跌落。DAC_ALIGN_12B_R12位右对齐。这是最常用的数据格式直接传一个0~4095的整数即可。左对齐是为了跟16位DMA数据传输配合时移位方便新手先用右对齐就好。3.2 main函数里的调用方式int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // 把PA4配成模拟模式 MX_DAC_Init(); // 例输出1.65V约等于4096的一半 DAC_SetVoltage(2048); while(1) { // 主循环里可以动态改变电压比如产生缓慢的呼吸灯效果 for(uint16_t v 0; v 4096; v 8) { DAC_SetVoltage(v); HAL_Delay(2); } } }这里有个容易犯的错在MX_GPIO_Init()里PA4的模式必须设置成GPIO_MODE_ANALOG。如果图省事用GPIO_MODE_OUTPUT_PP或GPIO_MODE_INPUTPA4引脚虽然能输出但DAC内部和GPIO的驱动电路会互相干扰实测波形在引脚悬空时可能出现奇怪的毛刺。3.3 标准库版的关键代码对照如果你还在维护标准库老工程DAC初始化基本是这几句RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); GPIO_InitStructure.GPIO_Mode GPIO_Mode_AN; // 模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_Init(GPIOA, GPIO_InitStructure); DAC_InitTypeDef DAC_InitStructure; DAC_InitStructure.DAC_Trigger DAC_Trigger_None; 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_SetChannel1Data(DAC_Align_12b_R, 2048);标准库的DAC_SetChannel1Data等价于HAL库的HAL_DAC_SetValue加HAL_DAC_Start的合体底层都是往DAC-DHR12R1这个数据保持寄存器写入值。理解这点很重要写寄存器是DAC输出的最后一步前面的初始化只是打通通道。3.4 输出值如何换算成电压这个公式值得贴出来输出电压 (DAC输出值 / 4096) × VREF比如VREF接3.3V输出值设4095理论电压是3.299V左右实测会略低原因放到后面常见问题里详细说。如果你想计算更精确不要拿3.3V做理想值而是用万用表实测一下板子上的VREF电压。很多开发板的3.3V实际是3.28V甚至3.31V直接用3.3计算出来的电压值和万用表读数会有点偏差在高精度信号源场景里不可接受。4. 让波形动起来三角波与正弦波生成4.1 静态电压只是开胃菜只输出固定电压没什么意思DAC真正的价值在于输出动态变化的电压。最简单的动态波形是三角波让DAC数值逐步增加到顶点再逐步下降。用纯软件延时来做while(1) { for(uint16_t v 0; v 4095; v) { DAC_SetVoltage(v); delay_us(50); } for(uint16_t v 4095; v 0; v--) { DAC_SetVoltage(v); delay_us(50); } }这种方式虽然能看到波形但有几个问题主循环被完全占住无法做其他事情延迟不稳定导致波形频率不准CPU白白空转。实际项目中更推荐定时器触发加DMA搬运数据让DAC自己按固定节奏刷新值CPU完全解放。4.2 定时器触发DAC生成三角波思路是这样的先在内存里准备好一段波形数据表然后用定时器的更新事件去触发DAC转换同时让DMA把波形数据从内存搬运到DAC的数据寄存器。整个过程中CPU不需要干预只要数据表准备好DAC就会按照定时器频率持续输出波形。以HAL库为例大致配置流程定时器6TIM6作为DAC触发源配置PCLK1分频后得到希望的触发频率DAC的Trigger参数改为DAC_TRIGGER_T6_TRGO配置DMA源地址指向波形数组目标地址指向DAC-DHR12R1使能DAC通道和DMA启动定时器注意TIM6在F1系列里是最常用的DAC触发定时器原因是它的更新事件直连DAC触发输入不需要额外配置重映射。用TIM2到TIM4也能触发但要仔细查复用关系容易搞晕。我实测过用这种方式生成1kHz的三角波示波器看到的线性度很好锯齿波每个台阶都很干净。相比纯软件延时频率精度提升了一个数量级而且主循环还能跑LCD刷新、按键扫描这些任务。4.3 查表法输出正弦波三角波只是练手真正常用的是正弦波。可以在PC上把正弦波采样值算好生成常量数组放在程序里比如128个点的正弦表const uint16_t sine_table[128] { 2048, 2148, 2248, 2347, 2445, 2542, 2636, 2728, 2818, 2904, 2987, 3066, 3140, 3210, 3274, 3334, // ... 中间省略 ... 1148, 1048, 949, 851, 754, 660, 568, 478, 390, 304, 222, 142, 70, 2, 0, 0 };输出时依然用定时器触发DMA搬运数组每轮流一遍就是一个周期。采样值公式采样值 2048 2047 × sin(2π × n / N)这里2048是12位DAC的中点对应约1.65V2047是振幅。如果要输出以0V为参考的双极性正弦波还需要在DAC输出后面加一级隔直电容和偏置电路这是模拟电路设计的内容先不展开。4.4 示波器看DAC输出的基本技巧接示波器时探头要打在DC耦合档别用AC耦合否则看到的波形低电平会掉到0V完全看不出正弦波本应有的1.65V偏置。还有探头的地夹子要就近接在板子的GND上别跨太远否则会引入共地噪声波形毛边很多。如果只是看电压是否正常用万用表也可以测PA4对GND的直流电压静态输出时应该稳定在设定值附近。万用表因为积分效应测动态波形会显示一个平均值这点心里要有数。5. 常见问题排查DAC输出失真和不出波的坑5.1 输出恒为0寄存器配置也没错这个问题排查起来最费时。首先量一下PA4引脚对GND有没有短路有时候杜邦线插反了就把引脚和GND碰在一起示波器看当然一直是0。排除硬件问题后检查引脚复用功能是否被其他外设抢占比如之前初始化了SPI1并且没有完全释放PA4的复用功能。还有一个容易忽略的点某些开发板的PA4引脚默认接了其他负载比如板载ADC分压电路、LED指示灯或传感器供电都可能把DAC输出电压拉低。我遇到过一块板子上PA4和板载光敏电阻的ADC输入共用一个引脚DAC输出怎么调都只有0.5V拔掉光敏电阻模块就恢复正常了。5.2 输出波形有台阶或毛刺如果看到DAC输出的正弦波不是平滑的而是有锯齿感首先检查波形频率和示波器采样率。DAC本身每次更新数据就输出一个电压台阶更新频率太低时台阶间的跳变就会很明显。改善方式有两种提高DAC更新频率或者在DAC输出后面接低通滤波器滤掉台阶的高频分量。最简单的一阶RC低通R取1kΩC取100nF截止频率大概在1.6kHz左右适合滤掉几十kHz以上的毛刺和高频噪声。如果生成的是音频信号这个滤波器还能顺便滤掉DAC的量化噪声听感干净很多。毛刺的另一个来源是DMA搬运数据和DAC更新的时序不同步。理论上DMA把数据写到DHR寄存器后DAC会在下一个触发沿把数据锁存到输出但如果DMA配置成错误的数据宽度或传输方向可能导致寄存器写入和输出不同步出现单个毛刺点。此时检查DMA的PeripheralDataSize和MemoryDataSize是否都设置成了HalfWord16位。5.3 输出带负载后电压跌落严重根源是DAC的输出能力有限。STM32内部DAC的输出缓冲器虽然比不用缓冲器时驱动能力强很多但毕竟不是运放驱动较大负载时依然有电压跌落。实际测试中用PA4直接驱动100Ω电阻输出从设定3.3V跌到了2.9V根本没法用。解决方案有两种一是在DAC输出后面加一级运放做电压跟随器提升驱动能力二是关闭DAC输出缓冲器接高阻抗负载比如后级仪表放大器或ADC输入让高阻抗负载的影响降到最小。如果要驱动的负载阻抗在10kΩ以下最好加运放。5.4 电压最大值达不到VREF用12位DAC输出满量程4095时理论电压应该等于VREF但实测往往会低几毫伏到几十毫伏。这涉及内部DAC的输出级非理想特性输出缓冲器的饱和压降以及DAC输出电压范围通常只能到VREF减去一小段余量。查数据手册DAC output voltage表格会看到最小值可能是VREF - 0.2V之类。如果应用对满量程精度要求极高可以考虑这几个方向把VREF接到高精度外部基准源或者在软件里做校准。实测一个已知电压后计算增益和偏置误差然后在输出值里做补偿。校准公式大致是实际电压 增益系数 × (DAC输出值 / 4096) × VREF 偏置误差5.5 PA4引脚同时当GPIO用导致输出异常因为PA4是复合引脚很多人习惯把它既当DAC输出又当普通IO读按键或控制LED。这在电平逻辑上走不通DAC输出的模拟电压既不是数字高电平也不是低电平如果外部还接了一个上拉或下拉电阻就会干扰DAC输出精度。我的建议是要用PA4做DAC输出就老老实实只把它当DAC用其余功能挪到别的引脚。硬件上不是绝对不行但排障成本很高得不偿失。6. 从PA4输出到完整信号链进阶思路把PA4这个DAC通道吃透之后你会意识到DAC只是信号链路的起点后续扩展方向很多。第一给DAC输出加电压基准。把VREF从板载3.3V换成外部2.5V基准芯片比如REF5025可以让DAC的分辨率分布更精准输出电压标定也更准。基准芯片的价格比DAC芯片便宜得多但精度提升非常明显。第二把DAC输出接到比较器或运放做成可编程阈值电压源。比如做一个简易电池电压检测器设定一个DAC输出的阈值电池电压超过阈值时比较器翻转精度高还可以用按键动态调整阈值比固定电阻分压灵活很多。第三DAC做音频输出。STM32的DAC加上DMA和定时器可以输出8kHz到44.1kHz采样率的音频数据配上小功放和喇叭就是一个简易音频播放器。网上很多STM32播放WAV的教程核心就是你在这篇文章里看到的PA4输出链路。最后说点个人体会。我当年学DAC时总觉得寄存器多、配置复杂但用熟之后发现它其实是STM32所有外设里最实在的一个——你把一个数值写进去它就真的输出一个对应的电压不像通信外设那样要纠结时序、应答、校验。这种所见即所得的感觉正是很多人爱上模拟电路和MCU结合的原因。如果你手头刚好有一块带DAC的板子不妨从今天这个最简单的PA4输出例程开始一步步把玩下去相信你会收获很多。本文还有配套的精品资源点击获取

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

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

免费获取报价