正弦波作为电子工程中最基础的模拟信号之一,其生成原理看似简单却蕴含着精妙的硬件协同设计。想象一下音乐播放器的工作方式:数字音频文件通过解码变成离散的数据点,再经过数模转换变成连续的声波。GD32的DAC+TIMER+DMA组合正是实现了类似的信号重构过程,只不过我们这次要生成的是纯净的正弦波。
选择GD32F103系列作为硬件平台有几个实际考量:首先它的DAC分辨率达到12位(4096级精度),对于大多数测试场景完全够用;其次它的定时器支持高达108MHz时钟,可以生成精确的触发间隔;最重要的是它的DMA控制器支持外设硬件触发,这才是实现"零CPU占用"的关键。我在实际项目中对比过STM32F103和GD32F103的同配置表现,发现GD32的DMA响应延迟更稳定,这对波形连续性非常重要。
硬件连接极其简单:只需将开发板的PA4(DAC0输出引脚)连接到示波器探头,但要注意接地共参考。这里有个容易忽略的细节——如果示波器显示波形抖动严重,很可能是开发板供电不足导致的。我习惯给开发板单独接一个线性稳压电源,而不是依赖USB供电,这样能明显改善波形质量。
GD32的DAC模块看似简单,但寄存器配置有几个"隐藏关卡"。先说说DAC的基础工作流程:当触发信号到来时,数据保持寄存器(DHR)的内容会转移到数据输出寄存器(DOR),这个转移过程需要约3个DAC时钟周期(在72MHz主频下约41.7ns)。这意味着如果定时器触发间隔小于这个时间,就会导致数据丢失。
配置DAC0需要重点关注三个寄存器:
这里有个坑我踩过:GD32的DAC在使能后需要约10us的稳定时间才能输出正确电压。如果一上电就启动输出,前几个波形点可能会异常。解决方法是在初始化代码中加入延时:
c复制dac_enable(DAC0);
delay_us(15); // 等待DAC稳定
DAC输出电压的计算公式很简单:
Vout = Vref * (DOR / 4095)
其中Vref通常是3.3V。但要注意这个线性度只在特定负载下成立,当输出接低阻抗负载时,建议增加运算放大器做缓冲。
TIMER6作为基础定时器,其核心任务就是产生精准的触发脉冲。假设我们要生成1kHz正弦波,一个周期采样100个点,那么触发频率应该是100kHz。相关计算如下:
定时器时钟 = 108MHz
分频值 = 108
自动重装载值 = 10
实际触发频率 = 108MHz / (108 * 10) = 100kHz
对应的寄存器配置要点:
c复制timer_parameter_struct timer_initpara;
timer_initpara.prescaler = 108 - 1;
timer_initpara.period = 10 - 1;
timer_init(TIMER6, &timer_initpara);
关键点在于触发源的选择。GD32的定时器主模式控制寄存器(TIMERx_CTL1)中,MMC位必须设置为010(更新事件作为触发输出)。这个设置在标准外设库中没有直接对应的函数,需要手动操作寄存器:
c复制TIMER_CTL1(TIMER6) |= TIMER_CTL1_MMC_0;
TIMER_CTL1(TIMER6) &= ~TIMER_CTL1_MMC_1;
实测中发现,如果定时器配置后没有立即启动,首次触发可能会有数十ns的偏差。建议的解决方法是先启动定时器,再使能主模式输出:
c复制timer_enable(TIMER6);
delay_us(1); // 确保定时器稳定运行
timer_master_output_trigger_source_select(TIMER6, TIMER_TRI_OUT_SRC_UPDATE);
DMA配置是整个系统中最容易出错的部分。GD32的DAC0固定使用DMA1通道2,这个映射关系不能更改。配置时需要特别注意以下几点:
完整的DMA初始化代码示例:
c复制dma_parameter_struct dma_init_struct;
dma_struct_para_init(&dma_init_struct);
dma_init_struct.periph_addr = (uint32_t)&DAC_DHR12R1;
dma_init_struct.memory_addr = (uint32_t)sin_table;
dma_init_struct.direction = DMA_PERIPH;
dma_init_struct.number = SIN_TABLE_SIZE;
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_init_struct.memory_inc = DMA_MEM_INCREASE_ENABLE;
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT;
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT;
dma_init_struct.priority = DMA_PRIORITY_HIGH;
dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_ENABLE;
dma_init(DMA1, DMA_CH2, &dma_init_struct);
外设握手机制是保证数据同步的关键。当DAC被定时器触发后,它会自动拉高DMA请求线,这时DMA控制器才会执行一次传输。这个过程完全由硬件协调,不需要CPU干预。我在调试时用逻辑分析仪抓取过这个握手信号,发现从触发到数据传输完成的延迟大约在5个时钟周期左右。
正弦波数据表的质量直接影响输出波形。常见的生成方法有两种:
对于固定频率应用,我推荐第一种方法。使用Python生成数据表既方便又灵活:
python复制import numpy as np
SAMPLE_COUNT = 100 # 一个周期的采样点数
MAX_VALUE = 4095 # 12位DAC满量程
sine_table = (np.sin(2 * np.pi * np.arange(SAMPLE_COUNT) / SAMPLE_COUNT) * MAX_VALUE/2 + MAX_VALUE/2).astype(int)
这里有几个优化技巧:
如果追求极致性能,可以采用查表法+线性插值。我在一个音频项目中测试过,这种方法可以将THD(总谐波失真)降低到0.1%以下。
当所有代码就绪后,示波器就成了最重要的调试工具。连接好探头后,建议按照以下步骤进行调试:
常见的波形问题及解决方法:
我用GD32F103C8T6实测过不同配置下的波形质量。在100kHz更新率、100点采样的情况下,示波器测得的THD约为0.8%。当开启DAC输出缓冲后,THD可以降到0.3%左右,但代价是输出阻抗会升高。
基础实现只能生成固定频率正弦波,通过动态调整定时器重装载值可以实现频率调节。这里分享一个实用的频率计算函数:
c复制void set_sine_frequency(uint32_t timer_clock, uint16_t samples, float freq)
{
uint16_t reload = (uint16_t)(timer_clock / (samples * freq)) - 1;
TIMER_CAR(TIMER6) = reload;
}
调用示例(设置1kHz正弦波,100个采样点):
c复制set_sine_frequency(108000000, 100, 1000.0f);
需要注意的是,频率改变时会出现短暂的波形失真。我在项目中采用双缓冲机制解决这个问题:准备两套正弦波数据表,在定时器更新中断中切换DMA目标地址,可以实现无缝频率切换。