STM32F103 DAC+DMA正弦波生成实战指南:从原理到示波器验证
第一次接触STM32的DAC功能时,看着开发板上那两个神秘的模拟输出引脚,我总好奇它们能否像函数发生器那样输出流畅的正弦波。直到某次智能家居项目中需要生成音频测试信号,才真正深入研究了DAC与DMA的黄金组合。本文将分享如何用STM32F103的片上DAC配合DMA控制器,实现高效、低CPU占用的正弦波输出方案。不同于简单的代码罗列,我会带你理解每个配置参数背后的设计考量,并教你用Python验证波形数据、用示波器捕捉真实输出效果。
1. 硬件基础与工程准备
STM32F103C8T6芯片内置两个12位DAC通道,最大转换速率可达1MHz。但直接通过CPU写入DAC数据寄存器会产生两个问题:一是波形更新间隔不稳定,二是会占用大量CPU资源。这就是我们需要引入定时器触发和DMA传输的原因。
开发环境准备清单:
- STM32F103C8T6开发板(如BluePill)
- ST-Link调试器
- Keil MDK或STM32CubeIDE
- 示波器(带宽≥20MHz)
- Python环境(用于波形验证)
注意:确保开发板的VDDA和VSSA引脚已正确连接3.3V和GND,这是DAC工作的电压基准。
配置工程时,需要开启以下外设时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);GPIO配置为模拟输入模式(AIN)至关重要,虽然DAC输出不需要输入功能,但这个模式会禁用内部上拉/下拉电阻,确保输出信号纯净:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure);2. 正弦波数据生成与验证
DAC本身没有内置正弦波发生器,我们需要预先计算好波形数据点。一个周期采样32点的12位正弦数组如下:
uint16_t Sine12bit[32] = { 2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056, 4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447, 2047, 1647, 1263, 909, 599, 344, 155, 38, 0, 38, 155, 344, 599, 909, 1263, 1647 };数据生成原理:
- 2047对应0V(12位DAC的中间值)
- 4095对应3.3V
- 每个点计算公式:
2047 + 2047 * sin(2π*i/32),其中i=0~31
用Python可以直观验证波形质量:
import matplotlib.pyplot as plt import numpy as np points = 32 x = np.linspace(0, 2*np.pi, points) y = 2047 + 2047 * np.sin(x) plt.plot(x, y, 'bo-') plt.show()实际项目中,可以通过以下方法优化波形:
- 增加采样点数(如128点)提升分辨率
- 添加谐波补偿算法改善THD(总谐波失真)
- 使用查表法替代实时计算
3. DMA双通道配置技巧
STM32的DAC支持双通道同步更新,这需要将两个通道的数据打包成32位字。低位16位是通道1数据,高位16位是通道2数据:
uint32_t DualSine12bit[32]; for(int i=0; i<32; i++) { DualSine12bit[i] = (Sine12bit[i] << 16) | Sine12bit[i]; }DMA配置的关键参数解析:
| 参数 | 设置值 | 说明 |
|---|---|---|
| PeripheralBaseAddr | 0x40007420 | DAC双通道数据寄存器地址 |
| MemoryBaseAddr | DualSine12bit数组地址 | 波形数据源 |
| Direction | PeripheralDST | 内存到外设传输 |
| BufferSize | 32 | 一个周期的采样点数 |
| PeripheralInc | Disable | DAC寄存器地址固定 |
| MemoryInc | Enable | 数组地址自动递增 |
| Mode | Circular | 循环模式实现连续输出 |
完整初始化代码:
DMA_InitStructure.DMA_PeripheralBaseAddr = 0x40007420; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)DualSine12bit; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 32; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_Init(DMA2_Channel4, &DMA_InitStructure);4. 定时器触发与系统联调
TIM8作为触发源,其更新频率决定了正弦波的输出频率。计算公式为:
输出频率 = TIM8时钟 / (TIM_Prescaler + 1) / (TIM_Period + 1) / 采样点数例如,当TIM8时钟为72MHz,Prescaler=0,Period=71时:
72,000,000 / 1 / 72 / 32 ≈ 31.25kHz实际配置示例:
TIM_TimeBaseStructure.TIM_Period = 71; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseInit(TIM8, &TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM8, TIM_TRGOSource_Update);常见问题排查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无输出 | DMA未使能 | 检查DMA_Cmd和DAC_DMACmd调用 |
| 波形畸变 | 缓冲区太小 | 增加采样点数到64或128 |
| 频率不准 | 定时器配置错误 | 重新计算TIM_Period值 |
| 噪声大 | 未禁用输出缓冲 | 设置DAC_OutputBuffer_Disable |
示波器测量时,如果发现波形阶梯明显,可以:
- 在DAC输出端添加RC低通滤波器(如1kΩ+100nF)
- 提高采样点数并相应降低输出频率
- 启用DAC的输出缓冲(但会限制最高输出电压)
5. 进阶应用与性能优化
当系统需要同时处理其他任务时,可以考虑以下优化策略:
动态频率调整:
void Set_Sine_Freq(uint32_t freq) { uint32_t clock = 72000000; // TIM8时钟频率 uint32_t arr = (clock / (32 * freq)) - 1; TIM8->ARR = arr; }双缓冲区技巧:
- 配置两个波形缓冲区
- DMA完成中断中切换缓冲区
- 允许实时更新波形数据而不影响输出
输出幅度控制:
void Set_Sine_Amplitude(float ratio) { for(int i=0; i<32; i++) { DualSine12bit[i] = (uint32_t)(Sine12bit[i] * ratio) << 16 | (uint32_t)(Sine12bit[i] * ratio); } }在电机控制测试中,我发现将DMA缓冲区设置为128点,配合TIM8的精确触发,可以生成THD<1%的优质正弦波。这种方案比PWM+滤波更高效,特别适合需要同时输出多路模拟信号的场合。