Step 34 of 56
Port expanders: TCA9555, CH422G and HC595
Three ways to buy more pins, and three quite different bargains — per-pin control, an all-or-nothing direction bit, and a shift register with no readback at all.
Two levels of locking, again
The two I2C expanders share the pattern the
ST7789 uses: a Session owns the
device, while the I2C host is locked only for
each transaction. The per-device guards live in a fixed library-level array
keyed by (host, strap value), not inside the Device record
— a protected object there would be a local PO, which this runtime
forbids.
TCA9555 — the conventional one
subtype Hardware_Address is Natural range 0 .. 7; -- three strap pins
type Pin_Number is range 0 .. 15; -- P0 = 0..7, P1 = 8..15
type Port_Value is mod 2**16;
type Direction is (Output, Input);
type Pin_State is (Low, High);
Sixteen pins in two 8-bit ports, per-pin direction, three address straps so up
to eight can share a bus, and an interrupt output with a .Interrupts
child. If you want an expander that behaves the way you expect, this is it.
CH422G — the one that will surprise you
Eight bidirectional pins plus four output-only ones. Same locking shape, very different chip:
It is not a register-pointer device. Each
operation is a single-byte transaction to a fixed, function-specific I2C
address: 0x24 system config, 0x23 write OC
outputs, 0x38 write IO outputs, 0x26 read IO inputs.
One consequence follows immediately — there are no address straps and
only one CH422G per bus, so Setup takes no
address.
Direction is global. A single IO_OE
bit makes all of IO0..IO7 inputs or all of them outputs. There
is no per-pin direction. If you need three inputs and five outputs from one
CH422G, you cannot have it.
type IO_Direction is (Inputs, Outputs); -- all of IO0..IO7, together
type OC_Drive is (Push_Pull, Open_Drain); -- all of OC0..OC3, together
The config, OC and IO-output registers cannot be read
back — only the IO pins can, via RD-IO. So the driver keeps a
shadow initialised to the datasheet's power-on defaults (IO inputs, OC high,
push-pull). That shadow is only correct if the chip really is at its defaults
when you start, so a warm restart without a power cycle can leave the driver's
idea of the outputs out of step with reality. The chip also has no interrupt
output, hence no .Interrupts child.
This is the expander that holds the GT911's reset line on the Waveshare ESP32-S3-Touch-LCD-7 — see step 31 for why that ordering matters.
HC595 — a shift register on the SPI bus
Not I2C at all: any number of 74HC595s daisy-chained (each chip's QH' into the
next chip's SER), giving N * 8 outputs from three wires.
type Controller (Chips : Positive) is limited private;
procedure Set_Output (C : in out Controller; Index : Natural; On : Boolean);
procedure Set_Byte (C : in out Controller; Chip : Natural; Value : Byte);
procedure Update (C : in out Controller); -- shift out + latch
procedure Write_Output (C : in out Controller; Index : Natural; On : Boolean);
procedure Clear_All / Set_All (C : in out Controller);
The driver keeps a shadow of the intended state; Update shifts
the whole string out in chain order and pulses RCLK to latch it. So
Set_Output is buffered and Write_Output is
set-then-update — batch your changes and call Update once
rather than paying a full string shift per bit.
It borrows the SPI host per Update with no
chip select asserted, so other devices on the bus are undisturbed. The
application must Setup and Configure_Pins the shared
SPI host first — the expander does not own it.