Jump to content
  • 0

Inquiry about Frequency Control of Ultrasonic Transducers Using AD2


towa

Question

Posted (edited)

Hi Everyone,
I have a question regarding Analog Discovery 2.

I am currently developing a system that uses Analog Discovery 2 to control the frequency of an ultrasonic transducer and keep its vibration amplitude constant. 
Specific objectives and implementation details are as follows:

The following process is based on the “Getting Started with WaveForms SDK” website.

1.    Drive an ultrasonic transducer with a resonant frequency of 28.24kHz at approximately 28.3kHz using the generate() function.
2.    Measure the output voltage of the ultrasonic transducer's pickup sensor using the AD2 oscilloscope function at a sampling frequency of 100MHz for 28µs, and calculate the peak amplitude Vmax using the record() function.
3.    Calculate the difference e(t) between the target sensor voltage value Vtarget (proportional to the target vibration amplitude value) and Vmax, apply a proportional gain to this difference to obtain Δf, and update the drive frequency using the FDwfAnalogOutNodeFrequencySet() function
4.    Repeat steps 2 and 3 to control the amplitude consistently.

Our goal is to get the frequency control period to 40 µs (or at least within 1 ms). However, currently the FDwfAnalogInStatus and FDwfAnalogInStatusData functions take a considerable amount of time, resulting in a control cycle period of approximately 10ms.

Is it possible to implement a solution such as calculating the peak sensor output voltage in the FPGA to achieve the desired processing time? If there are any other ways to reduce the processing time, I would appreciate any suggestions.

Additionally, do I need to purchase an FPGA board for this purpose?

I apologize for my poor English and limited knowledge, but I would appreciate your response.

The files below are what I used:

import time  # To handle timing operations
import ctypes  # To use C libraries in Python
from sys import platform, path  # To handle system path operations
import numpy as np

# Load the Digilent WaveForms SDK shared library using ctypes
dwf = ctypes.cdll.dwf

#------------------------------------------------------------------------------------------------------------------------
# Device connection and disconnection
class Data:  # Variables used in the script
    handle = ctypes.c_int(0)
    name = ""

    # Scope settings
    sampling_frequency = 100e06
    buffer_size = 2801

    # Generator settings
    gen_frequency = 28.3e03
    amplitude = 2.5
    run_time = 2

    # Control time
    vibrating_time = 2

def device_open():  # Open the first connected device
    # Specify the address of the connected device (initial value is integer)
    device_handle = ctypes.c_int()
    # Connect to the first available device by passing -1 as the argument, storing the handle in device_handle
    dwf.FDwfDeviceOpen(ctypes.c_int(-1), ctypes.byref(device_handle))
    Data.handle = device_handle
    return Data

def device_close(device_data):  # Close the device
    result = dwf.FDwfDeviceClose(device_data.handle)
    if result != 1:
        print(f"Failed to close device. Error code: {result}")
    return result

#------------------------------------------------------------------------------------------------------------------------

# Oscilloscope settings
def scope_open(device_data, sampling_frequency=Data.sampling_frequency, buffer_size=Data.buffer_size, offset=0, amplitude_range=50):
    # Enable channel 0
    dwf.FDwfAnalogInChannelEnableSet(device_data.handle, ctypes.c_int(0), ctypes.c_bool(True))
 
    # Set offset voltage
    dwf.FDwfAnalogInChannelOffsetSet(device_data.handle, ctypes.c_int(0), ctypes.c_double(offset))
 
    # Set maximum voltage
    dwf.FDwfAnalogInChannelRangeSet(device_data.handle, ctypes.c_int(0), ctypes.c_double(amplitude_range))
 
    # Set buffer size
    dwf.FDwfAnalogInBufferSizeSet(device_data.handle, ctypes.c_int(buffer_size))
 
    # Set sampling frequency (Hz)
    dwf.FDwfAnalogInFrequencySet(device_data.handle, ctypes.c_double(sampling_frequency))

    # Disable averaging to use raw data
    dwf.FDwfAnalogInChannelFilterSet(device_data.handle, ctypes.c_int(-1), ctypes.c_int(0))
    return

def scope_record(device_data, channel=1):  # Record voltage
    dwf.FDwfAnalogInAcquisitionModeSet(device_data.handle, ctypes.c_int(0))
    dwf.FDwfAnalogInConfigure(device_data.handle, ctypes.c_bool(False), ctypes.c_bool(True))

    # Store the buffer status
    status = ctypes.c_byte()
  
    while True:
        # Check acquisition status, 2: keep reading data, 3: store status in the status variable
        dwf.FDwfAnalogInStatus(device_data.handle, ctypes.c_bool(True), ctypes.byref(status))

        # End when status becomes 2 (data acquisition starts and data is stored in buffer)
        if status.value == ctypes.c_ubyte(2).value:
            break
    
    # Copy the data stored in the buffer
    # Create an empty buffer, with the number of significant digits * buffer size array (all elements initialized to 0)
    buffer = (ctypes.c_double * Data.buffer_size)()
  
    # Copy the acquired data array to the buffer
    dwf.FDwfAnalogInStatusData(device_data.handle, ctypes.c_int(channel - 1), buffer, ctypes.c_int(Data.buffer_size))

    # Convert data to floating point numbers, put them in the buffer list, and get the maximum value
    np_buffer = np.ctypeslib.as_array(buffer)
    
    V_sensor = np.max(np.abs(np_buffer))
    return V_sensor

def scope_close(device_data):
    result = dwf.FDwfAnalogInReset(device_data.handle)
    return

#------------------------------------------------------------------------------------------------------------------------

def generate(device_data, channel=1, function=ctypes.c_ubyte(1), offset=0, gen_frequency=Data.gen_frequency, amplitude=Data.amplitude, symmetry=50, wait=0, run_time=Data.run_time, repeat=100000):
    # Enable the channel (channel 1 is 0)
    channel = ctypes.c_int(channel - 1)
    # Enable the carrier wave
    dwf.FDwfAnalogOutNodeEnableSet(device_data.handle, channel, ctypes.c_int(0), ctypes.c_bool(True))
    # Disable the signal wave (2)
    dwf.FDwfAnalogOutNodeEnableSet(device_data.handle, channel, ctypes.c_int(2), ctypes.c_bool(False))
 
    # Set the waveform
    dwf.FDwfAnalogOutNodeFunctionSet(device_data.handle, channel, ctypes.c_int(0), function)
 
    # Set the frequency
    dwf.FDwfAnalogOutNodeFrequencySet(device_data.handle, channel, ctypes.c_int(0), ctypes.c_double(gen_frequency))
 
    # Set the voltage amplitude 0-p
    dwf.FDwfAnalogOutNodeAmplitudeSet(device_data.handle, channel, ctypes.c_int(0), ctypes.c_double(amplitude))
 
    # Set the offset
    dwf.FDwfAnalogOutNodeOffsetSet(device_data.handle, channel, ctypes.c_int(0), ctypes.c_double(offset))
 
    # Set the symmetry
    dwf.FDwfAnalogOutNodeSymmetrySet(device_data.handle, channel, ctypes.c_int(0), ctypes.c_double(symmetry))
 
    # Set the waveform generation time (seconds)
    dwf.FDwfAnalogOutRunSet(device_data.handle, channel, ctypes.c_double(run_time))
 
    # Set the waiting time before starting
    dwf.FDwfAnalogOutWaitSet(device_data.handle, channel, ctypes.c_double(wait))
 
    # Set the number of repetitions
    dwf.FDwfAnalogOutRepeatSet(device_data.handle, channel, ctypes.c_int(repeat))

    # Start
    dwf.FDwfAnalogOutConfigure(device_data.handle, channel, ctypes.c_bool(True))
 
    return

def generate_close(device_data, channel=0):
    channel = ctypes.c_int(channel - 1)
    dwf.FDwfAnalogOutReset(device_data.handle, channel)
    return

#------------------------------------------------------------------------------------------------------------------------

# Parameters
# Target amplitude (p-p) [μm]
A = 5

# Sensor voltage to amplitude conversion formula parameters V (voltage 0-p) = aA (amplitude p-p) + b
a = # Values are entered when driven
b = # Values are entered when driven

# Target voltage [V]
V_target = a * A + b
print(V_target)

# Gain
K_p = 1

#------------------------------------------------------------------------------------------------------------------------

# Create object, connect to device, and configure oscilloscope
device_data = Data()
device_open()

# Device configuration 2: initialize device parameters (False: do not initialize) 3: continuously acquire specified range data
# Start sampling
scope_open(device_data)

# Start generator operation
generate(device_data)
f_now = device_data.gen_frequency
time.sleep(1)

# Start time
t_start = time.time()

# Constant amplitude control
while True:

    # Measure the maximum value and calculate the error
    V_sensor = scope_record(device_data)

    e = V_target - V_sensor
    print(e)

    # Update amplitude
    df = K_p * e
    f_new = f_now - df
    print(f_new)

    dwf.FDwfDeviceAutoConfigureSet(device_data.handle, ctypes.c_int(3))
    dwf.FDwfAnalogOutNodeFrequencySet(device_data.handle, 0, ctypes.c_int(0), ctypes.c_double(f_new))

    f_now = f_new

    if time.time() - t_start >= 2:
        break
        
# Disconnect the device
generate_close(device_data)
scope_close(device_data)
device_close(device_data)

 

Edited by towa
Link to comment
Share on other sites

5 answers to this question

Recommended Posts

  • 0

Hi @towa

For better performance, to minimize the communication with the device set the following after device open:
# the device will only be configured when FDwf###Configure is called
dwf.FDwfDeviceAutoConfigureSet(hdwf, c_int(0)) 

 

Faster reading only one sample, the last ADC conversion and not transferring capture array:
dwf.FDwfAnalogInConfigure(hdwf, 0, 1)
while True:
        dwf.FDwfAnalogInStatus(hdwf, 0, byref(sts)) # 0 no data
        dwf.FDwfAnalogInStatusSample(hdwf, channel, byref(voltage))
        ...

 

If you need to capture more samples, you could reduce the buffer size from the default 32768:
dwf.FDwfAnalogInBufferSizeSet(hdwf, 512)

and you could use scan screen or shift mode to always return the last N samples
dwf.FDwfAnalogInAcquisitionModeSet(hdwf, acqmodeScanShift)
dwf.FDwfAnalogInConfigure(hdwf, 0, 1)
while True:
        dwf.FDwfAnalogInStatus(hdwf, 1, byref(sts))
        dwf.FDwfAnalogInStatusSamplesValid(hdwf, byref(cValid))
        dwf.FDwfAnalogInStatusData(hdwf, channel, byref(rgdSamples), cValid)

        ...

 

To change the frequency:
        dwf.FDwfAnalogOutNodeFrequencySet(...)
        dwf.FDwfAnalogOutConfigure(... 3) # 3 apply

Link to comment
Share on other sites

  • 0

Thanks @attila!

Quote

For better performance, to minimize the communication with the device set the following after device open:
# the device will only be configured when FDwf###Configure is called
dwf.FDwfDeviceAutoConfigureSet(hdwf, c_int(0)) 

To change the frequency:
        dwf.FDwfAnalogOutNodeFrequencySet(...)
        dwf.FDwfAnalogOutConfigure(... 3) # 3 apply

I used this code but every time I change the frequency, I lose voltage.

Is the only way to dynamically change the frequency to use "dwf.FDwfDeviceAutoConfigureSet(hdwf, c_int(3))" at the expense of speed?

 

Quote

If you need to capture more samples, you could reduce the buffer size from the default 32768:
dwf.FDwfAnalogInBufferSizeSet(hdwf, 512)

and you could use scan screen or shift mode to always return the last N samples
dwf.FDwfAnalogInAcquisitionModeSet(hdwf, acqmodeScanShift)
dwf.FDwfAnalogInConfigure(hdwf, 0, 1)
while True:
        dwf.FDwfAnalogInStatus(hdwf, 1, byref(sts))
        dwf.FDwfAnalogInStatusSamplesValid(hdwf, byref(cValid))
        dwf.FDwfAnalogInStatusData(hdwf, channel, byref(rgdSamples), cValid)

After implementing this code, the time of the loop “measure the output voltage of the ultrasonic transducer's pickup sensor 28 µs at 100 MHz sampling frequency, calculate the peak amplitude Vmax, and update the driving frequency based on the difference between Vmax and the target value of amplitude” was reduced to about 6 ms.
Thanks.


But I would like to reduce this time to less than 1 ms, is there any other way to reduce the time?

Link to comment
Share on other sites

  • 0

Hi @towa

With the following script I see 0.54ms average update rate pid2.py

image.png

It uses record which returns the last N peak values.
Also make sure to use newer software which highly improves the capture and AWG adjustment latency.

 

Link to comment
Share on other sites

  • 0

Thanks @attila!

I have updated the software to b'3.22.23'.

Thanks for sending me the code, I corrected my code based on it, but I have additional three questions.

 

In each loop, I want to measure only the latest value of the maximum in the sensor voltage readings for 28 microseconds.
Am I correct in understanding that if I use filterMinMax, the acquisition frequency should be set based on the following equation?

Acquisition frequency = ADC frequency / N = 10,000,000 / 1400 = 71,429 Hz

 

Since I only need to measure the latest maximum value, I set the buffer to the minimum value of 16.
However, when I run the following code, it takes about 1.3 ms to execute FDwfAnalogInStatus.
What is the problem?

 

I would like to create a code to find the maximum value in the most recent 28 microsecond sensor voltage measurement by processing filterMinMax and then quickly read that single data on a PC using FDwfAnalogInStatusData etc.

What code is needed to perform the above process at high speed?


Also, which should I use with respect to ACQMODE, acqmodeRecord or acqmodeScanShift, to achieve this objective?

 

from ctypes import * 
import time
import numpy as np
dwf = cdll.dwf 

hdwf = c_int()
dwf.FDwfDeviceOpen(-1, byref(hdwf))

if hdwf.value == 0:
    print("failed to open device")
    szerr = create_string_buffer(512)
    dwf.FDwfGetLastErrorMsg(szerr)
    print(szerr.value)
    quit()

dwf.FDwfDeviceAutoConfigureSet(hdwf, c_int(0)) 

# Parameters
f_start = 28.24e03
V_gen = 2.5 # Voltage amplitude 0-p
runtime_gen = 2
K_p = 0.8

# Amplitude Data Acquisition Frequency
adc_frequency = 100e06 # AD conversion frequency [Hz]
cycle = 28e-6
N = cycle / (1/adc_frequency) / 2
acquisition_frequency = adc_frequency / N

# Buffer min: 16
buffer_size = 16

# Target V (0-p) = a*A (p-p) + b
a = 0.4712
b = -0.2923
A_target = 5 # p-p
V_target = a*A_target + b # 0-p

# Generate 1:hdwf 2:channel 3:node
dwf.FDwfAnalogOutNodeEnableSet(hdwf, 0, 0, 1)
dwf.FDwfAnalogOutNodeEnableSet(hdwf, 0, 2, 0) # Disable AM modulation
dwf.FDwfAnalogOutNodeFunctionSet(hdwf, 0, 0, c_ubyte(1))
dwf.FDwfAnalogOutNodeFrequencySet(hdwf, 0, 0, c_double(f_start))
dwf.FDwfAnalogOutNodeAmplitudeSet(hdwf, 0, 0, c_double(V_gen))
dwf.FDwfAnalogOutNodeOffsetSet(hdwf, 0, 0, c_double(0))
dwf.FDwfAnalogOutNodeSymmetrySet(hdwf, 0, 0, c_double(50))
dwf.FDwfAnalogOutRunSet(hdwf, 0, c_double(runtime_gen))
dwf.FDwfAnalogOutWaitSet(hdwf, 0, c_double(0)) # Waiting time before start
dwf.FDwfAnalogOutRepeatSet(hdwf, 0, c_int(100000))
dwf.FDwfAnalogOutConfigure(hdwf, 0, 1)

# Scope
dwf.FDwfAnalogInChannelEnableSet(hdwf, 0, 1)
dwf.FDwfAnalogInChannelOffsetSet(hdwf, 0, 0)
dwf.FDwfAnalogInFrequencySet(hdwf, c_double(acquisition_frequency))
dwf.FDwfAnalogInChannelRangeSet(hdwf, 0, c_double(50)) # Maximum measurable voltage: -25~25V
dwf.FDwfAnalogInAcquisitionModeSet(hdwf, c_int(3)) # [3:record 1:shift]
dwf.FDwfAnalogInRecordLengthSet(hdwf, c_double(-1)) # Record data for an infinite time
dwf.FDwfAnalogInChannelFilterSet(hdwf, 0, c_int(2)) # filterMinMax: Return the maximum and minimum values inside the buffer
dwf.FDwfAnalogInBufferSizeSet(hdwf, c_int(buffer_size))
dwf.FDwfAnalogInConfigure(hdwf, 1, 1) # Apply new settings if the second argument is 1

time.sleep(1)

sts = c_int() # Status
cAvailable = c_int() # Number of available samples

t1 = time.perf_counter_ns()
dwf.FDwfAnalogInStatus(hdwf, 0, byref(sts))
t2 = time.perf_counter_ns()

print((t2-t1)/1e6)

dwf.FDwfDeviceCloseAll()

 

 

Link to comment
Share on other sites

  • 0

Hi @towa

You can use ScanShift, like here PID with 28 samples at 1MHz pid3.py

image.png

Edit: The code uses custom sine which gives lower latency for newer devices (AD3...), since these try to generate more precise frequencies which takes a bit more time. For AD1,2 it doesn't matter if funcSine or custom sine is used.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...