If you have started looking through our MATLAB and Python demos, you will probably have encountered code that looks like this:
Datapixx('RegWrRd'); DPxUpdateRegCache() device.updateRegCache()
And possibly this:
Datapixx('StopAllSchedules'); DPxStopAllScheds() device.din.stopSchedule()
If you’re curious about what these commands do, this guide is for you.
Registers and schedules are core mechanics of VPixx Technologies’ microsecond-precision synchronization systems. Along with a centralized clock, they allow us to record and align the timing of stimulus presentation, outgoing triggers and incoming signals.
While you can program your experiment without fully understanding these concepts, we find that knowing how they work helps you write more efficient code and take full advantage of the synchronization abilities of your devices.
In this guide we will cover both registers and schedules. We will address how and when they are used by your VPixx device, and how to write to and read from them. We will give examples and identify functions in both MATLAB and Python.
This guide applies to any VPixx hardware with support for MATLAB and Python APIs (APIs available for download here). This includes the DATAPixx video I/O hub, the PROPixx controller, the VIEWPixx and VIEWPixx /3D, and the TRACKPixx eye trackers.
Notably, this guide does not apply to the VIEWPixx /EEG, as it does not use our MATLAB and Python libraries.
All API-supported VPixx devices have onboard system settings. The settings include information about the current state and operation of the device. Collectively, these settings are saved in what we call the device register.
A copy of these settings is saved on your experiment computer as well. We call this copy the local register.
When we write a line of MATLAB or Python code that tells our VPixx device to do something, we are changing the contents of the local register. In other words, we are changing the copy of the device settings stored on our computer. With a few exceptions (see the blue Tip box below), this code will not automatically alter the state of the device register.
In order to update our device register with the changes we have made to the local register, we need to perform a register write.
Say we want to turn on Pixel Mode, a mode which automatically sends out digital TTL triggers based on the colour of the top leftmost pixel on the display. In MATLAB, we can do this with the command:
Datapixx('EnablePixelMode'); fprintf('Nothing happened?'); Datapixx('RegWr'); fprintf('Pixel Mode is active!');
The command ‘EnablePixelMode’ did indeed do something—it changed the state of the local register. In order for the VPixx device to actually initiate Pixel Mode, this mode has to be enabled on the device register as well. So, we push our command to the device using ‘RegWr’, and then Pixel Mode is enabled.
Almost all of our functions for MATLAB and Python require a register write in order for them to be implemented on your VPixx device. There are a handful of special functions (like ‘Open’ and ‘Close’) which are automatically passed to the device register. These functions don’t need to be followed by a register write.
A major advantage to register writing is that it allows several tasks to be executed simultaneously. Simply write multiple commands to the local register, and execute a single register write to enable all changes on the device register at the same time.
For example, say we want to start eye tracking and send a digital TTL trigger simultaneously:
%First, let’s set up an eye tracking schedule. We’ll cover schedules in more detail in a bit. For %now, we will say we are preparing the recording without starting it. Datapixx('SetupTPxSchedule'); %We pass this to the device register so it is ready to go when we want to start recording Datapixx('RegWr'); %Next, we tell the local register to start eye tracking. Recording won’t start until the next %register write when the command is passed to the device: Datapixx('StartTPxSchedule'); %Now let’s define our digital trigger. We will set DOut 0-3 to high, and everything else to low. doutValue = bin2dec('0000 0000 0000 0000 0000 1111'); Datapixx('SetDoutValues', doutValue); %Nothing has happened yet! Let’s initiate both the digital trigger and start recording %simultaneously, by updating the device register with our recent changes: Datapixx('RegWr');
Another advantage to using registers is that you can wait for a specific event to perform the write. Any new states or commands written in the local register will be pushed to and implemented on the device register when this event occurs. This allows us to synchronize our experiment I/O with properties of the video signal. Below is a list of currently supported register write commands:
Perform a register write immediately
Python (libdpx wrapper)
Perform a register write on next vertical sync pulse/start of the next frame
Python (libdpx wrapper)
Perform a register write on next instance of custom sequence of pixels
Python (libdpx wrapper)
Our Python tools come in two flavours, which reflect two distinct programming styles:
The libdpx wrapper is procedural, and prefaces all functions with “DPx.”
Our object-oriented programming tools require users to first define a VPixx device object, and then call its functions and attributes. Specific functionalities are stored in subclasses, such as AnalogIn and DigitalOut.
Say we want to send out a digital TTL trigger when our specific visual stimulus occurs. In the top left corner of the video frame containing the stimulus, we draw a sequence of 8 alternating red and green pixels. Then, we use a register write command which waits for this sequence of pixels to send the command to fire a TTL trigger to our device register.
Datapixx('Open') %First, we define the pixel sequence we are waiting for. In this case, it is a series of 8 red and %green pixels in the top corner of our image. We define these pixels in a 3 x 8 array, where the %columns are the number of pixels in the sequence and rows are the red, green and blue values of the %pixels. pixelTrigger = [255, 0, 255, 0, 255, 0, 255, 0; %R 0, 255, 0, 255, 0, 255, 0, 255; %G 0, 0, 0, 0, 0, 0, 0, 0]; %B %Next, we set our desired digital output. Let’s say we want to set DOut 4-7 to high, and everything %else to low. This command is stored in the local register for now. doutValue = bin2dec('0000 0000 0000 0000 1111 0000'); Datapixx('SetDoutValues', doutValue); %Nothing has happened on the device register yet. Let’s write an update to the device register on %the next appearance of our pixel sequence, i.e., when our target appears. Datapixx('RegWrPixelSync', pixelTrigger);
We recommend a trigger sequence of at least 8 pixels when using PixelSync, to ensure the trigger is unique. To align the trigger with stimulus onset, the pixels should be embedded in the top of your stimuli, without any blending or dithering applied.
The power of registers becomes obvious as you add more and more simultaneous tasks. If, along with your digital output, you also want to:
- play a tone
- record audio
- start analog output of eye position
- listen for a button press
You can simply add these tasks to the local register and push them all to the device on that single RegWrPixelSync. All tasks will be initiated when your stimulus occurs in the video signal as it is read by your VPixx device.
Reading the device register
We have covered some examples of when you might want to update the device register with the contents of the local register. There are some cases in which we might want to do the opposite, and update the local register with whatever is stored on the device.
For example, if the device is recording data, we may want to know how much new data has been recorded since we last checked. We also may want to information about the device status, or the timestamp of a certain event. All of this information is stored on the device register. In order to update our local register with this information, we need to perform a register read.
To perform a register read, we first have to write the read request to our local register and pass it to the device register. The device then sends back a copy of its register, which is used to update the local register. Rather than doing this procedure in two lines of code, we have a set of write-read commands which perform a register write immediately followed by a register read. In our Python library, this operation is often called a register update.
Perform a register write-read immediately
Python (libdpx wrapper)
Perform a register write-read on next vertical sync pulse/start of the next frame
Python (libdpx wrapper)
Perform a register write-read on next instance of custom sequence of pixels
Python (libdpx wrapper)
It’s important to keep in mind that register write-reads are blocking functions. That is, everything else in your script is put on hold until the read is performed. Other blocking functions include Psychtoolbox’s screen flip and listening functions like KBWait(). This is something to be aware of when deciding between a register write, versus a register write-read.
As an example, let’s look at sending out a trigger on a video frame containing our visual stimulus, which is a red circle.
%Let’s open a black screen and draw our red circle [windowPtr, ~] = Screen('OpenWindow', 2, [0,0,0]); Screen('FillOval', windowPtr, [255,0,0], [500, 500, 600, 600]); %Since our stimulus is a red circle on a black screen, let’s make our pixel trigger something %obvious: a series of 8 red pixels. This marks the start of our stimulus. pixelTrigger = [255, 255, 255, 255, 255, 255, 255, 255; %R 0, 0, 0, 0, 0, 0, 0, 0; %G 0, 0, 0, 0, 0, 0, 0, 0]; %B %For our digital trigger, we’ll set all even DOuts to 1, and odd to 0. doutValue = bin2dec('0101 0101 0101 0101 0101 0101'); Datapixx('SetDoutValues', doutValue); %Let’s set a write to update the device register on the next appearance of our sequence, and then %show our image Datapixx('RegWrPixelSync', pixelTrigger); Screen('Flip'); %Wait for a keypress to continue KBWait();
Using a write-read here would cause a problem. Because the write-read is a blocking function, it would cause the code to wait for the pixel sequence to perform the register read before moving to the next line of our script. The next line is our screen flip, which contains our red pixel, which means we will be waiting forever.
On the other hand, using a register write will not create any issues, as it does not wait for a return value and so will not block subsequent code.
Using GetTime and markers for timekeeping
Your VPixx device has a central clock, which it uses for all recordings and timestamps. This makes it very easy to align multiple streams of inputs and outputs. The clock can be accessed by reading the device register.
Say we want to know the exact moment our device register was last updated. We can simply call ‘GetTime’, which returns the time, in seconds, between device power-up and the most recent register write-read.
For example, the following code will return a timestamp of when your audio schedule began.
Datapixx('StartAudioSchedule'); Datapixx('RegWrRd'); audioStartTime = Datapixx('GetTime');
As we have just seen, we don’t always want to perform a register write-read because it can block subsequent code while waiting for data from the device. In this case, we can create a “marker” to be used with a register write. This marker is stored in the device register and can be accessed with a write-read later on in your code.
Let’s expand on the example using our red circle stimulus. Say we want to know exactly when that first red pixel occurred and our digital trigger was sent out:
[windowPtr, ~] = Screen('OpenWindow', 2, [0,0,0]); Screen('FillOval', windowPtr, [255,0,0], [500, 500, 600, 600]); pixelTrigger = [255, 255, 255, 255, 255, 255, 255, 255; %R 0, 0, 0, 0, 0, 0, 0, 0; %G 0, 0, 0, 0, 0, 0, 0, 0]; %B doutValue = bin2dec('0101 0101 0101 0101 0101 0101'); Datapixx('SetDoutValues', doutValue); %Let’s create our marker here, before the write Datapixx('SetMarker'); Datapixx('RegWrPixelSync', pixelTrigger); Screen('Flip'); KBWait(); %Now let’s retrieve our timestamp Datapixx('RegWrRd'); targetOnset = Datapixx('GetMarker');
If we were to use ‘GetTime’ here, the returned timestamp would reflect the time of the register write-read following the keypress. Since ‘SetMarker’ was called prior to the write on pixel sync, ‘GetMarker’ will instead return the timestamp of that register write, which corresponds to our stimulus and trigger onsets.
So far, we have discussed how to communicate between local and device registers to control the settings of our VPixx device. In this section we will turn to how we can use the onboard memory to store and record data.
This storage can be configured by defining buffers with a certain address and size. There are several types of buffers, that can be used for different types of data. Schedules are management functions which control the flow of information to/from each buffer.
For example, you might have one buffer that is logging participant button presses via the digital input, a second buffer which contains an audio file for playback during the experiment, and a third buffer that is that is recording eye tracking data. Each of these buffers has a unique address in the device memory:
Below is a summary table of the types of schedules currently available with VPixx devices. For specific code syntax in MATLAB, enter ‘Datapixx’ in the command line for a full list of functions. In Python, you can search our online documentation here.
VPixx Buffer Types
Stores mono or stereo audio output
Records audio on either MIC IN or Audio IN
Digital Input Log
Only records changes in the state of Digital IN (max 16 bits)
Stores TTL signals for playback on Digital OUT (max 24 bits)
Digital to Analog converter (DAC)
Stores content for playback on Analog OUT pins (+/- 10V)
Analog to Digital converter (ADC)
Records input from Analog IN pins (+/-10V)
Saves individual images to a buffer and shows them on the display. Simulates a tachistoscope
TRACKPixx Eye Tracking
Records a 20-column array of eye tracking data, at a rate of 2000 samples per second
Not all schedules can be used with all VPixx devices. For example, the “Lite” versions of our products do not have the audio or analog buffer types. Similarly, the TRACKPixx eye tracking schedule is only available to researchers who have one of our eye trackers and a DATAPixx3.
While there are several kinds of data that may be stored in a buffer, the strategy for managing buffers is the same. Below are some general guidelines for creating and managing buffer X:
- (Playback only) Write content to the buffer
- Set up a schedule to describe the type of data we expect the buffer to have, and other relevant parameters. If the buffer is going to be recording data rather than playing it, this is where the buffer address is specified
- Write these changes to the device register to update the configuration of the device
- Enter the command to start the schedule
- Write this change to the device register to trigger start (optionally, you can synchronize it to video output using one of the special register commands)
- Enter command to stop the schedule
- Write this change to the device register
Reading buffer contents
- Perform a register write-read to get the current status of the device register
- Get the schedule status, including number of new “frames ” of data in the buffer since the schedule started
- Retrieve the desired number of frames from the buffer (note: the read buffer family of commands are a rare case of functions that do not require a register write or write-read; instead they execute immediately)
The functions for writing to buffers and setting up schedules often supply default memory addresses and sizes, so it isn’t necessary to set them yourself. However, if you are running multiple concurrent schedules it is always a good idea to double check you are not at risk of overlapping buffers, which will overwrite data.
In this section we will show some examples in MATLAB/Psychtoolbox and Python, showing how to implement different schedules. For more examples please refer to our demo libraries.
Example 1. Play a beep 100 ms after visual stimulus onset
This example uses the audio buffer. Audio onset is triggered by pixel sync. The audio schedule is set up with an onset delay of 100 ms, so that the audio trails our visual stimulus onset by precisely that amount. We also use a marker to record the the time our visual stimulus appeared on screen.
%% %SETUP Datapixx('Open'); %Let’s define a 1 s, 500 Hz tone using Psychtoolbox’s MakeBeep and write it to a buffer at address %16e6 duration = 1; freq = 500; [tone, samplingRate] = MakeBeep(freq, duration); Datapixx('InitAudio') Datapixx('SetAudioVolume', [0,0.5]); bufferAddress = 16e6; [~, ~, ~] = Datapixx('WriteAudioBuffer', tone, bufferAddress); %Next we set up our audio schedule with a 100 ms delay start. Audio schedules also require a sampling %rate for playback, which should match the sampling rate of our beep. We also set a buffer size %based on the length of our tone bufferSize = duration*samplingRate; onsetDelay = 0.1; Datapixx('SetAudioSchedule', onsetDelay, samplingRate, bufferSize); %We will use pixel sync to trigger playback. Let's define the pixel sequence which indicates the %start of our stimuls: a green circle pixelTrigger = [0, 0, 0, 0, 0, 0, 0, 0; %R 255, 255, 255, 255, 255, 255, 255, 255; %G 0, 0, 0, 0, 0, 0, 0, 0]; %B %Configure settings on device Datapixx('RegWrRd'); %% %START [windowPtr, ~] = Screen('OpenWindow', 2, [0,0,0]); Screen('FillOval', windowPtr, [0,255,0], [500, 500, 600, 600]); Datapixx('SetMarker'); Datapixx('StartAudioSchedule'); Datapixx('RegWrPixelSync', pixelTrigger); Screen('Flip', windowPtr); WaitSecs(1.1); %% %STOP Datapixx('StopAudioSchedule'); Datapixx('RegWrRd'); stimulusStartTime = Datapixx('GetMarker'); Datapixx('Close'); Screen('Closeall');
Pixel Sync problems? Don’t panic! When the register write/write-read cannot find the triggering pixel sequence, it can lead to a timeout error. In this case, your graphics card is likely applying filtering or smoothing functions that are distorting your sequence. For a workaround, see Matlab Demo 8 – Synchronization on Digital to Analog Converter
Example 2. Reading simulated analog input on ADC
This example uses two schedules working together. First, we set up an ADC schedule to record analog input. We also store some simulated analog data on DAC for playback.
We wait for a keypress to start DAC and ADC schedules at the same time. The ‘EnableDacAdcLoopback’ setting allows our ADC schedule to read our simulated DAC data as if it was an incoming signal.
A second keypress ends the recording, and the results are plotted.
%% %SETUP Datapixx('Open'); %The ADC schedule requires a few parameters like onset, sampling rate and maximum size scheduleOnsetDelay = 0; scheduleRate = 20; %samples per frame; see documentation for more details maxScheduleFrames = 4800; ADCChannelToRecord = 0; Datapixx('SetAdcSchedule', scheduleOnsetDelay, scheduleRate, maxScheduleFrames, ADCChannelToRecord); %Simulate some data on the DAC buffer simulatedData= sin(2*pi*[0:0.01:10]); dacOnset = 0; %seconds dacFrequency = 20; %samples/second dacMaxFrames = 1001; dacChannels = 0; Datapixx('WriteDacBuffer', simulatedData); Datapixx('SetDacSchedule', dacOnset, dacFrequency, dacMaxFrames, dacChannels); %set some settings to enable reading simulated data from the DAC schedule, on ADC channel Datapixx('EnableDacAdcLoopback'); Datapixx('DisableAdcFreeRunning'); %Push everything to the device Datapixx('RegWr'); %% %START Datapixx('StartAdcSchedule'); Datapixx('StartDacSchedule'); %Wait for a keypress to start recording. We record for a minimum of 1 second KbWait(); Datapixx('RegWrRd'); startTime = Datapixx('GetTime'); WaitSecs(1); %% %STOP KbWait(); Datapixx('StopAdcSchedule'); Datapixx('StopDacSchedule'); Datapixx('DisableDacAdcLoopback'); Datapixx('RegWrRd'); %% %READ status = Datapixx('GetAdcStatus'); toRead = status.newBufferFrames; [bufferData, bufferTimetags, ~, ~] = Datapixx('ReadAdcBuffer', toRead); bufferTimetags=bufferTimetags - startTime; figure() plot(bufferTimetags, bufferData, '-b'); xlabel('Time'); ylabel('Voltage'); title('ADC Channel 0 input'); Datapixx('Close');
Example 3. Using markers and the Digital Input Log to reject trials with a response time > 5 seconds
This example simulates a simple reaction time task. A target is flashed and participants must press a button, which is monitored on digital input channel 1. At the end of each trial we read the Digital Input Log. If the button press occurred more than 5 s after the target onset, we reject the trial.
%% %SETUP Datapixx('Open'); %Next, we will set up a DInLog, using the default buffer address. This will record any changes in %the digital input, which we will treat as a button press. For more on how to interpret our digital
%input channels, see our RPx demos Datapixx('SetDinLog'); Datapixx('EnableDinDebounce'); %reduces jitter Datapixx('RegWr'); numberOfTrials = 5; responseTimeCutoff = 5; targetWidth=100; targetColour=[50, 50, 50]; %% %START [windowPtr, rect] = Screen('OpenWindow', 2, [0,0,0]); for k = 1:numberOfTrials isValid = 0; while ~isValid %Add a variable delay to keep participants attentive WaitSecs(randi(4)); %Generate a random location for top left corner of target, and draw targetLoc = [randi(rect(3)-targetWidth), randi(rect(4)-targetWidth)]; Screen('FillRect', ...
[targetLoc(1), targetLoc(2), targetLoc(1)+targetWidth, targetLoc(2)+targetWidth]); %Start our log and marker, implemented on next video frame Datapixx('StartDinLog'); Datapixx('SetMarker'); Datapixx('RegWrVideoSync'); Screen('Flip', windowPtr); Datapixx('RegWrRd'); startTime = Datapixx('GetMarker'); press = 0; %% %READ %Let's create an inner loop to check the log. The first thing in the log buffer should be our %press; if the buffer is empty, then we haven't recorded anything yet. while ~press WaitSecs(0.25); Datapixx('RegWrRd'); status=Datapixx('GetDinStatus'); if status.newLogFrames > 0 %% %STOP Datapixx('StopDinLog'); Datapixx('RegWr'); Screen('Flip', windowPtr); press = 1; end end %Let's get the timestamp of the first event in our log, which we assume is the button press. %This timestamp is on the same clock as our marker. Datapixx('RegWrRd'); [~, logTimetags, ~] = Datapixx('ReadDinLog'); if (logTimetags(1)-startTime) <= responseTimeCutoff isValid = 1; fprintf("Success! Your response time was %.2d seconds.\n", (logTimetags(1)-startTime)); else fprintf("Too slow! Your response time was %.2d seconds.\n", (logTimetags(1)-startTime)); end end end Datapixx('Close'); Screen('Closeall');
In this guide, we’ve covered the basics of registers and schedules. Register writing and write-reading allow us to perform multiple simultaneous tasks with a single VPixx device. We can trigger these tasks with a video-based event like the start of a new video frame or the onset of a specific sequence of pixels. We can also use ‘GetTime’ and markers to keep track of the time of these events.
Schedules allow us to play and record data stored in buffers in device memory, using the device’s central clock, high sampling rate and low-latency control. There are many different schedules which can run concurrently, offering flexibility in experimental design. We covered some examples of how to interact with data buffers and manage playback/recording.
Registers and schedules provide a way to carefully control timing and synchronization of outgoing and incoming data in your experiment. Now that you have a sense of how they work, we encourage you to explore our MATLAB and Python demos here. Many of these demos contain examples of working with registers and running different kinds of schedules.
Cite this guide
Fraser, L.. (2020, May 6). Introduction to Registers and Schedules. Retrieved [Month, Day, Year], from https://vpixx.com/vocal/introduction-to-registers-and-schedules/