嵌入式开发进阶:MicroPython 下 DS3502 的快速写入模式与波形生成技巧

1.DS3502 的自定义类

在以下代码中,我们实现了 DS3502 数字电位器的驱动程序,我们可以通过 I2C 接口控制 DS3502 的滑动寄存器(Wiper Register)位置,从而改变电位器的电阻值。

示例代码如下所示:

# Python env   : MicroPython v1.23.0
# -*- coding: UTF-8 -*-
# @Time    : 2024/11/4 下午3:20
# @Author  : 李清水
# @File    : ds3502.py
# @Description : 数字电位器芯片DS3502驱动模块

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

# 导入时间相关模块
import time
# 导入MicroPython相关模块
from micropython import const
# 导入硬件相关模块
from machine import I2C

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

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

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

# DS3502数字电位器自定义类
class DS3502:
    """
​    DS3502 类,用于通过 I2C 总线操作 DS3502 数字电位器芯片,实现电阻值的调节。
​    该类封装了对 DS3502 芯片的 I2C 通信,提供了设置滑动寄存器(WR)值、读取当前滑动位置、
​    设置控制寄存器(CR)模式等功能。

​    Attributes:
​        i2c (I2C): I2C 实例,用于与 DS3502 进行通信。
​        addr (int): DS3502 的 I2C 地址(0x28 到 0x2B 之间)。
​        mode (int): 当前工作模式(0 或 1),用于控制写入速度和非易失性存储行为。

​    Methods:
​        __init__(self, i2c: I2C, ​addr: int):
​            初始化 DS3502 类实例。
​        write_wiper(self, value: int) -> None:
​            写入滑动寄存器(WR)以设置滑动位置。
​        read_control_register(self) -> int:
​            读取控制寄存器(CR)的值,以确定当前控制寄存器的写入模式。
​        set_mode(self, mode: int) -> None:
​            设置控制寄存器的写入模式。
​        read_wiper(self) -> int:
​            读取滑动寄存器(WR)的值。
​    """

​    ​# 定义类变量:寄存器地址
    # WR滑动寄存器地址
    REG_WIPER = const(0x00)
    # CR控制寄存器地址
    REG_CONTROL = const(0x02)

    def __init__(self, i2c: I2C, addr: int):
        """
​        初始化 DS3502 类。

​        Args:
​            i2c (machine.I2C): I2C 对象,用于与 DS3502 通信。
​            addr (int): DS3502 的 I2C 地址(0x28 到 0x2B 之间)。

​        Raises:
​            ValueError: 如果地址不在 0x28 到 0x2B 之间。
​        """
​        ​# 判断I2C地址是否在0x28到0x2B之间
        if addr < 0x28 or addr > 0x2B:
            raise ValueError("Address must be between 0x28 and 0x2B")

        # 保存I2C对象
        self.i2c = i2c
        # 保存I2C地址
        self.addr = addr
        # 工作模式:
        #   0 - 将数据写入WR和IVR,速度慢,CR = 00h
        #   1 - 将数据写入WR,速度快,CR = 80h
        self.mode = 0

    def write_wiper(self, value: int) -> None:
        """
​        写入滑动寄存器(WR)以设置滑动位置。

​        Args:
​            value (int): 要写入的滑动寄存器值(0 到 127)。

​        Raises:
​            ValueError: 如果值不在 0 到 127 之间。
​        """

​        ​# 检查输入值是否在有效范围内
        if value < 0 or value > 127:
            raise ValueError("Value must be between 0 and 127")

        # 向DS3502的地址0x00写入值以更新WR寄存器
        self.i2c.writeto_mem(self.addr, DS3502.REG_WIPER, bytes([value]))

        # 根据工作模式判断是否需要延时
        if self.mode == 0:
            # 模式0延时100ms
            time.sleep_ms(100)

    def read_control_register(self) -> int:
        """
​        读取控制寄存器(CR)的值,以确定当前控制寄存器的写入模式。

​        Args:
​            None: 无。

​        Returns:
​            int: 控制寄存器的值(0 或 1,表示当前模式)。
​        """
​        ​# 发送从设备地址,设置R/W位为0,进行假写操作
        # 写入CR的地址
        self.i2c.writeto_mem(self.addr, DS3502.REG_CONTROL, b'')

        # 生成重复的START条件以保持通信
        # 读取CR寄存器的值
        data = self.i2c.readfrom_mem(self.addr, DS3502.REG_CONTROL, 1)

        # 修改mode属性
        if data[0] == 0x80:
            self.mode = 1

        # 返回读取的字节
        return self.mode

    def set_mode(self, mode: int) -> None:
        """
​        设置控制寄存器的写入模式。

​        Args:
​            mode (int): 模式选择,0 或 1。

​        Raises:
​            ValueError: 如果模式值不是 0 或 1。
​        """
​        ​if mode not in (0, 1):
            raise ValueError("Mode must be 0 or 1")

        # 根据模式选择写入控制寄存器
        # 写入模式0
        if mode == 0:
            # 设置MODE位为0,写入WR和IVR
            self.i2c.writeto_mem(self.addr, DS3502.REG_CONTROL, bytes([0x00]))
            self.mode = 0
        # 写入模式1
        else:
            # 设置MODE位为1,仅写入WR
            self.i2c.writeto_mem(self.addr, DS3502.REG_CONTROL, bytes([0x80]))
            self.mode = 1

    def read_wiper(self) -> int:
        """
​        读取滑动寄存器(WR)的值。

​        Args:
​            None: 无。

​        Returns:
​            int: 当前滑动位置的值(0 到 127)。
​        """
​        ​# 发送从设备地址,进行假写操作,准备读取WR寄存器
        # 写入WR的地址
        self.i2c.writeto_mem(self.addr, DS3502.REG_WIPER, b'')

        # 生成重复的START条件以保持通信
        # 读取WR寄存器的值
        data = self.i2c.readfrom_mem(self.addr, DS3502.REG_WIPER, 1)

        # 返回读取的滑动位置值
        return data[0]

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

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

该自定义类提供了如下方法:

1.png

  • ​初始化​__init__​方法:​这里,我们仅需要传入 I2C 接口对象和 DS3502 的 I2C 地址,我们首先进行地址范围检查,确保地址在 0x280x2B 之间(即二进制在 0101000 到 0101011 之间),否则抛出 ValueError 异常;同时保存 I2C 对象和地址,并初始化 mode0mode 控制写入数据的模式,0 表示慢速模式,1 表示快速模式。
  • 设置滑动寄存器值 write_wiper​方法:​该方法通过写入滑动寄存器,改变电位器的电阻值,首先进行输入范围检查,确保 value0127 之间;然后使用 writeto_mem 方法向 REG_WIPER 寄存器写入 value,更新滑动变阻器位置,同时如果 mode0(慢速模式),执行 100ms 的延时
  • 读取控制寄存器值 read_control_register​方法:​主要通过读取控制寄存器 CR 的值,检查 mode 状态,该方法使用 readfrom_mem 方法读取 REG_CONTROL 的值;如果读到的数据为 0x80,表示当前为模式 1,则更新 self.mode1,并且返回当前模式。
  • 设置控制寄存器模式 set_mode​方法:​主要根据 mode 参数,设置控制寄存器的值,首先确保 mode01,否则抛出 ValueError,接着将不同模式对应的寄存器值写入 REG_CONTROL 控制寄存器地址,并设置 self.mode 为当前模式。
  • 读取滑动寄存器值 read_wiper​方法:​主要功能为读取滑动寄存器的当前值,其使用 readfrom_mem 方法从 REG_WIPER 读取数据。

2.DS3502 实现波形发生器

这里,我们首先需要将风雅一号板-Grove接口扩展板插入到风雅一号板-通用兼容扩展板上,同时使用 HY2.0-4P 排线连接风雅一号板-Grove接口扩展板上的 I2C1 接口和GraftSense-基于DS3502的数字电位器模块上的 I2C 接口:

2.jfif

接着将 风雅一号板-通用兼容扩展板插入到风雅一号板-数据转换板上,同时打开风雅一号板-数据转换板SWITCH1 拨码开关的 SCLSDARDY 选项,同时打开 SWITCH2 拨码开关的 ADC1 开关选项:

3.jfif

4.jfif

然后,需要在 GraftSense-基于DS3502的数字电位器模块 背面,通过将地址选择端 A0A1 的三路焊盘短接至 GNDVCC 来配置器件地址;此外,将 SW1 拨码开关 的 RL 打开以与 GND 短接,并将 V+ 打开以与 RH 短接,并且给 U3 端子上 RLV+ 接入 +5V 电源(推荐使用​GraftSense-基于LM2596S的可调电源模块​):

5.png

6.png

插入后实物图如下:

7.jfif

8.jfif

9.jfif

除此之外,我们首先需要将 MiniUSB 数据线连接到风雅一号板-通用兼容扩展板上的串口,这里,GP0GP1 是树莓派 Pico 的硬件串口 UART0 的默认引脚:

10.png

这里,GraftSense-基于DS3502的数字电位器模块上信号输出接口为端子 U3RW,我们还需要使用 SMA 转杜邦线转接线将GraftSense-基于DS3502的数字电位器模块上端子 U3RW 信号输出接口与风雅一号板-数据转换板上的 ADC0 内部模数转换 SMA 接口相互连接,如下图所示:

11.jfif

整体连接图如下所示:

12.jfif

元器件接线情况如下表所示:

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

在以下示例代码中,在树莓派 Pico 上使用数字电位器芯片 DS3502 生成不同的波形信号,并通过串口输出 ADC0 采集的波形电压值。

首先我们将上一节定义的波形生成 WaveformGenerator 类单独放到一个 dac_waveformgenerator.py 文件中,同时对其进行一些修改:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2025/3/21 下午7:13   
# @Author  : 李清水            
# @File    : dac_waveformgenerator.py       
# @Description : 使用DS3502芯片生成正弦波、三角波、锯齿波的类
# 这部分代码由 leeqingshui 开发,采用CC BY-NC 4.0许可协议

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

# 导入数学库用于计算正弦波
import math
# 导入硬件模块
from machine import Timer
# 导入ds3502模块用于控制数字电位器芯片
from ds3502 import DS3502

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

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

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

class WaveformGenerator:
    def __init__(self, dac: 'DS3502', frequency: float = 1, amplitude: float = 1.65, offset: float = 1.65,
                 waveform: str = 'sine', rise_ratio: float = 0.5, vref: float = 3.3) -> None:
        """
​        初始化波形发生器实例。

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

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

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

​        Raises:
​            ValueError: 如果传入的参数不在预定范围内,将抛出该异常。
​        """
​        ​# 参数输入检查
        if not (0 < frequency <= 10):
            raise ValueError("Frequency must be between 0 and 10 Hz.")
        if not (0 <= amplitude <= vref):
            raise ValueError(f"Amplitude must be between 0 and {vref}V.")
        if not (0 <= offset <= vref):
            raise ValueError(f"Offset must be between 0 and {vref}V.")
        if not (0 <= amplitude + offset <= vref):
            raise ValueError(f"Amplitude + offset must be between 0 and {vref}V.")
        if waveform not in ['sine', 'square', 'triangle']:
            raise ValueError("Waveform must be 'sine', 'square', or 'triangle'.")
        if not (0 <= rise_ratio <= 1):
            raise ValueError("Rise ratio must be between 0 and 1.")
        if vref <= 0:
            raise ValueError("Vref must be greater than 0.")

        # 保存 DS3502 对象
        self.dac = dac

        # 初始化定时器对象
        self.timer = Timer(-1)

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

        # 固定 50 个采样点
        self.sample_rate = 50

        # DS3502 的分辨率为 7 位(128 级)
        self.dac_resolution = 127

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

        # 初始化当前采样点索引
        self.index = 0

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

​        Returns:
​            list[int]: 包含转换为 DS3502 数值的采样点列表。
​        """
​        ​# 将电压值转换为 DS3502 值的函数
        def to_dac_value(voltage):
            # DS3502 的分辨率为 7 位,电压范围为 0 到 vref
            return int(voltage / self.vref * self.dac_resolution)

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

        # 根据选定的波形生成采样点数据
        if self.waveform == 'sine':
            # 正弦波
            for i in range(self.sample_rate):
                angle = 2 * math.pi * i / self.sample_rate
                voltage = self.offset + self.amplitude * math.sin(angle)
                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
                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:
                    voltage = self.offset + 2 * self.amplitude * (
                            i / (self.sample_rate * self.rise_ratio)) - self.amplitude
                else:
                    voltage = self.offset + 2 * self.amplitude * (
                            (self.sample_rate - i) / (self.sample_rate * (1 - self.rise_ratio))) - self.amplitude
                samples.append(to_dac_value(voltage))

        return samples

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

​        Args:
​            t (Timer): 定时器对象。
​        """
​        ​# 将当前采样点的数据写入 DS3502
        self.dac.write_wiper(self.samples[self.index])
        # 更新采样点索引
        self.index = (self.index + 1) % self.sample_rate

    def start(self) -> None:
        """
​        启动波形生成器,开始定时器。
​        """
​        ​self.timer.init(freq=self.frequency * self.sample_rate, mode=Timer.PERIODIC, callback=self.update)

    def stop(self) -> None:
        """
​        停止波形生成器,停止定时器。
​        """
​        ​self.timer.deinit()
        self.index = 0

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

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

修改内容如下:

  • ​在初始化​__init__()​方法中:​将偏置电压作为初始化参数之一,这样,用户可以根据实际硬件配置(如不同的电源电压)动态设置偏置电压,而不是硬编码为 3.3V;同时类型注解中,将入口参数 dac 类型修改为 DS3502,并且在参数检查中,确保 amplitudeoffset 的值不超过 vref,同时新增对 vref 的检查,确保其大于 0。

13.png

同时将属性 dac_resolution 从 12 修改为 7:

14.png

  • 在生成采样点​数据​generate_samples()​方法中:​DS3502 的分辨率为 7 位(128 级),而不是 MCP4725 的 12 位(4096 级),因此需要调整采样点的生成逻辑:

15.png

  • 在定时器回调update()​函数中:​修改 write 方法为 write_wiper,以适配 DS3502 的 API:

16.png

主程序的示例代码如下所示:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2025/3/21 下午3:04   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 使用DS3502数字电位器输出任意波形

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

# 导入硬件模块
from machine import ADC, Timer, Pin, I2C, UART
# 导入时间相关模块
import time
# 导入访问和控制 MicroPython 内部结构的模块
import micropython
# 导入数学库用于计算正弦波
import math
# 导入ds3502模块用于控制数字电位器芯片
from ds3502 import DS3502
# 导入波形生成模块
from dac_waveformgenerator import WaveformGenerator

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

# DS3502芯片地址
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')

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

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

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

# 创建硬件I2C的实例,使用I2C1外设,时钟频率为400KHz,SDA引脚为10,SCL引脚为11
i2c = I2C(id=1, sda=Pin(10), scl=Pin(11), 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:
        # 如果设备地址在0x28-0x2B之间,则为DS3502芯片
        if 0x28 <= device <= 0x2B:
            print("I2C hexadecimal address: ", hex(device))
            DAC_ADDRESS = device

# 创建DS3502对象,使用I2C1外设,地址为DAC_ADDRESS
dac = DS3502(i2c, DAC_ADDRESS)
# 设置DS3502为快速模式(仅写入WR寄存器)
dac.set_mode(1)

# 创建串口对象,设置波特率为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实例:ADC0-GP26
adc = ADC(0)
# 创建软件定时器对象
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=5, amplitude=1.5, offset=1.5, waveform='sine', vref = 5)
# 启动波形生成
wave.start()
# 运行一段时间后停止生成
time.sleep(6)
wave.stop()

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

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

# 停止ADC采集
timer.deinit()

其主要流程和上一节中使用 DAC 芯片 MCP4725 实现波形发生器类似:

17.png

  1. ​首先进行初始化操作:​初始化 I2C 总线,扫描并连接 DS3502 数字电位器;初始化串口(UART)用于数据输出,初始化 ADC 用于采集输出电压,初始化定时器,定时触发 ADC 采集。
  2. ​波形​生成:使用 WaveformGenerator 类生成正弦波、方波和三角波,通过 DS3502 设置滑动寄存器(WR)的值,控制输出电压。
  3. ​数据采集与输出:​定时器每隔 1ms 触发一次,调用 timer_callback 函数读取 ADC 数据,将采集到的电压值通过串口发送。
  4. ​数据输出切换与停止:​每个波形生成 5 秒后停止切换到下一个波形生成,最后停止定时器和 ADC 采集。

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

18.png

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

19.png

000.gif

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

20.png

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

21.png

22.png

23.png

24.png

001.gif

在生成的波形中,可以看到,由于 DS3502 的分辨率较低(7 位),生成的波形有明显的阶梯状,其并不适合在高频或高精度要求的场景中使用。

posted @ 2026-09-03 14:40  FreakStudio  阅读(9)  评论(0)    收藏  举报