保姆级教程:用GD32F103的DAC+TIMER+DMA生成正弦波,示波器实测波形
从零实现GD32F103的DACTIMERDMA正弦波生成实战指南在嵌入式开发中信号生成是基础但至关重要的技能。无论是音频处理、电机控制还是传感器模拟能够精确输出模拟信号都是开发者必备的能力。GD32F103系列作为国产MCU的优秀代表其DAC数模转换器配合定时器和DMA的功能组合可以实现高效、精确的波形生成而无需持续占用CPU资源。本文将带你从零开始一步步在GD32F103C-START开发板上实现正弦波输出。不同于单纯的理论讲解我们聚焦于实际可操作的完整流程涵盖工程配置、寄存器设置、代码编写到示波器验证的全过程。特别针对初学者容易遇到的坑点——如DMA通道映射、定时器触发源选择等——提供清晰的解决方案。1. 开发环境准备与基础认知在动手编码之前我们需要确保开发环境就绪并理解核心组件的工作原理。以下是必备的软硬件清单硬件准备GD32F103C-START开发板核心芯片为GD32F103C8T6USB转串口调试器如CH340数字示波器用于波形观测杜邦线若干软件工具Keil MDK或IAR Embedded WorkbenchGD32F10x系列固件库串口调试工具如Putty关键外设功能速览外设核心作用在本项目中的角色DAC数字量转模拟电压输出最终输出正弦波信号TIMER6产生精确的时间基准控制正弦波采样点的输出间隔DMA内存与外设间直接数据传输自动将波形数据搬运到DAC寄存器提示GD32的DAC输出电压范围为0~3.3V12位分辨率意味着每个数字量对应约0.8mV的电压变化。正弦波生成的本质是通过DAC输出一系列按正弦规律变化的电压点。假设我们要生成1kHz的正弦波一个周期需要输出100个点那么TIMER需要配置为每10μs触发一次DAC转换。DMA则负责自动将这些预计算好的正弦数据从内存搬运到DAC数据寄存器。2. 工程创建与基础配置首先在Keil中新建工程选择GD32F103C8T6作为目标器件。导入必要的固件库文件后我们需要进行基础时钟配置// 系统时钟配置72MHz rcu_clock_freq_set(RCU_CKSYSSRC_PLL); rcu_pll_config(RCU_PLLSRC_HXTAL_8M, RCU_PLL_MUL_9); rcu_osci_on(RCU_PLL_CK); while(!rcu_osci_stab_wait(RCU_PLL_CK)); rcu_system_clock_source_config(RCU_SCSS_PLL);接下来配置PA4引脚为DAC输出// 启用GPIOA和DAC时钟 rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_DAC); // 配置PA4为模拟输入模式DAC输出专用模式 gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_4);关键点检查清单确认开发板外部晶振频率通常8MHz验证系统时钟是否配置正确可通过调试器查看SystemCoreClock变量确保DAC通道0对应的GPIO引脚配置正确3. TIMER6配置与触发设置TIMER6作为基础定时器其核心作用是产生精确的触发信号。假设我们要生成1kHz正弦波使用100个采样点则每个点间隔应为10μs// 启用TIMER6时钟 rcu_periph_clock_enable(RCU_TIMER6); // 定时器基础配置 timer_parameter_struct timer_initpara; timer_struct_para_init(timer_initpara); timer_initpara.prescaler 72 - 1; // 72MHz/72 1MHz timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_initpara.period 10 - 1; // 1MHz/(10) 100kHz (10μs) timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_init(TIMER6, timer_initpara); // 关键配置设置主模式触发输出 timer_master_output_trigger_source_select(TIMER6, TIMER_TRI_OUT_SRC_UPDATE); timer_update_event_enable(TIMER6);避坑指南触发源必须选择TIMER_TRI_OUT_SRC_UPDATE这是TIMER6唯一可用的触发源务必使能更新事件(timer_update_event_enable)否则不会产生触发信号预分频值计算定时器时钟系统时钟/(prescaler1)注意GD32与STM32在定时器触发源配置上存在差异直接移植STM32代码可能导致无法触发4. DMA配置与通道映射DMA配置是本项目最易出错的环节之一。GD32F103的DAC通道0只能使用DMA1的通道2这一映射关系必须严格遵守// 启用DMA1时钟 rcu_periph_clock_enable(RCU_DMA1); // DMA配置结构体 dma_parameter_struct dma_init_struct; dma_struct_para_init(dma_init_struct); dma_init_struct.periph_addr (uint32_t)DAC_R12DH0; // DAC0数据保持寄存器 dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_addr (uint32_t)sin_wave; // 正弦波数组地址 dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_init_struct.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init_struct.direction DMA_PERIPHERAL_TO_MEMORY; dma_init_struct.number SIN_WAVE_POINTS; // 传输数据量 dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA1, DMA_CH2, dma_init_struct); // 启用DMA循环模式 dma_circulation_enable(DMA1, DMA_CH2); dma_channel_enable(DMA1, DMA_CH2);地址计算要点DAC0_R12DH寄存器地址0x40007400(DAC_BASE) 0x08 0x40007408正弦波数组应定义为const uint16_t sin_wave[SIN_WAVE_POINTS]数组需对齐到4字节边界__attribute__((aligned(4)))5. DAC配置与联动设置最后配置DAC使其能够响应TIMER触发并使用DMA传输数据// DAC配置 dac_trigger_source_config(DAC0, DAC_TRIGGER_T6_TRGO); dac_trigger_enable(DAC0); dac_wave_mode_config(DAC0, DAC_WAVE_DISABLE); dac_output_buffer_enable(DAC0); dac_enable(DAC0); // 启用DAC DMA功能 dac_dma_enable(DAC0);正弦波数据生成技巧 使用Python可以快速生成优化的正弦波数据import numpy as np points 100 # 一个周期的采样点数 sin_wave np.sin(np.linspace(0, 2*np.pi, points)) * 2047 2048 print([int(x) for x in sin_wave]) # 输出为12位右对齐格式6. 系统集成与调试技巧将所有组件集成后启动定时器开始波形生成timer_enable(TIMER6);使用示波器观察PA4引脚应能看到清晰的正弦波。若波形异常可按以下步骤排查常见问题诊断表现象可能原因解决方案无输出DMA通道错误确认使用DMA1_CH2波形不连续触发间隔不正确检查TIMER6周期设置幅值不完整DAC未启用输出缓冲调用dac_output_buffer_enable波形畸变采样点不足增加SIN_WAVE_POINTS值DMA传输不启动外设地址错误确认DAC_R12DH0地址正确性能优化建议若要改变波形频率只需调整TIMER6的period值对于更高频率波形可减少采样点数但需权衡波形质量使用SRAM而非Flash存储波形数据可提高访问速度7. 进阶应用与扩展思路掌握了基础正弦波生成后可以进一步探索多波形合成// 在内存中混合多个波形 for(int i0; iPOINTS; i) { wave_data[i] (sin_wave[i] square_wave[i]) / 2; }动态频率调整// 运行时改变波形频率 void set_wave_freq(uint32_t freq_hz) { timer_disable(TIMER6); timer_initpara.period (1000000 / (freq_hz * POINTS)) - 1; timer_init(TIMER6, timer_initpara); timer_enable(TIMER6); }实际项目经验 在电机控制应用中我发现将DMA缓冲区分为双缓冲可以实现在输出当前波形的同时准备下一组数据实现无缝切换。另外对于低功耗场景可以配置TIMER6在输出完成后自动停止通过外部事件重新触发。