一文搞懂 MCP4725 波形发生器:MicroPython 代码实现、定时器驱动与调试流程全梳理

在以下代码中,在树莓派 Pico 上使用 DAC 芯片 MCP4725 生成不同的波形信号,并通过串口输出 ADC 采集的波形电压值,元器件接线和上节内容一样:

以下代码可以在我们提供的资料包中 elegance-devkit v1\Demo\63 DAC_WaveformGenerator 文件夹中找到。

示例代码如下所示:

Python 复制代码
# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/1 下午2:10   
# @Author  : 李清水            
# @File    : main.py       
# @Description : DAC类实验,使用外置DAC芯片产生不同波形

# ======================================== 导入相关模块 ========================================

# 导入硬件模块
from machine import ADC, Timer, Pin, I2C, UART
# 导入时间相关模块
import time
# 导入访问和控制 MicroPython 内部结构的模块
import micropython
# 导入数学库用于计算正弦波
import math
# 导入mcp4725模块用于控制DAC芯片
from mcp4725 import MCP4725

# ======================================== 全局变量 ============================================

# mcp4725芯片地址
DAC_ADDRESS = 0x00
# 电压转换系数
adc_conversion_factor = 3.3 / (65535)

# ======================================== 功能函数 ============================================

def timer_callback(timer: Timer) -> None:
    """
​    定时器回调函数,用于定时读取ADC数据并调用用户自定义的回调函数。

​    Args:
​        timer (machine.Timer): 定时器实例。

​    Returns:
​        None: 此函数没有返回值。

​    Raises:
​        None: 此函数不抛出异常。
​    """

​    ​# 声明全局变量
    global adc,adc_conversion_factor

    # 读取ADC数据
    value = adc.read_u16() * adc_conversion_factor
    # 调用用户自定义的回调函数
    micropython.schedule(user_callback, (value))

def user_callback(value: float) -> None:
    """
​    用户自定义的回调函数,用于处理ADC采集到的电压值并通过串口发送。

​    Args:
​        value (float): ADC采集到的电压值。

​    Returns:
​        None: 此函数没有返回值。

​    Raises:
​        None: 此函数不抛出异常。
​    """
​    ​# 声明全局变量
    global uart

    # 获取浮点数并将其四舍五入到两位小数
    formatted_value = "{:.2f}".format(value)
    # 串口发送采集到的电压数据
    uart.write(str(formatted_value) + '\r\n')

# ======================================== 自定义类 ============================================

# 波形发生器类,用于生成不同波形的电压信号
class WaveformGenerator:
    """
​    波形发生器类,用于生成不同波形的电压信号。

​    该类通过 DAC 芯片(如 MCP4725)生成正弦波、方波和三角波信号,支持设置频率、幅度、直流偏移等参数。
​    波形数据通过定时器周期性输出到 DAC,从而实现连续的波形生成。

​    Attributes:
​        dac (MCP4725): DAC 芯片实例,用于生成波形信号。
​        frequency (float): 信号频率,单位为 Hz。
​        amplitude (float): 信号幅度,单位为 V。
​        offset (float): 直流偏移,单位为 V。
​        waveform (str): 波形类型,支持 'sine'(正弦波)、'square'(方波)和 'triangle'(三角波)。
​        rise_ratio (float): 三角波的上升斜率,范围为 0 到 1。
​        sample_rate (int): 采样率,固定为 50 个采样点。
​        dac_resolution (int): DAC 的分辨率,12 位 DAC 的范围为 0 到 4095。
​        samples (list[int]): 生成的采样点数据列表。
​        index (int): 当前采样点索引,用于逐步输出波形数据。
​        timer (Timer): 定时器实例,用于周期性触发波形输出。

​    Methods:
​        __init__(dac, frequency=1, amplitude=1.65, offset=1.65, waveform='sine', rise_ratio=0.5):
​            初始化波形发生器实例。
​        generate_samples() -> list[int]:
​            根据波形类型生成采样点数据。
​        update(t: Timer) -> None:
​            定时器回调函数,用于输出下一个采样点的数据。
​        start() -> None:
​            启动波形生成器。
​        stop() -> None:
​            停止波形生成器。
​    """
​    ​def __init__(self, dac: 'MCP4725', frequency: float = 1, amplitude: float = 1.65, offset: float = 1.65,
                 waveform: str = 'sine', rise_ratio: float = 0.5) -> None:
        """
​        初始化波形发生器实例。

​        该方法用于初始化波形发生器的基本设置,包括 DAC 对象、信号频率、幅度、偏移、波形类型及三角波的上升斜率。

​        Args:
​            dac (MCP4725): DAC芯片实例,用于生成波形信号。
​            frequency (float, optional): 信号频率,默认值为 1 Hz。必须大于 0 并小于等于 10 Hz。
​            amplitude (float, optional): 信号幅度,默认值为 1.65V。必须在 0 到 3.3V 之间。
​            offset (float, optional): 直流偏移,默认值为 1.65V。必须在 0 到 3.3V 之间。
​            waveform (str, optional): 波形类型,可以是 'sine'(正弦波)、'square'(方波)或 'triangle'(三角波),默认值为 'sine'。
​            rise_ratio (float, optional): 三角波的上升斜率,默认为 0.5,值必须在 0 到 1 之间。

​        Returns:
​            None: 此方法没有返回值。

​        Raises:
​            ValueError: 如果传入的参数不在预定范围内,将抛出该异常。
​        """

​        ​# 参数输入检查
        # 参数输入检查,确保频率在 0 到 10 Hz 之间
        if not (0 < frequency <= 10):
            raise ValueError("Frequency must be between 0 and 10 Hz.")
        # 检查幅度范围是否在 0 到 3.3V 之间
        if not (0 <= amplitude <= 3.3):
            raise ValueError("Amplitude must be between 0 and 3.3V.")
        # 检查直流偏移范围是否在 0 到 3.3V 之间
        if not (0 <= offset <= 3.3):
            raise ValueError("Offset must be between 0 and 3.3V.")
        # 确保幅度和偏移的和不超过 3.3V,防止输出超出 DAC 的范围
        if not(0 <= amplitude+offset <= 3.3):
            raise ValueError("Amplitude + offset must be between 0 and 3.3V.")
        # 检查波形类型是否是正弦波、方波或三角波
        if waveform not in ['sine', 'square', 'triangle']:
            raise ValueError("Waveform must be 'sine', 'square', or 'triangle'.")
        # 检查三角波上升沿比例是否在 0 到 1 之间
        if not (0 <= rise_ratio <= 1):
            raise ValueError("Rise ratio must be between 0 and 1.")

        # 保存DAC对象
        self.dac = dac

        # 初始化定时器对象,-1 表示在初始化时不绑定到任何硬件定时器
        self.timer = Timer(-1)

        # 保存波形生成器的参数
        # 信号频率
        self.frequency = frequency
        # 信号幅度
        self.amplitude = amplitude
        # 直流偏移
        self.offset = offset
        # 波形类型
        self.waveform = waveform
        # 三角波上升沿比例
        self.rise_ratio = rise_ratio

        # 固定50个采样点,离散信号的采样点数量
        self.sample_rate = 50

        # 12位DAC的分辨率,意味着DAC输出范围是0到4095
        self.dac_resolution = 4095

        # 根据波形类型生成采样点数据
        self.samples = self.generate_samples()

        # 初始化当前采样点索引,用于逐步输出波形数据
        self.index = 0

    def generate_samples(self) -> list[int]:
        """
​        根据波形类型生成采样点数据。

​        该方法根据所选的波形类型(正弦波、方波、三角波)生成对应的采样点数据,并将每个采样点转换为 DAC 可接受的数值。

​        Returns:
​            list[int]: 包含转换为 DAC 数值的采样点列表。

​        Raises:
​            None: 此方法不会抛出异常。
​        """

​        ​# 将电压值转换为DAC值的函数
        def to_dac_value(voltage):
            # 通过电压值计算对应的 DAC 输出值,DAC 分辨率为 4095,电压范围为 0 到 3.3V
            return int(voltage / 3.3 * self.dac_resolution)

        # 初始化一个列表用于存储生成的采样点数据
        samples = []

        # 根据选定的波形生成采样点数据
        if self.waveform == 'sine':
            # 正弦波:根据角度生成对应的电压值
            for i in range(self.sample_rate):
                # 计算当前采样点对应的角度,角度以 2π 为周期
                angle = 2 * math.pi * i / self.sample_rate
                # 根据正弦函数计算当前采样点的电压值
                voltage = self.offset + self.amplitude * math.sin(angle)
                # 将电压值转换为 DAC 对应的数值,并加入样本列表
                samples.append(to_dac_value(voltage))

        elif self.waveform == 'square':
            # 方波:根据采样点序号生成高低电平交替的电压值
            for i in range(self.sample_rate):
                # 前半周期电压为高电平,后半周期电压为低电平
                if i < self.sample_rate // 2:
                    # 高电平
                    voltage = self.offset + self.amplitude
                else:
                    # 低电平
                    voltage = self.offset - self.amplitude
                # 将电压值转换为 DAC 对应的数值,并加入样本列表
                samples.append(to_dac_value(voltage))

        elif self.waveform == 'triangle':
            # 三角波:根据上升沿和下降沿生成电压值
            for i in range(self.sample_rate):
                if i < self.sample_rate * self.rise_ratio:
                    # 上升沿,电压从 offset - amplitude 上升到 offset + amplitude
                    voltage = self.offset + 2 * self.amplitude * (
                            i / (self.sample_rate * self.rise_ratio)) - self.amplitude
                else:
                    # 下降沿,电压从 offset + amplitude 下降到 offset - amplitude
                    voltage = self.offset + 2 * self.amplitude * (
                            (self.sample_rate - i) / (self.sample_rate * (1 - self.rise_ratio))) - self.amplitude
                # 将电压值转换为 DAC 对应的数值,并加入样本列表
                samples.append(to_dac_value(voltage))

        # 返回生成的采样点数据列表
        return samples

    def update(self, t: Timer) -> None:
        """
​        定时器回调函数,在定时器中断时调用,用于输出下一个采样点的数据。

​        该方法在定时器中断时被调用,每次被调用时,它将当前采样点的数据写入 DAC,并更新索引以指向下一个采样点。

​        Args:
​            t (Timer): 定时器对象,表示触发回调的定时器。

​        Returns:
​            None: 此方法没有返回值。

​        Raises:
​            None: 此方法不会抛出异常。
​        """

​        ​# 将当前采样点的数据写入 DAC,生成对应的电压输出
        self.dac.write(self.samples[self.index])
        # 更新采样点索引,以指向下一个采样点
        self.index = (self.index + 1) % self.sample_rate

    def start(self) -> None:
        """
​        启动波形生成器,开始定时器,周期性地生成并输出波形数据。

​        该方法会启动定时器,在每个周期触发时调用回调函数,按预定频率输出波形数据。

​        Args:
​            None: 此方法不接受任何参数。

​        Returns:
​            None: 此方法没有返回值。

​        Raises:
​            None: 此方法不会抛出异常。
​        """

​        ​# 初始化定时器,以频率 freq*self.sample_rate 触发定时器中断
        self.timer.init(freq=self.frequency * self.sample_rate, mode=Timer.PERIODIC, callback=self.update)

    def stop(self) -> None:
        """
​        停止波形生成器,停止定时器,停止波形数据的输出。

​        该方法会停止定时器的工作,波形输出将暂停。

​        Args:
​            None: 此方法不接受任何参数。

​        Returns:
​            None: 此方法没有返回值。

​        Raises:
​            None: 此方法不会抛出异常。
​        """

​        ​# 停止定时器,解除中断
        self.timer.deinit()
        # 将采样点索引复位为0,以确保下次启动时从第一个采样点开始
        self.index = 0

# ======================================== 初始化配置 ==========================================

# 延时3s等待设备上电完毕
time.sleep(3)
# 打印调试信息
print("FreakStudio : Using DAC to generate differential waveform")

# 创建硬件I2C的实例,使用I2C1外设,时钟频率为400KHz,SDA引脚为2,SCL引脚为3
i2c = I2C(id=1, sda=Pin(2), scl=Pin(3), freq=400000)

# 开始扫描I2C总线上的设备,返回从机地址的列表
devices_list = i2c.scan()
print('START I2C SCANNER')

# 若devices_list为空,则没有设备连接到I2C总线上
if len(devices_list) == 0:
    print("No i2c device !")
# 若非空,则打印从机设备地址
else:
    print('i2c devices found:', len(devices_list))
    # 便利从机设备地址列表
    for device in devices_list:
        # 如果设备地址在0x60-0x61之间,则为DAC芯片
        if 0x60 <= device <= 0x61:
            print("I2C hexadecimal address: ", hex(device))
            DAC_ADDRESS = device

# 创建DAC对象,使用I2C0外设,地址为DAC_ADDRESS
dac = MCP4725(i2c, DAC_ADDRESS)
# 读取DAC的配置信息
eeprom_write_busy, power_down, value, eeprom_power_down, eeprom_value = dac.read()
print(eeprom_write_busy, power_down, value, eeprom_power_down, eeprom_value)
# 配置DAC:取消电源关断模式,输出电压为0V,写入EEPROM
dac.config(power_down='Off',value=0,eeprom=True)
# 延时50ms,配置完成后立即读取会发生错误
time.sleep_ms(50)
# 读取DAC的配置信息
eeprom_write_busy, power_down, value, eeprom_power_down, eeprom_value = dac.read()
print(eeprom_write_busy, power_down, value, eeprom_power_down, eeprom_value)

# 创建串口对象,设置波特率为115200
uart = UART(0, 115200)
# 初始化uart对象,波特率为115200,数据位为8,无校验位,停止位为1
# 设置串口超时时间为100ms
uart.init(baudrate  = 115200,
          bits      = 8,
          parity    = None,
          stop      = 1,
          tx        = 0,
          rx        = 1,
          timeout   = 100)

# 创建ADC实例:ADC1-GP27
adc = ADC(1)
# 创建软件定时器对象
timer = Timer(-1)
# 启动定时器,每 1ms 触发一次 timer_callback 函数,ADC采集电压
timer.init(period=1, mode=Timer.PERIODIC, callback=timer_callback)

# ========================================  主程序  ===========================================

# 生成正弦波
print("FreakStudio : Generate Sine Waveform : 10Hz, 1.5V, 1.5V")
# 初始化波形生成器
wave = WaveformGenerator(dac, frequency=10, amplitude=1.5, offset=1.5, waveform='sine')
# 启动波形生成
wave.start()
# 运行一段时间后停止生成
time.sleep(5)
wave.stop()

# 生成方波
print("FreakStudio : Generate Square Waveform : 10Hz, 1.5V, 1.5V")
# 初始化波形生成器
wave = WaveformGenerator(dac, frequency=10, amplitude=1.5, offset=1.5, waveform='square')
# 启动波形生成
wave.start()
# 运行一段时间后停止生成
time.sleep(5)
wave.stop()

# 生成三角波
print("FreakStudio : Generate Triangle Waveform : 10Hz, 1.5V, 1.5V, 0.8")
# 初始化波形生成器
wave = WaveformGenerator(dac, frequency=10, amplitude=1.5, offset=1.5, waveform='triangle', rise_ratio=0.8)
# 启动波形生成
wave.start()
# 运行一段时间后停止生成
time.sleep(5)
wave.stop()

# 停止ADC采集
timer.deinit()

这里,我们自定义了一个 WaveformGenerator 类实现频率和幅度可调的方波、正弦波以及三角波,并将这些信号输出到 DAC (数字模拟转换器) 上:

  1. 首先在构造函数中对传入的参数(频率、幅度、偏移、波形类型等)进行验证,确保它们在合理范围内,并保存 DAC 对象和波形参数,同时初始化定时器,并且根据输入波形参数调用 generate_samples 方法来生成采样点数据;
  1. generate_samples 方法中,我们根据波形类型生成相应的采样点数据:
    1. 正弦波 :使用 math.sin 函数生成正弦波数据。
    2. 方波:根据采样点序号生成高低电平交替的数据。
    3. 三角波:根据上升沿和下降沿生成线性变化的数据。 之后返回生成的采样点数据列表。
  2. 在定时器回调函数 (update 方法)中,将当前采样点的数据写入 DAC 并更新采样点索引,以指向下一个采样点(循环使用采样点数据)。
  1. 创建完成 WaveformGenerator 类的实例后,我们通过 start 方法初始化定时器,频率为 self.frequency * self.sample_rate,周期性调用 update 方法;将需要波形发生后,可以通过 stop 方法停止定时器并将采样点索引重置为 0。

在主程序中,我们顺序创建了 3 个 WaveformGenerator 实例,用以生成正弦波、矩形波和三角波,其中三角波上升沿比率为 0.8,并且三个波形均是生成 5s 后停止,三个波形都生成完毕后关闭定时器,停止 ADC 采集。

程序整体时序图如下所示:

打开 SerialPlot 软件,选择 USB 转 TTL 芯片连接的对应端口,点击 Open 按钮:

数据格式选择 ASCII 编码,其他配置默认即可:

烧录代码,打开终端,输出如下:

打开 SerialPlot 软件,可以看到显示的 DAC 输出电压的频率与幅度均正常:

相比于 DDS 信号发生器,使用 MCP4725 DAC 实现波形发生必须通过软件在微控制器中计算波形的各个点,然后逐点发送给 ​**DAC ,这不仅增加了系统复杂性,还消耗了更多的处理资源​​ (通过定时器定时发送,当需要发送的信号频率较高时会频繁产生定时器中断,同时当需要波形的采样点数较多时会占用较多内存​​​,同时 波形的频率完全依赖于微控制器更新 DAC 值的速度**,频率精度和分辨率受到微控制器时钟和定时器分辨率的限制,难以实现非常高精度的频率控制,在 MicroPython 中使用软件定时器更新的最高频率为 1000Hz​**,这大大限制了生成信号的频率范围。**

并且,使用 MCP4725 时,由于数据传输通过 I2C 总线,数据传输可能受到 I2C 总线阻塞或微控制器其他任务的影响,导致波形发生器的输出存在抖动或不稳定性。因此,在实际应用中,我们往往使用外置的 DDS 信号发生芯片来实现波形发生功能。

相关推荐
与你40314 分钟前
STM32F103_AB_OTA_从零复现教程
嵌入式
慧都小项40 分钟前
汽车工具链重复测试太耗人?用 TestComplete 自动跑
功能测试·回归·汽车·嵌入式·软件工程·测试覆盖率
Discipline~Hai2 小时前
Linux网络编程02-TCP协议
linux·网络·tcp/ip·嵌入式·linux网络编程
Jason_zhao_MR3 小时前
电能质量监测为什么需要PTP精确时钟同步
linux·单片机·嵌入式硬件·嵌入式
Freak嵌入式4 小时前
全网最详细|树莓派 Pico ADC 软件控制全解析:从machine.ADC API 到定时温度采集实战(含 ISR 避坑指南)
嵌入式
嵌入式阿蔡7 小时前
存储_14:嵌入式存储测试岗面试——可靠性/兼容/车规面试题与项目话术
单片机·嵌入式硬件·面试·嵌入式·存储
Freak嵌入式1 天前
嵌入式 MQTT 项目实战:Pico+MicroPython 向 EMQX 发布 JSON 消息全解析
嵌入式
Freak嵌入式1 天前
MicroPython 开发:树莓派 Pico RTC 控制方法与 NTP 时间同步实现
嵌入式
Freak嵌入式1 天前
一篇搞懂:MicroPython urequests库与 Pico HTTP 客户端实现
嵌入式