尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

GD32VF103 RISC-V MCU的DAC实战:从静态电压到动态波形生成

GD32VF103 RISC-V MCU的DAC实战:从静态电压到动态波形生成 1. 项目缘起为什么要在RISC-V MCU上折腾DAC最近在捣鼓一块基于GD32VF103的国产RISC-V开发板项目里有个需求需要生成一个可编程的模拟电压信号。这让我把目光投向了这颗芯片内置的DAC数模转换器模块。说实话在嵌入式开发里DAC算是个“熟悉的陌生人”。我们天天跟ADC模数转换器打交道读取传感器电压但轮到要MCU自己“发声”、输出一个精确的电压时DAC的使用频率似乎就没那么高了。尤其是在资源相对紧凑的微控制器上像GD32VF103这种它的DAC功能到底实不实用精度如何会不会有坑这就是我这次想通过实际项目来摸清楚的事情。GD32VF103作为兆易创新基于RISC-V内核的明星产品其外设资源对标经典的ARM Cortex-M3/M4系列。它内置了两个12位的DAC通道支持多种触发方式理论上能满足音频生成、波形合成、电压基准等常见需求。但理论归理论从寄存器配置到稳定输出一个干净的电压中间还有不少细节需要踩实。网上关于GD32VF103的DAC资料比起它的ADC或者通用定时器要零散得多这也促使我决定把整个探索过程记录下来从环境搭建、原理理解、到代码实现和实测验证形成一个完整的参考。2. DAC基础与GD32VF103的硬件实况在深入代码之前我们得先搞清楚两件事DAC到底是怎么工作的以及GD32VF103为它提供了什么样的硬件舞台。2.1 数模转换的核心从数字码到模拟电压DAC的任务很明确把一个数字代码比如一个12位的整数转换成对应的模拟电压。对于最常见的电压输出型DAC其核心是一个电阻网络如R-2R梯形网络或电容阵列。GD32VF103采用的是电阻串型DAC这是一种在微控制器中非常常见的结构。它的工作原理可以简单理解芯片内部有一个高精度的参考电压源Vref。这个电压被一个精密电阻串分成了许多份对于12位DAC就是2^124096份。当你写入一个12位数字值比如0x800十进制2048到DAC的数据保持寄存器时内部开关网络会根据这个值接通电阻串上对应的抽头从而输出一个比例于该数字值的电压。计算公式非常直观Vout (DOR / 4096) * Vref其中DOR是你写入的数字值0-4095Vref是参考电压。在GD32VF103上DAC的参考电压通常与ADC共享可以选为VDDA模拟电源电压通常是3.3V或者一个内部参考电压如果芯片支持。这意味着你的输出精度和稳定度直接依赖于Vref的质量。2.2 GD32VF103的DAC外设探秘查阅GD32VF103的用户手册我们可以勾勒出它DAC模块的“能力画像”双通道独立拥有DAC0和DAC1两个完全独立的转换通道可以同时工作输出两路不同的电压。12位分辨率数字输入范围为0x000到0xFFF0-4095。注意虽然数据宽度是12位但其硬件架构保证了单调性即数字输入增加模拟输出也一定增加不会出现“倒退”这对于闭环控制很重要。多种触发源DAC转换可以由软件触发写一个寄存器位也可以由硬件事件触发这是它最强大的功能之一。支持的硬件触发源包括定时器TIMERx的TRGO事件。外部中断线。软件触发。 这意味着你可以用定时器来精确控制DAC输出更新的时刻从而实现精确的波形生成比如用定时器更新事件来驱动DAC就能产生固定频率的锯齿波或正弦波。输出缓冲器每个DAC通道都集成了一个输出缓冲放大器。这个缓冲器的作用是降低输出阻抗提高带负载能力可以直接驱动一些轻负载。但这里有一个至关重要的细节启用输出缓冲器会引入一定的偏移误差和非线性并且限制了输出电压的摆幅通常无法达到真正的0V和Vref。对于需要高精度或满幅输出的应用可能需要禁用缓冲器但此时你必须确保外部负载阻抗足够大通常在几十kΩ以上否则会严重影响精度。数据格式支持12位右对齐、12位左对齐以及8位右对齐高8位有效的数据格式方便与不同位宽的数据总线对接。DMA支持两个通道都支持DMA。这是实现高速、连续波形输出的关键。你可以预先在内存中存放一个波形表比如一个正弦波的采样值数组然后通过DMA自动、不停歇地将这些数据搬运到DAC的数据寄存器中配合定时器触发MCU内核几乎不用干预就能输出复杂的模拟信号。了解这些硬件特性是我们进行正确配置和避开潜在陷阱的基础。例如如果你发现输出电压总是到不了3.3V检查一下输出缓冲器是否被启用可能就是问题的关键。3. 开发环境搭建与基础驱动剖析工欲善其事必先利其器。在RISC-V生态下玩转GD32VF103环境搭建和库函数理解是第一步。3.1 工具链与工程模板选择目前针对GD32V系列最主流和官方的开发环境是芯来科技Nuclei提供的IDE或基于Eclipse的解决方案以及使用PlatformIO插件。我个人更倾向于在VSCode PlatformIO环境下开发因为它跨平台包管理方便。首先你需要在PlatformIO中安装“Nuclei”平台。PlatformIO会自动下载所需的RISC-V GCC工具链、OpenOCD调试器以及GD32VF103的SDKSoftware Development Kit。这个SDK包含了芯片的所有外设驱动库通常以gd32vf103_为前缀的头文件和源文件、启动文件和一些例程是我们编程的基础。创建一个新的PlatformIO项目选择Board为“GD32VF103C8T6”根据你的具体型号选择Framework选择“GD32VF103 Firmware Library”。这样一个包含基本驱动库和编译配置的项目骨架就生成了。3.2 解读DAC驱动库的关键函数GD32VF103的SDK提供了相对完善的DAC驱动API。我们不需要从零开始操作寄存器但必须理解这些API背后的逻辑。几个核心函数如下dac_deinit(): 将DAC寄存器复位为默认值。在初始化任何外设前这是一个好习惯。dac_trigger_source_config(uint32_t dac_periph, uint32_t triggersource): 配置触发源。这是关键配置之一。dac_periph: 指定通道如DAC0。triggersource: 选择触发源例如DAC_TRIGGER_T1_TRGO定时器1触发、DAC_TRIGGER_SOFTWARE软件触发。dac_trigger_enable(uint32_t dac_periph): 使能所选触发模式。调用此函数后DAC将等待触发事件来更新输出。dac_output_buffer_enable(uint32_t dac_periph)/dac_output_buffer_disable(...): 启用或禁用输出缓冲器。根据前文分析的场景谨慎选择。dac_data_set(uint32_t dac_periph, uint32_t dac_align, uint32_t data): 设置要转换的数据。dac_align: 数据对齐方式如DAC_ALIGN_12B_R12位右对齐。这里极易出错如果你选择12位右对齐那么data参数的低12位才是有效值你需要确保传入的值在0-4095之间。如果你传入一个16位变量需要先进行掩码操作data 0xFFF。dac_software_trigger_enable(uint32_t dac_periph): 产生一个软件触发事件。当配置为软件触发时调用此函数DAC会立即将数据寄存器DACx_DH的值搬移到数据输出寄存器DACx_DO并开始转换。一个常见的初始化流程伪代码如下// 1. 使能DAC和对应GPIO时钟 rcu_periph_clock_enable(RCU_DAC0); rcu_periph_clock_enable(RCU_GPIOA); // 假设DAC0在PA4 // 2. 配置GPIO为模拟模式至关重要 gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_4); // 3. 初始化DAC dac_deinit(); dac_trigger_source_config(DAC0, DAC_TRIGGER_SOFTWARE); // 先配置为软件触发 dac_trigger_enable(DAC0); // 使能触发 dac_output_buffer_disable(DAC0); // 根据需求选择是否禁用缓冲 dac_enable(DAC0); // 最后使能DAC模块 // 4. 设置数据并触发转换 dac_data_set(DAC0, DAC_ALIGN_12B_R, 2048); // 设置中间值 dac_software_trigger_enable(DAC0); // 软件触发输出电压注意GPIO必须配置为模拟输入模式GPIO_MODE_AIN。如果错误地配置为上拉/下拉或推挽输出不仅无法输出正确电压还可能损坏IO口或导致功耗异常。4. 从静态电压到动态波形实战应用模式拆解掌握了基础输出后我们可以玩些更高级的。DAC的应用场景很大程度上取决于其触发和更新方式。4.1 模式一静态电压基准输出这是最简单的模式。配置为软件触发设置一次数据并触发一次DAC就会持续输出对应的直流电压直到你写入新的数据并再次触发。这种模式常用于提供可编程的参考电压。控制压控器件如VCO的偏置。在调试中手动设定一个测试电压点。实操心得在这种模式下如果你发现输出电压有微小波动或噪声除了检查电源和地线布局外可以尝试在DAC输出引脚到地之间并联一个10nF到100nF的陶瓷电容这可以滤除来自芯片内部开关噪声的高频成分。4.2 模式二定时器触发的波形生成这是DAC的“主力”应用模式。通过将DAC的触发源配置为某个定时器的TRGO事件我们可以实现周期性的、精确定时的电压更新。场景生成一个1kHz的锯齿波。配置定时器以系统时钟72MHz为例配置一个基本定时器如TIMER5为向上计数模式自动重装载值ARR设置为72000000 / (1000 * 4096) ≈ 17。这里1000是频率1kHz4096是锯齿波一个周期需要的点数从0到4095。这样定时器每计数17次产生一次更新事件UEV频率就是72MHz / 17 ≈ 4.24MHz再除以4096约等于1.035kHz接近目标。配置DAC触发将DAC0触发源设置为DAC_TRIGGER_T5_TRGO。编写更新逻辑在定时器更新中断服务函数ISR中递增一个计数器从0到4095并将该值写入DAC的数据寄存器。注意在定时器触发模式下你只需要写数据到DACx_DH寄存器通过dac_data_set函数硬件会在下一个触发边沿自动将其转移到DACx_DO并转换无需在中断里调用软件触发函数。使能与启动使能DAC和定时器。这样一个粗糙的锯齿波就产生了。但这里有个问题在1kHz下每个点停留时间极短约1us用中断来更新数据中断开销可能成为瓶颈且会产生时间抖动jitter影响波形质量。4.3 模式三DMA驱动的“无核”波形输出为了解决中断模式的瓶颈DMA是终极方案。在这种模式下CPU只需要初始化好一切然后就可以“睡觉”了波形数据由DMA自动搬运。实现步骤准备波形表在内存中比如一个全局数组预先计算好一个完整周期的波形数据。例如一个12位、100个点的正弦波表。#define WAVE_TABLE_SIZE 100 uint16_t sine_wave_table[WAVE_TABLE_SIZE]; for(int i0; iWAVE_TABLE_SIZE; i) { sine_wave_table[i] (uint16_t)(2048 2048 * sin(2 * 3.1415926 * i / WAVE_TABLE_SIZE)); // 生成幅值为2048偏置为2048的正弦波 }配置DMA将DMA通道配置为从内存波形表地址到外设DAC0的数据寄存器地址DAC0_R12DH_ADDRESS的传输。设置传输数据宽度为半字16位循环模式每次定时器触发就传输一个数据。配置定时器触发同上定时器产生固定频率的更新事件作为TRGO。关联DAC与DMA配置DAC使其在定时器触发下工作并启用DAC的DMA请求。启动启动DMA启动定时器启动DAC。此时DMA会周而复始地将波形表的数据搬运给DACDAC则在每个定时器触发时刻更新输出。输出波形的频率 定时器触发频率 / 波形表长度。这种方式输出的波形极其稳定时间抖动极小是生成高质量音频或信号的首选。踩坑记录在配置DMA时务必注意外设地址DAC0_R12DH_ADDRESS是DAC数据保持寄存器的地址而不是随便一个DAC寄存器地址。写错地址会导致数据无法正确送达DAC。另外确保DMA传输的数据对齐方式与DAC数据寄存器要求的一致通常是右对齐。5. 精度校准与噪声应对实战当你兴冲冲地接上示波器或万用表可能会发现输出的电压和理论值有偏差或者波形上叠加了毛刺。别急这是模拟电路的常态我们需要一些技巧来优化。5.1 精度问题偏移与增益误差即使是一个12位的DAC其实际输出也可能存在两种主要误差偏移误差当数字输入为0时模拟输出不为0V。增益误差实际输出的满量程电压输入4095时与理论值Vref不符。GD32VF103的DAC出厂时没有提供像ADC那样的内置校准值。因此如果需要高精度必须进行手动校准。手动校准方法准备工作确保你的Vref通常是VDDA是稳定且精确的。使用一个外部精密基准源或至少是一个好的LDO给VDDA供电。测量工具万用表的精度要高于你的目标精度。测量偏移将DAC数字输入设置为0禁用输出缓冲以获得更真实的零电平如果应用允许测量实际输出电压Vos。测量满量程将DAC数字输入设置为4095测量实际输出电压Vfs。软件补偿在软件中建立一个补偿模型。最常用的是两点校准法。在输出函数中对你要设置的数字值D_in进行修正// 假设测得的 Vos 0.002V, Vfs 3.298V理论Vref3.300V float actual_scale (Vfs - Vos) / 3.300; // 实际增益系数 uint32_t D_corrected (uint32_t)( (D_in / 4095.0f) * actual_scale * 4095 (Vos / 3.300) * 4095 ); // 然后将 D_corrected 写入DAC dac_data_set(DAC0, DAC_ALIGN_12B_R, D_corrected 0xFFF);通过这种方式可以在软件层面大幅消除系统的偏移和增益误差。5.2 噪声问题电源、地线与滤波DAC输出中的噪声主要来源于电源噪声数字电路部分的开关噪声通过电源耦合到模拟部分。数字开关噪声DAC内部的电阻网络开关动作本身会产生毛刺Glitch尤其在数字码发生大幅跳变时如从0x7FF跳到0x800。PCB布局噪声模拟走线与数字走线、时钟线平行或交叉。应对策略电源隔离确保VDDA模拟电源和VSSA模拟地通过磁珠或0Ω电阻从主电源VDD和GND单独引出并在靠近芯片引脚处放置一个10μF的钽电容和一个100nF的陶瓷电容进行去耦。输出滤波在DAC输出引脚串联一个小的电阻如100Ω然后对地接一个电容如1nF~100nF形成一个简单的RC低通滤波器。这个滤波器的截止频率需要根据你的信号频率来选择在滤除高频噪声的同时不能过度衰减你的有用信号。硬件去毛刺对于要求极高的应用可以在DAC输出后接一个“去毛刺”电路通常是一个采样保持电路仅在DAC转换完成且稳定后才将电压输出到后级。但在大多数MCU应用中通过合理的PCB布局和滤波已能满足要求。软件平滑如果输出的是缓慢变化的信号可以通过软件对要输出的数字值进行滑动平均滤波避免突然的大幅跳变也能减少毛刺的影响。6. 进阶话题双通道同步与音频应用遐想GD32VF103的双DAC通道可以独立工作但能否同步在某些场景下比如生成立体声音频或者需要两个有严格相位关系的信号时同步性很重要。6.1 实现硬件同步触发虽然两个DAC通道是独立的但你可以让它们共享同一个硬件触发源。例如将DAC0和DAC1的触发源都配置为DAC_TRIGGER_T2_TRGO。这样当定时器2的TRGO事件发生时两个DAC会同时锁存各自数据寄存器中的新值并开始转换。这就实现了硬件的同步更新。配置要点两个DAC通道使用相同的触发源配置。确保在触发事件发生前两个通道的数据寄存器都已写入新值。如果使用DMA需要为两个通道分别配置DMA流如果支持或者使用一个支持双目标地址的DMAGD32VF103可能不支持需查手册或者采用“乒乓”缓冲等复杂策略。更简单的做法是如果数据更新不快可以在定时器中断中同时更新两个通道的数据寄存器。6.2 构建一个简易的音频播放器结合DMA、双通道DAC和定时器我们可以尝试在GD32VF103上实现一个简易的立体声WAV播放器低质量。思路如下音频数据准备将一段单声道或立体声的PCM WAV文件例如8kHz采样率8位或16位量化转换成两个数组左声道、右声道并缩放到12位DAC的范围内。DMA配置这可能是最复杂的部分。GD32VF103的DMA可能不支持轻松地将交织的立体声数据L,R,L,R...自动分离到两个外设地址。一种可行但效率较低的方案是将音频数据预处理成两个独立的数组左声道数组和右声道数组。使用两个DMA通道分别服务于DAC0和DAC1。配置两个相同的定时器或一个定时器产生两个同步的触发事件分别触发两个DMA。这样左声道数据通过DMA0送到DAC0右声道通过DMA1送到DAC1在硬件触发下同步播放。输出滤波与放大DAC输出的信号需要经过一个低通滤波器重建滤波器来平滑阶梯状波形滤除奈奎斯特频率以上的高频分量。然后可能需要一个运算放大器进行放大以驱动耳机或扬声器。这个项目对MCU的内存存放音频数据、DMA能力和定时器精度都有一定要求但它完美地展示了GD32VF103 DAC模块在复杂应用中的潜力。虽然受限于主频和内存它无法播放高质量音乐但用于提示音、简单音效或低频信号发生是完全可行的。通过这一系列从理论到实践从静态到动态从基础到进阶的探索我们可以看到GD32VF103的DAC模块虽然不如一些高端专用DAC芯片强大但其功能完整、配置灵活在嵌入式系统的模拟输出领域足以胜任多数角色。关键在于理解其硬件特性善用触发和DMA机制并注意模拟电路设计中的细节。希望这份详尽的踩坑与实战记录能帮助你在下一个RISC-V项目中让DAC乖乖地输出你想要的每一个电压。
返回列表