Jump to content
  • 2

PWM, Triggered Acquisition, and Python Scripting


bhclowers

Question

After going through a range of the SDK examples, I'm running into some strange behavior that I'm hoping someone can help explain.  The result I'm trying to achieve is sending out a low duty cycle pulse (Digital or Analog but something similar to a TTL) that triggers the repeated acquisition on the Analog Discovery.  I can achieve this result using the GUI but would like to insert this process into a python script so I can process and save the data.  

When I route W1 into CH1 in the following example what I see is expected.  However, when I try to route the digital out into CH1 things look extremely wonky.  Again, I'd like to see a waveform that would be considered a PWM waveform that has a duty cycle of ~99% low and I'd like to be able to control that duty cycle with a reasonable degree of precision and accuracy.  Any help would be appreciated and hope the solution is easy as I've been staring at this entirely too long.  Am I just trying to put too many points into the digital out?

Another potential solution that I'm not entirely sure how to implement, is loading a custom waveform from a file and running that through the waveform generator.  Is there an example for that that would also fit with the DAQ needs?

Cheers,

Brian

AnalogIn_Acquisition_5.py

Link to comment
Share on other sites

13 answers to this question

Recommended Posts

Hi Brian,

As an potential solution, I would go for a custom output:

CustomOutput = 0b1 << (pulsesLo / pulsesHi +1)
dwf.FDwfDigitalOutDataSet(hdwf, Channel, CustomOutput, 1+ (pulsesLo / pulsesHi))

This way always a sequence of 0b1000...0 is send. So if you want 99% low you send 100 bits of which 1 is high.

Cheers,
VonPuffelen

Link to comment
Share on other sites

This definitely puts me on the right track.  Thank you.  I still have a few questions:

1.) Now that I can create a custom array and see the output (which looks reasonable), how do I scale the frequency to match the range I'm after and the number of elements in my Custom Pulsing Array?  My initial approach was to take the length in time I'd like to pulsing array to cover, divide by the number of points in the pulsing array, convert that to a frequency in Hz and use that number to divide into the system clock.  However, whenever I do that I don't get the result I'm after.

2.) How to correclty set the number of bits in FDwfDigitalOutDataSet. Is it simply the sum of all the elements in the pulsing array?

Again, thanks again for the help.  I see that I'm close but still fumbling...

## Configure Digital Out Channel
hzSys = c_double()
dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(hzSys))
## Enable Digital pulses on IO pin 0
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(0), c_int(1))
## Configure for Custom Output
dwf.FDwfDigitalOutTypeSet(hdwf, c_int(0), DwfDigitalOutTypeCustom)
## prescaler to a function the target acquisition rate and the number of target samples. SystemFrequency/Acq Freq/NumDataPoints
print("DIGITAL Params:")
print(hzSys.value)
print(hzSys.value/hzAcq)
print(int(hzSys.value/hzAcq))

dwf.FDwfDigitalOutDividerSet(hdwf, c_int(0), c_int(int(hzSys.value/20)))#How to set this properly to cover the range I'm after?

pulsesLo = 50
pulsesHi = 974
pulseArray = []
for i in range(pulsesLo+pulsesHi):
  pulseArray.append(0)
for i in range(pulsesHi):
  pulseArray[i] = 1
CustomOutput = (c_byte * len(pulseArray))(*pulseArray)
dwf.FDwfDigitalOutDataSet(hdwf, c_int(0), CustomOutput, c_int(pulsesHi+pulsesLo))#Is this the correct way to set the number of bits
#Start Digital Out
dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))
time.sleep(2)

 

Link to comment
Share on other sites

Hi Brian,

We have to dig deeper into the data types we are using. The integer: 1048576 is binary: 0b100000000000000000000. it contains 20 zeroes and 1 one. This is what we want to send. Lets say we want to send at 100 khz, this means the width of high is 10 microsecond. To get this frequency out we need to divide the main frequency with a divider. This divider is hzSys.value/PrefferedFrequency.

channel0 = c_int(0)
enable   = c_int(1)
pulsesLo = 50
pulsesHi = 974
prefferedFrequency = 100e3
pulse    = c_int( 0b1<<(math.floor((pulsesHi+pulsesLo)/pulsesLo)) )
pulseLength = c_int( 1+math.floor((pulsesHi+pulsesLo)/pulsesLo) )


hzSys = c_double()
dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(hzSys))
dwf.FDwfDigitalOutEnableSet(hdwf, channel0, enable)
dwf.FDwfDigitalOutTypeSet(hdwf, channel0, DwfDigitalOutTypeCustom)
dwf.FDwfDigitalOutDividerSet(hdwf, channel0, c_int(int(hzSys.value/prefferedFrequency)))
dwf.FDwfDigitalOutDataSet(hdwf, channel0, pulse, c_int(pulsesHi+pulsesLo))
dwf.FDwfDigitalOutConfigure(hdwf, enable)

 

Link to comment
Share on other sites

Hi @bhclowers @vonPuffelen

There is no need for custom to create PWM signal. 
The easiest way is using the low and high counters.

DigitalOut_Duty.py

iChannel = 0
hzFreq = 1000 # PWM freq Hz
prcDuty = 1.23 # duty %

hzSys = c_double()
maxDiv = c_uint()
dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(hzSys))
dwf.FDwfDigitalOutCounterInfo(hdwf, c_int(0), 0, byref(maxDiv))

# for low frequencies use divider as pre-scaler to satisfy counter limitation of 32k, up to 0.003% duty resolution
cDiv = int(math.ceil(hzSys.value/hzFreq/maxDiv.value))
# count steps to generate the give frequency
cPulse = int(round(hzSys.value/hzFreq/cDiv))
# duty
cHigh = int(cPulse*prcDuty/100)
cLow = int(cPulse-cHigh)

print "Generated: "+str(hzSys.value/cPulse/cDiv)+"Hz "+str(100.0*cHigh/cPulse)+"% divider: "+str(cDiv)

dwf.FDwfDigitalOutEnableSet(hdwf, c_int(iChannel), c_int(1))
dwf.FDwfDigitalOutTypeSet(hdwf, c_int(iChannel), c_int(0)) # DwfDigitalOutTypePulse
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(iChannel), c_int(cDiv)) # max 2147483649, for counter limitation or custom sample rate
dwf.FDwfDigitalOutCounterSet(hdwf, c_int(iChannel), c_int(cLow), c_int(cHigh)) # max 32768

dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))

 

Link to comment
Share on other sites

vonPuffelen and the Prolific Atilla,

Thank you very much.  I believe I have arrived at a solution (see attached).  It appears to produce the correct output and seems quite stead.  On a somewhat related note, I'm trying to use the PWM output as a trigger for data acquisition at a trigger signal for another piece of hardware.  The challenge I'm seeing now is with the analog data acquisition.  It seems as though the the current script is pulling from the buffer both before and after the trigger.  Is there a way to adjust the range that is pulled from the buffer so that point 0 of the returned array is aligned with the rising edge of the trigger?  As shown in the attachement, the trigger signal is smack in the middle. I imagine I could rotate the data in the python script after it is returned but this, of course, adds to overhead. 

Again, thank you all for your help!  

Cheers,

Brian

AnalogIn_Acquisition_6.py

Capture.PNG

Link to comment
Share on other sites

Atilla,

After digging into the forums and example a bit, I see that in many examples (AnalogIn_Record.py and AnalogIn_Sample.py)  do not use triggers for acquisition.  However, examples like AnalogIn_ShiftScreen.py do.  It is my understanding that AnalogIn_Record can stream the data to disk at a reasonable sampling speed, however, I'm wondering how to marry the idea of triggered signal averaging without missing triggers.  In the AnalogIn_ShiftScreen.py shown below there is a time.sleep statement in the trigger loop, which I assume is designed to capture data after each trigger.  However, if you try to take all the data between triggers and then sleep, won't you miss a tigger?

Let's say you have a trigger pulse every 50 ms and you want to take data at 100ksps on the rising edge of a trigger, if you take analog in data for those 50 ms is there a way to know or test whether you've missed the next trigger?  

I guess, what I'm asking is how to combine the concepts in the AnalogIn and the repeated triggers?  I'd like to realize signal averaging on a trigger and not miss data after a trigger pulse.

Thanks again so much.  I feel like I'm making progress but run into new unanticipated problems...

Brian

 


# begin acquisition
dwf.FDwfAnalogInConfigure(hdwf, c_bool(False), c_bool(True))

for iTrigger in range(1,101):
    # new acquisition is started automatically after done state 

    while True:
        dwf.FDwfAnalogInStatus(hdwf, c_int(1), byref(sts))
        if sts.value == DwfStateDone.value :
            break
        time.sleep(0.001)#Is this necessary and will it cause triggers or data to be missed?
    
    dwf.FDwfAnalogInStatusData(hdwf, 0, rgdSamples, 8192) # get channel 1 data
    #dwf.FDwfAnalogInStatusData(hdwf, 1, rgdSamples, 8192) # get channel 2 data
    
    dc = sum(rgdSamples)/len(rgdSamples)
    print "Acquisition #"+str(iTrigger)+" average: "+str(dc)+"V"
    
dwf.FDwfDeviceCloseAll()

 

Link to comment
Share on other sites

I think I'm running in circles here and not sure how to solve the problem.  There are a few python examples provided in the SDK that seem to address the issues of triggered acquisition (AnalogIn_Trigger.py) and streaming data (AnalogIn_ShiftScreen.py) but I can't seem to figure out how to join the concepts. 

For AnalogIn_Trigger.py the following code has a while loop that, by my understanding, waits for the status of the AnalogIn to reach a state that will allow the data to be pulled out of the system.  However, earlier in the example you need to set the data acquisition rate and the length of the sample to be read.  Simply put, if you are trying to take all the data between the triggers this will not work. For example if you have triggers every 25 ms, sample at 100,000 ksps (every 10 us), and want to take 25 ms worth of data you will miss triggers. You can obviously reduce the number of points you want to take with this example to around 2400 and things will work out ok but you lose the last 100 data points to allow the system to get ready for the next trigger.

for iTrigger in range(1,101):
    # new acquisition is started automatically after done state 

    while True:
        dwf.FDwfAnalogInStatus(hdwf, c_int(1), byref(sts))
        if sts.value == DwfStateDone.value :
            break
        time.sleep(0.001)#Will removal of this cause any issues? Is this the slow point?
    
    dwf.FDwfAnalogInStatusData(hdwf, 0, rgdSamples, numSamples) # get channel 1 data
    #dwf.FDwfAnalogInStatusData(hdwf, 1, rgdSamples, 8192) # get channel 2 data
    
    dc = sum(rgdSamples)/len(rgdSamples)
    print "Acquisition #"+str(iTrigger)+" average: "+str(dc)+"V"

Alternatively, in the ShiftScreen example the data is continuously streamed to the array supporting the matplotlib window but there is no trigger component or way to align these data with a trigger.  What I'm struggling with is how to achieve a data acquisition script that allows for repeated acquisitions and the data following each trigger to be aligned.  For my application it is important not to miss triggers as this negates any effort at quantify a process after each trigger. 

Do I simply stream data from the trigger channel and the analogIn channel and then align them post-acquisition?

I've looked at these post too but can't seem to find a solution:

 

Link to comment
Share on other sites

Hi @bhclowers

If you have trigger every 25ms and you want to acquire similar span of data you will miss every second trigger, since after each acquisition it takes about 10ms to fetch the data to PC and rearm.
To not to loose any trigger event you should use "record" to stream the data to PC, which can be done at rate up to 1-2MHz.

(The sleep/wait in the examples is to reduce CPU usage. Removing this reduces the latency, improves fetch/rearm response.)

Link to comment
Share on other sites

After playing around with saving one channel, figuring out how to detect the rising edge of the incoming locator pulse (I'm purposely not using the word "trigger" here) and folding the data appropriately, how do I stream two channels?  I know that I need to setup both channels to accept data, create and new array for that information to go but how to handle the streaming component?

Is there a simple way to get both channels? I seem to be messing up the account of the cSamples and cAvailable variable when I try to make it happen.

 

cSamples = 0

while cSamples < nSamples:
    dwf.FDwfAnalogInStatus(hdwf, c_int(1), byref(sts))
    if cSamples == 0 and (sts == DwfStateConfig or sts == DwfStatePrefill or sts == DwfStateArmed) :
        # Acquisition not yet started.
        continue

    dwf.FDwfAnalogInStatusRecord(hdwf, byref(cAvailable), byref(cLost), byref(cCorrupted))

    cSamples += cLost.value
        
    if cLost.value :
        fLost = 1
    if cCorrupted.value :
        fCorrupted = 1

    if cAvailable.value==0 :
        continue

    if cSamples+cAvailable.value > nSamples :
        cAvailable = c_int(nSamples-cSamples)
    
    dwf.FDwfAnalogInStatusData(hdwf, c_int(0), byref(rgdSamples, cSamples), cAvailable) # get channel 1 data
    #How to configure cSamples and cAvailable to handle both channels?
    # dwf.FDwfAnalogInStatusData(hdwf, c_int(1), byref(rgdSamples2, cSamples), cAvailable) # get channel 2 data
    cSamples += cAvailable.value

 

Link to comment
Share on other sites

Hi @bhclowers

The acquisition on the scope channels is synchronous and you get the data like this:

...
dwf.FDwfAnalogInStatusData(hdwf, c_int(0), byref(rgdSamples1, cSamples), cAvailable) # get channel 1 data
dwf.FDwfAnalogInStatusData(hdwf, c_int(1), byref(rgdSamples2, cSamples), cAvailable) # get channel 2 data
cSamples += cAvailable.value
...

 

Link to comment
Share on other sites

To add some degree of closure to this issue, I wanted to share my largely functioning solution.  For my particular purposes I intend to fold the data to realize a signal averaging effect though this is not fully implmented in the code.  Compared to the original python API, I wound up building upon the following library

https://github.com/amuramatsu/dwf

Also, you'll need to the munch module:  https://github.com/Infinidat/munch

In short, it takes many of the suggestions from Attila and records both channels in a streaming fashion.  Given that one of my channels is simply monitoring a TTL for a start point for the data, I imagine the digial in stream could be implemented to lessen the memory load.  Generally, I feel that I've achieved 85% of my original goal.  In addition to the digial in recording rather than the analog for the trigger signal, I never was able to figure out how to externally trigger the data acquisition and PWM generation.  Any suggestions on that front would be appreciated.

One last question thought, what parameters do I need to reset if I want to repeat the acquisition within a class?  At present, I need to reinitialize the AI after closing it before I can repeat the acquisition.  Is that the procedue that needs to occur or is there a faster way?

Finally, Atilla, should you ever make it to Pullman, please let me know and I'd be happy to buy you a beer.

 

digilent_interface_v7.py

Link to comment
Share on other sites

Hi @bhclowers

To reduce the memory load you can use the newly added FDwfAnalogInStatusData16 function which returns data in 16bit signed integer.
These samples can be converted to voltage = range*sample/65536 + offset
Using the range and offset returned by FDwfAnalogInChannelRangeGet and FDwfAnalogInChannelOffsetGet.

To trigger the digitalout by external 1 trigger and analogin on digitalout:
...
dwf.FDwfDigitalOutTriggerSourceSet(hdwf, trigsrcExternal1)
dwf.FDwfAnalogInTriggerSourceSet(hdwf, trigsrcDigitalOut)
..,
dwf.FDwfAnalogInConfigure(hdwf, c_bool(False), c_bool(True))
dwf.FDwfDigitalOutConfigure(hdwf, c_int(True))

After closing the device or exiting the process the device needs to be reconfigured.
Otherwise to repeat the acquisition it is suffice to call configure (start) function(s).

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...