Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ Usage Example
while True:
pass

Color Order Example
-------------------

The driver accepts a ``color_order`` keyword argument (default ``"RGB"``).
Set it to ``"BGR"`` if your panel uses BGR pixel order instead of RGB.

For backward compatibility, a ``bgr`` boolean argument is still supported
but is deprecated; prefer ``color_order="BGR"``.

.. code-block:: python

import board
import displayio
import fourwire
import adafruit_ili9341

spi = board.SPI()
tft_cs = board.D9
tft_dc = board.D10

displayio.release_displays()
display_bus = fourwire.FourWire(spi, command=tft_dc, chip_select=tft_cs)

# Use BGR color order
display = adafruit_ili9341.ILI9341(display_bus, width=320, height=240, color_order="BGR")

Note: Display rotation continues to be handled by displayio. The driver does not
expose controller registers such as MADCTL directly.

Documentation
=============

Expand Down
60 changes: 50 additions & 10 deletions adafruit_ili9341.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ILI9341.git"

_MADCTL = 0x36
_MADCTL_MY = 0x80
_MADCTL_MX = 0x40
_MADCTL_MV = 0x20
_MADCTL_ML = 0x10
_MADCTL_BGR = 0x08
_MADCTL_MH = 0x04

_INIT_SEQUENCE = (
b"\x01\x80\x80" # Software reset then delay 0x80 (128ms)
b"\xef\x03\x03\x80\x02"
Expand Down Expand Up @@ -91,16 +99,48 @@ class ILI9341(BusDisplay):
"""
ILI9341 display driver

:param FourWire bus: bus that the display is connected to
:param str color_order: "RGB" (default) or "BGR"
:param bool bgr: (deprecated) legacy option for color order
:param bool invert: Invert the display
"""

def __init__(self, bus: FourWire, *, bgr: bool = False, invert: bool = False, **kwargs: Any):
init_sequence = _INIT_SEQUENCE
if bgr:
init_sequence += b"\x36\x01\x30" # _MADCTL Default rotation plus BGR encoding
else:
init_sequence += b"\x36\x01\x38" # _MADCTL Default rotation plus RGB encoding
if invert:
init_sequence += b"\x21\x00" # _INVON
# ruff: noqa: PLR0913
def __init__(
self,
bus,
*,
width=240,
height=320,
rotation=0,
color_order="RGB",
bgr=None,
invert=False,
**kwargs,
):
init_sequence = bytearray(_INIT_SEQUENCE)

if bgr is not None:
color_order = "BGR" if bgr else "RGB"

if str(color_order).upper() not in {"RGB", "BGR"}:
raise ValueError("color_order must be 'RGB' or 'BGR'")

madctl = 0x00
if str(color_order).upper() == "BGR":
madctl |= _MADCTL_BGR

init_sequence += bytes((_MADCTL, 0x01, madctl & 0xFF))

super().__init__(bus, init_sequence, **kwargs)
if invert:
init_sequence += b"\x21\x00"

self.init_sequence = bytes(init_sequence)

super().__init__(
bus,
init_sequence,
width=width,
height=height,
rotation=rotation,
**kwargs,
)
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ py-modules = ["adafruit_ili9341"]
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {optional = {file = ["optional_requirements.txt"]}}

[tool.ruff]
# Enable default linting rules
select = ["E", "F"]

# Ignore specific rules if needed
ignore = []

# Allow fixing issues automatically with `ruff check --fix`
fix = true

# Formatting (instead of Black)
line-length = 88
160 changes: 160 additions & 0 deletions tests/test_color_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# SPDX-FileCopyrightText: 2025 Ritesh
# SPDX-License-Identifier: MIT

import pathlib
import sys
import types

# 1) Ensure project root (where adafruit_ili9341.py lives) is importable
ROOT = pathlib.Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

# 2) Stub 'busdisplay' so the driver can import BusDisplay without hardware deps
busdisplay_stub = types.ModuleType("busdisplay")


def _to_bytes(obj):
if obj is None:
return b""
if isinstance(obj, (bytes, bytearray, memoryview)):
return bytes(obj)
if isinstance(obj, (list, tuple)):
try:
return bytes(obj)
except Exception:
return b""
return b""


def _pick_bytes_from_args_kwargs(args, kwargs):
# Prefer explicit kwarg value that looks bytes-like
for v in kwargs.values():
b = _to_bytes(v)
if b:
return b
# Common names: init_sequence, init, sequence
for key in ("init_sequence", "init", "sequence"):
if key in kwargs:
b = _to_bytes(kwargs[key])
if b:
return b
# Otherwise, search positional args after the bus (args[0] usually 'bus')
best = b""
for v in args[1:]:
b = _to_bytes(v)
if len(b) > len(best):
best = b
return best


class BusDisplay:
# Accept any signature; extract a bytes-like init sequence robustly
def __init__(self, *args, **kwargs):
init_seq = _pick_bytes_from_args_kwargs(args, kwargs)
self.init_sequence = init_seq
self._busdisplay_debug = {
"arg_types": [type(a).__name__ for a in args],
"kw_keys": list(kwargs.keys()),
"init_seq_len": len(init_seq),
}


busdisplay_stub.BusDisplay = BusDisplay
sys.modules["busdisplay"] = busdisplay_stub

import adafruit_ili9341 as ili


def _last_madctl(seq: bytes) -> int:
"""
Return the last MADCTL data byte written in the init sequence.

Init sequence encoding per Adafruit style:
[CMD][LEN|0x80 if delay][<LEN data bytes>][<1 delay byte if delay flag set>]
"""
cmd = ili._MADCTL
i = 0
last = None
L = len(seq)
while i < L:
if i >= L:
break
c = seq[i]
i += 1
if i >= L:
break
length_byte = seq[i]
i += 1

delay_flag = (length_byte & 0x80) != 0
n = length_byte & 0x7F # actual data length

# If this is MADCTL, expect exactly 1 data byte
if c == cmd:
assert n == 1, f"Expected MADCTL length 1, got {n}"
assert i + n <= L, "MADCTL payload truncated"
last = seq[i] # the one data byte
# advance over data
i += n
# consume delay byte if present
if delay_flag:
i += 1
assert last is not None, f"No MADCTL write found. seq={seq.hex(' ')}"
return last


def test_color_order_defaults_rgb():
d = ili.ILI9341(bus=object())
assert len(getattr(d, "init_sequence", b"")) > 0, (
f"Driver did not pass an init sequence to BusDisplay. "
f"debug={getattr(d, '_busdisplay_debug', {})}"
)
madctl = _last_madctl(d.init_sequence)
# Default is RGB => BGR bit NOT set
assert (madctl & ili._MADCTL_BGR) == 0


def test_color_order_bgr_sets_bit():
d = ili.ILI9341(bus=object(), color_order="BGR")
assert len(getattr(d, "init_sequence", b"")) > 0, (
f"Driver did not pass an init sequence to BusDisplay. "
f"debug={getattr(d, '_busdisplay_debug', {})}"
)
madctl = _last_madctl(d.init_sequence)
# BGR => BGR bit set
assert (madctl & ili._MADCTL_BGR) == ili._MADCTL_BGR


def test_legacy_bgr_true_sets_bit():
d = ili.ILI9341(bus=object(), bgr=True)
assert len(getattr(d, "init_sequence", b"")) > 0, (
f"Driver did not pass an init sequence to BusDisplay. "
f"debug={getattr(d, '_busdisplay_debug', {})}"
)
madctl = _last_madctl(d.init_sequence)
# legacy bgr=True still sets the bit
assert (madctl & ili._MADCTL_BGR) == ili._MADCTL_BGR


def _has_invon(seq: bytes) -> bool:
# Scan TLV-style sequence; check command 0x21 is present
i, L = 0, len(seq)
while i + 2 <= L:
cmd = seq[i]
i += 1
lb = seq[i]
i += 1
delay = (lb & 0x80) != 0
n = lb & 0x7F
i += n
if delay:
i += 1
if cmd == 0x21:
return True
return False


def test_invert_true_appends_invon():
d = ili.ILI9341(bus=object(), invert=True)
assert _has_invon(d.init_sequence)