Skip to content

Commit 5092227

Browse files
committed
Add the Cirrus40 keyboard
A 40% keyboard with focus on usable encoders. The PCB uses external pullups for encoder pins, hence the custom initialization logic. https://github.com/schuay/cirrus40
1 parent 84d44e6 commit 5092227

File tree

5 files changed

+281
-0
lines changed

5 files changed

+281
-0
lines changed

keyboards/cirrus40/cirrus40.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2025 Jakob Linke
2+
// SPDX-License-Identifier: GPL-2.0-or-later
3+
4+
#include QMK_KEYBOARD_H
5+
6+
#include "quantum.h"
7+
8+
#ifdef ENCODER_ENABLE
9+
extern volatile bool isLeftHand;
10+
// It would be nice if QMK would expose these:
11+
static bool encoder_pins_are_initialized = false; // Whether custom initialization ran.
12+
static pin_t encoders_pad_a_l[] = ENCODER_A_PINS;
13+
static pin_t encoders_pad_b_l[] = ENCODER_B_PINS;
14+
static pin_t encoders_pad_a_r[] = ENCODER_A_PINS_RIGHT;
15+
static pin_t encoders_pad_b_r[] = ENCODER_B_PINS_RIGHT;
16+
static pin_t* encoders_pad_a = NULL;
17+
static pin_t* encoders_pad_b = NULL;
18+
19+
// This function overrides the weak implementation in the core code.
20+
void encoder_wait_pullup_charge(void) {
21+
wait_us(10); // QMK is conservative and waits 100us.
22+
}
23+
24+
// We use external pullups and thus have to implement custom initialization.
25+
// This function overrides the weak implementation in the core code.
26+
void encoder_quadrature_init_pin(uint8_t index, bool pad_b) {
27+
if (!encoder_pins_are_initialized) {
28+
// We cannot hook in earlier than this, so do side initialization here.
29+
static_assert(sizeof(encoders_pad_a_l) / sizeof(encoders_pad_a_l[0]) == 1);
30+
static_assert(sizeof(encoders_pad_b_l) / sizeof(encoders_pad_b_l[0]) == 1);
31+
static_assert(sizeof(encoders_pad_a_r) / sizeof(encoders_pad_a_r[0]) == 1);
32+
static_assert(sizeof(encoders_pad_b_r) / sizeof(encoders_pad_b_r[0]) == 1);
33+
encoders_pad_a = isLeftHand ? encoders_pad_a_l : encoders_pad_a_r;
34+
encoders_pad_b = isLeftHand ? encoders_pad_b_l : encoders_pad_b_r;
35+
encoder_pins_are_initialized = true;
36+
}
37+
assert(index == 0);
38+
pin_t pin = pad_b ? encoders_pad_b[index] : encoders_pad_a[index];
39+
assert(pin != NO_PIN);
40+
gpio_set_pin_input(pin); // No pullup.
41+
}
42+
43+
// Optimize by reading both pins at once.
44+
// This function overrides the weak implementation in the core code.
45+
void encoder_driver_task(void) {
46+
extern void encoder_quadrature_handle_read(uint8_t index, uint8_t pin_a_state, uint8_t pin_b_state);
47+
static const int i = 0;
48+
pin_t pin_a = encoders_pad_a[i];
49+
pin_t pin_b = encoders_pad_b[i];
50+
// Read both pins at the same time since they are on the same port.
51+
assert(PAL_PORT(pin_a) == PAL_PORT(pin_b));
52+
ioportmask_t port_state = palReadPort(PAL_PORT(pin_a));
53+
uint8_t pin_a_state, pin_b_state;
54+
pin_a_state = (port_state >> PAL_PAD(pin_a)) & 1;
55+
pin_b_state = (port_state >> PAL_PAD(pin_b)) & 1;
56+
encoder_quadrature_handle_read(i, pin_a_state, pin_b_state);
57+
}
58+
#endif

keyboards/cirrus40/keyboard.json

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
"keyboard_name": "cirrus40",
3+
"manufacturer": "schuay",
4+
"maintainer": "schuay",
5+
"url": "https://github.com/schuay/cirrus40",
6+
"usb": {
7+
"device_version": "1.0.1",
8+
"pid": "0x23B7",
9+
"vid": "0xFAAD"
10+
},
11+
"development_board": "promicro_rp2040",
12+
"diode_direction": "COL2ROW",
13+
"features": {
14+
"encoder": true,
15+
"extrakey": true,
16+
"nkro": true
17+
},
18+
"build": {
19+
"lto": true
20+
},
21+
"host": {
22+
"default": {
23+
"nkro": true
24+
}
25+
},
26+
"matrix_pins": {
27+
"cols": ["GP20", "GP22", "GP26", "GP27", "GP28"],
28+
"rows": ["GP3", "GP4", "GP5", "GP6"]
29+
},
30+
"encoder": {
31+
"rotary": [
32+
{"pin_a": "GP9", "pin_b": "GP8", "resolution": 2}
33+
]
34+
},
35+
"split": {
36+
"enabled": true,
37+
"usb_detect": {
38+
"enabled": true
39+
},
40+
"encoder": {
41+
"right": {
42+
"rotary": [
43+
{"pin_a": "GP8", "pin_b": "GP9", "resolution": 2}
44+
]
45+
}
46+
},
47+
"serial": {
48+
"driver": "vendor",
49+
"pin": "GP1"
50+
},
51+
"handedness": {
52+
"pin": "GP0"
53+
}
54+
},
55+
"layouts": {
56+
"LAYOUT": {
57+
"layout": [
58+
{"label": "Q", "matrix": [0, 0], "hand": "L", "x": 0, "y": 0.5},
59+
{"label": "W", "matrix": [0, 1], "hand": "L", "x": 1, "y": 0.25},
60+
{"label": "E", "matrix": [0, 2], "hand": "L", "x": 2, "y": 0},
61+
{"label": "R", "matrix": [0, 3], "hand": "L", "x": 3, "y": 0.25},
62+
{"label": "T", "matrix": [0, 4], "hand": "L", "x": 4, "y": 0.5},
63+
{"label": "Y", "matrix": [4, 4], "hand": "R", "x": 8, "y": 0.5},
64+
{"label": "U", "matrix": [4, 3], "hand": "R", "x": 9, "y": 0.25},
65+
{"label": "I", "matrix": [4, 2], "hand": "R", "x": 10, "y": 0},
66+
{"label": "O", "matrix": [4, 1], "hand": "R", "x": 11, "y": 0.25},
67+
{"label": "P", "matrix": [4, 0], "hand": "R", "x": 12, "y": 0.5},
68+
69+
{"label": "A", "matrix": [1, 0], "hand": "L", "x": 0, "y": 1.5},
70+
{"label": "S", "matrix": [1, 1], "hand": "L", "x": 1, "y": 1.25},
71+
{"label": "D", "matrix": [1, 2], "hand": "L", "x": 2, "y": 1},
72+
{"label": "F", "matrix": [1, 3], "hand": "L", "x": 3, "y": 1.25},
73+
{"label": "G", "matrix": [1, 4], "hand": "L", "x": 4, "y": 1.5},
74+
{"label": "H", "matrix": [5, 4], "hand": "R", "x": 8, "y": 1.5},
75+
{"label": "J", "matrix": [5, 3], "hand": "R", "x": 9, "y": 1.25},
76+
{"label": "K", "matrix": [5, 2], "hand": "R", "x": 10, "y": 1},
77+
{"label": "L", "matrix": [5, 1], "hand": "R", "x": 11, "y": 1.25},
78+
{"label": ":", "matrix": [5, 0], "hand": "R", "x": 12, "y": 1.5},
79+
80+
{"label": "Z", "matrix": [2, 0], "hand": "L", "x": 0, "y": 2.5},
81+
{"label": "X", "matrix": [2, 1], "hand": "L", "x": 1, "y": 2.25},
82+
{"label": "C", "matrix": [2, 2], "hand": "L", "x": 2, "y": 2},
83+
{"label": "V", "matrix": [2, 3], "hand": "L", "x": 3, "y": 2.25},
84+
{"label": "B", "matrix": [2, 4], "hand": "L", "x": 4, "y": 2.5},
85+
{"label": "N", "matrix": [6, 4], "hand": "R", "x": 8, "y": 2.5},
86+
{"label": "M", "matrix": [6, 3], "hand": "R", "x": 9, "y": 2.25},
87+
{"label": ",", "matrix": [6, 2], "hand": "R", "x": 10, "y": 2},
88+
{"label": ".", "matrix": [6, 1], "hand": "R", "x": 11, "y": 2.25},
89+
{"label": "?", "matrix": [6, 0], "hand": "R", "x": 12, "y": 2.5},
90+
91+
{"label": "Left ENC", "matrix": [3, 1], "hand": "*", "encoder": 0, "x": 0.5, "y": 3.25},
92+
{"label": "Left Thumb1", "matrix": [3, 2], "hand": "*", "x": 1.5, "y": 3.25},
93+
{"label": "Left Thumb2", "matrix": [3, 3], "hand": "*", "x": 2.5, "y": 3.25},
94+
{"label": "Left Thumb3", "matrix": [3, 4], "hand": "*", "x": 3.5, "y": 3.5},
95+
{"label": "Right Thumb3", "matrix": [7, 4], "hand": "*", "x": 8.5, "y": 3.5},
96+
{"label": "Right Thumb2", "matrix": [7, 3], "hand": "*", "x": 9.5, "y": 3.25},
97+
{"label": "Right Thumb1", "matrix": [7, 2], "hand": "*", "x": 10.5, "y": 3.25},
98+
{"label": "Right ENC", "matrix": [7, 1], "hand": "*", "encoder": 1, "x": 11.5, "y": 3.25}
99+
]
100+
}
101+
}
102+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2025 Jakob Linke
2+
// SPDX-License-Identifier: GPL-2.0-or-later
3+
4+
#include QMK_KEYBOARD_H
5+
6+
enum Layers {
7+
kBase = 0,
8+
kOneHand,
9+
kNavigation,
10+
kNumbers,
11+
kFnKeys,
12+
kSymbols,
13+
};
14+
15+
enum CustomKeycodes {
16+
HRM_A = LGUI_T(KC_A),
17+
HRM_R = LALT_T(KC_R),
18+
HRM_S = LCTL_T(KC_S),
19+
HRM_T = LSFT_T(KC_T),
20+
HRM_N = LSFT_T(KC_N),
21+
HRM_E = LCTL_T(KC_E),
22+
HRM_I = LALT_T(KC_I),
23+
HRM_O = LGUI_T(KC_O),
24+
BTN_ESC = LT(kOneHand, KC_ESC),
25+
NAV_SPC = LT(kNavigation, KC_SPC),
26+
NM2_TAB = LT(kNumbers, KC_TAB),
27+
NUM_DEL = KC_DEL,
28+
FUN_ENT = LT(kFnKeys, KC_ENT),
29+
SYM_BSP = LT(kSymbols, KC_BSPC),
30+
WSP_L = G(KC_PGUP),
31+
WSP_R = G(KC_PGDN),
32+
TAB_L = C(KC_PGUP),
33+
TAB_R = C(KC_PGDN),
34+
};
35+
36+
// clang-format off
37+
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
38+
[kBase] = LAYOUT(
39+
KC_Q, KC_W, KC_F, KC_P, KC_B, KC_J, KC_L, KC_U, KC_Y, KC_SCLN,
40+
HRM_A, HRM_R, HRM_S, HRM_T, KC_G, KC_M, HRM_N, HRM_E, HRM_I, HRM_O,
41+
KC_Z, KC_X, KC_C, KC_D, KC_V, KC_K, KC_H, KC_COMM, KC_DOT, KC_UNDS,
42+
QK_LLCK, BTN_ESC, NAV_SPC, NM2_TAB, FUN_ENT, SYM_BSP, NUM_DEL, QK_LLCK
43+
),
44+
45+
[kOneHand] = LAYOUT(
46+
C(KC_A), C(KC_W), A(KC_TAB),C(KC_T), XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,
47+
WSP_L, TAB_L, TAB_R, WSP_R, KC_ENT, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,
48+
C(KC_Z), C(KC_X), C(KC_C), XXXXXXX, C(KC_V), XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,
49+
_______, _______, _______, _______, _______, _______, _______, _______
50+
),
51+
52+
[kNavigation] = LAYOUT(
53+
XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, KC_PGUP, _______, _______, _______, XXX,
54+
KC_LGUI, KC_LALT, KC_LCTL, KC_LSFT, XXXXXXX, KC_PGDN, KC_LEFT, KC_DOWN, KC_UP, KC_RGHT,
55+
_______, _______, _______, _______, XXXXXXX, _______, KC_HOME, _______, _______, KC_END,
56+
_______, _______, _______, _______, KC_ENT, KC_BSPC, KC_DEL, _______
57+
),
58+
59+
[kNumbers] = LAYOUT(
60+
QK_BOOT, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, KC_7, KC_8, KC_9, XXX,
61+
KC_LGUI, KC_LALT, KC_LCTL, KC_LSFT, XXXXXXX, S(KC_G), KC_1, KC_2, KC_3, KC_0,
62+
QK_RBT, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, KC_4, KC_5, KC_6, _______,
63+
_______, _______, _______, _______, KC_ENT, KC_BSPC, KC_DEL, _______
64+
),
65+
66+
[kFnKeys] = LAYOUT(
67+
KC_F10, KC_F9, KC_F8, KC_F7, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXX,
68+
KC_F11, KC_F3, KC_F2, KC_F1, XXXXXXX, XXXXXXX, KC_LSFT, KC_LCTL, KC_LALT, KC_LGUI,
69+
KC_F12, KC_F6, KC_F5, KC_F4, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXX,
70+
_______, KC_ESC, KC_SPC, KC_TAB, _______, _______, _______, _______
71+
),
72+
73+
[kSymbols] = LAYOUT(
74+
KC_QUOT, KC_LPRN, KC_RPRN, KC_SCLN, KC_PERC, KC_TILD, KC_PIPE, KC_AMPR, KC_GRV, KC_DQT,
75+
KC_EXLM, KC_EQL, KC_SLSH, KC_PLUS, KC_HASH, KC_ASTR, KC_LSFT, KC_LCTL, KC_LALT, KC_LGUI,
76+
KC_CIRC, KC_LCBR, KC_RCBR, KC_DLR, XXXXXXX, KC_BSLS, KC_AT, KC_LBRC, KC_RBRC, KC_QUES,
77+
_______, KC_ESC, KC_SPC, KC_TAB, _______, _______, _______, _______
78+
),
79+
};
80+
81+
const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM =
82+
LAYOUT(
83+
'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R',
84+
'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R',
85+
'L', 'L', 'L', 'L', 'L', 'R', 'R', 'R', 'R', 'R',
86+
'*', '*', '*', '*', '*', '*', '*', '*'
87+
);
88+
// clang-format on
89+
90+
#if defined(ENCODER_MAP_ENABLE)
91+
const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][NUM_DIRECTIONS] = {
92+
[kBase] = {ENCODER_CCW_CW(KC_PGUP, KC_PGDN),
93+
ENCODER_CCW_CW(KC_LEFT, KC_RIGHT)},
94+
95+
[kNavigation] = {ENCODER_CCW_CW(KC_UP, KC_DOWN), ENCODER_CCW_CW(KC_BSPC, KC_DEL)},
96+
[kSymbols] = {ENCODER_CCW_CW(KC_UP, KC_DOWN), ENCODER_CCW_CW(KC_BSPC, KC_DEL)},
97+
98+
[kOneHand] = {ENCODER_CCW_CW(KC_PGUP, KC_PGDN),
99+
ENCODER_CCW_CW(KC_LEFT, KC_RIGHT)},
100+
[kNumbers] = {ENCODER_CCW_CW(KC_PGUP, KC_PGDN),
101+
ENCODER_CCW_CW(KC_LEFT, KC_RIGHT)},
102+
[kFnKeys] = {ENCODER_CCW_CW(KC_PGUP, KC_PGDN),
103+
ENCODER_CCW_CW(KC_LEFT, KC_RIGHT)},
104+
};
105+
#endif
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ENCODER_MAP_ENABLE = yes

keyboards/cirrus40/readme.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Cirrus40
2+
3+
![Cirrus40](https://github.com/schuay/cirrus40/blob/working/img/img1.jpg?raw=true)
4+
5+
A 40% (3x5+3) split ortholinear keyboard with a focus on usable encoders. [More info](https://github.com/schuay/cirrus40)
6+
7+
* Keyboard Maintainer: [Jakob Linke](https://github.com/schuay)
8+
9+
Use QMK's [external userspace](https://docs.qmk.fm/newbs_external_userspace) to build this firmware.
10+
11+
See the [build environment setup](getting_started_build_tools) and the [make instructions](getting_started_make_guide) for more information. Brand new to QMK? Start with our [Complete Newbs Guide](newbs).
12+
13+
## Bootloader
14+
15+
Enter the bootloader by pressing the RESET button, shorting the RST pin to GND, or using the key mapped to `QK_BOOT'.

0 commit comments

Comments
 (0)