Skip to content

Firmata

pschatzmann edited this page Aug 31, 2026 · 2 revisions

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.

Overview

  • 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 SpiFirmata feature enabled. Classic StandardFirmata has no SPI support at all, so SPI.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.

Architecture

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.

1. Flash the remote board

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 ConfigurableFirmata library, then use (or build) a sketch that includes FirmataExt, DigitalInputFirmata, DigitalOutputFirmata, AnalogInputFirmata, I2CFirmata, and SpiFirmata. The ConfigurableFirmataStandard example that ships with the library is a good starting point - just make sure the Spi feature is included.

StandardFirmata defaults to 57600 baud; ConfigurableFirmata examples are usually configured the same way unless you changed it.

2. Connect to it from the emulator

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.

Real serial port

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_program

The SerialImpl/serialib-based path (USE_SERIALLIB) that would normally configure the port for you is currently broken in this repo - serialib.h pulls in <termios.h>, whose B0/B50/... baud-rate macros collide with Binary.h's legacy B0/B1/... enum, which fails to compile. Until that's fixed, use Serial1 + stty as shown above, or open the device yourself and wrap the file descriptor in your own Stream.

Anything else

Any Stream subclass works - a TCP socket to a Firmata-over-WiFi bridge, a Unix pipe, etc.

3. Build

cmake -B build -S . -DUSE_FIRMATA=ON
cmake --build build

USE_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).

4. Usage

GPIO + I2C + SPI together (recommended)

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);
}

I2C (Wire)

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.

SPI

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.

Standalone backends

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&).

API Reference

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
};

Limitations

  • 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's I2C_CONFIG message configures a read delay, not a bus clock frequency - the remote board picks its own I2C speed (typically the Wire library default, 100kHz).
  • SPI needs ConfigurableFirmata, not StandardFirmata (see step 1).
  • One HardwareSPI_FIRMATA per 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 or setTimeout() elapses - there is USB/serial round-trip latency on every call, same as real Firmata usage.

Troubleshooting

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.

Clone this wiki locally