Wiznet makers

ruilixin6

Published August 24, 2026 ©

182 UCC

0 VAR

0 Contests

0 Followers

0 Following

Original Link

MCP4725 Waveform Generator: MicroPython Code, Timer Driver & Debug Guide

MCP4725 waveform generator MicroPython timer‑driven implementation and debugging process

COMPONENTS
PROJECT DESCRIPTION

【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.

The code below runs on Raspberry‑Pi Pico, it uses the DAC chip MCP4725 to generate various waveform signals, and outputs ADC‑sampled waveform voltage values over serial port. Component wiring is identical to the previous section.

1.png

Source code can be found in the resource package under elegance‑devkit v1\Demo\63 DAC_WaveformGenerator.

Sample code:

# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/1 2:10 PM   
# @Author  : Li Qingshui            
# @File    : main.py       
# @Description : DAC experiment, generate different waveforms using external DAC chip
# ======================================== 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 waveform calculation
import math
# Import mcp4725 module for DAC chip control
from mcp4725 import MCP4725
# ======================================== Global variables ============================================
# MCP4725 chip address
DAC_ADDRESS = 0x00
# Voltage conversion factor
adc_conversion_factor = 3.3 / (65535)
# ======================================== Function definitions ============================================
def timer_callback(timer: Timer) -> None:
    """
    Timer callback function 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
    # Read ADC sample data
    value = adc.read_u16() * adc_conversion_factor
    # Invoke user‑defined callback function
    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
    # Round float value to two decimal places
    formatted_value = "{:.2f}".format(value)
    # Send sampled voltage over serial port
    uart.write(str(formatted_value) + '\r\n')

# ======================================== Custom classes ============================================
# Waveform generator class for generating voltage signals of different shapes
class WaveformGenerator:
    """
    Waveform generator class for generating different voltage‑signal waveforms.
    This class generates sine, square and triangle waveforms via DAC chip such as MCP4725.
    Parameters such as frequency, amplitude and DC offset are configurable.
    Waveform samples are sent periodically to DAC by timer interrupt to produce continuous output.
    Attributes:
        dac (MCP4725): DAC chip instance used for waveform output.
        frequency (float): Signal frequency in Hz.
        amplitude (float): Signal amplitude in Volts.
        offset (float): DC offset in Volts.
        waveform (str): Waveform type, supports 'sine', 'square', 'triangle'.
        rise_ratio (float): Triangle‑wave rise‑slope ratio, range 0 to 1.
        sample_rate (int): Fixed sample count, 50 samples.
        dac_resolution (int): DAC resolution, 12‑bit range 0‑4095.
        samples (list[int]): List of pre‑computed sample points.
        index (int): Current sample index for sequential output.
        timer (Timer): Timer instance for periodic waveform output trigger.
    Methods:
        __init__(dac, frequency=1, amplitude=1.65, offset=1.65, waveform='sine', rise_ratio=0.5):
            Initialize waveform‑generator instance.
        generate_samples() -> list[int]:
            Generate sample‑point array according to selected waveform.
        update(t: Timer) -> None:
            Timer callback, output next sample point.
        start() -> None:
            Start waveform generator.
        stop() -> None:
            Stop waveform generator.
    """
    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:
        """
        Initialize waveform‑generator instance.
        Sets basic parameters: DAC object, signal frequency, amplitude, DC offset, waveform type and triangle‑wave rise ratio.
        Args:
            dac (MCP4725): DAC chip 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 3.3V.
            offset (float, optional): DC offset voltage, default 1.65V. Must be between 0 and 3.3V.
            waveform (str, optional): Waveform type: 'sine', 'square', 'triangle'. Default 'sine'.
            rise_ratio (float, optional): Triangle‑wave rise‑edge ratio, default 0.5, valid 0‑1.
        Returns:
            None: This method returns nothing.
        Raises:
            ValueError: Raised when input parameters are out of valid ranges.
        """
        # Parameter validation
        if not (0 < frequency <= 10):
            raise ValueError("Frequency must be between 0 and 10 Hz.")
        if not (0 <= amplitude <= 3.3):
            raise ValueError("Amplitude must be between 0 and 3.3V.")
        if not (0 <= offset <= 3.3):
            raise ValueError("Offset must be between 0 and 3.3V.")
        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'.")
        if not (0 <= rise_ratio <= 1):
            raise ValueError("Rise ratio must be between 0 and 1.")

        # Store DAC instance
        self.dac = dac
        # Initialize software‑timer; ‑1 means not bound to hardware timer on creation
        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
        # Fixed sample‑point count for discrete waveform
        self.sample_rate = 50
        # 12‑bit DAC output range: 0‑4095
        self.dac_resolution = 4095
        # Generate sample‑point data according to waveform selection
        self.samples = self.generate_samples()
        # Current sample index for sequential output
        self.index = 0

    def generate_samples(self) -> list[int]:
        """
        Generate sample‑point array for selected waveform.
        Computes sample values and converts each sample into DAC‑compatible integer codes.
        Returns:
            list[int]: List of DAC integer sample codes.
        Raises:
            None: This method raises no exceptions.
        """
        # Helper: convert voltage to DAC code
        def to_dac_value(voltage):
            return int(voltage / 3.3 * 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. Writes next sample point to DAC on timer interrupt.
        Args:
            t (Timer): Timer object triggering this callback.
        Returns:
            None: This method returns nothing.
        Raises:
            None: This method raises no exceptions.
        """
        self.dac.write(self.samples[self.index])
        self.index = (self.index + 1) % self.sample_rate

    def start(self) -> None:
        """
        Start waveform generator, enable periodic timer for sample output.
        Returns:
            None: This method returns nothing.
        Raises:
            None: This method raises no exceptions.
        """
        self.timer.init(freq=self.frequency * self.sample_rate, mode=Timer.PERIODIC, callback=self.update)

    def stop(self) -> None:
        """
        Stop waveform generator, disable timer and reset sample index.
        Returns:
            None: This method returns nothing.
        Raises:
            None: This method raises no exceptions.
        """
        self.timer.deinit()
        self.index = 0

# ======================================== Initialization ==========================================
# 3‑second power‑on stabilization delay
time.sleep(3)
# Print debug information
print("FreakStudio : Using DAC to generate differential waveform")
# Create hardware I2C instance: I2C1, 400kHz, SDA=Pin2, SCL=Pin3
i2c = I2C(id=1, sda=Pin(2), scl=Pin(3), 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 0x60 <= device <= 0x61:
            print("I2C hexadecimal address: ", hex(device))
            DAC_ADDRESS = device
# Create DAC object
dac = MCP4725(i2c, DAC_ADDRESS)
# Read DAC configuration
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)
# Configure DAC: disable power‑down, output 0V, write settings to EEPROM
dac.config(power_down='Off',value=0,eeprom=True)
# 50 ms delay; immediate read after configuration causes errors
time.sleep_ms(50)
# Read DAC configuration again
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)
# Create UART instance, 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: ADC1‑GP27
adc = ADC(1)
# Create software‑timer for ADC sampling
timer = Timer(-1)
# Trigger timer_callback every 1 ms for ADC voltage acquisition
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=10, amplitude=1.5, offset=1.5, waveform='sine')
wave.start()
time.sleep(5)
wave.stop()

# Generate square wave
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()

# Generate triangle wave
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()

# Stop ADC sampling timer
timer.deinit()

We implemented a custom WaveformGenerator class to produce frequency‑ and amplitude‑adjustable square, sine and triangle waveforms and output them to DAC.

2.png

  1. In constructor method, validate input parameters (frequency, amplitude, offset, waveform type etc.) to keep them within reasonable ranges. Store DAC handle and waveform parameters, initialize timer object, call generate_samples to pre‑compute sample‑point array from given waveform settings.

3.png

  1. Inside generate_samples method compute sample‑point values for selected waveform:
    1. Sine wave: generate samples using math.sin.
    2. Square wave: produce alternating high‑level / low‑level samples according to sample index.
    3. Triangle wave: generate linearly‑changing samples for rising‑edge and falling‑edge segments. Return completed sample‑point list.
  2. In timer‑callback update method: write current sample value to DAC, increment sample index and wrap‑around to loop through sample array.

4.png

  1. After instantiating WaveformGenerator, invoke start() to initialize timer with frequency self.frequency * self.sample_rate and periodically execute update(). Call stop() to halt timer and reset sample index to zero.

5.png

In main program three WaveformGenerator instances are created sequentially for sine‑wave, square‑wave and triangle‑wave (rise‑ratio 0.8). Each waveform runs for 5 seconds then stops. After all waveforms complete, the ADC‑sampling timer is de‑initialized.

Overall program timing‑sequence diagram:

6.png

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

7.png

Select ASCII for data format, keep other options as default.

8.png

000.gif

Flash firmware and open serial terminal, example output:

9.png

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

10.png

11.png

12.png

13.png

Compared with DDS signal‑generator chips, waveform generation using MCP4725 DAC requires the microcontroller to compute every waveform sample in software and send each point one‑by‑one to DAC. This increases system complexity and consumes more CPU resources. Frequent timer interrupts occur at higher output frequencies; large sample‑point buffers consume significant RAM. Waveform frequency is completely limited by microcontroller DAC‑update speed. Frequency precision and resolution are bounded by MCU clock and timer granularity. Under MicroPython, practical maximum update rate for software timers is 1000 Hz, greatly limiting achievable signal frequency range.

Moreover MCP4725 communicates over I²C bus. Bus congestion or other MCU background tasks can introduce jitter and instability in output waveforms. For real‑world projects, dedicated external DDS chips are generally preferred for waveform‑generation tasks.

Documents
Comments Write