资讯动态

STM32F103双路DAC输出独立频率正弦波:定时器查表法实现详解

发布时间:2026/10/14 10:39:41 来源:尧图企业网站定制
简介STM32F103双路异频正弦波实现方案基于Cortex-M3内核与标准外设库完整展示定时器中断、PWM查找表与DMA自动搬运相结合的波形生成思路。压缩包共189个文件、约6.27MB核心为C语言源代码、头文件及Keil工程配套文件覆盖定时器、复位时钟、ADC、I2C、CAN、LCD显示等外设驱动并保留hex/axf固件与map/lst链接映射信息可在Keil uVision中直接打开编译与烧录验证。已有924人学习下载适合正在学习STM32定时器与PWM应用的嵌入式初学者也可作为信号发生器、音频合成或电机控制类项目的参考蓝本。工程预置两套独立定时器通道与不同频率的正弦波离散表调整预分频系数、自动重装载值或查表点数即可改变输出频率与波形精度必要时还可借助DMA自动载入数据以降低中断负载配套的keilkilll清理脚本能一键移除临时文件保持目录整洁。整体模块划分明确关键位置有注释既利于理解波形生成原理也可快速迁移至实际产品开发。1. 两路不同频率正弦波一块STM32F103能做什么之前调试一套双通道激励设备需要两路频率不同、互不干扰的正弦波一路 1kHz 驱动压电陶瓷一路 2kHz 给振动台做参考信号。手头没有双通道信号发生器就用 STM32F103 的 DAC 结合定时器查表法一个定时器中断同时刷新两路 DAC相位步进分开累加这样两路输出频率完全独立想设多少就设多少。这套资源就是我当时整理的工程实现适合正在做信号源、波形发生器或者需要双路模拟输出的开发者照着配置就能在 PA4、PA5 上得到两路稳定正弦波频率和幅度都可以自己改。2. 先定方案DAC 查表还是 PWM 滤波2.1 DAC 查表法一张正弦表和一条时间轴DAC 查表法的思路很直接把正弦波一个周期的幅值离散成一张表用定时器控制的采样时钟周期性地把表里的值送到 DACDAC 输出端就还原出正弦波形。STM32F103 的 DAC 是 12 位分辨率输出电压范围从 GND 到 VREF通常 VREF 接 3.3V所以每步电压大约是 3.3V / 4095 ≈ 0.8mV精度足够日常信号源需求。资源里用的是 200 点正弦表每个表项是 12 位整数值幅值范围放在 0 ~ 4095 之间中心点 2048 对应 1.65V 直流偏置。查表时用一个相位累加器索引表项每次定时器中断把索引对应的值写入 DAC 数据寄存器然后相位累加器加上步进值。输出频率由三个量共同决定定时器触发频率、正弦表点数、相位步进。这里有个容易想偏的地方步进值是整数还是小数并没有硬约束。如果用整数步进比如每中断前进 1 个点200 个点的表意味着输出频率等于定时器频率除以 200如果想输出 1.5kHz而定时器频率是 200kHz那么步进就是 1.5处理器一样能处理只是需要把相位累加器换成定点格式。我一般用 Q14 格式做相位累加也就是把步进值左移 14 位累加完再右移 14 位取整作为表索引这样既避免了浮点运算的抖动又能实现任意频率分辨率。2.2 PWMRC 滤波另一条可行的路如果不占用 DAC也可以用高级定时器 PWM 输出把正弦表转换成占空比序列再经过 RC 低通滤波器还原波形。原理上 PWM 输出的是高频方波占空比按正弦规律变化滤波器把高频分量滤掉后剩下的平均值就接近正弦波。这种方案在 STM32F103 上同样可行但要考虑两个额外问题。一是 PWM 频率必须远高于目标正弦频率否则 RC 滤波器很难把载波残渣压干净。假设正弦频率 1kHzPWM 频率选 50kHzRC 低通截止频率大约 5kHz 到 10kHz 之间比较合适电容电阻的取值要用截止频率公式 f_c 1 / (2πRC) 反推算完还得实际焊接调试。二是占空比分辨率受限于定时器 ARR 值ARR 太小则输出阶梯明显谐波比较难滤ARR 太大则 PWM 频率被拉低滤波难度反而上升。所以 PWM 方案更适合那些不需要 DAC、或者想节省引脚资源PWM 引脚下探到几十 kHz 也没问题的场景。但要我说纯粹做双路正弦波DAC 查表法更省心输出波形平滑代码量也更小。下面的实现全部围绕 DAC 展开。2.3 两路独立频率的关键差异两路不同频率的正弦波有两种常见实现方式。第一种是给每路 DAC 配一个独立的定时器触发源例如 TIM2 触发通道 1、TIM3 触发通道 2两路频率互不干扰但占用两个定时器。第二种是只用一个定时器在同一个中断里同时更新两路 DAC但给两路分别维护相位累加器和步进值通过步进值的比例实现不同频率。两种方式我都试过资源里选了第二种。原因很实际如果两路频率存在整数倍关系例如 1kHz 和 2kHz单定时器方案天然保证了两路之间的相位关系是稳定的而两个独立定时器反而需要额外处理启动同步问题。再者单定时器两个中断频率一样中断负载可控。提供一张对比表方便你根据项目情况选对比项DAC 查表法单定时器PWMRC 滤波双定时器双 DAC输出波形平滑度好谐波少一般依赖滤波好频率独立性靠步进区分靠两个通道区分完全独立定时器占用1 个1~2 个2 个引脚需求PA4、PA5任意 TIM 通道PA4、PA5相位同步天然同步看配置需要额外对齐代码复杂度低中中结论很明确双路 DAC 输出是 STM32F103 上做正弦信号源的最佳路径配合单定时器步进法两路频率独立且代码结构简单。3. 算好参数采样点数、定时器分频和频率步进3.1 三个参数的关系输出频率、正弦表点数、定时器触发频率之间存在一个核心公式f_out (step × f_timer) / N其中 f_timer 是定时器更新中断的频率N 是正弦表点数step 是每次中断相位累加器的前进量。这个公式是调频率的总纲所有参数改动都围绕它展开。只要 step 是 1输出频率就等于 f_timer / N相当于每个正弦周期完整采样 N 个点step 大于 1 时采样点数变少波形阶梯感增强但频率变高step 小于 1 时波形更平滑但频率更低。设计时我习惯先定 N 和 f_timer再根据目标频率算 step。N 一般取 128 到 512 之间太小波形台阶明显需要额外滤波太大则表占内存STM32F103 的 Flash 放 512 点完全没压力但中断里多读几个数据也不会有问题主要看处理器负载。f_timer 的选择要看两路目标频率。快速经验法则让最短正弦周期内至少采样 20 个点波形才不至于失真太明显。假设最高输出频率是 10kHz那么 f_timer 至少要做到 200kHz。3.2 定时器时钟来源别被 APB1 的 36MHz 骗了STM32F103 的系统时钟典型配置是 72MHzAPB1 总线时钟默认是 36MHz但定时器时钟不是直接拿 APB1 用的。当 APB1 分频系数不等于 1 时定时器时钟会自动变成 APB1 的两倍也就是 72MHz。这一点几乎所有人都踩过坑后面避坑章节我会专门展开。在 72MHz 定时器时钟下定时器更新中断频率的计算公式是f_timer 72MHz / ((PSC 1) × (ARR 1))其中 PSC 是预分频系数ARR 是自动重装值。如果想让 f_timer 200kHz可以设 PSC 0ARR 359这样 (01)×(3591)36072MHz / 360 200kHz正好。3.3 一组具体算例1kHz 和 2kHz 双路输出现在算一组实际参数。正弦表 N 取 200 点定时器中断频率 f_timer 200kHz。目标频率 f1 1kHz根据公式 step1 f1 × N / f_timer 1kHz × 200 / 200kHz 1.0。目标频率 f2 2kHzstep2 2kHz × 200 / 200kHz 2.0。把整数步进换算成 Q14 定点格式step_q step × 2^14。于是 step1_q 16384step2_q 32768。如果目标频率不是整数倍关系比如 f2 1.5kHzstep2 1.5定点后是 24576同样能输出准确的 1.5kHz不会有舍入误差导致的实际频率偏差超过 0.1%。这才是定点相位累加的真正价值。不同目标频率对应的参数收成一张表方便你以后直接查目标频率 (kHz)采样点数 N定时器中断频率 f_timerstep浮点stepQ140.5200200kHz0.581921.0200200kHz1.0163841.5200200kHz1.5245762.0200200kHz2.0327685.0200400kHz2.540960定时器中断频率也影响中断负载。200kHz 中断意味着每 5 微秒进一次中断中断服务函数里的代码执行时间必须控制在微秒级所以中断里尽量避免调用耗时函数直接写寄存器最稳妥。4. 代码落地定时器DAC 步进法的完整实现4.1 生成正弦表200 点 12 位幅值正弦表是整份工程的数据基础。我习惯先用 Python 生成一个 C 语言数组再粘贴到工程里。生成脚本的核心逻辑如下import math N 200 center 2048 amplitude 1790 samples [] for i in range(N): value center amplitude * math.sin(2 * math.pi * i / N) samples.append(int(round(value))) print(const uint16_t sinTable[%d] { % N) for i in range(0, N, 10): line , .join(str(v) for v in samples[i:i10]) print( %s, % line) print(};)逻辑说明200 个点均匀覆盖一个完整的 2π 周期每个点对应角度 2π·i/N。幅值中心是 2048波形峰值 1790所以输出范围在 258 到 3838 之间既不会触碰 0 也不会碰到 4095避免 DAC 输出饱和削顶。生成后粘贴到工程头文件即可。参数说明amplitude 决定正弦波幅度调成 2047 可以得到接近满量程的输出但要留出裕量防止运放饱和N 改成 128 或 512 时下面的 step 计算公式也要同步改 N 值。4.2 DAC 与 GPIO 初始化两路 DAC 输出引脚固定是 PA4 和 PA5配置成模拟输入模式即可。初始化代码如下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; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); DAC_InitStructure.DAC_Trigger DAC_Trigger_None; DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Disable; DAC_Init(DAC_Channel_1, DAC_InitStructure); DAC_Init(DAC_Channel_2, DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_Cmd(DAC_Channel_2, ENABLE); }逻辑说明DAC 初始化时把触发源设为 None意思是写入数据寄存器后立刻转换输出不需要等待硬件触发信号。这种模式配合定时器中断里的手动更新是最简洁的。输出缓冲我选择了 Disable这一点很关键开启缓冲虽然能提高驱动能力但会把 DAC 输出电压限制在 0.2V 到 VREF-0.2V 之间导致正弦波顶部和底部被削掉。关闭缓冲后输出阻抗变高所以后面必须接一个高阻抗负载或者运放跟随器不能直接驱动低阻负载。4.3 定时器中断配置200kHz 更新率定时器选择 TIM2配置成 200kHz 更新中断代码如下void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 359; // ARR 359 TIM_TimeBaseStructure.TIM_Prescaler 0; // PSC 0 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除挂起标志 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }逻辑说明72MHz 定时器时钟除以 (PSC1)×(ARR1)也就是 (01)×(3591)360得到正好 200kHz 的中断频率。这里 ARR 选择 359 是因为 72MHz 分解成 360 是整除避免频率出现小数偏差。中断优先级设成 1够用如果系统里还有其他实时任务建议把这个中断优先级提到最高保证波形输出不受其他中断抖动影响。4.4 中断服务函数定点相位累加与双路输出这是整份代码最核心的部分。相位累加器用 Q14 定点格式两路分别维护步进值中断里先后刷新两路 DAC#define SIN_TABLE_POINTS 200 #define PHASE_Q_BITS 14 #define PHASE_Q_SCALE (1 PHASE_Q_BITS) const uint16_t sinTable[SIN_TABLE_POINTS] { /* 由脚本生成 */ }; uint32_t phaseAcc1 0; uint32_t phaseAcc2 0; uint32_t phaseStep1 (uint32_t)(1.0f * PHASE_Q_SCALE); // 1kHz uint32_t phaseStep2 (uint32_t)(2.0f * PHASE_Q_SCALE); // 2kHz void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { uint16_t idx1 (phaseAcc1 PHASE_Q_BITS) % SIN_TABLE_POINTS; uint16_t idx2 (phaseAcc2 PHASE_Q_BITS) % SIN_TABLE_POINTS; DAC_SetChannel1Data(DAC_Align_12b_R, sinTable[idx1]); DAC_SetChannel2Data(DAC_Align_12b_R, sinTable[idx2]); phaseAcc1 phaseStep1; phaseAcc2 phaseStep2; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }逻辑说明每次中断先取两路相位累加器的高 14 位作为正弦表索引因为累加器低 14 位是小数部分右移后取整数部分刚好是对应采样点。索引用 200 取模保证相位越过 360 度后自动回绕。接着把表值写入两路 DAC 数据寄存器最后相位累加器各自加上步进值。这样两路频率只取决于 phaseStep 的数值互不干扰想改频率只需要改 step 变量。参数说明phaseStep1 是 1kHz 对应的 Q14 值 16384phaseStep2 是 2kHz 对应的 32768每次中断分别前进 1 个点和 2 个点。如果你把 PHASE_Q_BITS 改成 16那么所有 step 值要重新计算精度略高但溢出风险变大推荐保持 14。取模运算在中断里略耗时间也可以用位掩码替代前提是表点数必须是 2 的幂次比如 256 点表就可以用与运算优化。5. 避坑排查输出波形不对时先查这五个地方5.1 实际频率是预期频率的两倍现象设置输出 1kHz 正弦波示波器测出来是 2kHz所有参数核对过也没错。原因STM32F103 的定时器时钟在 APB1 分频系数不为 1 时会自动乘以 2。很多人初始化时钟后 APB136MHz就直接拿 36MHz 去算 PSC 和 ARR但定时器实际时钟是 72MHz算出来的中断频率正好翻倍。解决确认 SystemInit 之后 APB1 分频是 2定时器时钟按 72MHz 带入公式。想验证也很简单临时把 ARR 改成 719PSC 不变用示波器量中断引脚反转频率理论上应该是 100kHz。5.2 正弦波顶底部被削平现象波形中间部分正常但顶部和底部出现明显平台像是被限幅了。原因DAC 输出缓冲开启后输出电压被限制在约 VREF 减去几个十毫伏的范围内比如 3.3V 供电时输出只能到大概 3.1V。如果正弦表幅值接近 4095接近满量程的部分必然被削。解决两种处理方式。第一种把 DAC_OutputBuffer 设置为 Disable这样输出范围接近满量程但输出阻抗升高到几十 kΩ 级别必须外接高阻抗负载。第二种保留缓冲同时把正弦表幅值调小到 3500 左右留出裕量。关键是知道自己当前用哪种方式。5.3 波形抖动、频率不稳现象同一路输出频率在示波器上跳动或者波形触发不稳定。原因中断里使用了 float 类型的相位累加变量。浮点加法在 Cortex-M3 上需要编译器运行时库支持执行时间不确定而且中断嵌套或优先级抢占时累加时序会被打断输出波形就会抖动。解决改成 Q14 定点累加整个中断里只做整数加减法和移位执行时间固定且极短。实测切换后波形稳定度明显改善相位噪声在示波器上看不出来了。5.4 波形有明显的阶梯感现象输出波形像台阶而不是平滑曲线尤其是频率调高后特别明显。原因每次中断输出一个电压值理论上每个正弦周期采样点数等于 N / step。频率越高 step 越大实际采样点数越少DA 输出保持上一值直到下一次中断自然形成阶梯。解决有两条路一是提高定时器中断频率让每个周期采样点数恢复到 20 点以上代价是中断负载上升二是在 DAC 输出端加一级二阶低通有源滤波器截止频率设在目标频率的 3~5 倍把阶梯高频分量滤掉。资源里推荐先调 f_timer因为滤波电路调起来更玄学不同阻抗匹配差异很大。5.5 偶尔波形错位或者一路彻底无输出现象上电后两路波形正常但运行一段时间其中一路突然输出直流电平或者波形跳变一整周期。原因中断里的标志位清理顺序出了问题。如果先调用 TIM_ClearITPendingBit 再写 DAC 寄存器会有一段极短时间窗口内发生新的更新事件又被覆盖导致丢失一次刷新。另外如果两路 DAC 只有一个引脚虚焊也会表现为一路无输出。解决把清标志放在中断服务函数的最后也就是写完 DAC 和累加器之后再清。养成习惯后基本不会再遇到波形错位问题。硬件方面PA4、PA5 是相邻引脚焊接时容易桥连用万用表量一下对地电阻就能排查。6. 验证与进阶把示波器接上看波形也看频率把工程编译下载后第一步先用示波器同时接 PA4 和 PA5确认两路都有输出。测频率时注意示波器触发源选哪一路如果触发源的那一路频率是 2kHz另一路 1kHz 的波形看起来会不稳定这不是故障是触发设置问题。建议分别触发测量两路频率确认与预期值误差在千分之一以内因为 72MHz 晶振本身精度有限这个误差范围是正常的。第二步把示波器打开 FFT 功能观察输出频谱正常情况在主频处有一个尖峰谐波分量应明显低于基波。如果看到很多杂散谱线优先怀疑中断里是否有浮点运算或长期阻塞操作。想在线改频率不需要重新编译。把 phaseStep1、phaseStep2 改成 volatile 变量在主循环里直接赋值即可下一次中断就会按新步进输出。我用这个方式做过扫频测试从 1kHz 步进到 10kHz每档停留 100ms全程波形切换干净利落没有卡顿或毛刺。再进一步还可以从查表法改成 DDS 思路用高精度 NCO 做相位累加在中断里输出任意波形表而不局限于正弦波。资源里的工程基于标准外设库如果想移植到 HAL 库只需要把 DAC_SetChannel1Data 和 TIM2_IRQHandler 对应替换定时器和 GPIO 初始化部分基本可以平移。从那以后我每次做这类波形输出都会强制走一遍定点相位累加、关输出缓冲、测中断频率这三步基本没翻过车。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑