diff --git a/keyboards/metakey/tenet/boards/METAKEY_TENET/board.c b/keyboards/metakey/tenet/boards/METAKEY_TENET/board.c
new file mode 100644
index 000000000000..8cf60bcad665
--- /dev/null
+++ b/keyboards/metakey/tenet/boards/METAKEY_TENET/board.c
@@ -0,0 +1,100 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * PAL static configuration for the Metakey Tenet 70.
+ *
+ * USB_DM=PA11, USB_DP=PA12 (fixed by silicon).
+ * SWDIO=PA13, SWCLK=PA14 — preserved for SWD recovery.
+ */
+
+#include "hal.h"
+
+#define PBIT(PORT, LINE) ((PAL_PORT(LINE) == PORT) ? (1 << PAL_PAD(LINE)) : 0)
+#define PAFIO_L(PORT, LINE, AF) (((PAL_PORT(LINE) == PORT) && (PAL_PAD(LINE) < 8)) ? (AF << (PAL_PAD(LINE) << 2)) : 0)
+#define PAFIO_H(PORT, LINE, AF) (((PAL_PORT(LINE) == PORT) && (PAL_PAD(LINE) >= 8)) ? (AF << ((PAL_PAD(LINE) - 8) << 2)) : 0)
+#define PAFIO(PORT, N, LINE, AF) ((N) ? PAFIO_H(PORT, LINE, AF) : PAFIO_L(PORT, LINE, AF))
+
+// Matrix rows (outputs, active low).
+#define OUT_BITS(PORT) ( \
+ PBIT(PORT, B8) | PBIT(PORT, B7) | PBIT(PORT, B6) | PBIT(PORT, C3) | PBIT(PORT, C2) | \
+ 0)
+
+// Matrix cols (inputs with pull-ups). Col 17 = PA11 handled in matrix.c;
+// col 14 = PA14 (SWCLK) AFIO override done in matrix_init_custom().
+#define IN_BITS(PORT) ( \
+ PBIT(PORT, A6) | PBIT(PORT, A7) | PBIT(PORT, A8) | PBIT(PORT, A9) | PBIT(PORT, A10) | \
+ PBIT(PORT, A14) | \
+ PBIT(PORT, B13) | PBIT(PORT, B14) | PBIT(PORT, B15) | \
+ PBIT(PORT, C0) | PBIT(PORT, C4) | PBIT(PORT, C5) | PBIT(PORT, C8) | \
+ PBIT(PORT, C10) | PBIT(PORT, C11) | PBIT(PORT, C12) | PBIT(PORT, C13) | \
+ 0)
+
+#define AF_BITS(PORT, N) ( \
+ PAFIO(PORT, N, B8, AFIO_GPIO) | PAFIO(PORT, N, B7, AFIO_GPIO) | PAFIO(PORT, N, B6, AFIO_GPIO) | \
+ PAFIO(PORT, N, C3, AFIO_GPIO) | PAFIO(PORT, N, C2, AFIO_GPIO) | \
+ PAFIO(PORT, N, A6, AFIO_GPIO) | PAFIO(PORT, N, A7, AFIO_GPIO) | PAFIO(PORT, N, A8, AFIO_GPIO) | \
+ PAFIO(PORT, N, A9, AFIO_GPIO) | PAFIO(PORT, N, A10, AFIO_GPIO) | \
+ PAFIO(PORT, N, A0, AFIO_GPIO) | PAFIO(PORT, N, A1, AFIO_GPIO) | PAFIO(PORT, N, A4, AFIO_GPIO) | \
+ PAFIO(PORT, N, A5, AFIO_GPIO) | \
+ PAFIO(PORT, N, C0, AFIO_GPIO) | PAFIO(PORT, N, C1, AFIO_GPIO) | PAFIO(PORT, N, C4, AFIO_GPIO) | \
+ PAFIO(PORT, N, C5, AFIO_GPIO) | PAFIO(PORT, N, C6, AFIO_GPIO) | PAFIO(PORT, N, C7, AFIO_GPIO) | \
+ PAFIO(PORT, N, D0, AFIO_GPIO) | PAFIO(PORT, N, B2, AFIO_GPIO) | \
+ 0)
+
+const PALConfig pal_default_config = {
+ .setup[0] = { // GPIO A
+ .DIR = OUT_BITS(IOPORTA),
+ .INE = IN_BITS(IOPORTA),
+ .PU = IN_BITS(IOPORTA),
+ .PD = 0x0000,
+ .OD = 0x0000,
+ .DRV = 0x0000,
+ .LOCK = 0x0000,
+ .OUT = 0x0000,
+ .CFG[0] = AF_BITS(IOPORTA, 0),
+ .CFG[1] = AF_BITS(IOPORTA, 1),
+ },
+ .setup[1] = { // GPIO B
+ .DIR = OUT_BITS(IOPORTB),
+ .INE = IN_BITS(IOPORTB),
+ .PU = IN_BITS(IOPORTB),
+ .PD = 0x0000,
+ .OD = 0x0000,
+ .DRV = 0x0000,
+ .LOCK = 0x0000,
+ .OUT = 0x0000,
+ .CFG[0] = AF_BITS(IOPORTB, 0),
+ .CFG[1] = AF_BITS(IOPORTB, 1),
+ },
+ .setup[2] = { // GPIO C
+ .DIR = OUT_BITS(IOPORTC),
+ .INE = IN_BITS(IOPORTC),
+ .PU = IN_BITS(IOPORTC),
+ .PD = 0x0000,
+ .OD = 0x0000,
+ .DRV = 0x0000,
+ .LOCK = 0x0000,
+ .OUT = 0x0000,
+ .CFG[0] = AF_BITS(IOPORTC, 0),
+ .CFG[1] = AF_BITS(IOPORTC, 1),
+ },
+ .setup[3] = { // GPIO D
+ .DIR = OUT_BITS(IOPORTD),
+ .INE = IN_BITS(IOPORTD),
+ .PU = IN_BITS(IOPORTD),
+ .PD = 0x0000,
+ .OD = 0x0000,
+ .DRV = 0x0000,
+ .LOCK = 0x0000,
+ .OUT = 0x0000,
+ .CFG[0] = AF_BITS(IOPORTD, 0),
+ .CFG[1] = AF_BITS(IOPORTD, 1),
+ },
+ .ESSR[0] = 0x00000000,
+ .ESSR[1] = 0x00000000,
+};
+
+void __early_init(void) { ht32_clock_init(); }
+
+void boardInit(void) {}
diff --git a/keyboards/metakey/tenet/boards/METAKEY_TENET/board.h b/keyboards/metakey/tenet/boards/METAKEY_TENET/board.h
new file mode 100644
index 000000000000..82356875b071
--- /dev/null
+++ b/keyboards/metakey/tenet/boards/METAKEY_TENET/board.h
@@ -0,0 +1,20 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#define BOARD_NAME "Metakey Tenet 70"
+
+#define HT32F52352
+
+#define FLASH_SIZE (0x20000 - 0x4000) // 128kB - 16kB bootloader
+
+#if !defined(_FROM_ASM_)
+#ifdef __cplusplus
+extern "C" {
+#endif
+void boardInit(void);
+#ifdef __cplusplus
+}
+#endif
+#endif /* _FROM_ASM_ */
diff --git a/keyboards/metakey/tenet/boards/METAKEY_TENET/board.mk b/keyboards/metakey/tenet/boards/METAKEY_TENET/board.mk
new file mode 100644
index 000000000000..664e943fe300
--- /dev/null
+++ b/keyboards/metakey/tenet/boards/METAKEY_TENET/board.mk
@@ -0,0 +1,2 @@
+BOARDSRC = $(BOARD_PATH)/boards/METAKEY_TENET/board.c
+BOARDINC = $(BOARD_PATH)/boards/METAKEY_TENET
diff --git a/keyboards/metakey/tenet/chconf.h b/keyboards/metakey/tenet/chconf.h
new file mode 100644
index 000000000000..908a19920f45
--- /dev/null
+++ b/keyboards/metakey/tenet/chconf.h
@@ -0,0 +1,31 @@
+/* Copyright 2020 QMK
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+/*
+ * This file was auto-generated by:
+ * `qmk chibios-confmigrate -i keyboards/annepro2/chconf.h -r platforms/chibios/common/configs/chconf.h`
+ */
+
+#pragma once
+
+#define CH_CFG_ST_FREQUENCY 1000
+
+#define CH_CFG_ST_TIMEDELTA 0
+
+#define CH_CFG_TIME_QUANTUM 20
+
+
+#include_next
diff --git a/keyboards/metakey/tenet/config.h b/keyboards/metakey/tenet/config.h
new file mode 100644
index 000000000000..c8fa1c6c9369
--- /dev/null
+++ b/keyboards/metakey/tenet/config.h
@@ -0,0 +1,31 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "pin_defs.h"
+
+/* OLED: SSD1326 256x32, driven directly by the main MCU over I2C0
+ * (PB0 SDA / PB1 SCL at AFIO mode 7, PD1 active-low RST). Driver at
+ * ssd1326.c uses raw register writes — chibios-contrib has no HT32 I2C LLD. */
+
+/* Orientation detect: stock firmware reads PA15 (pin idx 0x29) via
+ * FUN_0000a0ec at Ghidra 0xa0ec. HIGH = regular, LOW = southpaw.
+ * Exposed as `tenet_read_orientation()`. */
+
+/* Keyboard-reserved EEPROM datablock: 16 bytes persistent OLED text plus
+ * headroom for future state. */
+#define EECONFIG_KB_DATA_SIZE 32
+
+/* HT32 has no on-chip EEPROM. QMK uses a RAM-backed transient store, sized for
+ * core eeconfig plus the keyboard data block and RGB matrix state. */
+#define TRANSIENT_EEPROM_SIZE 1024
+
+#define PERMISSIVE_HOLD
+
+/* RGB Matrix: 76 LEDs across the mainboard (64) and daughterboard (12)
+ * sub-MCUs. ws2812_custom.c mirrors stock's SPI1 sub-MCU command protocol. */
+#define RGB_MATRIX_LED_COUNT 76
+#define RGB_MATRIX_KEYPRESSES
+#define RGB_MATRIX_DEFAULT_VAL 100
+#define RGB_MATRIX_SLEEP
diff --git a/keyboards/metakey/tenet/halconf.h b/keyboards/metakey/tenet/halconf.h
new file mode 100644
index 000000000000..b73fd475426a
--- /dev/null
+++ b/keyboards/metakey/tenet/halconf.h
@@ -0,0 +1,31 @@
+/* Copyright 2020 QMK
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+/*
+ * This file was auto-generated by:
+ * `qmk chibios-confmigrate -i keyboards/annepro2/halconf.h -r platforms/chibios/common/configs/halconf.h`
+ */
+
+#pragma once
+
+#define HAL_USE_SERIAL TRUE
+
+#define SERIAL_USB_BUFFERS_SIZE 256
+
+#define HAL_USE_SPI FALSE
+#define HAL_USE_I2C FALSE
+
+#include_next
diff --git a/keyboards/metakey/tenet/keyboard.json b/keyboards/metakey/tenet/keyboard.json
new file mode 100644
index 000000000000..2dec089e85ad
--- /dev/null
+++ b/keyboards/metakey/tenet/keyboard.json
@@ -0,0 +1,108 @@
+{
+ "manufacturer": "Metakey",
+ "keyboard_name": "Tenet 70 (QMK)",
+ "maintainer": "connorgallopo",
+ "url": "https://clickclack.io/pages/metakey-tenet",
+ "usb": {
+ "vid": "0x3311",
+ "pid": "0xF005",
+ "device_version": "0.0.1"
+ },
+ "processor": "cortex-m0plus",
+ "bootloader": "custom",
+ "diode_direction": "COL2ROW",
+ "matrix_size": {
+ "rows": 5,
+ "cols": 18
+ },
+ "features": {
+ "bootmagic": true,
+ "console": true,
+ "extrakey": true,
+ "mousekey": true,
+ "nkro": true
+ },
+ "layouts": {
+ "LAYOUT": {
+ "layout": [
+ {"matrix": [0, 0], "x": 0, "y": 0, "label": "Esc"},
+ {"matrix": [0, 1], "x": 1, "y": 0, "label": "1"},
+ {"matrix": [0, 2], "x": 2, "y": 0, "label": "2"},
+ {"matrix": [0, 3], "x": 3, "y": 0, "label": "3"},
+ {"matrix": [0, 4], "x": 4, "y": 0, "label": "4"},
+ {"matrix": [0, 5], "x": 5, "y": 0, "label": "5"},
+ {"matrix": [0, 6], "x": 6, "y": 0, "label": "6"},
+ {"matrix": [0, 7], "x": 7, "y": 0, "label": "7"},
+ {"matrix": [0, 8], "x": 8, "y": 0, "label": "8"},
+ {"matrix": [0, 9], "x": 9, "y": 0, "label": "9"},
+ {"matrix": [0, 10], "x": 10, "y": 0, "label": "0"},
+ {"matrix": [0, 11], "x": 11, "y": 0, "label": "-"},
+ {"matrix": [0, 12], "x": 12, "y": 0, "label": "="},
+ {"matrix": [2, 13], "x": 13, "y": 0, "w": 2, "label": "Bksp"},
+ {"matrix": [0, 15], "x": 16.25,"y": 0, "label": "Ins"},
+ {"matrix": [0, 16], "x": 17.25,"y": 0, "label": "Home"},
+ {"matrix": [0, 17], "x": 18.25,"y": 0, "label": "PgUp"},
+
+ {"matrix": [1, 0], "x": 0, "y": 1, "w": 1.5, "label": "Tab"},
+ {"matrix": [1, 1], "x": 1.5, "y": 1, "label": "Q"},
+ {"matrix": [1, 2], "x": 2.5, "y": 1, "label": "W"},
+ {"matrix": [1, 3], "x": 3.5, "y": 1, "label": "E"},
+ {"matrix": [1, 4], "x": 4.5, "y": 1, "label": "R"},
+ {"matrix": [1, 5], "x": 5.5, "y": 1, "label": "T"},
+ {"matrix": [1, 6], "x": 6.5, "y": 1, "label": "Y"},
+ {"matrix": [1, 7], "x": 7.5, "y": 1, "label": "U"},
+ {"matrix": [1, 8], "x": 8.5, "y": 1, "label": "I"},
+ {"matrix": [1, 9], "x": 9.5, "y": 1, "label": "O"},
+ {"matrix": [1, 10], "x": 10.5, "y": 1, "label": "P"},
+ {"matrix": [1, 11], "x": 11.5, "y": 1, "label": "["},
+ {"matrix": [1, 12], "x": 12.5, "y": 1, "label": "]"},
+ {"matrix": [1, 13], "x": 13.5, "y": 1, "w": 1.5, "label": "Backslash"},
+ {"matrix": [1, 15], "x": 16.25,"y": 1, "label": "Del"},
+ {"matrix": [1, 16], "x": 17.25,"y": 1, "label": "End"},
+ {"matrix": [1, 17], "x": 18.25,"y": 1, "label": "PgDn"},
+
+ {"matrix": [2, 0], "x": 0, "y": 2, "w": 1.75, "label": "Ctrl"},
+ {"matrix": [2, 1], "x": 1.75, "y": 2, "label": "A"},
+ {"matrix": [2, 2], "x": 2.75, "y": 2, "label": "S"},
+ {"matrix": [2, 3], "x": 3.75, "y": 2, "label": "D"},
+ {"matrix": [2, 4], "x": 4.75, "y": 2, "label": "F"},
+ {"matrix": [2, 5], "x": 5.75, "y": 2, "label": "G"},
+ {"matrix": [2, 6], "x": 6.75, "y": 2, "label": "H"},
+ {"matrix": [2, 7], "x": 7.75, "y": 2, "label": "J"},
+ {"matrix": [2, 8], "x": 8.75, "y": 2, "label": "K"},
+ {"matrix": [2, 9], "x": 9.75, "y": 2, "label": "L"},
+ {"matrix": [2, 10], "x": 10.75,"y": 2, "label": "Semi"},
+ {"matrix": [2, 11], "x": 11.75,"y": 2, "label": "Quote"},
+ {"matrix": [2, 12], "x": 12.75,"y": 2, "w": 2.25, "label": "Enter"},
+
+ {"matrix": [3, 0], "x": 0, "y": 3, "w": 2.25, "label": "LShift"},
+ {"matrix": [3, 2], "x": 2.25, "y": 3, "label": "Z"},
+ {"matrix": [3, 3], "x": 3.25, "y": 3, "label": "X"},
+ {"matrix": [3, 4], "x": 4.25, "y": 3, "label": "C"},
+ {"matrix": [3, 5], "x": 5.25, "y": 3, "label": "V"},
+ {"matrix": [3, 6], "x": 6.25, "y": 3, "label": "B"},
+ {"matrix": [3, 7], "x": 7.25, "y": 3, "label": "N"},
+ {"matrix": [3, 8], "x": 8.25, "y": 3, "label": "M"},
+ {"matrix": [3, 9], "x": 9.25, "y": 3, "label": "Comma"},
+ {"matrix": [3, 10], "x": 10.25,"y": 3, "label": "Dot"},
+ {"matrix": [3, 11], "x": 11.25,"y": 3, "label": "Slash"},
+ {"matrix": [3, 12], "x": 12.25,"y": 3, "w": 1.75, "label": "RShift"},
+ {"matrix": [3, 13], "x": 14, "y": 3, "label": "Fn2"},
+ {"matrix": [3, 15], "x": 16.25,"y": 3, "label": "OLED-"},
+ {"matrix": [3, 16], "x": 17.25,"y": 3, "label": "Up"},
+ {"matrix": [3, 17], "x": 18.25,"y": 3, "label": "OLED+"},
+
+ {"matrix": [4, 0], "x": 0, "y": 4, "w": 1.5, "label": "LCtrl"},
+ {"matrix": [4, 1], "x": 1.5, "y": 4, "label": "Win"},
+ {"matrix": [4, 2], "x": 2.5, "y": 4, "w": 1.5, "label": "Alt"},
+ {"matrix": [4, 4], "x": 4, "y": 4, "w": 7, "label": "Space"},
+ {"matrix": [4, 6], "x": 11, "y": 4, "w": 1.5, "label": "RAlt"},
+ {"matrix": [4, 7], "x": 12.5, "y": 4, "label": "Fn"},
+ {"matrix": [4, 8], "x": 13.5, "y": 4, "w": 1.5, "label": "RCtrl"},
+ {"matrix": [4, 15], "x": 16.25,"y": 4, "label": "Left"},
+ {"matrix": [4, 16], "x": 17.25,"y": 4, "label": "Down"},
+ {"matrix": [4, 17], "x": 18.25,"y": 4, "label": "Right"}
+ ]
+ }
+ }
+}
diff --git a/keyboards/metakey/tenet/keymaps/default/keymap.c b/keyboards/metakey/tenet/keymaps/default/keymap.c
new file mode 100644
index 000000000000..365033ae9d94
--- /dev/null
+++ b/keyboards/metakey/tenet/keymaps/default/keymap.c
@@ -0,0 +1,27 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include QMK_KEYBOARD_H
+
+enum tenet_layers {
+ _BASE = 0,
+ _FN,
+};
+
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
+ [_BASE] = LAYOUT(
+ KC_ESC, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_INS, KC_HOME, KC_PGUP,
+ KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSPC, KC_DEL, KC_END, KC_PGDN,
+ KC_LCTL, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT,
+ KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, MO(_FN), KC_OLED_L, KC_UP, KC_OLED_R,
+ KC_LCTL, KC_LGUI, KC_LALT, KC_SPC, KC_RALT, MO(_FN), KC_RCTL, KC_LEFT, KC_DOWN, KC_RGHT
+ ),
+
+ [_FN] = LAYOUT(
+ QK_BOOT, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, KC_TRNS, KC_OLED_EDIT, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS
+ ),
+};
diff --git a/keyboards/metakey/tenet/ld/HT32F52352_TENET.ld b/keyboards/metakey/tenet/ld/HT32F52352_TENET.ld
new file mode 100644
index 000000000000..453842d213e4
--- /dev/null
+++ b/keyboards/metakey/tenet/ld/HT32F52352_TENET.ld
@@ -0,0 +1,44 @@
+/*
+ * Tenet 70 memory layout:
+ * 0x00000 - 0x03FFF : Hexcore IAP bootloader (16KB, do not touch)
+ * 0x04000 - 0x1FFFF : application firmware (112KB available)
+ * SRAM 0x20000000 - 0x20003FFF : 16KB
+ */
+MEMORY {
+ flash0 : org = 0x00004000, len = 128k - 16k
+ flash1 : org = 0x00000000, len = 0
+ flash2 : org = 0x00000000, len = 0
+ flash3 : org = 0x00000000, len = 0
+ flash4 : org = 0x00000000, len = 0
+ flash5 : org = 0x00000000, len = 0
+ flash6 : org = 0x00000000, len = 0
+ flash7 : org = 0x00000000, len = 0
+ ram0 : org = 0x20000000, len = 16k
+ ram1 : org = 0x00000000, len = 0
+ ram2 : org = 0x00000000, len = 0
+ ram3 : org = 0x00000000, len = 0
+ ram4 : org = 0x00000000, len = 0
+ ram5 : org = 0x00000000, len = 0
+ ram6 : org = 0x00000000, len = 0
+ ram7 : org = 0x00000000, len = 0
+}
+
+REGION_ALIAS("VECTORS_FLASH", flash0);
+REGION_ALIAS("VECTORS_FLASH_LMA", flash0);
+REGION_ALIAS("XTORS_FLASH", flash0);
+REGION_ALIAS("XTORS_FLASH_LMA", flash0);
+REGION_ALIAS("TEXT_FLASH", flash0);
+REGION_ALIAS("TEXT_FLASH_LMA", flash0);
+REGION_ALIAS("RODATA_FLASH", flash0);
+REGION_ALIAS("RODATA_FLASH_LMA", flash0);
+REGION_ALIAS("VARIOUS_FLASH", flash0);
+REGION_ALIAS("VARIOUS_FLASH_LMA", flash0);
+REGION_ALIAS("RAM_INIT_FLASH_LMA", flash0);
+REGION_ALIAS("MAIN_STACK_RAM", ram0);
+REGION_ALIAS("PROCESS_STACK_RAM", ram0);
+REGION_ALIAS("DATA_RAM", ram0);
+REGION_ALIAS("DATA_RAM_LMA", flash0);
+REGION_ALIAS("BSS_RAM", ram0);
+REGION_ALIAS("HEAP_RAM", ram0);
+
+INCLUDE rules.ld
diff --git a/keyboards/metakey/tenet/matrix.c b/keyboards/metakey/tenet/matrix.c
new file mode 100644
index 000000000000..14bb149ea5c9
--- /dev/null
+++ b/keyboards/metakey/tenet/matrix.c
@@ -0,0 +1,88 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Custom matrix driver for the Metakey Tenet 70.
+ *
+ * 5 rows × 18 cols. Two special-case pins:
+ * - Col 14 = PA14 (SWCLK): forced out of SWD alt-fn at init.
+ * - Col 17 = PA11 (USB D-): alt-fn claimed/released around each scan so
+ * the key share doesn't disturb the USB peripheral.
+ */
+
+#include "quantum.h"
+#include "matrix.h"
+
+// HT32 AFIO registers for pin alt-function control.
+#define HT_AFIO_GPACFGHR (*(volatile uint32_t *)0x40022024U) // PA8-PA15
+
+static const pin_t row_pins[MATRIX_ROWS] = { B8, B7, B6, C3, C2 };
+
+// Col pins 0-16 (GPIO). Col 17 = A11 handled separately.
+static const pin_t col_pins_gpio[17] = {
+ A6, A7, C4, C5, C8, B13, B14, B15,
+ C0, C10, C11, C12, C13, A8, A14, A9, A10
+};
+
+#define COL17_PIN A11
+#define COL17_IDX 17
+
+static inline void col17_claim(void) {
+ HT_AFIO_GPACFGHR &= ~(0xFu << 12);
+ gpio_set_pin_input_high(COL17_PIN);
+}
+
+static inline void col17_release(void) {
+ HT_AFIO_GPACFGHR = (HT_AFIO_GPACFGHR & ~(0xFu << 12)) | (1u << 12);
+}
+
+void matrix_init_custom(void) {
+ // Force PA13/PA14 out of SWD mode.
+ HT_AFIO_GPACFGHR &= ~((0xFu << 20) | (0xFu << 24));
+
+ // Init row pins as GPIO output high (inactive).
+ for (uint8_t r = 0; r < MATRIX_ROWS; r++) {
+ gpio_set_pin_output(row_pins[r]);
+ gpio_write_pin_high(row_pins[r]);
+ }
+
+ // Init col pins 0-16 as input pull-up.
+ for (uint8_t c = 0; c < 17; c++) {
+ gpio_set_pin_input_high(col_pins_gpio[c]);
+ }
+}
+
+bool matrix_scan_custom(matrix_row_t current_matrix[]) {
+ bool changed = false;
+
+ for (uint8_t r = 0; r < MATRIX_ROWS; r++) {
+ gpio_set_pin_output(row_pins[r]);
+ gpio_write_pin_low(row_pins[r]);
+
+ for (volatile int i = 0; i < 20; i++) {}
+
+ matrix_row_t row_val = 0;
+ for (uint8_t c = 0; c < 17; c++) {
+ if (!gpio_read_pin(col_pins_gpio[c])) {
+ row_val |= (1UL << c);
+ }
+ }
+
+ // Col 17 (PA11 = USB D-).
+ col17_claim();
+ for (volatile int i = 0; i < 30; i++) {}
+ if (!gpio_read_pin(COL17_PIN)) {
+ row_val |= (1UL << COL17_IDX);
+ }
+ col17_release();
+
+ gpio_write_pin_high(row_pins[r]);
+
+ if (current_matrix[r] != row_val) {
+ current_matrix[r] = row_val;
+ changed = true;
+ }
+ }
+
+ return changed;
+}
diff --git a/keyboards/metakey/tenet/mcuconf.h b/keyboards/metakey/tenet/mcuconf.h
new file mode 100644
index 000000000000..c08db0fe8353
--- /dev/null
+++ b/keyboards/metakey/tenet/mcuconf.h
@@ -0,0 +1,68 @@
+/*
+ ChibiOS - Copyright (C) 2006..2015 Giovanni Di Sirio
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+#pragma once
+
+#include "nvic.h"
+
+#define HT32F52342_MCUCONF
+
+/*
+ * HAL driver system settings.
+ */
+
+/*
+ * Clock configuration.
+ */
+
+// This configuration:
+// 8 MHz HSE crystal
+// PLL multiplies HSE to 48 MHz core and peripheral clock
+// 48 MHz to UART
+// 48 MHz to USB
+
+#define HT32_CK_HSE_FREQUENCY 8000000UL // 8 MHz
+#define HT32_CKCU_SW CKCU_GCCR_SW_PLL
+#define HT32_PLL_USE_HSE TRUE
+#define HT32_PLL_FBDIV 6 // 8 MHz -> 48 MHz
+#define HT32_PLL_OTDIV 0
+#define HT32_AHB_PRESCALER 1 // 48 MHz -> 48 MHz
+#define HT32_USART_PRESCALER 1 // 48 MHz
+#define HT32_USB_PRESCALER 1 // 48 MHz -> 48 MHz
+// SysTick uses processor clock at 48MHz
+#define HT32_ST_USE_HCLK TRUE
+
+/*
+ * Peripheral driver settings
+ */
+
+#define HT32_SERIAL_USE_USART0 TRUE
+#define HT32_USART0_IRQ_PRIORITY 6
+
+#define HT32_SERIAL_USE_USART1 TRUE
+#define HT32_USART1_IRQ_PRIORITY 7
+
+/*
+ * USB driver settings
+ */
+
+#define HT32_USB_USE_USB0 TRUE
+#define HT32_USB_USB0_IRQ_PRIORITY 5
+
+#define HT32_SPI_USE_SPI0 FALSE
+#define HT32_SPI_USE_SPI1 FALSE
+
+#define HT32_I2C_USE_I2C0 FALSE
+#define HT32_I2C_USE_I2C1 FALSE
diff --git a/keyboards/metakey/tenet/readme.md b/keyboards/metakey/tenet/readme.md
new file mode 100644
index 000000000000..dacf8483059c
--- /dev/null
+++ b/keyboards/metakey/tenet/readme.md
@@ -0,0 +1,41 @@
+# Metakey Tenet 70
+
+
+
+A transformable (regular / southpaw) 70% keyboard with a 256x32 OLED and
+per-key RGB lighting.
+
+* Keyboard Maintainer: [connorgallopo](https://github.com/connorgallopo)
+* Hardware Supported: Metakey Tenet 70 (Holtek HT32F52352)
+* Hardware Availability: [clickclack.io](https://clickclack.io/pages/metakey-tenet)
+
+## How to compile
+
+After setting up your build environment, you can compile the default keymap
+using:
+
+ make metakey/tenet:default
+
+## Installing
+
+The Tenet 70 uses the stock Hexcore IAP bootloader, flashed with `hexcore_flash`
+from [Tenet70-Tools](https://github.com/connorgallopo/Tenet70-Tools) — a fork of
+[AnnePro2-Tools](https://github.com/OpenAnnePro/AnnePro2-Tools).
+
+1. Build the tool: clone [Tenet70-Tools](https://github.com/connorgallopo/Tenet70-Tools)
+ and run `cargo build --release`. The binary is `./target/release/hexcore_flash`.
+2. Put the keyboard into IAP mode by unplugging it, then holding `Esc` while
+ plugging it back in.
+3. Flash the firmware you built (substitute the correct `.bin` if you built a
+ different keymap):
+
+ hexcore_flash --device tenet-70 metakey_tenet_default.bin
+
+`QK_BOOT` (Fn + Esc in the default keymap) also re-enters IAP mode from within QMK.
+
+## Recovery
+
+If a bad build leaves the board unresponsive, the SWD pads at PA13/PA14 allow
+re-flashing with an ST-Link or J-Link.
+
+See the [build environment setup](https://docs.qmk.fm/#/getting_started_build_tools) and the [make instructions](https://docs.qmk.fm/#/getting_started_make_guide) for more information. Brand new to QMK? Start with our [Complete Newbs Guide](https://docs.qmk.fm/#/newbs).
diff --git a/keyboards/metakey/tenet/rules.mk b/keyboards/metakey/tenet/rules.mk
new file mode 100644
index 000000000000..d6ebd6ee327c
--- /dev/null
+++ b/keyboards/metakey/tenet/rules.mk
@@ -0,0 +1,22 @@
+MCU = cortex-m0plus
+ARMV = 6
+USE_FPU = no
+MCU_FAMILY = HT32
+MCU_SERIES = HT32F523xx
+MCU_LDSCRIPT = HT32F52352_TENET
+MCU_STARTUP = ht32f523xx
+
+BOARD = METAKEY_TENET
+
+BOOTLOADER = custom
+PROGRAM_CMD = hexcore_flash --device tenet-70 --boot $(BUILD_DIR)/$(TARGET).bin
+
+RGB_MATRIX_ENABLE = yes
+RGB_MATRIX_DRIVER = ws2812
+WS2812_DRIVER = custom
+
+CUSTOM_MATRIX = lite
+SRC = \
+ matrix.c \
+ ws2812_custom.c \
+ ssd1326.c
diff --git a/keyboards/metakey/tenet/ssd1326.c b/keyboards/metakey/tenet/ssd1326.c
new file mode 100644
index 000000000000..9d5edc558a9d
--- /dev/null
+++ b/keyboards/metakey/tenet/ssd1326.c
@@ -0,0 +1,300 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * SSD1326 OLED driver for the Metakey Tenet 70.
+ *
+ * The main MCU drives the OLED directly over I2C0 (PB0 SDA, PB1 SCL, AFIO
+ * mode 7, open-drain) with PD1 as active-low RST. 7-bit device addr 0x3C.
+ *
+ * chibios-contrib has no I2C LLD for the HT32F523xx family, so this file
+ * drives the peripheral via raw register writes.
+ *
+ * The control byte before each transfer selects command (0x00) or data
+ * (0x40) stream per SSD1326 datasheet.
+ */
+
+#include "ssd1326.h"
+#include "quantum.h"
+#include
+
+/* I2C0_BASE, AFIO_GPIO, AFIO_I2C come from the HT32F523xx CMSIS header. */
+#define I2C0_CR (*(volatile uint32_t *)(I2C0_BASE + 0x000))
+#define I2C0_SR (*(volatile uint32_t *)(I2C0_BASE + 0x00C))
+#define I2C0_SHPGR (*(volatile uint32_t *)(I2C0_BASE + 0x010))
+#define I2C0_SLPGR (*(volatile uint32_t *)(I2C0_BASE + 0x014))
+#define I2C0_DR (*(volatile uint32_t *)(I2C0_BASE + 0x018))
+#define I2C0_TAR (*(volatile uint32_t *)(I2C0_BASE + 0x01C))
+#define I2C0_TOUT (*(volatile uint32_t *)(I2C0_BASE + 0x028))
+
+#define CR_I2CEN (1u << 3)
+#define CR_STOP (1u << 1)
+
+#define SR_TXDE (1u << 17)
+#define SR_RXNACK (1u << 9)
+
+#define CKCU_APBCCR0 (*(volatile uint32_t *)0x4008802CU)
+#define CKCU_I2C0_EN (1u << 0)
+
+#define HT_AFIO_GPBCFGLR (*(volatile uint32_t *)0x40022028U)
+#define HT_AFIO_GPDCFGLR (*(volatile uint32_t *)0x40022038U)
+#define HT_GPIOB_ODR (*(volatile uint32_t *)0x400B2010U)
+
+#define HT_GPIOD_DIRCR (*(volatile uint32_t *)0x400B6000U)
+#define HT_GPIOD_INER (*(volatile uint32_t *)0x400B6004U)
+#define HT_GPIOD_DOUTR (*(volatile uint32_t *)0x400B6020U)
+
+#define PIN_PD1 (1u << 1)
+
+#ifndef OLED_I2C_ADDR_7
+# define OLED_I2C_ADDR_7 0x3C
+#endif
+
+#define CTRL_CMD 0x00
+#define CTRL_DATA 0x40
+
+#define I2C_POLL_TIMEOUT 2000u /* ~200 µs at 48 MHz */
+
+static uint8_t ssd1326_fb[SSD1326_WIDTH * SSD1326_HEIGHT / 8];
+static bool ssd1326_dirty = true;
+
+/* Minimal 5x7 font, chars 0x20..0x5F. */
+static const uint8_t font5x7[][5] = {
+ {0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},
+ {0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14},
+ {0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},
+ {0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},
+ {0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},
+ {0x08,0x2A,0x1C,0x2A,0x08},{0x08,0x08,0x3E,0x08,0x08},
+ {0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},
+ {0x00,0x60,0x60,0x00,0x00},{0x20,0x10,0x08,0x04,0x02},
+ {0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},
+ {0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},
+ {0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},
+ {0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},
+ {0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},
+ {0x00,0x36,0x36,0x00,0x00},{0x00,0x56,0x36,0x00,0x00},
+ {0x00,0x08,0x14,0x22,0x41},{0x14,0x14,0x14,0x14,0x14},
+ {0x41,0x22,0x14,0x08,0x00},{0x02,0x01,0x51,0x09,0x06},
+ {0x32,0x49,0x79,0x41,0x3E},{0x7E,0x11,0x11,0x11,0x7E},
+ {0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
+ {0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},
+ {0x7F,0x09,0x09,0x01,0x01},{0x3E,0x41,0x41,0x51,0x32},
+ {0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},
+ {0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},
+ {0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x04,0x02,0x7F},
+ {0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
+ {0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},
+ {0x7F,0x09,0x19,0x29,0x46},{0x46,0x49,0x49,0x49,0x31},
+ {0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},
+ {0x1F,0x20,0x40,0x20,0x1F},{0x7F,0x20,0x18,0x20,0x7F},
+ {0x63,0x14,0x08,0x14,0x63},{0x03,0x04,0x78,0x04,0x03},
+ {0x61,0x51,0x49,0x45,0x43},{0x00,0x00,0x7F,0x41,0x41},
+ {0x02,0x04,0x08,0x10,0x20},{0x41,0x41,0x7F,0x00,0x00},
+ {0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40},
+};
+
+/* One I2C transaction: START, address, bytes..., STOP.
+ *
+ * HT32 auto-clears CR_STOP once the STOP condition is generated, so the
+ * inter-transaction wait at the top polls CR_STOP (not SR_BUSBUSY — BUSBUSY
+ * only drops after CR_STOP clears, and waiting on it first would timeout).
+ * Writing TAR triggers START + address. Last byte's TXDE wait ensures it
+ * has left the shift register before we raise STOP. */
+static int i2c0_write(uint8_t addr7, const uint8_t *buf, uint16_t len) {
+ uint32_t t;
+
+ t = I2C_POLL_TIMEOUT; while ((I2C0_CR & CR_STOP) && --t) {}
+ if (t == 0) return -1;
+
+ I2C0_TAR = (uint32_t)(addr7 & 0x7Fu);
+
+ for (uint16_t i = 0; i < len; i++) {
+ t = I2C_POLL_TIMEOUT; while (!(I2C0_SR & SR_TXDE) && --t) {}
+ if (t == 0) return -2;
+ if (I2C0_SR & SR_RXNACK) return -3;
+ I2C0_DR = buf[i];
+ }
+
+ t = I2C_POLL_TIMEOUT; while (!(I2C0_SR & SR_TXDE) && --t) {}
+ I2C0_CR |= CR_STOP;
+
+ return 0;
+}
+
+static int oled_cmd(uint8_t c) {
+ uint8_t buf[2] = { CTRL_CMD, c };
+ return i2c0_write(OLED_I2C_ADDR_7, buf, sizeof(buf));
+}
+
+/* SSD1326 init command stream, sent as one I2C transaction under a single
+ * CMD control byte (0x00 = multi-command mode). The controller consumes
+ * params inline per each command's spec. */
+static const uint8_t ssd1326_init_cmds[] = {
+ 0xFD, 0x12, /* unlock command lock */
+ 0xAE, /* display off */
+ 0xA8, 0x1F, /* multiplex ratio = 32 rows */
+ 0xA2, 0x00, /* display offset = 0 */
+ 0xA1, 0x04, /* display start line = 4 */
+ 0xA0, 0x00, /* re-map: default; vertical flip handled in software */
+ 0x81, 0x3D, /* contrast = 0x3D */
+ 0xBE, 0x0F, /* VCOMH deselect */
+ 0xBC, 0x20, /* precharge voltage */
+ 0xB1, 0x11, /* phase length */
+ 0xB3, 0xF1, /* oscillator freq */
+ 0xA4, /* entire display ON (resume from forced on/off) */
+ 0xAF, /* display ON */
+};
+
+void ssd1326_send_command(uint8_t cmd) { oled_cmd(cmd); }
+
+void ssd1326_send_data(const uint8_t *data, uint16_t len) {
+ /* SSD1326 I2C stream: one control byte (0x40) then payload per
+ * transaction. Chunk at 64 bytes to keep the on-stack buffer small. */
+ uint8_t buf[65];
+ buf[0] = CTRL_DATA;
+ while (len > 0) {
+ uint16_t chunk = len > 64 ? 64 : len;
+ memcpy(&buf[1], data, chunk);
+ i2c0_write(OLED_I2C_ADDR_7, buf, chunk + 1);
+ data += chunk;
+ len -= chunk;
+ }
+}
+
+void ssd1326_render(void);
+
+static void i2c0_bus_init(void) {
+ /* Enable I2C0 peripheral clock (APBCCR0 bit 0, per HT32F52352 CKCU). */
+ CKCU_APBCCR0 |= CKCU_I2C0_EN;
+
+ /* PB0 SDA, PB1 SCL → AFIO mode 7 (I2C). Open-drain for both. */
+ HT_AFIO_GPBCFGLR = (HT_AFIO_GPBCFGLR & ~0xFFu) | (AFIO_I2C << 0) | (AFIO_I2C << 4);
+ HT_GPIOB_ODR |= (1u << 0) | (1u << 1);
+
+ /* SCL_freq = PCLK / (SHPGR + SLPGR + 7). PCLK = 48 MHz → ~400 kHz
+ * (SSD1326 fast-mode spec max). */
+ I2C0_SHPGR = 56;
+ I2C0_SLPGR = 56;
+ I2C0_TOUT = 0;
+
+ I2C0_CR = CR_I2CEN;
+}
+
+void ssd1326_init(void) {
+ /* PD1 = RST. Set AFIO to GPIO explicitly (board.c doesn't configure
+ * PD1), then pulse it HIGH → LOW → HIGH. */
+ HT_AFIO_GPDCFGLR = (HT_AFIO_GPDCFGLR & ~(0xFu << 4)) | (AFIO_GPIO << 4);
+ HT_GPIOD_DIRCR |= PIN_PD1;
+ HT_GPIOD_INER &= ~PIN_PD1;
+ HT_GPIOD_DOUTR |= PIN_PD1;
+ wait_ms(1);
+ HT_GPIOD_DOUTR &= ~PIN_PD1;
+ wait_ms(10);
+ HT_GPIOD_DOUTR |= PIN_PD1;
+ wait_ms(10);
+
+ i2c0_bus_init();
+
+ uint8_t init_buf[1 + sizeof(ssd1326_init_cmds)];
+ init_buf[0] = CTRL_CMD;
+ memcpy(&init_buf[1], ssd1326_init_cmds, sizeof(ssd1326_init_cmds));
+ i2c0_write(OLED_I2C_ADDR_7, init_buf, sizeof(init_buf));
+
+ memset(ssd1326_fb, 0, sizeof(ssd1326_fb));
+ ssd1326_dirty = true;
+ ssd1326_render();
+}
+
+void ssd1326_fb_pixel(uint16_t x, uint8_t y, bool on) {
+ if (x >= SSD1326_WIDTH || y >= SSD1326_HEIGHT) return;
+ /* The panel is case-mounted with COM0 at the visual bottom, so y=0 (top)
+ * maps to the last physical row. */
+ y = SSD1326_HEIGHT - 1 - y;
+ uint16_t idx = (y * SSD1326_WIDTH + x) / 8;
+ uint8_t m = 0x80 >> (x % 8);
+ if (on) ssd1326_fb[idx] |= m;
+ else ssd1326_fb[idx] &= ~m;
+ ssd1326_dirty = true;
+}
+
+void ssd1326_fb_char(uint16_t x, uint8_t y, char c, uint8_t scale) {
+ if (c < 0x20 || c > 0x5F) c = '?';
+ const uint8_t *g = font5x7[c - 0x20];
+ for (uint8_t col = 0; col < 5; col++)
+ for (uint8_t row = 0; row < 7; row++)
+ if (g[col] & (1 << row))
+ for (uint8_t sy = 0; sy < scale; sy++)
+ for (uint8_t sx = 0; sx < scale; sx++)
+ ssd1326_fb_pixel(x + col*scale + sx, y + row*scale + sy, true);
+}
+
+void ssd1326_fb_string(uint16_t x, uint8_t y, const char *s, uint8_t scale) {
+ while (*s) { ssd1326_fb_char(x, y, *s, scale); x += 6 * scale; s++; }
+}
+
+void ssd1326_render(void) {
+ if (!ssd1326_dirty) return;
+ ssd1326_dirty = false;
+
+ /* Set column/row window to cover the whole display. SSD1326 packs 2 px
+ * per byte in 4-bit grayscale mode; 256 px wide → 128 column bytes. */
+ oled_cmd(0x15); oled_cmd(0x00); oled_cmd(0x7F); /* col 0..127 (in byte units) */
+ oled_cmd(0x75); oled_cmd(0x00); oled_cmd(0x1F); /* row 0..31 */
+
+ /* Expand each 1-bit source byte into four 4-bit output nibbles (full ON
+ * or OFF — no actual grayscale). Stream in 64-byte chunks behind one
+ * CTRL_DATA control byte. */
+ uint8_t buf[65];
+ buf[0] = CTRL_DATA;
+ uint8_t cnt = 1;
+ for (uint16_t i = 0; i < sizeof(ssd1326_fb); i++) {
+ uint8_t b = ssd1326_fb[i];
+ for (int bit = 7; bit >= 0; bit -= 2) {
+ uint8_t nib = ((b >> bit) & 1) ? 0xF0 : 0x00;
+ nib |= ((b >> (bit - 1)) & 1) ? 0x0F : 0x00;
+ buf[cnt++] = nib;
+ if (cnt == sizeof(buf)) {
+ i2c0_write(OLED_I2C_ADDR_7, buf, cnt);
+ cnt = 1;
+ }
+ }
+ }
+ if (cnt > 1) i2c0_write(OLED_I2C_ADDR_7, buf, cnt);
+}
+
+void ssd1326_render_mono(const uint8_t *buf) {
+ memcpy(ssd1326_fb, buf, sizeof(ssd1326_fb));
+ ssd1326_dirty = true;
+ ssd1326_render();
+}
+
+void ssd1326_clear(void) {
+ memset(ssd1326_fb, 0, sizeof(ssd1326_fb));
+ ssd1326_dirty = true;
+ ssd1326_render();
+}
+
+/* Clear the framebuffer, draw `text` horizontally + vertically centred
+ * (scale 2, falling back to scale 1 when the string won't fit at 2), render. */
+void ssd1326_set_text(const char *text) {
+ memset(ssd1326_fb, 0, sizeof(ssd1326_fb));
+ if (!text || *text == 0) {
+ ssd1326_dirty = true;
+ ssd1326_render();
+ return;
+ }
+
+ uint8_t len = 0;
+ while (text[len] && len < 64) len++;
+
+ uint8_t scale = (len * 12 <= SSD1326_WIDTH) ? 2 : 1;
+ uint16_t text_w = len * 6 * scale;
+ uint8_t text_h = 7 * scale;
+
+ uint16_t x = (text_w < SSD1326_WIDTH) ? (SSD1326_WIDTH - text_w) / 2 : 0;
+ uint8_t y = (text_h < SSD1326_HEIGHT) ? (SSD1326_HEIGHT - text_h) / 2 : 0;
+
+ ssd1326_fb_string(x, y, text, scale);
+ ssd1326_render();
+}
diff --git a/keyboards/metakey/tenet/ssd1326.h b/keyboards/metakey/tenet/ssd1326.h
new file mode 100644
index 000000000000..d7dbaf239a22
--- /dev/null
+++ b/keyboards/metakey/tenet/ssd1326.h
@@ -0,0 +1,24 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+
+// SSD1326 256x32 mono OLED, I2C0 (PB0 SDA / PB1 SCL, AFIO mode 7,
+// PD1 active-low RST). 7-bit device address 0x3C.
+
+#define SSD1326_WIDTH 256
+#define SSD1326_HEIGHT 32
+
+void ssd1326_init(void);
+void ssd1326_send_command(uint8_t cmd);
+void ssd1326_send_data(const uint8_t *data, uint16_t len);
+void ssd1326_clear(void);
+void ssd1326_render(void);
+void ssd1326_render_mono(const uint8_t *framebuf);
+void ssd1326_fb_pixel(uint16_t x, uint8_t y, bool on);
+void ssd1326_fb_char(uint16_t x, uint8_t y, char c, uint8_t scale);
+void ssd1326_fb_string(uint16_t x, uint8_t y, const char *str, uint8_t scale);
+void ssd1326_set_text(const char *text);
diff --git a/keyboards/metakey/tenet/tenet.c b/keyboards/metakey/tenet/tenet.c
new file mode 100644
index 000000000000..916ac23f2a9e
--- /dev/null
+++ b/keyboards/metakey/tenet/tenet.c
@@ -0,0 +1,194 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "tenet.h"
+#include "config.h"
+#include "quantum.h"
+#include "ssd1326.h"
+#include "eeconfig.h"
+#include
+#include
+
+#ifdef RGB_MATRIX_ENABLE
+led_config_t g_led_config = {
+ { // Key Matrix to LED Index (5 rows × 18 cols)
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, NO_LED, 14, 15, 16 },
+ { 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, NO_LED, 31, 32, 33 },
+ { 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, NO_LED, NO_LED, NO_LED, NO_LED },
+ { 48, NO_LED, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, NO_LED, 61, 62, 63 },
+ { 64, 65, 66, NO_LED, 67, NO_LED, 68, 69, 70, NO_LED, NO_LED, NO_LED, NO_LED, NO_LED, NO_LED, 71, 72, 73 },
+ },
+ { // LED Index to Physical Position (placeholder — refine after measuring)
+ {0,0},{12,0},{24,0},{36,0},{48,0},{60,0},{72,0},{84,0},{96,0},{108,0},{120,0},{132,0},{144,0},{162,0}, {192,0},{204,0},{216,0},
+ {0,16},{18,16},{30,16},{42,16},{54,16},{66,16},{78,16},{90,16},{102,16},{114,16},{126,16},{138,16},{150,16},{165,16}, {192,16},{204,16},{216,16},
+ {0,32},{21,32},{33,32},{45,32},{57,32},{69,32},{81,32},{93,32},{105,32},{117,32},{129,32},{141,32},{159,32},{174,32},
+ {0,48},{27,48},{39,48},{51,48},{63,48},{75,48},{87,48},{99,48},{111,48},{123,48},{135,48},{150,48},{168,48}, {192,48},{204,48},{216,48},
+ {0,64},{18,64},{33,64},{72,64},{132,64},{150,64},{168,64}, {192,64},{204,64},{216,64},
+ },
+ { // LED Index to Flag
+ 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
+ 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
+ 4,LED_FLAG_INDICATOR,4,4,4,4,4,4,4,4,4,4,4,4,
+ 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
+ 4,4,4,4,4,4,4,4,4,4,
+ }
+};
+#endif
+
+/* PA15 is the orientation-detect pin. HIGH = regular, LOW = southpaw (pulled
+ * low by the daughterboard's southpaw-side connector). */
+tenet_orientation_t tenet_read_orientation(void) {
+ gpio_set_pin_input_high(A15);
+ wait_us(20);
+ return gpio_read_pin(A15) ? TENET_ORIENTATION_REGULAR : TENET_ORIENTATION_SOUTHPAW;
+}
+
+/* OLED text: stored in the kb EEPROM datablock as a flat byte run.
+ * On read, the first non-printable byte terminates (blank EEPROM reads 0x00
+ * on the transient HT32 backend; real EEPROM reads 0xFF). */
+static char oled_text[TENET_OLED_TEXT_MAX_LEN + 1];
+
+const char *tenet_oled_text_get(void) { return oled_text; }
+
+void tenet_oled_text_set(const char *text) {
+ uint8_t i;
+ for (i = 0; i < TENET_OLED_TEXT_MAX_LEN && text[i]; i++) {
+ char c = text[i];
+ if (c >= 'a' && c <= 'z') c -= 0x20;
+ if (c < 0x20 || c > 0x5F) c = ' '; /* font covers 0x20..0x5F */
+ oled_text[i] = c;
+ }
+ oled_text[i] = '\0';
+
+ uint8_t eebuf[TENET_OLED_TEXT_MAX_LEN];
+ memset(eebuf, 0xFF, sizeof(eebuf));
+ memcpy(eebuf, oled_text, i);
+ eeconfig_update_kb_datablock(eebuf, 0, sizeof(eebuf));
+
+ ssd1326_set_text(oled_text);
+}
+
+static void load_text_from_eeprom_or_default(void) {
+ uint8_t eebuf[TENET_OLED_TEXT_MAX_LEN];
+ eeconfig_read_kb_datablock(eebuf, 0, sizeof(eebuf));
+
+ if (eebuf[0] < 0x20 || eebuf[0] > 0x7E) {
+ strncpy(oled_text, TENET_OLED_TEXT_DEFAULT, TENET_OLED_TEXT_MAX_LEN);
+ oled_text[TENET_OLED_TEXT_MAX_LEN] = '\0';
+ return;
+ }
+ uint8_t i;
+ for (i = 0; i < TENET_OLED_TEXT_MAX_LEN; i++) {
+ uint8_t c = eebuf[i];
+ if (c < 0x20 || c > 0x7E) break;
+ oled_text[i] = (char)c;
+ }
+ oled_text[i] = '\0';
+}
+
+void keyboard_post_init_kb(void) {
+ ssd1326_init();
+ load_text_from_eeprom_or_default();
+ ssd1326_set_text(oled_text);
+ keyboard_post_init_user();
+}
+
+/* Hexcore IAP bootloader checks a RAM magic word on reset and stays in IAP
+ * mode if it matches. Same convention as Anne Pro 2. */
+void bootloader_jump(void) {
+ *((volatile uint32_t *)0x20001ffc) = 0x0000fab2;
+ __disable_irq();
+ NVIC_SystemReset();
+}
+
+/* -------- Edit mode (KC_OLED_EDIT) -------- */
+
+static bool edit_active = false;
+static char edit_buf[TENET_OLED_TEXT_MAX_LEN + 1];
+static uint8_t edit_len = 0;
+
+static void edit_enter(void) {
+ edit_active = true;
+ edit_len = 0;
+ edit_buf[0] = '\0';
+ ssd1326_set_text("EDIT_");
+}
+
+static void edit_repaint(void) {
+ char display[TENET_OLED_TEXT_MAX_LEN + 2];
+ memcpy(display, edit_buf, edit_len);
+ display[edit_len] = '_';
+ display[edit_len + 1] = '\0';
+ ssd1326_set_text(display);
+}
+
+static void edit_commit_and_exit(void) {
+ edit_buf[edit_len] = '\0';
+ tenet_oled_text_set(edit_buf);
+ edit_active = false;
+}
+
+static void edit_cancel(void) {
+ edit_active = false;
+ ssd1326_set_text(oled_text);
+}
+
+static char keycode_to_ascii(uint16_t keycode, uint8_t mods) {
+ bool shift = mods & (MOD_BIT(KC_LSFT) | MOD_BIT(KC_RSFT));
+ if (keycode >= KC_A && keycode <= KC_Z) return 'A' + (keycode - KC_A); /* uppercase always */
+ if (keycode >= KC_1 && keycode <= KC_9) return shift ? "!@#$%^&*("[keycode - KC_1] : '1' + (keycode - KC_1);
+ if (keycode == KC_0) return shift ? ')' : '0';
+ if (keycode == KC_SPC) return ' ';
+ if (keycode == KC_MINS) return shift ? '_' : '-';
+ if (keycode == KC_EQL) return shift ? '+' : '=';
+ if (keycode == KC_DOT) return shift ? '>' : '.';
+ if (keycode == KC_COMM) return shift ? '<' : ',';
+ if (keycode == KC_SLSH) return shift ? '?' : '/';
+ return 0;
+}
+
+static bool oled_l_held = false;
+static bool oled_r_held = false;
+
+bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
+ /* 3-key recovery chord: OLED-L + OLED-R held, then ESC → enter IAP. */
+ if (keycode == KC_OLED_L) { oled_l_held = record->event.pressed; return false; }
+ if (keycode == KC_OLED_R) { oled_r_held = record->event.pressed; return false; }
+ if (record->event.pressed && keycode == KC_ESC && oled_l_held && oled_r_held) {
+ bootloader_jump();
+ return false;
+ }
+
+ if (keycode == KC_OLED_EDIT) {
+ if (record->event.pressed) {
+ if (edit_active) edit_cancel();
+ else edit_enter();
+ }
+ return false;
+ }
+
+ if (edit_active) {
+ /* Swallow press AND release while in edit mode so no stray
+ * characters leak to the host. */
+ if (record->event.pressed) {
+ if (keycode == KC_ENTER || keycode == KC_PENT) {
+ edit_commit_and_exit();
+ } else if (keycode == KC_BSPC) {
+ if (edit_len > 0) edit_len--;
+ edit_repaint();
+ } else if (keycode == KC_ESC) {
+ edit_cancel();
+ } else {
+ char c = keycode_to_ascii(keycode, get_mods());
+ if (c && edit_len < TENET_OLED_TEXT_MAX_LEN) {
+ if (c >= 'a' && c <= 'z') c -= 0x20;
+ edit_buf[edit_len++] = c;
+ edit_repaint();
+ }
+ }
+ }
+ return false;
+ }
+
+ return process_record_user(keycode, record);
+}
diff --git a/keyboards/metakey/tenet/tenet.h b/keyboards/metakey/tenet/tenet.h
new file mode 100644
index 000000000000..5ebafd909acf
--- /dev/null
+++ b/keyboards/metakey/tenet/tenet.h
@@ -0,0 +1,32 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "quantum.h"
+
+#define RGB_MATRIX_LED_COUNT 76
+
+typedef enum {
+ TENET_ORIENTATION_REGULAR = 0,
+ TENET_ORIENTATION_SOUTHPAW = 1,
+} tenet_orientation_t;
+
+// Reads PA15: HIGH = regular, LOW = southpaw.
+tenet_orientation_t tenet_read_orientation(void);
+
+// OLED custom-text API. `text` must be null-terminated; anything past
+// TENET_OLED_TEXT_MAX_LEN-1 bytes is truncated. Persisted in the keyboard
+// EEPROM datablock.
+#define TENET_OLED_TEXT_MAX_LEN 16
+#define TENET_OLED_TEXT_DEFAULT "TENET QMK"
+
+const char *tenet_oled_text_get(void);
+void tenet_oled_text_set(const char *text);
+
+enum tenet_keycodes {
+ KC_OLED_EDIT = QK_KB_0,
+ KC_OLED_L, // left OLED key — holding as part of a chord
+ KC_OLED_R, // right OLED key — holding as part of a chord
+ TENET_KC_SAFE_RANGE,
+};
diff --git a/keyboards/metakey/tenet/ws2812_custom.c b/keyboards/metakey/tenet/ws2812_custom.c
new file mode 100644
index 000000000000..57a01be4af0d
--- /dev/null
+++ b/keyboards/metakey/tenet/ws2812_custom.c
@@ -0,0 +1,221 @@
+// Copyright 2026 Connor Gallopo (@connorgallopo)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * LED driver for the Metakey Tenet 70.
+ *
+ * The 76 per-key RGBs are driven by two Holtek HT32F523xx sub-MCUs (marked
+ * "BFWH AAD7") that live on a shared SPI1 bus:
+ * - mainboard sub-MCU: CS = PA3, drives 64 main-cluster LEDs
+ * - daughterboard sub-MCU: CS = PA5, drives 12 nav-cluster LEDs
+ *
+ * Pin map:
+ * PA0/PA1/PA2 = SPI1 NSS/SCK/MOSI (AFIO mode 5)
+ * PA3 = CS for mainboard sub-MCU
+ * PA4 = shared RESET, LOW at boot, HIGH to bring the chips up
+ * PA5 = CS for daughterboard sub-MCU
+ *
+ * Init: 0xA0 config-page writes then 216 × 0xA4 register writes per chip,
+ * matching stock FUN_00010478.
+ *
+ * Frame: one 218-byte write per chip = {0xA2, 0x00, then 216 colour bytes
+ * at positions given by the tenet_led_map lookup table that was extracted
+ * from stock flash at file offset 0xbebc}. Matches stock FUN_000102f4.
+ */
+
+#include "ws2812.h"
+#include "rgb_matrix.h"
+#include
+
+#define SPI1_CR0 (*(volatile uint32_t *)0x40044000U)
+#define SPI1_CR1 (*(volatile uint32_t *)0x40044004U)
+#define SPI1_IER (*(volatile uint32_t *)0x40044008U)
+#define SPI1_CPR (*(volatile uint32_t *)0x4004400CU)
+#define SPI1_DR (*(volatile uint32_t *)0x40044010U)
+#define SPI1_SR (*(volatile uint32_t *)0x40044014U)
+#define SPI1_FCR (*(volatile uint32_t *)0x40044018U)
+
+#define SPI_CR0_SPIEN (1u << 0)
+#define SPI_CR0_SELOEN (1u << 3) /* NSS output enable */
+#define SPI_CR1_MODE (1u << 14) /* master */
+#define SPI_CR1_FORMAT_MODE3 (0x5u << 8) /* CPOL=1 CPHA=1 */
+#define SPI_SR_TXE (1u << 1)
+#define SPI_SR_BUSY (1u << 8)
+
+/* CR1 = 0x4508, CPR = 2 → 8 MHz SCK on 48 MHz PCLK. */
+#define STOCK_CR1 (SPI_CR1_MODE | SPI_CR1_FORMAT_MODE3 | 0x08u)
+#define STOCK_CPR 2u
+
+#define CKCU_APBCCR0 (*(volatile uint32_t *)0x4008802CU)
+#define CKCU_SPI1_EN (1u << 5)
+
+#define HT_AFIO_GPACFGLR (*(volatile uint32_t *)0x40022020U)
+
+/* Port A GPIO register offsets per HT32F523xx TRM. DOUTR is at 0x020
+ * (not 0x014, which is DRVR, drive current; not 0x028, which is RR,
+ * write-1-to-clear). */
+#define HT_GPIOA_DIRCR (*(volatile uint32_t *)0x400B0000U)
+#define HT_GPIOA_INER (*(volatile uint32_t *)0x400B0004U)
+#define HT_GPIOA_DOUTR (*(volatile uint32_t *)0x400B0020U)
+
+#define PIN_CS1 (1u << 3) /* PA3 */
+#define PIN_EN (1u << 4) /* PA4 */
+#define PIN_CS2 (1u << 5) /* PA5 */
+
+/* LED position lookup, extracted from stock Tenet firmware at flash offset
+ * 0xbebc. Each cell holds three signed int16 byte offsets into the 216-byte
+ * colour payload: (R_pos, G_pos, B_pos). -1 means no LED at this cell.
+ *
+ * Cols 0-14 address the mainboard sub-MCU; cols 15-17 address the
+ * daughterboard sub-MCU. Both accept the same byte stream — only the CS
+ * line distinguishes which register file receives it. */
+static const int16_t tenet_led_map[5][18][3] = {
+ { { 12, 13, 14},{ 30, 31, 32},{ 48, 49, 50},{ 66, 67, 68},
+ { 84, 85, 86},{ 102,103,104},{ 120,121,122},{ 138,139,140},
+ { 156,157,158},{ 174,175,176},{ 192,193,194},{ 210,211,212},
+ { 141,142,143},{ 159,160,161},{ 177,178,179},
+ { 17, 16, 15},{ 53, 52, 51},{ 89, 88, 87} },
+ { { 9, 10, 11},{ 27, 28, 29},{ 45, 46, 47},{ 63, 64, 65},
+ { 81, 82, 83},{ 99,100,101},{ 117,118,119},{ 135,136,137},
+ { 153,154,155},{ 171,172,173},{ 189,190,191},{ 207,208,209},
+ { 213,214,215},{ 195,196,197},{ -1, -1, -1},
+ { 14, 13, 12},{ 50, 49, 48},{ 86, 85, 84} },
+ { { 2, 1, 0},{ 20, 19, 18},{ 38, 37, 36},{ 56, 55, 54},
+ { 74, 73, 72},{ 92, 91, 90},{ 110,109,108},{ 128,127,126},
+ { 146,145,144},{ 164,163,162},{ 182,181,180},{ 200,199,198},
+ { 206,205,204},{ -1, -1, -1},{ -1, -1, -1},
+ { -1, -1, -1},{ -1, -1, -1},{ -1, -1, -1} },
+ { { 5, 4, 3},{ 23, 22, 21},{ 41, 40, 39},{ 59, 58, 57},
+ { 77, 76, 75},{ 95, 94, 93},{ 113,112,111},{ 131,130,129},
+ { 149,148,147},{ 167,166,165},{ 185,184,183},{ 203,202,201},
+ { 170,169,168},{ 188,187,186},{ -1, -1, -1},
+ { 21, 22, 23},{ 57, 58, 59},{ 93, 94, 95} },
+ { { 8, 7, 6},{ 26, 25, 24},{ 44, 43, 42},{ -1, -1, -1},
+ { 80, 79, 78},{ 98, 97, 96},{ 116,115,114},{ 134,133,132},
+ { 152,151,150},{ -1, -1, -1},{ -1, -1, -1},{ -1, -1, -1},
+ { -1, -1, -1},{ -1, -1, -1},{ -1, -1, -1},
+ { 24, 25, 26},{ 60, 61, 62},{ 96, 97, 98} },
+};
+
+#define LED_FRAME_SIZE 218 /* 0xA2 + 0x00 + 216 color bytes */
+
+/* One 218-byte frame per sub-MCU, filled on every flush. */
+static uint8_t chip_frame[2][LED_FRAME_SIZE];
+
+ws2812_led_t ws2812_leds[WS2812_LED_COUNT];
+
+/* Send a 3-byte (opcode, addr, data) register write with CS toggling. */
+static void chip_write3(uint32_t cs_mask, uint8_t opcode, uint8_t addr, uint8_t data) {
+ uint32_t t;
+
+ HT_GPIOA_DOUTR &= ~cs_mask;
+
+ t = 2000; while (!(SPI1_SR & SPI_SR_TXE) && --t) {}
+ SPI1_DR = opcode;
+ t = 2000; while (!(SPI1_SR & SPI_SR_TXE) && --t) {}
+ SPI1_DR = addr;
+ t = 2000; while (!(SPI1_SR & SPI_SR_TXE) && --t) {}
+ SPI1_DR = data;
+
+ t = 2000; while ((SPI1_SR & SPI_SR_BUSY) && --t) {}
+
+ HT_GPIOA_DOUTR |= cs_mask;
+}
+
+/* ~500 µs busy-wait at 48 MHz — stock inserts this between the first few
+ * init writes; the sub-MCU firmware needs it. */
+static void chip_delay(void) {
+ for (volatile uint32_t i = 0x5DC0; i > 0; i--) {}
+}
+
+/* Sub-MCU init: three 0xA0 config-page writes (registers 0x2F, 0x00, 0x01)
+ * followed by 216 × 0xA4 auto-increment writes that zero the PWM register
+ * bank. Must run on each chip before the first 0xA2 colour frame. */
+static void chip_init(uint32_t cs_mask) {
+ chip_delay();
+ chip_write3(cs_mask, 0xA0, 0x2F, 0xAE);
+ chip_delay();
+ chip_write3(cs_mask, 0xA0, 0x00, 0xB1);
+ chip_delay();
+ chip_write3(cs_mask, 0xA0, 0x01, 0xFF);
+ for (uint8_t r = 0; r < 0xD8; r++) {
+ chip_write3(cs_mask, 0xA4, r, 0xFF);
+ }
+}
+
+void ws2812_init(void) {
+ /* PA3/PA4/PA5 as GPIO outputs. CS lines idle HIGH, PA4 (reset) starts LOW. */
+ HT_GPIOA_DIRCR |= (PIN_CS1 | PIN_EN | PIN_CS2);
+ HT_GPIOA_INER &= ~(PIN_CS1 | PIN_EN | PIN_CS2);
+ HT_GPIOA_DOUTR = (HT_GPIOA_DOUTR & ~(PIN_CS1 | PIN_EN | PIN_CS2))
+ | PIN_CS1 | PIN_CS2;
+
+ /* Enable SPI1 clock, route PA0/PA1/PA2 to the SPI1 alt function (mode 5). */
+ CKCU_APBCCR0 |= CKCU_SPI1_EN;
+ HT_AFIO_GPACFGLR = (HT_AFIO_GPACFGLR & ~0xFFFu)
+ | (5u << 0) | (5u << 4) | (5u << 8);
+
+ SPI1_CR0 = 0;
+ SPI1_IER = 0;
+ SPI1_FCR = 0;
+ SPI1_CR1 = STOCK_CR1;
+ SPI1_CPR = STOCK_CPR;
+ SPI1_CR0 = SPI_CR0_SPIEN | SPI_CR0_SELOEN;
+
+ /* Raise PA4 to release both sub-MCUs from reset, then init each. */
+ HT_GPIOA_DOUTR |= PIN_EN;
+ chip_delay();
+ chip_init(PIN_CS1);
+ chip_init(PIN_CS2);
+}
+
+void ws2812_set_color(int index, uint8_t red, uint8_t green, uint8_t blue) {
+ ws2812_leds[index].r = red;
+ ws2812_leds[index].g = green;
+ ws2812_leds[index].b = blue;
+}
+
+void ws2812_set_color_all(uint8_t red, uint8_t green, uint8_t blue) {
+ for (int i = 0; i < WS2812_LED_COUNT; i++) ws2812_set_color(i, red, green, blue);
+}
+
+/* Polled SPI1 send: assert cs_mask, clock out n bytes (TXE handshake per
+ * byte), wait for the shift register to drain, deassert. */
+static void spi1_send_frame(uint32_t cs_mask, const uint8_t *buf, uint16_t n) {
+ HT_GPIOA_DOUTR &= ~cs_mask;
+ for (uint16_t i = 0; i < n; i++) {
+ uint32_t t = 2000;
+ while (!(SPI1_SR & SPI_SR_TXE) && --t) {}
+ SPI1_DR = buf[i];
+ }
+ uint32_t t = 20000;
+ while ((SPI1_SR & SPI_SR_BUSY) && --t) {}
+ HT_GPIOA_DOUTR |= cs_mask;
+}
+
+void ws2812_flush(void) {
+ /* Frame = [0xA2, 0x00, <216 colour bytes>]. Re-init header each flush,
+ * then zero the data region and fill it from the LED state. */
+ chip_frame[0][0] = chip_frame[1][0] = 0xA2;
+ chip_frame[0][1] = chip_frame[1][1] = 0x00;
+ memset(&chip_frame[0][2], 0, 216);
+ memset(&chip_frame[1][2], 0, 216);
+
+ for (uint8_t row = 0; row < 5; row++) {
+ for (uint8_t col = 0; col < 18; col++) {
+ uint8_t led = g_led_config.matrix_co[row][col];
+ if (led == NO_LED || led >= WS2812_LED_COUNT) continue;
+ int16_t rp = tenet_led_map[row][col][0];
+ if (rp < 0) continue;
+ int16_t gp = tenet_led_map[row][col][1];
+ int16_t bp = tenet_led_map[row][col][2];
+ uint8_t chip = (col < 15) ? 0 : 1;
+ chip_frame[chip][2 + rp] = ws2812_leds[led].r;
+ chip_frame[chip][2 + gp] = ws2812_leds[led].g;
+ chip_frame[chip][2 + bp] = ws2812_leds[led].b;
+ }
+ }
+
+ spi1_send_frame(PIN_CS1, chip_frame[0], LED_FRAME_SIZE);
+ spi1_send_frame(PIN_CS2, chip_frame[1], LED_FRAME_SIZE);
+}