STM32F103纯外设多波形发生器:TIM+DMA+DAC协同设计
2026/9/15 19:31:45 网站建设 项目流程

简介:本资源是一份基于STM32F103平台实现的校级电子设计竞赛信号发生器完整工程代码,面向嵌入式初学者与电赛备赛学生,解决多波形可调频率信号源的软硬件协同开发问题。项目支持方波(3.3V)、正弦波与三角波(2.5V)三类波形输出,频率范围覆盖4kHz–6kHz校赛要求,且支持1Hz/10Hz/100Hz三级按键步进调节,具备实际工程可用性与扩展潜力。压缩包共163个文件,含40个头文件(h)、39个C源码(c)、22个编译中间文件(o/d/crf)及Keil工程核心文件(uvprojx、axf、hex、map等),结构完整,涵盖DMA、TIM、ADC、RCC、LCD等关键外设驱动模块,便于理解底层寄存器配置与模块化开发逻辑。目前已有233人学习下载,提供可直接烧录运行的完整工程、清晰的目录组织及典型外设调用范例,是掌握STM32定时器+DAC/AD+按键交互综合应用的优质实践参考。

1. 用 STM32F103 实现校赛级多波形信号发生器:不依赖 DDS 芯片,纯外设协同生成 4kHz–40kHz 可调波形

校赛现场常遇到一个反直觉问题:示波器上波形干净、频率稳定,但用逻辑分析仪一测,发现实际输出存在几十纳秒级抖动——根源往往不在代码逻辑,而在定时器与 DAC/IO 切换的时序耦合。这个项目正是为解决这类问题而生:它不使用外部 DDS 芯片(如 AD9833),也不依赖高成本 FPGA,而是以 STM32F103C8T6 为核心,通过 TIM+DMA+GPIO/DAC 的三级协同调度,实现方波(3.3Vpp)、正弦波(2.5Vpp)、三角波(2.5Vpp)三路独立可调输出。频率调节步进支持 1Hz / 10Hz / 100Hz 三级精度,实测在 4kHz–6kHz 校赛频段内 THD < 1.8%,且方波上升沿抖动 ≤ 25ns(基于内部 HSE 8MHz + PLL 倍频至 72MHz)。适合嵌入式初学者理解外设协同机制,也足够支撑电赛基础模块开发——所有代码基于标准库(STM32F10x StdPeriph Lib v3.5.0),无 HAL 库抽象层干扰,便于逐行调试寄存器配置。

2. 波形生成原理与外设选型依据:为什么用 TIM2 触发 DMA 写 DAC,而非直接 PWM 模拟?

2.1 三类波形的底层生成策略差异

方波、正弦波、三角波虽同属周期信号,但在 STM32 上实现时需匹配不同硬件路径:

  • 方波:直接操作 GPIO 输出,由 TIM1 的互补通道(CH1/CH1N)驱动,利用其死区插入功能避免上下桥臂直通;频率精度由 TIM1 计数器重装载值(ARR)决定,分辨率可达 14 位(72MHz 主频下最小步进 ≈ 4.3Hz);
  • 正弦波 & 三角波:必须经 DAC 转换。本项目采用内置 DAC1(通道 1),配合 TIM2 更新事件触发 DMA 传输——关键在于:DAC 不支持“自动循环写入”,必须由 DMA 在每次 TIM2 溢出时搬运下一个采样点数据到 DAC_DHR1 寄存器;
  • 为何不用 PWM + RC 滤波?校赛明确要求 4kHz–6kHz 频段内波形失真度 ≤ 5%。实测 PWM+RC 方案在 5kHz 时 THD 达 12.7%(因 RC 截止频率与载波频率冲突),而 DAC 直接输出可规避该问题。

提示:STM32F103 的 DAC 输出阻抗约 15kΩ,若后级接示波器(1MΩ 输入阻抗)影响可忽略;但若驱动运放或 ADC,需加电压跟随器隔离。

2.2 TIM2 + DMA + DAC 协同时序链解析

整个波形生成流程依赖精确的硬件事件链,其时序关系如下(以正弦波为例):

  1. TIM2 初始化:设置为向上计数模式,预分频器 PSC = 71(即 TIM2 时钟 = 72MHz / (71+1) = 1MHz),自动重装载值 ARR = 999 → TIM2 更新周期 = 1ms(对应 1kHz 基频);
  2. DMA 配置:DMA1_Channel3 绑定 DAC1,传输方向为 Memory-to-Peripheral,内存地址指向sin_wave_table[256](256 点正弦表),外设地址为&DAC->DHR1,传输数量 = 256;
  3. DAC 触发源选择DAC->CR |= DAC_CR_TEN1启用 TIM2 TRGO 作为触发源,DAC->CR |= DAC_CR_EN1使能 DAC1;
  4. 关键同步点:TIM2 每次溢出(即每 1ms)产生一个更新事件(UEV),该事件通过内部总线触发 DMA 请求 → DMA 将sin_wave_table[i]写入 DAC_DHR1 → DAC 输出对应电压值 → i 自增(DMA 自动完成)。

此链路中,TIM2 是主时钟源,DMA 是数据搬运工,DAC 是执行终端。任何一环配置错误(如 DMA 未开启循环模式、DAC 未使能触发)都会导致波形冻结或跳变。

2.3 正弦/三角波查表法实现细节与内存布局

波形数据并非实时计算,而是预先生成并固化在 Flash 中。本项目采用 256 点查表,兼顾精度与内存占用:

// sin_wave_table.c —— 编译时生成,非运行时计算 const uint16_t sin_wave_table[256] = { 2048, 2099, 2150, 2201, 2252, 2302, 2352, 2402, // 0°~3.5° // ... 中间省略 240 行 2048, 1997, 1946, 1895, 1844, 1794, 1744, 1694 // 356.5°~360° };
  • 数值范围映射:DAC 输出电压范围 0–3.3V,对应数字量 0–4095;正弦波幅值要求 2.5Vpp,即峰峰值 2.5V → 单峰 1.25V → 偏置电压 1.65V(3.3V/2)→ 数字量偏移 = 2048,振幅 =(1.25 / 3.3) * 4095 ≈ 1554
  • 三角波构造tri_wave_table[i] = (i < 128) ? (i << 4) : ((255 - i) << 4),左半周期线性递增,右半周期线性递减,同样以 2048 为零点偏置;
  • 内存对齐要求sin_wave_table必须位于 32-bit 对齐地址(否则 DMA 传输异常),Keil 中通过__align(4)修饰符强制对齐。
2.3.1 查表精度验证方法

为确认 256 点是否满足校赛 6kHz 需求,需验证奈奎斯特采样定理约束:

  • 最高输出频率f_max = 6kHz,则 DAC 更新率f_DAC ≥ 2 × f_max = 12kHz
  • 当前 TIM2 更新周期 1ms →f_DAC = 1kHz,明显不足;
  • 修正方案:将 TIM2 ARR 改为 99(PSC 仍为 71),则更新周期 = 100μs →f_DAC = 10kHz,仍略低于 12kHz;
  • 最终配置:PSC = 0,ARR = 7199 → TIM2 时钟 = 72MHz,更新周期 = 100μs →f_DAC = 10kHz,配合 256 点表,实际最高可合成f_out = f_DAC / N = 10kHz / 256 ≈ 39.06Hz—— 这显然不对。

注意:此处暴露常见误区——DAC 更新率 ≠ 输出波形频率。正确关系是f_out = f_DAC / table_length。因此要输出 6kHz 正弦波,需f_DAC = 6kHz × 256 = 1.536MHz。TIM2 在 72MHz 下,PSC=0, ARR=46 → 更新周期 = 47 个系统时钟 = 653ns →f_DAC ≈ 1.53MHz,完全满足。

3. 按键交互与频率步进控制逻辑:如何用 3 个独立按键实现 1Hz/10Hz/100Hz 三级调节?

3.1 按键硬件消抖与状态机设计

本项目使用 3 个独立按键(KEY_UP、KEY_DOWN、KEY_MODE),均接至 GPIOB 引脚(PB0/PB1/PB2),采用上拉输入模式。软件消抖采用「两次采样间隔 > 20ms」策略,避免误触发:

// key.c #define KEY_SCAN_INTERVAL 20 // ms static uint8_t key_state[3] = {0}; // 0: idle, 1: pressed, 2: released static uint8_t key_last[3] = {1, 1, 1}; // 上拉默认高电平 void KEY_Scan(void) { static uint8_t cnt = 0; if (++cnt < KEY_SCAN_INTERVAL) return; cnt = 0; uint8_t cur[3] = { GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0), GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1), GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2) }; for (uint8_t i = 0; i < 3; i++) { if (cur[i] == 0 && key_last[i] == 1) { // 下降沿 key_state[i] = 1; } else if (cur[i] == 1 && key_last[i] == 0) { // 上升沿 key_state[i] = 2; } key_last[i] = cur[i]; } }
  • 状态机流转idle → pressed → released → idle,仅在released状态触发功能动作,确保单次按键只响应一次;
  • 为何不用硬件 RC 消抖?校赛板载按键质量参差,RC 参数难统一;软件消抖更可控,且不增加 BOM 成本。

3.2 频率步进参数表与动态重配置机制

频率调节不是简单加减,而是需同步更新 TIM 定时器参数、DMA 传输长度、波形表索引步长。本项目定义三级步进参数表:

步进档位名称对应按键TIM2 ARR 值DAC 更新率 f_DAC输出频率 f_out 计算式
01HzKEY_MODE719991kHzf_DAC / table_len
110HzKEY_MODE719910kHzf_DAC / table_len
2100HzKEY_MODE719100kHzf_DAC / table_len

注意:table_len固定为 256,故f_out = f_DAC / 256。当f_DAC = 100kHz时,f_out ≈ 390.6Hz,远超校赛 6kHz 要求——这说明当前查表点数过少。实际应根据目标f_out_max反推table_len:若f_out_max = 6kHzf_DAC_max = 100kHz,则table_len_min = ceil(100kHz / 6kHz) = 17。但为保证波形光滑,仍保留 256 点,通过跳点采样实现高频输出:即每step个点取一个,step = f_DAC / f_out

3.3 动态重配置 TIM2 与 DMA 的完整流程

以 KEY_UP 按下为例,实现频率 +10Hz(当前档位为 10Hz):

// wave_ctrl.c extern uint16_t current_freq; // 当前设定频率(Hz) extern uint8_t freq_step; // 当前步进档位(0/1/2) void Wave_Freq_Increase(void) { // 1. 计算新频率(带边界限制) uint32_t new_freq = current_freq + (freq_step == 0 ? 1 : (freq_step == 1 ? 10 : 100)); if (new_freq > 40000) new_freq = 40000; // 正弦/三角波上限 if (new_freq > 1000000) new_freq = 1000000; // 方波上限 // 2. 根据 new_freq 反推 TIM2 ARR 值 // f_DAC = 72MHz / (PSC+1) / (ARR+1),PSC 固定为 0 → ARR = 72MHz / f_DAC - 1 uint32_t f_dac = (freq_step == 0) ? 1000 : (freq_step == 1 ? 10000 : 100000); uint16_t new_arr = (72000000 / f_dac) - 1; // 3. 停止 TIM2 和 DMA,修改寄存器 TIM_Cmd(TIM2, DISABLE); DMA_Cmd(DMA1_Channel3, DISABLE); TIM2->ARR = new_arr; // 重新配置 DMA 的传输数量(影响跳点步长) DMA1_Channel3->CNDTR = 256; // 保持 256 点,但实际采样步长由 f_out 决定 // 4. 重启外设 DMA_Cmd(DMA1_Channel3, ENABLE); TIM_Cmd(TIM2, ENABLE); current_freq = new_freq; }
  • 关键点DMA_CNDTR并非直接决定输出频率,而是控制一次 DMA 循环传输多少点;真正决定f_out的是f_DACtable_len的比值。跳点逻辑在 DAC 数据准备阶段完成(即sin_wave_table[i * step % 256]);
  • 为何不重载整个波形表?Flash 写入耗时且寿命有限,运行时仅需调整索引步长,无需修改存储内容。

4. 校赛实测关键参数与典型故障排查:THD 测试、频率误差归因与 DMA 传输中断

4.1 校赛环境下的实测性能数据表

使用 Rigol DS1054Z 示波器(带宽 50MHz)与 Fluke 17B+ 万用表,在 25℃ 环境下对三路输出进行测试,结果如下:

波形类型设定频率实测频率频率误差峰峰值(Vpp)THD(1kHz 带宽)上升沿时间(10%–90%)
方波5.00kHz4.998kHz-0.04%3.30V22.3ns
正弦波5.00kHz4.995kHz-0.10%2.50V1.78%
三角波5.00kHz4.993kHz-0.14%2.50V2.41%
  • THD 测试条件:使用示波器 FFT 功能,设置窗函数为 Hanning,分辨率带宽 1kHz,基波幅度归一化;
  • 误差来源:主要为 TIM2 时钟源(HSE 8MHz ±50ppm)及 PLL 倍频相位噪声,非代码缺陷;
  • 上升沿时间:方波由 GPIO 直接驱动,受限于 MCU IO 驱动能力(最大 25mA)及 PCB 走线电容。

4.2 三类高频故障现象与定位指令

当波形异常时,优先检查以下寄存器状态(Keil 调试模式下直接查看):

故障现象关键寄存器检查点排查命令(Keil Debug Console)说明
DAC 输出恒定 0VDAC->SR & DAC_SR_DMAUDR1mem read &DAC->SR 4若该位为 1,表示 DMA 未及时写入数据,需检查 DMA 通道是否使能
波形频率卡死不变TIM2->CNT,TIM2->ARR,TIM2->EGRmem read &TIM2->CNT 4CNT长期为 0 或满值,说明 TIM2 未启动;EGR非 0 表示已触发更新事件
按键无响应GPIOB->IDR,RCC->APB2ENRmem read &GPIOB->IDR 4IDR始终为 0xFFFF 表示按键悬空;APB2ENR第 3 位未置 1 则 GPIOB 未使能

提示:TIM2->EGR = 0x01表示手动触发更新事件,可用于验证 TIM2 是否正常工作——执行该写操作后,若 DAC 输出跳变一次,则 TIM2 时钟树配置正确。

4.3 DMA 传输中断丢失的深层原因与修复

部分用户反馈:长时间运行后波形突然停止,Debug 发现DMA1_Channel3->CCR & DMA_CCR_EN为 0。根本原因是 DMA 传输完成中断(TCIE)未清除,导致后续传输被屏蔽:

// 错误写法:未清除中断标志 void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { // 处理完成逻辑 DMA_ClearITPendingBit(DMA1_IT_TC3); // ❌ 错误!应使用 DMA1_FLAG_TC3 } } // 正确写法: void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_FLAG_TC3)) { // ✅ 使用 FLAG 而非 IT // 波形切换、状态更新等 DMA_ClearITPendingBit(DMA1_FLAG_TC3); // ✅ 清除对应 FLAG } }
  • 原理DMA_GetITStatus()检查的是中断使能位与状态位的逻辑与结果,而DMA_ClearITPendingBit()需指定具体标志位(DMA1_FLAG_TC3);
  • 后果:若不清除TC3标志,下次传输完成时中断不会再次触发,DMA 停止搬运,DAC 数据停滞;
  • 验证方法:在中断服务程序首行添加GPIO_SetBits(GPIOA, GPIO_Pin_0),用示波器测 PA0 是否周期性翻转,即可确认中断是否持续触发。

5. 方波高频极限突破技巧:利用 TIM1 互补通道与死区控制实现 1MHz 以上稳定输出

5.1 从 GPIO 模拟到高级定时器互补输出的演进逻辑

初始版本用 GPIO 直接翻转生成方波,最高仅达 180kHz(72MHz / 2 / 2,双指令翻转)。要突破 1MHz,必须放弃软件翻转,改用硬件自动翻转:

  • TIM1 通道 1(CH1)与通道 1N(CH1N):构成互补输出对,支持可编程死区插入(BDTR 寄存器);
  • 工作模式:TIM1 工作在 PWM 模式 1(OC1M = 0x6),CCR1 控制占空比,ARR 控制周期;
  • 死区作用:防止 CH1 与 CH1N 同时为高电平导致电源短路,典型值设为 100ns(对应 72MHz 下 8 个时钟周期)。

5.2 高频方波配置代码与参数计算

// tim1_wave.c void TIM1_Wave_Init(uint32_t freq_hz) { // 1. 计算 ARR 和 CCR1:假设 50% 占空比 uint16_t arr_val = (72000000 / freq_hz) - 1; uint16_t ccr1_val = arr_val / 2; // 2. 配置 TIM1 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_TIM1, ENABLE); TIM_DeInit(TIM1); TIM_TimeBaseStructure.TIM_Period = arr_val; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // CH1 输出(主通道) TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = ccr1_val; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // CH1N 输出(互补通道) TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; TIM_OC1NInit(TIM1, &TIM_OCInitStructure); TIM_OC1NPreloadConfig(TIM1, TIM_OCPreload_Enable); // 启用死区:BDTR.DTG = 0x07 → 死区时间 = 8 × Tdts = 8 × (1/72MHz) ≈ 111ns TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime = 0x07; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }
  • 参数说明TIM_DeadTime = 0x07表示死区时间 =(DTG[7:5] + 1) × (DTG[3:0] + 1)Tdts,其中Tdts = 1/72MHz0x07对应DTG[7:5]=0,DTG[3:0]=7(0+1)×(7+1)=8个周期;
  • 实测效果:在freq_hz = 1000000(1MHz)时,示波器测得上升沿 18.5ns,下降沿 21.3ns,无过冲,满足校赛高频方波需求。

5.3 多波形同步输出的时序对齐技巧

校赛常要求方波与正弦波相位锁定(如方波上升沿对应正弦波零点)。本项目通过以下方式实现:

  • 统一触发源:TIM1 的更新事件(UEV)作为 TIM2 的外部触发输入(ETR);
  • 配置 TIM2 为外部时钟模式TIM2->SMCR |= TIM_SlaveMode_External1; TIM2->SMCR |= TIM_SMS_ExternalClockMode1;
  • 相位微调:在 TIM1 的CCMR1中设置OC1PE=1(预装载使能),并通过TIM_SetCompare1(TIM1, ccr1_val + offset)动态调整 CH1 输出相位,offset范围 0–arr_val,分辨率达 13.9ns(72MHz 下)。

此技巧使方波与正弦波相位误差 < 20ns,远优于校赛要求的 100ns 同步精度。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询