Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Build ZMK firmware

on: [push, pull_request, workflow_dispatch]

jobs:
build:
# Pinned to v0.3.0 so the workflow's toolchain matches the manifest in
# zmk-config/west.yml. Do not switch to @main without also moving the manifest.
uses: zmkfirmware/zmk/.github/workflows/build-user-config.yml@v0.3.0
with:
config_path: zmk-config
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@
hardware/kicad_5.99/.xdp-anaphase.pro-tgFd8n
zmk-config/boards/arm/anaphase/widgets/temp
.vscode/settings.json

# macOS / KiCad noise
.DS_Store
hardware/kicad_5.99/*.lck

# west workspace (created by `west init -l zmk-config` for local builds)
.west/
zmk/
zephyr/
modules/
tools/
bootloader/
build/
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
# anaphase wireless split keyboard

anaphase is a Bluetooth/USB split ergonomic keyboard with a 3D printed case and custom PCB base.

## Firmware

ZMK config lives in `zmk-config/`, defining `anaphase_left` (BLE central) and `anaphase_right`
as full boards (E73-2G4M08S1C nRF52840 modules, not a shield). Pinned to **ZMK v0.3.0**
(Zephyr 3.5, LVGL 8.3.7). CI builds both halves via the shared ZMK user-config workflow;
grab `anaphase_left-zmk.uf2` / `anaphase_right-zmk.uf2` from the Actions artifacts and
drag onto the Adafruit bootloader mass-storage device (double-tap reset).

### Rollback (last pre-migration build, 2022-03-05)

**Primary: `archive/2022 dumps/CURRENT_{left,right}.UF2`** — bootloader readbacks of the
actual firmware both halves ran for four years, captured 2026-08-09 before the v0.3.0
migration was flashed. Verified: contiguous 0x1000..0xEA000, valid vector tables, includes
the NVS storage region (BLE bonds as of capture). To restore: double-tap reset on the
affected half, drag the matching file onto the mass-storage volume. The family ID differs
from newly-built uf2s (0x239A0029 board-specific vs 0xADA52840 generic) — bootloader 0.6.2
accepts both, verified against its source.

Secondary (reconstruction from source, if the dumps are ever lost):

- zmk-config: commit `9e7d2f7`
- zmkfirmware/zmk: commit `f1b5dc408144e7f334bad59da6e9ac788a56e0c7` (main @ 2022-03-05)
- which pins zephyr `v2.5.0+zmk-fixes` (LVGL 7.6.1)
- build container: `zmkfirmware/zmk-build-arm:2.5` (local Docker build; modern CI images
will not build this tree)

Bootloader on both halves: Adafruit nRF52 UF2 bootloader 0.6.2, pca10056 build
(`archive/2022 dumps/INFO_UF2_*.TXT`). App flashing never touches it.
Binary file added archive/2022 dumps/CURRENT_left.UF2
Binary file not shown.
Binary file added archive/2022 dumps/CURRENT_right.UF2
Binary file not shown.
4 changes: 4 additions & 0 deletions archive/2022 dumps/INDEX_left.HTM
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!doctype html>
<html><body><script>
location.replace("https://www.nordicsemi.com/Software-and-Tools/Development-Kits/nRF52840-DK");
</script></body></html>
4 changes: 4 additions & 0 deletions archive/2022 dumps/INDEX_right.HTM
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!doctype html>
<html><body><script>
location.replace("https://www.nordicsemi.com/Software-and-Tools/Development-Kits/nRF52840-DK");
</script></body></html>
5 changes: 5 additions & 0 deletions archive/2022 dumps/INFO_UF2_left.TXT
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
UF2 Bootloader 0.6.2 lib/nrfx (v2.0.0) lib/tinyusb (0.10.1-293-gaf8e5a90) lib/uf2 (remotes/origin/configupdate-9-gadbb8c7)
Model: Nordic nRF52840 DK
Board-ID: nRF52840-pca10056-v1
SoftDevice: not found
Date: Sep 10 2021
5 changes: 5 additions & 0 deletions archive/2022 dumps/INFO_UF2_right.TXT
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
UF2 Bootloader 0.6.2 lib/nrfx (v2.0.0) lib/tinyusb (0.10.1-293-gaf8e5a90) lib/uf2 (remotes/origin/configupdate-9-gadbb8c7)
Model: Nordic nRF52840 DK
Board-ID: nRF52840-pca10056-v1
SoftDevice: not found
Date: Sep 10 2021
5 changes: 5 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Build matrix for the shared ZMK user-config workflow.
# Board IDs = the *_defconfig basenames under zmk-config/boards/arm/anaphase/.
include:
- board: anaphase_left
- board: anaphase_right
30 changes: 10 additions & 20 deletions zmk-config/boards/arm/anaphase/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
# SPDX-License-Identifier: MIT

zephyr_library()
zephyr_library_sources(board.c)
zephyr_library_include_directories(${ZEPHYR_BASE}/drivers)

# -b argument must match &code_partition address in numble.dts
set_property(GLOBAL APPEND PROPERTY extra_post_build_commands
COMMAND ${PYTHON_EXECUTABLE} ${ZEPHYR_BASE}/../tools/uf2/utils/uf2conv.py
-c
-b 0x1000
-f 0xADA52840
-o ${PROJECT_BINARY_DIR}/${CONFIG_KERNEL_BIN_NAME}.uf2
${PROJECT_BINARY_DIR}/${CONFIG_KERNEL_BIN_NAME}.bin
)

target_sources_ifdef(CONFIG_CUSTOM_WIDGET_BATTERY_STATUS app PRIVATE widgets/battery_status.c)
target_sources_ifdef(CONFIG_CUSTOM_WIDGET_OUTPUT_STATUS app PRIVATE widgets/output_status.c)
target_sources_ifdef(CONFIG_CUSTOM_WIDGET_LAYER_STATUS app PRIVATE widgets/layer_status.c)
target_sources_ifdef(CONFIG_CUSTOM_WIDGET_LAYER_STATUS app PRIVATE widgets/wpm_status.c)
# UF2 output is now produced natively via CONFIG_BUILD_OUTPUT_UF2 (family ID
# and base address come from SOC_NRF52840_QIAA + the code_partition chosen),
# replacing the old hand-rolled uf2conv.py post-build hook.
# DC/DC setup moved from board.c to Kconfig (SOC_DCDC_NRF52X_HV).

if(CONFIG_ZMK_DISPLAY)
target_sources_ifdef(CONFIG_CUSTOM_WIDGET_BATTERY_STATUS app PRIVATE widgets/battery_status.c)
target_sources_ifdef(CONFIG_CUSTOM_WIDGET_OUTPUT_STATUS app PRIVATE widgets/output_status.c)
target_sources_ifdef(CONFIG_CUSTOM_WIDGET_LAYER_STATUS app PRIVATE widgets/layer_status.c)
endif()

zephyr_library()
zephyr_library_include_directories(${ZEPHYR_LVGL_MODULE_DIR})
zephyr_library_include_directories(${ZEPHYR_BASE}/lib/gui/lvgl/)
zephyr_library_sources_ifdef(CONFIG_ZMK_DISPLAY custom_status_screen.c)
zephyr_library_sources(${ZEPHYR_BASE}/misc/empty_file.c)
zephyr_library_include_directories(${CMAKE_SOURCE_DIR}/include)
zephyr_library_include_directories(${ZEPHYR_BASE}/drivers)
16 changes: 15 additions & 1 deletion zmk-config/boards/arm/anaphase/Kconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# SPDX-License-Identifier: MIT

# Main (REG1) DC/DC converter. Deliberately default n: the original 2021
# config never enabled this (the old `depends on BOARD_ANAPHASE` referenced a
# nonexistent symbol, so it was unselectable), the shipped firmware was
# power-validated in that state, and the E73 module does not bond out
# DCC/DEC4 so buck-mode support for this stage is unverified.
config BOARD_ENABLE_DCDC
bool "Enable DCDC mode"
select SOC_DCDC_NRF52X
default n
depends on (BOARD_ANAPHASE_LEFT || BOARD_ANAPHASE_RIGHT)

# High-voltage (REG0) DC/DC converter, LiPo -> VDDH -> 1V8. Default y:
# this replaces the removed board.c, which force-enabled it on every boot
# since 2021. The DCCH inductor (L1/L2, 10uH) is fitted on the PCB.
config BOARD_ENABLE_DCDC_HV
bool "High voltage DCDC converter"
select SOC_DCDC_NRF52X_HV
default y
depends on BOARD_ANAPHASE
depends on (BOARD_ANAPHASE_LEFT || BOARD_ANAPHASE_RIGHT)
74 changes: 37 additions & 37 deletions zmk-config/boards/arm/anaphase/Kconfig.defconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ if BOARD_ANAPHASE_LEFT
config ZMK_KEYBOARD_NAME
default "anaphase"

# Left half is the BLE central. (Old-name alias ZMK_SPLIT_BLE_ROLE_CENTRAL
# still exists at v0.3.0 but new configs should set this symbol.)
config ZMK_SPLIT_ROLE_CENTRAL
default y

# USB HID output is central-only at v0.3.0 (ZMK_USB now depends on
# ZMK_SPLIT_ROLE_CENTRAL). The right half keeps the bare USB device stack
# for charge detection — see anaphase_right_defconfig.
config ZMK_USB
default y

endif # BOARD_ANAPHASE_LEFT

if BOARD_ANAPHASE_RIGHT
Expand All @@ -20,7 +31,7 @@ config BOARD
default "anaphase"

config ZMK_SPLIT
default y
default y

if USB

Expand All @@ -38,31 +49,27 @@ config BT_CTLR
config ZMK_BLE
default y

config ZMK_USB
default y

# Battery is sensed on VDDH (LiPo direct to VDDH, SAADC internal VDDHDIV5).
config ZMK_BATTERY_NRF_VDDH
default y

config ZMK_DISPLAY
select LVGL_FONT_UNSCII_8
select LVGL_USE_LABEL
select ZMK_WPM
select LV_FONT_UNSCII_8
select LV_USE_LABEL

choice ZMK_DISPLAY_STATUS_SCREEN
default ZMK_DISPLAY_STATUS_SCREEN_CUSTOM
default ZMK_DISPLAY_STATUS_SCREEN_CUSTOM

endchoice

if ZMK_DISPLAY

choice LVGL_THEME_DEFAULT_FONT_NORMAL
default LVGL_THEME_DEFAULT_FONT_NORMAL_UNSCII_8

endchoice
if ZMK_DISPLAY

choice LVGL_THEME_DEFAULT_FONT_SMALL
default LVGL_THEME_DEFAULT_FONT_SMALL_UNSCII_8
# UNSCII-8 everywhere: the 96x48 text layout only fits with this font.
# (ZMK's own default is Montserrat, which does not.)
# The theme "small" font counterpart is set in the *_defconfig files —
# a choice-default override here loses to ZMK's own default.
choice LV_FONT_DEFAULT
default LV_FONT_DEFAULT_UNSCII_8

endchoice

Expand All @@ -72,38 +79,31 @@ config SPI
config SSD1306
default y

config LVGL_HOR_RES_MAX
default 96

config LVGL_VER_RES_MAX
default 48

config LVGL_VDB_SIZE
# Panel resolution now comes from the width/height DT properties on the
# ssd1306 node (the old LVGL_HOR_RES_MAX/VER_RES_MAX symbols are gone).
config LV_Z_VDB_SIZE
default 100

config LVGL_DPI
config LV_DPI_DEF
default 50

config LVGL_BITS_PER_PIXEL
config LV_Z_BITS_PER_PIXEL
default 1

choice LVGL_COLOR_DEPTH
default LVGL_COLOR_DEPTH_1
choice LV_COLOR_DEPTH
default LV_COLOR_DEPTH_1

endchoice

endif # ZMK_DISPLAY

menuconfig CUSTOM_WIDGET_BATTERY_STATUS
bool "custom battery status widget"

menuconfig CUSTOM_WIDGET_OUTPUT_STATUS
bool "custom output status widget"
config CUSTOM_WIDGET_BATTERY_STATUS
bool "custom battery status widget"

menuconfig CUSTOM_WIDGET_LAYER_STATUS
bool "custom layer status widget"
config CUSTOM_WIDGET_OUTPUT_STATUS
bool "custom output status widget"

menuconfig CUSTOM_WIDGET_WPM_STATUS
bool "custom wpm status widget"
config CUSTOM_WIDGET_LAYER_STATUS
bool "custom layer status widget"

endif # BOARD_ANAPHASE_LEFT || BOARD_ANAPHASE_RIGHT
endif # BOARD_ANAPHASE_LEFT || BOARD_ANAPHASE_RIGHT
29 changes: 29 additions & 0 deletions zmk-config/boards/arm/anaphase/anaphase-pinctrl.dtsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2021
*
* SPDX-License-Identifier: MIT
*/

&pinctrl {
/*
* OLED SPI. Write-only bus: the SSD1306 has no MISO, so no
* SPIM_MISO psel is assigned (the pre-Zephyr-3.0 config sacrificed
* P0.11 to a dummy miso-pin the old binding required).
* CS (P0.07) is driven as a GPIO via cs-gpios on &spi0, and
* D/C (P0.04) / RESET (P0.12) belong to the display node.
*/
spi0_default: spi0_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 8)>,
<NRF_PSEL(SPIM_MOSI, 0, 6)>;
};
};

spi0_sleep: spi0_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 8)>,
<NRF_PSEL(SPIM_MOSI, 0, 6)>;
low-power-enable;
};
};
};
34 changes: 25 additions & 9 deletions zmk-config/boards/arm/anaphase/anaphase.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include <dt-bindings/zmk/matrix_transform.h>
#include "anaphase-pinctrl.dtsi"

/ {
model = "anaphase";
Expand All @@ -16,11 +17,19 @@
zephyr,code-partition = &code_partition;
zephyr,sram = &sram0;
zephyr,flash = &flash0;
// ZMK >= mid-2022 finds the panel via this chosen node; before that it
// was looked up by device name ("DISPLAY"). Without this line the
// display subsystem fails to compile.
zephyr,display = &oled;
zmk,kscan = &kscan0;
zmk,matrix_transform = &transform;
zmk,battery = &vbatt;
};
#define K(n) RC(0,n)
// columns/rows here describe the *keymap grid*, not the kscan (which is
// direct-wired, 1 row x 42 "columns"). This works because direct kscan
// always reports row 0, so the lookup index (row*columns)+col == col.
// Load-bearing coincidence — leave as is.
transform: transform {
compatible = "zmk,matrix-transform";
columns = <12>;
Expand All @@ -35,29 +44,36 @@

vbatt: vbatt {
compatible = "zmk,battery-nrf-vddh";
label = "BATTERY";
};
};

&spi0 {
compatible = "nordic,nrf-spim";
status = "okay";
sck-pin = <8>;
mosi-pin = <6>;
pinctrl-0 = <&spi0_default>;
pinctrl-1 = <&spi0_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 7 GPIO_ACTIVE_LOW>;
clock-frequency = <8000000>;
miso-pull-down;
// unused pin, but needed by SPI definition
miso-pin = <11>;

oled: ssd1306@0 {
compatible = "solomon,ssd1306fb";
reg = <0>;
label = "DISPLAY";
data_cmd-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
spi-max-frequency = <8000000>;
width = <96>;
// Non-standard panel, 64x48 *visible*: multiplex-ratio/prechargep/
// com-invdir/segment-remap below were tuned by trial in 2021
// (commits 9f465ef..d633f66). Do not "normalise" to 128x64 values.
//
// width is deliberately 128 = the full controller RAM, of which the
// glass shows a 64-column window covering (roughly) the framebuffer
// range x=32..95 — which is why every widget is anchored at x=32.
// The 2021 value of 96 left RAM columns >=96 unwritten, and the
// window edge shows ~4 of them: power-on garbage, invisible for four
// years under the dark theme, exposed as a lit 4px bar by the LVGL 8
// migration. Rendering all 128 columns keeps every column the window
// can reach painted. Content position on glass is unchanged.
width = <128>;
height = <48>;
segment-offset = <0>;
page-offset = <0>;
Expand Down
Loading
Loading