# 3.5" TFT LCD Shield (ILI9486/ILI9488, 8-bit parallel) — Programming Guide

- **Audience:** an AI agent or developer writing firmware for this board on **any** MCU and framework.
- **Scope:** the board itself — pinout, power, display bus, controller, touch, microSD — independent of any project.
- **Revision:** 2.1 — 2026-09-18 (supersedes the photo-only assessment of 2026-09-15).
- **Evidence:** owner's front and rear photos; one hardware-verified bring-up on an ESP32-S3 host (§12);
  library sources checked on 2026-09-18 (TFT_eSPI 2.5.43, MCUFRIEND_kbv, ESP-IDF `esp_lcd`); bus timing
  and pixel formats checked against the ILI9488 V1.00 and ILI9486 V0.01 datasheets (§13).

## How to use this document

Every non-trivial claim carries a status:

| Status | Meaning |
|---|---|
| **VERIFIED** | Observed on this physical unit: read from its silkscreen/photos, or confirmed working on hardware (§12). |
| **LIKELY** | Strong inference, datasheet statement or proven library practice — not yet observed on this unit. |
| **UNKNOWN** | Not determined. Measure before relying on it. |

Rules:

- Never promote LIKELY or UNKNOWN to fact in code, comments, logs or docs. Example: do not log an
  *assumed* controller as "detected".
- When you verify something on hardware, update the status here and add an entry to §12.
- Project-specific details (GPIO numbers, framework, file paths) belong in the project, not in this
  guide — except as dated evidence in §12.

---

## 1. Quick facts

| Item | Value | Status |
|---|---|---|
| Board | Generic "3.5" TFT LCD Shield", Arduino Uno R3 shield form factor, 28 header pins | VERIFIED |
| Panel | 320 × 480 native (portrait), used as 480 × 320 in landscape | VERIFIED |
| Display bus | Intel 8080 (i80; the datasheets call it DBI Type B), **8-bit parallel**: `LCD_D0…D7` + `LCD_CS`, `LCD_RS`, `LCD_WR`, `LCD_RD`, `LCD_RST`. Not SPI. | VERIFIED |
| Controller | ILI9486 or ILI9488 (both printed on the PCB). This unit works correctly with the ILI9488 init table. | LIKELY ILI9488 |
| Power | 5 V into the `5V` pin + GND; onboard LDO makes 3.3 V | VERIFIED |
| Host logic level | 3.3 V logic drives the board correctly | VERIFIED |
| | 5 V logic (Arduino Uno) is the design target of this shield type | LIKELY |
| Working pixel format | `COLMOD (0x3A) = 0x66` → RGB666, 3 bytes/pixel | VERIFIED |
| Faster pixel format | `COLMOD = 0x55` → RGB565, 2 bytes/pixel | LIKELY — test it |
| Color order | BGR (MADCTL bit 3 set) | VERIFIED |
| Landscape | `MADCTL (0x36) = 0x28` → 480 × 320, correct, not mirrored | VERIFIED |
| Write clock | 20 MHz on `LCD_WR` (50 ns per byte), stable over jumper wires | VERIFIED |
| Write clock limit | ILI9488: write cycle ≥ 40 ns (25 MHz); ILI9486: ≥ 66 ns (≈ 15 MHz) — §3.4 | LIKELY (datasheets) |
| Readback (`LCD_RD`) | Never tested. The board may be write-only. | UNKNOWN |
| Touch | 4-wire resistive, no touch-controller IC, electrodes share LCD lines | type VERIFIED, wiring UNKNOWN |
| microSD | SPI socket on the shield (`SD_CS`, `SD_DI`, `SD_DO`, `SD_SCK`) | present VERIFIED, function UNKNOWN |
| Backlight | No control pin; always on while powered | LIKELY |
| Full frame | 153,600 px → 307,200 B (RGB565) or 460,800 B (RGB666) | arithmetic |

### Rules of thumb

1. It is a **parallel** display. Do not reuse code or assumptions from SPI ILI9486/ILI9488 modules.
2. **Wire by the shield's labels.** `LCD_D0`/`LCD_D1` sit at the Uno's D8/D9 positions, not D0/D1.
3. Power from **5 V into `5V`**. 3.3 V host logic is fine.
4. Known-good start: **ILI9488 init table (§4.6 A) + `COLMOD 0x66` + `MADCTL 0x28`** at ≤ 20 MHz.
5. Keep **`LCD_RD` high**. Do not design around reading the controller ID — the board may be write-only.
6. Touch has no controller: it needs **ADC-capable host pins** on shared LCD lines (§6).

---

## 2. Board hardware

### 2.1 Headers and pinout

Four male headers (28 pins) on the rear, at Arduino Uno R3 positions. Silkscreen designators J1–J4 are
printed next to them. Labels and positions: VERIFIED from the rear photo. "Unlabeled = not used": LIKELY.

| Header (Uno position) | Uno pin | Silkscreen label | Function | Direction |
|---|---|---|---|---|
| Analog header | A0 | `LCD_RD` | Read strobe, active low | host → board |
| | A1 | `LCD_WR` | Write strobe, active low; panel latches data on the rising edge | host → board |
| | A2 | `LCD_RS` | Register select (D/C): 0 = command, 1 = data | host → board |
| | A3 | `LCD_CS` | Chip select, active low | host → board |
| | A4 | `LCD_RST` | Reset, active low | host → board |
| | A5 | *(unlabeled)* | not used | — |
| Power header | VIN | *(unlabeled)* | not used | — |
| | GND | `GND` | ground | — |
| | GND | `GND` | ground | — |
| | 5V | `5V` | supply input, feeds the onboard 3.3 V LDO | host → board |
| | 3.3V | `3V3` | connection UNKNOWN — do not use as a supply (§2.3) | — |
| | RESET | *(unlabeled)* | not used | — |
| Digital header D0–D7 | D0, D1 | *(unlabeled)* | not used (Uno serial stays free) | — |
| | D2…D7 | `LCD_D2`…`LCD_D7` | data bits 2–7 | host → board |
| Digital header D8–AREF | D8 | `LCD_D0` | data bit 0 | host → board |
| | D9 | `LCD_D1` | data bit 1 | host → board |
| | D10 | `SD_CS` | microSD chip select, active low | host → board |
| | D11 | `SD_DI` | microSD MOSI | host → board |
| | D12 | `SD_DO` | microSD MISO | board → host |
| | D13 | `SD_SCK` | microSD clock | host → board |
| | GND, AREF | *(unlabeled)* | not needed | — |

Data lines are driven by the panel (board → host) only during a read, and only if the board is readable (§2.4).

Wiring notes:

- On an Uno the shield simply plugs in. On any other host use jumper wires (female Dupont fits the male
  pins); this is how the verified setup was wired.
- **Labels are printed only on the rear.** With the screen facing you, the left and right header rows
  swap sides. Wire strictly by label.
- Connect GND first. Keep the 13 bus wires short and of similar length; a second GND wire helps at high
  clock rates.

### 2.2 Components and identification marks (from photos)

| Item | Observation | Status |
|---|---|---|
| U1 | SOT-223 3.3 V LDO, marking looks like AMS1117-3.3, fed from `5V` | part LIKELY |
| U2, U3 | Two TSSOP-20 logic ICs (bus buffers / level shifters). Markings not readable. | part and direction UNKNOWN |
| R1 | Small resistor near U2/U3, purpose not traced (possibly backlight) | UNKNOWN |
| Controller | Chip-on-glass under the panel, not visible | — |
| Controller boxes | `ILI9488` and `ILI9486` printed with identical boxes — no selection mark | VERIFIED |
| Silkscreen | `3.5" TFT LCD Shield`, PCB date `2026-01-27`, designators J1–J4, U1–U3, R1 | VERIFIED |
| Touch overlay | 4-contact flex at the top edge; glass marking `H1454 7-3` | VERIFIED |
| Front surface | Scratches in the upper right area; unknown whether on a protective film or on the touch glass | UNKNOWN |

### 2.3 Power

- **VERIFIED:** 5 V into the `5V` pin plus GND powers the whole board. The onboard LDO (U1) supplies
  3.3 V to the panel logic, the buffers and (probably) the backlight.
- **`3V3` pin — UNKNOWN.** It may be tied to U1's output, to something else, or to nothing. Do not use it
  as the only supply. Never feed `5V` and `3V3` from independent sources at the same time. To find out,
  check continuity between `3V3` and U1's large tab (for an AMS1117 the tab is VOUT).
- **Current draw — UNKNOWN**, the backlight dominates. If the host's 5 V rail is weak (USB-powered dev
  board, small regulator), measure it once with an ammeter.
- Before the first power-up, check for shorts between `5V`, `3V3` and GND.

### 2.4 Logic levels and bus buffers

- Host → board lines (`LCD_*`, `SD_CS`, `SD_DI`, `SD_SCK`) are expected to enter the U2/U3 buffers
  (LIKELY). The buffers drive the 3.3 V panel, so they run from 3.3 V, and 3.3 V host logic meets their
  input thresholds. **VERIFIED in practice:** a 3.3 V ESP32-S3 drove all 13 LCD lines at 20 MHz.
- Board → host lines: `SD_DO` (3.3 V from the card) and, only if the data buffer is bidirectional,
  `LCD_D0…D7` during reads (3.3 V). Touch electrodes are passive. → Safe for 3.3 V hosts (LIKELY).
- **Buffer direction — UNKNOWN.** Many shields of this family hard-wire the buffers host → panel. That
  makes the display **write-only**: no ID read, no pixel readback. Ways to check:
  1. Continuity, board unpowered: for a 74xx245-type buffer, pin 1 is DIR. If `LCD_RD` reaches pin 1 of
     the data-bus buffer, reads are possible. If that pin is tied to a supply rail, the bus is write-only.
  2. Software probe (§4.2).

### 2.5 Backlight (LIKELY)

No labeled pin controls the backlight, so it is lit whenever the board is powered.

- Brightness cannot be changed in software.
- `DISPOFF (0x28)` and `SLPIN (0x10)` blank the image, but the backlight stays on.
- To save power, switch the 5 V supply off or modify the hardware.

---

## 3. Display bus (Intel 8080 / i80, 8-bit)

### 3.1 Signals and idle levels

| Signal | Active | Idle | Notes |
|---|---|---|---|
| `LCD_CS` | low | high | Frames transactions. Tying it permanently low is common practice when the bus is dedicated (LIKELY safe; not tested here). |
| `LCD_RS` | — | any | 0 = command byte, 1 = parameter or pixel data. |
| `LCD_WR` | low pulse | high | The panel latches `D0…D7` on the **rising** edge. |
| `LCD_RD` | low pulse | **high** | Must never float. Hold it high unless reading. VERIFIED approach: held high by a GPIO. |
| `LCD_RST` | low | high | Hardware reset. |
| `LCD_D0…D7` | — | — | The host drives them for writes. The panel drives them only while `RD` is low. |

### 3.2 Write transactions

```text
command:    CS=0  RS=0  D=cmd   WR=0 -> WR=1
parameter:  RS=1  D=byte        WR=0 -> WR=1        (repeat per byte, MSB-first for 16-bit values)
pixels:     command 0x2C (RAMWR), then stream pixel bytes with RS=1
            0x3C continues the previous memory write; any other command ends the stream
end:        CS=1 (optional between transactions)
```

### 3.3 Read transactions (only if the board is readable)

- Switch the host's D0–D7 to input, keep `RS=1` and `WR=1`, pulse `RD` low, sample D0–D7, raise `RD`.
- The first byte after a read command is a **dummy** byte.
- Never drive D0–D7 from the host while `RD` is low.
- On a write-only board, pulling `RD` low makes the panel drive its data pins against the buffer outputs.
  MCUFRIEND_kbv's `readID()` does this on every shield, so a one-off probe is tolerated in practice. Do not
  read repeatedly.

### 3.4 Timing and clock

- **VERIFIED:** 20 MHz write clock (50 ns per byte), stable over jumper wires, ILI9488 init, sustained
  full-screen video.
- On new wiring start at ≤ 10 MHz. Verify with full-screen fills and gradients, then raise the clock.
  Random speckles that get worse with frequency → lower the clock, shorten wires, add GND.
- Datasheet AC limits for writes (LIKELY: datasheet statements, not measured here):

  | Parameter | ILI9488 (V1.00, §17.4.1) | ILI9486 (V0.01, §17.3.1) |
  |---|---|---|
  | Write cycle `twc`, min | 40 ns (25 MHz) | 66 ns (≈ 15 MHz) |
  | `WR` low / high, min | 15 ns / 15 ns | 15 ns / 15 ns |
  | Data setup / hold, min | 10 ns / 10 ns | 10 ns / 10 ns |
  | Frame-memory read cycle, min | 450 ns | 450 ns |

  The verified 20 MHz (50 ns) is inside the ILI9488 limit but **above the ILI9486 limit**. On a board
  that turns out to carry an ILI9486, start at ≤ 15 MHz: many panels tolerate more, but that is outside
  the datasheet. Reads are an order of magnitude slower than writes.
- Reset timing used in the verified setup: `RST` high → 50 ms → low → 50 ms → high → 150 ms, then
  `SWRESET (0x01)` + 120 ms. After `SLPOUT (0x11)` wait 120 ms, after `DISPON (0x29)` 25 ms.

### 3.5 Pin budget

- Full: 13 host pins (`D0…D7`, `WR`, `RS`, `CS`, `RST`, `RD`), plus 4 SPI pins if the shield's microSD
  is used.
- Possible savings (LIKELY, not tested here): `RD` tied to 3.3 V → 12. `CS` tied low → 11. `RST` driven
  by the host's reset or an RC network, relying on `SWRESET` → 10. TFT_eSPI's RP2040 parallel setups tie
  CS to 0 V and RD to 3V3.
- Touch needs ADC capability on some of these lines (§6.2).

---

## 4. Controller

### 4.1 What is known

- The PCB accepts ILI9486 or ILI9488. Both are 320RGB × 480 with internal GRAM and an 8-bit 8080
  interface. The controller is chip-on-glass and cannot be seen.
- **This unit** shows a correct, stable image with the ILI9488 table (§4.6 A) and `COLMOD 0x66`
  → most likely an ILI9488 (LIKELY). The ID was never read.
- Caveat: both controllers share the MIPI-DCS core commands. The wrong init table often still shows a
  picture, just with poor gamma or contrast, flicker or wrong voltages. **"A picture appears" is not an
  identification.** Another board of the same model may carry the other controller.

### 4.2 Identification procedure

1. **Readback probe** (one-off, bit-banged, works on any MCU and needs no special clock):

   ```c
   // One-off controller ID probe (RDID4). Keep WR high throughout.
   // CS must stay low from the command through the last read.
   set_pin(LCD_CS, 0);
   lcd_write_cmd(0xD3);
   data_bus_as_input();
   set_pin(LCD_RS, 1);
   uint8_t id[4];
   for (int i = 0; i < 4; i++) {
       set_pin(LCD_RD, 0);
       delay_ns(500);                 // generous read strobe
       id[i] = read_data_bus();
       set_pin(LCD_RD, 1);
       delay_ns(500);
   }
   data_bus_as_output();
   set_pin(LCD_CS, 1);
   // Readable board: id[0] = dummy, id[1] = 0x00, id[2] = 0x94, id[3] = 0x86 (ILI9486) or 0x88 (ILI9488)
   ```

   | Result | Meaning |
   |---|---|
   | `00 94 86` / `00 94 88` in bytes 1–3 | Readable board; controller identified. |
   | Every byte `0xD3` (MCUFRIEND reports ID `0xD3D3`) | Write-only board: the host read back its own command byte. MCUFRIEND's `graphictest_kbv` marks this case `// write-only shield`. |
   | All `0x00` or all `0xFF` | Write-only board, or an `RD`/wiring fault. |

   The expected bytes follow the Ilitek RDID4 convention and MCUFRIEND_kbv's detection (LIKELY). Record
   the raw bytes in §12.
2. **If write-only, identify empirically.** Initialize with table A, draw the test pattern (solid
   fills, gray and R/G/B gradients, 1-px grid). Repeat with table B. Keep the table that gives correct
   colors, smooth gradients, good contrast and no flicker. Record the choice as LIKELY, not VERIFIED.
3. With an Uno at hand, MCUFRIEND_kbv is a convenient probe jig: `LCD_ID_readreg` dumps the registers,
   `graphictest_kbv` prints the ID.

### 4.3 Command cheat sheet (common to both controllers)

| Cmd | Name | Parameters | Notes |
|---|---|---|---|
| `0x00` | NOP | — | Harmless; some drivers use it as a flush/barrier. |
| `0x01` | SWRESET | — | Wait 120 ms. |
| `0x10` / `0x11` | SLPIN / SLPOUT | — | Wait 120 ms after SLPOUT. |
| `0x20` / `0x21` | INVOFF / INVON | — | Default (off) is correct on this unit — VERIFIED. |
| `0x28` / `0x29` | DISPOFF / DISPON | — | Backlight is not affected. |
| `0x2A` | CASET | `x0_hi x0_lo x1_hi x1_lo` | Column range = logical X after MADCTL. |
| `0x2B` | PASET | `y0_hi y0_lo y1_hi y1_lo` | Row range = logical Y. |
| `0x2C` | RAMWR | pixel stream | Starts at the window origin. |
| `0x3C` | RAMWRC | pixel stream | Continues the previous write. |
| `0x36` | MADCTL | 1 byte | Orientation + BGR (§4.5). |
| `0x3A` | COLMOD | 1 byte | Pixel format (§4.4). |
| `0xD3` | RDID4 | read 4 bytes | dummy, `0x00`, `0x94`, `0x86`/`0x88`. |

Power, VCOM and gamma registers (`0xB0–0xB7`, `0xC0–0xC5`, `0xE0/0xE1`, `0xF7`) are controller-specific.
Use the matching table from §4.6. Never mix values between controllers.

### 4.4 Pixel formats (COLMOD `0x3A`)

| COLMOD | Format | Bytes/pixel | Byte order on D0–D7 | Status |
|---|---|---|---|---|
| `0x66` | RGB666, 18 bpp | 3 | R, G, B; each color in bits 7..2, bits 1..0 ignored | VERIFIED (table A) |
| `0x55` | RGB565, 16 bpp | 2 | high byte first: `RRRRRGGG`, then `GGGBBBBB` | LIKELY |

- "ILI9488 supports only 18-bit color" is a limitation of its **serial (SPI)** interface, not of this
  parallel board. The ILI9486 datasheet lists only 3-bit and 18-bit colour for SPI. The ILI9488 datasheet
  also lists RGB565 for 4-line SPI, but in practice both controllers are driven with RGB666 over SPI
  (Linux DRM driver notes, TFT_eSPI, LovyanGFX, Arduino_GFX). For the 8-bit parallel bus both datasheets
  list 65K (2 bytes per pixel) and 262K (3 bytes per pixel). TFT_eSPI's `ILI9488_Init.h` uses
  `0x55 // 16-bit colour for parallel` and `0x66 // 18-bit colour for SPI`; `ILI9486_Init.h` does the same.
- `0x55` cuts bus traffic by a third (307,200 vs 460,800 B per frame). Test it on hardware before relying
  on it, then update this table.
- With the BGR bit set in MADCTL, send colors in plain R, G, B order as shown — VERIFIED. Fix red/blue
  swaps with the BGR bit, never in application code.
- RGB565 → RGB666 on the host:

  ```c
  uint8_t r = (uint8_t)((c >> 8) & 0xF8);   // R5 -> bits 7..3
  uint8_t g = (uint8_t)((c >> 3) & 0xFC);   // G6 -> bits 7..2
  uint8_t b = (uint8_t)((c << 3) & 0xF8);   // B5 -> bits 7..3
  ```

- Mismatch symptom: if the bytes sent per pixel do not match COLMOD, solid fills show fine stripes of
  wrong colors and cover only part of the window (or wrap past it).

### 4.5 MADCTL (`0x36`) and rotation

Bits: `MY 0x80` (row order), `MX 0x40` (column order), `MV 0x20` (row/column exchange), `ML 0x10`,
`BGR 0x08`, `MH 0x04`.

| Rotation | MADCTL | Logical size | Status |
|---|---|---|---|
| 0 — portrait | `0x48` (MX, BGR) | 320 × 480 | LIKELY (standard; same as TFT_eSPI) |
| 1 — landscape | `0x28` (MV, BGR) | 480 × 320 | **VERIFIED**: correct, not mirrored, colors correct |
| 2 — portrait, 180° | `0x88` (MY, BGR) | 320 × 480 | LIKELY |
| 3 — landscape, 180° | `0xE8` (MY, MX, MV, BGR) | 480 × 320 | LIKELY |

- Which physical edge is "up" in rotation 1 was not recorded. Note it when you test.
- After changing MADCTL, use the new logical width and height for CASET/PASET.
- Fix mirroring with MX/MY and swapped axes with MV — not by transforming coordinates in the app.

### 4.6 Initialization sequences

Notation: `CMD: parameters ; comment`. Delays are after the command.

**A) ILI9488 — VERIFIED on this unit.** Register values are identical to TFT_eSPI 2.5.43
`TFT_Drivers/ILI9488_Init.h`. The verified run used `COLMOD 0x66`.

```text
RST=1, 50 ms ; RST=0, 50 ms ; RST=1, 150 ms          ; hardware reset
0x01                                                  ; SWRESET, wait 120 ms
0x28                                                  ; DISPOFF
0xE0: 00 03 09 08 16 0A 3F 78 4C 09 0A 08 16 1A 0F    ; positive gamma
0xE1: 00 16 19 03 0F 05 32 45 46 04 0E 0D 35 37 0F    ; negative gamma
0xC0: 17 15                                           ; power control 1
0xC1: 41                                              ; power control 2
0xC5: 00 12 80                                        ; VCOM control
0x36: 48                                              ; MADCTL portrait + BGR
0x3A: 66                                              ; COLMOD 18-bit (verified); 55 = 16-bit (to test)
0xB0: 00                                              ; interface mode control
0xB1: A0                                              ; frame rate control
0xB4: 02                                              ; display inversion control
0xB6: 02 02 3B                                        ; display function control
0xB7: C6                                              ; entry mode set
0xF7: A9 51 2C 82                                     ; adjust control 3
0x11                                                  ; SLPOUT, wait 120 ms
0x29                                                  ; DISPON, wait 25 ms
0x36: 28                                              ; rotation 1: landscape 480x320 (verified)
```

**B) ILI9486 — candidate, NOT verified on this board.** From TFT_eSPI 2.5.43
`TFT_Drivers/ILI9486_Init.h`, parallel branch (derived from Linux fbtft). MCUFRIEND_kbv's `0x9486` table
in `MCUFRIEND_kbv.cpp` is an alternative written for Uno shields.

```text
0x01                                                  ; SWRESET, wait 120 ms
0x11                                                  ; SLPOUT, wait 120 ms
0x3A: 55                                              ; COLMOD 16-bit
0xC0: 0E 0E                                           ; power control 1
0xC1: 41 00                                           ; power control 2
0xC2: 55                                              ; power control 3
0xC5: 00 00 00 00                                     ; VCOM control
0xE0: 0F 1F 1C 0C 0F 08 48 98 37 0A 13 04 11 0D 00    ; positive gamma
0xE1: 0F 32 2E 0B 0D 05 47 75 37 06 10 03 24 20 00    ; negative gamma
0x20                                                  ; INVOFF
0x36: 48                                              ; MADCTL portrait + BGR
0x29                                                  ; DISPON, wait 150 ms
```

### 4.7 Drawing

```c
static void set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {  // inclusive, logical coords
    uint8_t ca[4] = { (uint8_t)(x0 >> 8), (uint8_t)x0, (uint8_t)(x1 >> 8), (uint8_t)x1 };
    uint8_t pa[4] = { (uint8_t)(y0 >> 8), (uint8_t)y0, (uint8_t)(y1 >> 8), (uint8_t)y1 };
    lcd_write_cmd(0x2A); lcd_write_data(ca, 4);    // CASET
    lcd_write_cmd(0x2B); lcd_write_data(pa, 4);    // PASET
}

static void fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *px, size_t px_bytes) {
    set_window(x, y, x + w - 1, y + h - 1);
    lcd_write_cmd(0x2C);                           // RAMWR
    for (uint32_t i = 0; i < (uint32_t)w * h; i++) {
        lcd_write_data(px, px_bytes);              // 3 bytes (0x66) or 2 bytes (0x55); batch in real code
    }
}
```

Stream as many pixels as possible inside one window. A window per pixel costs about 11 extra bytes per
pixel and is several times slower.

---

## 5. Host integration (any MCU)

### 5.1 Pin planning checklist

- 13 outputs for the display (§3.5), plus 4 SPI pins if the shield's microSD is used.
- Planning touch? Put `LCD_WR`, `LCD_RS` and `LCD_CS` on **ADC-capable** pins (§6.2). Prefer ADCs that
  work alongside your radio (for example ESP32 ADC1, not ADC2, when Wi-Fi is on).
- Avoid boot-strapping pins, flash/PSRAM pins, USB and console UART pins, and input-only pins.
- Using a hardware parallel peripheral? Check its pin constraints first (§5.3).
- 3.3 V GPIO is fine (VERIFIED). 5 V GPIO is the design target (LIKELY).
- Record the final signal → GPIO map in the project.

### 5.2 Driver approaches and throughput (480 × 320 frame)

Bus time per frame = bytes per frame / write clock.

| Approach | Result | Status |
|---|---|---|
| `digitalWrite()` per bit | Hundreds of ms to seconds per frame | LIKELY (estimate) — avoid |
| Port / masked-register writes (bit-bang) | ~110 ms per RGB666 frame on ESP32-S3 (≈ 4 MB/s) | Reported in the verified project's notes |
| Hardware 8080 peripheral + DMA at 20 MHz | 23.0 ms per frame RGB666, 15.4 ms RGB565 | Bus-limited arithmetic; 20 MHz setup VERIFIED stable |

### 5.3 Platform options

| Host | Recommended path | Constraints and notes |
|---|---|---|
| Arduino Uno (plug-in) | MCUFRIEND_kbv + Adafruit_GFX (+ Adafruit TouchScreen) | Native mapping. 2 KB SRAM → no buffers. I2C unavailable (A4 = `LCD_RST`). D0/D1 serial stays free. |
| Arduino Mega (plug-in) | MCUFRIEND_kbv | Works through a slower pin mapping; still an 8-bit bus. |
| ESP32-S3 | ESP-IDF `esp_lcd` i80 (LCD_CAM + DMA) — **VERIFIED at 20 MHz**. Also LovyanGFX, Arduino_GFX, TFT_eSPI. | LCD_CAM routes any GPIO through the GPIO matrix; data pins need not be contiguous. `esp_lcd` i80 has **no read API** → hold RD high. TFT_eSPI parallel writes GPIO registers directly (no LCD_CAM, no DMA); D0–D7, WR, DC on GPIO 0–31. |
| ESP32 / ESP32-S2 | `esp_lcd` i80 (I2S LCD mode), TFT_eSPI, LovyanGFX | TFT_eSPI: D0–D7, WR, DC on GPIO 0–31. On ESP32, GPIO34–39 are input-only: Uno-format boards such as the Wemos D1 R32 route A2–A4 (`LCD_RS`, `LCD_CS`, `LCD_RST`) to them, so rewire those three. |
| RP2040 | TFT_eSPI (PIO), Arduino_GFX | TFT_eSPI PIO: D0–D7 on **sequentially increasing** GPIOs. |
| STM32 | FSMC/FMC 8080 mode if the pins allow; otherwise port writes | TFT_eSPI: D0–D7 on bits 0–7 of one port is ~8× faster. FSMC pins are fixed per package. |
| Anything else | Own HAL (§5.4): bit-bang first, then DMA or peripheral | — |

Library support for ILI9486/ILI9488 on 8-bit parallel buses exists in LovyanGFX (`Bus_Parallel8`: I2S on
ESP32, LCD_CAM on ESP32-S3, with an RD pin for reads) and Arduino_GFX (`Arduino_ESP32LCD8` on the S3's
LCD_CAM, software `Arduino_ESP32PAR8*`, `Arduino_UNOPAR8` for Uno shields; drivers `Arduino_ILI9486`,
`Arduino_ILI9488`). Not tested on this unit; check each library's notes on framework versions.

### 5.4 Minimal HAL

```c
void lcd_bus_init(void);                   // configure pins or peripheral; idle: CS=1, WR=1, RD=1
void lcd_hw_reset(void);
void lcd_write_cmd(uint8_t cmd);
void lcd_write_data(const uint8_t *p, size_t n);
void lcd_write_pixels(const void *p, size_t nbytes);    // may be asynchronous (DMA)
void lcd_wait_idle(void);                  // block until all queued transfers have finished
void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1);
void lcd_set_rotation(uint8_t r);          // MADCTL + logical width/height
bool lcd_read_reg(uint8_t cmd, uint8_t *buf, size_t n); // optional; returns false on a write-only bus
```

Keep the controller init tables (§4.6) separate from the bus layer so the ILI9486/ILI9488 choice is one
switch.

### 5.5 Memory and DMA

- A full framebuffer is 307,200 B (RGB565) or 460,800 B (RGB666). It is only viable with external PSRAM,
  and only if your DMA can read from PSRAM. Otherwise render directly, in tiles or per scanline.
- Verified pattern: two ping-pong DMA buffers of 32 lines (320 × 32 × 3 = 30,720 B each) in DMA-capable
  internal RAM, streamed chunk by chunk inside one address window.
- **Never modify a buffer while its DMA transfer is pending.** With N buffers, wait for buffer k's
  transfer to finish before refilling it.
- ESP-IDF `esp_lcd` specifics:
  - `esp_lcd_panel_io_tx_color()` is asynchronous: it queues the transfer and returns.
  - `esp_lcd_panel_io_tx_param()` first waits for all in-flight transfers, so it works as a barrier
    (for example, sending NOP `0x00`).
  - The i80 panel IO has no read function.
- Stream images from storage in rows or blocks and convert the pixel format on the fly into the DMA
  buffer.

---

## 6. Resistive touch

### 6.1 Facts

- 4-wire resistive overlay with a 4-contact flex at the top edge — VERIFIED (photo).
- No touch-controller IC (no XPT2046) — VERIFIED (photo). The host measures the plates with its own ADC.
  Do not add an XPT2046 driver.
- No dedicated touch pins exist on the headers → the electrodes share LCD bus lines on the header (host)
  side of the buffers (LIKELY; standard for this shield family).
- Touch wiring and calibration on this unit: UNKNOWN — never tested.

### 6.2 Which lines carry the electrodes

- `YP` and `XM` must be read by an ADC. In known variants they sit on the analog-side lines
  `LCD_WR` (A1), `LCD_RS` (A2) or `LCD_CS` (A3).
- `XP` and `YM` are digital. They sit on data lines among `LCD_D5`…`LCD_D7` (D5–D7) and
  `LCD_D0`/`LCD_D1` (D8/D9).
- MCUFRIEND's pin diagnosis scans A0–A3 against D5–D9. `LCD_RD` (A0) is therefore also a candidate.

Variants listed in MCUFRIEND_kbv `TouchScreen_Calibr_native`:

| Variant | Uno pins | Shield signals |
|---|---|---|
| "most common" | `XP=6, YP=A1, XM=A2, YM=7` | XP=`LCD_D6`, YP=`LCD_WR`, XM=`LCD_RS`, YM=`LCD_D7` |
| "next common" | `XP=7, YP=A2, XM=A1, YM=6` | XP=`LCD_D7`, YP=`LCD_RS`, XM=`LCD_WR`, YM=`LCD_D6` |

Other shields use `LCD_D0`/`LCD_D1` with `LCD_RS`/`LCD_CS`. **Always measure.**
Design rule for any host: `LCD_WR`, `LCD_RS`, `LCD_CS` (and ideally `LCD_RD`) on ADC-capable pins.

### 6.3 Identify the wiring with a multimeter (any host)

With the board unpowered and disconnected:

1. Measure resistance between each of {`LCD_RD`, `LCD_WR`, `LCD_RS`, `LCD_CS`} and each of
   {`LCD_D5`, `LCD_D6`, `LCD_D7`, `LCD_D0`, `LCD_D1`}.
2. Two pairs should read a few hundred ohms: these are the X and Y plates (MCUFRIEND quotes ~300 Ω for
   X and ~500 Ω for Y). The analog-side end of each pair is `XM` or `YP`.
3. Pressing the screen connects the plates, so the reading *between* the two pairs changes from open to a
   finite value.
4. Which plate is X and which is Y, and the axis directions, are settled during calibration.

On an Uno, MCUFRIEND's `TouchScreen_Calibr_native` diagnoses the pins and prints calibration values
automatically.

### 6.4 Reading sequence

1. Wait until the LCD bus is idle: no queued DMA, no transaction in progress.
2. The electrode lines toggle during the measurement. Keep the LCD control line that is **not** an
   electrode inactive so the panel ignores it. Usually that is `CS` held high; if `CS` is an electrode,
   hold `WR` high.
3. Measure (standard 4-wire method, as in Adafruit TouchScreen):
   - **X:** `XP` = 1, `XM` = 0, `YM` = high-Z, read ADC on `YP`.
   - **Y:** `YP` = 1, `YM` = 0, `XP` = high-Z, read ADC on `XM`.
   - **Pressure:** `XP` = 0, `YM` = 1, read ADC on `XM` and `YP`. Reject weak or excessive presses.
4. Take several samples and use the median.
5. **Restore** all four lines to outputs at LCD idle levels (`CS`=1, `WR`=1) before the next LCD
   transaction. MCUFRIEND's example restores `YP` and `XM` with `pinMode(..., OUTPUT)` right after
   `getPoint()`. Skipping this causes random pixels and a frozen display.
6. Map raw values to screen coordinates with the calibration for the current rotation, then clamp to the
   screen size. Calibration constants depend on the ADC resolution (10-bit AVR vs 12-bit ESP32, etc.).

### 6.5 With a hardware bus peripheral

When LCD_CAM (ESP32-S3), I2S (ESP32), FSMC (STM32) or PIO (RP2040) owns the pins:

- Wait for all queued transfers to finish.
- Detach the touch lines from the peripheral (GPIO matrix or alternate function → GPIO/analog), measure,
  then re-attach them.
- ESP-IDF `esp_lcd` has no public API to release pins temporarily. Either delete and re-create the i80
  bus, or re-route the pins with low-level GPIO-matrix calls. Decide this before fixing the pin map.
- Never measure touch while a frame is being sent.

### 6.6 Calibration

- Calibrate per unit and per rotation.
- Store the values separately from UI code, tagged with the unit or revision.
- Re-run calibration if the overlay is replaced or the rotation convention changes.
- Clamp mapped coordinates to the active screen size.

---

## 7. microSD

### 7.1 Facts

- Socket on the shield, SPI: `SD_CS` (D10), `SD_DI` = MOSI (D11), `SD_DO` = MISO (D12),
  `SD_SCK` (D13) — labels VERIFIED.
- Function never tested on this unit — UNKNOWN.
- Whether `SD_CS`/`SD_DI`/`SD_SCK` pass through the buffers — UNKNOWN. The card is 3.3 V, so a 3.3 V host
  needs no level shifting either way.
- If unused, leave `SD_*` unconnected (VERIFIED harmless in the 2026-09 setup), or drive `SD_CS` high.
- The LCD bus is electrically independent of SPI. On a non-Uno host the SD can go to any SPI-capable pins.

### 7.2 Bring-up checklist

- Use a known-good, modest-capacity card formatted FAT16/FAT32.
- Set `SD_CS` as an output and drive it high before initializing any other SPI device on the same bus.
- Initialize at a conservative SPI clock (≤ 400 kHz for card init). Raise it only after repeated read tests.
- Test create/write/flush/reopen/read, not just a directory listing.
- Never cut power or remove the card while a file is open for writing.
- Stream images in rows or blocks. For BMP files, handle row padding and bottom-up storage.

---

## 8. Bring-up procedure (any host)

| # | Step | Pass condition |
|---|---|---|
| 1 | Inspect: no shorts between `5V`, `3V3` and GND; header pins straight | No shorts |
| 2 | Wire GND, `5V`, then the 13 LCD lines **by label**. `RD` high from the start. SD and touch later. | — |
| 3 | At ≤ 10 MHz: hardware reset, init table A, `MADCTL 0x28`, full-screen fills black/white/red/green/blue | Correct solid colors over the whole screen, no speckles |
| 4 | 1-px border, corner markers and text in rotations 0–3 | Markers in the corners, no offset, wrap or mirroring; note the physical "up" edge |
| 5 | Gray and R/G/B gradients | Smooth, good contrast, no flicker. If poor, try table B (§4.2). |
| 6 | Raise the write clock step by step to the target (20 MHz verified); repeat steps 3–5. Optionally test `COLMOD 0x55`. | Same results at the target clock |
| 7 | Optional one-off readback probe (§4.2) | Raw bytes recorded |
| 8 | Optional touch: identify lines (§6.3), calibrate | Stable, accurate points in all corners |
| 9 | Optional microSD (§7.2) | Write/read integrity |
| 10 | Soak ≥ 30 min: fills, animation, touch and SD if used | No resets or artifacts; U1 not overheating |
| 11 | Update the status tags and §12 | — |

---

## 9. Verification matrix (this unit, 2026-09)

| Test | Pass condition | Status |
|---|---|---|
| Solid fills | Correct colors over the whole screen | VERIFIED (RGB666) |
| Landscape, rotation 1 | Correct, not mirrored | VERIFIED |
| Rotations 0, 2, 3 | Correct orientation | not tested |
| Color order | Red and blue not swapped (BGR bit) | VERIFIED |
| Inversion | Not negative without INVON | VERIFIED |
| Sustained streaming at 20 MHz | Stable full-screen video, no artifacts | VERIFIED |
| RGB565 (`0x55`) | Correct colors with 2 B/pixel | not tested |
| Readback / ID | ID bytes readable | not tested |
| Sleep/wake | Recovers after SLPIN/SLPOUT and reset | not tested |
| Touch | Calibrated, stable points | not tested |
| microSD | Write/read integrity | not tested |

---

## 10. Failure guide

| Symptom | Likely cause / check |
|---|---|
| White screen, backlight on | No or wrong init; wrong `CS`/`WR`/`RS`/`RST` wiring; `RD` floating or low; reset not released |
| Completely dark, no backlight | No 5 V on `5V`, or GND missing |
| Solid fills show fine stripes of wrong colors and cover only part of the screen | Bytes per pixel do not match COLMOD (2 vs 3) |
| Correct shapes, red and blue swapped | BGR bit in MADCTL |
| Negative image | INVON/INVOFF (`0x21`/`0x20`) |
| Mirrored, rotated or offset image | MADCTL MX/MY/MV vs the logical size used in CASET/PASET |
| Picture works but washed out, dark, flickering or banded | Wrong controller init table — try the other one (§4.2) |
| Random speckles that grow with clock rate | Write clock too high for the wiring; shorten wires, improve GND |
| Occasional corrupted blocks with DMA | A buffer was refilled before its DMA transfer finished |
| ID reads `0xD3D3` (every byte `0xD3`) | Write-only board — identify empirically |
| ID reads `0x0000` or `0xFFFF` | Write-only board, `RD` wiring or bus-direction fault |
| Random pixels during or after a touch read | Shared pins not restored; `CS`/`WR` not held inactive during the measurement |
| Touch axes swapped or reversed | Electrode assignment or calibration mapping |
| SD init fails | Wrong CS, SPI clock too high, card or format, another CS line floating |
| Uno crashes on images or fonts | SRAM exhausted — stream data, keep constants in flash |

---

## 11. Open questions (measure and record in §12)

- Controller silicon ID, and whether the board is readable at all (§4.2).
- `COLMOD 0x55` (RGB565) on this unit.
- Orientation of rotations 0, 2, 3; the physical "up" edge per rotation.
- Maximum stable write clock above 20 MHz, and the wire lengths used.
- Where the `3V3` header pin connects.
- Buffer part numbers and direction (sharp macro photo of U2/U3).
- 5 V current draw; backlight circuit.
- Touch electrode lines, plate resistances, calibration.
- microSD function; whether its lines are buffered.
- Whether the front scratches are on a removable film or on the touch glass.

---

## 12. Evidence log

### 2026-09 — ESP32-S3 host, write-only 8080 through a hardware peripheral (VERIFIED, owner-confirmed)

- **Host:** ESP32-S3 (ESP32-S3-CAM N16R8 board), Arduino framework on PlatformIO, ESP-IDF `esp_lcd` i80
  bus (LCD_CAM + DMA).
- **Power:** shield from 5 V into `5V`, common GND; host logic 3.3 V.
- **Bus:** 8-bit, 20 MHz write clock, CS driven by the peripheral (active low), D/C levels
  command = 0 / data = 1, WR idle high. `RD` held high by a GPIO — no reads. `RST` by a GPIO.
- **Controller setup:** init table A (ILI9488), `COLMOD 0x66`, rotation 1 (`MADCTL 0x28`, 480 × 320).
- **Data path:** two 30,720 B ping-pong DMA buffers (32 lines RGB666) in internal RAM.
- **Result:** stable, correct full-screen 480 × 320 MJPEG video at 25 fps from the host board's own microSD
  (1-bit SDMMC), decoded with JPEGDEC; colors and orientation correct. Frame rate owner-confirmed on
  2026-09-18.
- **Not connected:** shield microSD and touch.
- **Example pin map** (host-specific, do not copy blindly): D0=GPIO17, D1=GPIO16, D2=GPIO12,
  D3=GPIO11, D4=GPIO10, D5=GPIO9, D6=GPIO8, D7=GPIO18, RD=GPIO4, WR=GPIO5, RS=GPIO6, CS=GPIO7, RST=GPIO15.
  On that board these are camera GPIOs, so the camera was not used during the test.

### Change log

- **2026-09-18, rev 2.1.** Published with the article at <https://build-mode-ak.com/3-5-inch-tft-lcd-shield/>.
  - Added: datasheet write-timing limits (§3.4); SPI colour-format note (§4.4); library classes (§5.3);
    the 25 fps result (§12).
  - Changed: the bus is named Intel 8080 / i80 (DBI Type B), as in the datasheets.
  - Removed: a file path on the owner's machine.
- **2026-09-18, rev 2.** Rewritten as a platform-independent guide.
  - Added: the hardware-verified configuration; full header pinout; register, pixel-format, MADCTL and
    init tables; DMA rules; touch identification for any host; backlight and power specifics.
  - Changed: reading the controller ID is no longer mandatory, since the board may be write-only.
- **2026-09-15, rev 1.** Initial identification from photos; Arduino/MCUFRIEND-oriented.

---

## 13. References

1. MCUFRIEND_kbv — <https://github.com/prenticedavid/MCUFRIEND_kbv>. Examples: `LCD_ID_readreg`;
   `graphictest_kbv` (`if (ID == 0xD3D3) ID = 0x9481; // write-only shield`); `TouchScreen_Calibr_native`
   (touch pin variants, pin diagnosis, restoring shared pins).
2. TFT_eSPI 2.5.43 — <https://github.com/Bodmer/TFT_eSPI>.
   - `TFT_Drivers/`: `ILI9488_Init.h`, `ILI9486_Init.h`, `ILI9488_Rotation.h`.
   - `User_Setups/`: `Setup70d_ILI9488_S3_Parallel.h`, `Setup16_ILI9488_Parallel.h`,
     `Setup100_RP2040_ILI9488_parallel.h`, `Setup35_ILI9341_STM32_Port_Bus.h`.
3. ESP-IDF I80 LCD — <https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/lcd/i80_lcd.html>;
   source `components/esp_lcd/src/esp_lcd_panel_io_i80.c`.
4. LovyanGFX — <https://github.com/lovyan03/LovyanGFX>
5. Arduino_GFX — <https://github.com/moononournation/Arduino_GFX>
6. Adafruit TouchScreen (4-wire resistive reading) — <https://github.com/adafruit/Adafruit_TouchScreen>
7. Adafruit GFX guide — <https://learn.adafruit.com/adafruit-gfx-graphics-library/overview>
8. ILI9488 datasheet V1.00 (ILI9488_IDT_V100_20121128) —
   <https://www.elecrow.com/download/ILI9488%20Data%20Sheet_100.pdf>; also listed at
   <https://www.crystalfontz.com/controllers/Ilitek/ILI9488/> and <https://www.hpinfotech.ro/ILI9488.htm>
9. ILI9486 application notes (panel init examples; not the full datasheet) —
   <https://cdn.velleman.eu/downloads/29/infosheets/ili9486_datasheet.pdf>
10. ILI9486 datasheet V0.01, preliminary, 312 pages — <https://www.lcdwiki.com/res/MAR3501/datasheet_ILI9486.pdf>
11. Linux DRM note that the ILI9488 accepts only RGB666 over SPI (N. Trønnes, 2024-05-12) —
    <https://lkml.rescloud.iu.edu/2405.1/04247.html>

Datasheet copies are third-party mirrors; a stable public Ilitek URL was not found. No manufacturer,
SKU, schematic or BOM of the shield itself is known.
