-
Notifications
You must be signed in to change notification settings - Fork 14
Firmata
Firmata support lets the Arduino Emulator control GPIO, I2C and SPI on a separate physical board (an Uno, a Mega, an ESP32, ...) over a serial connection, by talking the Firmata protocol to a sketch running on that board. This is the mirror image of the FTDI/Raspberry Pi backends: instead of the emulator bit-banging local hardware, it drives a remote MCU that does the bit-banging for you.
-
GPIO (
pinMode/digitalWrite/digitalRead/analogRead/analogWrite) works against plain StandardFirmata, the sketch that ships with the Arduino IDE under File > Examples > Firmata > StandardFirmata. -
I2C (
Wire) also works against plain StandardFirmata - it already includes I2C support. -
SPI requires ConfigurableFirmata with its
SpiFirmatafeature enabled. Classic StandardFirmata has no SPI support at all, soSPI.transfer()will not do anything useful unless the remote board runs ConfigurableFirmata. - Only pins 0-15 are addressable (a limit of the classic Firmata wire format, which packs a port/pin number into a 4-bit nibble).
- Firmata has no I2C/SPI slave mode and no GPIO
tone()/pulseIn()equivalents - those calls are safe no-ops.
A single serial connection to the remote board carries GPIO reports, I2C replies and SPI replies all multiplexed together. Because of that, GPIO/I2C/SPI share one FirmataTransport, which owns the one background thread that is allowed to read the Stream and dispatches parsed messages to whichever backend registered for them:
Stream (your serial connection)
│
▼
FirmataTransport (one reader thread, thread-safe writes)
│
├─ HardwareGPIO_FIRMATA → global GPIO
├─ HardwareI2C_FIRMATA → global Wire
└─ HardwareSPI_FIRMATA → global SPI
You never have to build this by hand - HardwareSetupFIR.h wires up all three backends onto one transport (see below). Each backend can also be used completely standalone with its own private transport if you only need GPIO, say - see Standalone backends.
Using the Arduino IDE (or arduino-cli), flash the board that will be controlled:
- GPIO + I2C only: File > Examples > Firmata > StandardFirmata, upload as-is.
-
GPIO + I2C + SPI: install the
ConfigurableFirmatalibrary, then use (or build) a sketch that includesFirmataExt,DigitalInputFirmata,DigitalOutputFirmata,AnalogInputFirmata,I2CFirmata, andSpiFirmata. TheConfigurableFirmataStandardexample that ships with the library is a good starting point - just make sure theSpifeature is included.
StandardFirmata defaults to 57600 baud; ConfigurableFirmata examples are usually configured the same way unless you changed it.
HardwareGPIO_FIRMATA/HardwareI2C_FIRMATA/HardwareSPI_FIRMATA only need an arduino::Stream - they don't care whether it's a real serial port, a socket, or anything else.
The built-in Serial1 object (ArduinoCore-Linux/cores/arduino/FileStream.h) opens /dev/ttyACM0 and is usable as-is. Because it doesn't configure the tty itself, put the port into raw mode before starting your program, matching the baud rate your sketch uses:
stty -F /dev/ttyACM0 57600 raw -echo -echoe -echok
./my_firmata_programThe
SerialImpl/serialib-based path (USE_SERIALLIB) that would normally configure the port for you is currently broken in this repo -serialib.hpulls in<termios.h>, whoseB0/B50/... baud-rate macros collide withBinary.h's legacyB0/B1/... enum, which fails to compile. Until that's fixed, useSerial1+sttyas shown above, or open the device yourself and wrap the file descriptor in your ownStream.
Any Stream subclass works - a TCP socket to a Firmata-over-WiFi bridge, a Unix pipe, etc.
cmake -B build -S . -DUSE_FIRMATA=ON
cmake --build buildUSE_FIRMATA=ON compiles HardwareGPIO_FIRMATA, HardwareI2C_FIRMATA, HardwareSPI_FIRMATA, FirmataTransport and HardwareSetupFIR.h into the library. It adds no external dependency (unlike USE_FTDI, which needs libftdi1).
HardwareSetupFIR.h starts one shared transport and points the global GPIO, Wire and SPI objects at it, the same way HardwareSetupFTDI.h/HardwareSetupRPI.h do for their platforms - the one difference is begin() takes the Stream to use, since (unlike local hardware) Firmata needs to know which device to talk to.
#include "Arduino.h"
#ifdef USE_FIRMATA
#include "HardwareSetupFIR.h"
#endif
void setup() {
Serial.begin(115200);
#ifdef USE_FIRMATA
if (FIRMATA.begin(Serial1)) { // Serial1 = /dev/ttyACM0, see step 2
Serial.println("Firmata connected");
} else {
Serial.println("Firmata connection failed");
}
#endif
pinMode(13, OUTPUT); // routed to the remote board via GPIO
pinMode(2, INPUT);
}
void loop() {
digitalWrite(13, !digitalRead(13));
Serial.println(digitalRead(2) == HIGH ? "Button HIGH" : "Button LOW");
delay(500);
}void setup() {
Serial.begin(115200);
FIRMATA.begin(Serial1);
Wire.beginTransmission(0x76); // e.g. a BME280
Wire.write(0xF4); // register address
Wire.write(0x27); // value
uint8_t error = Wire.endTransmission();
Serial.println(error == 0 ? "write ok" : "write failed");
}
void loop() {
Wire.beginTransmission(0x76);
Wire.write(0xFA); // register to read from
Wire.endTransmission(false); // repeated start, not a full stop
Wire.requestFrom(uint8_t(0x76), size_t(3));
while (Wire.available()) {
Serial.print(Wire.read(), HEX);
Serial.print(' ');
}
Serial.println();
delay(1000);
}requestFrom() blocks (up to Wire.setTimeout(ms), default 1000ms) until the remote board's I2C_REPLY arrives, then returns the number of bytes actually received - just like the real Wire library.
Requires ConfigurableFirmata on the remote board (see step 1) - plain StandardFirmata has no SPI support and transfer() calls will simply time out.
void setup() {
Serial.begin(115200);
FIRMATA.begin(Serial1);
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
}
void loop() {
uint8_t response = SPI.transfer(0x42);
Serial.print("SPI response: 0x");
Serial.println(response, HEX);
delay(1000);
}By default the remote board handles chip-select itself if you pass a csPin to the HardwareSPI_FIRMATA/HardwareSetupFIRMATA constructor; otherwise, toggle a GPIO pin around each transfer() yourself via digitalWrite(), same as any other SPI device.
If you only need one bus, you can skip HardwareSetupFIR.h/FIRMATA entirely and use a backend directly with its own private transport:
#include "HardwareGPIO_FIR.h"
arduino::HardwareGPIO_FIRMATA gpio;
void setup() {
gpio.begin(Serial1); // owns its own transport
GPIO.setGPIO(&gpio);
pinMode(13, OUTPUT);
}HardwareI2C_FIRMATA/HardwareSPI_FIRMATA have the same begin(Stream&) shape. Only combine this with HardwareSetupFIR.h/FIRMATA if you're sure you're not double-opening the same connection - sharing one physical connection requires sharing one FirmataTransport, via begin(FirmataTransport&) instead of begin(Stream&).
class HardwareSetupFIRMATA {
public:
HardwareSetupFIRMATA() = default;
explicit HardwareSetupFIRMATA(uint8_t spiDeviceId, int8_t spiCsPin = -1);
bool begin(Stream &stream, bool asDefault = true);
void end();
HardwareGPIO_FIRMATA* getGPIO();
HardwareI2C_FIRMATA* getI2C();
HardwareSPI_FIRMATA* getSPI();
};
extern HardwareSetupFIRMATA FIRMATA;
class HardwareGPIO_FIRMATA : public HardwareGPIO {
public:
bool begin(Stream &stream);
bool begin(FirmataTransport &transport);
void end();
// pinMode/digitalWrite/digitalRead/analogRead/analogWrite, as usual
};
class HardwareI2C_FIRMATA : public HardwareI2C {
public:
bool begin(Stream &stream);
bool begin(FirmataTransport &transport);
void setTimeout(unsigned long timeout_ms); // default 1000ms, for requestFrom()
// beginTransmission/write/endTransmission/requestFrom/available/read, as usual
};
class HardwareSPI_FIRMATA : public HardwareSPI {
public:
explicit HardwareSPI_FIRMATA(uint8_t deviceId = 0, int8_t csPin = -1);
bool begin(Stream &stream);
bool begin(FirmataTransport &transport);
void setTimeout(unsigned long timeout_ms); // default 1000ms, for transfer()
// beginTransaction/transfer/transfer16/endTransaction, as usual
};- Pins 0-15 only (classic Firmata's port/pin nibble encoding).
-
No I2C/SPI slave mode -
Wire.begin(address),Wire.onReceive()/onRequest()are no-ops. -
No
tone()/noTone()/pulseIn()/pulseInLong()on the GPIO backend - Firmata has no wire message for them; calls are safe no-ops / return 0. -
analogWriteFrequency()/analogWriteResolution()are no-ops - classic Firmata always uses 8-bit PWM at a fixed frequency set by the remote sketch. -
Wire.setClock()is a no-op. Firmata'sI2C_CONFIGmessage configures a read delay, not a bus clock frequency - the remote board picks its own I2C speed (typically theWirelibrary default, 100kHz). - SPI needs ConfigurableFirmata, not StandardFirmata (see step 1).
-
One
HardwareSPI_FIRMATAper transport. SPI replies are dispatched per top-level SysEx command, not per device, so don't attach two independent SPI backend instances to the same connection. - Requests (
Wire.requestFrom(),SPI.transfer()) block the calling thread until a reply arrives orsetTimeout()elapses - there is USB/serial round-trip latency on every call, same as real Firmata usage.
FIRMATA.begin() returns true but nothing happens. begin() only confirms the transport started; it can't confirm the sketch on the other end is actually Firmata-compatible or running at the right baud rate. Double check the baud rate matches the sketch, and that the serial port is in raw mode (see step 2).
digitalRead()/analogRead() always returns 0/LOW. These are report-driven: the backend has to first tell the remote board "start reporting this pin" (it does so automatically on first read/pinMode(..., INPUT)), then wait for the next unsolicited report to arrive. Give it a moment after pinMode() before reading.
Wire.requestFrom()/SPI.transfer() return 0 / unchanged data. Usually a timeout - either the remote sketch isn't running the feature you're using (e.g. SPI on plain StandardFirmata), the address/deviceId is wrong, or the baud rate mismatches. Raise setTimeout() temporarily while debugging.
Garbled bytes / nothing ever arrives. The serial port almost certainly isn't in raw mode - canonical mode intercepts control characters (like 0xF7, END_SYSEX, if it happens to collide with a line-discipline special character) and can corrupt the binary protocol. Re-run the stty command from step 2.