When the Camera Eats All Your GPIO: PCF8574, a Hex Inverter (74HC04), and a 470µF Surprise

The ESP32-S3 has 45 GPIO pins. This sounds like plenty until you actually need a camera on it.
The OV5640 is a 5MP camera module with autofocus. To interface it with the ESP32-S3, you need the full parallel camera bus: 8 data lines (D0–D7), plus XCLK, PCLK, HREF, VSYNC, and the I2C lines for camera configuration. That's 13 pins before you've done anything else. Add the SD card — even in 1-bit SDMMC mode, the most conservative option — and you've used 3 more. The WS2812 RGB status LED takes another. The BOOT button claims GPIO 0.
By the time you've assigned everything that's baked into the silicon, the list of genuinely free pins for the project's actual job — detecting a cat, activating a water mist, controlling LED strips — is discouragingly short. And many of the "free" pins have hidden constraints: GPIO 1 and 3 are UART0 (used by the serial monitor during development), GPIO 19 and 20 are USB D- and D+, and various pins have boot-time strapping requirements that restrict their use.
The solution is a GPIO expander. But choosing one comes with its own problems.
The PCF8574
The PCF8574 is an 8-bit I2C GPIO expander. It needs exactly two pins from the host microcontroller — SDA and SCL — and in return provides 8 quasi-bidirectional I/O lines. With the PCF8574's three address pins tied to ground, it sits at address 0x20 on the I2C bus, leaving room for up to seven more devices on the same two wires if needed later.
The word "quasi-bidirectional" is doing a lot of work in that description. The PCF8574's I/O lines are not like the GPIO pins you're used to. Each pin has a weak internal pull-up and is driven by an open-drain transistor. Writing a 1 to a pin pulls it high — weakly, via the internal pull-up. Writing a 0 pulls it low — actively, via the transistor. This asymmetry matters when you need to drive any load with meaningful current: the PCF8574 can sink about 25mA per pin, but it can barely source anything.
In the BootBoots system, the PCF8574 controls four outputs (atomiser, two LED strip channels, flash LED) and reads four inputs (PIR sensor, light sensor, pushbutton, pressure sensor). The input side works beautifully — the weak pull-ups hold input pins high until an external signal pulls them low, which is exactly what you want for reading sensors. The output side needs help.
The hex inverter
Enter the hex inverter (74HC04) — a chip containing six independent NOT gates. Each gate takes a logic input and produces its complement: HIGH in, LOW out; LOW in, HIGH out.
Its role here is twofold.
First: current driving. Each gate of the hex inverter can source and sink around 25mA — better than the PCF8574's weak open-drain outputs, but still marginal for driving a MOSFET gate or relay coil reliably. The fix is to wire two gates in parallel on the same signal: tie both inputs together, tie both outputs together. The 74HC04 has six gates in one package; with only a handful of signals to invert, there are spare gates sitting idle. Two gates in parallel gives you ~50mA of drive, which was enough. Feed a PCF8574 output through the paralleled gates and you get a properly buffered signal capable of controlling downstream loads without browning out the expander.
Second: active-low becomes active-high (and vice versa). The PCF8574 starts up with all pins high — that's the safe state. For outputs that control potentially dangerous things (water atomiser, mains-voltage relay), you want the default state to be OFF. If OFF corresponds to a high output, you can wire the load directly. If OFF corresponds to a low output (because the load is wired active-high), you've got a problem: the PCF8574's high startup state would immediately activate the load at power-on.
The LED strips and atomiser are wired active-low through the expander: a LOW on the PCF8574 pin activates them. The inverter flips this so the load-side signal is active-high, whilst the PCF8574-side logic stays safe. Power comes on, all PCF8574 pins start high, all loads start off. No surprises.
Or so I thought.

The blip
During early testing, I noticed that the water atomiser occasionally fired a brief pulse at power-on — a fraction of a second, barely enough to produce a fine mist, but unmistakable once I started paying attention. It happened consistently. Not always, but reliably under certain conditions.
The cause is an interaction between two things happening simultaneously at power-up: the ESP32-S3 is initialising its internal subsystems, and the PCF8574 is waiting for its first I2C write to establish pin states. There is a window between these two events during which the PCF8574 pins are in their reset state (all high, all safe) — but the GPIO pins that drive the hex inverter's inputs haven't been configured yet. Before the firmware runs Wire.begin() and sends the initial pin state over I2C, those PCF8574 inputs may float briefly.
A floating input on a CMOS logic gate is undefined behaviour. The hex inverter's output for that input could be anything. For a brief moment, the atomiser control line was seeing a signal it shouldn't have, and the solenoid valve was responding to it.
The software fix would be to initialise the I2C bus and write a safe state to the PCF8574 as early as possible in setup(). The hardware fix is a 470µF electrolytic capacitor across the atomiser solenoid's power supply. When the blip appears, the capacitor absorbs it — the solenoid sees a voltage spike that the capacitor clamps before it has enough energy to open the valve. By the time the capacitor charges up to a level that could actually drive the solenoid, the firmware has initialised the PCF8574 and the pin state is defined.
The capacitor also protects against the back-EMF spike when the solenoid deactivates — the magnetic field collapsing through the coil sends a voltage spike back up the supply rail. Without the capacitor, this spike could disturb other components on the same supply. With it, the capacitor absorbs the spike and releases it slowly.
The initial pin state
The PCF8574Manager library initialises the expander with 0x7F — all pins high except P7 (the flash LED, which is active-high rather than active-low and starts off at LOW):
static constexpr uint8_t PCF8574_INITIAL_PIN_STATE = 0x7F;
// P0-P6: HIGH (inputs float safely; active-low outputs are inactive)
// P7: LOW (flash LED is active-high, starts off)
This is written to the PCF8574 in the first I2C transaction of the firmware. Until that transaction completes, the hardware state is held by the capacitor and the PCF8574's own reset values. After it completes, everything is deterministic.
The 30 milliseconds or so between power-on and the first I2C write might seem like an insignificant window. A solenoid valve doesn't care how long the pulse lasts, though — it only cares whether the pulse has enough energy to open it. A 470µF capacitor sized correctly ensures it doesn't.
What I'd do differently
The elegance of the PCF8574 + hex inverter (74HC04) combination is that it's almost entirely passive — two ICs, a handful of resistors, a decoupling capacitor, and you've added 8 GPIO lines to a microcontroller that had run out. The hex inverter handles level shifting and current driving in a single package with no programming required.
If I were starting over, I might look at the TCA9534 or similar expanders that have configurable output types (push-pull vs open-drain) and a hardware reset pin for a cleaner startup sequence. But the PCF8574 solution works, the capacitor handles the edge case, and the cats have no complaints about the power-on behaviour.
Except Boots. But Boots' complaints are the point.
Martin Harris connects embedded hardware to cloud infrastructure. The 470µF capacitor was the last component added and the first one that actually mattered.
If you find this blog useful, please consider a donation.
