ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103 DAC实战:从GPIO配置到定时器触发与正弦波生成

STM32F103 DAC实战:从GPIO配置到定时器触发与正弦波生成 简介面向STM32F103的DAC模拟电压输出例程包适合刚接触数字模拟转换器或在嵌入式项目中需要快速产生模拟电压的开发者。工程完整演示了从时钟开启、引脚模拟模式配置、DAC通道初始化到写入数字量、触发转换输出的全过程代码结构清晰包含可用的初始化与输出函数便于学习与复用。压缩包共253个文件以C语言源文件与头文件为主配有Keil工程文件同时提供编译生成的可执行映像、中间文件及少量说明文档整体约4.97MB打开即可直接浏览源码与调试。除DAC外包内还包含ADC、RTC、红外解码等多个外设参考例程可对照理解不同外设的初始化逻辑也可作为后续项目的基础工程模板。目前已有1492人学习下载对想通过完整工程案例掌握STM32F103外设配置的开发者是一份能快速落地的参考资料。1. 一块被低估的12位DACF103模拟电压从PA4、PA5走出来做基于STM32的毕业设计或者小整机时很多人默认模拟输出就用PWM加RC滤波直到发现纹波、负载调整率和响应速度都不够才回头翻手册。STM32F103内置的12位DAC其实是一块被低估的外设两个独立通道挂在PA4和PA5上配合内置运放缓冲、定时器TRGO触发和DMA能直接输出平滑的直流电压、三角波和正弦波不用外挂DAC芯片。这个压缩包里除了DAC工程文件还带红外解码、RTC、ADC等备份工程说明它原本是一整套外设模板DAC部分只是其中一块。适合正在做信号发生、鱼缸灯光调光、电机给定电压这类需要真正模拟量输出的项目也适合想搞清楚标准库里DAC到底怎么配置的人。2. DAC通道架构与时钟、GPIO配置把隐蔽的模拟外设点起来2.1 两个DAC通道的引脚与标准库工程文件对应关系STM32F103的DAC单元挂在APB1总线上包含两个相互独立的转换通道。DAC_OUT1固定在PA4DAC_OUT2固定在PA5这两个引脚在复用功能上同时属于SPI3的SCK和MISO这也是很多人用SPI时莫名其妙发现引脚电平不对的原因。通道数据寄存器是12位的支持左对齐、右对齐和12位右对齐带符号三种写入方式标准库中用DAC_Align_12b_R这类枚举区分。这份模板里同时出现了stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_adc.c说明工程并不是只点一个DAC就结束而是把定时器、ADC、Flash都编译进来了。实际阅读时建议先看工程模板主文件确认DAC初始化被调用在哪个外设之后。常见顺序是先配置RCC时钟再配置GPIO复用最后初始化DAC本身顺序反了会出现寄存器写入成功但引脚无输出的情况。2.2 时钟使能与GPIO模拟输入模式DAC的时钟开关位于RCC_APB1ENR寄存器的DACEN位标准库封装为RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE)。同时PA4和PA5所在GPIOA挂在APB2总线上也需要单独使能时钟。这两个时钟一个是外设时钟一个是端口时钟缺一不可。GPIO配置是DAC最容易出错的地方。DAC引脚必须配置为模拟输入模式GPIO_Mode_AIN而不是复用推挽输出。原因在于模拟输入模式会关闭施密特触发器避免数字输入电路在模拟电压摆到中间电平时产生附加电流和闩锁效应同时把引脚内部的上拉下拉电阻全部断开保证DAC输出阻抗和电压精度不受额外负载影响。配置GPIO_Speed字段对模拟模式没有实际意义但结构体字段必须写全否则GPIO_Init内部会读取未初始化数据。void DAC_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); }这段代码把PA4和PA5一起处理即使这次只打算用通道1也建议把两个引脚都配成模拟输入防止PA5残留的复用功能配置干扰后续扩展。GPIO_Speed_50MHz在这里不参与电气参数只是结构体赋值的完整性要求。如果只配置PA4而忘记配置PA5当DAC通道2被意外使能时PA5会保持上一次的配置状态可能直接短路到电源或者变成push-pull输出表现很难排查。2.3 触发源软件触发与硬件触发的选择DAC的转换由DAC_CR寄存器的TEN1、TEN2位控制使能触发方式由EXTSEL字段决定。F103的DAC支持软件触发、定时器2/4/6/7的TRGO事件、外部中断线9以及片上定时器触发联动。软件开发中最常用的触发方式是软件触发和TIM6_TRGO。软件触发适合调整直流电平只要调用DAC_SoftwareTriggerCmd就能立即刷新输出需要周期性波形时必须用定时器触发否则CPU要一直干预。EXTSEL[2:0]触发源适用场景000软件触发直流电压输出、手动步进调整001TIM2_TRGO双通道同时输出与ADC同步采样010TIM4_TRGO电机控制中与PWM同步刷新011TIM6_TRGO周期波形生成最常用100TIM7_TRGO第二路独立频率波形101EXTI9外部中断线外部边沿触发转换选择硬件触发时有一个细节定时器必须先使能并跑起来再使能DAC通道。如果反过来DAC使能瞬间会丢失第一个触发边沿导致输出停留在一个旧值直到下一个周期到来。用示波器看就是波形头尾不一致偶尔还出现起始电平跳变。还有一点DAC触发源是三位的如果把DAC_InitTypeDef里的DAC_Trigger误填成类似DAC_Trigger_None之外的枚举却没有同步配置对应定时器DAC会一直等待触发软件触发也不再生效现象是SetValue之后输出纹丝不动。3. 标准库三步走DAC初始化、数据写入与单位电压换算3.1 DAC_InitTypeDef结构体里三个字段怎么填标准外设库对DAC的初始化封装在DAC_InitTypeDef里只有三个成员DAC_Trigger、DAC_WaveGeneration、DAC_OutputBuffer。这三个字段恰好对应DAC_CR寄存器里最核心的控制位。DAC_Trigger选择上面说的触发源如果填DAC_Trigger_NoneDAC不会自动开始工作必须显式调用软件触发命令DAC_WaveGeneration用DAC_WaveGeneration_None关闭内部波形发生器开启后外部写入的数据会被噪声或三角波发生器覆盖初学者经常在这里踩坑以为写入了设置电压实际输出的是内部生成的波形。DAC_OutputBuffer是内部运放缓冲的开关直接影响带负载能力。开启缓冲后DAC输出可以直接驱动数十千欧级别的负载输出阻抗明显降低但满量程电压会比VREF低一点关闭缓冲后输出阻抗高一般需要外接运放跟随器才能带负载。大多数板载DAC输出都建议开启缓冲除非后续电路本身就接了高阻抗输入。void DAC_Channel1_Init(void) { 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_SoftwareTriggerCmd(DAC_Channel_1, ENABLE); }DAC_Cmd使能通道后DOR寄存器还是零必须先写入数据再触发输出才会跳到目标电压。DAC_SetChannel1Data只把数据写到DHR寄存器真正送到DOR需要等触发信号。软件触发模式下DAC_SoftwareTriggerCmd一次调用就会把DHR内容锁存到DOR不需要每次写入数据后都触发连续修改两次数据只触发一次时输出直接跳变到最终值不会停在中间值这在电压渐变场景里要注意。3.2 位码写入对齐方式影响数据含义DAC数据寄存器支持12位右对齐、12位左对齐和8位右对齐三种格式。DAC_Align_12b_R是最直观的写法写入范围0到4095直接对应输出电压从0到接近VREFDAC_Align_12b_L适合从16位数据的高12位提取数据要左移四位DAC_Align_8b_R适合把DAC当8位用但精度浪费了四分之一。位码和电压的换算关系是Vout约等于DOR除以4096乘以VREF。数字量12位右对齐VREF3.3V时输出VREF2.5V时输出00V0V10240.825V0.625V20481.65V1.25V30722.475V1.875V4095约为3.29V约为2.49V不少板子的VREF并不是严格的3.3V而是通过REF引脚单独供电。Core407这类板子把VREF直接接VDDA所以满载输出会跟着电源波动如果项目对绝对电压精度有要求应该用万用表实测VREF再反推位码而不是直接套3.3V计算。缓冲模式下满量程离VREF会有几十毫伏压降以MCU数据手册里的DAC输出特性曲线为准。3.3 从直流渐变到多通道同步在现有初始化基础上做电压渐变只需循环改写DHR寄存器并软件触发但要注意每次循环加一个延时否则连续写入太快输出端看起来还是突变。常见做法是每次递增或者递减1个LSB延时1毫秒这样从0爬到1.65V需要约2秒肉眼能看到平滑过渡也方便用示波器观察台阶。uint16_t value 0; while (value 2048) { DAC_SetChannel1Data(DAC_Align_12b_R, value); DAC_SoftwareTriggerCmd(DAC_Channel_1, ENABLE); value; delay_ms(1); }上述代码示意了渐变核心逻辑实际工程里delay_ms需要替换成自己的延时函数。如果两个通道都需要输出DAC_SetChannel2Data和DAC_SoftwareTriggerCmd(DAC_Channel_2, ENABLE)是同样的调用方式唯一区别是DAC1和DAC2的数据寄存器和触发位各自独立使能通道2前别忘了把PA5初始化成AIN。4. 定时器触发DAC与ADC回采让输出自己跑起来4.1 用TIM6_TRGO替代软件触发得到稳定刷新率软件触发模式下DAC输出什么时候更新完全取决于CPU执行到哪一行代码中断一多相邻两次更新的间隔会抖动。生成周期波形时应该用定时器做触发源让DAC在固定时间点从DHR锁存数据到DOR配合DMA还能做到CPU完全不参与数据传输。F103里最常用于触发DAC的定时器是TIM6和TIM7它们是基本定时器没有外部引脚专门用来生成内部触发事件。TIM6的更新事件通过TIM_SelectOutputTrigger配置成TRGO输出DAC_InitTypeDef里把DAC_Trigger改成DAC_Trigger_T6_TRGO这样DAC就会在每次TIM6更新时自动转换。触发频率由72MHz主频经过PSC分频再经过ARR重装载得到计算公式为f 72000000 / (PSC 1) / (ARR 1)。要注意这个频率是DAC的数据更新率不是输出波形频率。输出正弦波时波形频率等于更新率除以单周期点数很多人以为定时器配了1kHz就能看到1kHz正弦波实际看到的是39Hz就是因为256点、1kHz更新率。void TIM6_DAC_Trigger_Init(uint16_t psc, uint16_t arr) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_InitStructure.TIM_Period arr; TIM_InitStructure.TIM_Prescaler psc; TIM_InitStructure.TIM_ClockDivision 0; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, TIM_InitStructure); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_Cmd(TIM6, ENABLE); }TIM_Period填ARR值TIM_Prescaler填PSC值。TIM_SelectOutputTrigger把更新事件作为TRGO送到DAC配置成DAC_Trigger_T6_TRGO后DAC硬件会自动响应。这里没有开TIM6中断因为DAC触发只需要事件信号不需要CPU进中断开了反而增加额外开销。目标更新率PSCARR实际更新率1kHz719991000Hz10kHz7899约9.99kHz20kHz11799约19.99kHz选参数时先定PSC再定ARRPSC尽量取小因为PSC分频会损失时间分辨率ARR再精细也无法补偿。比如目标1kHz时PSC取71得到1MHz计数频率ARR取999刚好1ms如果PSC取719计数频率变成100kHzARR只能取9实际触发频率变成10kHz而不是1kHz这就说明PSC和ARR是配合关系不是独立选择。4.2 与ADC回采配合验证输出电压DAC输出是否正确不能只靠万用表看一个平均值最好用ADC把一个通道的输出采集回来做闭环校验。F103的ADC1可以配置成规则通道单次转换把DAC_OUT1连接到一个空闲ADC引脚比如PA1分压后采样。注意不要直接把DAC输出接到ADC引脚然后又同时驱动负载ADC采样电容在采样瞬间会抽取电荷导致DAC输出电压跌落特别是DAC缓冲关闭时跌落更明显。中间串一个100欧电阻或者用运放缓冲后再进ADC引脚都能缓解。void ADC1_CH1_Single_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); RCC_ADCCLKConfig(RCC_PCLK2_Div6); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); } uint16_t ADC1_Read(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); return ADC_GetConversionValue(ADC1); }ADC_ExternalTrigConv_None表示软件启动转换ADC_RegularChannelConfig里的ADC_SampleTime_239Cycles5是采样时间这里取最大采样周期是为了降低DAC输出阻抗带来的采样误差。ADC_Read返回12位原始值和DAC写入的位码对比理论上应该接近。如果发现ADC读到的值和DAC设定值差异超过几十个LSB先检查DAC负载有没有接入再检查VREF共用路径上的走线压降最后考虑是不是PA1被其他外设复用了。4.3 波形抖动和频率不准的排错方向定时器触发DAC后看到波形抖动第一步不是改代码而是检查触发源是不是真的来自硬件。用示波器看DAC输出如果边沿位置忽前忽后常见原因是DAC触发源里混入了软件触发比如初始化流程里又调用了DAC_SoftwareTriggerCmd两个触发源竞争导致DOR更新时刻不稳定。另外电源噪声和地线噪声也会叠加到输出上尤其是示波器探头地线夹太长时波形上会看到明显的高频毛刺这属于测量方式问题而非DAC问题。正确做法是用探头最短的地弹簧接地或者把探头打到10x档降低探头电容对PA4输出节点的影响。5. 正弦波查表与一阶RC整形让DAC信号更靠近理想值5.1 用查表代替实时计算生成正弦波最直接的想法是每次触发算一次sin但在MCU上sin运算耗时很长F103做浮点运算又慢波形频率根本提不上去。常见做法是在电脑上把一张正弦波表算好做成常量数组烧进Flash。这个工程里恰好有stm32f10x_flash.c说明原本就可能涉及查表或者参数存储。查表点数取128或256比较平衡单周期点数越多波形越细但内存占用和DMA搬运量也上去了。用Python生成C数组的写法很顺手import math N 256 table [int((math.sin(2 * math.pi * i / N) 1) / 2 * 4095) for i in range(N)] for i in range(0, N, 16): print(, .join(f0x{v:03X} for v in table[i:i16]) ,)这段脚本生成256个12位右对齐的正弦波采样点范围0到4095直流偏置在2048附近正负幅值各约2048。生成后粘贴成const uint16_t sine_table[256]数组。注意波形幅值受限于DAC满量程最大输出是VREF减去缓冲压降最小接近0V不会出现负半周需要交流信号时要在外部加隔直电容。5.2 用DMA让DAC自己搬数据有了正弦表还需要在每次TIM6触发时把下一个表项写入DWR。如果放在中断里做每个周期都会进一次中断波形频率高时CPU占用率直线上升。F103的DAC1请求映射在DMA1_Channel3配置成内存到外设、循环模式DMA会自动把正弦表逐个搬到DAC的DHR12R1寄存器搬完一圈又从表头开始CPU完全不用管波形刷新。void DAC_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)sine_table; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 256; 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); DAC_DMACmd(DAC_Channel_1, ENABLE); DMA_Cmd(DMA1_Channel3, ENABLE); }DMA_PeripheralBaseAddr填DAC的DHR12R1地址标准库宏DAC_Channel_1在这里无效必须直接取寄存器地址。DMA_BufferSize等于正弦表点数循环模式让DMA自动回绕。DMA使能和DAC使能的顺序反过来可能导致第一次搬运落空建议先DAC_DMACmd再DMA_Cmd。正弦波输出频率等于定时器更新率除以256点亮示波器前先按这个公式估算避免把频率设错。5.3 输出级加RC滤波器抑制量化台阶DAC输出直接接示波器会看到阶梯状波形尤其正弦波每个周期只有128点时台阶特别明显。加一级一阶RC低通滤波器能把台阶抹平截止频率按fc 1 / (2 * π * R * C)估算。R取1千欧、C取100纳法时fc约为1.6kHz适合几百赫兹到一点几kHz的波形想保留更高频率分量C换小到10纳法fc抬到约16kHz。注意RC滤波器同时会把负载调整率变差后级输入阻抗至少要大于10倍R否则分压误差直接落在输出电压上。滤波后波形能不能看用示波器FFT看谐波最直接。把探头切到10x地线用短弹簧测量1kHz正弦波输出基频周围不该看到明显的高次毛刺如果在几十kHz处发现能量堆积多半是定时器触发抖动带来的杂散分量。此时回到触发配置检查PSC和ARR取值原理上分频数越小触发时间均匀性越好波形越干净。本文还有配套的精品资源点击获取
返回列表