Conversation
…width of the space character
Add CoreS3 ILI9342E panel support
Panel_SSD1677_4Gray now dispatches quality/text/fast to a shared absolute refresh path (Mode 2 face-parity recovery, parity-aware full-frame transfer, one Mode 1 activation) whose only per-mode difference is a static LUT, and fastest to a differential monochrome update with a static LUT and software-computed transition groups. - quality: oscillation prefix erases image history with a group-balanced net drive, preventing ghost accumulation over repeated refreshes; the tone-forming tail runs twice so the endpoints stay as stable as the vendor waveform ones - text: white-first waveform, forms the four levels without a black flash - fast: absolute 4-gray waveform derived from the community SDK, with the black darken return shortened to 28 frames (the excess overdrive rebounds toward gray during later undriven holds) - fastest: 2-frame reverse-polarity prepulse plus 8-frame dose with state-aware holds outside the updated region; VCOM matches the panel offset so held pixels sit field-free - absolute refreshes wait on the BUSY line without the 400 ms refresh-minimum floor - the fastest fallback now records its display baseline, so a fastest-only usage no longer degrades into a full refresh on every update
lgfx_qoi.c unconditionally defined QOI_DEBUG, so error paths always called fprintf(stderr). This pulls in newlib stdio and can fail to link on cores that do not provide the _write syscall. The png/jpg decoders reference no stdio; this aligns qoi with them. Define QOI_DEBUG externally to re-enable the debug output.
The loop counter was int while count is uint_fast8_t. On ARM cores uint_fast8_t is unsigned int, so the comparison triggers -Wsign-compare (on xtensa/riscv it is unsigned char, promoted to int, which is why ESP32 builds never warned). Harmless at runtime; use the same type as count.
Rework SSD1677 4-gray refresh into per-mode single-activation waveforms
Use stable absolute Mode 1 waveforms for Quality and Text, absolute Mode 2 for Fast, and differential Mode 2 for Fastest. Preserve ping-pong face parity across mode changes and rebuild display history after sleep or power transitions. Optimize transition-plane generation and keep waveform-tuning helpers out of the public panel API.
Finalize SSD1677 four-mode refresh handling
The GPIO read used for the VAMeter / Cardputer / CardputerADV split was overwritten by the VAMeter I2C probe result, so the subsequent (result & 0x0C) == 0x0C test saw a bool and could never select CardputerADV. This surfaces whenever an I2C Cap or Unit is attached: on CardputerADV G5/G6 are the external pins, so the Cap pull-ups make (result & 3) == 3 true, the VAMeter probe runs, finds nothing and leaves result at 0. M5Unified then picks the plain Cardputer pin table, whose in_i2c entry is 255/255, and every access to a built-in device on the internal bus fails. Keep the probe result in its own variable so the G8/G9 bits survive.
Fix CardputerADV being misdetected as plain Cardputer
Update CoreS3 ILI9342E init parameters
The I2C code was written when every port of a chip was the same peripheral, so the FIFO depth and the clock feeding the SCL divider were written as constants, and the number of ports was taken from SOC_I2C_NUM. None of that holds since the ESP32-C6, which carries a low power I2C alongside the normal one. - SOC_I2C_NUM counts the low power port too, so it can no longer tell how many normal ports there are. That is why the conditionals had a list of chip names growing next to it. Ask SOC_HP_I2C_NUM instead, keeping the list only as the fallback for the ESP-IDF versions that predate the macro, so it stops growing. - read the FIFO depth from SOC_I2C_FIFO_LEN through a per port accessor, rather than assuming 32 in writeBytes() and readBytes(). - move the source clock selection into a per port accessor as well. It has to stay a function rather than a value cached at init, because on the older chips it follows the CPU frequency, which changes at run time. No behaviour change on the chips where the normal port is 32 bytes deep, which is all of them except the ESP32-C2 at 16. The transfers on that chip were being chunked past the end of its FIFO and are now chunked correctly. That one is not verified on hardware. This only prepares the structure; the low power port is not reachable yet.
The low power I2C controller shares its register layout with the normal ports and the port numbering places it right after them, so the existing transaction code can drive it once the port is reachable. What differs is everything around the registers: - Bring-up goes through the ESP-IDF master bus driver, which knows how to route the low power pads (fixed IOMUX pair on C5 / C6, LP GPIO matrix on P4) and their clock source. The port is usable when the driver headers and the low power clock definitions are available (ESP-IDF 5.4 or later). - The pads live in the low power IO domain and keep their routing across a reset, so release() hands them back explicitly and set_pin() clears a leftover routing before taking a pin for a normal port. Without this the pins stay bound to the low power port even into the next program. - set_pin() itself is skipped for a low power port: it would re-route the pins through the normal GPIO matrix on every transaction and disconnect the port from its own pads. - The controller reaches the bus only through its internal open drain mode, so the forced outputs the normal ports use are left off. With them on, the state machine runs to completion without ever driving the lines. - The TwoWire pin propagation in setPins() applies only to the normal ports; handing a low power port's pins to a Wire instance would redirect that port instead. Verified on hardware for all three chips: a scan on the low power port finds the same devices as a normal port on the same physical bus, and register reads return real data.
A negative port selects a software implementation that drives the pins directly: port -1 is slot 0, port -2 is slot 1. It reaches a bus without claiming a peripheral unit, for when every unit is taken or when touching one is undesirable. The convention matches the existing touch driver setting where a negative spi_host already means bit banged GPIO. Negative ports were never valid before (they fell through the upper bound checks into a negative array index), so no existing meaning changes. The implementation lives in soft_i2c.inl, a code fragment that a platform implementation includes inside its i2c namespace, so every function gets internal linkage there: the only public contract is the negative port number itself. The esp32 implementation consumes it here, delegating at the top of each public function. Platform specifics enter through macro hooks: line driving (esp32 toggles the open drain latches its pinMode() provides, the default switches the pin direction), locking (a FreeRTOS mutex here, single threaded by default) and the yield used while a stretched clock is waited out. Clock stretching is honored wherever SCL is released, and the transaction semantics mirror the peripheral path: an unacknowledged address surfaces at the next operation or at endTransaction, not at beginTransaction. Verified on hardware against the same physical bus: the scan results and register reads of both software slots are identical to the normal port and the low power port, and an out of range slot is rejected at init.
A negative host selects a software bus driven on the pins directly, without claiming an SPI peripheral: host -1 is slot 0, host -2 is slot 1. The convention matches the touch driver configs, where a negative host has always meant bit banged GPIO. Like the software I2C, the implementation is an internal fragment (soft_spi.inl) included inside the spi namespace of a platform implementation, and the esp32 implementation consumes it here by delegating at the top of each public function. Only the portable pin functions are used, so the fragment is platform independent. Verified with a loopback on hardware: every SPI mode passes at throttled and unthrottled speeds, both through a single open drain pin and through a jumper between two pins, and an out of range host is rejected at init. The quad SPI init refuses a negative host: the software slots are single bit only.
The autodetect probes now go over the software I2C port, so nothing on the hardware side is claimed or reconfigured until a board is confirmed; a candidate that does not match leaves no trace beyond the pins, which the existing pin backups already restore. Once a board is confirmed, the bus is handed over to the hardware port that its backlight and touch use at run time, preserving the open-port state those consumers rely on. _detect_i2c_device() carried its own bit banged address probe; it now rides on the same software port. This also removes the ACK contention it had (the probe drove the lines push-pull, so a device pulling the acknowledge low fought the driver), and the pull-up plausibility check it performs beforehand stays as it was. The paths where no board matched did not release the hardware port on the C6 and P4 sections; with the probe on the software port there is no hardware claim to leak, and the software slot is returned explicitly. Verified on hardware for the three affected chips (C5 / C6 / P4 boards): detection, display, backlight and touch all work, and the internal I2C scan results are byte for byte identical to builds without this change.
The config carried the port in an unsigned byte, so a software port could not be named there. Store it signed, the way the touch configs already do, and skip the peripheral busy checks for a software port, whose transfers are synchronous. The values in actual use (0 and 1) read the same either way.
Low power I2C support and software (bit-banged) I2C / SPI on negative port numbers
Both functions picked the register block with #if I2C_NUM_MAX > 1, but I2C_NUM_MAX is an enum constant, not a macro: the preprocessor evaluates the condition as 0 > 1, the I2C1 branch never compiled, and every port was polled as I2C0 (verified on ESP-IDF 4.4 and 5.x). On a second hardware port, and on the low power ports now that they are reachable, the functions report an idle bus while a transfer is still on the wire. Resolve the device through the i2c implementation instead, which already knows every port kind, as new i2c::wait() / i2c::busy() entry points; a software port reports not busy there, since its transfers are synchronous. The bus busy bit moves into a getBusBusy() accessor next to the other per-target register accessors, replacing both the duplicated target lists in beginTransaction() and the member detection templates that Bus_I2C.cpp carried for the same purpose.
Poll the port the transfer is on in Bus_I2C::wait() and busy()
The port arguments were bounds checked against I2C_NUM_MAX, which counts every port the ESP-IDF headers know of. On an ESP-IDF recent enough to number the low power port but older than the 5.4 that the low power support here requires, that bound accepts the low power port number while getDev() has no low power branch and folds it onto a high power port: the functions silently drive that peripheral instead, alongside its legitimate user and without its lock. Check against the ports this implementation can actually drive instead (the high power ports, plus the low power ones when compiled in), so an unsupported low power port number fails with invalid_arg. On the configurations where the support is present, and on the ESP-IDF versions that predate the low power port entirely, both bounds are equal and nothing changes.
Refuse a low power I2C port number when the low power support is absent
The consecutive reads exist to reject a frame whose bytes changed mid-read, but while a finger is moving no two reads agree: the retries run out and the function reported no touch at all, so drags and flicks kept breaking up mid-gesture. Adopt the most recent reading instead, the way Touch_FT5x06 already does. A frame whose coordinate payload could not be read is dropped rather than adopted, so a torn read does not turn into a touch at (0,0).
On the GDMA chips (C3 / S3 / C6) the OUT_LINK register carries the descriptor address bits [19:0] together with the START bit, and Bus_SPI ORs the address into its START writes. The AHB_DMA generation dropped the address bits from OUT_LINK and reads them from a separate OUT_LINK_ADDR_CHn register: the OR is silently ignored, the DMA starts with no descriptor, and the first DMA transfer hangs waiting for an OUT FIFO that never fills. The P4 (AXI_DMA) already needed a split address register; reuse that two-register path on the C5 and C61, whose OUT_LINK_ADDR_CHn registers sit 4 bytes apart. The H4 carries an AHB_DMA too but lays these registers out inside each channel block, so it keeps the old path. Verified on an ESP32-C5.
The internal bus (SDA=G2 / SCL=G3) sits exactly on the C5's fixed LP_I2C pads. Move the backlight, the touch panel and the post-detection bus onto the low power port; Port A is a level-shifted branch of the same bus, so this leaves the C5's single high power I2C controller entirely free. On Arduino builds this also takes the internal devices off TwoWire, since the low power port is driven by the ESP-IDF driver directly. The port choice follows the exact condition the common I2C code compiles its low power support under (ESP-IDF 5.4 with the new driver); anything older stays on the high power port. Pairs with the M5Unified change that moves In_I2C to the same port.
The same ILI9342 panel already runs at this rate on the Core2 and Tough; verified on the actual board.
ToughC5 fixes: SPI DMA on AHB_DMA, internal I2C on the low power port, touch retries
…lized A pin routed to the low power I2C keeps that routing across a reset, because it is held in the low power domain rather than by the CPU. set_pin() already hands such pins back before taking them for a hardware port, but the software port path did not: the bit-banged lines stayed bound to the low power peripheral and never reached the pads, so the board autodetect probe (which runs on a software port) failed on every warm boot until a power cycle reset the routing. Release the pads in init() for a software port as well, the same way the hardware port path does.
ESP-IDF v6 drops the ESP32-classic HSPI_HOST/VSPI_HOST aliases, which broke the autodetect path and the Atom/Module display headers when building against it. SPI2_HOST/SPI3_HOST are the canonical names since IDF v4, so this stays compatible with every Arduino core and IDF version the CI floor covers.
- The IDF matrix gains esp32c5 (5.5.2), esp32c6 on 5.5.2 so the low power peripheral paths compile, and esp32 on 6.0.2 to surface next major deprecations early. - The Arduino matrix gains esp32c5 on arduino-esp32 3.3.11 (C5 support landed in the 3.3 series). - Both workflows now cancel superseded runs of the same branch or pull request, keyed by event name and PR number.
Fix IDF v6 build (HSPI/VSPI aliases) and extend the CI matrix
The backlight was previously on/off only, so setBrightness had no dimming effect. Drive it with the IO expander PWM channel in the same way as the ChainCaptain, with a gamma 2.0 curve so the perceived brightness follows the setting.
Add PWM brightness control for the M5ToughC5 backlight
ESP-IDF v6 spi_bus_initialize() routes bus pins with gpio_matrix_output() and no longer clears the pad open-drain flag (v5 used gpio_set_direction(), which did). If the pads were previously configured as open-drain outputs (e.g. by the autodetect pull-up probing of the bus pins), SCLK/MOSI stay open-drain, the waveform cannot rise at MHz clock rates through the weak pull-up, and the panel never responds. Explicitly restore the bus pins to push-pull after bus initialization. Verified on M5Stack CoreS3SE: ESP-IDF v6.0.1 autodetect and draw/readRect round-trip now pass; no regression on ESP-IDF v5.5.4 (CoreS3SE) or M5StickS3 (v6.0.1).
…s in pinMode The pad_driver and func_out_sel register layouts are named differently on some chips (e.g. ESP32-C61), which required chip-specific #ifdef branches and SFINAE helpers. Delegate to gpio_ll_od_enable/od_disable and esp_rom_gpio_connect_out_signal, whose per-chip implementations absorb the naming differences, so future chips need no new branches here. Direct register access remains only as a fallback for ESP-IDF v3. esp_rom_gpio_connect_out_signal also normalizes the inv/oen matrix bits, which matches the intended plain-GPIO-output contract of pinMode. Build-verified on esp32s3 (ESP-IDF v5.5.4 / v6.0.1), esp32c61, esp32 (v5.5.4), and Arduino-ESP32 2.x (ESP-IDF v4.4); runtime-verified on M5Stack CoreS3SE with ESP-IDF v6.0.1.
Fix CoreS3 LCD not working on ESP-IDF v6 (SPI bus pins left in open-drain)
The fix for SPI bus pins left in open-drain mode on ESP-IDF v6 (#192) covered only the single-bit spi::init path. spi::initQuad (used by QSPI panels) calls spi_bus_initialize the same way and is affected by the same behavior: ESP-IDF v6 routes bus pins with gpio_matrix_output() and no longer clears the pad open-drain flag. Factor the pad restoration out of spi::init into a shared helper and apply it to SCLK/IO0-3 in spi::initQuad as well. Verified on M5StopWatch (CO5300, QSPI): with the bus pins deliberately set to open-drain before init, the pads remained open-drain after init on ESP-IDF v6.0.1 without this fix, and are restored to push-pull with it. No regression on ESP-IDF v5.5.4.
The LEDC pin attach does not fully normalize the pad state: the pad open-drain flag is never cleared (on any IDF version), and starting with ESP-IDF v6 the ledc driver no longer selects the GPIO function in IO_MUX either. If the backlight pin was previously configured as an input or open-drain output (e.g. by probing code or a previous application state), the PWM output cannot drive the pin high and the backlight stays off. Explicitly normalize the pin as a push-pull GPIO output at the start of init. The pin is first latched to the "off" level to avoid a visible glitch while the pin is temporarily a plain GPIO output. Verified on M5Stack Core FIRE (backlight on GPIO32) with ESP-IDF v6.0.1: with the pin deliberately set to open-drain before init, the pad remained open-drain after init without this fix, and is restored to push-pull with it.
Neither esp_lcd_new_i80_bus (ESP32-S3, since ESP-IDF v5.4 removed its gpio_set_direction call) nor the plain GPIO matrix routing used on ESP32-S2 configures the pad direction or clears the open-drain flag on the data pins. If a data pin was previously configured as an input or open-drain output, the bus cannot drive it high and the panel receives corrupted data. Bus_EPD already guards against this by calling pinMode(output) on its data pins; apply the same guard to Bus_Parallel8/16. The WR/RD/RS control pins were already normalized via gpio_set_direction. Verified on ESP32-S3 (ESP-IDF v6.0.1) by pulse-counting bus edges with PCNT on a scope-free rig: with the data pins deliberately set to open-drain before init, a 0x00/0xFF pattern produced 0 edges on D0/D7 (WR counting normally) without this fix, and the expected edge count with it.
Light_PWM::init ignored the result of the LEDC configuration calls and always returned true. Propagate failures instead: init now fails early when the configured pin cannot be driven as a GPIO output, the ESP-IDF path checks both ledc_channel_config and ledc_timer_config, the Arduino 3.x path returns the ledcAttach result, and the pre-3.x path checks the frequency returned by ledcSetup (0 on failure) and only attaches the pin when setup succeeded. On failure the initial setBrightness call is skipped. The caller (Panel_Device::init) ignores the return value, so panel initialization is unaffected; the result is now meaningful for callers that choose to check it.
Clear stale open-drain pad state in QSPI, parallel bus, and PWM backlight init
…PENDENT removed) ESP-IDF v6 removed the SOC_GPIO_SUPPORT_RTC_INDEPENDENT soc capability macro. pinMode treated the missing macro as 0, which routes RTC-capable pins through the RTC-domain pull path meant for the plain ESP32. On ESP32-S2/S3 (and other chips with independent digital pads) that path does not drive the pad pulls: input_pullup left the IO_MUX FUN_PU bit clear and the line did not rise. Map the capability to an internal LGFX_GPIO_RTC_INDEPENDENT macro that uses the SoC value when available and otherwise derives it from the build target: only the plain ESP32 needs the RTC path. Verified on ESP32-S3 (floating RTC-capable pin, ESP-IDF v6.0.1): input_pullup previously read back low with FUN_PU clear; with this fix the pull-up is driven and the pin reads high. No regression on v5.5.4 (macro still provided by the SoC caps) and no behavior change for the plain ESP32.
Fix pinMode pulls not driven on ESP-IDF v6 (SOC_GPIO_SUPPORT_RTC_INDEPENDENT removed)
Panel_CO5300 supports direct drawing: the optional PSRAM frame buffer (initPanelFb) may fail to allocate and is not required, so the PSRAM guard (introduced alongside the EPD boards, which genuinely need the buffer) is unnecessary here. Worse, with PSRAM disabled the guard skipped the panel setup after the board had already been identified, and the autodetection then fell through and misidentified the device as an AtomS3Lite. Direct drawing has a limitation: drawing whose origin is at an odd coordinate may render incorrectly, so a warning is logged when the build lacks OPI-PSRAM. Verified on the real device: with PSRAM disabled the board is now detected as M5StopWatch and the display works via direct drawing; builds with and without OPI-PSRAM are both green.
When PSRAM was disabled in the build, the EPD board blocks (PaperMono, PaperS3/PaperDIY, PaperColor, ChainCaptain) logged the requirement but skipped the panel setup and fell through the rest of the autodetection, which ended with board_unknown and let the caller misidentify the device as a display-less model. Jump to init_clear instead so the board identification is kept; the display simply stays unavailable. Clear _panel_last and _touch_last before the jump: autodetect() only detaches the active panel at entry, so a stale panel object from a previous detection round would otherwise be re-attached at init_clear.
Fix board misidentification when OPI-PSRAM is disabled
The ST71xx touch controller needs several tens of milliseconds to respond again when it is reset during scan operation. The previous 3-attempt probe gave up too early, so a warm reset from a running state (esp_restart or the reset caused by connecting a serial monitor) left the panel type undetected and the screen blank. Poll until a recognized ST71xx firmware version is read or the GT911 (ILI9881C variant) ACKs, up to roughly 600 ms. The ST variants still require the touch FW version to distinguish their DSI timings; a unit whose touch never responds falls back to the previous behavior (the ILI9881C variant is still identified by its DSI ID).
Fix M5Tab5 black screen after a warm reset (wait for the touch controller response)
Some VLW exporters (e.g. the Processing font converter) omit the space glyph from the file. Falling through to drawCharDummy would render the missing-glyph box for every space, so restore the previous silent guessed-width behavior for that case only. Fonts that do contain a space glyph keep using the actual glyph metrics.
Use actual glyph width, not the guessed spaceWidth, to determine the width of the space character in VLW fonts
Some GC9107 batches return a valid ID over the RDDID command only at low clock rates; at the 8 MHz probe speed the readout is garbage, so autodetect rejected the panel and the unit appeared dead (issue #222). When the first probe matches neither known ID, re-probe once at 100 kHz. Verified on a healthy GC9107 unit that the 100 kHz readout returns the correct ID (0x079100), and on both GC9107 and ST7735 units that the normal first-probe path is unchanged. The ST7735 does not answer the RDDID readout at low clock rates at all (verified: 0xFFFFFFFF at 1 MHz and below, correct at 8 MHz), so the slow retry cannot misidentify it; a healthy ST7735 has already been caught by the first probe.
AtomS3R: re-probe the panel ID at low clock speed before giving up
Bump version to 0.2.27
Update the version define missed in the 0.2.27 bump
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.
New device / panel support
New features
ESP-IDF v6 compatibility
Fixes