Bit-Addressable Ports
I/O port registers are normally byte-addressable. Changing 1 pin without disturbing others requires a read-modify-write sequence.
Bit-addressable ports expose individual bits as independently addressable locations. A single instruction can set, clear, or test 1 pin without reading and rewriting the full byte.
On the 8051, dedicated bit-address instructions operate directly on individual port bits:
SETB P1.0
Set bit 0 of Port 1 HIGH.CLR P1.3
Clear bit 3 of Port 1 LOW.MOV C, P2.5
Copy bit 5 of Port 2 into the carry flag.
Only SFRs whose address is divisible by 8 are bit-addressable on the 8051. Examples: P0 at 80H, P1 at 90H.
I/O Port Hardware
Each GPIO port is built from flip-flops and tri-state buffers.
Flip-Flops
A flip-flop is a bistable latch that holds 1 bit indefinitely until overwritten.
- Output path
When the CPU writes a value to a port register, it is clocked into a flip-flop. The flip-flop drives the output pin continuously. Without the latch, the output pin would float the moment the CPU bus moved to the next operation. - Input path
A separate flip-flop (the input register) samples the voltage on the pin on a clock edge and presents a stable value to the CPU data bus. Sampling synchronously prevents metastability. An external signal can change at any time, but the CPU always reads a settled, single-clock-domain value.
2 flip-flops per pin decouple 3 independent concerns:
- What the CPU last wrote (output latch)
- What the pin is currently driven to (output driver)
- What the external world is presenting (input register)
Tri-State Buffers
A tri-state buffer has 3 output states: HIGH, LOW, and high-impedance (Hi-Z). In Hi-Z the output is effectively disconnected. It neither sources nor sinks current and does not influence the line it is connected to.
Required for:
- Shared bus isolation
The internal data bus connects many registers and peripherals. If 2 drivers are simultaneously active on the same wire, 1 driving HIGH and 1 LOW, the result is a near-short circuit and an undefined voltage. Each peripheral’s connection to the bus is gated by a tri-state buffer enabled only when that peripheral is selected. - Input pin configuration
When a GPIO pin is configured as input, its output driver is placed in Hi-Z. The pin is purely receptive. The external circuit drives the line without the MCU’s output stage fighting it. - Bidirectional lines
SPI MISO, I²C SDA, and PSP data lines are bidirectional. Tri-state buffers allow the direction to be switched under software control. 1 side enables its driver while the other is in Hi-Z.
Bidirectional Port
A GPIO port where each pin can be independently configured as either input or output at runtime.
Pin direction is controlled by a direction register (DDR). Each bit in the DDR corresponds to 1 pin. Writing 1 configures the pin as output. Writing 0 configures it as input. The data register and direction register are separate, so direction can be changed without disturbing the output latch value.
Internal structure per pin:
- Output latch (flip-flop) stores the value to drive when configured as output.
- Direction bit gates the tri-state output buffer. DDR=1 enables the driver. DDR=0 forces it to Hi-Z.
- Input register samples the pin regardless of direction, so software can read back the actual pin state even when configured as output.
A single physical pin can serve different roles at different points in program execution.
Examples:
- A pin configured as output to drive an LED, then reconfigured as input to read a button on the same line.
- I²C SDA: the MCU drives the line to send data, then releases to Hi-Z to receive an acknowledgement, all on the same pin within 1 transaction.
- 1-Wire bus: master drives the line LOW to initiate, then releases to Hi-Z to sample the device response.
Writing to an Input Pin
The output latch and the direction register are independent. Writing a value to a pin configured as input stores the value in the output latch but does not drive the pin. The tri-state output buffer remains in Hi-Z.
2 consequences:
- On most ARM Cortex-M MCUs
The written value sits dormant in the output latch. When the pin is switched to output mode, that latched value immediately appears on the pin. Pre-loading the output value before enabling the driver prevents a glitch on direction change. - On AVR MCUs
Writing HIGH to an input-mode pin activates the internal pull-up resistor. Writing LOW deactivates it. The PORTx register doubles as the pull-up enable register when DDRx is 0. ARM MCUs use a separate pull-up enable register instead.
In Arduino, digitalWrite() called on an INPUT-mode pin enables or disables the internal pull-up. The behaviour is documented.
Reading an Output Pin
The input register samples the actual voltage on the physical pin, independent of direction setting. Reading an output pin returns the real pin state, not the output latch value.
The 2 differ when an external circuit overrides the pin:
- Shorted output
If an output driving HIGH is shorted to GND externally, the input register reads LOW while the output latch holds HIGH. Comparing the 2 detects the fault. - Open-drain/open-collector
The pin is Hi-Z when released. An external device may hold the line LOW. Reading the pin reflects the actual bus state. - Slow external load
A heavily capacitive line takes time to reach the driven voltage. Reading immediately after writing may return the old level until the line settles.
On AVR, reading PINx on an output pin confirms the actual driven level, providing hardware-level output verification with no extra pins.