From bd96c127f0fbb5ba96a347ddb2d08958ee215e21 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 01:59:42 +0000 Subject: [PATCH 1/2] Add Panel_TM1680 LED matrix driver I2C driver for the Titan Micro TM1680 in 24ROW x 16COM mode, used as a 16x16 monochrome LED matrix. - The frame buffer keeps the Panel_1bitOLED page layout and is converted to the TM1680 RAM layout (row bits A3-A0 in the upper nibble half) when pushed by display(). - The whole frame is only 32 bytes, so display() always sends it in full as a single transaction. - The controller does not respond to I2C reads, so init() skips the read probe used by other 1bit panels. - There is no display-invert command; inversion is applied while packing the buffer. - Brightness maps to the global 16-step PWM duty. --- src/lgfx/v1/panel/Panel_TM1680.cpp | 142 +++++++++++++++++++++++++++++ src/lgfx/v1/panel/Panel_TM1680.hpp | 76 +++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 src/lgfx/v1/panel/Panel_TM1680.cpp create mode 100644 src/lgfx/v1/panel/Panel_TM1680.hpp diff --git a/src/lgfx/v1/panel/Panel_TM1680.cpp b/src/lgfx/v1/panel/Panel_TM1680.cpp new file mode 100644 index 00000000..04f4dda9 --- /dev/null +++ b/src/lgfx/v1/panel/Panel_TM1680.cpp @@ -0,0 +1,142 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) +/----------------------------------------------------------------------------*/ +#include "Panel_TM1680.hpp" +#include "../Bus.hpp" +#include "../platforms/common.hpp" + +#include + +namespace lgfx +{ + inline namespace v1 + { +//---------------------------------------------------------------------------- + + bool Panel_TM1680::init(bool use_reset) + { + // The TM1680 does not respond to I2C reads, so the read probe of + // Panel_1bitOLED::init cannot be used; send the init commands without probing. + if (!Panel_HasBuffer::init(use_reset)) + { + return false; + } + // Panel_HasBuffer::init does not clear the allocated buffer; + // clear it here so setInvert below does not push garbage to the LEDs. + memset(_buf, 0, _get_buffer_length()); + + startWrite(true); + for (size_t i = 0; auto cmds = getInitCommands(i); i++) + { + size_t idx = 0; + while (cmds[idx] != 0xFF || cmds[idx + 1] != 0xFF) ++idx; + if (idx) { _bus->writeBytes(cmds, idx, false, true); } + } + setInvert(_invert); + setRotation(_rotation); + endWrite(); + + return true; + } + + void Panel_TM1680::setInvert(bool invert) + { + _invert = invert; + // The TM1680 has no display-invert command; invert while packing in display(). + if (_buf) + { + startWrite(); + display(0, 0, _cfg.panel_width, _cfg.panel_height); + endWrite(); + } + } + + void Panel_TM1680::setSleep(bool flg) + { + startWrite(); + if (flg) + { + _bus->writeCommand(CMD_LED_OFF, 8); + _bus->writeCommand(CMD_SYS_DIS, 8); + } + else + { + _bus->writeCommand(CMD_SYS_EN, 8); + _bus->writeCommand(CMD_LED_ON, 8); + } + endWrite(); + } + + void Panel_TM1680::setBrightness(uint8_t brightness) + { + startWrite(); + if (brightness) + { + _bus->writeCommand(CMD_PWM_DUTY + (((brightness * 16) >> 8) & 0x0F), 8); + _bus->writeCommand(CMD_LED_ON, 8); + } + else + { + _bus->writeCommand(CMD_LED_OFF, 8); + } + endWrite(); + } + + void Panel_TM1680::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) + { + if (0 < w && 0 < h) + { + uint_fast16_t xs = x, xe = x + w - 1; + uint_fast16_t ys = y, ye = y + h - 1; + _update_transferred_rect(xs, ys, xe, ye); + } + if (_range_mod.empty()) { return; } + + // The whole frame is only 32 bytes; always send it in full. + // The leading byte is the RAM write address. + uint8_t sendbuf[1 + ((16 * 16) >> 3)]; + sendbuf[0] = 0x00; + bool invert = _invert ^ _cfg.invert; + + for (uint_fast8_t py = 0; py < 16; ++py) + { + uint32_t mask = 1 << (py & 7); + auto src = &_buf[(py >> 3) * 16]; + for (uint_fast8_t xb = 0; xb < 2; ++xb) + { + uint_fast8_t v = 0; + for (uint_fast8_t bit = 0; bit < 8; ++bit) + { + if (src[(xb << 3) + bit] & mask) + { // in TM1680 RAM, rows A3-A0 occupy the upper nibble half + v |= 1 << ((bit > 3) ? (bit & 3) : (4 + bit)); + } + } + sendbuf[1 + py * 2 + xb] = invert ? (v ^ 0xFF) : v; + } + } + + _bus->writeBytes(sendbuf, sizeof(sendbuf), true, true); + _range_mod.top = INT16_MAX; + _range_mod.left = INT16_MAX; + _range_mod.right = 0; + _range_mod.bottom = 0; + } + +//---------------------------------------------------------------------------- + } +} diff --git a/src/lgfx/v1/panel/Panel_TM1680.hpp b/src/lgfx/v1/panel/Panel_TM1680.hpp new file mode 100644 index 00000000..d07dbdb8 --- /dev/null +++ b/src/lgfx/v1/panel/Panel_TM1680.hpp @@ -0,0 +1,76 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) +/----------------------------------------------------------------------------*/ +#pragma once + +#include "Panel_SSD1306.hpp" + +namespace lgfx +{ + inline namespace v1 + { +//---------------------------------------------------------------------------- + + // Titan Micro TM1680 LED matrix driver (I2C, 24ROW x 16COM mode). + // The frame buffer keeps the Panel_1bitOLED page layout and is converted + // to the TM1680 RAM layout when pushed by display(). + // The protocol has no command/data prefix byte: configure Bus_I2C with prefix_len = 0. + struct Panel_TM1680 : public Panel_1bitOLED + { + Panel_TM1680(void) + { + _cfg.memory_width = _cfg.panel_width = 16; + _cfg.memory_height = _cfg.panel_height = 16; + _auto_display = true; + } + + bool init(bool use_reset) override; + void setInvert(bool invert) override; + void setSleep(bool flg) override; + void setBrightness(uint8_t brightness) override; + void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; + + protected: + static constexpr uint8_t CMD_SYS_DIS = 0x80; + static constexpr uint8_t CMD_SYS_EN = 0x81; + static constexpr uint8_t CMD_LED_OFF = 0x82; + static constexpr uint8_t CMD_LED_ON = 0x83; + static constexpr uint8_t CMD_RC_MASTER = 0x98; // internal RC oscillator, master mode + static constexpr uint8_t CMD_COM_16NMOS = 0xA4; // 24ROW x 16COM (NMOS) + static constexpr uint8_t CMD_PWM_DUTY = 0xB0; // +0..15 = duty 1/16..16/16 + + const uint8_t* getInitCommands(uint8_t listno) const override + { + static constexpr uint8_t list0[] = { + CMD_SYS_DIS, + CMD_COM_16NMOS, + CMD_RC_MASTER, + CMD_SYS_EN, + CMD_PWM_DUTY + 1, + CMD_LED_ON, + 0xFF, 0xFF, // end + }; + switch (listno) { + case 0: return list0; + default: return nullptr; + } + } + }; + +//---------------------------------------------------------------------------- + } +} From 12976dec020817778cce60eceee6695cb0a3c7c5 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 01:59:42 +0000 Subject: [PATCH 2/2] Add support for M5Stack CoreMatrix (ESP32-C61) Autodetect probes the M5PM1 and M5IOE1 over software I2C first, then powers the LED matrix rail through M5IOE1 PIN4 before checking for the TM1680: the controller sits on that rail and does not respond on I2C until powered. The TM1680 has no ID register, so presence is confirmed by an address ACK only. The default rotation is 1 with offset_rotation 3, which shows upright with the ABC buttons at the top (verified on hardware). --- src/M5GFX.cpp | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 8cbcc4c3..861f6c69 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -19,6 +19,7 @@ #include "lgfx/v1/panel/Panel_ILI9342.hpp" #include "lgfx/v1/panel/Panel_SSD1306.hpp" #include "lgfx/v1/panel/Panel_SSD1677.hpp" +#include "lgfx/v1/panel/Panel_TM1680.hpp" #include "lgfx/v1/panel/Panel_ST7735.hpp" #include "lgfx/v1/panel/Panel_ST7789.hpp" #include "lgfx/v1/panel/Panel_GC9A01.hpp" @@ -50,6 +51,11 @@ static constexpr int_fast16_t in_i2c_port = I2C_NUM_1; #include #endif + +#elif defined ( CONFIG_IDF_TARGET_ESP32C61 ) + +#include "lgfx/v1/platforms/esp32/Bus_I2C.hpp" + #endif #else @@ -879,6 +885,12 @@ namespace m5gfx // Touch_M5ToughC5 は lgfx::Touch_CHSC6540 に統合済み +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + + static constexpr int32_t i2c_freq = 400000; + static constexpr int_fast16_t i2c_port = I2C_NUM_0; + static constexpr std::uint8_t tm1680_i2c_addr = 0x72; // CoreMatrix LED matrix driver + #elif defined (CONFIG_IDF_TARGET_ESP32C6) static constexpr int32_t i2c_freq = 400000; @@ -3449,6 +3461,81 @@ The usage of each pin is as follows. for (auto &bup : backup_pins) { bup.restore(); } } +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + + if (board == 0 || board == board_t::board_M5CoreMatrix) + { + // CoreMatrix: system I2C SDA=G0 / SCL=G1 (physically shared with Grove and M-Bus) + static constexpr int_fast16_t corematrix_i2c_sda = GPIO_NUM_0; + static constexpr int_fast16_t corematrix_i2c_scl = GPIO_NUM_1; + + gpio::pin_backup_t backup_pins[] = + { GPIO_NUM_0 + , GPIO_NUM_1 + }; + + // Probe over software I2C; the hardware port is left untouched until the board is confirmed + lgfx::i2c::init(probe_i2c_port, corematrix_i2c_sda, corematrix_i2c_scl); + + if (_check_m5pm1(probe_i2c_port) && _check_m5ioe1(probe_i2c_port)) { + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); // I2C sleep disable + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); // WDT disable + + // Drive M5IOE1 PIN4 (LEDS_EN) high to power the LED matrix rail. + // The TM1680 sits on that rail and does not respond on I2C until powered. + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); // I2C sleep disable + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x13, 1 << 3, m5ioe1_i2c_freq); // PIN4 push-pull + lgfx::i2c::bitOn (probe_i2c_port, m5ioe1_i2c_addr, 0x03, 1 << 3, m5ioe1_i2c_freq); // PIN4 output + lgfx::i2c::bitOn (probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4 HIGH + lgfx::delay(20); // wait for the rail to rise and the TM1680 to complete POR + + // The TM1680 has no ID register; check for an address ACK only + bool hit = lgfx::i2c::beginTransaction(probe_i2c_port, tm1680_i2c_addr, 100000, false).has_value() + && lgfx::i2c::endTransaction(probe_i2c_port).has_value(); + if (hit) + { + board = board_t::board_M5CoreMatrix; + ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5CoreMatrix"); + + // Board confirmed: hand the bus over to the hardware I2C port + lgfx::i2c::release(probe_i2c_port); + + auto bus_i2c = new Bus_I2C(); + { + auto cfg = bus_i2c->config(); + cfg.i2c_port = i2c_port; + cfg.freq_write = i2c_freq; + cfg.freq_read = i2c_freq; + cfg.pin_sda = corematrix_i2c_sda; + cfg.pin_scl = corematrix_i2c_scl; + cfg.i2c_addr = tm1680_i2c_addr; + cfg.prefix_len = 0; // the TM1680 protocol has no command/data prefix byte + bus_i2c->config(cfg); + } + _bus_last.reset(bus_i2c); + + auto p = new lgfx::Panel_TM1680(); + { + auto cfg = p->config(); + cfg.bus_shared = false; + // rotation 1 (the default) shows upright with the buttons at the top + cfg.offset_rotation = 3; + p->config(cfg); + p->setRotation(1); + } + p->bus(bus_i2c); + _panel_last.reset(p); + + goto init_clear; + } + // Not this board: restore the LED matrix power rail + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); + } + // Reached only when no board was detected; only the software port was touched + lgfx::i2c::release(probe_i2c_port); + for (auto &bup : backup_pins) { bup.restore(); } + } + #elif defined (CONFIG_IDF_TARGET_ESP32H2) #endif @@ -3514,6 +3601,7 @@ The usage of each pin is as follows. case board_M5Tough: title = "M5Tough"; break; case board_M5ToughC5: title = "M5ToughC5"; break; case board_M5StampC5: title = "M5StampC5"; break; + case board_M5CoreMatrix: title = "M5CoreMatrix"; break; case board_M5Station: title = "M5Station"; break; case board_M5StopWatch: title = "M5StopWatch"; break; case board_M5ChainCaptain: title = "M5ChainCaptain"; break;