MCP4725+MicroPython波形发生器:I2C定时器驱动与调试详解 📅 发布时间:2026/9/11 20:17:10 👁 浏览次数: MCP4725 这颗 12 位 DAC 在嵌入式圈子里算是老熟人了。最近帮朋友调一个基于 MicroPython 的波形发生器项目正好把这块芯片从寄存器配置到定时器驱动的完整流程走了一遍顺带把调试过程中踩的坑也整理了出来。如果你正打算用 MCP4725 做信号源、音频输出或者自动化测试设备这篇内容可以帮你节约至少一个下午的查资料时间。我会直接拆解代码实现、硬件连接、定时器中断的用法以及最后怎么在示波器上把波形调干净。1. 为什么选 MCP4725 做波形发生器1.1 这颗 DAC 芯片的核心优势MCP4725 是 Microchip 出品的一款单通道 12 位 DACI2C 接口内置 EEPROM。对于波形发生器这个场景来说它有几个非常讨喜的特质。首先是分辨率足够用。12 位分辨率意味着输出端有 4096 个离散电压等级对于常见的 5V 或 3.3V 参考电压理论最小步进可以做到 0.8mV 左右。这个精度用于生成音频范围的信号、传感器激励信号或者频率不高的控制信号完全够用。其次是它内置了输出缓冲放大器可以直接驱动一定负载省去了外部运放跟随的麻烦对面包板原型验证特别友好。还有一个容易被忽略的点它内置了上电复位后从 EEPROM 加载输出代码的功能。也就是说你可以把某个预设电压写入 EEPROM下次上电不用初始化DAC 就能直接输出这个电压。这在某些需要快速上电响应的场景里非常实用。1.2 和单片机内置 DAC 对比的取舍很多 MCU 内部也有 DAC比如 STM32 的 F1/F4 系列就自带 12 位 DAC。那为什么还要外挂 MCP4725我自己的使用体验是内置 DAC 的通道数少、引脚固定而且某些型号的参考电压和 MCU 供电绑定输出范围受限。MCP4725 的优势在于通道扩展灵活一个 I2C 总线可以挂多片通过 A0、A1、A2 地址引脚区分。参考电压由 VDD 引脚决定可以和 MCU 供电分离独立控制输出范围。I2C 接口只占两根线对引脚紧张的板子很友好。当然缺点也很明显通过 I2C 刷新 DAC 的速度有限标准模式 100kHz、快速模式 400kHz极限输出更新率远不如内置 DAC 的 DMA 触发。所以 MCP4725 做波形发生更适合中低频段比如 DC 到几 kHz 的正弦波、三角波、方波再往上就需要换并行接口 DAC 或者带 SPI 的 DAC 了。1.3 工作场景和适用人群从我实际的测试结果来看MCP4725 MicroPython 这套组合最适合这几类场景实验室仪器控制信号给可调电源、激光器、温控设备提供模拟控制电压。音频信号测试生成特定的音调信号或扫频信号作为后端放大电路、滤波电路的输入源。传感器激励源给电桥传感器、热敏电阻网络提供稳定的偏置电压。教学演示和电子竞赛比如 2015 年电赛中有多款信号源类题目用 MCP4725 快速搭建波形发生器是常见的省事方案。不管你是学生、电子爱好者还是已经工作的嵌入式工程师这套方案都适合拿来快速验证想法。MicroPython 的快速开发特性加上 MCP4725 的简单外设接口能让你把更多精力放在波形算法和系统集成上而不是折腾寄存器配置。2. 整体设计思路与硬件准备2.1 系统架构梳理在动手写代码之前先把整体架构想清楚。一个典型的基于 MCP4725 的波形发生器由这几部分组成主控 MCU负责波形数据的生成、定时中断触发、I2C 数据发送。我这里用的是树莓派 Pico运行 MicroPython 固件。你也可以用 ESP32、ESP8266 或其他支持 I2C 的板子。DAC 芯片 MCP4725接收主控通过 I2C 发来的 12 位数字代码转换成对应的模拟电压输出。输出调理电路如果需要加一级 RC 低通滤波器把 DAC 输出的阶梯状信号平滑成连续波形。也可以加运放做增益调整或电平搬移。定时驱动源定时器中断Timer在精确的时间间隔内触发 DAC 更新决定波形的频率稳定性和相位连续性。上位机或调试工具示波器或逻辑分析仪用于观察实际波形输出串口终端用于打印调试信息。2.2 硬件连接方案我用的连接方式很简单MCP4725 的六根引脚全部接出来和 Pico 的连接如下MCP4725 引脚连接到说明VDD3.3VDAC 参考电压也决定输出满幅电压VSSGND地线SCLPico GP0/I2C0 SCLI2C 时钟线需要上拉电阻模块上一般自带SDAPico GP1/I2C0 SDAI2C 数据线同上A0GND 或 VDD 或悬空地址引脚决定 I2C 地址VOUT示波器/负载模拟电压输出注意 I2C 地址问题。MCP4725 的 7 位地址是 0x60二进制 1100000再加上 A2、A1、A0 三个引脚的组合。如果三个引脚都接地地址就是 0x60。如果 A0 接 VDD地址变成 0x61。我手头这块模块 A0 悬空或接地所以地址就是 0x60。焊接或接线之前先确认地址不然扫描 I2C 设备时会找不到芯片。注意MCP4725 是 12 位 DAC但 I2C 一个字节只有 8 位所以 DAC 的 12 位数据要拆成高 8 位和低 4 位发送低 4 位放在第二个字节的高 4 位上。后面代码里会具体体现。2.3 MicroPython 固件准备如果你用的是树莓派 Pico直接下载官方的 MicroPython 固件就可以不需要额外移植。我这里用的是支持 USB Host 的微雪 MicroPython 固件版本主要是为了后续扩展一些 USB 外设调试方便但核心功能上官方固件完全够用。下载固件时注意区分 .uf2 文件和 .hex 文件。Pico 用 .uf2按住 BOOTSEL 键插入 USB会弹出 U 盘把固件拖进去就完成刷写。ESP32 这类芯片用 .bin 文件可以用 esptool.py 通过串口烧录。刷写完固件之后用 Thonny 或者 Mu 编辑器连接板子能正常看到 REPL 交互界面就说明环境 OK 了。3. 从零实现波形发生器代码3.1 第一步扫描 I2C 设备确认通信代码部分的核心是把 I2C 通信调通。我先写一个最简单的 I2C 扫描脚本确认 MCP4725 能被正确识别from machine import Pin, I2C import time # 初始化 I2C0, SCLGP0, SDAGP1, 频率 400kHz i2c I2C(0, sclPin(0), sdaPin(1), freq400_000) print(I2C 设备扫描结果:, [hex(addr) for addr in i2c.scan()])如果一切正常你会看到输出列表里包含0x60。如果列表为空优先检查接线和上拉电阻另外确保模块供电正常。如果你用软件 I2C 方式模拟碰到的时序问题会更多我一般优先用硬件 I2C 外设。这份扫描代码重要的是确认了两件事一是硬件连接正确二是 MicroPython 的 I2C 对象参数设置无误。400kHz 的快速模式让后续波形更新频率的上限更高这个细节在后面调高频输出时会体现出来。3.2 第二步定义 DAC 写入函数MCP4725 支持几种写入方式我选了最直接的一种写入 DAC 寄存器但不写 EEPROM。这样每次上电不会改变 DAC 的默认状态也避免了 EEPROM 写寿命损耗官方标称 100 万次擦写但是能省则省。import time from machine import Pin, I2C DAC_ADDR 0x60 # 快速写入模式两个字节分别是 12 位数据的高 8 位和低 4 位左对齐到高 4 位 def dac_write_12bit(p_value): # 确保值在 0~4095 范围内 p_value max(0, min(4095, p_value)) high_byte (p_value 8) 0x0F low_byte p_value 0xFF # 命令字节: 0x30 表示写入 DAC 寄存器PD10, PD00, 正常模式 i2c.writeto(DAC_ADDR, bytes([0x30, high_byte, low_byte]))这里解释一下命令字节 0x30 的含义。MCP4725 的写命令第一个字节是控制字节其中高四位是命令位低四位和后续数据相关。0x30 对应的二进制是 0011 0000表示正常模式、写入 DAC 寄存器、不写 EEPROM。这样每次调用dac_write_12bit()都会立即更新模拟输出适合波形发生这种需要高频持续更新的场景。实际运行中我发现如果只追求速度还可以用“快速写入”命令命令字节 0x40只需要两个字节高 8 位 低 4 位不包含 DAC 值以外的多余数据。上面的代码实际上将 DAC 的 12 位值左对齐到两个字节的高 12 位然后通过write发送。如果你用快速写入需要自己把数据打包到 2 个字节命令可以省掉。不过我上面的写法可读性更好初始用这种即可。提示把 DAC 值限定在 0~4095 范围内的max/min操作非常有必要。调试阶段 I2C 参数传错或者数组越界时这个 clamp 保护能避免给 DAC 发一个非法值否则输出可能出现不可预测的毛刺。3.3 第三步生成不同波形数据的实现波形数据的核心思路是查表法。预先计算好一个周期的采样点定时器按固定频率依次把采样点送入 DAC就能在输出端得到连续的模拟波形。正弦波数据表生成import math SAMPLE_NUM 256 # 一个周期采样点数 def generate_sine_table(): table [] for i in range(SAMPLE_NUM): # 幅度调整到 0.5V ~ 2.5V 范围方便示波器观察 value int((math.sin(2 * math.pi * i / SAMPLE_NUM) 1) / 2 * 4095) table.append(value) return table这里把标准的 -1 到 1 正弦值映射成 0 到 4095 DAC 输出的整数值。加 1 除以 2 是为了把幅值搬移到正向范围避免负电压输出DAC 只能输出正电压。如果你需要双极性输出后面要加一个 I/V 变换电路来把电压中心移到 0V这个后面会讲到。我特意选 256 个采样点是因为 256 是 2 的幂后面配合定时器分频计算频率时特别方便。理论上采样点数越多波形越平滑但 I2C 刷新速度会限制最高输出频率这也是后面要平衡的关键点。三角波和锯齿波表生成def generate_triangle_table(): table [] half SAMPLE_NUM // 2 for i in range(SAMPLE_NUM): if i half: value int(i / half * 4095) else: value int((1 - (i - half) / half) * 4095) table.append(value) return table def generate_sawtooth_table(): table [] for i in range(SAMPLE_NUM): value int(i / SAMPLE_NUM * 4095) table.append(value) return table这两张表相对简单。三角波是线性上升再线性下降锯齿波是线性上升到顶后瞬间归零。实际输出时DAC 的更新步进会让波形呈阶梯状阶梯的细腻程度取决于采样点数。方波直接处理方波不用查表直接输出高低两个电压即可def output_square_wave(p_high_time_ms, p_low_time_ms): dac_write_12bit(4095) # 高电平 time.sleep_ms(p_high_time_ms) dac_write_12bit(0) # 低电平 time.sleep_ms(p_low_time_ms)当然这种用阻塞式延时的方法频率精度不高在演示功能时可以用后面定时器驱动方案会替换它。3.4 第四步集成到 MicroPython 工程中把上面的表生成和 DAC 驱动函数放在单独的dac_wave.py模块里主程序只需导入并调用即可。我的目录结构大概是这样的micropython-wave-generator/ main.py # 主程序入口 dac_wave.py # DAC 驱动和波形表生成 readme.md # 项目说明dac_wave.py中把所有波形表生成函数、DAC 写函数封装好主程序里from dac_wave import *即可使用。这种模块化组织方式对于以后要扩展更多波形类型比如高斯白噪声、调频信号非常有帮助也便于把波形生成逻辑和硬件操作解耦。然后你可能已经发现一个核心问题如果只用time.sleep_ms()或者time.sleep_us()来做波形的节拍控制频率根本稳定不了CPU 还要响应其他任务时就更乱了。解决问题的标准做法是用硬件定时器中断来驱动这是嵌入式波形发生器的精髓也是我下一节要重点讲的内容。4. 定时器驱动与波形频率控制的完整拆解4.1 为什么必须用定时器而非延时函数有人会觉得生成波形不就是dac_write(点1); delay(); dac_write(点2); delay(); ...吗没错低频率要求下确实能跑但一旦频率要求高一点或者系统里有其他任务这种死循环延时方案会暴露三个致命问题CPU 被完全占用延时期间 CPU 不能做别的事情如果同时还要扫描按键、刷新 LCD 屏幕波形频率就会抖动。频率精度和稳定性差time.sleep_ms()在 MicroPython 里底层依赖系统 Tick 和 VM 调度实际延时时长有一定误差尤其在while循环里有 I2C 操作时误差会被放大。波形相位不连贯如果中途被其他中断打断返回后继续发送采样点的时机不可控波形会出现相位跳跃。定时器中断方案则完全不同。硬件定时器按照设定的频率触发中断每次中断只做一件事把下一个采样点写入 DAC然后更新索引。CPU 绝大多数时间都是在主循环里处理其他业务波形输出由硬件自动推进。频率决定的参数是定时器的分频和重装值精度由晶振和硬件定时器保证抖动大幅降低。4.2 MicroPython Timer 中断的配置要点MicroPython 的machine.Timer提供了硬件定时器封装。不同开发板的 Timer 通道数量不同Pico 有 8 个定时器通道ESP32 有 4 个硬件定时器。我以 Pico 为例用Timer(1)举例。初始化代码如下from machine import Timer wave_timer Timer(1) # 每 100 微秒触发一次中断频率就是 10kHz def timer_callback(t): global wave_idx dac_write_12bit(sine_table[wave_idx]) wave_idx (wave_idx 1) % SAMPLE_NUM wave_timer.init(period100, modeTimer.PERIODIC, callbacktimer_callback)period参数的单位是微秒。如果你希望输出锯齿波频率为 1kHz一个周期有 256 个采样点那么每个采样点之间的时间间隔就是 1ms / 256 3.90625ms也就是 3906 微秒左右。这个计算在代码里可以用常量定义SAMPLE_NUM 256 WAVE_FREQ_HZ 1000 SAMPLE_INTERVAL_US int(1_000_000 / (SAMPLE_NUM * WAVE_FREQ_HZ)) print(采样间隔:, SAMPLE_INTERVAL_US, 微秒)把这个值作为period参数传入timer.init()就可以得到频率为 1000Hz 的波形输出。4.3 频率调节和动态切换方法定时器一旦初始化period参数就固定了。如果想要动态改变输出波形的频率最简单的办法是调用timer.deinit()然后重新init()。但这样有一个小问题重新初始化定时器的瞬间定时器计数器的相位会重置波形可能从任意点重新开始LED 或功放看起来就像突然卡了一下。更好的做法是使用分频表法。定义几个固定频率对应的period值切换时只重新赋值。MicroPython 的 Timer 在init()之后不支持直接在线修改 period所以通常做法还是deinit()后再init()。想要彻底解决相位连续性问题需要把频率控制逻辑放到中断外在中断里只做索引步进的递增而步进量由主循环根据目标频率动态更新。这个方案偏高级,本篇文章先不展开。关于 Pico 的machine.Timer需要特别注意它不是严格的硬件中断定时器而是在 MicroPython 的调度器上实现的。因此理论上它依赖 Runtime 的调度延迟不能和 C 裸机代码的定时精度相比。实测下来Pico 固件的 Timer 在中断回调里做写 I2C 的操作最高稳定输出频率大概在 5kHz 以下。超过之后波形上会出现明显的毛刺或丢点所以如果你要做更高频率的波形发生器要么换 C 语言写固件要么换 SPI/并行接口的 DAC。4.4 中断回调中 I2C 操作的风险和优化这里重点强调一个我踩过的坑。MicroPython 的中断回调ISR里尽量不要做 I2C 操作。原因有两条第一I2C 通信本身是同步阻塞的需要消耗几百微秒到毫秒级的时间远长于定时器中断周期第二MicroPython 中断回调里的阻塞操作可能会和主循环里的 I2C 操作产生冲突导致总线状态错乱。如果你确实需要在 ISR 里写 DAC尽量做到ISR 中只更新内存中的索引和缓存不直接发 I2C。把实际的dac_write_12bit()放在主循环中通过标志位触发执行。但这里存在一个矛盾如果把 DAC 写入放到主循环波形的时序精度又会被主循环的任务调度影响很难保证严格的采样周期。我自己的做法是中低频段几百 Hz 以下直接在主循环查表发送高频段妥协一下在 ISR 里直接写 DAC。如果你要做工业级的信号源这时候就该考虑用微控制器自带的 DAC DMA或者专门的波形发生器芯片了。关于 MicroPython 中断里 I2C 操作的另一个优化方案是利用硬件 DMA。然而标准 MicroPython 固件里没有直接暴露 DAC 的 DMA 接口需要 C 扩展模块。这在社区里有几个比较成熟的示例但配置复杂度会明显提升。5. 调试流程与常见坑位排查5.1 调试环境的搭建技巧调试波形发生器最理想的工具当然是示波器。如果没有示波器可以用逻辑分析仪看 DAC 输出的数字码型是否按预期跳变但这只能间接验证。我更常用的做法是DAC 输出端接一个 1kΩ 电阻到地再用万用表量平均电压粗略判断输出是否在动。正弦波的平均电压大概在 1.65V 左右3.3V 参考电压的一半如果测出来是这个值附近说明波形至少是在交替输出高低电平的大概率是通的。当然有条件还是建议用一个入门级的数字示波器比如几百元的基于 STM32 的虚拟示波器配合电脑上的上位机软件就能看到波形。我当时在示波器上看到正弦波阶梯状输出第一个反应就是采样点数不够多或频率太高了后来把采样点提高到 256频率降到 1kHz 以下波形立刻平滑了不少。5.2 经典问题波形频率忽高忽低这是我调试中最常见的问题。原因通常不是 DAC 芯片或者波形表出了问题而是 MicroPython 的中断处理机制不稳定。machine.Timer的period参数单位是微秒但实际触发时间的粒度受固件调度影响。排查方法很简单在中断回调里对一个全局计数变量count做累加然后在主循环里定时打印count的增量。按理说每秒增量应该等于1_000_000 / period如果实际值波动较大说明中断触发不稳定。我实测中发现 Pico 在运行 WiFi 或 USB 功能时中断抖动会增大导致波形频率抖动。解决办法关闭或暂停不相关的任务比如 OLED 刷新、WiFi 连接。中断回调里只做最少的操作比如用i2c.writeto直接发送不打印调试信息。如果可能把频率在上位机端固定用 SPI/I2C 从机方式接收波形点主控只负责 DAC。5.3 经典问题输出波形顶部削平或者底部削平削顶现象通常不是代码问题而是 DAC 的输出电压范围受限。MCP4725 的 VDD 是参考电压也是输出的满幅电压。如果 VDD 接 3.3V那么输出最高就是 3.3V 左右还要减去 DAC 输出缓冲管的饱和压降实际最高可能只有 3.2V 左右。如果你的波形幅度范围设计成 0 到 4095 对应 0 到 3.3V那接近满幅时会先于你的预期削平。解决思路有两种降低波形表的最大值比如限制到 4000留出一点余量。或者牺牲一点幅度线性度把峰顶稍微压低换取削顶不明显。另外还有一个容易忽略的点如果 VDD 直接接 5V而主控是 3.3V 的逻辑电平I2C 引脚会过压可能导致 DAC 或主控损坏。尽量保持 VDD 和 I2C 电平在同一域。5.4 经典问题波形有量化噪声或毛刺DAC 本身是 12 位的输出有残留量化噪声属于正常现象。如果你在示波器上看到相邻台阶之间有明显的尖锐毛刺通常有两个原因I2C 时钟线或数据线上有反射尝试减小 I2C 速率到 100kHz或者在 SDA/SCL 线上串联 33Ω 电阻。DAC 输出端缺少去耦电容在 VOUT 引脚和地之间加一个 0.1μF 的陶瓷电容能显著减少高频毛刺。如果是想要平滑的正弦波可以在 DAC 输出后级增加一个简单的 RC 低通滤波器。截止频率设为波形最高频率的 3~5 倍比如最高输出 1kHz 正弦波滤波器截止频率可以设在 3kHz 到 5kHz。电阻取 1kΩ电容就按公式f 1 / (2 * π * R * C)计算3kHz 时 C 约为 53nF取标准值 47nF 或者 56nF 都可以。5.5 经典问题MicroPython 代码运行正常但 DAC 无输出这个问题比较诡异。代码运行没报错I2C 扫描也识别到了但 VOUT 一直为 0。我遇到过一次是模块上的 VDD 引脚和 VCC 引脚混了。很多市面上卖的 MCP4725 模块丝印上既有 VCC 也有 VDD实际上模块内部直接把 VDD 当逻辑参考和模拟参考。如果 VCC 接对了但 VDD 悬空I2C 能通因为 I2C 上拉通常到 VCC但 DAC 的模拟部分不工作输出始终为 0。排查方法是量模块上 MCP4725 各引脚的电压确认 VDD 和 VSS 都正常且 VOUT 引脚电压会随着你手动调用dac_write_12bit(2048)而变化。如果手动写值输出有变化说明代码逻辑没问题问题应该在定时器驱动部分。5.6 实用调试清单检查项操作验证结果I2C 通信i2c.scan()能扫描到0x60DAC 写数据dac_write_12bit(0)和dac_write_12bit(4095)VOUT 从接近 0V 到接近 VDD波形表尺寸检查len(sine_table)与SAMPLE_NUM一致定时器周期主循环统计中断次数频率误差小于 5% 可接受输出波形示波器观察无毛刺、无削顶、频率稳定6. 波形发生器的进阶扩展方向如果你已经把基础的波形发生器跑通了以下几个方向非常值得继续探索。6.1 加上按键和 OLED 实现人机交互可以在主循环里扫描按键根据按键状态切换波形类型和频率实时把频率和波形类型显示在 OLED 屏上。这部分可以和定时器中断并行工作关键是中断回调里保持精简按键扫描和显示都放到主循环完成。如果 UI 刷新卡了可以适当降低刷新率比如 10Hz 刷新频率运行时人眼完全感知不到延迟。6.2 用 AD9833 或 AD9850 做高频扩展MCP4725 的 I2C 瓶颈决定了它不适合高频。如果你需要几十 kHz 甚至 1MHz 以上的信号建议换用 AD9833 或 AD9850 这类专用 DDS 芯片。它们直接通过 SPI 配置频率和相位寄存器输出正弦波、三角波、方波频率范围远比 MCP4725 宽。这部分代码在 MicroPython 里也有现成库上手速度很快。6.3 做一个上位机控制的小工具如果你不想每次改频率都去 IDE 里改代码可以用 Python 的serial库写一个上位机通过串口发送指令比如SIN 1000、SQR 500等。主控接收到指令后解析频率重新配置波形表和定时器就能在电脑上像操作仪器一样控制波形发生器。我经常这样用尤其是在做滤波器扫频测试时非常方便。6.4 增加输出增益级如果你需要输出 -5V 到 5V 甚至更大的电压摆幅DAC 直接输出是做不到的。后级加一个运放电路按照虚短虚断原理把 DAC 输出的 0~3.3V 映射到双极性范围。一个常见方案是用加法器和反相放大器组合把中心电压搬移。计算公式为Vout (Vdac - Vref) * Gain其中 Vref 是你要设定的中心电压。这部分电路设计需要仔细计算电阻比例否则很容易出现输出饱和或偏移。7. 完整代码示例汇总最后把最常用的版本整理一下你可以直接拷贝使用。这个版本支持正弦波、三角波、锯齿波和方波频率在主循环中动态调整。import math import time from machine import Pin, I2C, Timer # 硬件初始化 i2c I2C(0, sclPin(0), sdaPin(1), freq400_000) DAC_ADDR 0x60 # DAC 驱动 def dac_write(p_value): p_value max(0, min(4095, int(p_value))) high (p_value 8) 0x0F low p_value 0xFF i2c.writeto(DAC_ADDR, bytes([0x30, high, low])) # 波形表生成 SAMPLE_NUM 256 def gen_sine(): return [int((math.sin(2 * math.pi * i / SAMPLE_NUM) 1) / 2 * 4095) for i in range(SAMPLE_NUM)] def gen_triangle(): tab [] half SAMPLE_NUM // 2 for i in range(SAMPLE_NUM): if i half: tab.append(int(i / half * 4095)) else: tab.append(int((1 - (i - half) / half) * 4095)) return tab def gen_sawtooth(): return [int(i / SAMPLE_NUM * 4095) for i in range(SAMPLE_NUM)] # 波形对象 wave_table gen_sine() wave_idx 0 # 定时器中断回调 def timer_cb(t): global wave_idx dac_write(wave_table[wave_idx]) wave_idx (wave_idx 1) % SAMPLE_NUM # 主程序 wave_timer Timer(1) def set_wave_type(wave_type): global wave_table, wave_idx if wave_type sine: wave_table gen_sine() elif wave_type triangle: wave_table gen_triangle() elif wave_type sawtooth: wave_table gen_sawtooth() wave_idx 0 def set_wave_freq(freq_hz): interval_us int(1_000_000 / (SAMPLE_NUM * freq_hz)) wave_timer.deinit() wave_timer.init(periodinterval_us, modeTimer.PERIODIC, callbacktimer_cb) # 使用示例 set_wave_type(sine) set_wave_freq(1000) try: while True: time.sleep_ms(100) except KeyboardInterrupt: wave_timer.deinit() dac_write(0) print(程序退出)请注意set_wave_freq()里的wave_timer.deinit()之后重新init()会重置定时器相位所以切换频率时波形可能会从零相位重新开始。如果你需要相位连续还需要在此基础上额外处理。8. 写在最后的经验建议这套基于 MCP4725 的波形发生器我后来实际用了很久有几点感触比较深。首先是频率上限不要硬撑。当时我为了让示波器上看到一个好看的 10kHz 正弦波想尽办法优化中断代码、降低采样点数最后还是不太稳定。后来换了思路用更低频率输出配合外部锁相倍频或者直接换 DDS 芯片效果立竿见影。做嵌入式信号源方案选型比写代码更重要。其次是I2C 线上尽量用短连线。MCP4725 的 I2C 速率跑到 400kHz 时如果杜邦线超过 20 厘米波形毛刺会明显增加。换成短跳线或者飞线焊接后问题基本消失。如果你用面包板做原型尽量把模块插在靠近主控的位置。最后想说的是调试流程一定要讲究顺序。先把 DAC 静态输出调通再上定时器最后再接波形表。这样每加一层功能出问题时能快速定位是硬件、驱动还是算法的问题。千万不要一上来就整一个完整的定时器 查表程序调试难度会成倍增加。MCP4725 虽然只是入门级的 DAC但用它来做波形发生器可以让你用最少的成本把信号链路的每个环节都摸透。从查表法生成波形、定时器驱动时基到最后的滤波、调试全程走一遍对嵌入式模拟信号设计的理解会提升一个台阶。希望这份梳理能让你少走一些弯路。