1. 从需求到方案:为什么要在STM32上做扫频?
扫频,简单说就是让一个信号的频率按照特定规律(比如线性、对数)随时间变化。这功能在电子测量、音频分析、材料检测、无线通信测试里太常见了。比如,你想测一个音箱的频率响应,就得用扫频信号去“激励”它,然后分析它的输出。以前干这活儿,要么用专门的信号发生器(贵),要么用电脑声卡(精度和频率范围有限)。现在,一块几十块钱的STM32开发板就能搞定,这就是嵌入式开发的魅力——把专业设备的功能“平民化”。
STM32实现扫频,核心就两点:精准的时序控制和灵活的信号生成。时序控制靠定时器,这是STM32的看家本领;信号生成,对于简单的正弦波、方波扫频,用DAC(数模转换器)输出模拟电压,或者直接用定时器产生PWM(脉宽调制)波都行。如果想玩得更花,比如生成任意波形的扫频,或者频率分辨率要求极高,那就得上DDS(直接数字频率合成)技术了,这通常需要外接专门的DDS芯片(如AD9833、AD9959),或者用STM32的高性能型号配合DMA(直接存储器访问)和数学运算来模拟。
我这次要聊的,是基于STM32内部DAC和定时器,实现一个基础但完全可用的正弦波扫频发生器。这个方案成本最低,硬件最简单(一块带DAC的STM32核心板就行),特别适合入门学习和对性能要求不苛刻的应用场景,比如音频范围内的信号源、简单的网络分析仪前端等。我们会从原理、配置、代码到调试,把整个链路掰开揉碎了讲清楚。
2. 硬件选型与核心原理:为什么是DAC+定时器?
首先得选对芯片。不是所有STM32都有DAC,比如经典的STM32F103C8T6(蓝色小板)就没有,而STM32F103C8T6的“大哥”STM32F103RCT6就有。更通用的选择是STM32F4系列,比如STM32F407,性能更强,外设更丰富。我这里以STM32F103RCT6为例,因为它比较常见,且足以说明问题。
扫频的核心原理可以分解为两个并行的任务:
- 频率控制:需要一个高精度的定时器(如TIM2)来设定“步进”的时间间隔。比如,我们希望频率从1kHz线性扫到10kHz,用时10秒。如果每秒改变100次频率(即步进率为100Hz),那么每次频率需要增加 (10k - 1k) / (10 * 100) = 9 Hz。定时器就负责每隔10ms(1/100Hz)产生一次中断,告诉我们:“该更新频率了!”
- 波形生成:需要一个能输出模拟电压的部件,这就是DAC。DAC需要源源不断地得到数字量,才能输出连续的电压。我们预先计算好一个正弦波表(Sine Table)存在数组里。DAC的输出频率
F_dac由另一个定时器(如TIM6)的触发频率决定。这个定时器以固定的速率(比如100kHz)触发DAC进行转换。每次触发,DAC就从正弦波表中取出一个点输出。
关键的一步是让这两个任务联动:任务1(频率控制定时器)的中断里,我们去改变任务2(波形生成)的“节奏”。具体来说,就是改变那个正弦波表的“步进指针增量”。DAC每次触发都固定地从表中取下一个点,如果我们每次指针加1,输出的正弦波频率就是F_dac / 表长度。如果我们改变这个增量,比如变成加2,那么DAC“跑完”整个正弦波表的速度就快了一倍,输出的频率也就翻倍了。这个“指针增量”就是控制输出频率的关键变量。
所以,整个系统的信号流是这样的:
定时器A(TIM6)以固定频率
F_trigger触发DAC转换 -> DAC从正弦波表中按当前“指针位置”取值输出 -> 指针位置每次增加一个“步进增量”(Step Increment)-> 另一个定时器B(TIM2)周期性中断,根据扫频算法(线性/对数)重新计算这个“步进增量” -> DAC输出的正弦波频率随之改变。
为什么不用PWM?PWM也可以滤波后得到模拟电压,但要做高质量的正弦波,滤波电路复杂,且动态改变频率时,滤波器的特性可能不匹配,导致波形失真。DAC是直接输出模拟电压,更干净,更直接。当然,如果只是产生方波扫频,用PWM就非常合适。
3. 工程搭建与关键外设配置
我们使用STM32CubeMX进行初始化配置,然后用Keil MDK或者STM32CubeIDE写代码。这样效率最高,也最不容易在底层寄存器上出错。
3.1 时钟树配置
这是稳定性的基石。STM32F103RCT6最高主频72MHz。我们让系统时钟(SYSCLK)跑在72MHz,APB1总线时钟(TIM2, TIM6挂在这里)也设为72MHz(注意:APB1最大36MHz,这里设置后,定时器时钟会倍频)。APB2总线时钟(DAC挂在这里)设为72MHz。确保所有用到的外设时钟都被使能。
3.2 DAC配置
- 在CubeMX中启用DAC1,选择通道1(PA4)。
- 关键配置项:
- 输出缓冲(Output Buffer):启用。这能提高DAC的驱动能力,但会引入一点失真。对于音频范围(20Hz-20kHz),开启没问题。如果追求极低失真,可以关闭,但需要外部运放缓冲。
- 触发源(Trigger):选择定时器6触发输出(Timer 6 TRGO)。这样DAC的转换将由TIM6自动触发,无需CPU干预。
- 波形生成(Wave generation):禁用。我们不用DAC内置的噪声波、三角波生成,而是用自己的正弦波表。
- DMA:强烈建议启用!为DAC通道1添加一个DMA请求,方向为存储器到外设(Memory to Peripheral),数据宽度为半字(Half Word)。这样,只要DAC被触发,DMA就会自动把正弦波表的下一个数据搬给DAC,完全解放CPU。这是实现高质量、稳定波形的关键。
3.3 定时器6(TIM6)配置:DAC的“节拍器”
TIM6是一个基本定时器,非常适合做这种简单的触发源。
- 配置时钟源为内部时钟(Internal Clock)。
- 计算参数:
- 目标触发频率(F_trigger):我们希望DAC的更新率是100kHz。这个值越高,生成波形的频率上限也越高(奈奎斯特采样定理),但对DAC和DMA的速度要求也越高。100kHz对于音频扫频绰绰有余。
- 定时器时钟(TIM_CLK):APB1时钟为72MHz,但基本定时器挂在APB1上,如果APB1预分频系数不为1,定时器时钟会倍频。在我们的配置里,TIM6的时钟就是72MHz。
- 预分频器(PSC):设置为
71。这样定时器时钟 = 72MHz / (71+1) = 1MHz。 - 自动重载值(ARR):设置为
9。这样定时器的更新频率 = 1MHz / (9+1) = 100kHz。这个更新事件(Update Event)会被设置为触发输出(TRGO),从而触发DAC转换。
- 在CubeMX中,设置PSC=71,ARR=9。
- 开启主模式(Master Mode),将主模式选择(Master Mode Selection)设置为更新(Update)。这样每次定时器溢出更新时,都会产生一个触发信号(TRGO)给DAC。
3.4 定时器2(TIM2)配置:扫频的“指挥家”
TIM2是一个通用定时器,我们用它的周期性中断来更新频率。
- 配置时钟源为内部时钟。
- 计算参数:
- 扫频步进率(Step Rate):假设我们想要100Hz的步进率,即每10ms更新一次频率。
- 定时器时钟:同样为72MHz。
- 预分频器(PSC):设置为
7199。定时器时钟 = 72MHz / (7199+1) = 10kHz。 - 自动重载值(ARR):设置为
99。这样定时器中断频率 = 10kHz / (99+1) = 100Hz,即中断周期为10ms。
- 在CubeMX中,设置PSC=7199,ARR=99。
- 开启更新中断(Update Interrupt)。
3.5 DMA配置
在DAC的配置中启用DMA后,CubeMX会自动生成DMA的初始化代码。我们需要检查并确保:
- DMA通道(如DMA1_Channel3)已配置。
- 模式为循环模式(Circular),这样当DMA传输完整个正弦波表后,会自动回到开头继续传输,形成连续的波形。
- 存储器地址自增(Increment Memory)开启,外设地址不增(Increment Peripheral)。
- 数据宽度为半字(16位),因为DAC数据寄存器是12位右对齐的,我们用16位变量存储刚好。
3.6 生成代码与基础框架
点击生成代码,选择熟悉的IDE。在生成的工程里,我们需要做以下几件事:
定义正弦波表:在
main.c的开头,定义一个全局数组。表的大小很重要,太小时波形阶梯状明显,太大占用内存且计算增量时精度可能受影响。256点或512点是个不错的折中。#define SINE_TABLE_SIZE 512 uint16_t sineTable[SINE_TABLE_SIZE];填充正弦波表:在
main函数的初始化部分(/* USER CODE BEGIN 2 */之后),用数学函数计算正弦值,并缩放到DAC的范围内(0-4095对应0V-3.3V)。为了得到以0V为中间值的交流信号,需要叠加一个直流偏置(2048对应1.65V)。#include <math.h> // ... 在初始化部分 for(int i = 0; i < SINE_TABLE_SIZE; i++) { // 计算一个完整正弦周期 float sineValue = sin(2 * M_PI * i / SINE_TABLE_SIZE); // 缩放到0-4095,并加上1.65V偏置 (4095/2 = 2047.5) sineTable[i] = (uint16_t)(2048 + 2047 * sineValue); }注意:使用
sin函数需要链接数学库(-lm)。在Keil中,需要在Target选项的Linker栏勾选Use MicroLIB,或者手动添加-lm。更高效的做法是预先用电脑生成这个表,然后以常量数组形式存储,节省运行时的计算开销。启动DMA传输:在初始化部分,将正弦波表的地址和长度告诉DMA,并启动传输。
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sineTable, SINE_TABLE_SIZE, DAC_ALIGN_12B_R);启动定时器:启动TIM6(触发DAC)和TIM2(控制扫频)。
HAL_TIM_Base_Start(&htim6); HAL_TIM_Base_Start_IT(&htim2); // 启动并开启中断
4. 核心算法实现:动态改变频率
前面提到,改变DAC输出频率的关键是改变从正弦波表中取样的“步进增量”。我们不是让DMA顺序地(每次+1)取数据,而是以一个可变的步长在表中跳跃式取数。这个步长就是相位增量(Phase Increment)。
4.1 相位累加器与频率控制字
我们需要一个“相位累加器”(phaseAccumulator),它是一个远大于正弦表索引的变量(比如32位无符号整数)。每次DMA请求(或定时器触发)时,我们不是直接给DMA一个索引,而是让phaseAccumulator加上一个“频率控制字”(phaseIncrement)。
uint32_t phaseAccumulator = 0; uint32_t phaseIncrement = 0; // 这个值决定了输出频率输出频率F_out的计算公式为:F_out = (phaseIncrement * F_trigger) / (2^N * SINE_TABLE_SIZE)其中:
F_trigger是TIM6触发DAC的频率(100kHz)。N是相位累加器的位宽(比如32)。SINE_TABLE_SIZE是正弦波表长度(512)。
这个公式的推导基于DDS原理。简单理解:phaseIncrement决定了相位累加器跑得多快,跑得越快,正弦波表被“遍历”得越快,输出频率就越高。
为了从phaseAccumulator得到正弦表索引,我们取它的高若干位(因为累加器位数多,表索引位数少)。例如,对于32位累加器和512(2^9)点的表,我们可以取phaseAccumulator的[31:23]这9位作为索引。
// 获取当前正弦表索引(假设phaseAccumulator是32位) uint16_t tableIndex = (phaseAccumulator >> 23) & 0x1FF; // 取高9位,对应512点 // 然后将 sineTable[tableIndex] 提供给DAC但是,我们用了DMA循环模式,DMA是自动顺序访问数组的,怎么实现这个“跳跃”呢?这里有个技巧:我们不直接改变DMA的访问方式,而是动态地改变DMA传输的目标数组。也就是说,我们准备多个不同频率的正弦波表,或者实时计算波形数据填充到DMA传输的缓冲区。对于STM32F1这类性能有限的芯片,实时计算负担较重,更实用的方法是:
方法:双缓冲区与实时更新
- 我们设置DMA为双缓冲区模式(Double Buffer Mode),或者使用两个数组(
bufferA和bufferB)和普通循环DMA,通过手动切换。 - 在TIM2的中断服务函数(10ms一次)里,我们根据当前要求的频率,计算出一个完整的、对应新频率的正弦波周期数据,填充到下一个缓冲区(比如
bufferB)。 - 当DMA传输完当前缓冲区(
bufferA)时,会产生一个**半传输完成(Half Transfer Complete)或传输完成(Transfer Complete)**中断。 - 在这个DMA中断里,我们将DMA的目标地址切换到已经准备好的新缓冲区(
bufferB)。 - 这样,输出的波形在缓冲区边界处实现了频率的无缝切换。由于切换发生在DMA传输间隙,且整个缓冲区是一个完整的周期,因此不会产生波形断裂或毛刺。
4.2 TIM2中断服务函数:扫频逻辑
在stm32f1xx_it.c中找到TIM2_IRQHandler,或者使用HAL库的回调函数HAL_TIM_PeriodElapsedCallback。
// 在main.c中定义全局变量 float startFreq = 1000.0; // 起始频率 1kHz float stopFreq = 10000.0; // 停止频率 10kHz float sweepTime = 10.0; // 扫频时间 10秒 float currentFreq = startFreq; uint32_t stepCount = 0; uint32_t totalSteps = sweepTime * 100; // 步进率100Hz,扫10秒,共1000步 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2) { // 1. 计算当前步的目标频率 (线性扫频) float targetFreq; if(stepCount < totalSteps) { targetFreq = startFreq + (stopFreq - startFreq) * stepCount / totalSteps; stepCount++; } else { targetFreq = stopFreq; // 扫频结束,保持最终频率 // 也可以在这里重置stepCount,实现循环扫频 } // 2. 根据目标频率计算相位增量 phaseIncrement // F_out = (phaseIncrement * F_trigger) / (2^32 * TABLE_SIZE) // 所以 phaseIncrement = F_out * (2^32 * TABLE_SIZE) / F_trigger // 注意:这里用浮点计算,实际应用可考虑定点数优化 double freqRatio = (double)targetFreq / (double)SAMPLE_RATE; // SAMPLE_RATE = F_trigger phaseIncrement = (uint32_t)(freqRatio * 4294967296.0); // 2^32 = 4294967296 // 3. 使用新的phaseIncrement,生成一个完整周期的正弦波数据到“准备缓冲区” // 这是一个计算密集型任务,放在中断里做要非常小心! // 更好的做法是:在中断里只设置一个标志位,在主循环或更低优先级任务里完成计算和缓冲区填充。 sweepUpdateFlag = 1; targetFrequencyForUpdate = targetFreq; // 传递目标频率 } }重要提示:在定时器中断里进行大量的浮点运算和数组填充是非常危险的行为,会严重阻塞系统,可能导致DMA传输断流、波形失真。正确的做法是中断只做最轻量的工作(更新标志、计算关键参数),把耗时的波形计算和填充放到主循环或一个专门的任务中。对于STM32F1,如果没有RTOS,可以在主循环中不断检查
sweepUpdateFlag,为真则进行计算。
4.3 主循环中的波形计算与缓冲区切换
// 定义双缓冲区 uint16_t dacBufferA[SINE_TABLE_SIZE]; uint16_t dacBufferB[SINE_TABLE_SIZE]; uint16_t *activeBuffer = dacBufferA; // DMA当前正在传输的缓冲区 uint16_t *nextBuffer = dacBufferB; // 正在准备的下一个缓冲区 int bufferReadyFlag = 0; // 下一个缓冲区已准备就绪标志 int main(void) { // ... 初始化代码(HAL_Init, SystemClock_Config, 外设初始化) // 填充初始缓冲区 dacBufferA generateSineWave(dacBufferA, SINE_TABLE_SIZE, startFreq); // 启动DMA传输,指向dacBufferA HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)dacBufferA, SINE_TABLE_SIZE, DAC_ALIGN_12B_R); // 启动定时器 HAL_TIM_Base_Start(&htim6); HAL_TIM_Base_Start_IT(&htim2); while (1) { // 检查是否需要更新波形(由TIM2中断置位) if(sweepUpdateFlag) { sweepUpdateFlag = 0; // 根据最新的targetFrequencyForUpdate,生成一个完整周期的正弦波到nextBuffer generateSineWave(nextBuffer, SINE_TABLE_SIZE, targetFrequencyForUpdate); bufferReadyFlag = 1; // 标记下一个缓冲区已就绪 } // 其他任务... // 缓冲区切换通常放在DMA传输完成中断中,这里只是示意主循环逻辑 } } // DMA传输完成中断回调函数(半传输或全传输) void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) { // 全传输完成(即一个缓冲区传输完了) if(bufferReadyFlag) { // 切换DMA目标到下一个缓冲区 // 注意:HAL库可能没有直接提供切换函数,可能需要停止DMA,更改地址,再启动。 // 更安全的方法是使用DMA双缓冲区模式(如果支持),硬件自动切换。 bufferReadyFlag = 0; // 交换缓冲区指针 uint16_t *temp = activeBuffer; activeBuffer = nextBuffer; nextBuffer = temp; // 重新配置DMA目标地址(这里需要调用HAL库函数或直接操作寄存器) // __HAL_DMA_SET_COUNTER(&hdma_dac_ch1, SINE_TABLE_SIZE); // hdma_dac_ch1.Instance->CMAR = (uint32_t)activeBuffer; // 具体操作需参考HAL库手册和芯片参考手册。 } }generateSineWave函数负责根据给定频率,计算一个完整周期的正弦波数据。这里的关键是,对于不同的频率,我们采样的是同一个数学上的正弦函数,只是采样点之间的“相位步进”不同。我们可以利用前面计算的phaseIncrement和相位累加器来高效生成这个波形表。
void generateSineWave(uint16_t *buffer, uint32_t size, float freq) { uint32_t phaseInc = calculatePhaseIncrement(freq); // 计算该频率对应的相位增量 uint32_t phaseAcc = 0; for(uint32_t i = 0; i < size; i++) { phaseAcc += phaseInc; // 相位累加 uint32_t index = (phaseAcc >> 23); // 取高9位作为表索引(假设原始正弦表是512点标准表) // 这里有个问题:我们没有一个“标准表”。我们需要直接计算正弦值。 // 因此,更直接的方法是使用sin函数计算每个点的值。 } }实际上,对于性能有限的MCU,在每次频率更新时都用sin函数计算512个点可能太慢。一个优化技巧是:我们仍然预存一个标准的512点正弦波表(sineTableStandard)。当需要生成频率为f的波形时,我们根据phaseIncrement在这个标准表中进行“重采样”。这相当于一个查表加插值的过程,比直接计算sin快得多,但会引入一些失真。对于音频应用,如果SINE_TABLE_SIZE足够大(比如1024),线性插值的精度是可以接受的。
void generateSineWaveByResampling(uint16_t *buffer, uint32_t size, uint32_t phaseInc) { uint32_t phaseAcc = 0; uint32_t standardTableSize = 512; // 标准表大小 uint16_t *stdTable = sineTableStandard; // 预计算的标准正弦表 for(uint32_t i = 0; i < size; i++) { phaseAcc += phaseInc; // 将32位的相位累加器映射到标准表的索引(取高几位) uint32_t indexHigh = phaseAcc >> (32 - 10); // 假设我们需要10位索引精度(1024点) indexHigh = indexHigh & (standardTableSize - 1); // 取模,确保在表范围内 // 线性插值(可选,提高精度) uint32_t indexLow = indexHigh + 1; if(indexLow >= standardTableSize) indexLow = 0; float frac = (phaseAcc >> (32 - 10 - 8)) & 0xFF; // 获取低8位作为小数部分(假设8位插值) float fracRatio = frac / 256.0f; float value = stdTable[indexHigh] + (stdTable[indexLow] - stdTable[indexHigh]) * fracRatio; buffer[i] = (uint16_t)value; } }5. 调试、优化与常见问题
5.1 调试手段
- 用示波器看波形:这是最直接的。连接DAC输出引脚(PA4)到示波器,你应该能看到一个干净的正弦波。改变扫频参数,观察频率是否平滑变化。检查波形底部和顶部是否有削顶(说明DAC值超出0-4095范围),是否有明显的台阶(正弦表点数太少或DAC更新率太低)。
- 用逻辑分析仪或调试器看时序:可以监控TIM2和TIM6的中断是否准时发生,DMA传输是否连续。如果波形有毛刺或中断,很可能是DMA传输被高优先级任务打断,或者缓冲区切换不及时。
- 串口打印调试信息:在TIM2中断里,通过串口打印当前计算出的
targetFreq和phaseIncrement,确认扫频逻辑是否正确。
5.2 性能优化与注意事项
- 中断服务程序(ISR)要短:这是嵌入式开发的铁律。务必把耗时的操作(如浮点计算、填充数组)移出中断。像计算
phaseIncrement这种简单的浮点乘除,如果速度成为瓶颈,可以预先计算好查表,或者使用定点数运算。 - DMA双缓冲区 vs 手动切换:STM32F1的DMA可能不支持自动的双缓冲区模式。手动切换缓冲区时,一定要确保在DMA传输完整个缓冲区的空隙进行切换。使用**半传输完成(HT)和传输完成(TC)**中断可以很好地管理两个缓冲区,实现“乒乓操作”,确保波形连续。
- 时钟精度:系统的时钟精度决定了输出频率的精度。如果使用内部RC振荡器(HSI),精度较差(±1%)。对于要求高的应用,必须使用外部晶振(HSE)。
- DAC输出缓冲与滤波:DAC输出引脚直接驱动能力有限,且输出可能有高频毛刺(来自数字开关噪声)。建议在DAC输出后接一个**电压跟随器(运算放大器)**进行缓冲,并可根据需要添加一个简单的RC低通滤波器,滤除高于目标频段的噪声。
- 动态范围与偏置:我们生成的正弦波是叠加在1.65V(2048)直流偏置上的。如果你的后级电路是交流耦合(有隔直电容),这个偏置会被滤掉,得到正负摆动的正弦波。如果需要真正的双极性输出(如±1.65V),则需要使用双电源运放电路将DAC的单极性输出转换为双极性。
5.3 常见问题与排查
- 没有波形输出:
- 检查DAC和TIM6的时钟是否使能。
- 检查DAC输出引脚(PA4)的配置是否正确(模拟模式)。
- 用调试器检查
phaseAccumulator和phaseIncrement变量是否在变化,正弦波表数据是否正常。 - 检查DMA是否成功启动,传输计数器(CNDTR)是否在减少。
- 波形频率不对:
- 检查
F_trigger(TIM6的更新频率)计算是否正确。 - 检查
phaseIncrement的计算公式,特别是2^N的值是否与相位累加器位数匹配。 - 检查正弦波表大小
SINE_TABLE_SIZE的定义是否与实际数组大小一致。
- 检查
- 波形有杂音或断点:
- 最可能的原因是缓冲区切换不及时,导致DMA读到了错误的数据或同一数据被重复读取。仔细检查DMA中断和缓冲区切换的代码逻辑,确保没有竞态条件。
- 可能是中断被其他更高优先级的中断长时间阻塞。优化中断优先级,确保DMA和定时器中断能得到及时响应。
- 检查堆栈空间是否足够,中断中的局部变量是否过多导致栈溢出。
- 扫频速度不线性或卡顿:
- 检查TIM2的中断周期是否稳定。如果系统中有其他中断或任务负载过重,可能导致TIM2中断被延迟。
- 确认
generateSineWave函数执行时间是否过长。如果它执行时间超过10ms(TIM2中断周期),就会错过下一次频率更新。考虑优化该函数,或者降低扫频步进率(即增大TIM2的周期)。
6. 进阶思路:从音频到射频
上面实现的是一个音频范围的扫频源。如果你想挑战更高频率或者更复杂的波形,可以考虑以下方向:
- 使用高性能STM32:比如STM32F4/F7/H7系列,主频更高,带FPU(浮点运算单元),甚至有的带DSP指令集,可以更快地计算波形。
- 使用DDS芯片:对于高达几十MHz的射频扫频,STM32的DAC和定时器就力不从心了。此时可以外接一片DDS芯片,如AD9833(低成本,数MHz)、AD9850(几十MHz)、AD9959(四通道,数百MHz)。STM32只需要通过SPI或并行接口向DDS芯片发送频率控制字,剩下的高精度波形生成全部由DDS芯片完成,性能有质的飞跃。
- 实现任意波形扫频:我们的正弦波表可以替换成任意形状的波形表(方波、三角波、自定义波形)。扫频时,动态更新这个波形表即可。但要注意,波形变化过快可能会引入高频分量。
- 加入幅度控制:在输出波形的同时,用另一个DAC通道或PWM控制一个数字电位器或VCA(压控放大器),实现扫频过程中的幅度调制(AM)。
- 与上位机联动:通过串口、USB或网络接收来自电脑的扫频参数(起止频率、时间、波形类型),实现程控信号发生器。
这个项目虽然基础,但它串联了STM32的定时器、DAC、DMA、中断这几个最核心的外设,对理解嵌入式系统中的实时信号处理非常有帮助。调试过程中遇到的每一个问题,都会让你对“时间”、“中断”、“数据流”这些概念有更深的理解。