Internal of MATLAB Support Package for Arduino Hardware
Don’t ask me how I ended up playing with MATLAB. That is on my university, not me.
While uploading code to an Arduino, one thing bothered me. MATLAB kept the object returned by the arduino
constructor alive after my script finished, so I had to clear it every time. Fine. The error message was interesting though:
1
2
Error using arduino (line 158)
MATLAB connection to Uno at /dev/ttyUSB0 exists in your workspace. To create a new connection, clear the existing object.
And:
1
2
3
$ lsof /dev/ttyUSB0
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
MATLAB 6026 iman 741u CHR 188,0 0t0 919 /dev/ttyUSB0
MATLAB is holding file descriptor 741 on /dev/ttyUSB0
which is the serial link to the board. During setup it even said “Upload Arduino Server”. So I asked myself, what does that mean? I usually live in C and C++ for embedded, so I had to dig in.
I found the Firmata approach (for Java or other languages). MATLAB does not use Firmata though. It uses something “similar”.
- https://www.yorku.ca/professor/drsmith/2024/06/11/firmata-example-i2c-sensor-java-firmata4j/ (Written by Professor James Andrew Smith)
Investigation
Error strings are always good breadcrumbs. That message led me to validateConnection()
, called by initHardware()
in the arduino
class. Keep in mind that the arduino
class subclasses matlabshared.hwsdk.controller
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
classdef (Sealed) arduino < matlabshared.hwsdk.controller
...
end
classdef controller < matlabshared.hwsdk.controller_base &...
matlab.mixin.CustomDisplay
...
function initHardware(obj, varargin)
...
% Check if transport layer already occupied, or hardware connection already exists.
validateConnection(obj.ConnectionController, addressKey);
...
end
end
TRANSPORT LAYER?! Now we are getting somewhere. I turned on tracing to find out more about what is going on.
1
2
3
4
5
% > help arduino
% TraceOn - Program log and Arduino commands log
ledPin = "D4";
a = arduino("/dev/ttyUSB0", "Uno", TraceOn=true);
configurePin(a, ledPin, "DigitalInput");
Output:
1
2
3
4
5
>> arduino_test
ArduinoTraceOn::pinMode(4, INPUT);
ArduinoTraceOn::pinMode(4, INPUT);
>> clear a
UnconfigureDigitalPin::pinMode(4, INPUT);
Interesting, but not enough! Time for strace
. Assume $SupportPackageRoot
is MATLAB/SupportPackages/<version>/
.
1
2
3
4
5
6
$ strace -F -e openat matlab -desktop -sd /home/iman/w/matlab/ &| rg -v "arduinoio" | rg "arduino"
openat(AT_FDCWD, ".$SupportPackageRoot/toolbox/matlab/hardware/supportpackages/sharedarduino", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 62
openat(AT_FDCWD, "$SupportPackageRoot/toolbox/matlab/hardware/supportpackages/sharedarduino/+arduinodriver", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 67
openat(AT_FDCWD, "$SupportPackageRoot/toolbox/matlab/hardware/supportpackages/sharedarduino/+arduinodriver/ArduinoDigitalIO.p", O_RDONLY) = 77
openat(AT_FDCWD, "$SupportPackageRoot/toolbox/matlab/hardware/supportpackages/sharedarduino/+arduinodriver/ArduinoDigitalIO.m", O_RDONLY) = 77
...
Okay, don’t panic. These are just the files MATLAB opens when the script runs, nothing weird is going on.
Package Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ls $SupportPackageRoot/toolbox/matlab/hardware/supportpackages/sharedarduino--tree -L2
├── +arduinodriver # high-level MATLAB classes for Analog/Digital/I2C/SPI/Serial APIs
│ ├── ArduinoDigitalIO.m
│ ├── ArduinoDigitalIO.p
│ ├── ArduinoI2C.m
│ ├── ArduinoI2C.p
...
├── +arduinoio
│ ├── +internal
│ ├── CLIRoot.m # points to arduino-cli
...
│ └── SharedArduinoRoot.m
├── +simulinkio
│ ...
└── target # on-board server firmware (C/C++)
├── ArduinoServer.ino
├── server/
└── transport/
MATLAB ships many internals as p-code. It means that they are content-obscured, and you can’t read them without getting your hand dirty. If a
.m
and.p
share a name in the same directory, MATLAB runs the.p
and uses the.m
only for help and signatures.pcode
hides the implementation while keeping the public API visible.
Sketch Upload Process
What gets uploaded
When MATLAB decides it needs to reflash, it uploads target/ArduinoServer.ino
plus the modules you enabled, for example I2C and SPI.
1
2
3
4
5
6
7
8
9
target/
ArduinoServer.ino % setup/loop + packet server(...)
server/
MW_digitalIO.cpp % pinMode/read/write
MW_AnalogInput.cpp % analogRead
MW_I2C.cpp, MW_SPI.cpp % buses
...
transport/
rtiostream_serial_*.cpp % serial/BLE/Wi-Fi backends
These use the usual Arduino core API; like pinMode
and digitalWrite
internally.
How it gets uploaded
MATLAB invokes arduino-cli
from the support package.
+arduinoio/CLIRoot.m
:
1
2
3
4
5
function output = CLIRoot()
% This function returns the installed directory of Arduino CLI
% Copyright 2023 The MathWorks, Inc.
output = arduinoio.getArduinoCLIRootDir;
end
1
2
3
>> arduinoio.getArduinoCLIRootDir
ans =
"$SupportPackageRoot/3P.instrset/arduinoide.instrset/aCLI"
And:
1
2
$ ls $SupportPackageRoot/3P.instrset/arduinoide.instrset/aCLI/
arduino-cli arduino-cli.yaml data LICENSE.txt user
arduino-cli
is the tool that compiles the sketch (using gcc-avr
on my system) and uploads it. To verify this, watch the arduino-cli
executable, upload a sketch from somewhere other than MATLAB, then return to MATLAB and run your script. You should see this on the Command Window:
1
Updating server code on board Uno (/dev/ttyUSB0). This may take a few minutes.
If you trace the MATLAB process, you can see the arguments passed to it as well:
1
2
$ strace -F -s 1000 -e execve matlab -desktop -sd /home/iman/w/matlab/ &| rg "arduino-cli"
execve("$SupportPackageRoot/3P.instrset/arduinoide.instrset/aCLI/arduino-cli", ["$SupportPackageRoot/3P.instrset/arduinoide.instrset/aCLI/arduino-cli", "-v", "--fqbn", "arduino:avr:uno", "compile", "--upload", "/tmp/iman/ArduinoServerUno", "--port", "/dev/ttyUSB0", "--build-path", "/tmp/iman/ArduinoServerUno/MW", "--config-file", "$SupportPackageRoot/3P.instrset/arduinoide.instrset/aCLI/arduino-cli.yaml"], 0x60e8f9422650 /* 70 vars */) = 0
In other words:
1
2
3
4
5
6
7
8
9
$ arduino-cli \
-v \
--fqbn arduino:avr:uno \
compile \
--upload "/tmp/iman/ArduinoServerUno" \
--port /dev/ttyUSB0 \
--build-path "/tmp/iman/ArduinoServerUno/MW" \
--config-file "$SupportPackageRoot/3P.instrset/arduinoide.instrset/aCLI/arduino-cli.yaml"
How MATLAB Talks to the Board
Two execution modes behind one API
From files like +arduinodriver/ArduinoDigitalIO.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ~coder.target('MATLAB')
% This is executed during MATLAB and Simulink code generation <==========
pin = varargin{1};
mode = varargin{2};
coder.cinclude('MW_arduino_digitalio.h');
coder.ceval('digitalIOSetup',uint8(pin), mode);
else
% This is executed during MATLAB and Simulink IO <==========
writeStatus = configureDigitalPinInternal@matlabshared.ioclient.peripherals.DigitalIO(obj, varargin{:});
if(nargout == 1)
varargout{1} = writeStatus;
end
end
Non-codegen (interactive MATLAB): you run scripts in MATLAB on the PC like what I just did; calls like arduino(...)
, writeDigitalPin(...)
send requests with different IDs to a server sketch that MATLAB flashed onto the board. No C/C++ is generated.
Codegen (generated firmware): MATLAB (via MATLAB Coder) turns your code into C/C++ that runs standalone on the MCU (no MATLAB, no server sketch at runtime). Same API, different backend.
(Simulink can also generate code for Arduino; I’m not covering it here)
What the arduino
object holds
It is not only a serial port. It wraps a transport handle, a connection registry that stops duplicate connections, and the library set you selected when the server sketch was built. That is why the serial port stays open and why you need to clear the object when you are done.
Where your MATLAB call turns into packets (non-codegen)
Your .m
class hands off to a p-coded superclass that builds and sends packets. In ArduinoDigitalIO.m
you will see calls like:
1
2
3
4
configureDigitalPinInternal@matlabshared.ioclient.peripherals.DigitalIO(...)
writeDigitalPinInternal@matlabshared.ioclient.peripherals.DigitalIO(...)
readDigitalPinInternal@matlabshared.ioclient.peripherals.DigitalIO(...)
unconfigureDigitalPinInternal@matlabshared.ioclient.peripherals.DigitalIO(...)
The protocol in one glance over serial
There is no public documentation for it, but if you open the ArduinoServer.ino
sketch you will see it includes IO_packet.h
and related files. If you go to your MATLAB installation path, you can read them. In short, MATLAB talks to the board using a private, packetized protocol (header + payload length + CRC-8 J1850). The serial, BLE, and Wi-Fi transport/rtiostream_*
files are only the transport; framing and CRC live in IO_packet.*
on the target and in MATLAB’s host code. On receive, the firmware waits for the header, reads the length, validates it if CRC is enabled, then reads IDs and payload and executes. For example, if you call:
1
writeDigitalPin(a, "D4", 1);
MATLAB packs something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+---------------------+
| HEADER_HOSTTOTARGET |
+---------------------+
| payloadSize |
+---------------------+
| uniqueId |
+---------------------+
| requestId = 0x0001 | <--- WRITEDIGITALPIN
+---------------------+
| isRawRead = 0x00 |
+---------------------+
| payload: |
| pinNo |
| value (LOW/HIGH) |
+---------------------+
| CRC8 |
+---------------------+
You can see the full list of requestId
macros in ioserver/inc/IO_requestID.h
. MATLAB then sends the packet over the selected transport, serial in my case. On the Arduino, the server()
function parses it, sees WRITEDIGITALPIN
, calls MW_digitalIO_write
, which calls digitalWrite
and sets the pin HIGH.
TL;DR
Interactive MATLAB means a server on the board and packets over serial, Wi Fi, or BLE.
Deploy and codegen means no server, just compiled C/C++ on the MCU.
The
arduino
object manages the transport and the connection registry, so the port stays busy until you clear it.