简介:基于STM32的信号发生器工程源码,适合用作毕设、课程设计或工程实训项目,面向希望学习STM32外设驱动、波形生成与界面交互的初中级开发者。资源共47个文件,以22个c源码与22个h头文件为主体,覆盖LCD显示、菜单交互、AD9851波形合成、DAC输出、FFT、参数管理等功能模块;另含SignalGenerator_v0.1.ioc的CubeMX配置文件和README说明文档,整体仅101KB,结构清晰。项目支持正弦波、方波、三角波及其线性组合波形,可实现100Hz~20kHz频率、0~5V幅度调节,并具备波形存储和显示功能。已有601人学习,适合作为波形发生器类课题的参考蓝本,尤其可借鉴其AD9851驱动框架、菜单与参数管理思路,快速搭建或改进自己的信号源方案。
1. 为什么做信号发生器,第一件事不是查表
很多人在 STM32 上做信号发生器,第一反应是“建一个正弦波表,用定时器中断喂给 DAC”。这个思路没有错,但如果你把频率从 1kHz 调到 10kHz,会发现波形点数变稀疏,失真肉眼可见;再往上调,中断频繁到把主循环拖垮。问题不在查表本身,而在你没有一个“频率可变、波形连续”的机制。
“基于 STM32 的信号发生器”这个项目,真正要解决的是三件事:合成什么样的波形、如何让频率连续可调、以及输出到外部电路时信号还能不能看。DDS(直接数字频率合成)是消费级和工业级信号发生器里最常用的方案,相位累加器加查找表,既能保证频率分辨率,又能让波形相位连续,不会在切换频率时出现跳变。本文用 STM32F103 为主控,走通一套从 DDS 原理到 DAC 输出、再到运放调理和串口控制的完整路径,你拿到手能用,也能看懂每一行代码为什么这么写。适合做毕业设计、仪器类项目,或者单纯想把波形发生器模块做成产品原型的工程师。
2. DDS 波形合成原理与频率、幅值参数的计算方法
2.1 相位累加器为什么是 32 位而不是 16 位
DDS 的核心结构是相位累加器加波形查找表。系统时钟每个周期,相位累加器累加一次频率控制字(FCW,Frequency Control Word),累加的结果作为查表地址,查找表输出对应的波形幅值,送给 DAC 转换。这个结构里没有“中断”,也没有“分频”,频率只要改变 FCW 就立即变化,所以能做到频率连续可调。
相位累加器的宽度直接决定了频率分辨率。假设系统时钟为 Fclk,相位累加器位宽为 N,那么:
最小频率步进 = Fclk / 2^NSTM32F103 最高主频 72MHz,如果用 32 位相位累加器,最小步进约为 0.0168Hz。这意味着在 1kHz 附近,你可以做到小数点后两位的频率调节。如果换成 16 位位宽,步进直接跳到 1098Hz,连 1kHz 的信号都出不来,所以 32 位累加器是必须的。相位累加器高位作为查表地址,比如查找表有 1024 个点,就取累加器的高 10 位(累加器为 32 位时,即可用acc >> 22得到)。
频率控制字的计算公式是:
FCW = Fout * 2^32 / Fclk其中 Fout 为目标信号频率。反过来说,实际输出频率为:
Fout = FCW * Fclk / 2^32这里有几个 Fclk 要注意:是 DAC 被驱动的时钟频率,不是 CPU 主频。DAC 触发来源如果设为定时器,那么 Fclk 就是定时器的计数频率。最常见的做法是让定时器以 72MHz 计数,每个计数周期触发一次 DAC 转换,此时 Fclk 就是 72MHz。下面给出一个频率和相位控制字的对照表,方便你验证自己的固件有没有写错:
| 目标频率 (Hz) | 系统时钟 (MHz) | 32位 FCW (十进制) | 32位 FCW (十六进制) |
|---|---|---|---|
| 100 | 72 | 5976885 | 0x005B366D |
| 1000 | 72 | 59768852 | 0x0390204A |
| 10000 | 72 | 597688524 | 0x23A06FCA |
| 100000 | 72 | 5976885238 | 0x647ACE5E |
验算一下:0x647ACE5E 约为 1685MHz,乘以 72MHz 再除以 2^32,就得到约 100kHz。你需要在自己代码里把 FCW 定义成uint32_t,并且注意乘法溢出问题,Fout * 4294967296ULL要用 64 位中间变量来算。
2.2 查找表不只正弦波,相位修正才是关键
查找表里既可以放正弦波,也可以放三角波、方波、锯齿波。正弦波表最常见的生成方式是 C 语言里用math.h的sin()函数在 PC 端生成一个整数数组,然后复制到 STM32 工程里。但这里有一个很容易出错的细节:DAC 输出的正弦波,理想情况下一个周期里有两个过零点,如果查找表没有包含 0 值对应的点,输出波形会叠加一个直流偏置。
以 1024 点表、12 位 DAC 为例,每个点的值应该是:
dac_value = (sin(2 * PI * i / 1024) + 1) * 2047.5注意,先加 1 再乘以 2047.5,这样输出范围是 0 到 4095 的满幅。如果漏了+1,负半周全部被 DAC 截掉。还有一个细节:DAC 是单极性输出,直接出正弦波会有 1.65V 左右的直流偏置(在 3.3V 电源下),后面需要运放做偏置扣除。
方波和锯齿波可以直接用公式生成,不需要存储整张表。但为了统一代码逻辑,一般情况下我会直接生成一张 1024 点的通用查表,每种波形独立成数组。这样波形切换只需要更换查找表指针,DDS 核心代码不用改。
下面是生成查找表的 Python 脚本,你可以把它放在 PC 上,输出 C 语言头文件直接拷贝进工程:
import math # 1024 点查询表 SAMPLE_COUNT = 1024 DAC_MAX = 4095 def generate_sine_table(): table = [] for i in range(SAMPLE_COUNT): val = (math.sin(2 * math.pi * i / SAMPLE_COUNT) + 1) * (DAC_MAX / 2) table.append(int(round(val))) return table def generate_square_table(): table = [] for i in range(SAMPLE_COUNT): if i < SAMPLE_COUNT // 2: val = DAC_MAX else: val = 0 table.append(val) return table sine_table = generate_sine_table() square_table = generate_square_table() with open("wave_table.h", "w") as f: f.write("#ifndef WAVE_TABLE_H\n") f.write("#define WAVE_TABLE_H\n\n") f.write("const uint16_t sine_wave_table[1024] = {\n") for i in range(0, len(sine_table), 8): f.write(" " + ", ".join(str(x) for x in sine_table[i:i+8]) + ",\n") f.write("};\n\n") f.write("const uint16_t square_wave_table[1024] = {\n") for i in range(0, len(square_table), 8): f.write(" " + ", ".join(str(x) for x in square_table[i:i+8]) + ",\n") f.write("};\n\n#endif\n")这段代码生成两个数组,每个数组是 1024 个 16 位整数。注意生成方波时,前 512 点全为高,后 512 点全为低,这会让 DAC 输出直接饱和而不是产生平滑的边沿,实际示波器上看到的就是一条垂直的边沿,这在低频时没问题,但高频方波会受运放压摆率影响变圆,是正常的物理现象。
2.3 把 FCW 和查表地址串起来:DDS 核心代码
下面是 STM32 上最小可用的 DDS 更新函数,可以放在定时器更新中断里调用,也可以放在 DAC 转换完成中断里调用:
// dds.c #include "dds.h" volatile uint32_t phase_acc = 0; // 32位相位累加器 volatile uint32_t phase_inc = 0; // 频率控制字 FCW const uint16_t *waveform_table = sine_wave_table; // 指向当前波形表 uint32_t table_mask = 0x3FF; // 1024点 -> 取高10位,等效于 & 0x3FF void DDS_SetFrequency(uint32_t freq_hz, uint32_t sys_clk_hz) { // 使用 64 位计算避免溢出 phase_inc = (uint64_t)freq_hz * 4294967296ULL / sys_clk_hz; } // 在定时器触发中断中调用 void DDS_UpdateSample(void) { uint32_t table_index; phase_acc += phase_inc; table_index = phase_acc >> 22; // 32位累加器高10位,范围0~1023 // 从查找表读取输出值,写入DAC数据寄存器 uint16_t dac_val = waveform_table[table_index]; DAC->DHR12R1 = dac_val; }中断每次触发,相位累加器增加一个 FCW,高位作为查表索引,查表结果直接写入 DAC 数据寄存器DAC->DHR12R1。phase_acc >> 22这一步是把 32 位累加器的高 10 位取出来,等价于(phase_acc / 1024) % 1024,但移位操作更节省 CPU 周期。table_mask在做波形表长度不是 2 的整数次幂时才需要用按位与代替移位,这里 1024 是 2 的 10 次幂,所以直接移位即可。
要用的话,把DDS_SetFrequency放在初始化时调用一次,主循环里改频率时再调用。关键在于这个函数里 64 位运算的写法:如果不加ULL后缀,freq_hz * 4294967296在 32 位平台上会溢出成一个奇怪的小数,最终频率输出完全错乱。这是 DDS 代码里最常见的 bug 之一,没有之一。
3. STM32 HAL 库实现 DAC + DMA 的波形输出与工程配置
3.1 DAC 选定时器触发还是 DMA 循环:优先 DMA 循环
DAC 输出信号的流畅程度取决于你喂数据的方式。用HAL_DAC_SetValue()在中断里每次都调用,CPU 占用率会随采样率上升。72MHz 主频下,如果输出 100kHz 信号、每个周期 1024 个点,那么采样率就是 102.4MHz,超过 DAC 最大转换速率,这个方案根本走不通。实际 DDS 系统中,输出频率上限受限于两个因素:定时器触发速率和 DAC 转换时间,STM32F103 的 DAC 转换时间从设置数据到电压稳定大约需要 2us 左右,所以单通道输出最高支持到约 200kHz 的信号但波形点数会变少,一般建议不超过 50kHz。
DMA 循环模式是最省 CPU 的做法:DAC 的数据寄存器地址作为 DMA 外设地址,波形表数组作为存储器地址,DMA 配置为循环模式,定时器触发 DMA,DMA 把数据搬运到 DAC 输出寄存器。整个过程 CPU 完全不参与,你只需要在初始化时启动 DMA,之后修改频率只需改相位增量和定时器周期。
工程配置方面,在 STM32CubeMX 里需要设置以下内容:
| 外设 | 配置项 | 参数 |
|---|---|---|
| DAC | Output Buffer | Enable |
| DAC | Trigger | Timer 2 Update Event |
| DMA | DAC_CH1 | Circular Mode,数据宽度 Half Word(16位) |
| Timer 2 | Prescaler | 0 |
| Timer 2 | Counter Period | 1 (即 72MHz / 1 = 72MHz 触发) |
| Timer 2 | Auto-Reload Preload | Enable |
注意 DMA 数据宽度必须选择 Half Word,因为查找表类型是uint16_t。如果错设成 Word,DAC 会每隔一个表项跳变,示波器上看到的波形像被压缩了一半,频率是全错的。
3.2 最小可运行固件:初始化、启动、改频三步走
下面是一份可以运行在 STM32F103C8T6 上的 HAL 库工程代码,只保留了信号发生器核心逻辑,串口部分在下一章展开:
// main.c 关键片段 #include "main.h" #include "dac.h" #include "dma.h" #include "tim.h" #include "usart.h" #include "dds.h" // 外部已链接波形表 extern const uint16_t sine_wave_table[1024]; extern volatile uint32_t phase_inc; extern volatile uint32_t phase_acc; extern const uint16_t *waveform_table; int main(void) { HAL_Init(); SystemClock_Config(); // 配置为72MHz主频 MX_GPIO_Init(); MX_DMA_Init(); MX_DAC_Init(); // 触发源选择 Timer2 Update MX_TIM2_Init(); // 72MHz / (0+1) / (1+1) = 36MHz 触发 MX_USART1_UART_Init(); // 用于后续串口控制 // 配置 DAC 通道1,12 位右对齐,使能输出缓冲 DAC_ChannelConfTypeDef sConfig = {0}; sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; sConfig.DAC_Trigger = DAC_TRIGGER_T2_TRGO; sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1); // 启动 DMA 循环搬运,波形表首地址,长度1024 HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)sine_wave_table, 1024, DAC_ALIGN_12B_R); // 启动定时器触发 HAL_TIM_Base_Start(&htim2); // 初始输出 1kHz 正弦波 DDS_SetFrequency(1000, 72000000); while (1) { // 主循环不做任何事,波形由 DMA 自动输出 // 后续在这里处理串口接收指令 } }初始化顺序有一个坑:必须先启动 DMA 再启动定时器,否则定时器先触发时 DMA 还没就绪,第一个采样点会丢数据,偶尔能看到波形开头有一段毛刺。而HAL_DAC_Start_DMA内部会同时配置 DAC 和 DMA,所以先调用它,再调HAL_TIM_Base_Start才是正确的。
DAC_ALIGN_12B_R表示数据右对齐,DMA 搬运的每个 16 位数据只有低 12 位生效,高 4 位忽略。这里查找表生成时范围是 0 到 4095,正好落在低 12 位里。
3.3 修改输出频率的正确方式:改 FCW 而不是改定时器
很多人做到这一步,会在主循环里直接改定时器周期来调频率,比如__HAL_TIM_SET_AUTORELOAD(&htim2, value)。这在小范围变频时没问题,但会引发两个隐患:一是定时器计数方向不确定会导致当前周期长度突变,示波器上频率切换点出现一个明显拉长或缩短的波形周期;二是 DAC 采样率随频率变化,当定时器周期变大,采样率降低,高频信号的点数反而减少,波形失真加剧。
推荐的做法是保持定时器触发频率固定,只修改 DDS_SetFrequency 里的 phase_inc。这样采样率恒定不变,波形质量在所有频率范围内一致。TIM2 的 ARR 保持为 1(72MHz 触发太频繁,实际可用 36MHz 以下触发,可以设置 PSC=1,这样 DAC 有足够转换时间),DDS 每接收一个采样点输出一个值,而输出信号频率由 FCW 决定。
// dds.c 中的频率设置接口 void DDS_SetFrequency(uint32_t freq_hz, uint32_t sys_clk_hz) { phase_inc = (uint64_t)freq_hz * 4294967296ULL / sys_clk_hz; } // 使用示例 DDS_SetFrequency(5000, 36000000); // 输出 5kHz DDS_SetFrequency(8000, 36000000); // 输出 8kHz,无缝切换修改了 FCW 之后,DMA 仍按固定速率搬运数据,CPU 完全不参与波形生成。实测用这种方案,波形频率切换的相位连续性比改定时器 ARR 高一个量级,示波器上几乎看不到切换点毛刺。
4. 输出信号的质量优化:从 DAC 到外部电路的关键调理
4.1 DAC 直接输出的三大问题:直流偏置、负载能力、输出阻抗
从 STM32 的 PA4 引脚直接测量波形,你会发现三个问题同时存在。第一,DAC 是单极性输出,正弦波在 0 到 3.3V 之间摆动,平均值大约是 1.65V,这是系统电源的一半,不是我们习惯的双极性信号。第二,DAC 输出缓冲器有驱动能力限制,STM32F103 的 DAC 在缓冲关闭时输出阻抗约为 15kΩ,直接接 1kΩ 负载电压会跌掉一截。第三,波形上叠加了高频量化噪声,特别是输出高阻负载时更明显。
解决思路是 DAC 后面加一个二阶低通滤波器和一个运放调理电路。滤波器滤掉采样率相关的镜像频率,运放负责扣除直流偏置和把信号放大到目标幅值。运放的选型也很关键,直接选LM358或LM324这类通用运放在 100kHz 以上会明显变形,压摆率跟不上。推荐使用 SGM8521 或 OPA340 这类轨对轨运放,压摆率在 1V/us 以上,带宽 1MHz 以上,对 0-100kHz 的信号足够。
4.2 二阶低通滤波器的参数计算:截止频率的确定
低通滤波器在这里的作用是滤除采样镜像频率。如果 DAC 以 36MHz 速率输出,那么信号在 36MHz ± Fout 处都会有镜像,必须滤掉。但同时滤波器不能衰减我们需要的目标信号,所以截止频率一般设为最高输出频率的 3 到 5 倍。
比如系统最高输出 50kHz 的波形,滤波器截止频率设为 150kHz,这样 50kHz 处衰减很小,而 36MHz 镜像被大幅压制。下面是巴特沃斯二阶低通的元件参数:
| 截止频率 (kHz) | R1 (Ω) | R2 (Ω) | C1 (nF) | C2 (nF) |
|---|---|---|---|---|
| 100 | 1000 | 1000 | 1.6 | 0.8 |
| 150 | 1000 | 1000 | 1.06 | 0.53 |
| 200 | 1000 | 1000 | 0.8 | 0.4 |
具体计算方法是:令 R1 = R2 = R,C1 和 C2 分别取 1.414 倍和 0.707 倍的关系,然后代入fc = 1 / (2 * PI * sqrt(R1 * R2 * C1 * C2))。实际采购时使用标准容值即可,比如 200kHz 截止可以取 C1=820pF,C2=390pF,截止频率略微偏移不影响使用。
注意滤波器前面串联电阻要靠近 DAC 引脚,后面接运放输入。如果系统中还有数字电路干扰,在运放电源引脚上并联 100nF 去耦电容,能显著减小高频毛刺。
4.3 运放做偏置扣除和增益调整:电路计算与标定方法
运放电路推荐使用差分放大结构,一端接 DAC 信号,一端接一个参考电压,这样能够直接把 0-3.3V 的单极性信号变成 -Vref 到 +Vref 的双极性信号,同时通过电阻比设置增益。
典型电路参数如下:
- DAC 输出接 R1 = 10kΩ 到运放反相输入端
- 反相输入端 Rf = 20kΩ 反馈电阻到输出,增益 = -Rf / R1 = -2
- 运放同相输入端通过 R3 = 10kΩ 接地,R2 = 10kΩ 接 VREF = 1.65V
- VREF 直接从 STM32 的 VREFINT 输出或用电阻分压后加跟随器
设 DAC 输出为 Vdac,运放输出电压 Vout = (1 + Rf / R1) * Vref - Vdac * (Rf / R1),取 Rf = R1 = 10kΩ,Vref = 1.65V,则 Vout = 3.3V - Vdac。所以当 Vdac = 0V 时,Vout = 3.3V;当 Vdac=3.3V 时,Vout=0。波形范围被翻转并且压缩,这不是我们要的效果。因此需要重新设定电阻比例。
实用做法是:Vref 接一个可调电阻分压到 0 到 3.3V 之间,Rf=R1 时,整体波形上下平移。调节 Vref 使得 DAC 输出中点(数值 2048)时运放输出为 0V,这时候正弦波就会在 0V 上下摆动,消除了直流偏置。实测这一步至少需要 10 分钟时间,但做完后信号才是真正可用的“信号发生器”输出。
4.4 波形失真的边界:量化噪声与压摆率约束
即使滤波器和运放都做好了,输出波形依然有极限边界。首先是 STM32 DAC 是 12 位分辨率,理论信噪比约为 74dB,但实际布局不好时,数字开关噪声耦合到模拟电源上,信噪比可能掉到 60dB 以下,表现为波形包络出现细密毛刺。改善方法是把 STM32 的 VDDA 引脚单独用磁珠或小电阻从 VDD 隔离,加上 1uF 和 100nF 两级去耦电容。
其次是运放压摆率导致的高频失真。SGM8521 压摆率约 0.6V/us,双极性输出满幅 6.6V 时,最高输出频率不能超过SR / (2 * PI * Vp),即 0.6e6 / (2 * 3.14 * 3.3) 约 29kHz。超过这个频率,运放追踪不上信号速率,正弦波会变成三角波。输出更高频信号时必须选择压摆率更高的运放(比如 OPA340 压摆率 6V/us),或者降低输出幅值。这一点在项目开头就要想清楚,否则后面改板子成本很高。
5. 串口在线调频与扫频模式:给信号发生器加一个实用操控接口
5.1 串口指令协议与最小实现
信号发生器作为一个台式仪器,必须有外部控制接口。最实用的是用串口发文本指令来控制频率和波形,不需要复杂协议。建议用单行文本,每行一条指令,格式为:
FREQ 1000 // 设置频率,单位 Hz WAVE SINE // 切换波形,可选 SINE / SQUARE AMPL 1.5 // 设置幅值,需配合硬件增益控制 SWEEP 100 1000 5 // 从 100Hz 扫到 1kHz,持续时间 5 秒STM32 端用HAL_UART_Receive_IT接收字符,遇到\n解析一行。下面是核心解析代码:
// uart_cmd.c uint8_t rx_buffer[64]; uint8_t rx_index = 0; uint8_t rx_data; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { if (rx_data == '\n') { rx_buffer[rx_index] = '\0'; ParseCommand((char *)rx_buffer); rx_index = 0; } else { if (rx_index < sizeof(rx_buffer) - 1) { rx_buffer[rx_index++] = rx_data; } } HAL_UART_Receive_IT(&huart1, &rx_data, 1); } } void ParseCommand(char *cmd) { char param[32]; uint32_t freq; if (sscanf(cmd, "FREQ %lu", &freq) == 1) { DDS_SetFrequency(freq, TIMER_TRIGGER_HZ); // 可选:把设定值回传,方便上位机确认 char ack[32]; sprintf(ack, "FREQ OK %lu\r\n", freq); HAL_UART_Transmit(&huart1, (uint8_t *)ack, strlen(ack), 100); } else if (sscanf(cmd, "WAVE %s", param) == 1) { if (strcmp(param, "SINE") == 0) { waveform_table = sine_wave_table; } else if (strcmp(param, "SQUARE") == 0) { waveform_table = square_wave_table; } HAL_UART_Transmit(&huart1, (uint8_t *)"WAVE OK\r\n", 9, 100); } }这段代码用sscanf而不是手动字符串比较,是为了方便扩展参数。注意sscanf解析%lu时对应unsigned long,STM32 上uint32_t就是unsigned long,但保险起见可以强转一下。频率接收建议加上限幅度:小于 1Hz 或大于 50000Hz 直接丢弃,避免误输入导致 DAC 输出不可预期的波形。如果上位机持续以 10Hz 频率改频率,DDS_SetFrequency里的 64 位除法每次大约耗时几微秒,完全不影响主循环。
5.2 扫频模式的时序设计与频率步进策略
扫频是信号发生器的高频使用场景,用于测滤波器频率响应、扬声器阻抗曲线或者做振动台扫频测试。扫频设计有两个难点:频率步进怎么选,以及每个频率点停留多久。
如果从 100Hz 扫到 10kHz,总共 9900Hz 范围,步进 10Hz,那么需要 990 个频率点。每个点停留 10ms,总耗时约 10 秒。但要注意的是人耳对对数频率更敏感,所以线性步进在低频段听感变化快,高频段变化慢,更好的方式是对数步进:频率按倍数增长,比如每次乘以 1.01,这样在示波器上看到的频谱包络更均匀。
代码实现可以用非阻塞方式,在主循环里检查时间:
// sweep.h/c —— 非阻塞扫频 #define SWEEP_POINTS 200 typedef struct { uint32_t start_freq; uint32_t end_freq; uint32_t duration_ms; uint32_t last_update; uint8_t active; float freq_multiplier; } sweep_ctx_t; void Sweep_Start(sweep_ctx_t *ctx, uint32_t start, uint32_t end, uint32_t duration_ms) { ctx->start_freq = start; ctx->end_freq = end; ctx->duration_ms = duration_ms; ctx->last_update = HAL_GetTick(); ctx->active = 1; float ratio = (float)end / (float)start; ctx->freq_multiplier = powf(ratio, 1.0f / SWEEP_POINTS); DDS_SetFrequency(start, TIMER_TRIGGER_HZ); } void Sweep_Tick(sweep_ctx_t *ctx) { if (!ctx->active) return; uint32_t now = HAL_GetTick(); uint32_t elapsed = now - ctx->last_update; uint32_t step_ms = ctx->duration_ms / SWEEP_POINTS; if (elapsed >= step_ms) { ctx->last_update = now; static uint32_t current_freq = 0; if (current_freq == 0) { current_freq = ctx->start_freq; } current_freq = (uint32_t)(current_freq * ctx->freq_multiplier); if (current_freq >= ctx->end_freq) { current_freq = ctx->start_freq; // 循环扫描 } DDS_SetFrequency(current_freq, TIMER_TRIGGER_HZ); } }扫频过程中由于 DDS 相位累加器不回零,频率切换时相位是连续的,不会出现“咔咔”的爆音。但如果频率跨度太大,比如 1Hz 到 50kHz,频率字变化范围跨越几千倍,一次性切换会产生很大的瞬时相位增量,听起来像杂音。解决方法是把扫频改成多次小步进,每次频率增量不超过当前频率的 5%。
5.3 验证输出是否准确的三个手段
固件写完后,验证环节不能省,以下三个方法廉价有效,按优先级排列:
第一,用示波器读频率。示波器自带频率测量功能,在 1kHz 和 10kHz 两个典型频率点上读值,误差应在 0.1% 以内,超过这个数说明系统主频不准或者 FCW 计算有溢出。第二,用万用表 AC 档测输出幅值,对 1kHz 正弦波,读到的交流电压应约为设计满幅的 0.707 倍,例如设计 3.3V 峰峰值,读数约 1.17V。如果读数偏差超过 20%,检查运放增益电阻是否焊错。第三,用耳机或者简易音频放大器听扫频过程,正常扫频应该是平滑上升的音调,如果有“嘶嘶”声则说明波形表里有坏点,检查数组越界或者查找表索引计算。
还有最后一个很多人忽略的验证点:把信号发生器的输出直接接到 STM32 的 ADC 输入,用 ADC 采回来对比原始波形表,可以算出失真度。但 ADC 采样和 DAC 输出共用同一个系统时钟,存在频谱泄漏问题,读数会偏乐观,所以这个只做辅助,最终以独立示波器为准。
本文还有配套的精品资源,点击获取