Sending triggers and pulse trains using the digital output schedule
Digital triggers are a form of rapid, simple communication between different experiment systems. For an in-depth explanation, see our Introduction to digital signalling.
There are several ways to send digital output triggers on our devices:
Via Pixel Mode: Sending Triggers with Pixel Mode
By setting DOut values directly in your experiment code: Send a digital output trigger on VSync
Loading DOut values or sequences into our hardware and triggering playback from our device.
In this demo, we show how to implement the third option.
VPixx data acquisition systems have onboard memory for recording, storing, and playing digital, audio and analog signals. This data management is handled using a schedule that defines specific properties such as the address of data in memory, the signal type, playback rate, and more. You can learn more about schedules in our VOCAL guide The Logic of VPixx Hardware Control.
DOut schedules are very useful when sending many signals with limited channels on your receiver. Consider a receiver with only two digital input channels. Using Pixel Mode or direct programming of the ports, you can send 22 unique signals to the receiver:
Signal | Channel 1 | Channel 2 |
---|---|---|
1 | 0 | 0 |
2 | 1 | 0 |
3 | 0 | 1 |
4 | 1 | 1 |
This is not a very good resolution, and we quickly run out of options for sending complex event codes to our receiver.
Digital schedules allow us to send signal waveforms or pulse trains. This way, a single channel can encode multiple ‘events’ that are distinguishable by the presence and number of pulses. For instance, we can use channel 1 to send three types of information about our participant’s responses:

No signal indicates the participant is not responding; a single pulse is used to denote a successful response; and a double pulse indicates a failure. These waveforms are stored in unique addresses in VPixx device memory. During an experiment, we instruct the hardware to play signals at the specific addresses corresponding to the message we want to pass to the receiver. So for instance, our single pulse on channel 1 is stored at address 10,000 in memory. In our code, we would write something like:
if correct_response
play_signal(10000)
There are other parameters we can configure as well, such as playback speed and maximum playback time. We will see how to implement this in the code below.
Demo: Sending triggers to a 2-channel system
In this demo, we present a blue or green dot on the center of the screen. Depending on the dot's colour, the participant must respond by pressing ‘b' or 'g’ keys on the keyboard.
We assign Channel 0 to keep track of stimulus onset with a single pulse and Channel 1 to keep track of responses ('Correct' and ‘Error’) with single and double pulses, respectively. Let’s visualize this demo using a LabMaestro Hardware Simulator Home. We configure the signal viewer to show channels 0-2:
Recording of demo. Left side shows main display with experiment; right side is experimenter’s display with the LabMaestro Simulator signal viewer.
We can see that in the above recording, the participant answered correctly on trials 1, 2 and 5. They answered incorrectly on trials 3 and 4. Below is the code to implement this demo. Helper functions configureDigitalOutputs
and sendTrigger
are used to manage the configuration and playback of our signals.
from psychopy import visual, core
from psychopy.hardware import keyboard
import random
from pypixxlib import _libdpx as dp
def configureDigitalOutputs(config_dict, currentAddress):
"""
Configures digital outputs based on the provided trigger dictionary and
assigns a memory address. Each signal's bits are shifted according to the
specified output channel, and then written to the VPixx hardware memory.
Parameters:
config_dict (dict): Dictionary with entries in the format:
{
'event_name': {'signal': [0, 1, 0, ...], 'channel': int
}
}
currentAddress (int): The starting memory address for storing signal
data.
Returns:
config_dict (dict): The updated dictionary with assigned memory addresses
for each event.
"""
# Loop through each event in the configuration dictionary
for event, details in config_dict.items():
# Ensure the currentAddress is even; if it's odd, increment by 1
if currentAddress % 2 != 0:
currentAddress += 1
details['address'] = currentAddress
channel = details.get('channel')
signal = details.get('signal', [])
signal_length = len(signal)
# Shift each bit in the signal to the left by the value of the channel.
# This positions the bit correctly for the digital output channel.
toggled_signal = [(bit << channel) for bit in signal]
# Write the modified signal (toggled_signal) into the VPixx hardware memory
# at the specified address.
dp.DPxWriteRam(currentAddress, toggled_signal)
print(f"Configured: {event} is {signal} on DOut channel {channel}")
# Update the current memory address by adding the length of the signal.
# Important to multiply by 2 to reserve enough space.
currentAddress += signal_length * 2
# After configuring all events, commit changes to the register cache of the
# hardware
dp.DPxWriteRegCache()
return config_dict
def sendTrigger(entry, delay=0.0, samplingRate=10):
"""
Sends a digital trigger signal based on the provided dictionary entry.
Parameters:
entry (dict): A dictionary entry containing keys 'signal', 'channel',
and 'address'.
delay (float): Delay (in seconds) before the trigger signal starts
(default is 0.0).
samplingRate (int): Sampling rate in Hz for the digital output (default
is 10).
"""
# Determine the length of the signal (number of bits) for scheduling purposes
signal_length = len(entry.get('signal', []))
# Retrieve the memory address for this signal from the entry
address = entry.get('address')
# Schedule the digital output signal on the hardware:
# - delay: when to start the signal,
# - samplingRate: how often to sample the signal,
# - signal_length: the duration of the signal,
# - address: the location in memory where the signal is stored.
dp.DPxSetDoutSchedule(delay, samplingRate, signal_length, address)
# Start the digital output schedule to send the trigger signal
dp.DPxStartDoutSched()
####### MAIN SCRIPT #################
# Initialize the VPixx device
dp.DPxOpen()
# Define a dictionary of triggers with associated signal patterns and output
# channels.
myTriggers = {
"stimulus_on": {"signal": [1, 0], "channel": 0},
"response_correct": {"signal": [1, 0, 0, 0], "channel": 1},
"response_incorrect": {"signal": [1, 0, 1, 0], "channel": 1},
}
# Configure digital outputs with the given trigger definitions.
configuredTriggers = configureDigitalOutputs(
myTriggers, currentAddress=int(8e6)
)
# Create a PsychoPy window & keyboard object
win = visual.Window(fullscr=True, color='black', units='pix')
kb = keyboard.Keyboard()
# Define a mapping between stimulus colors and the corresponding correct
# response keys.
colors = {'blue': 'b', 'green': 'g'}
# Create a circular stimulus to be used in the task.
circle = visual.Circle(win, radius=50, edges=128)
num_trials = 5
# Loop over the defined number of trials
for trial in range(num_trials):
# Randomly select a color and its corresponding correct key from the
# colors dictionary.
color_name, correct_key = random.choice(list(colors.items()))
# Set the fill color of the circle stimulus to the randomly selected color.
circle.fillColor = color_name
# Draw the circle stimulus to the back buffer.
circle.draw()
# Send a trigger to indicate that the stimulus is being presented.
sendTrigger(configuredTriggers["stimulus_on"])
# Update the hardware's register cache immediately after synchronizing
# with video, ie on the next flip
dp.DPxUpdateRegCacheAfterVideoSync()
# Flip the window to show the stimulus
win.flip()
# Wait for a keypress response from the participant.
# Allowed keys: 'b', 'g' (for responses) or 'escape' (to quit).
keys = kb.waitKeys(keyList=['b', 'g', 'escape'])
# Check if the 'escape' key was pressed to exit the task early.
if 'escape' in keys:
break
# If the correct key was pressed:
elif keys[0] == correct_key:
print(f"Trial {trial+1}: Correct!")
# Send a trigger for a correct response.
sendTrigger(configuredTriggers["response_correct"])
# Update the register cache after sending the trigger.
dp.DPxUpdateRegCache()
else:
# For an incorrect response:
print(f"Trial {trial+1}: Incorrect.")
# Send a trigger for an incorrect response.
sendTrigger(configuredTriggers["response_incorrect"])
# Update the register cache after sending the trigger.
dp.DPxUpdateRegCache()
# Clear the window by flipping
win.flip()
# Wait for 1 second before the next trial (inter-trial interval)
core.wait(1)
# After all trials, close the window gracefully and quit the core to clean up
# resources.
win.close()
core.quit()