资讯动态

STM32波形发生器实战:定时器+DAC+DMA实现可编程信号源

发布时间:2026/9/3 2:36:04 来源:尧图企业网站定制
在实际嵌入式开发项目中生成特定波形是信号处理、设备测试和教学演示中的常见需求。使用通用MCU配合DAC数模转换器或PWM脉宽调制来构建一个可编程波形发生器是理解MCU定时器、中断、DMA以及信号合成原理的绝佳实践。STM32系列微控制器因其丰富的外设和成熟的生态成为实现此类项目的热门选择。本文面向有一定STM32和C语言基础的开发者旨在通过一个完整的“基于STM32的波形发生器”项目带你从原理分析、环境搭建、代码实现到调试优化构建一个能够产生正弦波、方波、三角波和锯齿波的可调频、调幅信号源。你将了解到如何利用STM32的定时器触发DAC配合DMA实现后台、无CPU干预的波形数据自动输出从而生成稳定、高精度的模拟信号。我们不仅会完成核心功能的代码还会深入探讨关键参数如采样率、波形点数的计算与选择并针对实际开发中可能遇到的输出失真、频率不准、噪声过大等问题提供具体的排查思路和解决方案。1. 理解波形发生器的核心原理与STM32实现方案在开始写代码之前必须清楚我们要用STM32做什么以及为什么选择特定的外设组合。一个数字波形发生器的本质是将一系列预先计算好的、代表目标波形瞬时电压值的数字序列按固定的时间间隔通过DAC转换为连续的模拟电压输出。1.1 数字合成波形的基本概念对于正弦波、三角波等周期信号我们无法输出真正的连续曲线。取而代之的是“采样点”方法在一个信号周期内均匀地取N个点计算出每个点对应的电压值数字量然后将这个包含N个点的数据表循环输出给DAC。输出的频率Fout由两个因素决定数据表中一个完整周期的点数N以及每个点输出的时间间隔Ts即采样周期。关系为Fout 1 / (N * Ts)。Ts的倒数就是DAC的更新速率即采样率Fs。例如要产生一个1kHz的正弦波假设我们一个周期取100个点N100那么DAC的更新速率必须达到Fs Fout * N 1kHz * 100 100kHz即每10微秒必须更新一次DAC数据。1.2 STM32外设选型为何是定时器TIMDMADAC为了实现精确、稳定的Ts并解放CPUSTM32提供了完美的外设协作链定时器TIM作为整个系统的“心跳”。我们可以将其配置为以精确的频率例如上例的100kHz产生更新事件Update Event。这个事件将作为触发源。DAC数模转换器负责将数字量转换为模拟电压。STM32的DAC支持外部触发转换我们可以将其触发源设置为上述定时器的更新事件。DMA直接存储器访问负责在定时器每次触发DAC时自动将内存中下一个波形数据点搬运到DAC的数据寄存器中整个过程无需CPU参与。这种“定时器触发 - DMA搬运 - DAC输出”的流水线保证了输出时序的高度精确和稳定性CPU仅在需要改变波形或参数时才介入极大地提高了系统效率。1.3 关键参数设计考量波形点数N点数越多波形越平滑但占用的内存也越大且在固定采样率下能输出的最高频率越低因为Fmax Fs / N。需要在精度和性能间权衡通常64、100、128、256点是常见选择。DAC分辨率STM32的DAC通常是12位即输出数字量范围为0-4095对应电压0-Vref参考电压。这决定了输出电压的精度。定时器频率与采样率FsFs必须至少是目标输出频率Fout的2倍奈奎斯特采样定理实际中为了波形质量需要N倍。Fs由定时器的时钟和分频系数、重载值决定。Fs TIM_Clock / ((PSC 1) * (ARR 1))其中PSC为预分频器ARR为自动重载值。2. 开发环境准备与工程配置我们将使用STM32CubeMX进行图形化初始配置然后使用Keil MDK-ARM进行代码编写和调试。这是一种高效且主流的工作流。2.1 硬件与软件清单项目说明备注MCU型号STM32F103C8T6 (核心板)俗称“BluePill”性价比高具备基本DAC和DMA。其他如F4、H7系列原理类似。开发环境Keil uVision 5 (MDK-ARM)或 STM32CubeIDE本文以Keil为例。配置工具STM32CubeMX用于初始化时钟、外设、生成工程框架。调试工具ST-Link V2用于程序下载和调试。示波器数字示波器用于观察最终生成的波形验证结果。2.2 使用STM32CubeMX创建基础工程选择MCU打开CubeMX选择STM32F103C8Tx。配置时钟在RCC中将HSE设置为Crystal/Ceramic Resonator。转到Clock Configuration标签页将系统时钟SYSCLK配置为72MHz这是F103的常用最高频率。通常路径为HSE8MHz - PLL输入 - PLL倍频x9 - 选择PLL作为系统时钟源。配置DAC在Pinout Configuration标签页的左侧找到Analog-DAC。启用OUT1 Configuration对应PA4引脚或OUT2PA5。在Parameter Settings中Output Buffer使能Enable。这可以提供驱动能力但会使输出电压范围约为0~Vdda。如果要求精确的0-Vref需禁用但驱动能力弱。Trigger选择Timer 6 Trigger Out event。这意味着DAC转换将由TIM6的更新事件触发。配置定时器TIM6找到Timers-TIM6。将Clock Source设为Internal Clock。在Parameter Settings中我们的目标是产生一个100kHz的更新事件去触发DAC。假设TIM6的时钟APB1 Timer Clocks为72MHz。计算PSC和ARRUpdate Rate 72MHz / ((PSC1)*(ARR1)) 100kHz。令PSC 71则PSC172计数器时钟变为1MHz。令ARR 9则ARR110更新频率为1MHz / 10 100kHz。因此设置Prescaler (PSC) 71Counter Period (ARR) 9。在Trigger Output (TRGO) Parameters中将TRGO Selection设置为Update Event。这样每次定时器更新时都会产生一个触发信号输出到DAC。配置DMA在DAC1的配置页切换到DMA Settings标签页。点击Add选择DAC1Stream通常会自动分配如DMA1_Channel3。设置参数Direction:Memory To PeripheralPriority:HighMode:Circular(循环模式使波形能连续输出)Increment Address:Memory(内存地址自增)Data Width:Word(DAC数据寄存器是32位低12位有效但通常用半字/字)生成工程转到Project Manager标签页设置项目名称、路径选择Toolchain / IDE为MDK-ARM V5。在Code Generator中选择Generate peripheral initialization as a pair of .c/.h files per peripheral这样代码更清晰。点击GENERATE CODE。2.3 工程结构与关键文件生成后的Keil工程主要包含以下关键文件Core/Src/main.c: 主程序入口while(1)主循环在这里。Core/Src/stm32f1xx_it.c: 中断服务函数文件。虽然我们用了DMA但DMA传输完成中断可能用于更高级的控制本例暂不需要。Core/Inc/main.h: 主头文件。Core/Src/dac.c/Core/Inc/dac.h: DAC的初始化代码。Core/Src/tim.c/Core/Inc/tim.h: 定时器的初始化代码。Core/Src/dma.c/Core/Inc/dma.h: DMA的初始化代码。3. 波形数据生成与核心代码实现CubeMX已经为我们生成了外设的初始化代码MX_DAC_Init(),MX_TIM6_Init(),MX_DMA_Init()。我们需要做的是1) 生成波形数据表2) 启动DMA传输3) 启动定时器。3.1 计算并存储波形数据表在main.c文件开始处定义波形点数和数据数组。我们以生成正弦波为例。/* USER CODE BEGIN PV */ #define WAVE_POINTS 100 // 一个周期的点数 uint16_t sineWave[WAVE_POINTS]; // DAC数据数组12位数据放在低12位 /* USER CODE END PV */在main函数初始化部分/* USER CODE BEGIN 2 */区域编写一个函数来填充sineWave数组。/* USER CODE BEGIN 2 */ void GenerateSineWave(uint16_t *buffer, uint32_t points, uint16_t amplitude, uint16_t offset) { // amplitude: 幅值 (0- amplitude 对应DAC输出范围) // offset: 直流偏置 for(uint32_t i 0; i points; i) { // 计算正弦值范围[-1, 1] float radian (2 * 3.1415926f * i) / points; float sineValue sinf(radian); // 使用math.h的sinf函数 // 将[-1,1]映射到[0, amplitude]并加上偏置 // 先映射到[0, amplitude*2]然后除以2等效于 amplitude * (sineValue 1) / 2 // 为防止溢出使用浮点计算再转换 float scaledValue (sineValue 1.0f) * amplitude / 2.0f offset; // 限制到DAC范围 (0-4095 for 12-bit) if(scaledValue 4095) scaledValue 4095; if(scaledValue 0) scaledValue 0; buffer[i] (uint16_t)scaledValue; } } // 生成幅值为2047偏置为0的正弦波 GenerateSineWave(sineWave, WAVE_POINTS, 2047, 0); /* USER CODE END 2 */注意使用sinf函数需要包含math.h并在Keil的Target Options - Target中勾选Use MicroLIB或者添加其他数学库。类似地可以编写生成方波、三角波、锯齿波的函数。void GenerateSquareWave(uint16_t *buffer, uint32_t points, uint16_t low, uint16_t high) { uint32_t half points / 2; for(uint32_t i 0; i half; i) { buffer[i] high; } for(uint32_t i half; i points; i) { buffer[i] low; } } void GenerateTriangleWave(uint16_t *buffer, uint32_t points, uint16_t min, uint16_t max) { uint32_t half points / 2; float slope (float)(max - min) / half; for(uint32_t i 0; i half; i) { buffer[i] min (uint16_t)(slope * i); } for(uint32_t i half; i points; i) { buffer[i] max - (uint16_t)(slope * (i - half)); } }3.2 启动DMA传输与定时器在生成数据后需要启动DMA将数据源数组和目标DAC数据寄存器关联起来然后启动定时器。/* USER CODE BEGIN 2 */ // ... 生成波形数据 ... // 启动DAC通道1对应PA4使用DMA HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t*)sineWave, WAVE_POINTS, DAC_ALIGN_12B_R); // 启动定时器6它将开始产生触发信号 HAL_TIM_Base_Start(htim6); /* USER CODE END 2 */HAL_DAC_Start_DMA函数参数DAC句柄hdac通道DAC_CHANNEL_1数据源地址(uint32_t*)sineWave(需转换为uint32_t*指针)数据长度WAVE_POINTS数据对齐方式DAC_ALIGN_12B_R(12位数据右对齐这是我们常用的)HAL_TIM_Base_Start启动了定时器6的计数器它会按照预设的PSC和ARR自动重载并计数并在每次溢出更新事件时触发DAC。3.3 主循环与波形切换控制在主循环中我们可以通过按键或串口命令来切换波形。这里以按键切换为例假设连接了一个按键到PC13BluePill板载按键。/* USER CODE BEGIN WHILE */ while (1) { // 检测按键按下假设低电平有效并做了简单消抖 if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_RESET) { HAL_Delay(50); // 消抖延时 if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_RESET) { static uint8_t wave_index 0; wave_index (wave_index 1) % 4; // 循环切换4种波形 // 先停止DMA HAL_DAC_Stop_DMA(hdac, DAC_CHANNEL_1); // 根据索引生成新的波形数据 switch(wave_index) { case 0: GenerateSineWave(sineWave, WAVE_POINTS, 2047, 0); break; case 1: GenerateSquareWave(sineWave, WAVE_POINTS, 0, 4095); break; case 2: GenerateTriangleWave(sineWave, WAVE_POINTS, 0, 4095); break; case 3: // 锯齿波生成... break; } // 重新启动DMA传输 HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t*)sineWave, WAVE_POINTS, DAC_ALIGN_12B_R); // 等待按键释放 while(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_RESET); } } HAL_Delay(10); // 主循环延时 } /* USER CODE END WHILE */4. 编译、下载与波形验证4.1 编译与下载在Keil中点击RebuildF7编译工程确保无错误。连接ST-Link到核心板的SWDIO和SWCLK引脚并确保共地。在Keil中点击LoadF8或Download按钮将程序下载到STM32。下载完成后按一下核心板的复位键NRST程序开始运行。4.2 使用示波器验证将示波器探头的地线夹在STM32的GND引脚上探头尖端连接到PA4DAC_OUT1引脚。上电后你应该能在示波器上看到一个稳定的正弦波。测量频率应接近Fs / N 100kHz / 100 1kHz。使用示波器的频率测量功能。测量幅值幅值应接近(2047 / 4095) * Vref。如果Vref接VDDA3.3V则幅值约为(2047/4095)*3.3V ≈ 1.65V是峰峰值3.3V的一半。按下按键示波器上的波形应依次切换为正弦波、方波、三角波等。4.3 关键参数验证与计算验证采样率Fs可以通过测量DAC输出更新一个点的时间来间接验证。更准确的方法是使用定时器更新事件触发一个GPIO翻转然后用示波器测量该GPIO的方波频率它应该等于Fs100kHz。计算输出频率FoutFout Fs / N 100kHz / 100 1kHz。如果示波器测得的频率偏差较大请检查系统时钟SYSCLK是否准确配置为72MHz定时器时钟APB1是否正确在72MHz系统时钟下APB1时钟通常是36MHz但定时器时钟是APB1的2倍即72MHzCubeMX会处理好这个。定时器的PSC和ARR计算是否正确5. 常见问题、现象与排查路径在实际操作中你可能会遇到以下问题。这里提供系统的排查思路。5.1 无输出或输出为固定电平现象可能原因检查点与解决方案PA4引脚无电压变化1. DAC未使能或配置错误。2. 定时器未启动。3. DMA未配置或未启动。4. 引脚配置冲突。1. 检查MX_DAC_Init()是否被调用hdac实例是否有效。2. 在调试模式下查看TIM6-CR1寄存器的CEN位是否为1。3. 检查MX_DMA_Init()确认DMA通道已正确链接到DAC。单步调试确认HAL_DAC_Start_DMA函数被成功执行且未返回错误。4. 检查CubeMX中PA4是否被正确配置为DAC_OUT1且未被其他功能如SPI、USART复用。输出为固定高电平如3.3V或低电平0V1. 波形数据表所有值相同或为极值。2. DAC输出缓冲器Buffer使能但负载阻抗太小。1. 在调试模式下查看sineWave数组内容确认其值在0-4095间变化。2. DAC输出缓冲器提供电流能力有限约几十mA。如果直接驱动低阻抗负载电压会被拉低或无法变化。建议在DAC输出后接一个电压跟随器运算放大器进行缓冲。5.2 波形失真或频率不准现象可能原因检查点与解决方案正弦波呈阶梯状不光滑波形点数N太少。增加WAVE_POINTS例如从100增加到256。注意这会降低最大输出频率Fmax Fs / N并增加内存占用。波形频率远高于或低于预期1. 定时器时钟源或分频计算错误。2. 波形点数N设置错误。3. 公式Fout Fs / N理解有误。1. 使用示波器测量一个与Fs同步的GPIO翻转频率验证Fs是否正确。2. 确认代码中WAVE_POINTS宏的值与实际用于DMA传输的长度一致。3.Fs是DAC更新率Fout是最终模拟波形的频率。一个周期输出N个点所以Fout Fs / N。方波上升/下降沿有台阶或斜坡DAC的建立时间Settling Time或DMA响应时间不足导致输出速度跟不上。1. 降低Fs即降低定时器触发频率给DAC和DMA留出足够时间。2. 检查DAC时钟确保其足够快通常与APB1时钟相关。3. 对于极高频率方波考虑使用PWM滤波或专用高速DAC方案。波形底部或顶部被削平限幅波形数据值超出了DAC的有效范围0-4095。检查波形生成函数中的幅值amplitude和偏置offset计算确保offset ± amplitude/2的结果在0到4095之间。使用示波器观察时对应电压不应超过Vref通常为VDDA。5.3 噪声与稳定性问题现象可能原因检查点与解决方案波形上有明显的毛刺噪声1. 电源噪声。2. 数字电路对模拟部分的干扰。3. 参考电压Vref不稳定。1. 为模拟部分VDDA、VSSA使用独立的LC滤波电路并与数字电源VDD隔离。2. 在DAC输出引脚附近添加一个小的滤波电容如10nF-100nF到地可以滤除高频噪声但会影响输出速度。3. 确保Vref引脚连接了高质量、低噪声的基准电压源或已正确连接到稳定的VDDA。波形幅度随时间缓慢漂移DAC或运放的温漂。对于精度要求高的场合需要使用外部精密基准电压源并选择低温漂的运算放大器。6. 优化、扩展与生产环境考量基础功能实现后可以从以下几个方面进行优化和扩展使其更接近一个实用的信号发生器。6.1 性能与功能优化动态频率调整目前频率由Fs和N固定。要动态改变Fout有两种方法改变定时器频率Fs在运行时修改TIM6的ARR或PSC寄存器。注意在DMA循环传输过程中改变Fs可能导致波形相位不连续。建议在改变前停止DMA和定时器修改配置后再重启。改变波形点数N使用更精细的波形表更大的N但通过DMA传输时跳点例如每2个点传1个来实现倍频这种方法叫“DDS”直接数字频率合成的简化版实现复杂但频率切换快。代码示例修改ARR改变频率// 假设要将频率提高一倍Fs不变则需将N减半不对FoutFs/N。应改变Fs或N // 正确做法改变定时器ARR以改变Fs从而改变Fout。 // 目标新Fs 原Fs * 2 uint32_t new_arr __HAL_TIM_GET_AUTORELOAD(htim6) / 2; // ARR减半频率加倍 if(new_arr 1) new_arr 1; __HAL_TIM_SET_AUTORELOAD(htim6, new_arr); // 可能需要重新计算预分频PSC以保持计数器时钟在合理范围动态幅度与偏置调整不需要重新生成整个波形表。可以在DMA传输完成后中断中对传输的数据进行批量缩放和偏移运算或者使用双缓冲机制在后台计算好新波形数据后再切换DMA目标地址。使用高精度定时器和DAC对于更高频率或精度的需求可以考虑STM32F4/F7/H7系列它们具有更高主频、更快的DAC和更灵活的定时器。6.2 工程化与生产环境建议配置参数化将波形点数、默认频率、幅度、偏置等定义为易于修改的宏或存储在非易失性存储器如Flash中。错误处理为HAL_DAC_Start_DMA等函数添加返回值检查并设计相应的错误处理机制如LED指示、串口报错。抗干扰设计PCB布局时将模拟部分DAC输出、运放、基准源与数字部分MCU、时钟、开关电源分开用地平面隔离。VDDA和VSSA使用磁珠或0Ω电阻从VDD和VSS隔离并接去耦电容。DAC输出走线尽量短远离高频数字信号线。校准由于DAC存在增益误差和偏移误差对于精度要求高的应用需要在上电或生产时进行校准。可以测量DAC输出的几个已知电压点计算出校正系数并在软件中应用。通信接口添加串口USART或USBCDC通信功能接收来自上位机的指令设置波形、频率、幅度等实现程控信号发生器。通过这个项目你不仅实现了一个波形发生器更深入理解了STM32中定时器、DAC、DMA协同工作的精妙之处。这种“硬件自动处理”的思想是提高嵌入式系统性能和实时性的关键。下一步你可以尝试实现DDS算法来获得更精细的频率控制或者集成一个简单的LCD菜单界面进行交互这将使你的技能栈更加完整。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价