Jump to content
  • 0

MCC 1808X as "Echo repeater"


Scardig

Question

Hello,

we have a MCC 1808X and we want to test it as an "echo repeater" that is everything that we read on ANALOG_IN_0 should be played as soon as possible on ANALOG_OUT_0. Our problem is that the signal on ANALOG_OUT_0 is always delayed of 400ms. As a test we used the attached Python code where we also record ANALOG_IN_0 input on file_1.bin and ANALOG_IN_1 on file_2.bin so that if you wire ANALOG_OUT_0 to ANALOG_IN_1 you can see the problem. How can we fix it ?

Thanx

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import time, datetime, serial, socket, struct, sys
from uldaq import get_daq_device_inventory, DaqDevice, AInScanFlag, AOutScanFlag, ScanStatus, ScanOption, create_float_buffer, InterfaceType, AiInputMode


def main():
	f1 = open("file_1.bin", "wb")
	f2 = open("file_2.bin", "wb")
	
	# ==== MCC GENERAL SETUP ====	
	interface_type = InterfaceType.USB
	descriptor_index = 0

	INPUT_SAMPLE_RATE = 100000
	input_low_channel = 0
	input_high_channel = 7
	input_voltage_range_index = 0
	input_scan_options = ScanOption.CONTINUOUS
	input_flags = AInScanFlag.DEFAULT
	input_status = ScanStatus.IDLE
	
	OUTPUT_SAMPLE_RATE = 100000
	output_low_channel = 0
	output_high_channel = 0
	output_voltage_range_index = 0
	output_scan_options = ScanOption.CONTINUOUS
	output_scan_flags = AOutScanFlag.DEFAULT
	output_status = ScanStatus.IDLE

	daq_device = None
	devices = get_daq_device_inventory(interface_type)
	number_of_devices = len(devices)
	if number_of_devices == 0:
		raise Exception("Errore: Nessun dispositivo MCC trovato")
	print("Trovato(i)", number_of_devices, "dispositivo(i) MCC:")
	for i in range(number_of_devices):
		print("  ", devices[i].product_name, " (", devices[i].unique_id, ")", sep = "")
	daq_device = DaqDevice(devices[descriptor_index])

	ai_device = None
	ai_device = daq_device.get_ai_device()
	if ai_device is None:
		raise Exception("Errore: Il dispositivo non supporta un input analogico")
	ai_info = ai_device.get_info()
	if not ai_info.has_pacer():
		raise Exception("\nErrore: Il dispositivo non supporta un input analogico hardware-paced")

	ao_device = None
	ao_device = daq_device.get_ao_device()
	if ao_device is None:
		raise RuntimeError("Errore: Il dispositivo non supporta un output analogico")
	ao_info = ao_device.get_info()
	if not ao_info.has_pacer():
		raise RuntimeError("\nErrore: Il dispositivo non supporta un output analogico hardware-paced")

	descriptor = daq_device.get_descriptor()
	print("\nSto connettendo", descriptor.dev_string, "- attendere...")
	daq_device.connect()

	# ==== MCC INPUT SETUP ====
	input_mode = AiInputMode.SINGLE_ENDED # input_mode = AiInputMode.DIFFERENTIAL

	ranges = ai_info.get_ranges(input_mode)
	if input_voltage_range_index >= len(ranges):
		input_voltage_range_index = len(ranges) - 1

	print("\n", descriptor.dev_string, " INPUT pronto", sep = "")
	print("    Canali:", input_low_channel, "-", input_high_channel)
	print("    Modalità input:", input_mode.name)
	print("    Range:", ranges[input_voltage_range_index].name)
	print("    Sample rate:", INPUT_SAMPLE_RATE, "Hz")
	options = []
	if input_scan_options == ScanOption.DEFAULTIO:
		options.append(ScanOption.DEFAULTIO.name)
	for so in ScanOption:
		if so & input_scan_options:
			options.append(so.name)
	print("    Opzioni di scan:", ", ".join(options))

	# ==== MCC OUTPUT SETUP ====
	chan_string = str(output_low_channel)
	num_channels = output_high_channel - output_low_channel + 1
	if num_channels > 1:
		chan_string = " ".join((chan_string, "-", str(output_high_channel)))

	voltage_ranges = ao_info.get_ranges()
	if output_voltage_range_index < 0:
		output_voltage_range_index = 0
	elif output_voltage_range_index >= len(voltage_ranges):
		output_voltage_range_index = len(voltage_ranges) - 1
	voltage_range = ao_info.get_ranges()[output_voltage_range_index]

	print("\n", descriptor.dev_string, " OUTPUT pronto", sep = "")
	print("    Canali:", chan_string)
	print("    Range:", voltage_range.name)
	print("    Sample rate:", OUTPUT_SAMPLE_RATE, "Hz")
	options = []
	if input_scan_options == ScanOption.DEFAULTIO:
		options.append(ScanOption.DEFAULTIO.name)
	for so in ScanOption:
		if so & input_scan_options:
			options.append(so.name)
	print("    Opzioni di scan:", ", ".join(options))


	def Processa(data_buffer):
		ch_1 = data_buffer[0::8]
		f1.write(struct.pack("<%if" % len(ch_1), *ch_1))
		ch_2 = data_buffer[1::8]
		f2.write(struct.pack("<%if" % len(ch_2), *ch_2))
		return ch_1

	try:
		started = None

		while(True): # MAIN LOOP
			if not started == 1:
				print("running, ctrl+c to stop...")
				started = 1

				output_sample_rate = OUTPUT_SAMPLE_RATE
				output_samples_per_channel = 2 * 4000
				out_buffer = create_float_buffer(output_high_channel - output_low_channel + 1, output_samples_per_channel)
				output_buffer_mid_point = int(len(out_buffer) / 2)

				input_sample_rate = INPUT_SAMPLE_RATE
				input_samples_per_channel = 2 * 4000
				in_buffer = create_float_buffer(input_high_channel - input_low_channel + 1, input_samples_per_channel)
				input_buffer_mid_point = int(len(in_buffer) / 2)				
				input_buff_check = 0 # 0 = lowerhalf, 1 = upperhalf
				actual_input_rate = ai_device.a_in_scan(input_low_channel, input_high_channel, input_mode, ranges[input_voltage_range_index], input_samples_per_channel, input_sample_rate, input_scan_options, input_flags, in_buffer)
				input_status, input_transfer_status = ai_device.get_scan_status()
				while (input_transfer_status.current_index < input_buffer_mid_point):
					input_status, input_transfer_status = ai_device.get_scan_status()
				first_start = True
				to_play = None

			input_status, input_transfer_status = ai_device.get_scan_status()
			if input_transfer_status.current_index > input_buffer_mid_point:
				if input_buff_check == 0:
					input_buff_check = 1
					to_play = Processa(in_buffer[:input_buffer_mid_point])
			else:
				if input_buff_check == 1:
					input_buff_check = 0
					to_play = Processa(in_buffer[input_buffer_mid_point:])

			if to_play:
				output_status, output_transfer_status = ao_device.get_scan_status()
				if output_transfer_status.current_index > output_buffer_mid_point or first_start:
					out_buffer[:output_buffer_mid_point] = to_play
					if first_start:
						actual_output_rate = ao_device.a_out_scan(output_low_channel, output_high_channel, voltage_range, output_samples_per_channel, output_sample_rate, output_scan_options, output_scan_flags, out_buffer)
						first_start = False
				else:
					out_buffer[output_buffer_mid_point:] = to_play
				to_play = None

	except KeyboardInterrupt:
		pass


if __name__ == "__main__":
	main()

 

Link to comment
Share on other sites

7 answers to this question

Recommended Posts

  • 0

If you want the two processes to run close together, call a_in_scan and a_out_scan immediately after each other. The two should be close if you provide a_out_scan with the buffer from a_in_scan. In other words, a_in_scan will write directly to a_out_scan's buffer. 

Another solution is to set both subsystems to use the external clock scan option (BACKGROUND + CONTINUOUS + EXTCLOCK) and then use a timer output to supply the external clock frequency. Both will start on the timer's first pulse, and both will be synchronized with each other. Both the a_in_scan and a_out_scan will return immediately, allowing you to call pulse_out_start to start the timer output. Just so you know, you will need to get a timer device for this call. 

Link to comment
Share on other sites

  • 0
1 hour ago, JRys said:

Update: Using the same buffer for both input and output will work only if the same number of channels are used for both subsystems. My second suggestion is a better option. 

Just tried your solution with the following modifications on my code (and shorting together TMR0 ICLKI OCLKI):

input_scan_options = ScanOption.CONTINUOUS | ScanOption.EXTCLOCK
output_scan_options = ScanOption.CONTINUOUS | ScanOption.EXTCLOCK

timer_number = 0
frequency = 100000.0 # Hz
duty_cycle = 0.5  # 50 percent
pulse_count = 0  # Continuous
initial_delay = 0.0
idle_state = TmrIdleState.LOW
tmr_options = PulseOutOption.DEFAULT
tmr_device = None

tmr_device = daq_device.get_tmr_device()

actual_input_rate = ai_device.a_in_scan(input_low_channel, input_high_channel, input_mode, ranges[input_voltage_range_index], input_samples_per_channel, input_sample_rate, input_scan_options, input_flags, in_buffer)

actual_output_rate = ao_device.a_out_scan(output_low_channel, output_high_channel, voltage_range, output_samples_per_channel, output_sample_rate, output_scan_options, output_scan_flags, out_buffer)

(frequency, duty_cycle, initial_delay) = tmr_device.pulse_out_start(timer_number, frequency, duty_cycle, pulse_count, initial_delay, idle_state, tmr_options)    


                                    
Everything is working (input is sampled at 100000 Hz and played at 100000 Hz.) BUT the 400 ms delay is still there: as you can see from the following image (input signal VS the output signal) it shows itself as a 400ms of "silence" followed by the exact replica of the input signal (obviously with a 400 ms delay)

 

Where I'm doing wrong ?

Clipboard_01-03-2024_02.jpg

Link to comment
Share on other sites

  • 0

When the output scan starts, the driver takes half the buffer immediately. If the lower half is updated, it will go out once the buffer rolls over. You can call get_status directly afterward to see how much it grabbed.  Now that I think about it, this may be why your original program also had a delay. 

 

Link to comment
Share on other sites

  • 0
41 minutes ago, JRys said:

When the output scan starts, the driver takes half the buffer immediately. If the lower half is updated, it will go out once the buffer rolls over. You can call get_status directly afterward to see how much it grabbed.  Now that I think about it, this may be why your original program also had a delay. 

 

I've already taken this into account: if you look at my original code I start a_out_scan the first time only after I have the first half of the input buffer AND already copied it in the first half of the output buffer. The initial delay is related to the input buffer dimension, that is, the play will start only after half the input buffer is collected ( so, the lower the buffer dimension the shortest delay) BUT I cannot go lower than 400ms. WHY ?

Link to comment
Share on other sites

  • 0

To demonstrate that using the external clock to synchronize the AI with the AO works, I created a program that uses a single channel for AI and AO, and on the device, I physically wired them together. Before calling a_out_scan, I preloaded the buffer with sine wave data. Once started, I monitor for the input get_status index to pass the midpoint or for it to roll over. When it does, I print a few values from each buffer and the results show they are in sync. However, the input is off by one, but this is to be expected because both operations take place on the same clock edge. Suppose the buffer is updated with different data immediately after the a_out_scan function call, and the buffer is set to hold one second of data. In that case, I see the change approximately a half second later, and it is because the driver takes half the buffer each time to work on. When the AO Index points to the lower half, the driver has already taken the upper half.

 

Link to comment
Share on other sites

  • 0
15 hours ago, JRys said:

To demonstrate that using the external clock to synchronize the AI with the AO works, I created a program that uses a single channel for AI and AO, and on the device, I physically wired them together. Before calling a_out_scan, I preloaded the buffer with sine wave data. Once started, I monitor for the input get_status index to pass the midpoint or for it to roll over. When it does, I print a few values from each buffer and the results show they are in sync. However, the input is off by one, but this is to be expected because both operations take place on the same clock edge. Suppose the buffer is updated with different data immediately after the a_out_scan function call, and the buffer is set to hold one second of data. In that case, I see the change approximately a half second later, and it is because the driver takes half the buffer each time to work on. When the AO Index points to the lower half, the driver has already taken the upper half.

 

I agree with the results of your test (and thanx for the effort in trying it on real hardware) but, even if the basic principle is the same, my setup is the "opposite": read AI then write AO. Anyway, by taking a look at the main loop of my code in the first post (that you can run in whole without modifications to reproduce the behaviour: no timer involved, just wire AO0 to AI1 and use an external wavegen to AO0) this is what happens when I first start my script:

1. OUTPUT and INPUT setup

output_sample_rate = 100000
output_samples_per_channel = 2 * 4000
out_buffer = create_float_buffer(output_high_channel - output_low_channel + 1, output_samples_per_channel)
output_buffer_mid_point = int(len(out_buffer) / 2)

input_sample_rate = 100000
input_samples_per_channel = 2 * 4000
in_buffer = create_float_buffer(input_high_channel - input_low_channel + 1, input_samples_per_channel)
input_buffer_mid_point = int(len(in_buffer) / 2)                
input_buff_check = 0 # 0 = lowerhalf, 1 = upperhalf

2. Then I wait to have the in_buffer lower-half loaded (= 4000 samples of data) before proceeding further:

input_status, input_transfer_status = ai_device.get_scan_status()
while (input_transfer_status.current_index < input_buffer_mid_point):
    input_status, input_transfer_status = ai_device.get_scan_status()
first_start = True
to_play = None

3. Then I use current_index to determine in which half of the buffer the driver is working on (at first start, given the above lines, it's in the upper half so I'm going to Process the above 4000 samples)

input_status, input_transfer_status = ai_device.get_scan_status()
if input_transfer_status.current_index > input_buffer_mid_point:
    if input_buff_check == 0:
        input_buff_check = 1
        to_play = Processa(in_buffer[:input_buffer_mid_point])
else:
    if input_buff_check == 1:
        input_buff_check = 0
        to_play = Processa(in_buffer[input_buffer_mid_point:])

4. Now I have 4000 samples to play and I do it with the following lines (please note that I first preload out_buffer with my 4000 samples and THEN I start a_out_scan for the first time)

if to_play:
    output_status, output_transfer_status = ao_device.get_scan_status()
    if output_transfer_status.current_index > output_buffer_mid_point or first_start:
        out_buffer[:output_buffer_mid_point] = to_play
        if first_start:
            actual_output_rate = ao_device.a_out_scan(output_low_channel, output_high_channel, voltage_range, output_samples_per_channel, output_sample_rate, output_scan_options, output_scan_flags, out_buffer)
            first_start = False
    else:
        out_buffer[output_buffer_mid_point:] = to_play
    to_play = None

5. Main loop now goes back at point 3.

If you run the above code by changing input and output samples_per_channel to, let's say, 2 * 100000, you correctly see 1 second of "silence" in the start of the second file (this is expected since I have to wait 1 second to have something to play thus my output will play 1 s later). 2 * 50000 -> half second later, ... BUT when I go lower I hit the limit of 400ms.

Am I doing something wrong or I'm I hitting some hardware limit (MCC, Linux(Raspberry PI), Python, ...) ?

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