尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于STM32的数字波形发生器实现:从PWM模拟到硬件DAC的完整方案

发布时间:2026/9/3 12:03:37

资讯中心
01
ARTICLE

基于STM32的数字波形发生器实现:从PWM模拟到硬件DAC的完整方案

基于STM32的数字波形发生器实现:从PWM模拟到硬件DAC的完整方案
最近在做一个嵌入式信号处理项目时需要生成特定频率和形状的测试信号。一开始尝试用现成的信号发生器但发现成本高、灵活性差特别是需要与STM32主控进行深度交互时很不方便。于是我决定基于手头的STM32F103C8T6核心板自己动手实现一个低成本、高灵活性的数字波形发生器。本文将完整分享从原理分析、硬件选型、软件设计到代码实现的全部过程并提供可直接编译运行的工程源码。无论你是正在做课程设计的学生还是需要快速原型验证的工程师都能从中获得一套完整的解决方案。1. 波形发生器的核心概念与STM32方案选型在开始动手之前我们首先要明确“波形发生器”到底是什么以及为什么选择STM32来实现它。1.1 什么是数字波形发生器波形发生器或称信号发生器是一种能产生特定频率、幅度和波形形状的电信号的设备。传统的模拟信号发生器基于振荡电路而数字波形发生器DDS直接数字频率合成则完全在数字域内完成信号的生成再通过数模转换器DAC输出模拟电压。基于STM32的数字波形发生器其核心原理是在MCU内部预先计算或存储一个周期波形的离散采样点例如正弦波的256个点然后通过定时器精确地控制DAC或PWM模拟DAC以固定的时间间隔依次输出这些点。通过改变输出采样点的速度即定时器频率就能改变输出波形的频率通过改变采样点的数值就能改变波形的幅度和直流偏移。1.2 为什么选择STM32STM32系列单片机因其高性能、丰富的外设和极高的性价比成为嵌入式开发的首选。对于波形发生器项目STM32具备以下优势高性能内核Cortex-M系列内核提供足够的算力进行实时波形计算如查表、插值。丰富定时器高级定时器TIM1, TIM8和通用定时器TIM2-TIM5可以产生高精度的PWM或触发DAC这是控制波形输出节拍的关键。内置DAC许多STM32型号如F103C8T6没有但F103CBT6有F407/F429等则普遍具备集成了12位DAC可直接输出模拟电压简化硬件设计。DMA支持直接存储器访问可以在不占用CPU的情况下自动将波形数据从内存搬运到DAC数据寄存器实现极低抖动的稳定输出。灵活的通信接口USART、SPI、I2C等便于连接屏幕、按键、编码器等人机交互模块也可以接收上位机的控制指令。1.3 常见实现方案对比根据硬件资源的不同主要有三种实现方案方案A无硬件DAC使用PWM RC低通滤波器模拟DAC。这是最经济的方式利用STM32的PWM输出通过滤波得到平滑的模拟电压。缺点是分辨率、精度和带宽受PWM频率和滤波器性能限制适合要求不高的场景。方案B有硬件DAC使用STM32内置DAC。这是最直接、性能最好的方式输出稳定、精度高。本文将以STM32F103C8T6无DAC和STM32F103CBT6有DAC为例分别讲解两种方案。方案C外置高速DAC通过SPI或并行接口连接专用DDS芯片如AD9833或高速DAC如AD9959。这种方式能实现极高的频率和精度但成本和复杂度也最高。考虑到通用性和学习价值我们将重点讲解方案APWM模拟和方案B内置DAC并提供一个融合两种方案的通用工程框架。2. 开发环境与硬件准备工欲善其事必先利其器。在写代码之前确保你的软硬件环境已就绪。2.1 软件环境清单集成开发环境IDEKeil uVision 5 MDK。这是STM32开发最主流的环境。确保已安装对应芯片的Device Family Pack例如STM32F1xx_DFP。固件库STM32标准外设库Standard Peripheral Library或HAL库。标准库更贴近硬件代码效率高HAL库抽象程度高移植方便。本文示例将基于标准库因其更利于理解底层原理。串口调试工具SecureCRT、Putty或任意串口助手用于接收调试信息。逻辑分析仪或示波器可选但强烈推荐用于观察最终生成的波形验证频率和幅度是否正确。Saleae逻辑分析仪或国产替代品是性价比之选。2.2 硬件物料清单本项目核心硬件如下你可以根据手头资源灵活调整组件型号/规格数量说明主控MCUSTM32F103C8T6 (核心板)1“蓝色药丸”无内置DAC采用PWM方案。或主控MCUSTM32F103CBT6 (核心板)1有内置DAC可直接使用DAC方案。OLED显示屏SSD1306, 0.96寸, I2C接口1用于显示波形类型、频率、幅度等信息。旋转编码器EC11, 带按键1用于调整参数频率、幅度等按键用于切换功能。按键轻触开关2-3用于波形切换、模式选择等。低通滤波器电阻(1kΩ)、电容(0.1uF)1套用于PWM方案滤除高频开关噪声。运放电路可选LM358或TL0821套用于信号放大、电平偏移或驱动负载。杜邦线、面包板-若干用于连接电路。硬件连接示意图以STM32F103C8T6为例OLED (I2C) SCL - PB6, SDA - PB7, VCC - 3.3V, GND - GND。编码器 CLK - PA0, DT - PA1, SW - PA2 (按键), VCC - 3.3V, GND - GND。PWM输出 使用定时器通道例如 TIM3_CH2 (PA7)。连接至RC低通滤波器PA7 - 1kΩ - 输出点 - 0.1uF - GND。串口1 (调试) PA9(TX) - USB-TTL的RX, PA10(RX) - USB-TTL的TX。如果你使用STM32F103CBT6则将PWM输出点改为DAC输出通道PA4或PA5。3. 核心原理与软件架构设计理解了硬件连接我们来看软件如何驱动这些硬件来产生波形。3.1 波形生成的核心定时器与数据缓冲区无论采用PWM还是DAC其核心流程都是一样的可以抽象为以下几步波形表生成在程序初始化时根据所需波形正弦波、方波、三角波、锯齿波计算出一个周期内的离散采样值并存入数组波形表。例如一个12位精度的正弦波表有256个点。定时器中断配置一个定时器使其以固定的频率产生更新中断。这个频率决定了波形输出的“采样率”。输出频率 定时器中断频率 / 波形表长度。中断服务程序在定时器中断中将波形表中的下一个值通过PWM的占空比或DAC的数据寄存器输出。DMA搬运高级用法为了减轻CPU负担并提高输出稳定性可以配置DMA让定时器的更新事件自动触发DMA将波形表数据直接搬运到PWM的CCR寄存器或DAC的DHR寄存器。CPU只在需要改变波形或参数时才介入。3.2 系统软件架构为了代码清晰和可维护我们采用模块化设计。整个工程包含以下核心模块main.c 主循环调度各模块。waveform.c/h 波形数据生成器。负责生成正弦、方波、三角波、锯齿波的波形表。signal_output.c/h 信号输出驱动器。抽象层根据宏定义选择使用PWM_DAC驱动还是硬件DAC驱动。pwm_dac.c/h PWM模拟DAC的具体实现。dac_driver.c/h 硬件DAC的具体实现。encoder.c/h 旋转编码器驱动用于参数调整。oled_menu.c/h OLED显示与用户界面。sys_tick.c/h 系统滴答定时器用于延时和软件定时。uart_debug.c/h 串口调试信息输出。4. 完整实战代码实现与解析下面我们将分步骤实现各个核心模块。为了节省篇幅这里展示最关键的代码片段完整工程可以在文末的链接中获取。4.1 工程创建与基础配置首先在Keil中创建一个基于STM32F10x标准库的新工程。配置系统时钟为72MHz。初始化必要的系统文件system_stm32f10x.c等。4.2 波形表生成模块 (waveform.c)这个模块的核心是生成不同波形的查找表LUT。我们以12位分辨率0-4095和256点为一个周期为例。// waveform.h #ifndef __WAVEFORM_H #define __WAVEFORM_H #include stm32f10x.h #define WAVE_TABLE_SIZE 256 // 波形表大小 #define DAC_RESOLUTION 4096 // 12位DAC分辨率 typedef enum { WAVE_SINE 0, WAVE_SQUARE, WAVE_TRIANGLE, WAVE_SAWTOOTH, WAVE_NUM // 波形种类总数 } WaveformType; extern uint16_t WaveTable[WAVE_NUM][WAVE_TABLE_SIZE]; void Waveform_GenerateAll(void); void Waveform_GenerateSine(uint16_t *buffer, uint16_t size, uint16_t amplitude, uint16_t offset); void Waveform_GenerateSquare(uint16_t *buffer, uint16_t size, uint16_t amplitude, uint16_t offset, uint8_t duty); void Waveform_GenerateTriangle(uint16_t *buffer, uint16_t size, uint16_t amplitude, uint16_t offset); void Waveform_GenerateSawtooth(uint16_t *buffer, uint16_t size, uint16_t amplitude, uint16_t offset); #endif// waveform.c #include waveform.h #include math.h uint16_t WaveTable[WAVE_NUM][WAVE_TABLE_SIZE]; // 生成所有波形表 void Waveform_GenerateAll(void) { // 默认幅度为满量程的80%偏移为满量程的50%即中点 uint16_t default_amp (uint16_t)(DAC_RESOLUTION * 0.8); uint16_t default_offset DAC_RESOLUTION / 2; Waveform_GenerateSine(WaveTable[WAVE_SINE], WAVE_TABLE_SIZE, default_amp, default_offset); Waveform_GenerateSquare(WaveTable[WAVE_SQUARE], WAVE_TABLE_SIZE, default_amp, default_offset, 50); // 50%占空比 Waveform_GenerateTriangle(WaveTable[WAVE_TRIANGLE], WAVE_TABLE_SIZE, default_amp, default_offset); Waveform_GenerateSawtooth(WaveTable[WAVE_SAWTOOTH], WAVE_TABLE_SIZE, default_amp, default_offset); } // 生成正弦波表 void Waveform_GenerateSine(uint16_t *buffer, uint16_t size, uint16_t amplitude, uint16_t offset) { for (uint16_t i 0; i size; i) { // 计算sin值范围[-1, 1]映射到[0, amplitude]然后加上偏移 float radian 2 * 3.1415926f * i / size; float sin_val sinf(radian); uint16_t value (uint16_t)(amplitude * (sin_val 1.0f) / 2.0f offset); // 限制在12位范围内 if (value DAC_RESOLUTION) value DAC_RESOLUTION - 1; buffer[i] value; } } // 生成方波表 void Waveform_GenerateSquare(uint16_t *buffer, uint16_t size, uint16_t amplitude, uint16_t offset, uint8_t duty) { uint16_t high_point (size * duty) / 100; for (uint16_t i 0; i size; i) { buffer[i] (i high_point) ? (offset amplitude/2) : (offset - amplitude/2); // 边界保护 if(buffer[i] DAC_RESOLUTION) buffer[i] DAC_RESOLUTION - 1; if(buffer[i] 0) buffer[i] 0; // 注意uint16_t不会小于0这里逻辑上判断 } } // 生成三角波和锯齿波的代码类似此处省略... // 完整代码需处理上升沿和下降沿的线性计算关键点使用浮点数计算正弦波在初始化时完成运行时直接查表效率极高。幅度(amplitude)和偏移(offset)参数可以动态调整实现调幅和调偏功能。方波的占空比(duty)参数可调可以产生PWM波。4.3 PWM模拟DAC驱动 (pwm_dac.c)对于没有硬件DAC的芯片如F103C8T6我们需要用PWM来模拟。// pwm_dac.c #include pwm_dac.h #include stm32f10x_tim.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h #define PWM_TIMER TIM3 #define PWM_TIMER_CHANNEL TIM_Channel_2 #define PWM_TIMER_CLK RCC_APB1Periph_TIM3 #define PWM_GPIO_PORT GPIOA #define PWM_GPIO_PIN GPIO_Pin_7 #define PWM_GPIO_CLK RCC_APB2Periph_GPIOA #define PWM_AF_PIN_SOURCE GPIO_PinSource7 #define PWM_AF_TIMER_SOURCE GPIO_AF_TIM3 // PWM周期对应DAC的最大值。频率越高滤波后纹波越小但分辨率会下降。 // 这里设置为4096与12位DAC对齐。 #define PWM_PERIOD 4095 static uint16_t current_dac_value 0; void PWM_DAC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(PWM_GPIO_CLK, ENABLE); RCC_APB1PeriphClockCmd(PWM_TIMER_CLK, ENABLE); // 2. 配置GPIO为复用推挽输出 GPIO_InitStructure.GPIO_Pin PWM_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(PWM_GPIO_PORT, GPIO_InitStructure); // 3. 配置定时器基础向上计数预分频器为072MHz直接驱动周期为PWM_PERIOD TIM_TimeBaseStructure.TIM_Period PWM_PERIOD; TIM_TimeBaseStructure.TIM_Prescaler 0; // 72MHz / (01) 72MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(PWM_TIMER, TIM_TimeBaseStructure); // 4. 配置PWM输出模式通道2模式1输出使能 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比为0 TIM_OCInit(PWM_TIMER, PWM_TIMER_CHANNEL, TIM_OCInitStructure); // 5. 使能定时器预装载寄存器 TIM_OC2PreloadConfig(PWM_TIMER, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(PWM_TIMER, ENABLE); // 6. 启动定时器 TIM_Cmd(PWM_TIMER, ENABLE); } // 设置PWM占空比即模拟DAC输出值 void PWM_DAC_SetValue(uint16_t value) { if (value PWM_PERIOD) { value PWM_PERIOD; } current_dac_value value; switch (PWM_TIMER_CHANNEL) { case TIM_Channel_1: TIM_SetCompare1(PWM_TIMER, value); break; case TIM_Channel_2: TIM_SetCompare2(PWM_TIMER, value); break; case TIM_Channel_3: TIM_SetCompare3(PWM_TIMER, value); break; case TIM_Channel_4: TIM_SetCompare4(PWM_TIMER, value); break; default: break; } } uint16_t PWM_DAC_GetValue(void) { return current_dac_value; }关键点PWM_PERIOD设置为4095这样PWM的占空比分辨率就是1/4096模拟了12位DAC。PWM的频率是72MHz / (PWM_PERIOD 1) ≈ 17.57 kHz。这个频率需要后续的RC低通滤波器能够有效滤除否则输出会有很大的纹波。输出引脚必须配置为复用推挽输出GPIO_Mode_AF_PP。4.4 硬件DAC驱动 (dac_driver.c)对于有硬件DAC的芯片配置相对简单且性能更好。// dac_driver.c #include dac_driver.h #include stm32f10x_dac.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h #include stm32f10x_tim.h // 使用定时器触发DAC #define DAC_CHANNEL DAC_Channel_2 // 使用PA5 #define DAC_TRIGGER DAC_Trigger_T2_TRGO // 使用TIM2作为触发源 #define DAC_TIMER TIM2 #define DAC_TIMER_CLK RCC_APB1Periph_TIM2 void DAC_Driver_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB1PeriphClockCmd(DAC_TIMER_CLK, ENABLE); // 2. 配置DAC输出引脚PA5为模拟输入这是DAC的要求 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置DAC通道2 DAC_InitStructure.DAC_Trigger DAC_TRIGGER; // 外部触发 DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; // 不使用内置波形 DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Enable; // 使能输出缓冲 DAC_Init(DAC_CHANNEL, DAC_InitStructure); // 4. 使能DAC DAC_Cmd(DAC_CHANNEL, ENABLE); // 5. 配置触发定时器TIM2基础设置频率在输出模块中动态调整 TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 初始值会被动态修改 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 72MHz/72 1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(DAC_TIMER, TIM_TimeBaseStructure); // 6. 配置TIM2的TRGO事件 TIM_SelectOutputTrigger(DAC_TIMER, TIM_TRGOSource_Update); // 7. 先不启动定时器频率由输出模块控制 } // 设置DAC输出值软件触发单次写入 void DAC_Driver_SetValue(uint16_t value) { if (value 4096) value 4095; DAC_SetChannel2Data(DAC_Align_12b_R, value); } // 启动DAC的定时器触发并设置触发频率 void DAC_Driver_StartTimer(uint32_t timer_freq_hz) { // 计算定时器重载值 ARR // 定时器时钟 72MHz / (PSC1) 1MHz (上面配置的) // 定时器更新频率 1MHz / (ARR1) // 所以 ARR (1MHz / timer_freq_hz) - 1 if(timer_freq_hz 0 || timer_freq_hz 1000000) timer_freq_hz 1000; // 默认1kHz uint32_t arr (1000000 / timer_freq_hz) - 1; if(arr 0xFFFF) arr 0xFFFF; // 限制在16位范围内 TIM_SetAutoreload(DAC_TIMER, arr); TIM_Cmd(DAC_TIMER, ENABLE); // 启动定时器 }关键点DAC引脚必须配置为模拟输入模式GPIO_Mode_AIN。使用定时器触发DAC_Trigger_T2_TRGO可以实现自动、定时的数据更新这是实现波形连续输出的关键。数据更新需要通过DMA来完成下文会介绍。DAC_OutputBuffer建议使能以增强驱动能力但要注意缓冲器会引入一定的压降和非线性。4.5 信号输出抽象层与DMA配置 (signal_output.c)这是最核心的模块它负责以固定的速率将波形表中的数据送出。我们使用定时器更新事件触发DMA传输这是实现高质量波形输出的最佳实践。// signal_output.h #ifndef __SIGNAL_OUTPUT_H #define __SIGNAL_OUTPUT_H #include stdint.h #include stdbool.h typedef struct { uint32_t frequency; // 输出波形频率 (Hz) uint16_t amplitude; // 幅度 (0-4095) uint16_t offset; // 直流偏移 (0-4095) uint8_t wave_type; // 波形类型对应WaveformType uint8_t duty_cycle; // 方波占空比 (0-100%) } SignalConfig_t; void SignalOutput_Init(void); void SignalOutput_Start(const SignalConfig_t *config); void SignalOutput_Stop(void); void SignalOutput_UpdateConfig(const SignalConfig_t *config); bool SignalOutput_IsRunning(void); #endif// signal_output.c #include signal_output.h #include waveform.h #include pwm_dac.h // 或 dac_driver.h通过宏定义切换 #include stm32f10x_dma.h #include stm32f10x_tim.h // 选择输出方式定义 USE_HW_DAC 则使用硬件DAC否则使用PWM模拟 // #define USE_HW_DAC #ifdef USE_HW_DAC #include dac_driver.h #define OUTPUT_SET_VALUE(v) DAC_Driver_SetValue(v) #define OUTPUT_TIMER TIM2 #define OUTPUT_DMA_CHANNEL DMA1_Channel4 #define OUTPUT_DMA_FLAG_TC DMA1_FLAG_TC4 #else #include pwm_dac.h #define OUTPUT_SET_VALUE(v) PWM_DAC_SetValue(v) #define OUTPUT_TIMER TIM3 #define OUTPUT_DMA_CHANNEL DMA1_Channel2 // TIM3_CH2的DMA请求是Channel2 #define OUTPUT_DMA_FLAG_TC DMA1_FLAG_TC2 #endif static SignalConfig_t current_config; static volatile bool is_running false; static uint16_t *current_wave_table NULL; void DMA_Configuration(void) { DMA_InitTypeDef DMA_InitStructure; // 1. 使能DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置DMA DMA_DeInit(OUTPUT_DMA_CHANNEL); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(TIM3-CCR2); // PWM模式目标地址 #ifdef USE_HW_DAC DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(DAC-DHR12R2); // DAC模式目标地址 #endif DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)current_wave_table; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 内存到外设 DMA_InitStructure.DMA_BufferSize WAVE_TABLE_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式自动重复 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(OUTPUT_DMA_CHANNEL, DMA_InitStructure); // 3. 使能DMA DMA_Cmd(OUTPUT_DMA_CHANNEL, ENABLE); } void TIM_ConfigurationForDMA(uint32_t update_freq_hz) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 定时器时钟为72MHz // 计算预分频器(PSC)和自动重载值(ARR) uint32_t timer_clock 72000000; // 72MHz uint32_t prescaler 0; uint32_t period (timer_clock / update_freq_hz) - 1; // 如果频率太低需要增大PSC if (period 0xFFFF) { prescaler (period / 0xFFFF) 1; period (timer_clock / (prescaler 1) / update_freq_hz) - 1; } TIM_TimeBaseStructure.TIM_Period period; TIM_TimeBaseStructure.TIM_Prescaler prescaler; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(OUTPUT_TIMER, TIM_TimeBaseStructure); // 配置定时器更新事件触发DMA请求 #ifdef USE_HW_DAC TIM_SelectOutputTrigger(OUTPUT_TIMER, TIM_TRGOSource_Update); // DAC已配置为定时器触发此处确保DAC DMA使能 DAC_DMACmd(DAC_Channel_2, ENABLE); #else // 对于PWM需要配置CCR寄存器的DMA请求 TIM_DMACmd(OUTPUT_TIMER, TIM_DMA_CC2, ENABLE); #endif // 使能定时器 TIM_Cmd(OUTPUT_TIMER, ENABLE); } void SignalOutput_Init(void) { // 初始化底层输出驱动PWM或DAC #ifdef USE_HW_DAC DAC_Driver_Init(); #else PWM_DAC_Init(); #endif // 生成波形表 Waveform_GenerateAll(); current_wave_table WaveTable[WAVE_SINE]; // 默认正弦波 is_running false; } void SignalOutput_Start(const SignalConfig_t *config) { if (is_running) { SignalOutput_Stop(); } current_config *config; // 1. 根据波形类型选择波形表 if (config-wave_type WAVE_NUM) { current_wave_table WaveTable[config-wave_type]; // 动态更新幅度和偏移这里简化处理实际需要根据参数重新生成或缩放波形表 // 更优方案维护一个原始波形表在DMA传输前或使用双缓冲进行实时缩放。 } // 2. 计算定时器更新频率 // 波形输出频率 定时器更新频率 / 波形表长度 uint32_t timer_update_freq config-frequency * WAVE_TABLE_SIZE; // 3. 配置DMA源地址是当前波形表 DMA_Configuration(); // 4. 配置定时器频率并启动 TIM_ConfigurationForDMA(timer_update_freq); is_running true; }关键点DMA循环模式这是实现无缝波形输出的核心。DMA会自动、循环地将整个波形表的数据搬运到外设寄存器无需CPU干预。频率计算输出频率由定时器更新频率和波形表长度共同决定。F_output F_timer_update / N。提高F_timer_update或减少N可以提高输出频率但受限于DMA速度和定时器精度。动态参数文中注释提到幅度和偏移的动态调整是一个难点。简单的做法是每次参数改变后重新生成整个波形表。更高效的做法是使用双缓冲CPU在一个缓冲区计算缩放后的新数据DMA从另一个缓冲区读取计算完成后交换缓冲区。4.6 主程序与用户交互 (main.c)主程序负责初始化所有模块并在循环中处理用户输入编码器、按键更新显示。// main.c (精简版) #include stm32f10x.h #include sys_tick.h #include encoder.h #include oled_menu.h #include signal_output.h #include uart_debug.h SignalConfig_t sig_cfg { .frequency 1000, // 1kHz .amplitude 2048, // 50% 幅度 .offset 2048, // 中点 .wave_type WAVE_SINE, .duty_cycle 50 }; int main(void) { // 系统初始化 SysTick_Init(72); // 1ms中断 UART_Debug_Init(115200); printf(Waveform Generator Boot...\r\n); // 模块初始化 Encoder_Init(); OLED_Menu_Init(); SignalOutput_Init(); // 生成初始波形并开始输出 SignalOutput_Start(sig_cfg); // 显示初始信息 OLED_ShowConfig(sig_cfg); while (1) { // 1. 处理编码器旋转调整频率/幅度等 Encoder_State_t enc_state Encoder_GetState(); if (enc_state ! ENC_NONE) { if (current_menu_item MENU_FREQ) { // 调整频率步进为10Hz sig_cfg.frequency (enc_state ENC_CW) ? 10 : -10; if (sig_cfg.frequency 1) sig_cfg.frequency 1; if (sig_cfg.frequency 50000) sig_cfg.frequency 50000; // 限制最高频率 SignalOutput_UpdateConfig(sig_cfg); } // ... 处理其他菜单项 OLED_ShowConfig(sig_cfg); // 刷新显示 } // 2. 处理编码器按键切换菜单项或波形 if (Encoder_GetButton() BUTTON_PRESSED) { current_menu_item (current_menu_item 1) % MENU_ITEM_NUM; OLED_HighlightMenuItem(current_menu_item); Delay_ms(200); // 简单消抖 } // 3. 处理独立按键切换波形类型 // ... (按键扫描代码) // 4. 其他任务如LED闪烁指示系统运行 static uint32_t led_tick 0; if (SysTick_GetTick() - led_tick 500) { led_tick SysTick_GetTick(); GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13))); } } }5. 常见问题与调试技巧在实现过程中你可能会遇到以下问题5.1 输出波形频率不准或抖动大可能原因1定时器频率计算错误。排查检查SignalOutput_Start函数中timer_update_freq的计算。确保系统时钟配置正确通常是72MHz。使用示波器测量实际输出频率。解决timer_update_freq desired_freq * WAVE_TABLE_SIZE。定时器的ARR和PSC设置需根据此频率重新计算。可能原因2DMA传输或中断延迟。排查如果使用中断方式而非DMA高频率下CPU可能无法及时响应中断导致波形点输出间隔不均。解决务必使用DMA循环模式。确保DMA和定时器优先级设置正确避免被其他高优先级中断打断。可能原因3波形表长度不合适。排查WAVE_TABLE_SIZE太小会导致波形阶梯状明显太大则限制最高输出频率。解决对于音频范围20Hz-20kHz256点或512点是常用选择。可通过实验权衡。5.2 PWM方案输出有毛刺或不是平滑正弦波可能原因1RC低通滤波器截止频率设置不当。排查PWM频率约为17.57kHz。RC滤波器的截止频率f_c 1/(2πRC)应远低于PWM频率但高于你想要的最高信号频率。例如想要1kHz正弦波截止频率可设为2-5kHz。解决调整R和C的值。例如R1kΩ, C0.1uF截止频率≈1.6kHz。可以尝试多级滤波两级RC。可能原因2电源噪声或接地不良。排查用示波器观察MCU的3.3V和GND引脚。解决在电源引脚就近放置去耦电容如100nF和10uF。确保信号地线连接良好。5.3 硬件DAC输出幅度不对或噪声大可能原因1DAC输出缓冲器未使能。排查检查DAC_InitStructure.DAC_OutputBuffer设置。解决使能输出缓冲DAC_OutputBuffer_Enable以提高驱动能力但需注意它并非轨到轨输出最大输出电压约为Vdda - 0.2V。可能原因2参考电压Vdda不稳定。排查STM32的DAC输出与Vdda直接相关。如果Vdda是3.3V但纹波较大输出也会波动。解决确保Vdda引脚连接了稳定的3.3V电源并添加足够的滤波电容。可能原因3代码中未正确对齐数据。排查DAC数据寄存器有左对齐、右对齐之分。DAC_SetChannel2Data(DAC_Align_12b_R, value)中的value应是0-4095。解决确保传入的数据值在0-4095范围内并使用正确的对齐方式。5.4 旋转编码器读数不稳定抖动可能原因机械编码器抖动。解决在软件中实现消抖。常用的方法是定时扫描如每10ms并采用状态机判断。encoder.c中应实现类似逻辑而不是在中断中直接计数。6. 优化与进阶实践完成基础功能后可以考虑以下优化方向让你的波形发生器更实用、更专业。6.1 性能优化使用TIMDMA双缓冲这是专业波形发生器的标配。创建两个波形缓冲区BufferA和BufferB。DMA正在传输BufferA时CPU在后台计算并填充BufferB。当DMA传输完BufferA后自动切换到BufferB并触发一个中断或DMA半传输完成中断通知CPU去填充BufferA。这样可以实现波形参数频率、幅度的实时、无毛刺切换。优化波形表生成算法对于正弦波如果追求极致的生成速度可以使用查表法结合线性插值。预先存储一个粗精度的正弦表如64点输出时通过插值计算中间点能节省大量存储空间并提高最高输出频率。使用更高精度定时器对于需要极高频率分辨率的应用可以尝试使用定时器的PSC和ARR寄存器进行更精细的分频。或者如果芯片支持使用高精度定时器如TIM2是32位定时器STM32F407等有更高时钟。6.2 功能扩展增加波形种类除了基础波形可以添加任意波形ARB允许用户通过串口或SD卡上传自定义的波形点数据。增加调制功能实现调幅AM、调频FM。这需要动态修改波形表的幅度或定时器的频率。可以结合DMA双缓冲在后台计算被调制后的波形。增加扫频功能让输出频率在设定的时间内从起始频率线性或对数地变化到终止频率。这需要动态地、平滑地改变定时器的重载值ARR。连接上位机通过串口或USBCDC虚拟串口与PC通信开发一个简单的上位机软件用图形界面控制波形发生器的所有参数。6.3 工程化建议模块化与可移植性正如本文所做将硬件驱动PWM/DAC、波形生成、输出逻辑、用户界面严格分离。通过宏定义如USE_HW_DAC来切换硬件平台提高代码复用率。参数存储使用STM32的内部Flash或外接EEPROM如AT24C02保存用户最后设置的波形参数实现掉电保存。输入保护与校准对用户输入的频率、幅度等参数进行范围检查和限幅。如果对精度要求高可以增加一个“校准”模式通过测量实际输出修正DAC的增益和偏移误差。完善的UI反馈OLED屏幕上除了显示参数还可以用简单的图形显示当前波形的大致形状。编码器旋转时提供视觉或听觉蜂鸣器反馈。7. 项目总结与资源获取通过这个项目我们不仅实现了一个可用的STM32波形发生器更深入理解了DDS原理、STM32的定时器、DAC、PWM、DMA等核心外设的协同工作方式。从PWM模拟DAC到硬件DAC从CPU中断搬运数据到DMA自动搬运每一步的优化都带来了性能的显著提升。关键知识点回顾DDS原理通过相位累加器、波形查找表和DAC构成数字频率合成核心。PWM模拟DAC利用高分辨率PWM和低通滤波器低成本实现模拟输出。定时器触发DMA实现高精度、低抖动周期性操作的黄金组合。模块化编程驱动层、算法层、应用层分离代码清晰易维护。你可以基于本文提供的代码框架轻松替换波形生成算法、修改输出频率范围、增加新的控制方式。将USE_HW_DAC宏定义注释掉或打开即可在PWM方案和DAC方案之间切换。获取完整工程代码由于篇幅限制文中仅展示了核心代码片段。完整的Keil MDK工程包含所有模块的源码、头文件、详细的注释以及原理图我已整理好。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。