简介面向STM32F103系列的DAC开发例程压缩包适合嵌入式初学者与项目开发者快速掌握数字模拟转换器外设的配置方法。例程基于HAL库编写完整演示了RCC时钟使能、GPIO引脚复用、DAC通道初始化、数据写入以及触发源选择的流程并配有实验说明与可直接打开的Keil工程模板。资源共266个文件其中包括约50个头文件、48个C源文件以及编译生成的O/CRF/AXF等中间文件还附带uvproj工程配置、hex烧录文件与map映射文件压缩包整体约7.18MB目录结构清晰。目前已有349人学习参考。借助该例程可以直观理解DAC1/DAC2通道的初始化与输出过程掌握HAL库下软件触发及定时器触发的配置方法并将这一能力迁移到音频播放、电机控制、信号发生等需要模拟量输出的实际项目中适合作为嵌入式和STM32外设驱动学习的配套练习。1. 为什么 STM32 DAC 例程值得先啃时钟和引脚很多做嵌入式的人第一次接触 STM32F103 的 DAC都是直接从网上下一个STM32-DAC.zip这类例程包Keil 打开、编译、下载看到 PA4 上有电压就以为完事了。但如果你真的要在项目里用 DAC 输出音频、驱动电机调速或者做可编程电源光会点灯式地调用DAC_SetChannel1Data()是不够的——DAC 虽然只是写一个寄存器就能出电压但它的参考源、缓冲器、触发时序和 PCB 布局任何一个环节没跟上输出的信号就会带着莫名其妙的毛刺和漂移。这个压缩包里的 STM32F103 DAC 例程覆盖了标准外设库SPL下从 RCC 时钟使能到 GPIO 模拟输入模式配置的完整链路适合两类人一是刚要把 DAC 用进项目、需要一份可编译工程做参照的开发者二是已经在用 DAC 但苦于输出波形质量不稳定的工程师。后面所有分析都以这个例程为骨架展开讲清楚配置顺序背后的硬件逻辑而不是背函数名。2. STM32F103 DAC 的时钟域与引脚分配2.1 DAC 外设挂在 APB1 上时钟使能顺序不能乱STM32F103 的 DAC 外设挂在 APB1 总线上这意味着它的工作时钟来自 PCLK1。在系统主频 72MHz 的配置下APB1 预分频通常被设为 /2所以 PCLK1 36MHz。代码层面第一件事不是初始化 GPIO而是先把 DAC 外设时钟打开这一步在 SPL 库里的写法是RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 如果用 DMA 搬运波形数据才需要DAC 时钟使能之后才能访问 DAC_CR、DAC_DHR12R1 这些寄存器。这里有个容易被忽略的细节DAC 本身不产生独立的时钟源它直接使用 PCLK1但 DAC 的采样保持和输出更新时刻由触发源决定。很多人在调试时发现 DAC 输出的模拟电压更新比预期慢半拍往往是因为忽略了触发源配置而不是时钟没开。如果例程里没有用到 DMADMA1的时钟使能可以省掉但如果要生成连续波形DMA 几乎是必选项后面第 4 章会展开。2.2 PA4/PA5 的复用模式与外部电路约束STM32F103 的 DAC_OUT1 固定在 PA4DAC_OUT2 固定在 PA5这两个引脚在配置 GPIO 时必须设置为模拟输入模式。初学者最容易犯的错误是把它们配成复用推挽输出以为输出模拟电压就要用输出模式实际上 DAC 的模拟输出通路是直接连接到引脚的模拟开关上的GPIO 的数字输出缓冲器必须被禁用否则会引入数字噪声并破坏输出阻抗。GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure);需要同时打开 GPIOA 的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);。参数说明GPIO_Mode_AIN表示模拟输入这是 STM32 所有模拟外设复用引脚的通用配置方式。改引脚号时要注意F103 上只有 PA4 和 PA5 对应 DAC 输出不像 ADC 有多个可复用通道DAC 的引脚是固定的。2.3 参考电压与输出缓冲器的取舍DAC 的满量程输出电压由 VREF 引脚决定F103 的 VREF 在 LQFP48 以上的封装中是独立引脚默认情况下可以接到 VDDA但如果你的系统里有更高精度的基准源比如 2.5V 或 3.0V 的基准芯片VREF 必须单独连接。例程代码里通常不涉及这部分因为开发板上 VREF 已经和 VDDA 短接了。输出缓冲器是否需要使能取决于负载阻抗和输出幅值。DAC 内置的输出缓冲可以降低输出阻抗直接驱动 10kΩ 级别的负载没有问题但如果你用 DAC 输出做音频信号源外接的滤波电路输入阻抗较低关掉缓冲器后容易产生削波或驱动不足。一个可复用的经验参数是输出缓冲开启时输出电压范围在 0.05V 到 VREF - 0.05V 附近关闭缓冲时输出范围更接近轨到轨但驱动能力明显下降。实际项目里如果发现 DAC 输出电压在小幅值时明显偏低优先检查是否误关了输出缓冲。参数开启输出缓冲关闭输出缓冲输出阻抗约 10Ω数十 kΩ 级别带载能力可驱动 10kΩ 以下负载需要外接运放缓冲输出范围约 0.05V 至 VREF-0.05V更接近轨到轨3. 从 DAC_Init 到第一路模拟电压输出3.1 标准外设库下的 DAC 通道初始化SPL 库对 DAC 的封装非常薄初始化函数只有DAC_Init()一个入口。该例程中典型的通道 1 配置代码如下DAC_InitTypeDef DAC_InitStructure; DAC_InitStructure.DAC_Trigger DAC_Trigger_None; DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude DAC_LFSRUnmask_Bit0; DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Disable; DAC_Init(DAC_Channel_1, DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE);参数说明DAC_Trigger可选DAC_Trigger_None软件触发、DAC_Trigger_T6_TRGO和DAC_Trigger_T8_TRGO等例程里先设为 None后续改成定时器触发即可DAC_WaveGeneration是内置噪声/三角波发生器如果只是自己写数据给寄存器设置为 None 即可DAC_LFSRUnmask_TriangleAmplitude是波形发生器的掩码位不用时可以保留默认值DAC_OutputBuffer前面提过例程默认关闭这意味着你测到的电压需要外部高阻负载读取。3.2 写数据DHR 寄存器与输出寄存器的关系初始化之后真正让输出电压变化的是写 DHR数据保持寄存器的过程。SPL 库给出的封装是DAC_SetChannel1Data()系列函数DAC_SetChannel1Data(DAC_Align_12b_R, 2048);这条语句将 12 位右对齐的数据 2048 写入 DAC_DHR12R1再触发一次软件转换才能把数据锁存到 DOR 寄存器。如果是DAC_Trigger_None模式写入 DHR 后输出立即更新如果设置了定时器触发DHR 的数据要等触发事件到达时才更新到 DOR。参数DAC_Align_12b_R表示 12 位右对齐这是最常见的形式其余可选项为DAC_Align_12b_L左对齐和DAC_Align_8b_R8 位右对齐。逻辑说明12 位分辨率下输出模拟电压的计算公式为V_OUT VREF × (DOR / 4096)。以 3.3V 参考电压为例写 2048 时应得到约 1.65V 输出。实际例程中如果用万用表测不到这个电压先确认引脚是不是 PA4再确认代码中DAC_Cmd()是否被调用。3.3 软件触发模式下常见遗漏软件触发模式下有一种非常隐蔽的失败情况开发者调用了DAC_SetChannel1Data()也在示波器上看到了跳变但每次上电后输出值不固定。原因通常是没有在初始化之后显式调用DAC_SoftwareTriggerCmd()。SPL 库的DAC_Trigger_None模式并不要求软件触发指令但如果你在配置时误设了DAC_Trigger_T6_TRGO软件写入 DHR 后输出不会更新必须等待定时器事件这时你会误以为数据没写进去。建议在调试时统一按以下模式操作DAC_SetChannel1Data(DAC_Align_12b_R, 2048); DAC_SoftwareTriggerCmd(DAC_Channel_1, ENABLE);这样无论触发源配成什么软件写的数据都能立刻反映在输出端。等产品级代码定型时再把软件触发那条删掉改用定时器触发。这个顺序和验证方法可以直接迁移到任意 STM32 系列的 DAC 外设上本质上是因为 DHR 和 DOR 之间是锁存关系。4. 定时器触发 查找表驱动正弦波输出4.1 为什么软件触发生成不了连续平滑波形用软件触发生成正弦波不是不行但它有个绕不过去的瓶颈波形频率越高需要的采样点间隔越短而软件循环和中断响应的不确定性会直接反映在相邻采样点的时间抖动上。用 128 个点生成 1kHz 正弦波每个采样点间隔约 7.8μsCPU 在中断里要完成查表、写 DHR、返回中断现场这期间的任何延迟都会造成波形畸变。STM32F103 在 72MHz 主频下勉强能做但如果你把频率提高到 10kHz软件方案基本不可用。这时要用定时器触发 DMA 来搬运数据。4.2 TIM6 的 TRGO 事件触发 DAC 转换F103 中 TIM6 和 TIM7 这两个基本定时器是专门为 DAC 准备的触发源。利用定时器更新事件触发 DAC 转换的具体配置void TIM6_Config(uint32_t sample_rate_hz) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_InitStructure.TIM_Period 72000000 / sample_rate_hz - 1; TIM_InitStructure.TIM_Prescaler 0; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, TIM_InitStructure); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_Cmd(TIM6, ENABLE); }这里TIM_Period直接配置为 72MHz 除以所需采样率再减 1因为定时器时钟和系统主频一致。TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update)的作用是把定时器更新事件引到 TRGO 引脚而 DAC 侧配置为DAC_Trigger_T6_TRGO这样 DAC 会在每次定时器溢出时自动锁存数据。如果你改用了其他定时器比如 TIM8需要同时修改 DAC 的触发源配置和TIM_SelectOutputTrigger的参数。4.3 用 DMA 搬运查找表数据正弦波的数据源建议放在常量数组里通过 DMA 循环搬运到 DAC 的 DHR 寄存器这样 CPU 完全不参与波形生成。DMA 通道的选择有讲究DAC 通道 1 对应 DMA1 的通道 3通道 2 对应 DMA1 的通道 4。以下是和上面 TIM6 配置配合使用的完整代码#define WAVE_SAMPLES 256 const uint16_t sine_lut[WAVE_SAMPLES] { /* 正弦波查找表 */ }; 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_lut; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize WAVE_SAMPLES; 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_DMACmd(DAC_Channel_1, ENABLE); }关键点有三个DMA_PeripheralBaseAddr必须指向DAC-DHR12R1而非其他寄存器这决定了数据写入的位置DMA_Mode_Circular使查找表循环输出DAC_DMACmd()必须在 DMA 使能之后再调用否则 DMA 请求不会从 DAC 外设发出。数据宽度设为 HalfWord 是因为 DAC 的 DHR 寄存器是 16 位地址空间而正弦查找表用uint16_t存储。4.4 输出频率计算与精度拆解DAC 输出电压频率由采样率和查找表长度共同决定。设定时器溢出频率为f_s查找表长度为N则输出基波频率为f_out f_s / N。以 72MHz 定时器时钟、TIM_Period 72得到 1MHz 采样率配合 256 点查找表输出频率为 3906.25Hz。定时器 Period采样率Hz查找表长度输出频率Hz7110000001287812.57110000002563906.25719100000256390.62571991000025639.06实际应用时注意三个精度问题一是 F103 的 DAC 是 12 位分辨率写入sine_lut的幅值范围应控制在 0~4095正弦波信号如果要做直流偏置建议把中心电平放在 2048 附近以最大化动态范围二是定时器的TIM_Period最小不要低于 3否则定时器中断或 DMA 请求的仲裁延迟会开始影响时间精度三是当输出频率达到 100kHz 以上时DAC 的输出上升率slew rate和外部运放的带宽会成为新的瓶颈单纯调定时器参数不会有明显改善。5. 示波器验证与 PCB 布局降噪技巧5.1 三步验证 DAC 输出是否真正达标代码烧进去之后不要只看波形“像不像正弦波”先用万用表验证直流精度。把DAC_SetChannel1Data(DAC_Align_12b_R, 0)和2048分别写入测量 PA4 电压是否对应 0V 和 1.65V3.3V 参考下误差超过 20mV 就要检查 VREF 电压。第二步用示波器 AC 耦合看正弦波观察有无明显的台阶状畸变。第三步是检查毛刺的触发周期是否等于定时器周期如果毛刺和切换频率同步说明是 DAC 输出更新时的电荷注入效应属于正常现象幅度小于 5mV 不需要处理如果毛刺是随机出现的优先怀疑电源噪声。5.2 时钟抖动与电源噪声的 3 个 PCB 布局要点DAC 对电源噪声的敏感度比 ADC 更隐蔽因为输出是连续电压噪声会直接叠加在模拟信号上。项目中我一般强制自己遵守三条布局规则对改善输出信号质量非常有效。第一DAC 输出的滤波电容和运放输入尽量靠近 PA4/PA5 引脚走线不要经过过孔换层如果必须换层旁边要加一个接地过孔作回流路径。第二定时器晶振或者 HSE 时钟电路要远离 DAC 输出走线时钟信号的高次谐波会通过空间耦合进模拟输出这是示波器上看到高频纹波的常见来源。第三VREF 引脚的去耦电容用 1μF 陶瓷电容并联 10nF且电容接地端要直接回到模拟地模拟地和数字地建议在 MCU 下方用 0Ω 电阻单点连接。这三点做完DAC 输出端的噪声能降一个数量级以上尤其在音频或仪表类应用中效果差别明显。5.3 例程工程里值得保留的配置项回到这个压缩包里的工程模板keilkilll.bat这个脚本值得留作日常使用它可以一键清理 Keil 的中间编译文件能解决因.uvgui.Administrator配置损坏导致的界面异常问题。另外例程里的stm32f10x_rcc.c和stm32f10x_dac.c建议对照此前提到的初始化顺序重新梳理一遍确认 GPIO 配置在前还是 DAC 初始化在前。不同固件库版本对这两步顺序没有严格要求但把 GPIO 放在最前面可以让后续调试时用示波器保证引脚上没有异常电平再打开 DAC 功能确认输出建立过程是否平滑。调试波形质量时用频率和幅值两个维度分开评估先固定幅值调频率再固定频率调幅值可以避免同时改动多个变量。本文还有配套的精品资源点击获取