---
title: "DS3502 with MicroPython: Fast‑Write Mode & Waveform‑Generation Tips"
url: "https://maker.wiznet.io/ruilixin6/projects/ds3502-with-micropython-fastwrite-mode-waveformgeneration-tips/"
markdown_url: "https://maker.wiznet.io/ruilixin6/projects/ds3502-with-micropython-fastwrite-mode-waveformgeneration-tips/md"
type: "UCC: User Created Content"
author: "FreakStudio"
author_url: "https://maker.wiznet.io/ruilixin6/"
original_author: "Freakstudio"
original_url: "https://f1829ryac0m.feishu.cn/drive/folder/Fh5Eftd3WlbEIIdHGdtcSsIgn7c?from=from_copylfs"
published: "2026-08-24"
language: "en"
tags: ["MicroPython", "Digital-Potentiometer", "DS3502", "I2C"]
likes: 0
views: 114
comments: 0
source: "WIZnet Makers (https://maker.wiznet.io/)"
---

# DS3502 with MicroPython: Fast‑Write Mode & Waveform‑Generation Tips

> Advanced usage of DS3502 fast‑write mode and waveform generation techniques in MicroPython embedded projects

Original author: Freakstudio (source: https://f1829ryac0m.feishu.cn/drive/folder/Fh5Eftd3WlbEIIdHGdtcSsIgn7c?from=from_copylfs)

## Article

【Preliminary Note】The original hardware example in this article was written based on the RP2040. The actual hardware used in this hands-on demonstration features the W55RP20 as the main controller chip. The circuit logic and UF2 flashing operation principles are universally applicable, with only the main controller model differing. The original chip model mentioned in the circuit descriptions below is provided for reference purposes only.

![](https://maker.wiznet.io/upload/ckeditor5/383111824%5F1787581556%2Epng)

1. Custom DS3502 Class

In the code below, a driver for the DS3502 digital potentiometer is implemented. The I2C interface is used to control the position of the wiper register (WR), which changes the resistance value of the potentiometer.

Sample code:

```plaintext
# Python env   : MicroPython v1.23.0
# -*- coding: UTF-8 -*-
# @Time    : 2024/11/4 3:20 PM
# @Author  : Li Qingshui
# @File    : ds3502.py
# @Description : Driver module for digital potentiometer chip DS3502
# ======================================== Import related modules =========================================
# Import time‑related modules
import time
# Import MicroPython related modules
from micropython import const
# Import hardware‑related modules
from machine import I2C
# ======================================== Global variables ============================================
# ======================================== Function definitions ============================================
# ======================================== Custom classes ============================================
# Custom DS3502 digital potentiometer class
class DS3502:
    """
    DS3502 class for operating the DS3502 digital potentiometer chip over I2C bus to adjust resistance values.
    This class encapsulates I2C communication with DS3502. It provides functions to set the wiper register (WR) value, read current wiper position, and configure control register (CR) modes.
    Attributes:
        i2c (I2C): I2C instance used for communication with DS3502.
        addr (int): I2C address of DS3502 (range 0x28 to 0x2B).
        mode (int): Current operating mode (0 or 1), controls write speed and non‑volatile storage behaviour.
    Methods:
        __init__(self, i2c: I2C, addr: int):
            Initialize DS3502 class instance.
        write_wiper(self, value: int) -> None:
            Write to wiper register (WR) to set wiper position.
        read_control_register(self) -> int:
            Read value from control register (CR) to get current write‑mode setting.
        set_mode(self, mode: int) -> None:
            Set write mode of control register.
        read_wiper(self) -> int:
            Read value from wiper register (WR).
    """
    # Class variables: register addresses
    # Wiper register address
    REG_WIPER = const(0x00)
    # Control register address
    REG_CONTROL = const(0x02)

    def __init__(self, i2c: I2C, addr: int):
        """
        Initialize DS3502 class.
        Args:
            i2c (machine.I2C): I2C object for DS3502 communication.
            addr (int): DS3502 I2C address, between 0x28 and 0x2B.
        Raises:
            ValueError: Raised if address is outside 0x28‑0x2B range.
        """
        # Validate I2C address range
        if addr < 0x28 or addr > 0x2B:
            raise ValueError("Address must be between 0x28 and 0x2B")
        # Store I2C object
        self.i2c = i2c
        # Store I2C address
        self.addr = addr
        # Operating mode:
        #   0‑ write to WR and IVR, slow speed, CR = 00h
        #   1‑ write only to WR, fast speed, CR = 80h
        self.mode = 0

    def write_wiper(self, value: int) -> None:
        """
        Write wiper register (WR) to set wiper position.
        Args:
            value (int): Value to write into wiper register (0 to 127).
        Raises:
            ValueError: Raised if value is outside 0‑127 range.
        """
        # Validate input value range
        if value < 0 or value > 127:
            raise ValueError("Value must be between 0 and 127")
        # Write value to address 0x00 of DS3502 to update WR register
        self.i2c.writeto_mem(self.addr, DS3502.REG_WIPER, bytes([value]))
        # Apply delay depending on operating mode
        if self.mode == 0:
            # 100 ms delay for mode 0
            time.sleep_ms(100)

    def read_control_register(self) -> int:
        """
        Read control register (CR) value to determine current write mode.
        Args:
            None.
        Returns:
            int: Control‑register value (0 or 1 representing current mode).
        """
        # Dummy‑write to set target register address
        self.i2c.writeto_mem(self.addr, DS3502.REG_CONTROL, b'')
        # Generate repeated‑start condition and read control register
        data = self.i2c.readfrom_mem(self.addr, DS3502.REG_CONTROL, 1)
        # Update local mode attribute
        if data[0] == 0x80:
            self.mode = 1
        # Return current mode value
        return self.mode

    def set_mode(self, mode: int) -> None:
        """
        Set write mode for control register.
        Args:
            mode (int): Mode selection, either 0 or 1.
        Raises:
            ValueError: Raised if mode is not 0 or 1.
        """
        if mode not in (0, 1):
            raise ValueError("Mode must be 0 or 1")
        # Write corresponding value to control register according to selected mode
        if mode == 0:
            # Set MODE bit to 0: write to WR and IVR
            self.i2c.writeto_mem(self.addr, DS3502.REG_CONTROL, bytes([0x00]))
            self.mode = 0
        else:
            # Set MODE bit to 1: write only to WR
            self.i2c.writeto_mem(self.addr, DS3502.REG_CONTROL, bytes([0x80]))
            self.mode = 1

    def read_wiper(self) -> int:
        """
        Read wiper register (WR) value.
        Args:
            None.
        Returns:
            int: Current wiper‑position value (0 to 127).
        """
        # Dummy‑write to set target wiper‑register address
        self.i2c.writeto_mem(self.addr, DS3502.REG_WIPER, b'')
        # Generate repeated‑start condition and read wiper register
        data = self.i2c.readfrom_mem(self.addr, DS3502.REG_WIPER, 1)
        # Return read wiper‑position value
        return data[0]
# ======================================== Initialization ==========================================
# ======================================== Main program ===========================================
```

Methods provided by this custom class:

![1.png](http://openwrite.cn/uploads/21597/64064/b44a3568-c6f6-4a22-81d8-36dddbc81cfd.png)

`__init__` initialization method Pass in only the I2C interface object and DS3502 I2C address. Perform address‑range check ensuring address lies between `0x28` and `0x2B`. Raise `ValueError` upon invalid address. Store I2C object and address, initialize `mode` to `0`. Mode `0` means slow‑write mode, mode `1` means fast‑write mode.

`write_wiper` method for setting wiper‑register value Changes potentiometer resistance by writing to wiper register. Validate input value is within `0`‑`127`. Use `writeto_mem` to write value into `REG_WIPER` register to update wiper position. When `mode` equals `0` (slow‑write mode), execute a `100 ms` delay.

`read_control_register` method for reading control‑register value Reads `CR` register value to inspect mode status. Uses `readfrom_mem` to fetch `REG_CONTROL` content. If received byte equals `0x80`, set `self.mode` to `1` and return current mode.

`set_mode` method for setting control‑register mode Sets control‑register value based on mode argument. Check input is either `0` or `1`, otherwise raise `ValueError`. Write corresponding register value to `REG_CONTROL` and assign `self.mode`.

`read_wiper` method for reading wiper‑register value Reads current wiper‑register value using `readfrom_mem` against `REG_WIPER`.

1. Waveform Generator Implemented with DS3502

Insert **Elegance‑One Grove‑Interface Expansion Board** onto **Elegance‑One Universal Compatible Expansion Board**. Use `HY2.0‑4P` flat cable to connect the `I2C1` port on the **Elegance‑One Grove‑Interface Expansion Board** to the `I2C` port on the **GraftSense‑DS3502‑based Digital Potentiometer Module**.

![2.jfif](http://openwrite.cn/uploads/21597/64064/8977ee5c-5a2c-49b5-b312-7fbb17139827.jfif)

Then plug the **Elegance‑One Universal Compatible Expansion Board** onto the **Elegance‑One Data‑Conversion Board**. Turn on the `SCL`, `SDA` and `RDY` switches of the `SWITCH1` DIP switch on the Data‑Conversion Board. Also enable the `ADC1` switch option of `SWITCH2`.

![3.jfif](http://openwrite.cn/uploads/21597/64064/5e2aaf1c-cce3-47ef-adf1-86422b9ba829.jfif)

![4.jfif](http://openwrite.cn/uploads/21597/64064/5bd61000-b059-416d-b1ca-88c43a22a370.jfif)

On the reverse side of the **GraftSense‑DS3502‑based Digital Potentiometer Module**, configure device address by short‑circuiting solder pads of address pins `A0` and `A1` to `GND` or `VCC`. Toggle the `RL` switch of `SW1` to short to `GND` and toggle `V+` switch to short to `RH`. Supply +5 V power to `RL` and `V+` terminals on `U3` (recommended: **GraftSense‑LM2596S‑based adjustable power‑supply module**).

![5.png](http://openwrite.cn/uploads/21597/64064/4e5b75b0-34d9-43bc-bbca-1bf1119efe6c.png)

![6.png](http://openwrite.cn/uploads/21597/64064/b34615d4-48dd-43d7-a687-4ca09f360b7a.png)

Physical photos after assembly:

![8.jfif](http://openwrite.cn/uploads/21597/64064/828ef618-a312-44e0-bcb2-4ca3e3d96247.jfif)

![9.jfif](http://openwrite.cn/uploads/21597/64064/97e0b1ae-717c-4d44-9d51-b8d124a4a55c.jfif)

Connect Mini‑USB cable to serial port on **Elegance‑One Universal Compatible Expansion Board**. `GP0` and `GP1` are default hardware UART0 pins for Raspberry Pi Pico.

![10.png](http://openwrite.cn/uploads/21597/64064/7ef84567-2bf2-4701-8fb6-03b311db86d6.png)

Signal output on **GraftSense‑DS3502‑based Digital Potentiometer Module** is the `RW` pin of terminal block `U3`. Use SMA‑to‑DuPont‑wire adapter cable to connect `RW` output of `U3` to the `ADC0` SMA input port on **Elegance‑One Data‑Conversion Board**.

![11.jfif](http://openwrite.cn/uploads/21597/64064/cbea117b-a283-4fb6-84f9-428a26041a61.jfif)

Full connection diagram:

![12.jfif](http://openwrite.cn/uploads/21597/64064/1a322b5e-2125-4c63-a18c-2cae810b4e1c.jfif)

Component wiring table:

Source code is located in resource package under `elegance‑devkit v1\Demo\64 DAC_Digipot`.

The example below runs on Raspberry Pi Pico, uses DS3502 digital potentiometer to generate different waveforms and outputs ADC0‑sampled voltage values over serial port.

The `WaveformGenerator` class from previous section is saved into separate file `dac_waveformgenerator.py` with several modifications.

```plaintext
# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time    : 2025/3/21 7:13 PM
# @Author  : Li Qingshui
# @File    : dac_waveformgenerator.py
# @Description : Class for generating sine, triangle and sawtooth waveforms with DS3502 chip
# This code is developed by leeqingshui, released under CC BY‑NC 4.0 license
# ======================================== Import related modules =========================================
# Import math library for sine‑wave calculation
import math
# Import hardware‑related modules
from machine import Timer
# Import ds3502 module for digital‑potentiometer control
from ds3502 import DS3502
# ======================================== Global variables ============================================
# ======================================== Function definitions ============================================
# ======================================== Custom classes ============================================
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:
        """
        Initialize waveform‑generator instance.
        Sets basic parameters including DS3502 object, signal frequency, amplitude, DC offset, waveform type, triangle‑wave rise ratio and reference voltage.
        Args:
            dac (DS3502): DS3502 digital potentiometer instance for waveform generation.
            frequency (float, optional): Signal frequency, default 1 Hz. Must be greater than 0 and ≤10 Hz.
            amplitude (float, optional): Signal amplitude, default 1.65V. Must be between 0 and vref.
            offset (float, optional): DC offset voltage, default 1.65V. Must be between 0 and vref.
            waveform (str, optional): Waveform type: 'sine', 'square', 'triangle'. Default 'sine'.
            rise_ratio (float, optional): Triangle‑wave rise‑edge ratio, default 0.5, valid range 0‑1.
            vref (float, optional): Reference voltage, default 3.3V. Must be greater than 0.
        Returns:
            None: This method returns nothing.
        Raises:
            ValueError: Raised when input parameters are outside valid ranges.
        """
        # Parameter validation
        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.")
        # Store DS3502 object
        self.dac = dac
        # Initialize timer object
        self.timer = Timer(-1)
        # Save waveform‑generator parameters
        self.frequency = frequency
        self.amplitude = amplitude
        self.offset = offset
        self.waveform = waveform
        self.rise_ratio = rise_ratio
        self.vref = vref
        # Fixed sample‑point count: 50 samples
        self.sample_rate = 50
        # DS3502 resolution: 7‑bit (128 levels)
        self.dac_resolution = 127
        # Generate sample‑point array based on waveform setting
        self.samples = self.generate_samples()
        # Initialize current sample index
        self.index = 0

    def generate_samples(self) -> list[int]:
        """
        Generate sample‑point array for selected waveform.
        Returns:
            list[int]: List of sample values converted to DS3502 integer codes.
        """
        # Helper function to convert voltage to DS3502 code
        def to_dac_value(voltage):
            # DS3502 7‑bit resolution, voltage range 0 to 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:
        """
        Timer callback function to output next sample point.
        Args:
            t (Timer): Timer object.
        """
        # Write current sample value into DS3502
        self.dac.write_wiper(self.samples[self.index])
        # Advance sample index
        self.index = (self.index + 1) % self.sample_rate

    def start(self) -> None:
        """
        Start waveform generator and enable timer.
        """
        self.timer.init(freq=self.frequency * self.sample_rate, mode=Timer.PERIODIC, callback=self.update)

    def stop(self) -> None:
        """
        Stop waveform generator and disable timer.
        """
        self.timer.deinit()
        self.index = 0
# ======================================== Initialization ==========================================
# ======================================== Main program ===========================================
```

Modifications made:

In `__init__()` initialization method Add reference voltage as an initialization parameter, allowing users to dynamically set reference voltage according to real‑world hardware instead of hard‑coding to 3.3 V. Change type annotation of argument `dac` to `DS3502`. Add parameter checks ensuring `amplitude` and `offset` do not exceed `vref` and validate `vref` > 0.

![13.png](http://openwrite.cn/uploads/21597/64064/ab122357-eb72-4b4f-a02c-8af578bbefff.png)

Change attribute `dac_resolution` from 12 to 7.

In `generate_samples()` method Adjust sample‑generation logic for DS3502 7‑bit resolution (128 levels), different from MCP4725 12‑bit (4096 levels).

![15.png](http://openwrite.cn/uploads/21597/64064/eed521c8-2af5-4f11-b2d8-da4452909118.png)

In timer‑callback `update()` function Replace `write` call with `write_wiper` to match DS3502 API.

![16.png](http://openwrite.cn/uploads/21597/64064/f2f1a911-8070-4557-97a9-bea8a3ae8768.png)

Main‑program sample code:

```plaintext
# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time    : 2025/3/21 3:04 PM
# @Author  : Li Qingshui
# @File    : main.py
# @Description : Output arbitrary waveforms using DS3502 digital potentiometer
# ======================================== Import related modules ========================================
# Import hardware‑related modules
from machine import ADC, Timer, Pin, I2C, UART
# Import time‑related modules
import time
# Import MicroPython internal‑structure access module
import micropython
# Import math library for sine‑wave calculation
import math
# Import ds3502 module for digital‑potentiometer control
from ds3502 import DS3502
# Import waveform‑generator module
from dac_waveformgenerator import WaveformGenerator
# ======================================== Global variables ============================================
# DS3502 chip address
DAC_ADDRESS = 0x00
# Voltage conversion factor
adc_conversion_factor = 3.3 / (65535)
# ======================================== Function definitions ============================================
def timer_callback(timer: Timer) -> None:
    """
    Timer callback for periodic ADC reading and invoking user‑defined callback.
    Args:
        timer (machine.Timer): Timer instance.
    Returns:
        None: This method returns nothing.
    Raises:
        None: This method raises no exceptions.
    """
    global adc, adc_conversion_factor
    value = adc.read_u16() * adc_conversion_factor
    micropython.schedule(user_callback, (value))

def user_callback(value: float) -> None:
    """
    User‑defined callback function to process ADC‑sampled voltage and send data over serial port.
    Args:
        value (float): Voltage value sampled by ADC.
    Returns:
        None: This method returns nothing.
    Raises:
        None: This method raises no exceptions.
    """
    global uart
    formatted_value = "{:.2f}".format(value)
    uart.write(str(formatted_value) + '\r\n')
# ======================================== Custom classes ============================================
# ======================================== Initialization ==========================================
# 3‑second delay for power‑on stabilization
time.sleep(3)
print("FreakStudio : Using Digital Potentiometer chip DS3502 to generate differential waveform")
# Create hardware I2C instance: I2C1, 400 kHz, SDA=Pin10, SCL=Pin11
i2c = I2C(id=1, sda=Pin(10), scl=Pin(11), freq=400000)
# Scan I2C‑bus slave devices
devices_list = i2c.scan()
print('START I2C SCANNER')
if len(devices_list) == 0:
    print("No i2c device !")
else:
    print('i2c devices found:', len(devices_list))
    for device in devices_list:
        if 0x28 <= device <= 0x2B:
            print("I2C hexadecimal address: ", hex(device))
            DAC_ADDRESS = device
# Create DS3502 object
dac = DS3502(i2c, DAC_ADDRESS)
# Set DS3502 to fast mode (write only to WR register)
dac.set_mode(1)
# Create UART instance with baudrate 115200
uart = UART(0, 115200)
uart.init(baudrate=115200,
          bits=8,
          parity=None,
          stop=1,
          tx=0,
          rx=1,
          timeout=100)
# Create ADC instance: ADC0‑GP26
adc = ADC(0)
# Create software‑timer object
timer = Timer(-1)
# Trigger timer_callback every 1 ms for ADC voltage sampling
timer.init(period=1, mode=Timer.PERIODIC, callback=timer_callback)
# ======================================== Main program ===========================================
# Generate sine wave
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()
# Generate square wave
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()
# Generate triangle wave
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()
# Stop ADC sampling timer
timer.deinit()
```

Overall workflow is similar to the MCP4725 waveform‑generator example in previous section.

![17.png](http://openwrite.cn/uploads/21597/64064/258941e1-f3ab-442d-8323-329ca92a45b6.png)

1. Initialization: Initialize I2C bus, scan and connect DS3502 digital potentiometer. Initialize UART for data output, initialize ADC for output‑voltage acquisition, set‑up timer for periodic ADC sampling.

2. Waveform generation: Use `WaveformGenerator` class to produce sine, square and triangle waveforms. Control output voltage by setting wiper register (WR) values on DS3502.

3. Data acquisition and transmission: Every 1 ms the timer triggers `timer_callback` to read ADC samples, and sampled voltage values are sent out over serial port.

4. Waveform switching and termination: Each waveform runs for 5 seconds then stops before switching to next waveform. Finally de‑initialize timer and halt ADC sampling.

Launch SerialPlot software, select correct COM port for USB‑to‑TTL adapter and click Open.

![18.png](http://openwrite.cn/uploads/21597/64064/bf779aab-d65f-4cc6-b081-39f835e07f7a.png)

Select `ASCII` for data format, keep remaining settings as default.

![19.png](http://openwrite.cn/uploads/21597/64064/9171f586-f102-4da9-bdd1-f78813702fb1.png)

![000.gif](http://openwrite.cn/uploads/21597/64064/1567055c-2a6c-4ee3-ad63-ea1c87f9b08f.gif)

Flash firmware and open serial terminal, example output:

![20.png](http://openwrite.cn/uploads/21597/64064/c182e80e-c7b7-4795-9f38-9fef8087dea1.png)

Inside SerialPlot you can observe output waveforms with correct frequency and amplitude.

![21.png](http://openwrite.cn/uploads/21597/64064/f037e5d1-4fbe-488f-8ad9-95283dc3f200.png)

![22.png](http://openwrite.cn/uploads/21597/64064/be54ab56-042f-4a58-bed9-65f86f4755f5.png)

![23.png](http://openwrite.cn/uploads/21597/64064/0c5ea4aa-9fac-47f4-bd7d-4544749b5927.png)

![24.png](http://openwrite.cn/uploads/21597/64064/cdab7225-3c89-4a96-9467-410a53e2bc84.png)

Noticeable stair‑step artefacts appear on generated waveforms because DS3502 only provides 7‑bit resolution. It is not suitable for high‑frequency or high‑precision use‑cases.

---

Source: https://maker.wiznet.io/ruilixin6/projects/ds3502-with-micropython-fastwrite-mode-waveformgeneration-tips/
