资讯动态

STM32F103两路不同频率正弦波输出:DAC查表与PWM滤波方案解析

发布时间:2026/10/13 18:41:26 来源:尧图企业网站定制
简介STM32F103是意法半导体基于ARM Cortex-M3内核的高性能微控制器广泛用于嵌入式、物联网与自动化系统设计。该压缩包提供利用定时器和PWM生成两路不同频率正弦波的完整工程源码面向具备基础STM32开发知识、需要实现信号发生或音频合成的学习者与工程师方案可直接移植到同类项目。包内共189个文件以C语言源文件、头文件以及编译生成的hex、map、axf等文件为主同时包含Keil工程配置和烧录文件整体约6.27MB目录结构清晰便于按模块查阅。内容覆盖两个独立定时器的初始化、PWM通道配置、正弦波离散点查表、DMA自动搬运以及中断更新占空比等关键实现可直接在Keil中打开编译烧录并根据注释调整频率参数。已有924人学习下载适合作为从入门到进阶的可运行示例也可为电机控制等场景提供参考。1. 两路不同频率正弦波为什么不是复制一份定时器配置这么简单拿到“STM32F103输出两路不同频率正弦波.zip”这个包第一件事不是急着解压看代码而是想清楚一个关键问题两路不同频率到底靠什么实现。很多人第一反应是配置两个定时器通道各跑各的但同一个定时器的更新频率是固定的两个通道共享一个触发源出来的必然是同一频率。真正的差异来自两路各自独立的频率基准。所以这个标题背后其实是一套选型判断是用 DAC 双通道分别触发还是用 PWM 加低通滤波。F103 系列里不少型号自带 DAC有 DAC 就优先走查表加 DMA 的路线波形干净、CPU 开销小没有 DAC 的型号才退而求其次做 PWM 滤波。这篇就把两条路的原理、参数计算、代码落法和踩过的坑一次讲清。2. 先分清两条路DAC 查表输出和 PWM 滤波输出怎么选2.1 查表DMA定时器触发F103 输出正弦波的最小链路DAC 输出正弦波的思路并不复杂把一个周期的正弦波离散成 N 个点存成一张表然后按固定节奏把表里的值逐个写入 DAC 的数据寄存器DAC 引脚上就出现一个阶梯状逼近的正弦波。问题是这个“固定节奏”怎么给。F103 的 DAC 支持定时器触发每来一次触发事件DAC 就把当前数据寄存器里的值锁存到输出同时 DMA 把表里的下一个点搬到数据寄存器等待下一次触发。这样就形成了一条不需要 CPU 干预的链路DAC 输出、DMA 搬运、定时器产生节拍。这条链路里频率关系非常直接正弦波频率等于定时器触发频率除以表点数。比如定时器每毫秒触发一次1 秒触发 1000 次正弦表 100 个点那正弦波每秒就重复 10 个周期也就是 10Hz。反过来说想要 1kHz 正弦波、用 128 点表定时器就得按 128kHz 触发。这一下就把方案的关键约束暴露出来了表点数越多波形越细腻但相同正弦波频率下需要的触发频率越高对定时器时钟和 DMA 搬运速度的要求也越高。2.2 F103 的 DAC 资源不是每个型号都有别等焊完板才发现动手之前先确认手里这颗 F103 到底有没有 DAC。这是最容易翻车的地方。F103 系列里小容量和中容量的一部分型号并没有 DAC 外设常见的做法是C8T6 这类小 Flash 型号通常不带 DACZET6 这类大容量型号带完整的双通道 DAC。如果拿到的是无 DAC 型号后面所有 DAC 相关的代码都跑不起来。有 DAC 的情况下两个通道的引脚是固定的通道 1 在 PA4通道 2 在 PA5。这两个引脚要配置成模拟输入模式这是新手最容易写错的一处。DAC 输出引脚的 GPIO 模式不是推挽输出而是模拟输入配置错了引脚电压就出不来。F103 的 DAC 是 12 位精度参考电压接 VREF输出范围 0 到 VREF通常 VREF 直接接 3.3V所以数值 0 对应 0V4095 对应 3.3V。2.3 PWMRC 滤波没有 DAC 时的替代方案以及它的三个边界没有 DAC 的型号或者 DAC 引脚被占用时常见的替代是做 PWM 加 RC 低通滤波。思路是用一个定时器输出固定频率的 PWM载波频率远高于目标正弦波频率然后每隔一个很短的时间间隔修改 CCR 值让 PWM 的占空比按正弦表变化再经过 RC 低通把高频载波滤掉引脚上剩下的就是正弦波形状。这个方案能做但有三个边界需要提前知道。第一载波频率和占空比更新频率互相牵扯更新 CCR 一般靠定时器中断正弦波频率越高中断频率越高CPU 占用跟着涨1kHz 以上想做得细腻就比较吃力。第二RC 低通滤波对相位和幅值有影响截止频率越低波形越平滑但高频正弦波的幅值会被压低波形也从正弦变成前圆后尖的失真形状。第三两路不同频率需要两个独立定时器各自完成载波和更新资源占用翻倍。所以我的建议很明确有 DAC 的 F103 型号不要绕路直接用 DAC只有没 DAC 的型号才考虑 PWM 滤波而且把目标频率压到几百赫兹以内比较现实。对比项DAC 查表输出PWM RC 滤波硬件要求芯片必须带 DAC占 PA4/PA5任意定时器通道但需要外部 RC波形质量阶梯波滤波后接近标准正弦方波载波滤波后仍有残余纹波CPU 占用DMA 搬运基本为零CCR 更新进中断频率越高占用越大外围电路可选一级 RC有运放更好至少一级 RC建议两级适用型号F103 大容量带 DAC 型号无 DAC 的小容量型号3. 手写一个两路不同频率的 DAC 正弦波工程正弦表、DAC、DMA 与定时器配置3.1 生成正弦表用脚本算好 128 点代码里只管查正弦表不建议手填。128 个 0 到 4095 之间的数值手写既容易错又不好调参数。我一般用脚本生成然后把数组直接粘进 C 文件。import math N 128 table [int(2048 2047 * math.sin(2 * math.pi * i / N)) for i in range(N)] print(, .join(f0x{val:04X} for val in table))这段脚本生成了 128 个点的正弦表取值范围在 1 到 4095 之间中心值是 2048。之所以不直接用 0 到 4095 的满摆幅是因为 DAC 输出需要直流偏置正弦波围绕中心电压上下摆动这样才能输出完整的正负半周。2047 这个系数把峰值压缩到接近满量程但又不削波。如果想让两路初始相位不同第二张表可以在生成时加一个相位偏移这一点后面进阶部分会细说。生成表之后数组类型一定要用 uint16_t不要用 int16_t 或 char否则 DMA 搬运半字时符号扩展和字节序会带来莫名其妙的跳变。3.2 DAC 初始化PA4/PA5 配置为模拟输入触发源分开DAC 的初始化最关键的两件事GPIO 模式必须配成模拟输入两个通道的触发源要分别选择不同的定时器。void DAC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_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; // DAC 输出脚要配成模拟输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); DAC_InitStructure.DAC_Trigger DAC_Trigger_T6_TRGO; // 通道1挂 TIM6 DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude DAC_LFSRUnmask_Bit0; DAC_Init(DAC_Channel_1, DAC_InitStructure); DAC_InitStructure.DAC_Trigger DAC_Trigger_T7_TRGO; // 通道2挂 TIM7 DAC_Init(DAC_Channel_2, DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_Cmd(DAC_Channel_2, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_2, ENABLE); }注意 DAC_Init 对两个通道分别调用第二次调用不会覆盖通道 1 的配置函数内部按通道号区分寄存器。但结构体里的 DAC_Trigger 必须在两次调用之间改掉这就是两路不同频率的分家关键通道 1 由 TIM6 的更新事件触发通道 2 由 TIM7 的更新事件触发。输出缓冲建议打开这样 DAC 输出阻抗更低抗负载能力稍好。3.3 DMA 搬运通道 1 用 DMA1_Channel3通道 2 用 DMA1_Channel4DAC 的两个通道分别占用 DMA1 的两个通道对应关系是固定的DAC 通道 1 对应 DMA1_Channel3DAC 通道 2 对应 DMA1_Channel4。数据宽度必须是半字因为 DAC 数据寄存器是 12 位有效、16 位宽。#define TABLE_SIZE 128 void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 通道1搬运到 DAC 的 DHR12R1 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)SineTable1; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize TABLE_SIZE; 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); // 通道2搬运地址换成 DHR12R2DMA 通道换成 Channel4 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R2; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)SineTable2; DMA_Init(DMA1_Channel4, DMA_InitStructure); DMA_Cmd(DMA1_Channel4, ENABLE); }DMA 模式必须设成循环模式。如果用了普通模式表搬到最后一个点之后 DMA 就停住波形会卡在最后一个值上示波器上看就是一条直线或者突然跳变的平台。内存地址自增打开外设地址不自增这两个方向一旦反了搬运的就不是正弦表而是 DAC 寄存器自己。BufferSize 必须和正弦表长度一致否则循环回绕后的相位点错位波形周期会乱。3.4 两个定时器独立触发TIM6 管通道 1TIM7 管通道 2TIM6 和 TIM7 是 F103 里的基本定时器功能简单没有输入捕获和 PWM专门用来做 DAC 触发非常合适。它们的时钟来自 APB1 定时器时钟在标准 72MHz 主频、APB1 预分频为 2 的情况下定时器时钟是 72MHz。void Timer_Config(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_InitStructure.TIM_Prescaler 71; // 72MHz / (711) 1MHz TIM_InitStructure.TIM_Period 99; // 1MHz / (991) 10kHz 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); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE); TIM_InitStructure.TIM_Prescaler 35; // 72MHz / (351) 2MHz TIM_InitStructure.TIM_Period 99; // 2MHz / (991) 20kHz TIM_TimeBaseInit(TIM7, TIM_InitStructure); TIM_SelectOutputTrigger(TIM7, TIM_TRGOSource_Update); TIM_Cmd(TIM7, ENABLE); }这段配置里TIM6 的触发频率是 10kHzTIM7 是 20kHz都配合 128 点正弦表所以通道 1 输出约 78Hz通道 2 输出约 156Hz。触发频率的计算公式是 72MHz 除以分频值1再除以重载值1。你可以换任意一组分频和重载值只要两个定时器的计算结果不同两路正弦波频率就不同。主程序的启动顺序也有讲究先初始化 GPIO 和 DAC再配置 DMA最后启动定时器。定时器一旦开启触发事件立刻产生DMA 开始搬运。如果 DMA 还没准备好定时器就先跑了第一个触发事件可能搬走一个空值波形开头会有一个毛刺。4. 表点数、触发频率和 RC 滤波参数一组让你少试错的计算4.1 表点数 N 怎么定波形精度与最高输出频率的折中表点数直接决定阶梯波的细腻程度。点数越多每个台阶越窄滤波后的残余纹波越小谐波越少。但点数多也有代价同样的正弦波频率需要的触发频率变成 N 倍。比如想输出 1kHz 正弦波128 点表需要 128kHz 触发频率256 点表需要 256kHz。F103 的 DAC 触发虽然能扛住这个速度但留给定时器分频的余量就小了而且高频下 RC 滤波的截止频率也要跟着抬高滤波效果变差。实际工程里我一般这样选追求波形好看、频率不高时用 128 点或 256 点需要输出几百赫兹以上的正弦波时降到 64 点32 点只在极简单场景用滤波后波形轮廓还能看但失真明显。点是 2 的整数次幂还有个额外好处相位计算可以用移位代替除法对查表索引回绕有帮助。4.2 用重载值和分频值凑目标频率有误差时改哪个数已知目标正弦波频率 f、表点数 N触发频率 Ft 就是 f 乘以 N。然后找一组分频 PSC 和重载 ARR让 72MHz / ((PSC1) * (ARR1)) 尽量接近 Ft。举例要输出 100Hz128 点表Ft 12.8kHz。72MHz 除以 12.8kHz 等于 5625需要 (PSC1)*(ARR1) 5625取 PSC 24、ARR 22425 乘以 225 正好 5625触发频率精确无误差。要输出 200Hz同样条件Ft 25.6kHz72MHz 除以 25.6kHz 等于 2812.5不是整数这时只能取近似值。取 PSC 17、ARR 15518 乘以 156 等于 2808实际触发频率约 25.64kHz正弦波频率约 200.3Hz误差 0.15%一般应用够用。目标频率表点数理论触发频率PSCARR实际正弦频率误差100Hz12812.8kHz24224100.0Hz0%200Hz12825.6kHz17155200.3Hz0.15%440Hz6428.16kHz15158441.2Hz0.27%1kHz6464kHz42241000.0Hz0%凑不齐整数时优先调 PSC 还是 ARR我会先固定一个让另一个去凑。PSC 影响的是计数步长ARR 影响重载周期两个参数对最终频率的影响是乘性关系。实际调试时先设 PSC 把定时器时钟压到触发频率附近再用 ARR 细调。如果两个参数怎么凑都有较大误差可以考虑换表点数128 点不行试 125 点或 100 点只要不是 2 的整数次幂注意查表回绕时用取模而不是按位与。4.3 RC 低通滤波参数阶梯波和 PWM 方波要分开对待DAC 输出的阶梯波频谱里除了目标正弦波还有触发频率 Ft 以及它的高次谐波。低通滤波的截止频率要高于目标正弦波频率同时远低于触发频率。一阶 RC 的截止频率是 fc 1 / (2π R C)。工程上我一般取 fc 为目标正弦波频率的 5 到 10 倍。举例输出 100Hz 正弦波触发频率 12.8kHz取 fc 约 800Hz。R 取 10kΩC 等于 1 / (2π * 10k * 800) 约 19.9nF用标准值 22nF 或两个 10nF 并联。如果想滤得更干净用两级 RC中间用缓冲隔离避免两级直接级联互相影响截止频率。PWM 滤波方案就复杂得多。PWM 的频谱里载波是方波能量集中在载波频率和谐波上比 DAC 阶梯波难滤得多。一阶 RC 很难压干净建议至少两级而且每级之间加运放缓冲。载波频率至少取正弦波频率的 20 到 40 倍否则滤波后波形顶部明显起伏。还有一个容易被忽略的点RC 滤波本身会带来相位滞后和幅值衰减目标频率越高衰减越明显所以 PWM 方案我通常只做到几百赫兹以内。5. 两路正弦波调试中的 5 个典型翻车现场与排查顺序5.1 一路有波形一路恒为 0DAC 触发源和 DMA 通道配对问题现象示波器接 PA4 能看到正弦波接 PA5 是一条直线但 DAC 两个通道都已经使能。原因最常见的是 DMA 通道配对错了。DAC 通道 1 固定走 DMA1_Channel3通道 2 固定走 DMA1_Channel4。如果两路 DMA 都配置成了搬运到 DHR12R1第二路搬运的数据全写进了通道 1 的寄存器PA5 永远拿不到新数据。另一个可能是通道 2 的触发源选成了和通道 1 同一个定时器但那种情况通常两路波形都一样不会一路恒为 0。解决对照代码检查 DMA_PeripheralBaseAddr通道 2 必须是 DHR12R2DMA 通道号必须是 Channel4。再把 DAC_Init 里通道 1 和通道 2 的 Trigger 分别打印或对照寄存器确认F103 的 DAC_CR 寄存器里 TSEL1 和 TSEL2 是独立位段两个值必须不同。5.2 两路频率一样两个通道用了同一个定时器触发现象代码里明明给两个定时器配了不同的分频和重载值示波器一量两路正弦波频率完全相同。原因DAC 两个通道共用了一个触发源。比如两个通道的 DAC_Trigger 都设成了 TIM6那 TIM6 触发时两个通道同时锁存TIM7 虽然跑着但根本没有被 DAC 使用。还有一种隐蔽情况有人图省事把通道 2 的 DMA 也配置成搬运同一张表、同一个定时器触发CPU 不参与波形自然完全一致。解决把通道 1 的 Trigger 设为 TIM6通道 2 的 Trigger 设为 TIM7然后分别确认两个定时器的更新频率不同。验证方法很简单先只使能 TIM6PA4 有波形、PA5 无波形再只使能 TIM7结果反过来说明触发关系正确。5.3 波形上沿或下沿出现锯齿跳变DMA 循环模式和表类型问题现象正弦波大体形状在但每个周期里有一段明显的台阶跳变像波形被撕开一道口子频率还不稳定。原因DMA 没有配循环模式数组搬完最后一个点就停在原地后续触发事件拿不到新数据输出卡在表尾。还有一种常见原因是正弦表用了 signed short 或有符号 charDMA 以半字搬运时最高位被当成符号位数值变成负数DAC 输出直接跳到低电平。解决DMA_Mode 设为 DMA_Mode_Circular。表数组声明为 const uint16_t生成脚本输出 0x0000 到 0x0FFF 范围的正数。检查时先断开定时器触发用软件触发手动写一个固定值给 DAC如果输出稳定则问题在 DMA 搬运链路。5.4 输出频率比计算值偏小系统时钟配置和 PSC/ARR 的缓冲问题现象算出来应该是 100Hz示波器量出来 98Hz 或者 96Hz怎么调 PSC 都差一截。原因系统时钟不是 72MHz。很多工程直接用内部 HSI 时钟未配置 PLL实际主频只有 8MHz定时器时钟也跟着缩水频率差接近 9 倍。这种情况一般不会只偏一点点但如果你外接晶振晶振本身有几十到几百 ppm 的误差100Hz 输出偏 0.1Hz 属于正常。还有一种情况TIM_Prescaler 和 TIM_Period 在定时器运行中被修改但没有等待更新事件生效影子寄存器和当前计数周期不一致。解决先确认系统主频用 SysTick 做 1 秒翻转 GPIO 的测试或者直接读 RCC 配置寄存器。定时器参数修改后等待更新标志位置位再继续或者用 TIM_GenerateUpdate 强制产生更新事件确保 PSC 和 ARR 同时加载。5.5 接上负载波形变了DAC 输出缓冲和运放缓冲的边界现象空载时波形漂亮接一个几 kΩ 的电阻或者后级电路后幅值明显下降波形顶部被压平。原因DAC 输出缓冲能力有限。F103 的 DAC 虽然可以开启输出缓冲但驱动能力仍然不适合直接带低阻抗负载。输出缓冲打开时输出阻抗较低但电流能力有限关闭缓冲时输出阻抗更高带载能力更差。负载阻抗越低输出摆幅越被压缩削顶越严重。解决在 DAC 输出引脚和负载之间加一级运放电压跟随器。运放正输入端接 DAC 输出负输入端接输出端构成单位增益缓冲。运放供电注意摆幅范围轨到轨运放可以贴近 0V 和 3.3V普通运放会在上下两端砍掉一部分正弦波峰会被削平。另外RC 滤波建议放在运放后面避免滤波电阻直接吃 DAC 的输出电流。6. 改频率、加相位差还有怎么证明波形是干净的运行中改频率比想象中简单。F103 定时器的 PSC 和 ARR 都有预装载缓冲运行时修改不会立刻生效而是等到下一个更新事件才加载。想要平滑改变正弦波频率直接改 ARR 即可PSC 保持不动。改 ARR 会让触发频率连续变化但注意触发频率变化后正弦表点数不变输出频率也跟着变变化过程里波形周期会平滑拉长或压缩不会出现频率跳变。加相位差需要准备第二张表。生成表时多加一个 phase 参数phase 90 # 初始相位偏移单位度 offset int(phase / 360 * N) table2 [int(2048 2047 * math.sin(2 * math.pi * (i offset) / N)) for i in range(N)]这里 offset 是点数偏移。两路频率不同时所谓“相位差”只会保持初始时刻的设定之后随时间不断变化这是不同频率信号的固有特性。如果想验证波形是否干净别只盯着示波器看形状。把两路输出用 ADC 采样送回上位机做 FFT看基频之外有没有明显的谐波尖峰。128 点表配合合适 RC 滤波二次谐波通常能压到基频的百分之一以内如果谐波分量过高先检查表是不是有削波再检查 RC 截止频率是否偏高。之前有一次项目为了省事把两路 DAC 触发都挂在同一个定时器上示波器上怎么看都是同频折腾了一下午才反应过来是触发源的问题。从那以后我调这类输出第一件事就是先确认两路触发源各自独立再看波形。这个习惯帮我省下不少时间。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑