在以下代码中,在树莓派 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 (数字模拟转换器) 上:

- 首先在构造函数中对传入的参数(频率、幅度、偏移、波形类型等)进行验证,确保它们在合理范围内,并保存 DAC 对象和波形参数,同时初始化定时器,并且根据输入波形参数调用
generate_samples方法来生成采样点数据;

- 在
generate_samples方法中,我们根据波形类型生成相应的采样点数据:- 正弦波 :使用
math.sin函数生成正弦波数据。 - 方波:根据采样点序号生成高低电平交替的数据。
- 三角波:根据上升沿和下降沿生成线性变化的数据。 之后返回生成的采样点数据列表。
- 正弦波 :使用
- 在定时器回调函数 (
update方法)中,将当前采样点的数据写入 DAC 并更新采样点索引,以指向下一个采样点(循环使用采样点数据)。

- 创建完成
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 中使用软件定时器更新的最高频率为 1000 Hz**,这大大限制了生成信号的频率范围。**
并且,使用 MCP4725 时,由于数据传输通过 I2C 总线,数据传输可能受到 I2C 总线阻塞或微控制器其他任务的影响,导致波形发生器的输出存在抖动或不稳定性。因此,在实际应用中,我们往往使用外置的 DDS 信号发生芯片来实现波形发生功能。