ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于STM32与DDS技术的可调频波形发生器设计与实现

基于STM32与DDS技术的可调频波形发生器设计与实现 1. 项目缘起与整体设计思路信号发生器这东西搞电子的朋友应该都不陌生。实验室里那台动辄几千上万的台式设备核心功能说白了就一句话输出我想要的波形频率、幅度、相位我说了算。但很多时候我们并不需要那么贵的设备比如做个简单的滤波器测试、给传感器打个激励信号、或者验证一下ADC采样电路一台能输出正弦波、方波、三角波频率可调的小型信号发生器就够用了。这个项目要做的就是基于STM32微控制器和DDS直接数字频率合成技术从零搭建一台可调频的波形生成器。核心思路很直接用STM32内部的定时器触发DAC数模转换器按照DDS算法实时计算出波形样点值通过DAC输出模拟电压再经过简单的低通滤波和运放缓冲最终得到干净的模拟波形。频率调节通过改变DDS的频率控制字来实现分辨率可以做到毫赫兹级别这是传统模拟振荡器很难达到的。为什么选STM32加DDS这个组合先说STM32。它内置的DAC外设比如STM32F4系列、F7系列、H7系列都有可以直接输出模拟电压配合DMA直接存储器访问搬运数据CPU几乎不用干预波形输出非常稳定。而且STM32的定时器资源丰富可以精确控制DAC的更新速率这对DDS来说至关重要。再说DDS它的核心优势是频率切换速度快、频率分辨率高、相位连续而且实现起来主要是查表和累加运算对STM32来说负担很轻。整个系统的信号链是这样的STM32定时器产生固定频率的触发信号每次触发时DMA把DDS算法算好的样点值送到DAC数据寄存器DAC输出对应的模拟电压经过一个截止频率可调的低通滤波器滤除高频杂散再通过运放缓冲输出。用户通过按键或者串口命令设置目标频率和波形类型STM32实时更新DDS频率控制字和波形查找表。这个项目适合谁如果你正在学STM32想找一个能综合用到定时器、DAC、DMA、中断、查表算法的实战项目这个非常合适。如果你在做毕业设计需要一台成本可控的信号发生器作为测试工具这个方案也能直接落地。甚至如果你只是想搞明白DDS到底是怎么回事跟着做一遍比看十篇理论文章都管用。2. DDS核心原理与STM32上的实现细节2.1 DDS到底是怎么合成波形的DDS的全称是Direct Digital Synthesis直接数字频率合成。它的核心结构就三样东西相位累加器、波形查找表、DAC。工作流程可以用一个很直观的类比来理解。想象你有一个圆盘圆盘边缘刻满了波形的一个完整周期比如正弦波从0度到360度的所有值。圆盘上有一个指针指针每走一步就指向圆盘上的一个位置DAC就读出那个位置对应的电压值。指针走的快慢决定了输出波形的频率指针每步跨过的角度大小就是频率控制字。具体到数学上相位累加器是一个N位的寄存器通常N取32位每个时钟周期它累加一次频率控制字Frequency Tuning WordFTW。累加器的溢出就代表相位走完了一个完整周期。输出频率的计算公式是f_out (FTW × f_clk) / 2^N其中f_clk是DAC的更新速率也就是定时器的触发频率。举个例子如果f_clk是100kHzN是32位想要输出1kHz的正弦波那么FTW (1kHz × 2^32) / 100kHz 42949672.96取整就是42949673。这个频率分辨率有多高呢当FTW等于1时输出频率是100kHz / 2^32 ≈ 0.0000233Hz也就是23微赫兹。实际使用中当然不需要这么高的分辨率但这个数字说明了DDS在频率精度上的巨大优势。相位累加器的高位比如高12位或高8位用来索引波形查找表。为什么只用高位因为查找表的深度有限比如1024个点那就用相位累加器的高10位来查表。低位被舍弃这引入的误差就是相位截断误差但通过合理选择查找表深度和DAC位数这个误差可以控制在可接受范围内。2.2 STM32上DDS的三种实现方案对比在STM32上实现DDS根据对CPU占用、输出频率上限、波形质量的不同要求有三种常见方案。方案一纯查表加DMA搬运。这是最直接的做法。预先在内存里建好一个完整周期的波形查找表比如1024个点的正弦表。定时器以固定频率触发DACDMA每次把查找表的下一个值搬到DAC数据寄存器。输出频率通过改变定时器的触发频率来调节。这个方案的优点是实现简单CPU占用极低DMA搬完一轮自动重装。缺点是频率分辨率受限于定时器的分频精度而且改变频率时需要重新计算定时器参数频率切换不够平滑。另外如果查找表点数固定输出频率越高每个周期输出的点数越少波形失真越严重。方案二实时计算加DMA双缓冲。这个方案不依赖预存的查找表而是在DMA传输完成中断里实时计算下一批样点值。STM32H7系列有DMAMUXDMA请求复用器和双缓冲模式可以在一个缓冲区正在被DMA搬运到DAC的同时CPU计算下一个缓冲区的数据。这样频率切换非常平滑只需要改变频率控制字相位累加器继续累加即可输出波形相位连续。缺点是CPU需要参与计算如果样点计算复杂比如要加调制CPU负担会明显增加。STM32H7的主频高达480MHz算力充足这个方案非常合适。方案三定时器PWM加RC滤波。这不是严格意义上的DDS但有时候会被拿来对比。用定时器产生PWM波改变占空比来模拟模拟电压再通过RC低通滤波得到平均电压。这个方案成本最低但输出频率极低通常几十赫兹波形质量差只适合对波形要求不高的场合。做信号发生器的话这个方案基本可以排除。综合来看对于“可调频波形生成器”这个目标方案二在STM32H7或者F4系列上是最优解。如果手头只有STM32F103这类没有DAC的型号那就只能外挂DAC芯片比如DAC0832或者更现代的MCP4725但那就不是“STM32内置DAC”的玩法了。我这里以STM32F407为例它内置两个12位DAC通道配合DMA和定时器足够输出到几十kHz的干净波形。2.3 波形查找表的生成与优化查找表的质量直接决定输出波形的纯度。以正弦波为例最直接的方法是用数学公式生成for (int i 0; i TABLE_SIZE; i) { sine_table[i] (uint16_t)((sin(2 * PI * i / TABLE_SIZE) 1.0) * 2047.5); }这里TABLE_SIZE取1024DAC是12位所以输出范围是0到4095中心点2048对应零电平。加1.0再乘2047.5是为了把正弦波的-1到1映射到0到4095。但这样生成的表有个问题DAC输出的是单极性电压0到3.3V正弦波的中心点在1.65V。如果你需要双极性输出比如-1V到1V那就要在DAC输出后面加一个运放电路做电平偏移和放大。这个后面讲模拟前端的时候再说。查找表的点数选择是个权衡。点数越多波形越细腻但占用的内存也越大而且DMA搬运一轮的时间越长。对于12位DAC理论上1024个点已经足够因为DAC本身的量化误差已经限制了精度。但如果你要做频率很高的输出比如100kHz而DAC更新率只有1MHz那每个周期只有10个点波形就会严重失真。这时候要么提高DAC更新率要么减少每周期点数但接受失真。实际项目中我通常把查找表设为512或1024点DAC更新率设在200kHz到1MHz之间输出频率在1Hz到20kHz范围内都能得到不错的效果。方波和三角波的生成更简单。方波就是前半段输出最大值后半段输出最小值。三角波是线性递增再线性递减。但要注意方波如果直接用DAC输出边沿会受限于DAC的建立时间和运放的压摆率不可能像数字信号那样陡峭。这是模拟输出的固有特性不是代码能解决的。注意查找表建议用const修饰并放在Flash里节省RAM。STM32F407有1MB Flash放几个1024点的16位表绰绰有余。如果放在RAM里DMA访问更快但会占用宝贵的RAM空间。对于H7系列还可以把表放在DTCM RAM里DMA访问零等待。3. 硬件搭建与模拟前端设计3.1 最小系统与DAC输出引脚STM32F407的最小系统包括主控芯片、8MHz晶振HSE、32.768kHz晶振LSE可选、复位电路、BOOT引脚配置、SWD调试接口、3.3V稳压电路。这些是标准配置网上教程很多不再赘述。重点说DAC相关的部分。STM32F407的DAC输出引脚是PA4DAC_OUT1和PA5DAC_OUT2。这两个引脚是专用的模拟输出不能复用为其他功能。使用时要确保GPIO模式配置为模拟模式GPIO_Mode_AN否则DAC输出会不正常。这一点很多新手会忽略配置成推挽输出结果DAC没波形查半天查不出来。DAC的参考电压来自VREF引脚。在100脚以上的封装里VREF是独立引脚可以接一个干净的3.3V参考源。在64脚以下的封装里VREF内部连接到VDDA这时候VDDA的噪声会直接影响DAC输出精度。如果对波形纯度要求高建议用100脚以上的封装VREF外接一个低噪声LDO比如ADR4525或者REF5030。DAC的输出阻抗比较高数据手册里标称输出阻抗在15kΩ左右而且输出电流能力很弱。所以DAC输出不能直接驱动负载必须经过运放缓冲。这是模拟前端设计的第一步。3.2 低通滤波器与运放缓冲电路DAC输出的是阶梯波每个采样点保持一个电压值直到下一个采样点到来。这个阶梯波里包含了大量的高频谐波频率是DAC更新率的整数倍。要得到干净的正弦波必须用低通滤波器把这些高频成分滤掉。滤波器的截止频率怎么定假设DAC更新率是200kHz输出正弦波频率最高20kHz。那么截止频率应该设在20kHz以上、200kHz以下。通常取输出最高频率的2到3倍比如50kHz。这样20kHz以内的信号能通过200kHz的采样频率及其谐波被大幅衰减。滤波器类型选巴特沃斯或者切比雪夫。巴特沃斯通带平坦相位线性度好适合信号发生器。切比雪夫过渡带更陡但通带有纹波。对于这个项目二阶或四阶巴特沃斯低通滤波器就够用了。二阶的话用一个运放加几个电阻电容就能实现截止频率计算公式是f_c 1 / (2π√(R1R2C1C2))具体参数可以用在线滤波器设计工具算或者直接查巴特沃斯滤波器归一化表。我一般用TI的FilterPro或者Analog Devices的在线工具输入截止频率和增益直接出电路参数。运放选型方面NE5532、TL072、OPA2134这些都可以。关键是压摆率要够。压摆率Slew Rate决定了运放输出能多快变化。对于输出20kHz、幅度3Vpp的正弦波最大变化率是2π×20kHz×1.5V ≈ 188V/ms也就是0.188V/μs。NE5532的压摆率是9V/μs绰绰有余。但如果输出频率更高或者幅度更大就要选压摆率更高的运放比如OPA2134的20V/μs。运放供电用±5V或者±12V这样输出可以做到双极性。DAC输出是0到3.3V单极性经过一个隔直电容去掉直流分量再经过运放放大和偏移就能得到双极性输出。具体电路是一个反相放大器加偏置网络或者用差分放大器结构。这里不展开画电路图但思路是DAC输出先经过一个RC低通然后进入运放的同相输入端运放的反相输入端通过电阻网络引入一个负偏置输出就是围绕零点的双极性波形。实操心得PCB布局时DAC输出到运放的走线要尽量短远离数字信号线尤其是时钟线和PWM线。模拟地和数字地要分开最后在一点连接。DAC的VREF引脚旁边要放一个10μF钽电容并联一个100nF陶瓷电容滤除参考电压上的噪声。这些细节不做输出波形上会看到明显的毛刺和噪声。3.3 按键与显示交互用户交互部分最简单的方案是三个按键一个切换波形类型一个增加频率一个减少频率。频率步进可以设为1Hz、10Hz、100Hz、1kHz几档长按快速调节。显示用0.96寸OLED或者1602液晶显示当前波形类型和频率值。按键消抖用软件延时或者定时器扫描。我习惯用定时器每10ms扫描一次按键状态连续两次读到相同状态才确认这样消抖可靠且不阻塞主循环。OLED用I2C接口STM32的硬件I2C或者软件模拟都可以。软件模拟更灵活不受硬件I2C的bug影响STM32F103的硬件I2C确实有已知问题F4系列好很多。如果想做得更专业可以加一个旋转编码器代替加减按键调节频率更顺手。旋转编码器输出的是正交编码信号STM32的定时器有编码器模式可以直接硬件解码非常方便。不过这是锦上添花的功能核心的DDS波形生成才是重点。4. 软件架构与关键代码实现4.1 定时器触发DAC的配置流程整个软件的核心是定时器触发DAC加DMA搬运。以STM32F407为例用TIM6作为触发源因为它最简单只有基本定时功能没有PWM输出引脚不会干扰其他外设。配置步骤大致如下使能TIM6时钟、DAC时钟、DMA1时钟。配置TIM6预分频器和自动重装值决定触发频率。假设系统时钟84MHz要得到200kHz触发频率预分频器设为0自动重装值设为84MHz/200kHz - 1 419。配置DAC使能DAC通道1触发源选择TIM6 TRGO使能DMA请求。配置DMA源地址为波形查找表数组目标地址为DAC数据寄存器传输方向为内存到外设循环模式数据宽度16位。使能DMA流使能DAC使能TIM6。关键代码片段// TIM6配置 TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_InitStruct.TIM_Prescaler 0; TIM_InitStruct.TIM_Period 419; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, TIM_InitStruct); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); // DAC配置 DAC_InitTypeDef DAC_InitStruct; DAC_InitStruct.DAC_Trigger DAC_Trigger_T6_TRGO; DAC_InitStruct.DAC_WaveGeneration DAC_WaveGeneration_None; DAC_InitStruct.DAC_OutputBuffer DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, DAC_InitStruct); // DMA配置 DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_Channel DMA_Channel_7; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)DAC-DHR12R1; DMA_InitStruct.DMA_Memory0BaseAddr (uint32_t)wave_table; DMA_InitStruct.DMA_DIR DMA_DIR_MemoryToPeripheral; DMA_InitStruct.DMA_BufferSize TABLE_SIZE; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode DMA_Mode_Circular; DMA_Init(DMA1_Stream5, DMA_InitStruct);这里有个细节DAC的数据寄存器是DHR12R112位右对齐。写入这个寄存器的值会在下一个触发到来时被搬到DAC输出寄存器。DMA搬运的就是这个DHR12R1。为什么不用DHR12L1因为右对齐更方便低12位就是数据高4位忽略。DMA的通道选择要注意STM32F407的DAC1对应DMA1的Stream5通道7。不同型号的映射关系不一样查参考手册的DMA请求映射表确认。选错了DMA通道DAC不会有输出而且不会报错这是很隐蔽的坑。4.2 DDS频率控制字的计算与更新频率控制字的计算公式前面给过了在代码里实现#define PHASE_ACC_BITS 32 #define TABLE_SIZE 1024 #define DAC_UPDATE_FREQ 200000.0f uint32_t calc_ftw(float freq) { return (uint32_t)((freq * (1ULL PHASE_ACC_BITS)) / DAC_UPDATE_FREQ); }注意这里用了1ULL因为2^32超出了32位整数的范围要用64位无符号整数。算出来的FTW是32位的但实际查表时只用高10位因为TABLE_SIZE是1024。所以查表索引是uint16_t phase_acc 0; uint16_t table_index phase_acc (PHASE_ACC_BITS - 10);每次DAC更新时相位累加器加上FTW然后取高10位查表。但这里有个问题如果DAC更新率是200kHz而输出频率是1kHz那么每个周期有200个采样点相位累加器每次增加FTW (1kHz × 2^32) / 200kHz ≈ 21474836。高10位的变化量是21474836 22 ≈ 5.12也就是说每次查表索引增加约5。这样1024个点的表大约每200次更新走完一轮对应1kHz输出。这个逻辑是对的。但如果输出频率很高比如50kHzFTW (50kHz × 2^32) / 200kHz ≈ 1073741824高10位变化量是1073741824 22 256。每次索引增加2561024个点的表4次就查完了每个周期只有4个采样点。这时候波形严重失真正弦波看起来像三角波。所以DAC更新率必须远高于输出频率一般至少10倍以上最好20倍以上。实际项目中我会把DAC更新率设到1MHz这样输出20kHz时每周期还有50个点波形质量可以接受。但1MHz的DAC更新率对STM32F407来说DMA搬运和DAC建立时间都是挑战。DAC的建立时间典型值是1μs1MHz更新率意味着每个样点只有1μs刚好卡在边缘。实测下来500kHz更新率比较稳妥输出10kHz以内波形很干净。4.3 波形切换与实时更新策略波形切换不能直接改DMA的源地址因为DMA正在搬运中改地址会导致数据错乱。正确的做法是在DMA传输完成中断里或者用一个标志位等当前缓冲区搬完后再切换查找表指针。对于循环模式的DMA没有传输完成中断因为一直在循环。这时候可以用DMA的半传输中断和传输完成中断来同步。半传输中断表示搬完了一半传输完成中断表示搬完了一轮。在这两个中断里可以安全地更新查找表指针或者频率控制字。但更简单的做法是用双缓冲。定义两个查找表数组DMA搬运一个的时候CPU更新另一个。STM32F407的DMA不支持真正的双缓冲H7的DMAMUX支持但可以用两个DMA流交替或者用DMA的循环模式加手动切换。我通常的做法是波形切换时先停止DMA和定时器更新查找表指针再重新启动。这样会有短暂的输出中断但对于手动切换波形的场景几十微秒的中断用户根本感觉不到。频率更新则不需要停止DMA。只需要改变频率控制字变量DDS算法在计算下一个样点时自然会用新的FTW。但要注意如果是在DMA传输完成中断里更新FTW要确保FTW的写入是原子的。32位变量在32位MCU上写入是原子的不需要额外保护。常见问题很多人反映DAC输出波形频率不对算出来是1kHz实际只有500Hz。检查一下定时器的触发频率是不是算错了。TIM6的触发频率是系统时钟除以预分频器1再除以自动重装值1。如果系统时钟是84MHz预分频器0自动重装值419触发频率是84MHz/420 200kHz。但如果系统时钟配置成了168MHzF407超频那触发频率就翻倍了。所以系统时钟配置一定要确认清楚。5. 调试过程中踩过的坑与排查技巧5.1 DAC无输出或输出恒定的排查这是最常见的问题。DAC配置好了代码也下载了但用示波器测PA4引脚要么是0V要么是3.3V要么是一个不变化的直流电平。排查思路按以下顺序来第一确认GPIO模式。PA4必须配置为模拟模式GPIO_Mode_AN。如果配置成了推挽输出DAC的输出会被GPIO输出驱动器覆盖你看到的是GPIO的输出电平不是DAC的。这个错误极其常见因为很多人复制GPIO初始化代码时忘了改模式。第二确认DAC使能了。DAC_Cmd(DAC_Channel_1, ENABLE)这行代码有没有调用DAC的使能位在DAC_CR寄存器的EN1位不使能的话DAC完全不工作。第三确认触发源配置正确。DAC_Trigger_T6_TRGO对应TIM6的TRGO输出。如果TIM6没有配置TRGO输出或者TRGO源选错了比如选成了OC1REF而不是UpdateDAC就不会被触发。用示波器测TIM6的TRGO引脚是测不到的因为TRGO是内部信号。可以临时把DAC触发改成软件触发DAC_Trigger_Software然后在主循环里手动写DAC数据寄存器看有没有输出。如果有说明DAC本身没问题问题在触发链路上。第四确认DMA配置正确。DMA的源地址、目标地址、数据宽度、传输方向都要对。特别是目标地址必须是(DAC-DHR12R1)不能是DAC-DHR12R1再取地址那就成指针的指针了。还有DMA的通道号F407的DAC1是DMA1 Stream5 Channel7写错了DMA请求不会触发。第五确认DAC输出缓冲使能了。DAC_OutputBuffer_Enable可以增强驱动能力但会引入一定的偏移误差。如果输出缓冲禁用DAC输出阻抗很高示波器探头一接上去电压就塌了。所以调试阶段建议使能输出缓冲。5.2 波形失真与噪声问题的解决波形出来了但正弦波看起来毛毛糙糙的或者顶部和底部有削平或者频率高了之后波形变成三角波。这些问题分别对应不同的原因。毛糙和噪声先看电源。DAC的参考电压VREF干不干净用示波器交流耦合测VREF引脚如果看到几十毫伏的纹波那输出波形肯定有噪声。解决办法是在VREF旁边加滤波电容或者用独立的低噪声参考源。再看地线。模拟地和数字地有没有分开如果DAC的AGND和数字GND混在一起数字开关噪声会串到模拟输出上。PCB上应该把DAC的AGND引脚单独走线到电源入口的地不要和数字地混。削平失真DAC输出范围是0到VREF如果波形幅度超过了这个范围顶部或底部就会被削平。比如正弦波中心点设在2048幅度设了2048那峰值就是4096超过了12位DAC的最大值4095顶部就削平了。解决办法是减小幅度或者把中心点调低。另外如果运放电路的增益太大输出超过了运放的供电轨也会削平。检查运放供电电压和增益设置。高频时波形变三角这是采样点不足导致的。前面说过DAC更新率至少要是输出频率的10倍以上。如果输出20kHzDAC更新率只有100kHz每周期5个点正弦波就退化成三角波了。解决办法是提高DAC更新率或者降低输出频率范围。STM32F407的DAC在500kHz更新率下工作还算稳定再高就要看PCB布局和DAC建立时间了。避坑技巧调试DAC输出时先把DMA关掉用软件触发方式手动写几个固定值到DAC数据寄存器用万用表测PA4电压。写0x000输出0V写0xFFF输出3.3V写0x800输出1.65V。这三个点测对了说明DAC基本工作正常。然后再开DMA和定时器看波形。这样分段排查比一上来就全开然后抓瞎要高效得多。5.3 频率精度与相位连续性的验证DDS的卖点之一就是频率精度高。但实际做出来用频率计测输出频率发现和设定值有偏差。这个偏差来自哪里首先是定时器触发频率的误差。STM32的定时器时钟来自系统时钟而系统时钟来自晶振。晶振本身的精度通常是±20ppm到±50ppm也就是说100kHz的频率可能有2Hz到5Hz的误差。这个误差是晶振固有的换TCXO温度补偿晶振可以改善但成本上去了。对于一般应用这个精度足够了。其次是FTW计算的舍入误差。FTW是整数计算时的浮点运算会引入舍入。但这个误差非常小因为FTW是32位的舍入误差相对于2^32可以忽略。真正影响频率精度的是DAC更新率的实际值与理论值的偏差。比如你算出来定时器自动重装值是419触发频率应该是200kHz但实际上系统时钟可能不是精确的84MHz而是83.98MHz那触发频率就是199.95kHz输出频率就偏了0.025%。这个偏差可以通过实测输出频率后反推修正FTW来补偿。相位连续性方面DDS在改变频率时相位累加器不清零所以输出波形的相位是连续的。这一点在扫频应用里非常重要。验证方法用示波器触发模式设为正常触发调节频率观察波形过零点有没有跳变。如果相位连续过零点位置应该平滑移动不会突然跳半个周期。6. 项目扩展与进阶玩法6.1 加入幅度调制与频率扫描基础版本做完了可以往上加功能。幅度调制AM的实现思路是在DDS计算样点值之后再乘以一个调制系数。调制系数来自另一个低频DDS或者查表。比如用TIM7触发一个低频中断在中断里更新调制系数主DDS的样点值乘以这个系数后写入DMA缓冲区。这样输出就是调幅波。频率扫描Sweep更简单。设定起始频率、终止频率和扫描时间在定时器中断里线性增加FTW输出频率就线性上升。扫描时间到了之后回到起始频率重新开始。这个功能在测试滤波器频率响应时非常有用配合一个峰值检测电路就能画出幅频特性曲线。6.2 用STM32H7的DMAMUX实现真正的双缓冲STM32H7系列的DMAMUXDMA请求复用器支持双缓冲模式可以在DMA搬运一个缓冲区的同时CPU安全地更新另一个缓冲区。具体配置是DMA设置为双缓冲模式有两个内存地址指针DMA搬完一个自动切换到另一个并产生传输完成中断。在中断里CPU更新刚才搬完的那个缓冲区。这个机制对DDS来说非常理想。CPU可以在中断里计算下一批样点值写入空闲缓冲区DMA无缝搬运当前缓冲区。输出波形完全连续没有中断间隙。STM32H7的主频480MHz算1024个点的正弦波只需要几微秒完全跟得上DMA的节奏。配置DMAMUX双缓冲的关键寄存器是DMA_SxCR的DBM位和CT位。DBM使能双缓冲CT控制当前目标缓冲区。DMA_SxNDTR寄存器在双缓冲模式下自动重装。具体代码比较长可以参考STM32H7的HAL库例程里面有DMA双缓冲的完整示例。6.3 上位机控制与波形任意编辑用串口或者USB CDC把信号发生器连到电脑上写一个简单的上位机可以任意编辑波形。上位机把波形数据比如一个周期的样点数组通过串口发给STM32STM32收到后更新查找表。这样就能输出任意波形比如心电波形、噪声、自定义调制波。串口协议可以很简单一个帧头比如0xAA 0x55一个长度字节然后是波形数据最后是校验和。STM32收到完整帧后把数据写入查找表数组然后切换DMA源地址。注意切换时要先停止DMA更新完再启动避免数据错乱。上位机用Python的PyQt或者C#的WinForm都能快速做出来。如果不想写上位机用串口助手手动发数据也行只是效率低一些。这个扩展把信号发生器从固定波形升级到了任意波形发生器AWG实用性大大提升。7. 关键参数速查与选型参考7.1 核心参数计算表参数符号计算公式示例值输出频率f_outFTW × f_clk / 2^321kHz频率控制字FTWf_out × 2^32 / f_clk21474836频率分辨率Δff_clk / 2^320.0000233HzDAC更新率f_clk系统时钟 / ((PSC1)(ARR1))200kHz每周期采样点数Nf_clk / f_out200查找表索引步进ΔidxFTW (32 - log2(TABLE_SIZE))5这个表放在手边调参数的时候直接查比每次推导快得多。特别是频率控制字和频率分辨率的计算用计算器按一遍确认无误再写进代码。7.2 不同STM32型号的适配要点型号DAC位数DMA双缓冲推荐更新率适用场景STM32F103无DAC不支持外挂DAC入门学习需外扩STM32F40712位不支持500kHz性价比高够用STM32F42912位不支持500kHz带LCD适合做界面STM32H74312位支持1MHz高性能双缓冲STM32G47412位支持1MHz新一代性价比好F103没有DAC这是硬伤。如果手头只有F103可以用PWM加RC滤波做低速信号或者外挂DAC08328位并行或MCP472512位I2C。但外挂DAC的更新率受接口速度限制MCP4725的I2C最快400kHz更新率上不去输出频率最多几百赫兹。所以做信号发生器还是建议用带DAC的型号。7.3 元器件选型清单元器件推荐型号关键参数备注主控STM32F407VET6168MHz, 512KB Flash100脚VREF独立运放NE553210MHz带宽, 9V/μs双运放性价比高运放OPA21348MHz带宽, 20V/μs音频级低失真参考源REF50303.0V, 3ppm/°C低噪声高精度显示屏SSD1306 OLED0.96寸, I2C显示波形和频率编码器EC1120脉冲/圈带按键调节方便运放选NE5532就够用了价格便宜性能对于20kHz以内的信号完全足够。如果追求更低失真可以上OPA2134或者LM4562。参考源REF5030有点贵如果预算有限直接用STM32的VDDA加滤波电容也能凑合只是精度和温漂差一些。8. 实测效果与性能边界8.1 输出波形质量实测我用STM32F407VET6的核心板搭了一套DAC更新率设500kHz查找表1024点输出1kHz正弦波。用示波器看波形非常干净THD总谐波失真用音频分析仪测大约在-60dB左右也就是0.1%的失真。这个指标对于一般测试应用完全够用。输出10kHz时THD上升到-45dB左右约0.5%主要是因为每周期只有50个采样点阶梯效应更明显。输出20kHz时THD约-35dB波形看起来已经有点三角化的趋势了。方波输出在1kHz时边沿大约2μs这是运放压摆率和DAC建立时间共同决定的。想要更陡的边沿需要更高压摆率的运放和更快的DAC。但方波本身谐波丰富经过低通滤波器后边沿会变缓这是不可避免的。如果要做数字方波输出应该直接从GPIO输出不走DAC。三角波和锯齿波的线性度很好因为DAC的积分非线性INL在12位下通常小于±1LSB肉眼看不出非线性。8.2 频率范围与分辨率边界实测最低输出频率可以到0.1Hz此时每周期有500万个采样点波形非常平滑。最高输出频率受限于DAC更新率和每周期最少采样点数。我设定每周期至少10个点那么最高输出频率是500kHz/10 50kHz。但50kHz时波形已经严重失真实际可用上限大约20kHz。频率分辨率方面理论上FTW1时输出频率是500kHz/2^32 ≈ 0.000116Hz也就是0.116毫赫兹。实际使用时频率步进设到0.1Hz就足够了再小没有意义因为晶振本身的精度都不止这个数。频率切换时间方面从写入新FTW到输出频率稳定只需要一个DAC更新周期也就是2μs。这个速度非常快适合做跳频或者扫频应用。8.3 与商用信号发生器的对比指标本项目入门商用信号发生器高端商用信号发生器频率范围0.1Hz-20kHz1μHz-10MHz1μHz-1GHz频率分辨率0.1Hz1μHz1μHzDAC位数12位14位16位THD1kHz-60dB-70dB-80dB任意波形支持串口下载支持支持成本约50元约500元约50000元这个对比不是要证明DIY的能替代商用设备而是说明在低频、一般精度要求的场景下DIY方案完全够用。做学生实验、传感器激励、音频测试这台小设备都能胜任。而且自己做的过程中学到的DDS原理、STM32外设配置、模拟电路设计这些经验是买设备买不来的。最后分享一个调试小技巧如果输出波形上有规律性的毛刺而且毛刺频率和DAC更新率一致那多半是DMA搬运和DAC更新之间的时序问题。可以尝试在DAC触发配置里加一点延迟或者把DMA的优先级调到最高。STM32F407的DMA1 Stream5优先级默认是低如果被其他DMA请求打断DAC数据更新就会延迟输出波形上就会出现台阶。把DMA优先级设为VeryHigh这个问题基本就解决了。
返回列表