
简介这是一份关于 STM32F103 内置 DAC 输出正弦波的实战资源面向嵌入式开发初学者旨在解决数字量如何转换为模拟电压并形成连续波形的问题。内容基于 STM32F103RCT6完整展示了正弦波的数据点计算、DAC 配置、定时器触发、DMA 无干预传输以及波形输出的全过程还配有 LED 波形的仿真截图方便直观对照。压缩包内共有 76 个文件主要以 C 语言源文件和头文件为主同时包含工程配置文件、烧录用的 HEX 文件、反汇编参考以及初始化设置等整体体积只有 595KB目录按照硬件、系统、内核和用户代码等模块划分层次清晰便于学习时各个击破。已有 10312 人学习下载获得大量实际项目验证。通过本包可以掌握 DAC 加 DMA 的完整配置技巧理解查找表生成正弦波的思路并能够快速移植到其他单片机项目中。工程代码配合标准外设库可直接在集成开发环境中编译在开发板上观察真实波形调试优化部分还涉及滤波电路、电压范围和干扰排查能帮助读者从理论走向工程实践。 DAC 在 STM32F103 上容易被人忽略。毕竟这颗芯片不像音频专用芯片那样把模拟输出调到极致但板载的 12 位 DAC 外设配合 DMA 和基本定时器完全能输出一路干净、频率可控、不占 CPU 的正弦波。我自己在主控板上用这个方案做过简易信号源给后级运放提供激励信号实测效果比 PWM 滤波的方案好很多。这篇文章把整个实现过程展开写F103 DAC 的硬件限制、正弦表怎么生成、定时器触发和 DMA 搬运怎么配合、输出频率怎么精确计算最后还会聊几个我实际踩过的坑。适合手里有 STM32F103 最小系统板、想用 DAC 做波形输出的朋友参考。1. DAC输出正弦波的场景与选型为什么绕开PWM1.1 哪些应用真的需要这个功能DAC 输出正弦波最常见的领域是低频信号发生器和测试激励源。比如给电机驱动板的电流采样电路注入一个正弦信号验证运放和 ADC 采集链路是否线性或者模拟一个 1kHz 的传感器输出用来调试后级的滤波器和信号处理算法。这类场景不需要极低的谐波失真但要保证频率稳定、幅度可控、噪声不能太大DAC 加简单 RC 滤波基本就能满足。另一个常见用途是做声音提示信号。F103 可以用 PWM 输出蜂鸣器驱动但要模拟正弦音调避免刺耳DAC 就比 PWM 自然。《STM32F103 中文参考手册》里专门有一章讲数模转换器说明这颗芯片本来就把音频或波形输出纳入设计范围。我自己最常用的场景是用 F103 输出一路可编程频率的正弦波经过可变增益放大器作为自动化测试台上的标准信号源。这时候要求输出波形不能有特别大的毛刺DAC 直接输出比 PWM 后级滤波容易处理。1.2 DAC方案和PWM低通方案的差别很多人一提到输出正弦波第一反应是“用 PWM 加 LC/RC 滤波”。这个思路没有错但和 DAC 方案放到一起对比差距很现实。对比项PWM 低通方案DAC 直接输出硬件复杂度至少一级滤波要求高时需两级或运放只需一级 RC 平滑阶梯波形质量谐波多尤其在占空比接近 0% 或 100% 时12 位量化高频镜像少得多频率控制需要 PWM 高频载波最高输出频率受限只要定时器能触发几十 kHz 内都能出分辨率受 PWM 位数和载波频率互相制约12 位固定0~3.3V 范围约 0.8mV 一步资源占用定时器 PWM 通道占用率高时负载能力不足一个基本定时器加一路 DMA主循环完全不参与PWM 方案最大的问题是占空比和分辨率不可兼得而且载波频率不会凭空消失滤得不干净时示波器上能看到明显的馒头波叠加纹波。DAC 方案则是直接的电压输出输出端看到的是一系列宽度为“采样周期”的阶梯电平频率越高的信号阶梯越密后级只需要一个很便宜的 RC 低通就能把台阶抹平。所以“DAC 前后波形变化”这个词条里的现象我实测很清楚滤波前是阶梯状毛刺滤波后就是流畅正弦波。2. F103 DAC模块的几个关键硬件细节2.1 PA4/PA5引脚与初始化模式STM32F103 的 DAC 有两个独立通道通道 1 在 PA4通道 2 在 PA5。这不是说随便把引脚配置成推挽输出就能用DAC 的模拟输出引脚在复位后默认就是模拟输入状态初始化时必须把 GPIO 配成GPIO_Mode_AIN。这个细节非常容易翻车。我第一次用的时候按照习惯把 PA4 配置成GPIO_Mode_Out_PP结果输出一直是 0.8V 左右的固定电平查了半天才发现 DAC 引脚不是普通 GPIO 功能不需要配置成输出模式也不需要开启 AFIO 时钟。把引脚改成模拟输入模式后DAC 才恢复正常。这也解释了为什么很多初学者在论坛上问“DAC 配置好了PA4 怎么没有电压”九成都是卡在这一步。如果使用最小系统板还需要注意板上 VREF 和 VDDA 的接法。绝大多数 F103 开发板的 VREF 直接连到 VDDA也就是 3.3V 电源所以 DAC 输出范围就是 0 到 3.3V 附近。如果追求输出精度最好给 VDDA 单独加 LC 滤波避免数字电路开关噪声串进模拟电源。2.2 触发源、数据格式和输出范围F103 的 DAC 更新输出有两种常见方式一种是软件触发处理器往数据寄存器写值DAC 立即更新另一种是硬件触发由定时器或外部信号控制 DAC 在精确的时刻把数据寄存器里的值锁存到输出。想要输出正弦波必须用硬件触发因为正弦波要求每个采样点按时序更新软件循环写点不仅占 CPU抖动还大波形会发毛。具体来说标准外设库里有三个触发源可选定时器触发、外部触发和软件触发。最推荐的是DAC_Trigger_T6_TRGO也就是 TIM6 的 TRGO 事件触发 DAC。TIM6 属于基本定时器没有输入输出通道专门用来给 DAC 提供更新节拍选它不会和 PWM 通道抢资源。数据格式方面DAC 的寄存器支持 8 位右对齐、12 位左对齐和 12 位右对齐三种。工程里最常用的是 12 位右对齐数据范围是 0 到 4095对应输出电压大致是 0 到 VREF。查表时用uint16_t存放正弦值正好满足 16 位 DMA 搬运宽度。输出范围不是精确的 0V 到 3.3V实际会有满刻度误差具体看芯片手册里的电气特性表但常规用途不用纠结那几十毫伏的偏差。3. 正弦表怎么生成从数学公式到C数组3.1 采样点数和幅度的确定DAC 本身不会“算”正弦波需要先把一个周期的正弦波离线算好存成一张数据表运行时按固定节拍把表里的数据逐个送往 DAC。这一套方法就是查表 DDS只是 F103 没有真正的 DDS 硬件用 DMA 循环搬运来实现同样的效果。采样点数 N 的选择很关键。点数太少波形阶梯感强后级滤波压力大点数太多内存占用和数据搬运带宽会白白浪费。实测下来 128 点已经勉强够用256 点是工程里最稳妥的选择占用 512 字节内存F103 的 RAM 完全没压力一个周期的正弦波被切成 256 段后级一阶 RC 就能滤得比较漂亮。如果内存不是问题512 点更好特别是在输出频率比较低时阶梯之间的跳变会小很多。幅度方面很多教程直接用 2048 2047 * sin(θ)这样最大输出 4095最小输出 1刚好覆盖满量程。但对真实系统来说满幅输出容易触发后级运放的削顶而且当 VREF 有轻微毛刺时满幅正弦波的顶部畸变会被放大器放大。我现在的习惯是留 5% 到 10% 裕量比如输出范围控制在 0.2V 到 3.1V 之间对应数字量约 248 到 3847波形更安全后级调试也舒适。3.2 用Python把公式落地生成正弦表不需要在 C 里反复调三角函数用电脑上的 Python 一次性算好生成 C 数组粘贴到 Keil 里效率高且不容易错。import math N 256 VREF 3.30 VMAX 3.10 VMIN 0.20 CENTER (VMAX VMIN) / 2 AMPLITUDE (VMAX - VMIN) / 2 codes [] for i in range(N): s math.sin(2 * math.pi * i / N) v CENTER AMPLITUDE * s code int(round(v / VREF * 4095)) if code 4095: code 4095 if code 0: code 0 codes.append(code) # 按C数组格式打印 for i, c in enumerate(codes): if i % 16 0: print( , end) print(f0x{c:03X}, , end) if i % 16 15: print()这段代码生成的是 0.2V 到 3.1V 的输出中点约 1.65V峰值 1.45V。打印出来的 C 数组直接放到const uint16_t SineTable[256]里即可。数组加const是有原因的DMA 从 Flash 读取常量不占用 RAM如果放在 RAM 里搬运效果一样但白白浪费稀缺的 SRAM。把整个数组放在 DMA 的内存地址时要小心对齐标准库和主流编译器下const uint16_t数组天然满足 2 字节对齐不用额外处理。4. 定时器触发DMA搬运完整代码拆解4.1 输出频率的计算方法与分频选择输出频率由三个因素决定系统时钟频率、定时器分频系数、正弦表点数。基本关系是输出频率 定时器触发频率 / 正弦表点数而定时器触发频率 72MHz / (PSC 1) / (ARR 1)。这里有个容易理解错的地方APB1 总线上外设时钟是 36MHz但定时器时钟在 APB1 分频不为 1 时自动翻倍所以 TIM6 的时钟输入是 72MHz不是 36MHz。很多新手在这里算错频率实际输出和预期差一倍。假设目标输出频率是 1kHz正弦表取 256 点那么定时器触发频率需要 256kHz。72MHz / 256kHz 281.25说明无法通过整数分频精确得到 1kHz。两个可选参数PSC0ARR281触发频率 72MHz / 282 255.3kHz输出 997.6Hz误差约 -0.24%PSC0ARR280触发频率 72MHz / 281 256.2kHz输出 1001.6Hz误差约 0.16%实测中 0.2% 的误差不会影响绝大多数测试示波器就算开了频率测量也只是看到 1.001kHz 或者 997Hz。如果对频率有强迫症可以把目标频率改成能够整除的数值比如 50Hz 工频信号采样率 50 × 256 12.8kHz72MHz / 12800 5625取 PSC0、ARR5624输出就是精确的 50Hz。所以先定需求再反推分频值才是正确的选参顺序。4.2 标准外设库代码实现基于 STM32F10x 标准外设库 V3.5 的实现我贴一个精简但完整的 main.c逻辑上分为 GPIO 初始化、DAC 初始化、DMA 初始化和 TIM6 初始化四块。#include stm32f10x.h const uint16_t SineTable[256] { /* 这里放上一步Python生成的256个数据 */ }; void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); } void DAC_Config(void) { DAC_InitTypeDef DAC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); DAC_InitStructure.DAC_Trigger DAC_Trigger_T6_TRGO; DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, DAC_InitStructure); } void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)SineTable; 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); } void TIM6_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 280; // 输出约1kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM6, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); } int main(void) { GPIO_Config(); DMA_Config(); DAC_Config(); TIM6_Config(); DMA_Cmd(DMA1_Channel3, ENABLE); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); TIM_Cmd(TIM6, ENABLE); while (1) { // 主循环可以做别的事情 } }这段代码里有两处必须强调DMA 外设地址要写成DAC-DHR12R1而不是随便拿一个 DAC 数据寄存器地址因为 DHR12R1 是 12 位右对齐的保持寄存器DMA 每次搬运到这个地址后下一个触发信号会将数值锁存到输出寄存器。另一处是使能顺序先使能 DMA 通道再使能 DAC 通道再使能 DAC 的 DMA 请求最后才启动 TIM6。如果先把 TIM6 跑起来DMA 还没就绪第一个触发事件可能丢掉波形在启动瞬间会少一个点虽然循环模式下后面会补回来但强迫症看着难受。5. 实测波形、滤波处理和踩坑记录5.1 滤波前后的波形变化把代码烧进 F103 最小系统板用示波器探头接 PA4地线夹子尽量靠近芯片 GND会看到一串漂亮的阶梯状波形。每个台阶持续一个触发周期256 个台阶组成一个正弦周期这完全符合 DAC 零阶保持输出的特性不是异常。我经常听到初学朋友说“为什么我示波器上看到的不是标准正弦”其实是把阶梯波误解成了故障。DAC 本身的输出刷新率就是采样率256kHz 触发频率下每个台阶宽度约 3.9 微秒人眼和示波器屏幕反而容易看出锯齿感。这时在 PA4 和地之间加一级 RC 低通台阶被电容“抹平”示波器上就能看到光滑连续的正弦波。整套电路就一个电阻一个电容成本几毛钱。如果用 ADC 回采这个波形也就是用 STM32 自己的 ADC 去采集 DAC 输出需要注意采样率不能和 DAC 触发频率产生差拍。ADC 如果固定 1M 采样率去采 1kHz 正弦波配合片内硬件在 256 点内平均效果很干净但如果 ADC 采样率恰好接近 256kHz 的整数倍波形会显示出莫尔条纹式的叠加干扰这是采样混叠不是 DAC 的问题。5.2 RC低通滤波参数怎么选一阶 RC 低通的截止频率公式是 fc 1 / (2πRC)。把这个频率选在信号的 5 到 20 倍之间比较合理既能压住采样阶梯又不会把基波衰减太多。举例输出 1kHz 正弦波采样点 256 点采样率 256kHz。选 R1kΩ、C10nF则 fc ≈ 15.9kHz。对 1kHz 基波基本没有衰减对 256kHz 采样镜像有约 24dB 的衰减台阶明显消失。如果希望更平滑可以选 R1kΩ、C22nF截止频率降到 7.2kHz输出波形更圆润但幅度也会掉几个百分点实测需要看后级对幅度的敏感度。RC 还有一个隐藏作用阻抗隔离。DAC 使能内部输出缓冲后输出阻抗低到几十欧可以直接推 10kΩ 级别负载如果关闭缓冲输出阻抗高达几十千欧直接用示波器探头测量时会有衰减接上 RC 后要在电阻后测量才是真实空载波形。我的经验是最初调试阶段关掉缓冲方便观察输出级失配造成的波形凹陷正式应用时打开缓冲后级带载更稳。5.3 三个坑每个都折腾过我第一个坑就是 GPIO 模式配错。PA4 必须配置为GPIO_Mode_AIN这个上面已经说过一次但值得再提醒即使是模拟外设的引脚也真的不是 GPIO 输出模式复位后正是因为它默认在模拟输入状态DAC 才能正常输出。如果配成复用推挽DAC 出来的信号会被 GPIO 的输出级短路或钳位波形完全失真。第二个坑是 DMA 通道选错。DAC 通道 1 的 DMA 请求接在 DMA1_Channel3不是 Channel1也不是 DMA2。F103 没有 DMA2如果按照其他芯片的习惯去查 DMA2直接编译不出结果。查《STM32F103 中文参考手册》DMA 请求映射表能看到明显的对应关系。DAC 通道 2 才用 DMA1_Channel4别搞混。第三个坑是输出没有电压时先怀疑 VREF。很多最小系统板的 VREF 引脚虽然画出来了但没有外部灌入参考电压如果板上默认跳线没接DAC 输出始终是 0。万用表量 PA4 直流电压再量一下 VREF 是否为 3.3V这是排查“DAC 配好了没输出”最快的方法。还有一些板子 VDDA 没有单独滤波启动一瞬系统电流大VDDA 被拉低 0.3V正弦波顶部出现毛刺加一个 10μF 钽电容和 100nF 陶瓷电容并联到地就能解决。最后再分享一个和频率精度有关的经验如果你的场景要求 1kHz 输出必须控制在 ±0.01% 以内单靠内部 RC 振荡器或者普通贴片晶振是不够的建议用外部 8MHz 晶振并确认 PLL 配置为 72MHz然后再用示波器频率计功能实测校准。我在一块用了劣质晶振的板子上做过实验同一份代码在不同板子之间输出频率差了 0.8%这种误差很难从代码层面消除。硬件基础决定频率精度上限DAC 方案只是在给定的时钟精度下做到最好。本文还有配套的精品资源点击获取