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 + +![Metakey Tenet 70](https://clickclack.io/cdn/shop/files/blackside.jpg?v=1649246960&width=1080) + +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); +}