-
Notifications
You must be signed in to change notification settings - Fork 11
Added madctl parameter to ILI9341 driver for hardware rotation and RGB/BGR control, enabling faster rendering and more flexibility #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add docstring listings for the new args |
||
| 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, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,3 +46,16 @@ py-modules = ["adafruit_ili9341"] | |
| [tool.setuptools.dynamic] | ||
| dependencies = {file = ["requirements.txt"]} | ||
| optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} | ||
|
|
||
| [tool.ruff] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't generally want to make configuration changes to pyproject.toml in single repos like this. We have If there are configuration changes that we should consider then we'll want to discuss doing so more broadly with a patch across all repos instead of having it changed one-off in PRs like this. Please revert the changes in pyproject.toml for now. |
||
| # 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 | ||
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FourWire bus argument should not get removed from the docstrings I think