Peripherals and I/O
Buses, devices, expanders, analog and digital pins - and the poll loop that reads them.
Beyond the panel and the touch controller, a board has sensors, expanders, analog inputs and
plain digital pins. The Buses, Devices and I/O tabs describe them, and the generator writes
hardware.h and hardware.cpp to bring them up and read them on a
schedule. Nothing here ever draws: the readings go into caches that
variables read.
Buses
The Buses tab declares I2C and SPI buses beyond the display's. An I2C bus has SDA,
SCL, a clock in kHz and an internal pull-up option (weak - fit 4.7 kΩ externals for anything but
the shortest run). An SPI bus has SCK, MOSI, an optional MISO, a clock in MHz and the SPI
mode. The first bus of a kind becomes Wire or SPI in the generated code,
the next Wire1 or SPI1, and so on up to what the MCU has - except that an
I2C touch controller already has Wire: a bus on the touch controller's own pins shares
it, and a bus on any other pins starts at Wire1. The tab's heading for each bus shows
the object it became, which is the one to use from your own code. A device
attached to a bus that shares pins with the display's bus is legitimate and not flagged.
Devices
The Devices tab adds peripherals. Each has a Type, a Bus, an I2C
address or a CS pin depending on the bus, and a Poll interval - how often
hw_poll() reads it, independently of everything else, so a slow sensor never holds
up a fast one or the display. Below those comes the device's own configuration: every register
the driver library exposes, applied once in hw_init() after the device reports
present.

| Group | Device | Bus | Channels | Library | Configuration |
|---|---|---|---|---|---|
| Environment | BME280 | I2C | Temperature (C), Humidity (%), Pressure (hPa), Altitude (m) | adafruit/Adafruit BME280 Library | mode, osTemp, osPress, osHum, filter, standby, seaLevel |
| Environment | BMP280 | I2C | Temperature (C), Pressure (hPa) | adafruit/Adafruit BMP280 Library | mode, osTemp, osPress, filter, standby |
| Environment | SHT31 / SHT35 | I2C | Temperature (C), Humidity (%) | adafruit/Adafruit SHT31 Library | heater |
| Environment | AHT10 / AHT20 | I2C | Temperature (C), Humidity (%) | adafruit/Adafruit AHTX0 | - |
| Environment | SCD40 / SCD41 (CO2) | I2C | CO2 (ppm), Temperature (C), Humidity (%) | sparkfun/SparkFun SCD4x Arduino Library | tempOffset, altitude, asc |
| Environment | SEN66 (air quality) | I2C | PM1.0 (ug/m3), PM2.5 (ug/m3), PM4.0 (ug/m3), PM10 (ug/m3), Humidity (%), Temperature (C), VOC index, NOx index, CO2 (ppm) | sensirion/Sensirion I2C SEN66 1.3.1 | - |
| Environment | BH1750 (light) | I2C | Illuminance (lx) | claws/BH1750 | mode, mtreg |
| Environment | MCP9808 (temperature) | I2C | Temperature (C) | adafruit/Adafruit MCP9808 Library | resolution, wake |
| ADC | ADS1115 (16-bit ADC) | I2C | Channel 0 (V), Channel 1 (V), Channel 2 (V), Channel 3 (V), Differential 0-1 (V) | adafruit/Adafruit ADS1X15 | gain, rate |
| ADC | INA219 (current) | I2C | Bus voltage (V), Current (mA), Power (mW), Shunt voltage (mV) | adafruit/Adafruit INA219 | calibration |
| ADC | MAX17048 (battery gauge) | I2C | Cell voltage (V), State of charge (%) | adafruit/Adafruit MAX1704X | alertMin, alertMax |
| ADC | MCP3008 (8ch 10-bit ADC) | SPI | Channel 0 (counts), Channel 1 (counts), Channel 2 (counts), Channel 3 (counts), Channel 4 (counts), Channel 5 (counts), Channel 6 (counts), Channel 7 (counts) | adafruit/Adafruit MCP3008 | vref |
| Motion | MPU6050 (IMU) | I2C | Accel X (m/s2), Accel Y (m/s2), Accel Z (m/s2), Gyro X (rad/s), Gyro Y (rad/s), Gyro Z (rad/s), Temperature (C) | adafruit/Adafruit MPU6050 | accelRange, gyroRange, bandwidth, cycleMode |
| Motion | ADXL345 (accelerometer) | I2C | Accel X (m/s2), Accel Y (m/s2), Accel Z (m/s2) | adafruit/Adafruit ADXL345 | range, dataRate |
| Motion | QMI8658 (IMU) | I2C | Accel X (g), Accel Y (g), Accel Z (g), Gyro X (deg/s), Gyro Y (deg/s), Gyro Z (deg/s), Temperature (C); Steps with Count steps on | lewisxhe/SensorLib | pedometer (Count steps), stepSwing, stepPeak, stepFastest, stepSlowest, stepEntry, stepEvery |
| Motion | VL53L0X (distance) | I2C | Distance (mm) | adafruit/Adafruit_VL53L0X | sense |
| Motion | HC-SR04 (ultrasonic distance) | GPIO | Distance (cm), Something in range | built in (no library) | maxCm, tempC |
| Temperature | MAX6675 (thermocouple) | SPI | Temperature (C) | adafruit/MAX6675 library | settleMs |
| Temperature | MAX31855 (thermocouple) | SPI | Temperature (C), Cold junction (C) | adafruit/Adafruit MAX31855 library | settleMs |
| Temperature | MLX90614 (IR thermometer) | I2C | Object temperature (C), Ambient temperature (C) | adafruit/Adafruit MLX90614 Library 2.1.7 | - |
| Time | DS3231 (RTC) | I2C | Hour, Minute, Second, Day, Month, Year, Unix time (s), Die temperature (C) | adafruit/RTClib | sqw, enable32k, setOnBoot |
| Time | DS1302 (RTC) | GPIO | Hour, Minute, Second, Day, Month, Year, Day of week, Unix time (s), Clock running | Rtc by Makuna | trickle, setOnBoot |
| Storage | SD card | SPI | Card size (MB), Used (MB) | SD (bundled with the core) | freqMhz |
| Output | PCA9685 (16ch PWM) | I2C | - | adafruit/Adafruit PWM Servo Driver Library | pwmFreq, oscFreq, totemPole |
| Output | NeoPixel string (WS2812 / SK6812) | GPIO | Pixel count | adafruit/Adafruit NeoPixel | pixels, order, speed, brightness |
A device that fails to answer at boot is marked absent, its variables keep their initial
values, and the sketch says so on the serial monitor rather than hanging. Each device's library
is listed in LIBRARIES.md with the Library Manager search string.
CAN bus and OBD-II
CAN bus / OBD-II reads a car through its diagnostic socket. Every ESP32 has the CAN controller built in and none has the transceiver, so the one extra part is an SN65HVD230 or a TJA1051 between two pins of your choice and the socket: CAN-H is pin 6, CAN-L pin 14, ground pin 5. Pin 16 is the battery - 12 V and more - and must never reach the board directly. It is an Arduino export feature, and a board has one.
A car gives up its values in two ways, and the device reads both. Some the engine computer only gives when asked: a request names a PID and the reply carries the value. Others are simply on the bus, sent over and over in a frame with an identifier of its own. The standard PIDs below are built in, each shown on the device with its code and the arithmetic that makes a number of the reply:
| Value | PID | Worked out as | Unit |
|---|---|---|---|
| Coolant temperature | 05 | A - 40 | C |
| Fuel level | 2F | A * 100 / 255 | % |
| Engine speed | 0C | (A * 256 + B) / 4 | rpm |
| Road speed | 0D | A | km/h |
| Engine load | 04 | A * 100 / 255 | % |
| Throttle position | 11 | A * 100 / 255 | % |
| Intake air temperature | 0F | A - 40 | C |
| System voltage | 42 | (A * 256 + B) / 1000 | V |
Beside those, ECU answering is 1 while an engine computer has replied in the last three seconds and Bus active is 1 while anything at all is on the wire. A value the car does not report stays at 0: the firmware asks the car which PIDs it has before asking for any, so a missing one costs nothing. Fuel level is one many cars leave out.
Signals of your own
Anything else the bus carries is a signal you add on the device, with Add a PID or
Add a frame. A PID signal takes the code in hexadecimal, as PID tables give it, and a formula over
A, B, C and D - the bytes of the reply, named the way those
tables name them. A frame signal takes the frame's identifier (tick 29-bit for a long one), the first
byte, how many bytes, their order and whether the number is signed; its formula works on raw, the
number those bytes make, and can also name the frame's bytes B0 to B7. The row shows
what the formula gives for a sample value and says straight away if the export would refuse it.
Every value, built in or added, has a Variable button: it makes a variable that reads it, and that variable is what you pick under Data bindings on a widget - a label's text, an arc's value, a scale's needle. The variable's own formula, smoothing and automations then apply as to any reading.
Two settings matter on a car. Bus speed is 500 kbit/s on nearly all of them; the wrong one takes the board off the bus rather than reading slowly, and the firmware brings it back and keeps trying. Listen only never transmits and never acknowledges, so nothing on the bus can tell the board is there: no PID is asked for, and only frame signals are read. Use it on a bus you must not disturb. Cars older than about 2008 often use K-line rather than CAN, which this does not speak.
RS485 and Modbus
RS485 / Modbus RTU reads and writes the devices on an RS485 line: a drive, an energy meter, a temperature controller, a relay board. The board is the master - it asks, they answer. It needs an RS485 transceiver on a serial port of its own: TX to the transceiver’s DI, RX from its RO, and one more pin to DE and RE together, which the chip’s serial port then switches by itself to the bit. A board whose transceiver turns itself round, as Waveshare’s do, leaves that pin unset. It is an Arduino export feature for the ESP32 family, and a board has one.
Nothing is asked for until you say what to read. Add a register on the device takes:
| Setting | What it is |
|---|---|
| Device address | The address set on the device itself, 1 to 247. Its manual may call it the slave ID or unit ID. |
| Table | Holding register or input register (numbers), coil or discrete input (one bit). Holding registers and coils can be written; the other two are read only. |
| Register number | As the protocol counts it, from 0. Where a manual writes 40101 or 30101 it means register 100: drop the leading 4 or 3 and take one off. |
| Holds | 16-bit unsigned or signed, 32-bit unsigned or signed, or a 32-bit float. A 32-bit value is two registers, and Word order says which half comes first - devices disagree, and a reading that is nonsense is usually this. |
| Formula | Over raw, the number the device sent. A register in tenths of a degree is raw / 10. |
Each register then has a Variable button like any other value, and the row says straight away what the export would refuse. Registers are asked for one at a time, in turn, with a pause between requests that you set; nothing waits for an answer, so a device that is switched off costs the screen nothing. A register that stops answering keeps its last reading. Two values say how the line is doing: A device answering is 1 while anything has answered in the last few seconds, and Failed requests counts those that got no answer, a damaged one, or a refusal.
To write a holding register or a coil, use the action Write to peripheral: it offers the registers that can be written. What is sent is the number the device holds, before your formula - a formula runs one way - rounded to a whole number for the integer kinds and held to their range. A write goes out before the next read, is tried three times if nothing answers, and the register is read back straight after, so what the design shows is what the device now holds.
Every device on the line needs the same Speed and Format as the board. The Modbus standard asks for even parity (8E1); most low-cost devices ship set to 9600 baud, 8N1. If nothing answers, check those two, the device address, and then swap the A and B wires: makers disagree about which is which.
Modbus TCP
Modbus TCP (client) is the same thing over a network: a meter, a drive or a PLC that is on the network itself, or a gateway in front of an RS485 line. It has no pins - give it the device’s Address (an IP address, or a name the network’s DNS knows) and Port (502, Modbus TCP’s own) - and its registers are added exactly as on RS485, with one difference: a register’s device is a Unit id, 0 to 255. A device that is itself on the network usually answers to 1 or 255; behind a gateway, the unit id is the device’s address on its serial line.
It needs a network: a WiFi Setup or WiFi by phone widget, or an Ethernet port. In a design with both, Reached through chooses which, and starts as Ethernet. It keeps one connection open, asks for one register at a time, writes before it reads and reads a register straight after writing it, as the RS485 driver does, and it connects again whenever the connection is lost. All of it runs in a task of its own on core 0, so the screen never waits for the network. Besides each register, it says Connected to it, A device answering and Failed requests. A design may have several, one for each device or gateway. Arduino export, any ESP32; nothing here has spoken to a real device yet.
A camera
A camera with a DVP connector (an OV2640 or OV5640 module) is a device here too: its control lines on an I2C bus, its clock, sync and data lines on pins of its own. A Camera view widget plays it on a screen, the action Take a picture saves a JPEG, and it can be watched over Wi-Fi. It has a page of its own. ESP32-S3 with PSRAM, Arduino export.
On an ESP32-P4 the camera is the one on its MIPI connector (an OV5647, an SC2336 and the like): only its control bus is set here, since the chip's camera lanes are fixed. It does everything the S3's camera does, and can also be an RTSP camera on the network (Pro): the P4's camera.
Wired Ethernet
A wired network is a device as well: Ethernet over SPI (a W5500 module, on any ESP32) or an Ethernet PHY on RMII (a LAN8720 or the like, on the original ESP32 and the ESP32-P4). Everything that uses the network then works over the cable, with or without Wi-Fi. Which pins each needs, and what it says of itself, is on the network page. Arduino export.
A device’s line on an I/O extender
Two lines of a device may be a pin of an I/O extender rather than a GPIO, because some boards are wired that way. An SD card’s CS: the SD library cannot switch a pin it does not have, so the card is selected once and stays selected - which means it must be the only thing on its SPI bus, and the Errors tab says so if it is not. And a CAN bus’s enable / select line: a line the transceiver needs held one way to be on the bus, held that way while the device is on and the other way while it is switched off. Waveshare’s ESP32-S3-Touch-LCD-4.3 and ESP32-S3-Touch-LCD-7 have both: the card slot’s CS and the switch that hands GPIO 19 and 20 from the USB port to the CAN transceiver are pins of the CH422G. Their CAN bus therefore arrives switched off - switching it on takes the USB port away, and flashing is then through the UART port; those two boards have a page of their own. Only the Arduino export drives a device’s line through an extender; on the other two that device is left out of the board’s kit.
Which export reads which. The Arduino export reads every device here. The MicroPython export reads most of them through a driver module of its own for each – all but the BME280, BMP280, SCD4x, VL53L0X, the CAN bus, RS485, Modbus TCP and Ethernet – and ESPHome reads the ones it has a component for. A device the project’s export cannot read is greyed in the list with the reason. The I/O extenders below are driven by all three: every one on Arduino and MicroPython, seven on ESPHome.
Air quality and an infrared thermometer (SEN66, MLX90614)
The SEN66 is Sensirion’s air-quality module: particles (PM1.0, PM2.5, PM4.0 and PM10, in micrograms per cubic metre), humidity, temperature, a VOC index, a NOx index and CO2 in ppm, nine values from one part on I2C at 0x6B, its only address. It measures once a second by itself; the export starts it at power-up (a reset, which takes 1.2 s, then a start) and before each read asks whether a new set is ready, so the poll interval, 2 s to begin with, never reads one twice. The sensor says “not known yet” for a value while it warms up - CO2 for its first seconds, the NOx index for about ten - and such a value is not shown as a number: each one stays at 0 until the sensor has given it once, and after that keeps its last value. The VOC and NOx indices are relative to the air the sensor has known: 100 (VOC) and 1 (NOx) are “as usual”, and they take hours of running to settle.
The MLX90614 is Melexis’s infrared thermometer: Object temperature is whatever it points at, Ambient temperature the sensor itself, both in °C. It answers at 0x5A unless its address has been changed (the address is kept in the sensor’s own memory; 0x5B to 0x5D are offered too). It is an SMBus part, made for a 100 kHz bus at most: on a faster bus the Check tab says so, and the bus’s Clock is under Buses. Its emissivity is left as the sensor has it.
Two addresses that are often taken. 0x6B is also the QMI8658 motion sensor’s on many boards, and 0x5A the CST9217 / CST9220 touch panel’s; the Check tab refuses either clash on one bus. On the Waveshare ESP32-C6-Touch-AMOLED-2.16 both are soldered to the board’s one I2C bus (GPIO 8 / 7, the SCL and SDA pads): an MLX90614 there needs its address changed first, and a SEN66 cannot share those wires with the board’s QMI8658 - taking the motion sensor out of the design does not take the chip off the bus.
Both are read on Arduino (Sensirion’s and Adafruit’s libraries), ESP-IDF and MicroPython (drivers of
this export’s own, checked on a PC against those libraries), and ESPHome (its sen6x and
mlx90614 components). On ESPHome the SEN66 publishes nothing for its first minute, and the MLX90614
component writes an emissivity of 100 % into the sensor at every start. Compiled; not yet seen on a sensor.
Counting steps (QMI8658)
The QMI8658 has a step counter of its own - the one on Waveshare’s watch-shaped boards, the ESP32-S3-Touch-AMOLED-2.06 among them. Tick Count steps in its device configuration and it gives one more value, Steps, for a variable to read and a label or an arc to show, and one write, Reset steps, for a button’s Write to peripheral action. With Count steps off neither is there, and the device is written exactly as it was before.
The counter works from the accelerometer, so with it on the accelerometer and the gyroscope both run at 56 samples a second (rather than about 900); acceleration, rotation and temperature are read as before. The chip counts a walk only once several steps come in a row - ten, unless Steps before counting starts says otherwise - and then counts them all, so the number goes from 0 to 10 at once and then by ones. The other settings are the chip’s own: how big a swing is a step, how far above the average a peak has to rise, the shortest and the longest step. Their defaults are the example in QST’s datasheet; a wrist swings more than a belt, so a watch that counts too many is given a bigger swing.
The count is kept through a read that fails, and goes back to 0 only with Reset steps or a power cut. In Preview, Steps is a simulated reading in the drawer, and a Reset steps button puts it to 0. The Arduino, MicroPython and ESP-IDF exports all count; ESPHome’s own QMI8658 component has no step counter, and this export does not write the part on ESPHome.
Not yet seen working on a watch. The register writes are SensorLib’s, checked on a PC against the library itself, but nothing here has counted a real walk, and people have reported the counter reading 0 on some of Waveshare’s boards. If yours stays at 0 while you walk, say so through Report a problem.
QMP6988 barometer (M5Stack ENV III)
The QMP6988 (pressure) device, under Environment, is the barometer in M5Stack’s ENV III unit (its SHT30 is an SHT31 / SHT35 at 0x44). It sits on I2C at 0x70, or 0x56 with its address pad changed, and gives Pressure (hPa) and Temperature (C). Its configuration is Temp oversampling, Pressure oversampling (more is steadier and slower) and IIR filter. It needs no library, and it is read on every export.
A joystick
The Joystick (analogue thumbstick) device, under Input, is the common KY-023 kind: two potentiometers and a press switch to ground. Give it the pins VRx (X axis), VRy (Y axis) and SW (press), or under Read X and Y from choose An ADC device in this design and pick an ADS1115 or MCP3008 and a channel for each axis, which reads more steadily and frees pins. It gives X and Y (-100 to 100, 0 at rest), Pressed, and Direction (0 centre, 1 up, 2 right, 3 down, 4 left).
The resting position is taken as the centre from the first reads at start-up, so leave the stick alone while the board starts. Each side is scaled to its own end, so a stick whose centre is not half way still reaches 100 both ways. The settings are Dead zone (8 % by default: how far from the centre before X or Y leaves 0), Invert X, Invert Y and Swap X and Y (for a stick mounted on its side). Compiled, not run on a board.
Power chips (AXP192 and AXP2101)
The AXP192 (power and battery) and AXP2101 are X-Powers power chips on I2C. They give Battery voltage, Battery charge, Charging, Battery connected, USB power present, USB voltage and System voltage. On a board whose screen is powered through one, the preset switches the screen’s supply, reset and backlight through the chip before the panel starts: that is how the M5Stack Core2 presets (v1.0 with an AXP192, v1.1 with an AXP2101) light the screen. The AXP192 works on Arduino, ESP-IDF and MicroPython; ESPHome has no component for it and refuses it. Compiled, not run on a board.
Stream to card: a CSV of your variables
Hardware › Devices › Stream to card writes the design’s variables to a CSV file that a spreadsheet opens: a row at a time, each variable a column headed by its name. It is free, on the Arduino export, on the ESP32 family. With no SD card in the design a log is kept in the board’s own flash. Add log adds one; a design can have up to eight.
| Setting | What it does |
|---|---|
| Name | What the actions call this log. |
| Kept on | The SD card (take it out and read it on a computer) or The board’s own flash (works with no card, in files of the size set below). |
| File name | Letters, figures, - _ . and / for a folder; .csv is added when the name has no ending. Put {n} in it (log_{n}.csv) for a new file each time the log starts: log_1.csv, then log_2.csv. Without {n}, rows are added to the end of the same file under the header it already has, so after changing the columns give it a new name. |
| Columns | Tick the variables to write, in the order ticked. Whole numbers as they are, numbers with the variable’s own decimals, 0 or 1 for on and off, text in double quotes. A list is not a column. |
| First column | None, Seconds since the board started, or Date and time from the design’s clock (YYYY-MM-DD HH:MM:SS, empty until the clock knows the time; offered when the design has a Clock widget). |
| Every (s) | The time between rows. 0: a row only when an action asks. |
| Start | When the board starts. Off, the log waits for an action. |
| New file at (KB), Old files kept | When a row would take the file past this size it is renamed (log.1.csv, the one before that log.2.csv) and a new file starts with the header. Beyond Old files kept, the oldest is deleted. On the card the default is never; in flash a size is required (64 KB by default). |
Actions (group Stream to card): Stream to card: start or stop (Do: Start, Stop, or start if stopped and stop if running; a start picks the next free {n}) and Stream to card: write a row (one row now, running or not). Values on the Data tab, in a group Log: <name>: Running (1) or stopped (0) and Rows written since start-up.
Every row opens the file, appends and closes it, so a pulled card or a power cut loses one row at most. A log that cannot write stops itself. The card is found by the card watcher if it is swapped while the board runs. Not done: noticing that the columns changed, reading a log back on the board, and the ESP-IDF, MicroPython and ESPHome exports (each refuses it). In Preview the values read 0. Compiled, not run on a board.
Configuration worth knowing about
- BME280 / BMP280 - mode, per-channel oversampling, IIR filter, standby, and the
sea-level reference the altitude channel is computed from. The oversampling group is emitted
as one
setSampling()call. - SCD4x - temperature offset, altitude and automatic self-calibration. Changing these requires the measurement to be stopped and restarted, which the generated code brackets.
- ADS1115 - programmable gain and data rate; the channel readings are in volts after the gain is applied.
- INA219 - the shunt calibration preset.
- MPU6050 - accelerometer and gyro ranges, filter bandwidth, cycle mode.
- DS3231 - square-wave output, the 32 kHz pin, and set on boot, which writes the compile time into the clock on first run. Its Unix time channel is what a chart's time axis reads.
- A clock module is what a Clock can keep its time on. Choose a DS3231, a DS1302 or a
PCF85063 under Keeps time on on the Clock
and the time is read from it at power-up and written to it whenever the clock is set - by a
time server, or by hand with the Set the clock
action. Each has
per_<name>_get_time()andper_<name>_set_time()inhardware.hfor your own code, with or without a Clock. - DS1302 - the clock module with three pins of its own (CLK, DAT and RST) rather than a bus. A new one is stopped until its time is set, and Clock running says which; set on boot writes the compile time into one that is stopped or holds no real date. Trickle charge is for a module with a rechargeable cell or a supercapacitor and must stay off for a coin cell, which is not to be charged.
- HC-SR04 - the ultrasonic ranger: Trig and Echo on a pin each, and no library. Most modules are 5 V parts, so Echo needs dividing down for a 3.3 V pin. Furthest distance is how long a read waits for an echo - a read with nothing in range waits the whole of it, about 25 ms at four metres - and Air temperature is what the speed of sound is worked out from. Nothing in range reads 0, and Something in range says which 0 that is.
- PCA9685 and NeoPixel are outputs: they have no readable channels, and are driven by the Write to peripheral action or a Widget → variable binding.
- SD card - besides its size channels, this is the device that makes SD card available as the asset filesystem.
I/O extenders
Port expanders that add digital pins over I2C or SPI. Add one on the I/O tab with its
bus and address (or CS pin), and digital I/O entries can then be placed on its pins instead of
the MCU's. Each pin's direction is set by the entry that uses it - a digital input or output
placed on the pin, a line of the board's that runs through it, or the extender's own pin table
below, in that order where two name one pin - and the generated
ext_<name>_write() and _read() helpers are guarded by the
expander's presence.
| Extender | Bus | Pins | Library |
|---|---|---|---|
| PCF8574 | I2C | 8 | xreef/PCF8574 library |
| PCF8575 | I2C | 16 | xreef/PCF8575 library |
| MCP23017 | I2C | 16 | adafruit/Adafruit MCP23017 Arduino Library |
| MCP23S17 (SPI) | SPI | 16 | adafruit/Adafruit MCP23017 Arduino Library |
| CH422G | I2C | 12 | built in (no library) |
| TCA9554 / PCA9554 | I2C | 8 | built in (no library) |
| PCA9555 | I2C | 16 | built in (no library) |
The validator errors on an extender pin outside the part's width, on two entries claiming the same pin, and on an entry pointing at an extender that has been deleted. The same rules cover a display reset, touch reset or backlight routed through an extender - a backlight there switches on and off but cannot dim, and a touch controller whose reset is there is reset through it at boot and whenever it stops answering, with the bus left running (see Boards and displays).
The CH422G, TCA9554 and PCA9555 need no library: each is a few I2C register writes, and the
generator writes a small driver class into hardware.cpp for them. The PCF and MCP parts
use their Arduino libraries, listed in LIBRARIES.md as usual. A PCF8574 or PCF8575
has no direction register: its library sends the port when it starts, from the modes it has been
given, so the generated code gives every pin its mode first and starts it after.
Analog inputs
An Analog input is an MCU ADC pin read and scaled. Fields:
| Field | Meaning |
|---|---|
| Pin | An ADC-capable GPIO on the chosen MCU. |
| Attenuation | ESP32 only: 11 dB (0-3.3 V), 6 dB (0-2.2 V), 2.5 dB (0-1.5 V) or 0 dB (0-1.1 V). |
| Samples | How many conversions are averaged per read. |
| Poll (ms) | How often it is read. |
| Smoothing | An exponential filter: 0 is raw, 0.2 is a good default, closer to 1 reacts faster. |
| Scaling | A linear map from a raw range (e.g. 0-4095) to a value range (e.g. 0-100). |
| Formula | Optional; overrides the scaling. raw is the averaged ADC count. |
| Unit | A label carried through to bindings. |
The ADC pins per MCU come from the catalogue - the ESP32 has 16, the S3 20, the C3 6, the RP2040 4 - and a pin that is not one of them is an error. The STM32 and ESP32-P4 entries currently have no ADC pins listed, so analog inputs are not offered on them.
Digital I/O
A Digital input or output on an MCU pin or an extender pin:
| Field | Meaning |
|---|---|
| Source | MCU pin or I/O extender. |
| Pin / Extender pin | The GPIO, or the extender and its pin number. |
| Active low | Active when the pin reads LOW. Everything downstream uses "active", not the raw level. |
| Pull-up | Enable the internal pull-up on an input. |
| Debounce (ms) | How long a level must hold before it counts. |
| Initial state | For an output: start active or not. |
Inputs are debounced in hw_poll() and exposed to variables as a Boolean; the
Digital input changes and Digital input held
automation triggers watch them. Outputs are driven by the
Set digital output action. "Active" respects the active-low setting throughout, so a
relay module that switches on LOW is described once and reads correctly everywhere.
What the generated code does
hardware.cpp declares one driver object per device and the bus objects they sit
on. hw_init() begins each bus, probes each device, applies its configuration and
records whether it answered. hw_poll(), called from app_poll() every
loop, checks each device's own timer and reads it when due into a cache struct. Reads are cheap
to ask for and never block on a slow part - a BME280 polled every two seconds costs nothing to a
label refreshed ten times a second. The Refresh inputs now action forces every timer
due at once.
Code of your own reaches all of this - the buses, the widgets and the variables - from a file beside the sketch, through the setup and loop hooks: see Your own code.