RISC-V平台GD32VF103 DAC配置与波形生成实战指南

发布时间:2026/8/29 16:12:50
RISC-V平台GD32VF103 DAC配置与波形生成实战指南 1. 项目概述在RISC-V平台上玩转模拟信号输出最近在捣鼓一块基于RISC-V内核的GD32VF103开发板想用它来生成一些模拟信号比如做个简易的信号发生器或者驱动一个需要模拟电压控制的电机。这自然就绕不开板载的DAC数模转换器模块。和常见的ARM Cortex-M系列不同在RISC-V生态下尤其是GD32VF103这颗芯片上配置DAC的过程虽然底层逻辑相通但具体到寄存器操作和库函数使用还是有些“坑”要踩的。网上关于STM32的DAC教程一抓一大把但专门讲GD32VF103 DAC的、能说透细节的并不多。我花了些时间从数据手册啃起结合实战调试把整个流程跑通了。这篇文章就来详细拆解一下如何在GD32VF103上配置和使用DAC把数字代码变成实实在在的模拟电压并分享一些确保输出稳定、精确的实操心得。简单来说GD32VF103的DAC模块可以将一个12位的数字值0到4095转换成对应的模拟电压输出。对于VREF接3.3V的典型情况输出范围就是0V到3.3V。这个过程看似简单但要想输出稳定、响应快速、噪声低就需要对时钟、触发源、输出缓冲、对齐方式等有清晰的理解。我会从最基础的直接写入模式开始再到使用定时器触发实现自动更新最后聊聊如何通过校准和软件技巧来提升性能希望能给正在探索RISC-V和GD32V系列的朋友一份实用的参考。2. 核心模块解析与配置思路2.1 GD32VF103 DAC模块架构初探GD32VF103系列微控制器通常包含两个DAC通道DAC0和DAC1每个通道独立且结构相同。理解其内部架构是正确配置的前提。它的核心是一个12位的数字输入寄存器和一个12位的DAC数据保持寄存器。我们软件写入的数字值首先会放到数字输入寄存器里然后根据你选择的触发方式软件触发或外部事件触发这个值才会被转移到真正的DAC数据保持寄存器中DAC转换器基于保持寄存器里的值进行持续的模拟电压输出。这里第一个关键点就是“触发”。DAC提供了多种触发源软件触发SWTRIG写一个特定的寄存器位来启动转换。这是最简单直接的方式适合手动控制或低频更新。定时器触发例如TIMER2/3/4/5/6的TRGO事件。这是最常用的自动更新方式可以产生精确周期性的模拟波形如正弦波、三角波。外部中断线触发EXTI line9。可以由外部信号触发一次转换。选择哪种触发方式决定了你更新DAC输出数据的“节奏”。对于静态电压输出用软件触发一次写入即可对于动态波形必须依赖定时器触发。另一个重要部分是输出缓冲器Buffer。DAC内部集成了一个运算放大器作为输出缓冲默认是开启的。开启缓冲的好处是提高了驱动能力可以直接驱动一定的容性负载比如较长的导线输出阻抗很低。但缓冲器也会引入一定的失调误差和建立时间。在某些对精度和速度要求极高的场景或者负载是纯阻性且已知的情况下可以关闭缓冲器以获得更快的响应速度和更精确的零刻度输出。在GD32VF103上这个选择需要谨慎我后面会结合实测数据详细说。2.2 时钟与初始化流程设计和所有外设一样DAC需要时钟。在GD32VF103上DAC挂载在APB1总线下。因此第一步永远是使能对应的时钟。这里有个细节DAC模块的时钟使能位和GPIO的时钟使能是分开的你需要分别开启DAC时钟和对应输出引脚通常是PA4对应DAC0PA5对应DAC1的时钟。初始化流程可以概括为以下几步我建议按照这个顺序来避免出现奇怪的问题使能时钟开启RCU_APB1EN寄存器中的DACEN位以及RCU_APB2EN中对应GPIO端口的时钟如PA。配置GPIO将DAC输出引脚如PA4配置为模拟输入模式。是的你没看错是“模拟输入”模式。虽然它用作输出但在数据手册中明确要求配置为模拟模式以关闭内部上/下拉电阻和施密特触发器让引脚直接连接到内部的DAC模拟输出电路。使能DAC通道设置DAC_CTL寄存器中的DENx位x为0或1来使能对应的DAC通道。使能后DAC模块才开始工作但输出可能还未有效。配置触发方式通过DAC_CTL寄存器的DTSELx[2:0]位选择触发源。如果选择软件触发则置位SWTRx位即可触发一次转换如果选择定时器触发则需要配置好对应的定时器并使其能产生TRGO事件。配置输出缓冲通过DAC_CTL寄存器的DBOFFx位控制缓冲器开关。1为关闭0为开启默认。根据你的负载和性能需求决定。写入数据并触发将12位数据写入DAC_DHx寄存器右对齐然后根据触发方式执行触发操作写SWTRx或等待定时器事件。注意数据对齐方式很重要。GD32VF103的DAC支持12位右对齐数据格式。也就是说你需要把一个0-4095之间的整数直接写入到DAC_DH寄存器的低12位。不要尝试左对齐或8位对齐除非你非常清楚自己在做什么并且手册支持。3. 两种核心操作模式详解与代码实现3.1 模式一软件触发与静态电压输出这是最基础的模式适用于输出一个固定电压例如作为可编程的参考电压源。在这种模式下我们通过软件写入数据并立即触发转换。首先是完整的初始化函数示例。我以DAC通道0输出到PA4为例使用软件触发并开启输出缓冲。#include gd32vf103.h void dac_simple_init(void) { /* 1. 使能时钟 */ rcu_periph_clock_enable(RCU_GPIOA); // 使能GPIOA时钟 rcu_periph_clock_enable(RCU_DAC); // 使能DAC时钟 /* 2. 配置GPIO PA4 为模拟输入模式 */ gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_4); /* 3. 使能DAC通道0 */ dac_enable(DAC0); /* 4. 配置为软件触发不启用波形生成 */ dac_trigger_source_config(DAC0, DAC_TRIGGER_SOFTWARE); dac_trigger_enable(DAC0); // 使能触发功能对于软件触发也必须使能 /* 5. 开启输出缓冲 (默认就是开的这里显式配置一下) */ dac_output_buffer_enable(DAC0); /* 6. 初始化完成等待DAC稳定可选但建议 */ delay_1ms(1); // 简单延时等待电源稳定 }初始化之后输出一个特定电压就非常简单了。假设我们想输出一个中间电压即3.3V的一半1.65V。对应的数字值计算为1.65V / 3.3V * 4095 ≈ 2047。void dac_set_voltage(uint16_t value) { /* 确保值在12位范围内 */ if(value 4095) { value 4095; } /* 写入数据到DAC数据保持寄存器右对齐 */ dac_data_set(DAC0, DAC_ALIGN_12B_R, value); /* 软件触发一次转换 */ dac_software_trigger_enable(DAC0); } // 调用示例输出1.65V dac_set_voltage(2047);在这个模式下每次更新输出都需要调用一次dac_software_trigger_enable。如果你连续写入多个值而不触发只有最后一次触发生效。这种模式简单可靠但刷新率受限于你的代码执行速度和触发延迟不适合高速波形。3.2 模式二定时器触发与波形生成要产生连续的波形如正弦波、锯齿波必须使用自动触发。最常用的就是定时器触发。我们配置一个定时器以固定的频率产生更新事件TRGO这个事件会自动将DAC数据保持寄存器更新为下一个值从而实现“无人值守”的波形输出。这里的关键在于双缓冲机制。在定时器触发模式下我们通常使用DMA将波形数据表一个数组源源不断地搬运到DAC的数据寄存器中。DAC本身有一个“数据保持寄存器”我们最终写入的那个和一个“数据缓存寄存器”。当定时器触发事件到来时数据缓存寄存器中的值会自动载入到保持寄存器中进行转换同时如果DMA使能DMA会立即将下一个数据从内存写入到数据缓存寄存器等待下一次触发。这样就形成了一个流畅的流水线。下面是一个生成1kHz正弦波的示例步骤第一步准备波形数据表。我们生成一个周期正弦波的采样点数组。采样点越多波形越平滑但消耗的内存也越多。这里用一个周期的64个点。#define SINE_WAVE_POINTS 64 uint16_t sine_wave_table[SINE_WAVE_POINTS]; void generate_sine_wave_table(void) { for(int i 0; i SINE_WAVE_POINTS; i) { // 生成幅度为满量程一半2047带直流偏置2047的正弦波 // 公式值 幅度 * sin(2π*i/N) 偏置 float radian 2 * 3.1415926f * i / SINE_WAVE_POINTS; sine_wave_table[i] (uint16_t)(2047 * sinf(radian) 2047); // 确保值在0-4095范围内 if(sine_wave_table[i] 4095) sine_wave_table[i] 4095; } }第二步配置定时器。我们需要一个定时器以目标频率产生TRGO事件。目标频率 波形频率 * 每个周期的采样点数。对于1kHz正弦波64个点则定时器触发频率需要64kHz。void timer_for_dac_trigger_init(void) { // 假设使用TIMER2挂载在APB1时钟频率通常是108MHz/254MHz需根据系统时钟确认 rcu_periph_clock_enable(RCU_TIMER2); timer_deinit(TIMER2); // 定时器基础配置 timer_parameter_struct timer_initpara; timer_struct_para_init(timer_initpara); timer_initpara.prescaler 84 - 1; // 预分频54MHz / 84 642.857kHz timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_initpara.period 10 - 1; // 自动重载值642.857kHz / 10 64.2857kHz ≈ 64kHz timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_initpara.repetitioncounter 0; timer_init(TIMER2, timer_initpara); // 配置主模式输出将更新事件作为触发输出(TRGO) timer_master_output_trigger_source_select(TIMER2, TIMER_TRI_OUT_SRC_UPDATE); timer_enable(TIMER2); }计算过程系统核心时钟108MHzAPB1定时器时钟通常是54MHz。要得到64kHz的触发频率我们设置预分频和周期值。触发频率 定时器时钟 / (预分频1) / (周期1)。这里54MHz / 84 / 10 64.285kHz非常接近目标。第三步配置DAC为定时器触发模式并启用DMA。void dac_timer_trigger_init(void) { // ... 时钟和GPIO初始化与软件触发模式相同 ... dac_enable(DAC0); // 关键触发源选择TIMER2 TRGO事件 dac_trigger_source_config(DAC0, DAC_TRIGGER_T2_TRGO); dac_trigger_enable(DAC0); dac_output_buffer_enable(DAC0); // 根据需求选择 // 使能DAC通道0的DMA请求 dac_dma_enable(DAC0); }第四步配置DMA将内存中的波形数据自动搬运到DAC数据寄存器。void dma_for_dac_init(void) { rcu_periph_clock_enable(RCU_DMA0); // DAC DMA通常使用DMA0 dma_parameter_struct dma_init_struct; dma_deinit(DMA0, DMA_CH2); // GD32VF103中DAC通道0对应DMA0通道2需查数据手册确认 dma_struct_para_init(dma_init_struct); dma_init_struct.periph_addr (uint32_t)DAC0_R12DH; // 外设地址DAC0 12位右对齐数据保持寄存器 dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; // 外设地址不递增 dma_init_struct.memory_addr (uint32_t)sine_wave_table; // 内存源地址 dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; // 内存地址递增 dma_init_struct.number SINE_WAVE_POINTS; // 传输数据数量 dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_16BIT; // 外设数据宽度16位寄存器是32位但低16位有效 dma_init_struct.memory_width DMA_MEMORY_WIDTH_16BIT; // 内存数据宽度16位 dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init_struct.direction DMA_MEMORY_TO_PERIPH; // 传输方向内存到外设 dma_init_struct.circular_mode DMA_CIRCULAR_MODE_ENABLE; // **循环模式必须开启** 波形才能连续播放 dma_init(DMA0, DMA_CH2, dma_init_struct); dma_channel_enable(DMA0, DMA_CH2); }第五步启动整个系统。顺序很重要先启动DMA再使能DAC触发最后启动定时器。generate_sine_wave_table(); // 1. 生成波形表 dac_timer_trigger_init(); // 2. 初始化DAC触发模式 dma_for_dac_init(); // 3. 初始化并启动DMA timer_for_dac_trigger_init(); // 4. 初始化定时器此时还未使能 // 5. 启动定时器开始产生触发信号波形自动输出 timer_enable(TIMER2);一旦定时器开始运行DAC就会以64kHz的速率自动从sine_wave_table中读取数据并转换在PA4引脚上输出一个1kHz的模拟正弦波。整个过程无需CPU干预效率极高。4. 精度提升、噪声抑制与实战调试技巧4.1 理解并补偿DAC的误差来源即使代码正确实际输出的电压也可能与理论值有微小偏差。这些误差主要来自偏移误差当输入数字码为0时输出模拟电压不为0。GD32VF103的DAC在开启缓冲时这个误差通常很小几个mV但关闭缓冲后可能更明显。增益误差实际输出的满量程电压对应数字码4095与理想值如3.3V之间的偏差。积分非线性INL和微分非线性DNL这是DAC固有的线性度误差软件难以完全校正但好的PCB布局和供电可以将其影响最小化。对于要求不高的场合这些误差可以忽略。但如果需要较高精度可以进行简单的两点校准在代码中输出数字码0用高精度万用表测量实际电压V_zero。输出数字码4095测量实际电压V_full。那么对于任意目标电压V_target其对应的校准后数字码D_cal可以通过线性插值计算D_cal (V_target - V_zero) * 4095 / (V_full - V_zero)你可以将这个公式集成到你的dac_set_voltage函数中。4.2 输出缓冲的取舍与负载考量前面提到可以关闭输出缓冲dac_output_buffer_disable。关闭缓冲有什么利弊优点建立时间更快输出对数字变化的响应更迅速适合需要高速切换的场景。零刻度误差更小理论上输出能更接近0V。功耗略低。缺点驱动能力极弱输出阻抗很高通常为数十kΩ。这意味着它几乎不能直接驱动任何有电流需求的负载连一根长导线带来的寄生电容都可能导致信号严重失真。更容易受噪声干扰高阻抗节点对电磁干扰更敏感。实操心得除非你明确知道你的负载是一个高阻抗的运放同相输入端100kΩ并且对速度有苛刻要求否则强烈建议保持输出缓冲开启。在99%的应用中开启缓冲都是更稳妥、更可靠的选择。我曾为了追求“理论上的高速”关闭了缓冲去驱动一个简单的RC电路结果波形畸变严重折腾了半天才发现是驱动能力不足。开启缓冲后波形立刻干净漂亮。4.3 电源与PCB布局的噪声控制DAC是模拟电路对电源噪声非常敏感。即使代码完美一个糟糕的硬件设计也会毁掉输出质量。电源去耦必须在GD32VF103的VDD/VSS数字电源和VDDA/VSSA模拟电源引脚附近放置高质量的滤波电容。典型做法是每个电源引脚搭配一个0.1uF的陶瓷电容尽可能靠近芯片和一个1-10uF的钽电容或陶瓷电容。VDDA最好由LDO单独供电而不是直接从数字VDD取电。地平面良好的连续地平面至关重要。模拟地VSSA和数字地VSS应在芯片下方或附近单点连接。DAC输出走线应尽量短远离高频数字信号线如时钟、PWM、数据总线。如果必须长距离走线应考虑在输出端串联一个小的电阻如50-100Ω并接一个到地的滤波电容如几十pF这可以形成一个简单的低通滤波器并减少振铃但要注意与负载的相互作用。4.4 常见问题与排查实录在实际调试中你可能会遇到以下问题这里是我的排查清单现象可能原因排查步骤与解决方案无输出或输出为固定值1. 时钟未使能。2. GPIO模式配置错误未配为模拟输入。3. DAC通道未使能DEN位。4. 触发模式配置错误且未手动触发。1. 检查RCU寄存器确认DAC和GPIO时钟已开启。2. 用万用表测引脚电压或用逻辑分析仪看是否有数字信号模拟模式下应无数字信号。3. 确认dac_enable()已调用。4. 如果是软件触发检查是否调用了dac_software_trigger_enable()。输出电压值不对1. 参考电压VREF不准不是3.3V。2. 数据对齐方式错误如用了左对齐。3. 输出缓冲关闭且负载过重导致电压被拉低。1. 测量VDDA/VREF引脚的实际电压。2. 确认调用dac_data_set时第二个参数是DAC_ALIGN_12B_R。3. 尝试开启输出缓冲或断开负载测量空载电压。波形输出频率不对1. 定时器触发频率计算错误。2. 波形数据表点数与定时器频率不匹配。3. DMA未配置为循环模式。1. 重新计算定时器分频和重载值。用示波器测量定时器TRGO输出频率。2. 确认波形频率 定时器触发频率 / 波形表长度。3. 检查DMA初始化结构体中circular_mode是否设置为ENABLE。波形有毛刺或台阶1. 电源噪声大。2. 输出缓冲关闭抗干扰能力差。3. 代码中在DAC输出期间有高优先级中断或其它高速数字操作产生干扰。1. 用示波器查看VDDA和VSSA上的噪声加强电源滤波。2. 开启输出缓冲。3. 尝试在DAC输出关键时段关闭全局中断或优化代码减少数字噪声。DMA传输一次后停止DMA未配置循环模式。这是最常见的原因。确保dma_init_struct.circular_mode DMA_CIRCULAR_MODE_ENABLE;。一个特别的坑GD32VF103库函数版本。不同版本的GD32VF103固件库函数名或参数可能有细微差别。例如早期版本设置触发源的函数可能是dac_trigger_source_config而有些版本可能是dac_trigger_select。如果编译不通过或运行异常第一件事就是去查看你使用的gd32vf103_dac.h头文件中的函数原型确保调用正确。我建议以官方最新固件库的例程为基准进行开发。5. 进阶应用与性能压测思考5.1 双通道同步输出与相位控制GD32VF103有两个独立的DAC通道可以同时工作。一个有趣的应用是产生两路有固定相位差的同步波形。由于两个通道可以共享同一个定时器触发事件例如都使用TIMER2_TRGO因此它们的数据更新是严格同步的。要实现相位差关键在于两个通道的波形数据表。例如要产生一个正弦波和一个余弦波相位差90度你只需要准备两个数据表其中余弦波表就是正弦波表偏移四分之一周期长度的版本。然后分别为DAC0和DAC1配置DMA指向各自的数据表。当同一个定时器触发事件到来时两个DAC会同步更新为各自表中下一个点的值从而输出具有精确相位关系的两路信号。// 生成正弦和余弦表 for(int i 0; i TABLE_SIZE; i) { sine_table[i] 2047 2047 * sin(2*PI*i/TABLE_SIZE); cosine_table[i] 2047 2047 * cos(2*PI*i/TABLE_SIZE); // 余弦即相位提前90度的正弦 } // 分别配置DMA_CH2给DAC0sine_tableDMA_CH3给DAC1cosine_table5.2 动态更新波形表与任意波形生成在循环DMA模式下CPU和DMA可以并行工作。这意味着我们可以在DMA后台播放当前波形表的同时在内存中准备下一段波形数据然后在合适的时机例如一次DMA循环结束的中断里切换DMA的内存目标地址。这可以实现波形的动态切换、幅度调制或频率调制。GD32VF103的DMA支持双缓冲模式或通过手动重配置实现但更简单的方法是使用一个足够大的内存数组作为“环形缓冲区”DMA循环读取。CPU在一个“安全区域”比如DMA当前读指针后半圈写入新的波形数据。通过计算DMA的当前传输计数dma_transfer_number_get可以大致知道读指针位置避免读写冲突。这需要精细的编程但能实现非常灵活的实时信号生成。5.3 极限性能测试DAC的建立时间与更新率了解你手中DAC的性能极限很重要。GD32VF103的DAC在开启缓冲时建立时间输出达到目标值一定精度内所需时间典型值在几微秒量级。这意味着理论上它能稳定工作的最高更新率在几百kHz左右。你可以做一个测试配置定时器以最高频率触发比如1MHzDMA传输一个包含两个极值0和4095的短序列。用示波器观察输出方波。你会发现在过高频率下方波边沿会变斜顶部和底部可能不平坦这就是DAC和输出缓冲器建立时间跟不上更新速率的表现。找到波形开始明显失真的频率点那就是你这套配置下DAC可用的最高有效更新率。这个测试结果会受到供电质量、PCB布局、负载情况的影响它给你的是一个实际系统的性能边界而非数据手册上的理想值。最后再强调一个容易忽略的点仿真器的干扰。当你使用JTAG或SWD仿真器进行在线调试时仿真器本身可能会向目标板注入噪声影响DAC等模拟电路的输出质量。如果发现示波器上噪声在调试时变大断开仿真器让芯片独立运行程序再测试往往能得到更干净的结果。这提醒我们关键的性能测试最好在脱机运行状态下进行。