Skip to content

Commit 35d2187

Browse files
Add Game Designer's Kit (hw---gdk) micro:bit V2 Arcade shield
Introduce hw---gdk for the Game Designer's Kit: direct GPIO arcade buttons, ST7735 SPI display, active-low backlight fix, and single-shot SAADC for the on-board battery divider and low-battery LED. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0829b9f commit 35d2187

12 files changed

Lines changed: 306 additions & 0 deletions

File tree

docs/arcade-devices.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ These boards run MakeCode Arcade games. Choose a board to find out more about it
146146
"url": "https://tomatocube.com/product/makecode-arcade-display-shield-for-microbit",
147147
"variant": "hw---n3"
148148
},
149+
{
150+
"name": "Game Designer's Kit",
151+
"description": "micro:bit V2 shield with 1.8\" ST7735 color display and GPIO arcade buttons",
152+
"imageUrl": "/static/hardware/game-designers-kit.jpg",
153+
"url": "https://github.com/Deltabotixpvt/Game-Designer-s-Kit",
154+
"variant": "hw---gdk"
155+
},
149156
{
150157
"name": "Adafruit M4",
151158
"description": "Learn how to run your games on micro-controllers from Adafruit",

docs/hardware.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ Shields are based on our guidelines, adhere to open source hardware reference de
133133
"imageUrl": "/static/hardware/kitronik-shield.png",
134134
"url": "http://www.kitronik.co.uk/56116"
135135
},
136+
{
137+
"name": "Game Designer's Kit",
138+
"description": "micro:bit V2 shield with 1.8\" ST7735 color display and GPIO arcade buttons",
139+
"imageUrl": "/static/hardware/game-designers-kit.jpg",
140+
"url": "https://github.com/Deltabotixpvt/Game-Designer-s-Kit"
141+
},
136142
{
137143
"name": "Calliope GameKit Shield",
138144
"description": "Use the Calliope mini with GameKit to bring your game ideas to life",
52.7 KB
Loading

libs/hw---gdk/analog.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#include "pxt.h"
2+
#include "nrf.h"
3+
4+
namespace gdk {
5+
6+
// DMA target for the one conversion result. Must live in RAM (EasyDMA).
7+
static volatile int16_t adcResult;
8+
static bool saadcCalibrated = false;
9+
10+
// Busy-wait for a SAADC event register, with a hard timeout so a failed
11+
// conversion can never freeze the game loop. Returns true if the event fired.
12+
static bool saadcWait(volatile uint32_t *evt, uint32_t timeoutUs) {
13+
uint64_t start = pxt::current_time_us();
14+
while (*evt == 0) {
15+
if (pxt::current_time_us() - start > timeoutUs)
16+
return false;
17+
}
18+
*evt = 0;
19+
return true;
20+
}
21+
22+
// One-time offset calibration (recommended by Nordic before first use).
23+
static void saadcCalibrate() {
24+
NRF_SAADC->EVENTS_CALIBRATEDONE = 0;
25+
NRF_SAADC->TASKS_CALIBRATEOFFSET = 1;
26+
if (saadcWait(&NRF_SAADC->EVENTS_CALIBRATEDONE, 5000))
27+
saadcCalibrated = true;
28+
// After calibration the SAADC raises STOPPED; clear it so the real read starts clean.
29+
NRF_SAADC->EVENTS_STOPPED = 0;
30+
}
31+
32+
/**
33+
* Blocking single-shot ADC read of P0 (P0.02 / AIN0). Returns 0..1023 (same
34+
* scale as pins.analogReadPin), or -1 on hardware timeout.
35+
*
36+
* Absolute internal 0.6V reference + gain 1/6 => full scale 3.6V (VDD-independent,
37+
* so the reading stays correct as the battery/VDD drains). 40us acquisition suits
38+
* the on-board ~82k divider.
39+
*
40+
* Owns the SAADC exclusively for the ~50us the conversion takes, then fully
41+
* disables it. It does NOT touch PPI/timers/IRQs, so it cannot disturb the
42+
* display or audio. The complete manual sequence is:
43+
* ENABLE -> START -> (STARTED) -> SAMPLE -> (END) -> STOP -> (STOPPED) -> DISABLE
44+
* The SAMPLE task is the critical step the previous version was missing.
45+
*/
46+
//%
47+
int readP0Adc() {
48+
NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_14bit << SAADC_RESOLUTION_VAL_Pos;
49+
NRF_SAADC->OVERSAMPLE = SAADC_OVERSAMPLE_OVERSAMPLE_Bypass << SAADC_OVERSAMPLE_OVERSAMPLE_Pos;
50+
51+
// P0 = P0.02 = AIN0 on channel 0; all other channels unused.
52+
NRF_SAADC->CH[0].PSELP = SAADC_CH_PSELP_PSELP_AnalogInput0 << SAADC_CH_PSELP_PSELP_Pos;
53+
NRF_SAADC->CH[0].PSELN = SAADC_CH_PSELN_PSELN_NC << SAADC_CH_PSELN_PSELN_Pos;
54+
NRF_SAADC->CH[0].CONFIG =
55+
(SAADC_CH_CONFIG_RESP_Bypass << SAADC_CH_CONFIG_RESP_Pos) |
56+
(SAADC_CH_CONFIG_RESN_Bypass << SAADC_CH_CONFIG_RESN_Pos) |
57+
(SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos) |
58+
(SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos) |
59+
(SAADC_CH_CONFIG_TACQ_40us << SAADC_CH_CONFIG_TACQ_Pos) |
60+
(SAADC_CH_CONFIG_MODE_SE << SAADC_CH_CONFIG_MODE_Pos) |
61+
(SAADC_CH_CONFIG_BURST_Disabled << SAADC_CH_CONFIG_BURST_Pos);
62+
for (int c = 1; c < 8; c++) {
63+
NRF_SAADC->CH[c].PSELP = SAADC_CH_PSELP_PSELP_NC << SAADC_CH_PSELP_PSELP_Pos;
64+
NRF_SAADC->CH[c].PSELN = SAADC_CH_PSELN_PSELN_NC << SAADC_CH_PSELN_PSELN_Pos;
65+
}
66+
67+
adcResult = 0;
68+
NRF_SAADC->RESULT.PTR = (uint32_t)&adcResult;
69+
NRF_SAADC->RESULT.MAXCNT = 1;
70+
71+
NRF_SAADC->EVENTS_STARTED = 0;
72+
NRF_SAADC->EVENTS_END = 0;
73+
NRF_SAADC->EVENTS_STOPPED = 0;
74+
75+
NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled << SAADC_ENABLE_ENABLE_Pos;
76+
77+
if (!saadcCalibrated)
78+
saadcCalibrate();
79+
80+
// Ready the (D)MA buffer.
81+
NRF_SAADC->TASKS_START = 1;
82+
if (!saadcWait(&NRF_SAADC->EVENTS_STARTED, 5000)) {
83+
NRF_SAADC->ENABLE = 0;
84+
return -1;
85+
}
86+
87+
// Take the actual sample -- THIS is what fills RESULT and raises END.
88+
NRF_SAADC->TASKS_SAMPLE = 1;
89+
if (!saadcWait(&NRF_SAADC->EVENTS_END, 5000)) {
90+
NRF_SAADC->TASKS_STOP = 1;
91+
NRF_SAADC->ENABLE = 0;
92+
return -1;
93+
}
94+
95+
int sample = adcResult;
96+
if (sample < 0)
97+
sample = 0; // single-ended: clip small negative offset to 0
98+
99+
NRF_SAADC->TASKS_STOP = 1;
100+
saadcWait(&NRF_SAADC->EVENTS_STOPPED, 5000);
101+
NRF_SAADC->ENABLE = 0;
102+
103+
// 14-bit full scale is 16383; scale to the familiar 0..1023 range.
104+
return sample / 16;
105+
}
106+
107+
} // namespace gdk

libs/hw---gdk/batteryled.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Game Designer's Kit battery-low indicator LED (P6).
2+
3+
namespace gdk {
4+
const ADC_LOW_THRESHOLD = 700;
5+
const STARTUP_DELAY_MS = 2000;
6+
const REFRESH_MS = 5000;
7+
const SAMPLES = 4;
8+
9+
const led = pins.pinByCfg(DAL.CFG_PIN_P6);
10+
11+
function readAdcAveraged(): number {
12+
let acc = 0;
13+
let n = 0;
14+
for (let i = 0; i < SAMPLES; i++) {
15+
const v = readP0Adc();
16+
if (v >= 0) { acc += v; n++; }
17+
}
18+
return n > 0 ? Math.idiv(acc, n) : -1;
19+
}
20+
21+
function updateLed() {
22+
const adc = readAdcAveraged();
23+
if (adc < 0) return;
24+
const low = adc < ADC_LOW_THRESHOLD;
25+
if (led) led.digitalWrite(low);
26+
}
27+
28+
control.runInBackground(function () {
29+
pause(STARTUP_DELAY_MS);
30+
updateLed();
31+
while (true) {
32+
pause(REFRESH_MS);
33+
updateLed();
34+
}
35+
});
36+
}

libs/hw---gdk/config.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Game Designer's Kit: micro:bit V2 + 1.8" ST7735 (128x160) over edge-connector SPI.
2+
// Direct ST7735 (no 74HC165 / "smart display" auto-detect). GPIO arcade buttons.
3+
4+
namespace config {
5+
export const SETTINGS_SIZE = (8 * 1024)
6+
7+
export const PIN_DISPLAY_SCK = DAL.P0_17 // P13
8+
export const PIN_DISPLAY_MOSI = DAL.P0_13 // P15
9+
export const PIN_DISPLAY_MISO = DAL.P0_1
10+
export const PIN_DISPLAY_CS = DAL.P1_2 // P16
11+
export const PIN_DISPLAY_DC = DAL.P0_10 // P8
12+
export const PIN_DISPLAY_RST = DAL.P0_12 // P12
13+
// Active-LOW backlight on P10 (P-FET); see init.ts.
14+
export const PIN_DISPLAY_BL = DAL.P0_30 // P10
15+
16+
export const PIN_BTN_UP = DAL.P0_3 // P1
17+
export const PIN_BTN_DOWN = DAL.P0_4 // P2
18+
export const PIN_BTN_LEFT = DAL.P0_31 // P3
19+
export const PIN_BTN_RIGHT = DAL.P0_28 // P4
20+
export const PIN_BTN_A = DAL.P0_14 // P5
21+
export const PIN_BTN_B = DAL.P0_23 // P11
22+
export const PIN_BTN_MENU = DAL.P0_9 // P9
23+
24+
export const PIN_JACK_SND = DAL.P0_0
25+
export const SPEAKER_VOLUME = 255
26+
27+
export const DISPLAY_WIDTH = 160
28+
export const DISPLAY_HEIGHT = 128
29+
export const DISPLAY_DELAY = 300
30+
export const CLOCK_SPEED = 32
31+
export const DISPLAY_TYPE = 7735 // ST7735 direct panel
32+
33+
export const DISPLAY_CFG0 = 0x00000080
34+
export const DISPLAY_CFG1 = 0x00000603
35+
export const DISPLAY_CFG2 = 24
36+
37+
// P0: battery divider (analog.cpp). P6: low-battery LED (batteryled.ts).
38+
export const PIN_P0 = DAL.P0_2
39+
export const PIN_P6 = DAL.P1_5
40+
}

libs/hw---gdk/device.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// nothing here yet

libs/hw---gdk/init.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Game Designer's Kit backlight override.
2+
//
3+
// Why this file exists:
4+
// The PCB drives the LCD backlight through an AO3407A P-channel MOSFET high-side switch.
5+
// Source = 3V3, Drain = LCD_LED, Gate = P10 (with 10k pull-up to 3V3).
6+
// P-FET truth table on this board:
7+
// P10 LOW -> MOSFET ON -> 3V3 reaches LCD_LED -> backlight ON
8+
// P10 HIGH -> MOSFET OFF -> backlight OFF
9+
// The CODAL ST7735 driver does `bl->setDigitalValue(1)` once during init
10+
// (see node_modules/pxt-common-packages/libs/screen---st7735/screen.cpp around line 125),
11+
// which turns this PCB's backlight OFF. We override it back to LOW here.
12+
//
13+
// Ordering: `hw---gdk` depends on `screen---st7735`, so this file's top-level
14+
// statements run AFTER screen---st7735/targetoverrides.ts has triggered the C++ display
15+
// constructor (which is what writes BL=HIGH). Therefore the synchronous write below
16+
// reliably wins over the driver's default. The background watchdog re-asserts BL=LOW
17+
// every second as a defense against any later code path that re-initialises the screen.
18+
19+
namespace gdk {
20+
function driveBacklightOn(): void {
21+
const bl = pins.pinByCfg(DAL.CFG_PIN_DISPLAY_BL);
22+
if (bl) bl.digitalWrite(false);
23+
}
24+
25+
driveBacklightOn();
26+
27+
forever(function () {
28+
driveBacklightOn();
29+
pause(1000);
30+
});
31+
}

libs/hw---gdk/pxt.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "hw---gdk",
3+
"description": "Game Designer's Kit — micro:bit V2 handheld with ST7735 display and arcade buttons",
4+
"files": [
5+
"config.ts",
6+
"init.ts",
7+
"batteryled.ts",
8+
"device.d.ts",
9+
"shims.d.ts",
10+
"analog.cpp"
11+
],
12+
"card": {
13+
"name": "Game Designer's Kit",
14+
"description": "Build and play handheld games on micro:bit V2 with a color screen and arcade buttons.",
15+
"learnMoreUrl": "https://github.com/Deltabotixpvt/Game-Designer-s-Kit",
16+
"cardType": "hw",
17+
"imageUrl": "/static/hardware/game-designers-kit.jpg"
18+
},
19+
"compileServiceVariant": "nrf52833",
20+
"cppDependencies": {
21+
"accelerometer": "file:../accelerometer"
22+
},
23+
"dependencies": {
24+
"core---nrf52": "file:../core---nrf52",
25+
"screen---st7735": "file:../screen---st7735",
26+
"mixer---nrf52": "file:../mixer---nrf52",
27+
"game": "file:../game"
28+
},
29+
"public": true,
30+
"skipLocalization": true,
31+
"additionalFilePath": "../hw",
32+
"yotta": {
33+
"optionalConfig": {
34+
"DEVICE_JACDAC_DEBUG": 1
35+
},
36+
"config": {
37+
"DEVICE_USB": 0,
38+
"DEVICE_WEBUSB": 0,
39+
"DEVICE_BLE": 0,
40+
"ARCADE_MBIT_CODAL": 1,
41+
"MICROBIT_CODAL": 0
42+
}
43+
},
44+
"dalDTS": {
45+
"corePackage": "../core---nrf52"
46+
}
47+
}

libs/hw---gdk/shims.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Auto-generated. Do not edit.
2+
declare namespace gdk {
3+
4+
/**
5+
* Blocking single-shot ADC read of P0 (P0.02 / AIN0). Returns 0..1023 (same
6+
* scale as pins.analogReadPin), or -1 on hardware timeout.
7+
*
8+
* Absolute internal 0.6V reference + gain 1/6 => full scale 3.6V (VDD-independent,
9+
* so the reading stays correct as the battery/VDD drains). 40us acquisition suits
10+
* the on-board ~82k divider.
11+
*
12+
* Owns the SAADC exclusively for the ~50us the conversion takes, then fully
13+
* disables it. It does NOT touch PPI/timers/IRQs, so it cannot disturb the
14+
* display or audio. The complete manual sequence is:
15+
* ENABLE -> START -> (STARTED) -> SAMPLE -> (END) -> STOP -> (STOPPED) -> DISABLE
16+
* The SAMPLE task is the critical step the previous version was missing.
17+
*/
18+
//% shim=gdk::readP0Adc
19+
function readP0Adc(): int32;
20+
}
21+
22+
// Auto-generated. Do not edit. Really.

0 commit comments

Comments
 (0)