feat: add GT911 touch controller support (config packet 0x28)#5
Merged
Conversation
Port the GT911 touch subsystem from the Arduino reference firmware (OpenDisplay-Firmware src/touch_input.cpp) to the nRF54 port. - Parse config packet 0x28 (touch_controller, x4) into a 32-byte TouchController struct matching the reference and the py-opendisplay serializer on the wire. This also stops 0x28 from falling through to the default skip-to-CRC branch, which silently drops all later packets. - New opendisplay_touch.c/.h GT911 driver: address probing (0x5D/0x14), hardware reset/address selection via INT/RST, product-ID probe, point reading, lightweight probe-before-full-reset on EPD-refresh resume, and byte-identical MSD dynamic-byte encoding. - Software (bit-banged) open-drain I2C master over the existing nrf54_gpio helpers, since bus pins come from runtime config bytes. - Wire-up: init/process next to the button module, reserve the touch INT pin from button init, feed MSD via opendisplay_ble_set_dynamic_byte, and re-probe touch after EPD refresh from the display path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
This was referenced Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Ports the GT911 capacitive-touch subsystem from the Arduino reference firmware (
OpenDisplay-Firmwaresrc/touch_input.cpp) to the nRF54 port. Touch is a verified nRF52840-baseline feature and the nRF54 port had zero touch code, so this closes a genuine parity gap.Adds config packet
0x28(touch_controller, x4) parsing into a new 32-byteTouchControllerstruct, a newopendisplay_touch.c/.hGT911 driver, and wire-up in the BLE main loop, the button subsystem, and the display refresh path.Config packet 0x28
New
struct TouchController(src/opendisplay_structs.h) matches the reference layout exactly (OpenDisplay-Firmware/src/structs.h:215-228) and the Python serializer (OpenDisplay-py-opendisplay/src/opendisplay/protocol/config_serializer.py:447-463). The parse case (src/opendisplay_config_parser.c) follows the existingled/binary_inputspattern (max 4 instances, bounds-checked). Beyond adding the feature, giving0x28a real case stops it from hitting thedefault:skip-to-CRC branch (opendisplay_config_parser.c:506-509), which silently drops every packet ordered after it.On-wire layout (verified:
_Static_assert(sizeof == 32)+ per-field offset asserts pass on host gcc)structs.hopendisplay_structs.hinstance_numberuint8_tuint8_tBtouch_ic_typeuint16_t(LE)uint16_t(LE)Hbus_iduint8_tuint8_tBi2c_addr_7bituint8_tuint8_tBint_pinuint8_tuint8_tBrst_pinuint8_tuint8_tBdisplay_instanceuint8_tuint8_tBflagsuint8_tuint8_tBpoll_interval_msuint8_tuint8_tBtouch_data_start_byteuint8_tuint8_tbyteenable_pinuint8_tuint8_treserved[0]= 0)reserved[20]reserved[20]reserved[20]reserved[1:21]Total 32 bytes. The Python serializer packs
touch_data_start_bytethen 21 reserved bytes; its byte 11 isreserved[0]= 0, which equalsenable_pin= 0 (unused). So the packet is byte-identical on the wire;enable_pinis only ever non-zero from a producer that models the field explicitly (as the C reference does).GT911 driver (
opendisplay_touch.c/.h)Ports the reference behavior: I2C address probing (
0x5D/0x14), INT/RST reset sequencing with INT-selected address (high-before-RST-release =>0x14, low =>0x5D), product-ID probe with LE/BE register-order auto-detect, status/point reads, and the lightweight probe-before-full-reset resume from reference PR #75 (touch_input.cpp:392-443). The MSD dynamic-byte encoding is copied exactly (touch_input.cpp:699-717): byte0 low nibble = contacts 1-5 (down) / 6 (released, last XY kept) / 0 (never touched, block cleared), high nibble = last track id, bytes 1-4 = last X (LE) then last Y (LE) - so HA and other clients decode touch identically on both firmwares.I2C transport choice: software (bit-banged) open-drain master
Chosen over nrfx TWIM. Rationale: the touch bus pins arrive as runtime config bytes in the compact
(port<<4)|pinencoding (src/nrf54_gpio.cnrf54_pin_decode), read from thedata_bus(0x24) instance selected byTouchController.bus_id. Devicetree-fixed Zephyr I2C controllers can't bind arbitrary runtime PSEL pins, and the existingnrf54_gpiolayer already gives clean open-drain primitives (input+pull-up to release high, output-low to drive low, plus a read). Bit-banging over those helpers keeps the whole port on one GPIO abstraction with no new Kconfig/devicetree/nrfx surface, and GT911 is happy at the resulting sub-100 kHz clock. The driver models its tick/integration style onopendisplay_led.c/opendisplay_button.c: non-blocking, cooperatively polled from the main loop. INT-driven wakeups are not used (poll-only is a fully supported reference mode); the INT pin is still driven during reset for address selection and reserved from the button subsystem.Robustness
read/writeretry 3x; 5 consecutive failures disable the controller, matching the reference.process()is throttled to >=100 ms and per-controllerpoll_interval_ms; a worst-case pass (1-byte status + 8-byte point read) stays in the low-single-digit-ms range. All blocking reset/settle delays (300 ms pre / 200 ms post) run only at init / full-reset-resume, never in the per-tick path.Wire-up
opendisplay_ble.c:opendisplay_touch_init()next toopendisplay_button_init()(after config load);opendisplay_touch_process()next toopendisplay_button_process()in the main loop. Touch feeds the MSD viaopendisplay_ble_set_dynamic_byte()+opendisplay_ble_update_msd(true)+opendisplay_ble_boost_advertising(), exactly asopendisplay_button.c:103does.opendisplay_button.c: button init now skips any pin that is a configured GT911 INT (opendisplay_touch_gpio_is_touch_int), mirroringOpenDisplay-Firmware/src/device_control.cpp:572.opendisplay_display.cpp:opendisplay_touch_resume_after_refresh()is called after both the partial and full direct-write refresh paths complete.Refresh coordination
The reference wraps EPD refreshes in
touchSuspendForEpdRefresh()/touchResumeAfterEpdRefresh()(main.cpp:78,177,209,221,235). The suspend half is not applicable here: on the nRF54 port the panel refresh is a synchronous blocking call inside the pipe handler, so the main loop (henceopendisplay_touch_process()) is not serviced during it and there is no concurrent touch access to gate. The bit-bang bus is also on independent GPIOs, so there is no EPD-SPI contention. The resume half is applicable (a refresh can perturb a GT911 that shares the panel power rail), so it is preserved: after a refresh the driver does the reference's lightweight product-ID probe, falling back to a full reset + re-init only if that fails.Data-bus gating
The reference obtains its I2C pins from a
data_bus(0x24) instance viaTouchController.bus_id(touch_input.cpp:282-299). This port replicates that exactly: SCL =data_bus.pin_1, SDA =data_bus.pin_2, requiringbus_type == I2Cand both pins set. Unlike the Arduino build there is no implicit sharedWire, sobus_id == 0xFF(reference "I2C already up") or a missing/invaliddata_busdisables that controller with a log rather than guessing.Testing
CI (PlatformIO, both
seeed-xiao-nrf54l15andseeed-xiao-nrf54lm20a) is green. Hardware caveat: implemented from the datasheet and the Arduino reference only - it has not been validated against a real GT911 panel on nRF54 hardware. The reset timing, address selection, and register access mirror the working reference, but a smoke test on a physical touch panel is still needed before relying on it.Stacking
Stacked on PR #1 (
ci: build both nRF54 environments). The branch is based onfeat/ci. Note the siblingfeat/buzzer(PR #3) also adds a config-parser case and a struct, so a textual merge conflict inopendisplay_config_parser.c/opendisplay_structs.his expected and benign.🤖 Generated with Claude Code
https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR