☰
STM32波形发生器实战:定时器+DAC+DMA实现可编程信号源
2026/9/27 18:48:39 网站建设 项目流程

在实际嵌入式开发项目中,生成特定波形是信号处理、设备测试和教学演示中的常见需求。使用通用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个点(N=100),那么DAC的更新速率必须达到Fs = Fout * N = 1kHz * 100 = 100kHz,即每10微秒必须更新一次DAC数据。

1.2 STM32外设选型:为何是定时器+TIM+DMA+DAC?

为了实现精确、稳定的Ts,并解放CPU,STM32提供了完美的外设协作链:

  1. 定时器(TIM):作为整个系统的“心跳”。我们可以将其配置为以精确的频率(例如上例的100kHz)产生更新事件(Update Event)。这个事件将作为触发源。
  2. DAC(数模转换器):负责将数字量转换为模拟电压。STM32的DAC支持外部触发转换,我们可以将其触发源设置为上述定时器的更新事件。
  3. 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(参考电压)。这决定了输出电压的精度。
  • 定时器频率与采样率(Fs):Fs必须至少是目标输出频率(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创建基础工程

  1. 选择MCU:打开CubeMX,选择STM32F103C8Tx。
  2. 配置时钟:
    • 在RCC中,将HSE设置为Crystal/Ceramic Resonator。
    • 转到Clock Configuration标签页,将系统时钟(SYSCLK)配置为72MHz(这是F103的常用最高频率)。通常路径为:HSE(8MHz) -> PLL输入 -> PLL倍频(x9) -> 选择PLL作为系统时钟源。
  3. 配置DAC:
    • 在Pinout & Configuration标签页的左侧,找到Analog->DAC。
    • 启用OUT1 Configuration(对应PA4引脚)或OUT2(PA5)。
    • 在Parameter Settings中:
      • Output Buffer:使能(Enable)。这可以提供驱动能力,但会使输出电压范围约为0~Vdda。如果要求精确的0-Vref,需禁用(但驱动能力弱)。
      • Trigger:选择Timer 6 Trigger Out event。这意味着DAC转换将由TIM6的更新事件触发。
  4. 配置定时器TIM6:
    • 找到Timers->TIM6。
    • 将Clock Source设为Internal Clock。
    • 在Parameter Settings中,我们的目标是产生一个100kHz的更新事件去触发DAC。
      • 假设TIM6的时钟(APB1 Timer Clocks)为72MHz。
      • 计算PSC和ARR:Update Rate = 72MHz / ((PSC+1)*(ARR+1)) = 100kHz。
      • 令PSC = 71,则PSC+1=72,计数器时钟变为1MHz。
      • 令ARR = 9,则ARR+1=10,更新频率为1MHz / 10 = 100kHz。
      • 因此设置:Prescaler (PSC) = 71,Counter Period (ARR) = 9。
    • 在Trigger Output (TRGO) Parameters中,将TRGO Selection设置为Update Event。这样每次定时器更新时,都会产生一个触发信号输出到DAC。
  5. 配置DMA:
    • 在DAC1的配置页,切换到DMA Settings标签页。
    • 点击Add,选择DAC1,Stream通常会自动分配(如DMA1_Channel3)。
    • 设置参数:
      • Direction:Memory To Peripheral
      • Priority:High
      • Mode:Circular(循环模式,使波形能连续输出)
      • Increment Address:Memory(内存地址自增)
      • Data Width:Word(DAC数据寄存器是32位低12位有效,但通常用半字/字)
  6. 生成工程:
    • 转到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函数参数:
    1. DAC句柄&hdac
    2. 通道DAC_CHANNEL_1
    3. 数据源地址(uint32_t*)sineWave(需转换为uint32_t*指针)
    4. 数据长度WAVE_POINTS
    5. 数据对齐方式DAC_ALIGN_12B_R(12位数据右对齐,这是我们常用的)
  • HAL_TIM_Base_Start启动了定时器6的计数器,它会按照预设的PSC和ARR自动重载并计数,并在每次溢出(更新事件)时触发DAC。

3.3 主循环与波形切换控制

在主循环中,我们可以通过按键或串口命令来切换波形。这里以按键切换为例,假设连接了一个按键到PC13(BluePill板载按键)。

/* 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 编译与下载

  1. 在Keil中点击Rebuild(F7)编译工程,确保无错误。
  2. 连接ST-Link到核心板的SWDIO和SWCLK引脚,并确保共地。
  3. 在Keil中点击Load(F8)或Download按钮,将程序下载到STM32。
  4. 下载完成后,按一下核心板的复位键(NRST),程序开始运行。

4.2 使用示波器验证

  1. 将示波器探头的地线夹在STM32的GND引脚上,探头尖端连接到PA4(DAC_OUT1)引脚。
  2. 上电后,你应该能在示波器上看到一个稳定的正弦波。
    • 测量频率:应接近Fs / N = 100kHz / 100 = 1kHz。使用示波器的频率测量功能。
    • 测量幅值:幅值应接近(2047 / 4095) * Vref。如果Vref接VDDA(3.3V),则幅值约为(2047/4095)*3.3V ≈ 1.65V,是峰峰值3.3V的一半。
  3. 按下按键,示波器上的波形应依次切换为正弦波、方波、三角波等。

4.3 关键参数验证与计算

  • 验证采样率(Fs):可以通过测量DAC输出更新一个点的时间来间接验证。更准确的方法是使用定时器更新事件触发一个GPIO翻转,然后用示波器测量该GPIO的方波频率,它应该等于Fs(100kHz)。
  • 计算输出频率(Fout):Fout = Fs / N = 100kHz / 100 = 1kHz。如果示波器测得的频率偏差较大,请检查:
    1. 系统时钟(SYSCLK)是否准确配置为72MHz?
    2. 定时器时钟(APB1)是否正确?在72MHz系统时钟下,APB1时钟通常是36MHz,但定时器时钟是APB1的2倍(即72MHz),CubeMX会处理好这个。
    3. 定时器的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)或低电平(0V)1. 波形数据表所有值相同或为极值。
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 性能与功能优化

  1. 动态频率调整:

    • 目前频率由Fs和N固定。要动态改变Fout,有两种方法:
      • 改变定时器频率(Fs):在运行时修改TIM6的ARR或PSC寄存器。注意,在DMA循环传输过程中改变Fs可能导致波形相位不连续。建议在改变前停止DMA和定时器,修改配置后再重启。
      • 改变波形点数(N):使用更精细的波形表(更大的N),但通过DMA传输时跳点(例如每2个点传1个)来实现倍频,这种方法叫“DDS”(直接数字频率合成)的简化版,实现复杂但频率切换快。
    • 代码示例(修改ARR改变频率):
      // 假设要将频率提高一倍(Fs不变,则需将N减半?不对,Fout=Fs/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以保持计数器时钟在合理范围
  2. 动态幅度与偏置调整:

    • 不需要重新生成整个波形表。可以在DMA传输完成后中断中,对传输的数据进行批量缩放和偏移运算,或者使用双缓冲机制,在后台计算好新波形数据后再切换DMA目标地址。
  3. 使用高精度定时器和DAC:

    • 对于更高频率或精度的需求,可以考虑STM32F4/F7/H7系列,它们具有更高主频、更快的DAC和更灵活的定时器。

6.2 工程化与生产环境建议

  1. 配置参数化:将波形点数、默认频率、幅度、偏置等定义为易于修改的宏或存储在非易失性存储器(如Flash)中。
  2. 错误处理:为HAL_DAC_Start_DMA等函数添加返回值检查,并设计相应的错误处理机制(如LED指示、串口报错)。
  3. 抗干扰设计:
    • PCB布局时,将模拟部分(DAC输出、运放、基准源)与数字部分(MCU、时钟、开关电源)分开,用地平面隔离。
    • VDDA和VSSA使用磁珠或0Ω电阻从VDD和VSS隔离,并接去耦电容。
    • DAC输出走线尽量短,远离高频数字信号线。
  4. 校准:由于DAC存在增益误差和偏移误差,对于精度要求高的应用,需要在上电或生产时进行校准。可以测量DAC输出的几个已知电压点,计算出校正系数,并在软件中应用。
  5. 通信接口:添加串口(USART)或USB(CDC)通信功能,接收来自上位机的指令(设置波形、频率、幅度等),实现程控信号发生器。

通过这个项目,你不仅实现了一个波形发生器,更深入理解了STM32中定时器、DAC、DMA协同工作的精妙之处。这种“硬件自动处理”的思想是提高嵌入式系统性能和实时性的关键。下一步,你可以尝试实现DDS算法来获得更精细的频率控制,或者集成一个简单的LCD菜单界面进行交互,这将使你的技能栈更加完整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询