Serial port interface code -- a performance comparison

MATLAB has a fairly new (R2019B+) serial port interface, Serialport. Some of my arduino control code had previously used the serial command that MATLAB has now deprecated. So I was curious to compare the old and new interfaces. In addition I also compared IOPort, the PTB serial controller.

For the test I basically toggled the state of a digital pin on a Seeduino Xiao as fast as possible. I used the legacy MATLAB arduino interface sketch, which receives and sends the data via USB serial commands. Baudrate was set to 115200. MATLAB R2020B on Ubutnu 20.10.

t = tic;
for i = 1:10000
	
	digitalWrite(PIN,mod(i,2));
	
end
fprintf('It took %.4f seconds for 10,000 iterations\n',toc(t));

I used an oscilloscope to ensure the output signal was well formed, and used its measurement function to measure average rise-time widths. I also used a simplistic method[1] to generate a 1ms TTL using digitalWrite(1);WaitSecs(0.001);digitalWrite(0) to see how close to 1ms we got:

Serial

10,000 iterations = 6.1 seconds
Risetime range = 540 - 790 µs
1ms pulse width = 3.27 ms

Serialport

10,000 iterations = 3.4 seconds
Risetime range = 320 - 350 µs
1ms pulse width = 2.52 ms

IOPort

10,000 iterations = 0.7 seconds
Risetime range = 50 - 74 µs
1ms pulse width = 1.28 ms

Conclusions

The new serialport is clearly faster than the old one, and the variance is considerably lower. However, IOPort is still much faster than either MATLAB option, I was quite surprised as normally you cannot achieve such fast command-response times with USB devices (the Seeduino Xiao is faster than an Arduino Uno and uses USB 3). If you need to send triggers in a PTB drawing loop and you don’t have much overhead (i.e. fast monitor or complex stimuli), then this should help…


[1] for timed TTLs, it is better to send the timing command to the Arduino, that way MATLAB returns immediately and doesn’t wait for the digitalWrite to finish. See Arduino · Psychtoolbox-3/Psychtoolbox-3 Wiki · GitHub for details.

3 Likes

Hi Ian -

Thanks for this. Is the code for IOPort for use with the Seeduino Xiao included? Would you mind posting it if it is not in a Psychtoolbox demo already? I just ordered some from Amazon for a new stimulus setup.

Best
Steve

Hi Steve, I use a slightly modified version of the MATLAB legacy arduino interface. This utilises an arduino sketch that acts as a server receiving commands and then performing the task. The interface is really simple, you send bytes encoding the action required and the parameters, so for example, sending the serial command 0c1: 0=set pin mode as input or output | c=use pin 2 | 1=set to output. The sketch is a simple state machine that uses the first byte to change state then process the parameters. You can use any serial terminal to send these commands:

I then use the MATLAB interface class (I modified MATLAB’s original that uses serial, my update uses IOPort):

To use it, burn adio.ino to your Xiao or Arduino using the Arduino software. Work out what serial port is used (on linux it is normally /dev/ttyACM0 or /dev/ttyUSB0). Then call the MATLAB class like so (Xiao can use pin 0–10, arduino requires pin 2–13):

s = arduinoIOPort('/dev/ttyACM0',10,0); %parameters: port, end pin, start pin
s.pinMode(2,'output');
s.pinMode(6,'output');
s.timedTTL(2,5); %send a 5ms TTL on pin 2
s.digitalWrite(6,1); %write pin 6 HIGH
s.digitalWrite(6,0); %write pin 6 LOW
clear s; % close the serial port

Currently I only use the arduino/seeduino for single TTL commands, I do want to update to enable strobed 8-10bit words (for which I normally use Display++ or a LabJack, but the Xiao/Arduino is just as capable).

Thanks Ian! This is wonderful and now I see your opticka library for the first time. Great stuff there.

Best
Steve

Hi,
Thanks for this code. Everything works well except I’m having a problem with digitalRead. I can digitalWrite on Outputs but when I try to digitalRead on Inputs I get “NaN”. The pinMode of the pin is correctly set to ‘input’.
Any idea?

I’m stuck out of the lab and it is hard for me to test for a week or so. What code and what board exactly are you using? One thing to try is to use the original Legacy arduino package:

It is really easy to install and test (uses the older serial command, I modified it to use IOPort…), see if you can use digitalRead there?

Yes, sorry I couldn’t find a way to edit my previous post. I already tested it with both the Legacy and current Matlab versions of Arduino support and both work. It’s just the adaptation with the IOPort that doesn’t.

I am using an Arduino Uno with a simple switch on digital pin 2. On the the current/legacy Matlab versions, when I press the switch it reads “1”, otherwise “0”, which is expected.

s = arduinoIOPort('/dev/cu.usbmodem101',13,2)
s.pinMode(2, 'input');
s.digitalRead(2); % I also tried digitalRead(s, 2);

Thanks!

Hm, looking at the arduinoIOPort.digitalRead() code and I can see one potential problem, I’ll rummage around to find an uno to test…

Thank you, much appreciated!

I think the problem is a non-blocking mode typo, try this as the code for function val=digitalRead(a, pin), I also added a bit more error checking:

	if a.isDemo; return; end
	[n, ~, err] = IOPort('Write',a.conn,uint8([49 97+pin]),2);
	if n ~= 2; warning(['digitalRead send command went wrong?']); end
	[val, ~, err] = IOPort('Read',a.conn, 1, 3);
	if isempty(val)
		if ~isempty(err);
			warning(['arduinoIOPort.digitalRead() failed:' err]); 
		else
			warning('arduinoIOPort.digitalRead() empty');
		end
		val = NaN;
	else
		val = str2double(char(val(1)));
	end

The important bit is IOPort('Read',a.conn, 1, 3) which blocks [1] until 3 characters are available, IIRC the read data should be the value itself then a 13[CR] and 10[LF] so 3 bytes in total, but untested and this is from memory, see if it works…

It still returns NaN. The IOPort('Read',a.conn, 1, 3) part returns an empty matrix.

Hm, I just tested with an Uno clone and a Seeeduino Xiao and I do get data back, and it is 3 bytes for digitalRead ([data CR NL]). I noticed the first read of the Uno sometimes is delayed, so perhaps you are hitting the IOPort timeout? I’ve tweaked the code a bit for both adio.ino and arduinoIOPort.m. You can use configure to change the timeouts, e.g. IOPort('ConfigureSerialPort', conn, 'ReadTimeout=1.5'), I added a function to wrap this with arduinoIOPort.m too: a.configure('ReadTimeout=1.5').

What happens if you try to use the Serial Monitor in the Arduino IDE directly? You can send commands and read the reply (make sure baud = 115200). So the ascii command to set e.g. pin 4 to input is 0e0, then a digital read is: 1e — in this command-response language the first byte is the command (a number), the second byte is the pin (a letter), the third byte is optional data. To test, you can send 99 which should return 0, or X3 to return the same number, e.g. 3 in this case. This is the “raw” connection so we can exclude any MATLAB issue…

Wow, thanks! It does work now. Maybe it’s the USB A->C converter I use that added a delay so that the timeout became an issue? What kind of hub are you using?
Thanks again BTW, great work!

Something is still a bit strange with your setup. The default IOPort timeout is 1 second, and normally you should never hit that timeout; for me the time to open the device is ~13ms and the first digitalRead around 3ms (my arduinoManager is just a wrapper around arduinoIOPort):

>> a=arduinoManager('board','Xiao');
--->arduinoManager: Ports available: /dev/ttyS0
--->arduinoManager: Ports available: /dev/ttyACM0
>> a.open
===> All possible serial ports:  /dev/ttyACM0  /dev/ttyS0 
===> Your specified port /dev/ttyACM0 is present
===> Your specified port /dev/ttyACM0 is available
===> It took 0.013 secs to establish response: 0...
===> Basic Analog and Digital I/O (adio.ino) sketch detected !
===> Arduino successfully connected to port: /dev/ttyACM0!
>> tic;val=a.digitalRead(4);disp(['Value was: ' num2str(val)]);toc
Value was: 1
Elapsed time is 0.003512 seconds.

In this case I am using a USB3 hub built-in to my Dell monitor. According to LabJack engineers, command-response times are better for USB-2 devices when using a hub, but we are talking about a few ms either way, not a second…

Here is what I get with a Teensy4.1. There is quite a variability in the digitalRead. Even more so at the begining of the reading sequence.

===> All possible serial ports:  /dev/cu.Bluetooth-Incoming-Port  /dev/cu.usbmodem124098601  /dev/tty.Bluetooth-Incoming-Port  /dev/tty.usbmodem124098601 
===> Your specified port /dev/cu.usbmodem124098601 is present
===> Your specified port /dev/cu.usbmodem124098601 is available
===> It took 0.016 secs to establish response: 0...
===> Basic Analog and Digital I/O (adio.ino) sketch detected !
===> Arduino successfully connected to port: /dev/cu.usbmodem124098601!
>> tic;val=s.digitalRead(2);disp(['Value was: ' num2str(val)]);toc
Value was: 0
Elapsed time is 0.011491 seconds.
>> tic;val=s.digitalRead(2);disp(['Value was: ' num2str(val)]);toc
Value was: 0
Elapsed time is 0.002888 seconds.
>> tic;val=s.digitalRead(2);disp(['Value was: ' num2str(val)]);toc
Value was: 0
Elapsed time is 0.010729 seconds.
>> tic;val=s.digitalRead(2);disp(['Value was: ' num2str(val)]);toc
Value was: 0
Elapsed time is 0.005121 seconds.

For 10000 iterations of digitalRead:

b = [];
pause(1)
for i = 1:10000	
    tic;
	s.digitalRead(2);
    b(i) = toc;
end
>> mean(b)

ans =

   0.001013402959300

However, digitalWrite is way faster:

a = [];
pause(1)
for i = 1:10000
    tic;
	s.digitalWrite(14,mod(i,2));
    a(i) = toc;
end
>> sum(a)

ans =

   0.432772365000000

>> mean(a)

ans =

     4.327723649999997e-05

Even then, though, the first writes of the sequence are orders of magnitude slower (ie, the first write is 6.074 ms).

With an Arduino Uno:

===> All possible serial ports:  /dev/cu.Bluetooth-Incoming-Port  /dev/cu.usbmodem101  /dev/tty.Bluetooth-Incoming-Port  /dev/tty.usbmodem101 
===> Your specified port /dev/cu.usbmodem101 is present
===> Your specified port /dev/cu.usbmodem101 is available
===> It took 0.531 secs to establish response: 0...
===> Basic Analog and Digital I/O (adio.ino) sketch detected !
===> Arduino successfully connected to port: /dev/cu.usbmodem101!

For 10000 iterations of digitalRead, we can see there is indeed, a problem.

b = [];
pause(1)
for i = 1:10000	
    tic;
	s.digitalRead(2);
    b(i) = toc;
end
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
Warning: arduinoIOPort.digitalRead() was empty 
> In arduinoIOPort/digitalRead (line 237) 
>> sum(b)

ans =

     8.888205034199996e+02

(cannot post the plot, limited by the forum)

However, digitalWrite is still way faster:

>> a = [];
pause(1)
for i = 1:10000
    tic;
	s.digitalWrite(3,mod(i,2));
    a(i) = toc;
end
>> mean(a)

ans =

     2.542173692000001e-04

Although, it is not pretty either.

This is with the original USB-B to USB-A long cable provided with Arduino Uno with the USB-A to C adapter so, this might be a cause.

Of note, these tests are conducted on my own personnal machine (MacBook Pro M1 Max 2021) and not the one I plan to use for the experiment. So, it runs Matlab through Rosetta. The timings are thus, of course, not too good nor reliable. Nevertheless, the question of why the digitalReads are so inconsistent remains. Maybe there is something wrong with my Uno since the Teensy reading are so much better. However, the reason why the first readings of the sequence are slow eludes me. It is as if the digitalRead should be initialized or something.

Hm, testing using Ubuntu 22.04 on my Uno (actually it is a chinese clone of a genuine arduino) I get a digitalRead mean of 4.1ms ( ±0.001 SE) and my Seeeduino Xiao as 0.62ms ( ±0.0006 SE):

The Xiao at least has pretty low variance. The Uno is slightly bimodal, and ~4ms would not be so acceptable in a tight display loop. Neither show any increased variance over time. I think your M1 laptop is most of this problem, and on something native it would work more reliably.

As a workaround you can “warm up” the board by doing some digital reads in a loop before any experiment. I tend to do this with all my PTB functions, making sure MATLAB has everything in memory etc.

Yeah, most probably. I’m waiting for our new PC to arrive so I can test it on Ubuntu. Thanks for the help!

hi lan-
Thanks for this code.Everything works well except,but when I use a PTB drawing loop,the Risetime change from 70 µs to 0.7 s(when iii >1757). Do you have any idea about this problem ?

Best!
-NAHAN

[win, winRect] = Screen(‘OpenWindow’, 2,[ ],[ ],32);
ifi = Screen(‘GetFlipInterval’, win);
arduino_u = arduinoIOPort(‘COM9’); %parameters: port, end pin, start pin
vblTS = Screen(‘Flip’,win);
timelist = [ ];

for iii=1:1800
Screen(‘FillRect’,win, [255 128 0],winRect);
tic
arduino_u.digitalWrite(13,1);
tt2=toc;
vblTS = Screen(‘Flip’,win,vblTS+ifi * 0.5);
Screen(‘FillRect’,win, [0 0 255],winRect);
tic
arduino_u.digitalWrite(13,0);
tt4=toc;
vblTS = Screen(‘Flip’,win,vblTS+ifi * 0.5);
timelist=[timelist;tt2 tt4];
end

image

I can’t reproduce your problem, I tested to 3000 frames using your code at a 60Hz framerate, using Ubuntu 22.04 and PTB 3.0.18 with a Seeeduino Xiao:

My oscilloscope shows a really nice square wave @ 33ms period time confirming the digitial write.

1 Like