
简介本资源是一份基于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 为核心通过 TIMDMAGPIO/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%。实测 PWMRC 方案在 5kHz 时 THD 达 12.7%因 RC 截止频率与载波频率冲突而 DAC 直接输出可规避该问题。提示STM32F103 的 DAC 输出阻抗约 15kΩ若后级接示波器1MΩ 输入阻抗影响可忽略但若驱动运放或 ADC需加电压跟随器隔离。2.2 TIM2 DMA DAC 协同时序链解析整个波形生成流程依赖精确的硬件事件链其时序关系如下以正弦波为例TIM2 初始化设置为向上计数模式预分频器 PSC 71即 TIM2 时钟 72MHz / (711) 1MHz自动重装载值 ARR 999 → TIM2 更新周期 1ms对应 1kHz 基频DMA 配置DMA1_Channel3 绑定 DAC1传输方向为 Memory-to-Peripheral内存地址指向sin_wave_table[256]256 点正弦表外设地址为DAC-DHR1传输数量 256DAC 触发源选择DAC-CR | DAC_CR_TEN1启用 TIM2 TRGO 作为触发源DAC-CR | DAC_CR_EN1使能 DAC1关键同步点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.65V3.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 改为 99PSC 仍为 71则更新周期 100μs →f_DAC 10kHz仍略低于 12kHz最终配置PSC 0ARR 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 下PSC0, ARR46 → 更新周期 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_len110HzKEY_MODE719910kHzf_DAC / table_len2100HzKEY_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 / (PSC1) / (ARR1)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_DAC与table_len的比值。跳点逻辑在 DAC 数据准备阶段完成即sin_wave_table[i * step % 256]为何不重载整个波形表Flash 写入耗时且寿命有限运行时仅需调整索引步长无需修改存储内容。4. 校赛实测关键参数与典型故障排查THD 测试、频率误差归因与 DMA 传输中断4.1 校赛环境下的实测性能数据表使用 Rigol DS1054Z 示波器带宽 50MHz与 Fluke 17B 万用表在 25℃ 环境下对三路输出进行测试结果如下波形类型设定频率实测频率频率误差峰峰值VppTHD1kHz 带宽上升沿时间10%–90%方波5.00kHz4.998kHz-0.04%3.30V—22.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 直接翻转生成方波最高仅达 180kHz72MHz / 2 / 2双指令翻转。要突破 1MHz必须放弃软件翻转改用硬件自动翻转TIM1 通道 1CH1与通道 1NCH1N构成互补输出对支持可编程死区插入BDTR 寄存器工作模式TIM1 工作在 PWM 模式 1OC1M 0x6CCR1 控制占空比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→(01)×(71)8个周期实测效果在freq_hz 10000001MHz时示波器测得上升沿 18.5ns下降沿 21.3ns无过冲满足校赛高频方波需求。5.3 多波形同步输出的时序对齐技巧校赛常要求方波与正弦波相位锁定如方波上升沿对应正弦波零点。本项目通过以下方式实现统一触发源TIM1 的更新事件UEV作为 TIM2 的外部触发输入ETR配置 TIM2 为外部时钟模式TIM2-SMCR | TIM_SlaveMode_External1; TIM2-SMCR | TIM_SMS_ExternalClockMode1;相位微调在 TIM1 的CCMR1中设置OC1PE1预装载使能并通过TIM_SetCompare1(TIM1, ccr1_val offset)动态调整 CH1 输出相位offset范围 0–arr_val分辨率达 13.9ns72MHz 下。此技巧使方波与正弦波相位误差 20ns远优于校赛要求的 100ns 同步精度。本文还有配套的精品资源点击获取