十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SAMD21 DAC编程指南:从基础电压输出到任意波形生成

SAMD21 DAC编程指南:从基础电压输出到任意波形生成 1. 从数字到模拟为什么我们需要DAC在嵌入式开发的世界里我们常常听到“数字”和“模拟”这两个词。我们的微控制器比如今天要聊的SAMD21它的大脑是一个纯粹的数字世界处理的是0和1。但现实世界是连续的、模拟的——声音的起伏、光线的强弱、温度的升降这些都是模拟信号。当我们想让微控制器去“影响”这个模拟世界时比如驱动一个扬声器发出声音或者控制一个电机的转速就需要一座桥梁把内部的数字指令转换成外部的模拟电压。这座桥梁就是数模转换器。你可能已经熟悉了ADC模数转换器它负责把外界的模拟信号“翻译”成微控制器能懂的数字信号。DAC则恰恰相反它把微控制器生成的数字代码“翻译”成模拟电压输出。对于SAMD21这类基于ARM Cortex-M0内核的微控制器来说内置一个DAC模块是相当实用的特性它意味着你不需要外接专门的DAC芯片就能直接生成一个可编程的模拟电压大大简化了电路设计和成本。在MicroPython的生态里直接操作硬件外设变得异常简单。今天我们就来深入聊聊如何在搭载了SAMD21芯片的开发板比如常见的Adafruit Metro M0 Express、Seeed Studio XIAO SAMD21等上使用MicroPython来驾驭其内置的DAC。我们会从原理入手一步步走到实际应用比如生成一个简单的直流电压、一个正弦波甚至探讨一下在生成方波时可能遇到的“过冲”与“下冲”现象。无论你是想做一个简易的信号发生器、一个音频播放器的原型还是需要精密的电压基准这篇手册都能给你提供清晰的路径。2. SAMD21 DAC硬件模块深度解析在动手写代码之前理解你手中的“武器”至关重要。SAMD21微控制器内置的DAC模块其核心是一个10位的数字模拟转换器。这里的“10位”是它的分辨率意味着它可以将一个数字值从0到1023转换成对应的模拟电压。如果DAC的参考电压VREF是3.3V那么每个数字步进LSB对应的电压变化量就是 3.3V / 1024 ≈ 3.22mV。这个精度对于许多应用来说已经足够了。SAMD21的DAC输出通道通常对应一个特定的引脚。例如在常见的板型定义中DAC0可能对应着PA02引脚。你需要查阅你所使用的具体开发板的原理图或引脚定义图来确认。这个引脚会直接输出0V到VREF之间的电压。除了基本的转换功能SAMD21的DAC还有一些值得注意的特性输出缓冲器DAC内部通常包含一个输出缓冲放大器。这个缓冲器可以提供一定的驱动能力降低输出阻抗使得DAC输出的电压更稳定不易受后续电路负载的影响。但缓冲器也可能引入额外的响应时间在某些极高频率的应用中需要考虑。参考电压选择DAC的参考电压VREF决定了输出电压的范围。它通常可以选择内部参考电压如1.0V或2.23V或外部通过VREFA引脚提供的电压。使用稳定的参考电压源是保证DAC输出精度的关键。转换速度DAC的转换速度决定了它能以多快的频率更新输出电压。这对于生成波形尤其是高频波形至关重要。SAMD21的DAC转换速度需要结合其时钟配置来看通常它足以应对音频频率范围几十Hz到几十kHz的信号生成。理解这些硬件特性能帮助我们在软件层面做出更合理的配置。例如当你需要最高精度时应确保参考电压稳定当你需要驱动一个低阻抗负载时要意识到输出电流可能有限可能需要外加运放进行缓冲。3. MicroPython中的DAC类接口与基本操作MicroPython为DAC功能提供了一个简洁的machine.DAC类。它的设计哲学是“简单易用”将复杂的硬件寄存器配置封装在底层我们只需要关注几个关键参数。让我们来看看如何创建一个DAC对象并输出一个电压。首先你需要导入machine模块from machine import DAC, Pin然后创建一个DAC对象。创建时需要指定使用哪个DAC通道对于SAMD21通常是通道0dac DAC(0) # 创建DAC对象使用通道0有些板子的引脚命名更直观你也可以通过引脚对象来创建dac_pin Pin(“A0”) # 假设A0引脚对应DAC输出 dac DAC(dac_pin)创建好对象后最直接的操作就是写入一个数字值。DAC的write方法接受一个0到1023之间的整数对于10位DACdac.write(512) # 输出中间电压假设VREF3.3V则输出约1.65V执行这行代码后你如果用万用表测量DAC输出引脚应该能看到一个接近1.65V的直流电压。这就是最基本的DAC操作数字写值模拟输出。注意dac.write()方法是一个阻塞操作吗实际上在MicroPython的标准实现中它通常是非阻塞的。调用write()后函数会立即返回而硬件DAC模块会在后台完成转换并输出电压。这意味着你可以快速连续地写入多个值来生成波形。但是写入的速度受到MicroPython解释器执行速度的限制对于非常高频的信号可能需要用到更底层的方法或直接内存访问DMA这在SAMD21的MicroPython中可能不直接支持需要自己开发C模块或使用特定固件。除了直接写值DAC类可能还支持其他方法比如deinit()用于关闭DAC以节省功耗。在实际使用前查阅你所使用的MicroPython固件版本的具体文档总是个好习惯。4. 实战演练一生成稳定的直流电压与三角波掌握了基本操作我们来做点更有趣的事情。第一个实战项目是生成一个可调的直流电压和一个简单的三角波。这能帮助我们验证DAC的基本功能和工作状态。4.1 生成可调直流电压我们可以写一个简单的循环让DAC输出电压阶梯式变化就像一个小型的数字电位器。from machine import DAC import time dac DAC(0) print(“DAC直流电压扫描开始...”) try: while True: for value in range(0, 1024, 64): # 从0到1023每次增加64 dac.write(value) print(f“写入值{value}, 理论电压{value * 3.3 / 1023:.2f}V”) time.sleep(0.5) # 暂停0.5秒方便观察 except KeyboardInterrupt: print(“\n程序被用户中断”) finally: dac.write(0) # 程序结束时将输出置零 print(“DAC输出已归零”)将这段代码上传到板子并运行同时用万用表测量DAC输出引脚。你应该能看到电压在大约16个阶梯上逐步上升每步间隔约0.2V3.3V/16。这是一个非常直观的验证方法。如果发现电压不准确或跳变不稳定可能需要检查电源电压是否稳定或者负载是否过重。4.2 生成三角波三角波是测试DAC动态性能的好工具。它要求DAC能够线性、连续地改变输出。from machine import DAC import time dac DAC(0) delay_ms 10 # 每个点的延时单位毫秒控制波形频率 print(“生成三角波...”) try: while True: # 上升沿 for value in range(0, 1024, 8): # 步进为8平衡速度和光滑度 dac.write(value) time.sleep_ms(delay_ms) # 下降沿 for value in range(1023, -1, -8): dac.write(value) time.sleep_ms(delay_ms) except KeyboardInterrupt: print(“\n三角波生成停止”) dac.write(0)这里有几个关键点步进值8我们选择8而不是1。如果每次步进1一个完整的三角波周期需要写2046次结合延时频率会非常低。步进8牺牲了一些波形的分辨率从1024点降到128点但显著提高了可达到的频率。你可以根据需求调整这个值。延时控制频率delay_ms变量和步进值共同决定了波形的周期和频率。频率 ≈ 1 / (周期时间)。周期时间 (上升点数 下降点数) * 延时。在这个例子里周期约等于 (128128)*0.01秒 2.56秒频率约0.39Hz。这显然很慢因为time.sleep_ms()和MicroPython循环本身有较大开销。性能瓶颈用纯MicroPython循环生成波形频率上限很低通常不超过几百赫兹。对于更高频率的波形我们需要不同的策略这将在后面讨论。用示波器连接DAC输出你应该能看到一个锯齿状的三角波。如果示波器有测量功能可以观察其频率和幅度是否与计算值相符。5. 实战演练二合成正弦波与应对“过冲”和“下冲”生成正弦波是DAC的经典应用比如用于音频测试或简单的音乐合成。同时当我们尝试生成方波时可能会观察到输出并非理想的垂直跳变而是存在“过冲”和“下冲”的振荡现象。我们来一起探索这两个问题。5.1 正弦波查表法直接计算每个点的正弦值在MCU上比较耗时。更高效的方法是使用“查表法”即预先计算好一个正弦周期内的所有样本值存储在一个列表中然后循环输出这些值。from machine import DAC import math import time dac DAC(0) # 生成一个正弦波表 SAMPLE_COUNT 100 # 一个周期的采样点数 sin_table [] for i in range(SAMPLE_COUNT): # 计算正弦值范围[-1, 1] radian 2 * math.pi * i / SAMPLE_COUNT sin_value math.sin(radian) # 将正弦值映射到DAC范围[0, 1023]并取整 dac_value int((sin_value 1) / 2 * 1023) sin_table.append(dac_value) print(“正弦波表生成完毕开始输出...”) delay_us 2000 # 每个样本点的延时单位微秒 try: while True: for sample in sin_table: dac.write(sample) time.sleep_us(delay_us) # 使用微秒延时可以获得更精确的定时 except KeyboardInterrupt: print(“\n正弦波输出停止”) dac.write(512) # 回到中点电压采样点数SAMPLE_COUNT点数越多生成的波形越光滑但需要的存储空间越大输出一个完整周期的时间也越长。100个点对于低频波形已经足够光滑。延时控制delay_us这里使用了time.sleep_us()以获得更精细的定时控制。输出波形的频率 1 / (SAMPLE_COUNT * delay_us)。例如100点 * 2000us 200,000us 0.2秒周期即5Hz。这仍然不高原因同上。映射计算正弦函数输出在[-1,1]而DAC需要[0,1023]。(sin_value 1) / 2将范围变换到[0,1]再乘以1023并取整。5.2 理解“过冲”与“下冲”当你试图用DAC生成一个方波比如在0和1023之间快速切换时在示波器上可能会看到电压跳变的边缘不是干净的直线而是出现了向上突起过冲Overshoot和向下凹陷下冲Undershoot的振荡然后才稳定到目标电压。这通常不是DAC芯片本身的问题而是由输出电路的寄生电感和电容引起的。成因DAC的输出引脚、连接线、示波器探头甚至PCB走线本身都存在微小的寄生电感和电容。当DAC的输出电压试图快速变化时即输出电流急剧变化这些电感和电容会形成一个LC谐振电路导致电压在跳变瞬间发生振荡。DAC输出缓冲器的影响内置的输出缓冲运算放大器有其自身的压摆率Slew Rate和带宽限制。如果电压变化速度要求超过了运放的能力也会导致波形失真边缘变圆滑但在SAMD21的音频频率范围内更主要的往往是LC谐振。如何缓解降低跳变速度不要试图瞬间从0跳到1023。可以插入几个中间值形成一个微小的斜坡。这相当于降低了信号的高频分量从而减轻振荡。在输出端添加一个小电阻在DAC输出引脚串联一个几十到几百欧姆的电阻然后再连接到负载或示波器探头。这个电阻可以增加阻尼消耗振荡能量。这是最常用且有效的硬件方法。优化PCB布局和探头尽量缩短走线使用接地良好的探头并选择正确的探头档位如10X档位其输入电容更小。在MicroPython中我们可以通过软件方法来模拟“降低跳变速度”def write_smooth(dac_obj, target_value, steps10, delay_us50): 平滑写入DAC值减少过冲 current dac_obj.read() # 注意并非所有DAC实现都支持.read()可能需要自己记录当前值 step (target_value - current) / steps for i in range(steps): dac_obj.write(int(current step * i)) time.sleep_us(delay_us) dac_obj.write(target_value) # 确保最终值准确重要提示DAC.read()方法在标准MicroPython中可能不存在。你需要自己维护一个变量来记录当前写入的DAC值。这个平滑函数会显著降低方波的边缘速度牺牲了上升/下降时间换来了更干净的波形。在实际应用中需要根据示波器观测结果来调整steps和delay_us参数在速度和稳定性之间取得平衡。6. 提升性能超越time.sleep的波形生成前面的例子揭示了用time.sleep控制时序的根本性瓶颈它依赖于MicroPython解释器循环精度差、开销大且会被其他中断打断。要生成更高频率、更稳定的波形我们必须寻求其他方法。6.1 使用定时器中断驱动MicroPython的machine.Timer类可以配置一个硬件定时器以固定的时间间隔触发中断。在中断服务程序ISR中更新DAC值可以大大提高时序精度。from machine import DAC, Timer import array # 准备波形数据 SAMPLE_COUNT 200 sin_table array.array(‘H‘, [0] * SAMPLE_COUNT) # ‘H‘ 表示无符号短整型 for i in range(SAMPLE_COUNT): sin_table[i] int((math.sin(2 * math.pi * i / SAMPLE_COUNT) 1) / 2 * 1023) dac DAC(0) sample_index 0 def update_dac(timer): global sample_index dac.write(sin_table[sample_index]) sample_index (sample_index 1) % SAMPLE_COUNT # 创建定时器模式为周期性回调频率为5000Hz (即每200us触发一次) # 波形频率 定时器频率 / 采样点数 5000 / 200 25 Hz tim Timer(period200, modeTimer.PERIODIC, callbackupdate_dac) # period单位可能为毫秒需看具体端口实现 print(“定时器驱动DAC输出中...按CtrlC停止”) try: while True: time.sleep(1) # 主循环空转所有工作由中断完成 except KeyboardInterrupt: tim.deinit() # 停止定时器 dac.write(512) print(“波形输出已停止”)关键改进精度定时器由硬件控制触发间隔非常精确不受主循环负载影响。稳定性只要中断服务程序update_dac的执行时间短于定时器周期波形就能稳定输出。频率上限这种方法能实现的频率远高于纯循环。上限取决于1) 定时器的最小周期2) ISR的执行时间。对于SAMD21生成几千赫兹的音频波形是可行的。注意事项在中断服务程序中代码必须尽可能短小高效。避免在ISR内进行浮点运算、内存分配如创建新列表或调用可能阻塞的函数如print。这里我们使用了预计算的arraydac.write()也是快速操作。6.2 探索极限直接内存访问与高级技巧对于需要极高数据吞吐量的应用例如高质量音频播放即使是定时器中断也可能因为中断响应延迟和上下文切换开销而显得不足。更高级的方法是使用DMA直接内存访问。DMA控制器可以在不占用CPU的情况下自动将内存中的波形数据搬运到DAC的数据寄存器。CPU只需要在DMA传输开始前配置好之后就可以去处理其他任务DMA会在后台持续、高速地更新DAC输出。然而在标准的MicroPython固件中通常没有提供对DMA操作的直接Python接口。这属于更底层的硬件操作。要实现它通常有几种途径寻找或定制专用固件有些社区维护的MicroPython分支可能为特定板子添加了DAC DMA功能。编写C语言模块为MicroPython编写一个用C实现的扩展模块该模块直接配置SAMD21的DAC和DMA硬件。这是最灵活但也是最复杂的方式需要对SAMD21的硬件手册和MicroPython内部机制有深入了解。使用第三方库或框架如果存在的话。对于大多数MicroPython用户和应用场景使用定时器中断驱动DAC已经能很好地满足需求。只有在追求极低抖动、极高频率或极低CPU占用率的专业音频/信号生成应用中才需要考虑DMA方案。7. 综合应用构建一个简易的任意波形发生器原型将前面学到的知识组合起来我们可以尝试设计一个简易的任意波形发生器AWG原型。这个原型允许用户通过串口命令选择波形类型正弦、三角、方波和频率并在DAC上输出。7.1 系统设计思路波形表为每种波形预计算一个周期的样本值存储在内存中。定时器驱动使用一个定时器以固定频率采样率触发中断。中断服务程序在ISR中从当前激活的波形表中读取下一个样本写入DAC。用一个索引变量循环遍历波形表。用户接口通过UART串口接收简单的文本命令例如sin 100表示输出100Hz正弦波。主循环解析命令然后动态计算所需的定时器周期并更换波形表。7.2 核心代码框架from machine import DAC, Timer, UART import array import math # 硬件初始化 dac DAC(0) uart UART(0, baudrate115200) # 使用默认UART波特率115200 # 全局变量 current_waveform None sample_index 0 samples_per_cycle 200 output_freq 100 # 默认频率 sampling_rate 10000 # 采样率10kHz # 生成波形表 def generate_wave_table(wave_type, num_samples): table array.array(‘H‘, [0] * num_samples) if wave_type ‘sin‘: for i in range(num_samples): table[i] int((math.sin(2 * math.pi * i / num_samples) 1) / 2 * 1023) elif wave_type ‘triangle‘: half num_samples // 2 for i in range(half): table[i] int(i / half * 1023) table[i half] int(1023 - (i / half * 1023)) elif wave_type ‘square‘: for i in range(num_samples): table[i] 1023 if i num_samples // 2 else 0 else: # 直流 table array.array(‘H‘, [512] * num_samples) return table # 初始化一个默认波形 current_waveform generate_wave_table(‘sin‘, samples_per_cycle) # 定时器中断服务程序 def dac_update_isr(timer): global sample_index dac.write(current_waveform[sample_index]) sample_index (sample_index 1) % len(current_waveform) # 配置定时器 # 定时器周期 1 / 采样率。例如10kHz采样率周期为100微秒。 # 注意Timer的period参数单位需查阅具体端口文档可能是毫秒(ms)。 tim_period int(1_000_000 / sampling_rate) # 计算微秒数 tim Timer(periodtim_period, modeTimer.PERIODIC, callbackdac_update_isr) def set_waveform(freq, wave_type): 根据频率和波形类型重新配置系统 global sampling_rate, samples_per_cycle, current_waveform, tim # 1. 计算所需的采样率。根据奈奎斯特定理采样率至少是信号频率的2倍这里取10倍以上以保证波形质量。 required_sampling_rate freq * 20 # 使用一个固定的、足够高的采样率例如10kHz通过改变一个周期内的采样点数来改变频率。 # 输出频率 采样率 / 每周期采样点数 new_samples_per_cycle int(sampling_rate / freq) if new_samples_per_cycle 4: # 防止点数过少 new_samples_per_cycle 4 elif new_samples_per_cycle 1000: # 防止点数过多占用内存 new_samples_per_cycle 1000 # 2. 生成新的波形表 current_waveform generate_wave_table(wave_type, new_samples_per_cycle) print(f“波形设置{wave_type}, 目标频率{freq}Hz, 每周期点数{new_samples_per_cycle}“) # 主循环解析串口命令 print(“简易AWG就绪。输入命令如 ‘sin 100‘ 或 ‘square 50‘“) set_waveform(output_freq, ‘sin‘) # 初始化为100Hz正弦波 try: while True: if uart.any(): cmd uart.readline().decode(‘utf-8‘).strip().lower() parts cmd.split() if len(parts) 2: wave_type, freq_str parts try: freq float(freq_str) if 0.1 freq 2000: # 频率范围限制 set_waveform(freq, wave_type) else: print(“频率超出范围 (0.1-2000 Hz)“) except ValueError: print(“无效的频率格式”) else: print(“无效命令格式波形 频率 波形可选 sin, triangle, square“) time.sleep(0.1) except KeyboardInterrupt: tim.deinit() dac.write(512) print(“AWG已关闭”)7.3 潜在问题与优化方向这个原型还有很多可以完善的地方频率精度由于samples_per_cycle必须是整数实际输出频率sampling_rate / samples_per_cycle可能与用户设定的频率有微小偏差。对于要求精确频率的应用需要采用更复杂的定时器分频和DMA缓冲机制。波形切换毛刺在切换波形表的瞬间如果中断恰好在更新索引可能导致输出不连续。更安全的方法是在ISR中使用一个指向波形表的指针主程序只更新这个指针而不是直接替换数组。内存占用为每种波形和不同频率都生成表会占用大量内存。可以只存储一个高分辨率的基波表在ISR中使用相位累加器和插值算法来动态计算样本值这是专业DDS直接数字频率合成技术的思路但对计算能力要求较高。命令扩展可以增加设置幅度、偏置的命令。尽管如此这个原型已经成功地整合了DAC操作、定时器中断和串口通信构成了一个功能完整的嵌入式系统雏形。通过这个项目你不仅能掌握SAMD21 DAC的编程更能理解如何将不同的MicroPython模块组合起来解决实际问题。
返回列表