Data AcQuisition And Real-Time AnalysisScope - Spectrum - Spectrogram - Signal Generator
Software for Windows
Science with your Sound Card!
Contact us about
Pulse Meter Mini-App
Pulse_Meter is a macro mini-app that is included with Daqarta. It measures frequency, pulse width, and duty cycle on all active channels, displaying each channel in a separate color-coded custom meter.
Note that Pulse_Meter is designed to work with rectangular pulses or square waves; it will not work properly with other waveforms (like sine or triangle) without modification.
Set Trigger Level to half of Pos peak. Set desired Trigger Slope. Right-click any Meter for Help.
Note that Trigger is automatically toggled off when Pulse_Meter starts. This is necessary in order to measure waveforms slower than will fit on one 1024-sample display screen... about 47 Hz at the default 48000 Hz sample rate. If all channels that you want to measure are faster than this, you can toggle Trigger back on.
By default, Pulse_Meter also loads PulseTest.GEN to provide signals for initial testing. See Test Waveforms, below for a discussion and a simple way to disable this when you are ready.
If there are no active channels for Pulse_Meter to measure, the message instead is:
No active channels. Toggle Input and/or Generator on. Restart Pulse_Meter.
Pulse_Meter displays a meter for each active channel. If a channel is active but there is no rectangular waveform present, or if one is present but the Trigger controls are not adjusted properly, or if it is properly adjusted but the frequency is low, the meter will show "Cycle Wait" until the first cycle is measured.
Each meter is updated when a new cycle is measured. If there are certain active channels that you don't want to measure, just click on the [x] in the title bar of the appropriate meter.
As noted in the message, Trigger Level should be roughly half of the positive peak of the pulse. This is usually not critical, and can be adjusted while running even when Trigger has been toggled off.
Note, however, that this one setting must be a compromise among all the channels you want to measure. This is usually only an issue on Input channels, in cases where the levels are quite different. At the minimum, you need to set Trigger Level higher than the highest noise peaks on any measured channel. (But see Individual Trigger Slope and Level under Custom Modifications, below.)
Note that Hysteresis is not used in Pulse_Meter measurements, even if set in the Trigger Dialog. Although Hysteresis can give better noise immunity for triggering, it can also reduce pulse width measurement accuracy.
Once the meters are working satisfactorily, you can cancel the Trigger message with its [x] button. You can also close the Trigger dialog and re-open it at any time.
Closing all meters will cancel the mini-app and the Trigger message, as will attempting to re-run Pulse_Meter (which is often simpler than closing multiple meters).
Right-clicking any meter at any time will open Help to this topic.
Pulse_Meter assumes that you want to measure the width of pulses having the selected Trigger Slope; in other words, Pos will measure positive-going pulses and Neg will measure negative-going pulses. You can toggle the Trigger Slope buttons at any time during operation; each meter will need one full cycle after the change to produce a correct measurement.
The reported duty cycle will reflect your Trigger Slope choice. For example, if a positive-going pulse is high for 5% of a cycle, then setting Trigger Slope to Pos will result in the expected 5% duty cycle reading. Setting it to Neg will result in a 95% reading.
For your initial testing and familiarization, Pulse_Meter automatically loads the PulseTest.GEN Generator setup when run. This creates a 5 msec pulse on the Left Out channel at a nominal 10 Hz frequency, which is frequency modulated (FM) at +/-1 Hz deviation over a 10 sec period (0.1 Hz modulation frequency.) The Pulse Width Units are specified in Seconds (instead of Percent or Degrees), so the pulse width is constant even though the frequency (and hence duty cycle) are continuously changing.
The Right Out channel has a 100 Hz square wave with similar FM applied, so the nominal pulse width is also 5 msec, but this does change along with frequency. (Since it's a square wave, the duty cycle stays at 50%).
You can use a standard computer audio cable as a "loopback" from Line Out (or Spkr) to Line In to test Pulse_Meter with Input signals. Be sure to select Line in the Input dialog (thin unmarked button beneath Input on the toolbar). Make sure Input is toggled on, and adjust the Input Level at the bottom of that dialog, as well as the Generator output volume (F9 key), until the input signals aren't clipped... you should see a sharp peak after each edge, followed by a decay, with no flat spot on the peak.
Sound card inputs are AC-coupled, so they can't reproduce the flat DC portions that you see in the Generator signals. In fact, sound card outputs are also AC-coupled as well, and the flat-topped Generator waveforms are really the internal digital signals... by the time they get to the Line Out or Spkr jack they also have the peak-and-decay shape.
After you familiarize yourself with Pulse_Meter and are ready to use it for your own measurements, you can put a semicolon in front of the A.LoadGEN="PulseTest" line to disable it. Then use your own Generator setup (or none at all for externally-generated signals).
As noted in the Introduction, Trigger must be off to measure low frequencies. This is because Pulse_Meter can only operate on data that is acquired and made available for display, where it can be intercepted by the background task (_Pulse_Mtr_Task) that does the actual measurement work. In triggered operation, data is only acquired for display on the triggered edge of a pulse; with a wide pulse, the trailing edge may not be acquired at all.
When Trigger is off, however, data is acquired and displayed as fast as possible, as set by the Trace Update Interval. The default is 10 msec, which is less than half of the 1024-sample screen width at the default 48000 Hz sample rate. (1024 / 48000 = 0.02133 sec, or 21.33 msec.) This means that each display update will start about midway through the waveform shown on the prior update; this overlap insures that no samples are missed by _Pulse_Mtr_Task.
That assumes that Windows (and your system's graphics hardware) can actually draw the display in less than 10 msec... usually a safe assumption with simple pulse waveforms. But it also takes time to draw the text of the meter displays, and on some systems Windows has serious speed problems with large fonts.
For this reason Pulse_Meter sets all meter fonts to 50 pixels tall by default. You can drag a corner of each meter to enlarge it, but be careful... if the updates slow down to the point that there are gaps in the acquired data, pulse edges may be missed and you may see strange jumps in the measurements. For example, the frequency reading may fall to half the proper value due to missing the end of a cycle, making the full cycle appear twice as long when the next cycle end is found instead.
If you want to monitor the speed impact of large meters, toggle Spectrogram (Sgram) on and look at the X-axis. A full spectrogram screen is made up of 512 separate spectra, each shown as a vertical line taken every Trace Update Interval msec. At 10 msec per line, 512 lines gives 5.12 seconds for the full screen.
If the X-axis shows 10 seconds or more you should reduce the size of the meters, or toggle off any that you can do without.
If your system is fast enough and you want large meters, modify the UF=50 line in the Pulse_Meter macro to a larger value. (See the listing below.) Alternatively, delete the MtrV="<F(UF)" line just below that so that the meter fonts aren't set at all, meaning they start out at whatever size you last used. Either method will save you from having to resize the meters every time you start Pulse_Meter.
This is the macro you run to set up and install the _Pulse_Mtr_Task background task that runs on every trace update to perform the actual measurements. Pulse_Meter first checks to see if the task is already running, and if so assumes you want to exit.
Otherwise, it loads the PulseTest.GEN Generator setup, sets needed parameters, sets the titles, font sizes, and colors of the custom meters that will display the pulse measurements, displays "Cycle Wait" on each, and installs the task.
The meter color scheme is controlled by the UD=1 command. The UD variable is tested in the subsequent WHILE loop; if it is 1 the meters are shown with the same colors as the main display. By default, the background is dark blue, and the text is shown in the trace color for its respective channel: Left In is yellow, Right In is red, Left Out is green, Right Out is violet.
Alternatively, if you change the script to use UD=0 all the meter text is black, and the backgrounds are lighter versions of their respective channel trace colors. This uses the _Color_Tint macro to adjust the tint by the amount given in the QS=160 command; zero gives no change, positive values give lighter background tints, negative colors give darker shades.
Pulse_Meter then opens the Trigger dialog and shows the startup message. Note that the script breaks the message into separate lines with +n_ after each string of text. The +n specifies a new line in the displayed message, and the underscore specifies that the Msg command continues on the next script line, which starts with '+'.
;<Help=H490B Close= ;Close any open data file Task="?_Pulse_Mtr_Task" ;Task installed? IF.Task=1 ;IF so, Task="-_Pulse_Mtr_Task" ;Uninstall it Trig=1 ;Restore Trigger Gen=0 ;Generator off Mtr0= ;Remove Meters Mtr1= Mtr2= Mtr3= Msg= ;Remove Message ELSE. ;Else if not installed yet, A.LoadGEN="PulseTest" ;Load GEN file for test IF.Ch?0 | Ch?1 | Ch?2 | Ch?3=0 ;Any active channels? Msg="No active channels." +n _ +"Toggle Input and/or Generator on." +n _ +"Restart Pulse_Meter." ELSE. ;If so, Buf1="<=(0)" ;Fill Buf1 array with 0s Buf0#h=0 ;No Buf0 hysteresis TrigLevUnit=0 ;Trigger Level units = % TrigDelay=-10 ;Neg Trig Delay to see edge Trig=0 ;Trigger off at start UD=1 ;Use main display color scheme Ch=0 ;Set chan 0 (Left In) first WHILE.Ch=<4 ;Do all chans 0-3 MtrV="<<"+Ch(c) ;Meter title to "Left In", etc UF=50 ;Meter font size, pixels MtrV="<F(UF)" ;Set font IF.UD=0 ;Use alternate color scheme? MtrV="<C(0)" ;If so, set meter text to black QS=160 ;Color tint change QC=ColorNum?c ;Color of channel to tint @_Color_Tint ;Get lighter trace color in QC MtrV="<B(QC)" ;Use it for meter background ELSE. ;Else use main trace colors MtrV="<C(ColorNum?c)" ;Set meter text to trace color MtrV="<B(ColorNum?S)" ;Set background to screen color ENDIF. MtrV="<H490B" ;Set Help IF.chan(Ch)=1 ;Channel active? MtrV="Cycle Wait" ;Show meter if so ENDIF. Ch=Ch+1 ;Next channel WEND. ;Repeat for all chans 0-3 Task="_Pulse_Mtr_Task" ;Install Task TrigDlg=1 ;Show Trigger dialog Msg="Set Trigger Level to half of Pos peak." +n _ +"Set desired Trigger Slope." +n _ +"Right-click any Meter for Help." ENDIF. ENDIF.
This task runs every time the screen is updated, which defaults to 10 msec (100 times per second). For each active channel, it calls @_Pulse_Mtr_Update to detect pulse edges and keep track of their absolute time positions.
Custom display or Data logging commands can be added at the indicated location. See the Custom Modifications and Data Logging sections, below.
_Pulse_Mtr_Task also checks to see if all meters have been manually closed (via the [x] in the title bar) and uninstalls itself if so.
;<Help=H490B Ch=0 ;Set channel 0 (Left In) first WHILE.Ch=<4 ;Do all chans 0-3 IF.chan(Ch)=1 ;Channel active? @_Pulse_Mtr_Update ;If so, detect edges and update Mtr ENDIF. Ch=Ch+1 ;Next channel WEND. ;Repeat for all chans 0-3 ;(Insert custom display or data logging here) IF.Mtr0?O=0 ;Any meters open? Task="-_Pulse_Mtr_Task" ;Uninstall if not Trig=1 ;Restore Trigger Gen=0 ;Generator off Msg= ;Remove message ENDIF.
This macro is called as a subroutine by _Pulse_Mtr_Task for each active channel on every trace update (every 10 msec). It detects and keeps track of all pulse edges according to their polarity and absolute sample numbers.
Since it must handle up to 4 separate channels, it uses the Buf1 macro array to store intermediate results. There are 6 of these intermediates per channel, plus computed results for frequency, pulse width, and duty cycle, with space reserved for 7 additional values by spacing the channel data in groups of 16 in Buf1. Variable UC is used as the channel index, which is the channel number (0-3) times 16. The usage for each index is:
0 = Current slope (0 = rising, 1 = falling) 1 = Cycle flag (1 = trailing edge found) 2 = Leading edge absolute sample position 3 = Trailing edge absolute sample position 4 = Trigger Slope (leading edge) 5 = Trigger Level, raw units 6 = Computed frequency 7 = Computed pulse width 8 = Computed duty cycle 9-15 = Spare
The Buf0 array is used to hold the raw waveform data for each channel. The same array is thus filled and processed up to 4 times on each trace update, with the results stored in the respective channel sections of Buf1.
The basic strategy is simply to look through the raw data to find the leading edge, then look for the next trailing edge to find the pulse width. The next leading edge after that is the end of the cycle (and the start of the next). The actual implementation is complicated by the fact that the raw waveform arrives in 1024-sample chunks which are typically overlapping, and by the fact that a full pulse width or a full cycle may appear completely within in a single chunk, or be spread over multiple chunks.
A "trick" is used to detect edges: The derivative of the raw waveform data is taken, which just replaces each sample with the difference from the prior sample. This converts rising edges to positive spikes and falling edges to negative spikes, and ignores the "droop" (decay toward zero) caused by sound card AC-coupling. See the Integrals and Derivatives section of Macro Array Math Operations for a discussion and screen-capture image.
To find a positive-going edge, the macro looks for a positive spike that is above the Trigger Level; for a negative-going edge, it looks for a spike that is more negative than a negative version of Trigger Level. To do this it first converts Trigger Level from percent to raw sample units (+/-32767) and saves it in Buf1[UC+5] via Buf1[UC+5]=abs(TrigLevel) / 100 * 32768. It then sets L to +1 for positive edges or -1 for negative depending on the current slope being sought (Buf1[UC]). Then it multiplies these together to get the Buf0 threshold via Buf0#t=Buf1[UC+5]*L, and sets the Buf0 slope via Buf0#s=Buf1[UC]. Finally A=Buf0?E sets A to the first such edge found in Buf0, if any.
If an edge is found, its position is converted to absolute sample number by A=A+Posn?D, which adds the sample position of the display start. Then, if this was a leading edge and the cycle flag indicates a trailing edge was previously found, we show the results for the full cycle by calling the _Pulse_Mtr_Cycle macro. If it was the initial leading edge we scan for a trailing edge in the same buffer. Finding that, we scan for another leading edge and if found show the results for a full cycle, all in the same buffer.
Otherwise, if we have the initial leading edge plus trailing edge only, we set the slope and cycle flags to wait for the next leading edge in a subsequent buffer. If we have the leading edge only, we wait for the next trailing edge.
If the initial edge in the buffer was the trailing edge we scan for the next leading edge in the same buffer, and if found we show the full-cycle results. If not found, we set the flags to wait for it in a later buffer.
;<Help=H490B UC=16*Ch ;Up to 16 values per chan Buf1[UC+4]=TrigSlope ;Store Trigger Slope Buf1[UC+5]=abs(TrigLevel) / 100 * 32768 ;Triger Level raw units Buf0="<=W(Ch)" ;Copy wave of Chan to Buf0 Buf0="<D" ;Take derivative L=sign(0.5 - Buf1[UC]) ;+1 = Pos Slope, -1 = Neg Buf0#t=Buf1[UC+5]*L ;Buf0 trig level Buf0#s=Buf1[UC] ;Set initial Buf0 slope A=Buf0?E ;Find first Buf0 edge IF.A=>0 ;Edge found? A=A+Posn?D ;Absolute sample num if so IF.Buf1[UC]=Buf1[UC+4] ;Current slope = leading edge? IF.Buf1[UC+1]=1 ;If so, is this a complete cycle? @_Pulse_Mtr_Cycle ;Show results if so ELSE. ;Else scan for trailing edge Buf1[UC+2]=A ;Save leading edge Buf0#s=!Buf1[UC+4] ;Set opposite slope Buf0#t=-Buf1[UC+5]*L ;Set negative threshold A=Buf0?e ;Scan for next edge in Buf0 IF.A=>0 ;Trailing edge found? Buf1[UC+3]=A+Posn?D ;Save absolute sample if so Buf0#s=Buf1[UC+4] ;Set slope for leading edge Buf0#t=Buf1[UC+5]*L ;Set threshold A=Buf0?e ;Scan for next leading edge IF.A=>0 ;Leading edge found? A=A+Posn?D ;Get absolute sample @_Pulse_Mtr_Cycle ;Show complete cycle results ELSE. ;Else no leading edge Buf1[UC+1]=1 ;Flag = trailing edge found Buf1[UC]=Buf1[UC+4] ;Looking for leading edge ENDIF. ;Scan next buffer ELSE. ;No trailing edge in buffer Buf1[UC+1]=0 ;Flag = no trailing edge Buf1[UC]=!Buf1[UC+4] ;Looking for trailing edge ENDIF. ;Scan next buffer ENDIF. ELSE. ;Current slope = trailing edge Buf1[UC+3]=A ;Save trailing edge Buf0#s=Buf1[UC+4] ;Set slope to find leading edge Buf0#t=-Buf1[UC+5]*L ;Set threshold A=Buf0?e ;Scan for next leading edge IF.A=>0 ;Leading edge found? A=A+Posn?D ;Get absolute sample IF.A=>Buf1[UC+2] ;Later than prior leading edge? @_Pulse_Mtr_Cycle ;Show complete cycle results ENDIF. ELSE. ;No leading edge in same buffer Buf1[UC+1]=1 ;Flag = trailing edge found Buf1[UC]=Buf1[UC+4] ;Looking for leading edge ENDIF. ;Scan next buffer ENDIF. ENDIF.
This macro is called by _Pulse_Mtr_Update to compute and display the results of a full cycle, including frequency, pulse width, and duty cycle. On input, UC holds the channel pointer (16 times channel number 0-3) which is used to access data previously saved in Buf1. The initial leading edge and the trailing edge are obtained from Buf1 and used to set R and F, respectively. ('R' for Rising and 'F' for Falling, assuming a positive-going pulse.)
On input, A holds the absolute sample number of the leading edge that ends the cycle (and starts the next), so the full cycle duration C (in samples) is given by C=A-R. If it is non-zero, the frequency is found by dividing the sample rate by C.
Note that computed cycle duration may be negative if you have stopped and restarted the input, or changed a Generator setting (which causes it to restart), since the absolute sample number is from the time of the last restart. In this case, the cycle flag and the saved leading and trailing edges are reset, awaiting the start of a new cycle.
The pulse width (in samples) is found as the difference between the trailing and leading edges, and converted to milliseconds by multiplying by 1000 and dividing by the sample rate.
The duty cycle is the ratio of the pulse width over the cycle period, found by D=(F-R) / C * 100 for a reading in percent.
The computed frequency, pulse width, and duty cycle are saved in Buf1 positions 6-8, respectively, relative to the UC channel pointer. These allow subsequent data logging of the most recent measurements at specified intervals, rather than at the arbitrary times when each cycle completes.
;<Help=H490B R=Buf1[UC+2] ;Saved leading edge F=Buf1[UC+3] ;Saved trailing edge C=A-R ;Cycle = new LE - saved LE IF.C=>0 ;Valid cycle? Q=SmplRate?X/C ;Compute frequency, Hz H=(F-R) *1000 / SmplRate?X ;Pulse width, msec D=(F-R) / C * 100 ;Duty cycle, percent Buf1[UC+6]=Q ;Save frequency Buf1[UC+7]=H ;Save pulse width Buf1[UC+8]=D ;Save duty cycle MtrV=Q +" Hz" +n +H +" ms" +n +D +" %" Buf1[UC+2]=A ;Save new leading edge Buf1[UC+1]=0 ;Reset cycle flag ELSE. IF.C=<0 ;Neg = cycle not valid Buf1[UC+1]=0 ;Reset for new cycle Buf1[UC+2]=0 Buf1[UC+3]=0 ENDIF. ENDIF.
Data Smoothing: If you have rapidly-changing or noisy meter values, you can modify the above _Pulse_Mtr_Cycle macro to smooth the data before display. (See Meter Time Constants under Custom Meters for a full discussion of the method.)
Let's use Buf1[UC+9], Buf1[UC+10], and Buf1[UC+11] to hold the smoothed results for frequency, pulse width, and duty cycle, respectively. Set an integer time constant UT, typically with a value between 10 and 100. (You can set UT at the start of _Pulse_Mtr_Cycle for test purposes, but since it is a constant you may want to eventually move that to Pulse_Meter to avoid setting it redundantly on every _Pulse_Mtr_Cycle call.)
Then replace the MtrV line in _Pulse_Mtr_Cycle with:
Buf1[UC+9]=Buf1[UC+9] + (Buf1[UC+6] - Buf1[UC+9]) / UT Buf1[UC+10]=Buf1[UC+10] + (Buf1[UC+7] - Buf1[UC+10]) / UT Buf1[UC+11]=Buf1[UC+11] + (Buf1[UC+8] - Buf1[UC+11]) / UT MtrV=Buf1[UC+9] +" Hz" +n +Buf1[UC+10] +" ms" +n +Buf1[UC+11] +" %"
Single Meter: Alternatively, especially if you want to display custom computations involving more than one channel, you can omit the MtrV line from _Pulse_Mtr_Cycle and instead display a single meter in _Pulse_Mtr_Task.
You should use a fixed rate such as once per second... see the Data Logging example below for the timing method. Then insert your calculations and meter display instead of (or in addition to) the (Log update here) section.
Individual Trigger Slope and Level: By default, _Pulse_Mtr_Update uses the main Trigger Slope and Level for all channels. It stores these in Buf1[UC+4] and Buf1[UC+5], respectively, at the start of _Pulse_Mtr_Update.
You may, however, want each measured channel to use its own settings. For example, you might be interested in a positive-going pulse on one channel and a negative-going pulse on another, which would require different Slope settings.
Or you might need different Level settings due to large amplitude differences between channels.
In either case, you can start out with the default macros and adjust the main Trigger dialog to determine the optimum Slope and Level settings for each meter individually, without regard to proper operation of the others. Then toggle Pulse_Meter off and comment out the second two lines in _Pulse_Mtr_Update by preceding them with semicolons:
;Buf1[UC+4]=TrigSlope ;Buf1[UC+5]=abs(TrigLevel) / 100 * 32768
Finally, edit Pulse_Meter to add your individual settings. For example, if you want Left In to use Pos Slope and 50% Level, and Right In to use Neg Slope and 10% Level you could use:
Buf1=0 ;Left In (Ch0) Slope = Pos Buf1=50/100 * 32768 ;Level = 50% Buf1[16+4]=1 ;Right In (Ch1) Slope = Neg Buf1[16+5]=10/100 * 32768 ;Leve = 10%
Left Out (channel 2) would use Buf1[32+4] and Buf1[32+5], while Right Out (channel 3) would use Buf1[48+4] and Buf1[48+5] for Slope and Level, respectively. Add all these lines after the Buf1="<=(0)" command which clears Buf1.
You can add data logging to _Pulse_Mtr_Task by inserting code at the indicated location. Typically you should not update the log every time the task runs (100 times per second), nor every time any meter is updated (arbitrary times depending on frequency, and possibly multiple meter updates per task run).
Instead, you can use timing code. Set variable T=Timer in Pulse_Meter, then at the indicated spot in _Pulse_Mtr_Task you add code like this:
IF.(Timer - T)=>=1.00 ;Elapsed time >= 1.00 sec? (Log update here) ;Update log file if so T=Timer ;Update time ref ENDIF.
In this case, the log file will be updated about once per second. Change the 1.00 in the first line for other intervals. Timing has about 10 msec variablity due to the display update interval that determines how often _Pulse_Mtr_Task runs, but see also Factors Affecting Performance, above.
The items you choose to write to the log file will depend upon your needs. You can write macro subroutines to handle different applications, such as @_Pulse_Mtr_Log_1, etc.
As a simple example, suppose you only want to log Left In pulse width, with a time-stamp for each entry. Pulse width is stored in Buf1[UC+7] by _Pulse_Mtr_Cycle, where UC=0 for Left In. So the log update would look like:
LogTxt=n + Buf1 + p11 + t ;New line, pulse width, time at col 11
You can of course use frequency (Buf1) or duty cycle (Buf1) in place of pulse width, or use them all. Here is an example that logs everything for Left In with 12-character (0-based) column spacing:
LogTxt=n + Buf1 + p11 + Buf1 + p23 + Buf1 + p35 + t
To log multiple channels you'll probably want to put each on a separate line, with a label. Here's one way to do that:
IF.(Timer - T)=>=1.00 ;Elapsed time >= 1.00 sec? Ch=0 ;Start with Left In WHILE.Ch=<4 ;Do all 4 chans IF.chan(Ch)=1 ;Channel active? UC=16*Ch ;If so, get index ptr LogTxt=n + Ch(c) + p11 + Buf1[UC+6] _ + p23 + Buf1[UC+7] + p35 + Buf1[UC+8] + p47 + t ENDIF. Ch=Ch+1 ;Next channel WEND. T=Timer ;Update time ref ENDIF.
Questions? Comments? Contact us!We respond to ALL inquiries, typically within 24 hrs.
Over 35 Years of Innovative Instrumentation
© Copyright 2007 - 2022 by Interstellar Research
All rights reserved