
简介面向电子类课程设计与模拟电路实验的波形发生器设计报告围绕方波—三角波—正弦波函数发生器的完整设计流程展开适合电子信息、自动化等专业学生完成课程设计、撰写实验报告或准备电子竞赛时参考。压缩包内共1个doc文档大小约304KB正文涵盖设计要求、系统概述、单元电路设计、Multisim仿真与分析、整体电路、心得体会、附录及参考文献等模块。报告详细给出滞回比较器与RC充放电产生方波的原理、积分电路实现方波到三角波转换、差分放大器非线性特性完成三角波到正弦波变换并包含元件参数选择计算、比较器门限宽度推导、仿真波形与电路图。目前已有707人学习下载可帮助读者快速理解四波段频率范围、RC时间常数手控频率、峰峰值输出指标等关键要求对照电路框图与仿真步骤完成从理论推导到Multisim验证的闭环也可作为撰写课程设计报告的结构范式与排错参考。1. 波形发生器设计报告里最该先算清的三组参数很多波形发生器设计报告写到一半卡住不是电路画不出来而是指标没定死想输出 1MHz 正弦却选了 MCU 内置 DAC 加查表表长 256 点触发频率得上到 256MHz芯片根本给不了这个机会。所以报告第一页不该是框图而是三组数频率范围与分辨率、输出幅度与偏置范围、失真度和杂散要求。这三组数一旦落定方案基本被锁死——低频高精度走 MCU 查表中高频走专用 DDS 芯片要几十兆赫兹以上或任意波形只能上 FPGA 配高速 DAC。下面按选型、实现、程控、验证四步把波形发生器从纸面指标推到能上电出波形。2. 波形发生器三条技术路线的选型对比与 DDS 核心公式2.1 模拟振荡、MCU 查表、专用 DDS 芯片的适用边界模拟路线用运放搭文氏桥或者方波-三角波发生器优点是零量化噪声、无采样混叠、元件成本低到可以忽略。代价是频率靠电位器或者变容二极管调程控几乎做不了频率精度依赖 RC 元件的温漂实测能稳在 ±3% 已经算好。要用单片机调就得挂数字电位器或者程控电容阵列电路复杂度立刻超过数字方案。所以模拟路线适合固定频率、单次校准的场景比如传感器激励源。MCU 查表路线是课程设计和中小批量产品最常走的一条。定时器按固定频率触发 DACDMA 把码表一个个搬进 DAC 数据寄存器输出频率等于触发频率除以每周期点数。它的好处是波形随便改、频率分辨率高、上位机一句话就能调参坏处是输出频率上限被 DAC 刷新率和 DMA 带宽卡死。STM32F4 系列的 DAC 在 12 位精度下稳定跑到 1MSPS 左右比较现实再往上建立时间就成了瓶颈。专用 DDS 芯片是第三条路。以 AD9833 为例25MHz 参考时钟、28 位相位累加器输出频率分辨率约 0.1Hz正弦和三角波能到 12.5MHz。它把相位累加、正弦查找、DAC 全部集成在片内单片机只负责写寄存器。代价是输出为电流型必须外接运放做 I/V 转换正弦的谐波抑制一般宽带杂散需要靠外部滤波器压。| 路线 | 频率范围 | 频率分辨率 | 程控难度 | 典型成本 | 适合场景 | | 模拟振荡 | 1Hz~1MHz | 差靠元件 | 高 | 极低 | 固定频率激励 | | MCU 查表加 DAC | 1Hz~100kHz | 优 | 低 | 低 | 课程设计、低频信号源 | | 专用 DDS 芯片 | 0.1Hz~12.5MHz | 优 | 低 | 中 | 台式函数发生器 | | FPGA 加高速 DAC | 到几百 MHz | 优 | 高 | 高 | 任意波形、射频激励 |提示选型时先把「最高输出频率」乘上「每周期最少点数」得到需要的触发频率或者采样率再回头看候选器件的刷新能力。这一步比看数据手册第一页的主频有用得多。2.2 相位累加器与频率控制字DDS 输出的两个公式DDS 的核心是一个位宽为 N 的相位累加器每个时钟周期累加一次频率控制字 FTW。累加器溢出就代表相位绕了一圈输出频率由此决定f_out f_clk × FTW / 2^N FTW round(f_out × 2^N / f_clk)频率分辨率就是 FTW 步进 1 带来的频率变化等于f_clk / 2^N。N 取 28、f_clk 取 25MHz 时分辨率约 0.093Hz足够覆盖音频到中频段的所有需求。N 取 32、f_clk 取 100MHz 时分辨率降到 0.023Hz代价是累加器和查找表地址位宽变大。2.2.1 频率控制字的整数效应与误差估算FTW 只能取整数所以实际输出频率f_clk × FTW / 2^N和目标值总有偏差最大偏差不超过半个频率分辨率。低频段这个误差可以忽略高频段就不一样了输出 10MHz、分辨率 0.093Hz 时相对误差只有十亿分之一但输出 1Hz 时绝对误差可以忽略、相对误差也只有 0.09%。真正需要注意的是实际输出的是「量化后的频率」而不是设定值报告里应该写成实测值而不是设定值。def ftw(f_out, f_clk, n_bits28): 计算频率控制字及其对应的实际输出频率 full 1 n_bits ftw_val round(f_out * full / f_clk) # 钳位至少为 1 保证有输出最大不超过累加器满量程 ftw_val max(1, min(ftw_val, full - 1)) real f_clk * ftw_val / full return ftw_val, real if __name__ __main__: for f in (1, 100, 1000, 100_000, 1_000_000): w, real ftw(f, 25e6) print(f目标 {f:9} Hz - FTW{w:9d}, 实际 {real:14.6f} Hz, f偏差 {real - f:.6f} Hz)这段脚本解决的是「设定值能不能达到」这个反复被问的问题。f_clk换成你实际用的晶振频率n_bits换成芯片的相位累加器位宽跑一遍就能知道每个目标频率的量化误差。返回的两个值里ftw_val是要写进频率寄存器的数real是要写进报告的实测频率。2.3 每周期点数、采样率与重建滤波器截止频率怎么定查表方案里输出频率f_out f_trig / PP 是每周期码表点数f_trig是定时器触发频率。P 越大正弦台阶越细量化谐波越小但同样的 DAC 刷新能力下P 越大输出频率越低。这是一个必须显式做的取舍很多报告里只写了「取 256 点」却没写为什么。| 目标最高输出频率 | 每周期点数 | 需要触发频率 | 重建滤波器截止 | | 1 kHz | 256 | 256 kSPS | 2 kHz | | 10 kHz | 128 | 1.28 MSPS | 20 kHz | | 20 kHz | 64 | 1.28 MSPS | 40 kHz | | 100 kHz | 16 | 1.6 MSPS | 200 kHz |重建低通滤波器的截止频率通常取最高输出频率的 1.5 到 2 倍同时必须明显低于触发频率的一半。原因在于阶梯波的镜像分量出现在f_trig ± f_out附近如果滤波器截止接近f_trig/2这些镜像是压不下去的示波器 FFT 上会看到一串等间距杂散。截止取高了杂散漏出去取低了正弦顶部被削平、幅度随频率明显下降两边都要实测权衡。注意不要为了抬高输出频率把每周期点数砍到 8 点以下。8 点正弦的阶梯误差接近 10%THD 会直接从 1% 量级掉到影响观感的水平后面再怎么调滤波器也救不回来。3. 用 STM32 定时器触发 DAC 加 DMA 跑通波形发生器最小系统3.1 正弦码表生成与 12 位 DAC 的偏置处理内置 DAC 是单极性的输出范围 0 到参考电压12 位对应码值 0 到 4095。要输出有正有负的正弦必须把码表抬到中点再用后级电路把直流偏置去掉或者做成可调偏置。码表生成时留 5% 上下余量是有必要的DAC 输出级和后面的运放在接近电源轨时增益会下降码值贴着 0 或者 4095 时波形顶部会塌。#include math.h #include stdint.h #define SINE_POINTS 128 /* 每周期点数按最高输出频率取舍 */ #define DAC_CODE_MAX 4095.0f /* 12 位满量程码值 */ #define DAC_CODE_MID 2047.5f /* 单极性输出的中点 */ static uint16_t sine_tab[SINE_POINTS]; void sine_tab_build(void) { for (int i 0; i SINE_POINTS; i) { float ph 2.0f * 3.14159265f * (float)i / (float)SINE_POINTS; /* 0.90 是使用率上下各留 5% 余量避开输出级非线性区 */ float v DAC_CODE_MID DAC_CODE_MID * 0.90f * sinf(ph); sine_tab[i] (uint16_t)(v 0.5f); /* 加 0.5 做四舍五入 */ } }SINE_POINTS决定频率上限和波形质量0.90f这个系数直接对应输出幅度使用率改成 1.0 会削顶、改成 0.5 会浪费一半动态范围。sine_tab类型必须是uint16_t且按 12 位右对齐才能配合DAC_ALIGN_12B_R直接送进数据寄存器的高 12 位。3.2 TIM6 触发频率换算从目标频率反推 ARR 和 PSC定时器触发频率由预分频器 PSC 和自动重载寄存器 ARR 共同决定f_trig tim_clk / ((PSC1) × (ARR1))。在低频段固定 PSC 为 0、只调 ARR 最简单但 ARR 只有 16 位最低能到的触发频率就是tim_clk / 65536。typedef struct { uint32_t psc; uint32_t arr; uint32_t real_freq; /* 实际可达的输出频率单位 Hz */ } wave_tim_t; /* tim_clk定时器时钟f_out目标输出频率points每周期点数 */ wave_tim_t wave_set_freq(uint32_t f_out, uint32_t tim_clk, uint32_t points) { wave_tim_t r {0, 0xFFFF, 0}; if (f_out 0) return r; uint64_t target (uint64_t)tim_clk / ((uint64_t)f_out * points); if (target 2) target 2; /* ARR 至少为 1触发频率有上限 */ r.arr (uint32_t)(target - 1); if (r.arr 0xFFFF) { /* 超出 16 位只能用 PSC 补 */ r.psc (uint32_t)((target 0xFFFF) / 0x10000); r.arr (uint32_t)(target / (r.psc 1) - 1); } r.real_freq (uint32_t)((uint64_t)tim_clk / ((r.psc 1ULL) * (r.arr 1) * points)); return r; }target是总的分频比arr是它的整数部分减一超过 16 位时才启用psc。返回值里的real_freq必须拿去显示或者写进报告因为 ARR 取整后实际频率和目标值在高频段能差出几个百分点。以 84MHz 定时器时钟、128 点表为例| 目标输出 | 需要触发频率 | ARR | 实际输出 | | 100 Hz | 12.8 kHz | 6561 | 100.00 Hz | | 1 kHz | 128 kHz | 655 | 1000.15 Hz | | 10 kHz | 1.28 MHz | 64 | 10096 Hz | | 50 kHz | 6.4 MHz | 12 | 50538 Hz | | 100 kHz | 12.8 MHz | 5 | 109375 Hz |高目标频率一栏的误差已经很刺眼了根因是分频比整数化。要在这个频段继续走查表路线唯一的办法是缩短码表长度比如高频段切到 16 点表让分频比回到几十的量级。3.3 DAC 加 DMA 循环模式的初始化与启动顺序顺序搞反是新手最常见的失败点先开 DAC 再配 DMA第一个完整周期会被后面的配置打断先开 DMA 再配定时器会出现一段无触发的静止电平。正确的顺序是时钟、GPIO、DMA、DAC、定时器最后统一使能。void wave_start(void) { sine_tab_build(); /* 1. DMA内存到外设循环模式字长半字对应 12 位右对齐 */ hdma_dac1.Instance DMA1_Stream5; hdma_dac1.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_dac1.Init.PeriphInc DMA_PINC_DISABLE; hdma_dac1.Init.MemInc DMA_MINC_ENABLE; hdma_dac1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_dac1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_dac1.Init.Mode DMA_CIRCULAR; /* 关键循环重放码表 */ HAL_DMA_Init(hdma_dac1); __HAL_LINKDMA(hdac, DMA_Handle1, hdma_dac1); /* 2. 定时器TRGO 选更新事件DAC 每次更新输出一个点 */ htim6.Instance TIM6; htim6.Init.Prescaler 0; htim6.Init.CounterMode TIM_COUNTERMODE_UP; htim6.Init.Period 655; /* 对应 1kHz 输出见表 */ HAL_TIM_Base_Init(htim6); TIM6-CR2 TIM_TRGO_UPDATE; /* 3. DAC开启触发不要软件触发 */ HAL_DAC_Init(hdac); HAL_DAC_ConfigChannel(hdac, sConfig, DAC_CHANNEL_1); /* 4. 先起 DMA 搬运再起定时器保证第一个周期完整 */ HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t *)sine_tab, SINE_POINTS, DAC_ALIGN_12B_R); HAL_TIM_Base_Start(htim6); }Mode必须是DMA_CIRCULAR否则码表搬完一次就停输出变成一段直流。MemDataAlignment和PeriphDataAlignment都用半字对应 12 位数据放在 16 位寄存器低位、再靠DAC_ALIGN_12B_R对齐到高 12 位。DAC_Trigger必须配成DAC_TRIGGER_T6_TRGO如果用DAC_TRIGGER_NONEDAC 只会在软件写入时更新一次波形不会周期输出。3.4 输出频率的下限、上限与切换波形时的缓冲区安全下限由 ARR 的 16 位和 PSC 决定PSC 为 0、ARR 取 65535、128 点表时最低输出约 84e6/(65536×128) ≈ 10Hz。要往下走必须提高 PSC但 PSC 一高同一档 ARR 下可调的分辨率就变粗低频段微调会变得很难受。上限受两头夹击DAC 建立时间在 12 位精度下约 1µs 量级DMA 每个点还要占用总线周期实测稳定刷新在 1~2MSPS。按 128 点表算最高正弦输出也就 8~15kHz。这个数字必须在报告里写清楚而不是含糊地写「频率范围 1Hz 到 100kHz」——100kHz 只有在 16 点甚至 8 点表下才勉强够波形质量已经完全不是正弦。切换波形或频率时先HAL_DAC_Stop_DMA停搬运改完sine_tab指针和 ARR再从头启动。直接在半途改指针DMA 可能正在读旧表尾部、新表头部输出会插进去一段跳变后级功放或者被测电路可能被打出尖峰。4. 波形发生器的多波形切换、幅度偏置程控与串口调参实现4.1 一张表生成正弦、方波、三角波、锯齿波与其准备四张表占四份 RAM不如用一个生成函数按时序位置算出归一化值再统一映射到 DAC 码值区间。这样码表长度、使用率、偏置点全部是同一套参数切换波形时不用重新分配缓冲区。typedef enum { WAVE_SINE, WAVE_SQUARE, WAVE_TRI, WAVE_SAW } wave_type_t; void wave_tab_build(wave_type_t type, uint16_t *tab, int n) { for (int i 0; i n; i) { float r (float)i / (float)n; /* 归一化相位 0 ~ 1 */ float v; switch (type) { case WAVE_SINE: v 0.5f 0.5f * sinf(2.0f * 3.14159265f * r); break; case WAVE_SQUARE: v (r 0.5f) ? 1.0f : 0.0f; /* 占空比 50% */ break; case WAVE_TRI: v (r 0.5f) ? (2.0f * r) : (2.0f - 2.0f * r); break; case WAVE_SAW: v r; break; default: v 0.5f; break; } /* 同样留 5% 余量统一映射到 12 位码值 */ tab[i] (uint16_t)(4095.0f * (0.05f 0.90f * v) 0.5f); } }方波用查表输出的边沿取决于 DAC 刷新率1MSPS 下上升时间就是 1µs 量级做时钟源不够用。要陡边沿正确做法是从 DDS 或者码表信号后面接一个高速比较器把正弦整形为方波边沿能压到几十纳秒或者干脆用定时器 PWM 通道直接输出。WAVE_SQUARE走 DAC 只适合做逻辑电平演示报告中要注明。4.2 幅度与偏置数字域缩放还是模拟域 PGA幅度控制的三种做法差别很大选错了会白白丢掉 DAC 的有效位数。数字域缩放的实现最简单改一个系数乘进码表就行但输出幅度降到 10% 时DAC 实际只用到 400 个码值等效分辨率掉到 8 位多小信号时底噪立刻显出来。数字电位器调 DAC 参考电压的方式不损失位数但可调的参考源本身有噪声和温漂。后级 PGA 放在模拟通路里数字域保持满幅输出信噪比最好代价是增加器件和调试量。| 幅度方案 | 可调范围 | 有效位数损失 | 电路复杂度 | 建议用途 | | 数字域缩放 | 1%~100% | 明显小幅度差 | 无 | 快速验证 | | 调 DAC 参考电压 | 10%~100% | 无 | 中 | 需要稳定幅度 | | 后级 PGA 或数字电位器 | 0~满摆幅 | 无 | 高 | 输出大摆幅信号 |实际工程里常用组合数字域做粗调把幅度落在 PGA 的某个增益档附近PGA 做细调保证数字域始终输出接近满幅。偏置则用后级加法器实现一路接信号、一路接可调直流直流由第二个 DAC 通道或者 PWM 加 RC 滤波给出。提示偏置和幅度要分别接示波器直流耦合和交流耦合各测一次。只测交流耦合会看不到偏置漂移只测直流耦合又不容易发现偏置引入的偶次谐波。4.3 串口调参帧格式与解析上位机调参用带帧头、长度和校验的定长帧最省事丢包和粘包都容易定位。帧头用两个字节是为了避免数据区偶然出现帧头字节时误判。typedef struct __attribute__((packed)) { uint8_t head[2]; /* 固定 0xAA 0x55 */ uint8_t cmd; /* 0x01 设频率0x02 设幅度0x03 设偏置0x04 切波形 */ uint8_t len; /* 紧随其后的有效数据字节数最大 8 */ uint8_t data[8]; uint8_t crc; /* 从 cmd 到 data 末尾的 CRC-8 */ } wave_frame_t; static uint8_t crc8(const uint8_t *p, int n) { uint8_t c 0x00; while (n--) { c ^ *p; for (int i 0; i 8; i) c (c 0x80) ? (uint8_t)((c 1) ^ 0x07) : (uint8_t)(c 1); } return c; } /* 返回 0 表示校验通过并已执行 */ int wave_frame_handle(const wave_frame_t *f) { if (f-head[0] ! 0xAA || f-head[1] ! 0x55) return -1; if (f-len sizeof(f-data)) return -2; if (crc8((const uint8_t *)f-cmd, f-len 2) ! f-crc) return -3; switch (f-cmd) { case 0x01: { /* 小端 4 字节单位 Hz */ uint32_t hz (uint32_t)f-data[0] | ((uint32_t)f-data[1] 8) | ((uint32_t)f-data[2] 16) | ((uint32_t)f-data[3] 24); wave_apply_freq(hz); break; } case 0x02: /* 2 字节千分比 0~1000 */ wave_apply_amp((uint16_t)(f-data[0] | (f-data[1] 8))); break; case 0x04: wave_apply_type((wave_type_t)f-data[0]); break; default: return -4; } return 0; }crc8的覆盖范围是cmd到data末尾共len 2字节注意不要漏掉len本身。__attribute__((packed))在 Cortex-M 上让结构体不插入填充字节直接按字节流解析不会错位。命令执行函数内部先做范围钳位再改硬件参数避免上位机误发一个 0 或者超大值把 DMA 配置打乱。4.4 参数越界钳位与突变保护频率越界要钳到[f_min, f_max]并回一条带实际值的应答帧而不是静默丢弃。幅度变化建议加斜率限制比如每 10ms 只允许变化 1% 满量程直接跳变会让后级功放或者被测器件吃到一次冲击。波形切换和频率大幅变更都要走「停 DMA、改参数、重启」的完整流程中间不要插入延时否则输出会挂在一个固定电平上。5. 波形发生器实测验证与杂散失真排查5.1 用示波器 FFT 与脚本算 THD验证波形发生器最直接的方式是示波器开 FFT看基波和 2 到 5 次谐波的相对幅度。要注意的是示波器 FFT 的窗函数和点数会严重影响读数用矩形窗测非整周期信号泄漏出来的谱线会被误判成杂散。想做有说服力的数据把波形导成 CSV用脚本统一处理更靠谱。import numpy as np def thd_from_csv(path, fs, f0, n_harm5): fs采样率 Hzf0信号频率 Hz返回 THD 比值 x np.loadtxt(path, delimiter,, skiprows1)[:, 1] x x - x.mean() # 去直流避免 0Hz 分量抬高底噪 X np.abs(np.fft.rfft(x * np.hanning(len(x)))) freqs np.fft.rfftfreq(len(x), 1.0 / fs) def peak(f): i int(np.argmin(np.abs(freqs - f))) return X[max(0, i - 2):i 3].max() # 取峰附近 5 点防栅栏效应 h1 peak(f0) hr np.sqrt(sum(peak(k * f0) ** 2 for k in range(2, n_harm 1))) return hr / h1 print(fTHD {thd_from_csv(wave.csv, 1e6, 1000) * 100:.2f} %)fs要填示波器或者采集卡的采样率不是波形发生器的触发频率这两个值填反了整套算出来的都是错的。np.hanning加窗是为了压泄漏代价是主瓣变宽所以取峰值时要在中心点左右各扩两个 bin。算出来的比值乘 100 就是百分比1kHz 正弦在 128 点表、重建滤波器截止 2kHz 的情况下做到 1% 以内是比较正常的水平。5.2 输出异常与杂散的定位表| 现象 | 可能原因 | 定位动作 | | 正弦上有明显台阶 | 每周期点数太少或滤波器截止太高 | 点数翻倍再测台阶消失即确认 | | 频率比设定值偏高 | ARR 取整截断 | 读回 ARR 反算实际频率和设定值对比 | | 频谱上等间距杂散 | 重建滤波器缺失或截止频率过高 | 把截止降到输出频率 1.5 倍再测 | | 幅度随频率升高而下降 | 后级运放压摆率或带宽不足 | 换成高 SR 运放或降低输出幅度复测 | | 低频段波形抖动 | 码表被改写或中断抢占 DMA | 改双缓冲检查是否有高频中断 | | 小幅度时底噪明显 | 数字域缩放损失有效位 | 改用后级 PGA数字域保持满幅 |注意排查顺序建议从时钟侧往模拟侧走先确认触发频率和码表长度对不对再看 DAC 输出引脚上的原始阶梯波最后才看滤波器之后的波形。从后往前查很容易把滤波器的问题当成码表的问题。5.3 用 PSC 和 ARR 联合搜索逼近目标频率高频段 ARR 整数化带来的频率误差可以用 PSC 和 ARR 的组合来压。单独调 ARR 时可选的分频比是连续的整数加上 PSC 之后可用分频比变成(PSC1) × (ARR1)组合数多了几个数量级总能找到更接近目标的那个。def best_div(tim_clk, f_out, points, psc_max255): 穷举 PSC 找误差最小的 ARR 组合返回 (psc, arr, 实际频率, 误差) target tim_clk / (f_out * points) # 需要的总分频比 best None for psc in range(psc_max): arr round(target / (psc 1)) - 1 if arr 1 or arr 65535: continue real tim_clk / ((psc 1) * (arr 1) * points) err abs(real - f_out) if best is None or err best[3]: best (psc, arr, real, err) return best print(best_div(84e6, 50_000, 128)) # 128 点表下逼近 50kHzpsc_max取 255 已经足够因为 PSC 越大、分频比越粗继续往上找只会让误差变大。返回值里第一个是写进 PSC 寄存器的值第二个是 ARR第三个是实际输出频率第四个是绝对误差直接抄进报告的实测数据表。把这段脚本和wave_set_freq一起用先离线把每个频点的 PSC/ARR 算好存成表上电时直接查表写寄存器比在中断里做除法快得多也避免了运行时反复试参数导致输出中断。本文还有配套的精品资源点击获取