基于STM32的DAC+DMA波形发生器:从原理到实现的嵌入式信号生成方案
2026/9/3 5:38:37 网站建设 项目流程

如果你正在学习嵌入式开发,或者需要为某个项目生成特定频率和形状的测试信号,那么“自己动手做一个波形发生器”这个想法很可能已经在你脑海里盘旋过。市面上的专业信号发生器价格不菲,而简单的函数发生器模块功能又过于单一。有没有一种方案,既能让你深入理解信号产生的底层原理,又能得到一个功能灵活、成本可控的实用工具?

答案是肯定的,而STM32正是实现这个想法的绝佳平台。基于STM32的波形发生器,远不止是“让一个引脚输出高低电平”那么简单。它涉及到从核心定时器的精准时基控制,到数模转换器(DAC)的电压输出,再到波形算法的实时计算,是一套完整的嵌入式信号处理系统。很多人尝试后卡在波形失真、频率不准或控制不灵活上,其根本原因往往是对STM32的DAC、DMA和定时器协同工作的机制理解不透彻。

本文将为你彻底拆解一个基于STM32的实用波形发生器项目。我们不只讲“是什么”,更会深入“为什么”——为什么选择DAC而不是PWM?为什么需要DMA?查表法和实时计算法各自的优劣是什么?我们将从硬件选型、原理分析、到使用STM32CubeMX进行图形化配置,再到手写代码实现正弦波、方波、三角波,并最终通过按键或串口进行动态调频调幅。文章包含完整的代码示例、详细的配置说明和最常见的故障排查指南。无论你是正在做相关毕业设计的学生,还是希望为工控设备添加信号测试功能的工程师,这篇文章都能提供一条从理论到实践的清晰路径。

1. 波形发生器的核心:为什么是STM32 + DAC + DMA?

在开始写代码之前,我们必须先搞清楚技术选型。一个典型的波形发生器方案有很多种,比如使用专用DDS芯片(如AD9833)、利用PWM加滤波电路,或者就是我们即将详细讲解的MCU内置DAC方案。每种方案都有其适用场景。

  • 专用DDS芯片:性能高,频率精准,波形纯度高,但成本高,控制接口复杂,且“黑盒”操作不利于学习原理。
  • PWM + 滤波:成本极低,适合对波形质量要求不高的开关控制场景(如电机驱动),但想要获得干净的正弦波,需要复杂的多阶滤波电路,且频率和幅度调整相互耦合,精度差。
  • MCU内置DAC:在成本、性能和灵活性上取得了最佳平衡。STM32系列大多内置了12位DAC,分辨率高,线性度好。配合定时器触发和DMA传输,可以几乎不占用CPU资源地输出连续、稳定的模拟波形。这正是我们学习的重点。

这个方案的核心技术三角是:定时器 (TIM)数模转换器 (DAC)直接存储器访问 (DMA)

  • 定时器充当“节拍器”,以精确的间隔(例如,每秒100万次)产生触发信号。
  • DAC是“执行者”,收到触发后,将数字代码转换成对应的模拟电压输出。
  • DMA是“搬运工”,在后台自动将预先计算好的波形数据(存放在数组里)从内存搬运到DAC的数据寄存器,无需CPU干预。

这样,CPU只在需要改变波形参数(如频率、幅度)时进行计算和更新波形表,在输出阶段被完全解放,系统效率极高,波形也非常稳定。这是实现高质量波形发生器的关键架构。

2. 硬件平台与开发环境准备

2.1 硬件选型建议

并非所有STM32都适合。你需要选择一款带有DAC和DMA的外设,并且主频足够高以支持所需波形频率的计算。以下是几个常见选择:

  1. STM32F103系列(如F103C8T6,即“蓝桥杯”/“最小系统板”)
    • 优点:资料极多,价格低廉,社区支持好。
    • 注意:部分型号只有一个DAC通道,且DAC触发源可能不如高端型号灵活。对于学习入门和简单应用完全足够。
  2. STM32F407/F429系列
    • 优点:性能强劲,主频高(168MHz以上),DAC和定时器功能完整,支持更高精度和更复杂的波形算法(如实时DDS计算)。
    • 适合:对性能有要求,或希望探索更高级功能(如双通道同步输出)的开发者。
  3. STM32G4系列
    • 优点:较新的系列,性价比高,内置运算放大器,可与DAC直接连接,简化模拟前端设计。

最小系统需求:一块你选定的STM32开发板、一条ST-Link或DAP-Link下载调试器、若干杜邦线。输出端可以直接用示波器探头测量DAC输出引脚,或者连接一个简单的运放电路(如电压跟随器)以提高驱动能力。

2.2 软件环境搭建

我们使用STM32CubeMX进行图形化初始化配置,搭配Keil MDK-ARMSTM32CubeIDE进行代码编写和调试。这是目前最高效、最不易出错的开发方式。

  1. 安装STM32CubeMX:从ST官网下载并安装。它集成了所有STM32系列的芯片支持包。
  2. 安装Keil MDK-ARM:如果你选择Keil,需要安装对应芯片的Device Family Pack(DFP)。如果使用STM32CubeIDE(ST官方免费IDE),则无需此步骤。
  3. 安装串口调试助手:如SecureCRT、Putty或国产的XCOM,用于后续的波形参数控制。

3. 使用STM32CubeMX进行项目初始化配置

这是最关键的一步,正确的配置是代码成功的基础。我们以STM32F103C8T6为例,目标是让DAC1(PA4)在定时器2的触发下,通过DMA输出波形。

  1. 新建项目,选择芯片:打开CubeMX,点击“New Project”,在Part Number Search中输入你的芯片型号(如STM32F103C8T6),选中并开始项目。
  2. 配置时钟树 (Clock Configuration)
    • 通常使用内部高速时钟(HSI)或外部晶振(HSE)。为了获得更精确的定时,建议使用外部8MHz晶振(如果板载有)。
    • 在Clock Configuration标签页,将系统时钟(SYSCLK)配置到芯片允许的最高频率(对于F103,通常是72MHz)。这将为定时器提供高精度的时钟源。
  3. 配置DAC
    • 在Pinout & Configuration标签页的左侧边栏,找到Analog->DAC
    • 启用OUT1 Configuration(对应PA4)或OUT2 Configuration(对应PA5)。
    • Parameter Settings中:
      • Output Buffer:使能,以增加驱动能力。
      • Trigger:选择Timer 2 Trigger Out event。这意味着DAC转换将由TIM2来触发。
    • 此时,引脚PA4会自动被配置为模拟模式。
  4. 配置定时器 (TIM2)
    • 找到Timers->TIM2
    • Clock Source设为Internal Clock
    • Parameter Settings中:
      • Prescaler(预分频器):设置为71。如果系统时钟是72MHz,则72/(71+1)=1MHz,即定时器计数时钟为1MHz。
      • Counter ModeUp(向上计数)。
      • Counter Period(自动重装载值ARR):这是控制波形频率的关键。先设置为999。此时,定时器更新频率 = 1MHz / (999+1) = 1kHz。即每秒触发DAC 1000次。
      • Trigger Event Selection:将Update Event使能。这样每次定时器溢出更新时,就会产生一个触发信号给DAC。
  5. 配置DMA
    • 找到System Core->DMA
    • 点击Add,选择通道(对于F103的DAC1,通常是DMA1 Channel 3)。
    • 在下方配置:
      • DirectionMemory To Peripheral(内存到外设)。
      • PriorityHigh
      • ModeCircular(循环模式)。这是关键!它使得DMA在传输完一轮数据后自动从头开始,实现连续输出。
    • 然后回到DAC的配置页面,在DMA Settings子标签页,点击Add,选择刚才配置的DMA请求(如DAC1),方向会自动设为Memory To Peripheral
  6. 配置串口(用于控制,可选但推荐)
    • 找到Connectivity->USART1
    • 模式选择Asynchronous(异步通信)。
    • 配置合适的波特率(如115200)。
    • 引脚PA9和PA10会自动配置为TX和RX。
  7. 生成代码
    • 点击Project Manager标签页,设置项目名称、路径,选择IDE(MDK-ARM或STM32CubeIDE)。
    • Code Generator中,勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral,这样代码结构更清晰。
    • 点击GENERATE CODE

4. 核心代码实现:生成三种基础波形

生成了代码后,我们进入main.c和相关的用户代码区进行编写。核心任务是:生成波形数据表,并启动DMA传输

4.1 定义全局变量与波形表

/* USER CODE BEGIN PV */区域定义波形数据数组和参数。

/* USER CODE BEGIN PV */ #define WAVE_TABLE_SIZE 256 // 波形表大小,一个周期采样点数。越大,波形越平滑,但最高频率越低。 uint16_t sineWave[WAVE_TABLE_SIZE]; uint16_t squareWave[WAVE_TABLE_SIZE]; uint16_t triangleWave[WAVE_TABLE_SIZE]; uint16_t *currentWavePtr = sineWave; // 当前指向的波形表 uint32_t waveFrequency = 1000; // 当前波形频率,单位Hz uint32_t waveAmplitude = 2048; // 当前波形幅度,对应DAC输出中间值。12位DAC满量程4096。 uint8_t waveType = 0; // 波形类型:0-正弦,1-方波,2-三角波 /* USER CODE END PV */

4.2 生成波形表函数

/* USER CODE BEGIN 0 */区域编写波形表生成函数。DAC是12位,输出范围0-3.3V,对应数字值0-4095。我们通常让波形在中间值上下变化。

/* USER CODE BEGIN 0 */ #include <math.h> // 用于sin函数 void Generate_Waveforms(void) { // 1. 生成正弦波 // 公式:Value = Amplitude * sin(2π*i/N) + Offset // Offset设为2048(3.3V/2),使波形在1.65V上下对称。 for(int i=0; i<WAVE_TABLE_SIZE; i++) { sineWave[i] = (uint16_t)(waveAmplitude * sin(2 * M_PI * i / WAVE_TABLE_SIZE) + 2048); // 确保值在0-4095之间 if(sineWave[i] > 4095) sineWave[i] = 4095; } // 2. 生成方波 uint16_t highLevel = 2048 + waveAmplitude/2; uint16_t lowLevel = 2048 - waveAmplitude/2; for(int i=0; i<WAVE_TABLE_SIZE; i++) { squareWave[i] = (i < WAVE_TABLE_SIZE/2) ? highLevel : lowLevel; } // 3. 生成三角波 for(int i=0; i<WAVE_TABLE_SIZE; i++) { if(i < WAVE_TABLE_SIZE/2) { // 上升沿 triangleWave[i] = lowLevel + (highLevel - lowLevel) * i * 2 / WAVE_TABLE_SIZE; } else { // 下降沿 triangleWave[i] = highLevel - (highLevel - lowLevel) * (i - WAVE_TABLE_SIZE/2) * 2 / WAVE_TABLE_SIZE; } } } /* USER CODE END 0 */

4.3 更新定时器频率函数

波形频率由定时器的更新频率和波形表大小共同决定。公式为:输出频率 = 定时器触发频率 / 波形表大小我们需要在改变频率时,动态计算并重设定时器的ARR值。

/* USER CODE BEGIN 0 */ void Set_Wave_Frequency(uint32_t freq) { if(freq == 0) return; waveFrequency = freq; // 计算所需的定时器更新频率 // 更新频率 = 目标波形频率 * 波形表大小 uint32_t timerUpdateFreq = waveFrequency * WAVE_TABLE_SIZE; // 已知定时器时钟源为1MHz (TIM2的时钟) // ARR = 定时器时钟 / 更新频率 - 1 uint32_t arrValue = 1000000 / timerUpdateFreq - 1; // 确保ARR值在有效范围内 if(arrValue > 0xFFFF) arrValue = 0xFFFF; // TIM2是16位定时器 if(arrValue < 1) arrValue = 1; __HAL_TIM_SET_AUTORELOAD(&htim2, arrValue); // 如果需要立即生效,可以重载定时器 // __HAL_TIM_SET_COUNTER(&htim2, 0); // HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE); } /* USER CODE END 0 */

4.4 在main函数中初始化和启动

main函数的/* USER CODE BEGIN 2 */区域,调用生成函数,并启动DAC和定时器。

/* USER CODE BEGIN 2 */ // 1. 生成初始波形表 Generate_Waveforms(); // 2. 启动DAC,并启用DMA传输 // 将正弦波表的首地址和长度告诉DMA if (HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)currentWavePtr, WAVE_TABLE_SIZE, DAC_ALIGN_12B_R) != HAL_OK) { Error_Handler(); // 启动失败,进入错误处理 } // 3. 启动定时器,让它开始产生触发信号 HAL_TIM_Base_Start(&htim2); // 4. (可选)初始化串口并打印提示信息 printf("STM32 Waveform Generator Ready.\r\n"); printf("Current: Sine Wave, %lu Hz, %lu Amplitude.\r\n", waveFrequency, waveAmplitude); /* USER CODE END 2 */

4.5 实现串口控制(可选但实用)

通过串口发送简单指令来改变波形类型、频率和幅度。在main.c的串口中断回调函数或轮询接收函数中处理。

/* USER CODE BEGIN 4 */ // 假设在串口空闲中断或接收完成回调中处理 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { char cmd = uartRxBuffer[0]; // 假设接收到的第一个字符是命令 switch(cmd) { case 's': // 正弦波 currentWavePtr = sineWave; waveType = 0; break; case 'q': // 方波 currentWavePtr = squareWave; waveType = 1; break; case 't': // 三角波 currentWavePtr = triangleWave; waveType = 2; break; case 'f': // 设置频率,格式如 f1000 sscanf((char*)uartRxBuffer, "f%lu", &waveFrequency); Set_Wave_Frequency(waveFrequency); break; case 'a': // 设置幅度,格式如 a2000 sscanf((char*)uartRxBuffer, "a%lu", &waveAmplitude); Generate_Waveforms(); // 重新生成波形表 break; default: break; } // 更新DMA目标地址(注意:在DMA传输过程中修改目标地址需谨慎) // 更安全的方法是停止DMA,修改地址,再重启。或者使用双缓冲。 HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)currentWavePtr, WAVE_TABLE_SIZE, DAC_ALIGN_12B_R); // 重新开始接收 HAL_UART_Receive_IT(&huart1, uartRxBuffer, 1); } } /* USER CODE END 4 */

5. 编译、下载与效果验证

  1. 编译工程:在Keil或CubeIDE中点击编译按钮,确保0错误,0警告。
  2. 连接硬件:用ST-Link连接开发板的SWD接口(SWCLK, SWDIO, GND, 3.3V)。将DAC输出引脚(如PA4)连接到示波器探头。
  3. 下载程序:点击下载/调试按钮,将程序烧录到STM32。
  4. 上电运行:复位开发板。
  5. 示波器观测:你应该能在示波器上看到一个1kHz的正弦波(根据初始配置)。调整示波器的时基和电压档位,使波形清晰显示。
  6. 串口测试:打开串口调试助手,连接到开发板的串口,发送指令sqt,观察波形是否在正弦波、方波、三角波之间切换。发送f500,观察波形频率是否变为500Hz。

6. 常见问题与深度排查指南

即使按照步骤操作,你也可能会遇到一些问题。下表列出了最常见的问题及其解决方法:

问题现象可能原因排查思路解决方案
无输出,或输出为固定电压1. DAC或定时器未使能。
2. DMA配置错误(方向、模式)。
3. 波形表数据全为0或固定值。
4. 引脚配置错误(非模拟模式)。
1. 在main函数开始处添加HAL_Delay(100)后,用调试器查看hdachtim2相关寄存器是否已配置。
2. 单步调试,检查Generate_Waveforms函数生成的数组数据是否正确。
3. 检查CubeMX中DMA的DirectionMode
1. 确保HAL_DAC_Start_DMAHAL_TIM_Base_Start被成功调用。
2. 在CubeMX中仔细检查DAC、TIM、DMA的配置,特别是触发源和DMA请求映射。
3. 使用调试器的Memory窗口查看波形数组内容。
波形频率不准1. 系统时钟配置错误。
2. 定时器预分频器(PSC)和自动重装载值(ARR)计算错误。
3. 波形表大小WAVE_TABLE_SIZE影响计算。
1. 检查Clock Configuration中SYSCLK的频率是否与代码中计算所用频率一致。
2. 使用示波器测量实际频率,反推定时器实际工作频率。
3. 在Set_Wave_Frequency函数中添加打印,查看计算的ARR值。
1. 使用示波器校准。先固定一个频率(如1kHz),测量实际值,然后微调计算中的时钟频率参数(如将1000000改为999000)。
2. 确保WAVE_TABLE_SIZE是2的幂次(如256),可以提高计算效率。
波形失真(正弦波不圆滑,有台阶)1. 波形表大小WAVE_TABLE_SIZE太小。
2. DAC输出未加滤波电容。
3. 定时器触发频率接近DAC转换速率极限。
1. 增大WAVE_TABLE_SIZE(如512),观察波形改善情况。
2. 用示波器放大观察,看台阶是否是离散的采样点。
1. 在DAC输出引脚到地之间添加一个10pF-100pF的小电容,可以平滑毛刺。
2. 权衡WAVE_TABLE_SIZE和最高输出频率。频率越高,可用的采样点越少。
改变波形或参数时输出异常(毛刺、断流)1. 在DMA传输过程中直接修改了源数据数组。
2. 切换波形时未正确处理DMA传输。
1. 检查是否在Generate_Waveforms或切换currentWavePtr时,DMA仍在搬运旧数据。1.使用双缓冲:定义两个波形数组,DMA传输A缓冲区时,CPU填充B缓冲区,然后切换。这是最专业的方法。
2.安全切换:先HAL_DAC_Stop_DMA,修改数据或指针,再HAL_DAC_Start_DMA
输出幅度达不到预期1. DAC输出缓冲器未使能,驱动能力弱。
2. 负载阻抗太小,拉低了电压。
3. 波形表生成算法中幅度计算有误。
1. 测量空载时的输出电压是否正确。
2. 检查Generate_Waveforms函数中highLevellowLevel的计算。
1. 在CubeMX中确保DAC的Output Buffer已使能。
2. 在DAC输出后接一个电压跟随器(运放)来隔离负载。
3. 复核幅度计算代码,确保waveAmplitude变量被正确使用。

7. 进阶优化与最佳实践

一个基础可用的波形发生器已经完成,但要用于更严肃的场合,还需要考虑以下优化:

  1. 使用DMA双缓冲 (Double Buffer): 这是消除切换波形时产生毛刺的关键。原理是DMA配置两个内存缓冲区,当DMA传输完缓冲区0后,自动跳转到缓冲区1,并产生一个“半传输完成”或“传输完成”中断。在中断里,CPU可以安全地填充下一个周期的数据到空闲的缓冲区。

    // 在CubeMX中使能DMA的循环模式,并在代码中启用传输完成中断。 // 定义两个缓冲区 uint16_t waveBuffer0[WAVE_TABLE_SIZE]; uint16_t waveBuffer1[WAVE_TABLE_SIZE]; // 在DMA初始化后,启动双缓冲传输 HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)waveBuffer0, WAVE_TABLE_SIZE, DAC_ALIGN_12B_R); // 在DMA传输完成中断回调函数中,切换缓冲区并填充新数据 void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef* hdac) { // 填充刚刚传输完的缓冲区(例如Buffer1)为下一个波形数据 Fill_Wave_Data(waveBuffer1); }
  2. 实现更精细的频率控制: 上述方法通过改变定时器ARR来调频,频率分辨率受限于定时器时钟和波形表大小。对于更高精度的频率合成,可以研究直接数字频率合成 (DDS)算法。其核心是使用一个相位累加器,每次累加一个频率控制字(FTW),用累加器的高位作为波形表的索引。这样可以实现频率的连续、高分辨率调节。

  3. 增加模拟输出调理电路: DAC输出通常是0-3.3V,且带载能力有限。可以设计一个运放电路,实现:

    • 电压跟随器:提高驱动能力,隔离MCU与负载。
    • 放大电路:将信号幅度放大到所需范围(如±5V, 0-10V)。
    • 偏置电路:将单极性输出(0-3.3V)转换为双极性输出(-1.65V - +1.65V)。
  4. 设计友好的人机交互

    • 按键+编码器:用几个按键切换波形,用一个旋转编码器调节频率和幅度,配合OLED屏显示当前参数。
    • 图形化上位机:通过串口或USB与PC通信,在PC端用Qt、C#或Python开发一个控制软件,实现图形化参数设置和波形预览。
  5. 性能边界评估

    • 最高频率:受限于DAC的建立时间、DMA传输速度和定时器频率。公式近似为:Fmax = Timer_Clock / (WAVE_TABLE_SIZE * N),其中N是一个与DAC设置相关的因子(通常>=2)。对于STM32F103,输出几kHz到几十kHz的正弦波是稳妥的。
    • CPU占用率:在DMA模式下,输出波形时CPU占用率几乎为0,仅在改变参数时进行计算,非常适合多任务系统。

8. 总结与项目拓展方向

通过这个项目,你不仅得到了一个可用的波形发生器,更重要的是掌握了STM32中DAC、定时器触发和DMA协同工作这一核心技能。这套技术组合是许多高级应用的基础,例如音频播放、精密电压控制、任意波形生成等。

你可以以此为起点,进行更多探索:

  • 拓展波形:实现锯齿波、脉冲波,甚至通过读取SD卡内的数据来播放任意波形。
  • 提高性能:移植到STM32F4或H7系列高性能MCU,利用其更高的主频和更快的DAC,输出上百kHz的波形。
  • 集成控制:结合触摸屏(如使用LVGL库)或无线模块(如ESP8266),做一个网络遥控信号源。
  • 学习DDS:深入研究DDS原理,用STM32实现一个灵活的频率合成器,这是通信和测量设备中的常见模块。

这个项目的代码和思路具有很高的复用价值。当你下次需要产生一个模拟信号时,无论是用于测试、控制还是演示,你都可以快速搭建出这个可靠的信号源核心。建议你将本文的配置步骤和核心代码保存下来,它将成为你嵌入式工具箱中的一个实用模块。

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

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

立即咨询