Skip to content

Commit 0fd2eec

Browse files
committed
Daisy circuitpython port
Signed-off-by: Daniel Schaefer <dhs@frame.work>
1 parent 121489f commit 0fd2eec

13 files changed

Lines changed: 366 additions & 7 deletions

File tree

ports/zephyr-cp/Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ ifneq ($(CP_BOARD_CONF),)
4444
WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_CONF_FILE=$(CP_BOARD_CONF)
4545
endif
4646

47+
# Anything that is a property of the whole build rather than of the application
48+
# image - whether MCUboot is built, and how - belongs to sysbuild, which reads
49+
# it from a sysbuild.conf next to the *application*. A board's own therefore has
50+
# to be named explicitly, the same reason board.overlay and board.conf are
51+
# above. SB_EXTRA_CONF_FILE and not SB_CONF_FILE, so the port's own sysbuild.conf
52+
# still applies.
53+
CP_BOARD_SYSBUILD_CONF := $(wildcard $(CP_BOARD_DIR)/sysbuild.conf)
54+
ifneq ($(CP_BOARD_SYSBUILD_CONF),)
55+
WEST_CMAKE_ARGS += -DSB_EXTRA_CONF_FILE=$(CP_BOARD_SYSBUILD_CONF)
56+
endif
57+
4758
.PHONY: $(BUILD)/zephyr-cp/zephyr/zephyr.elf flash recover debug debug-jlink debugserver attach run run-sim clean menuconfig all clean-all sim clean-sim test fetch-port-submodules
4859

4960
export BSIM_COMPONENTS_PATH := $(CURDIR)/tools/bsim/components
@@ -66,6 +77,17 @@ $(BUILD)/firmware.exe: $(BUILD)/zephyr-cp/zephyr/zephyr.elf
6677
$(BUILD)/firmware.uf2: $(BUILD)/zephyr-cp/zephyr/zephyr.elf
6778
cp $(BUILD)/zephyr-cp/zephyr/zephyr.uf2 $@
6879

80+
# Both of these exist only when the board's sysbuild.conf enables MCUboot.
81+
# The signed image is what a bootloader already on the hardware accepts.
82+
$(BUILD)/firmware.signed.bin: $(BUILD)/zephyr-cp/zephyr/zephyr.elf
83+
cp $(BUILD)/zephyr-cp/zephyr/zephyr.signed.bin $@
84+
85+
# Bootloader and signed application in one hex, for flashing over SWD. Sysbuild
86+
# names it after the resolved Zephyr board, which the CircuitPython board alias
87+
# doesn't spell, so let the shell find it.
88+
$(BUILD)/firmware.merged.hex: $(BUILD)/zephyr-cp/zephyr/zephyr.elf
89+
cp $(BUILD)/merged_*.hex $@
90+
6991
$(BUILD)/firmware.rps: $(BUILD)/zephyr-cp/zephyr/zephyr.elf
7092
cp $(BUILD)/zephyr-cp/zephyr/zephyr.rps $@
7193

ports/zephyr-cp/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,70 @@ Behavior and precedence:
9999
- If neither is provided, defaults from `circuitpython.toml` are used.
100100
- Use `SHIELD=` (empty) to disable a board default shield for one build.
101101

102+
## Naming extra pins
103+
104+
`board` names pins it can work out from the devicetree: LEDs, buttons and
105+
recognised connectors. Anything a board merely routes somewhere has no name, so
106+
name it in the [`/zephyr,user`][zuser] node, in a board overlay:
107+
108+
```dts
109+
/ {
110+
zephyr,user {
111+
backlight-gpios = <&gpio3 11 GPIO_ACTIVE_HIGH>;
112+
ksi-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>, <&gpio0 1 GPIO_ACTIVE_HIGH>;
113+
};
114+
};
115+
```
116+
117+
A property with one entry names that pin after the property, so the first is
118+
`board.BACKLIGHT`. A property with several appends the index, so the second is
119+
`board.KSI0` and `board.KSI1` -- which keeps a wide keyboard matrix from needing
120+
one property per pin. Only the controller and pin are read; how a pin is driven
121+
is up to the Python code.
122+
123+
[zuser]: https://docs.zephyrproject.org/latest/build/dts/zephyr-user-node.html
124+
125+
## Framework Daisy
126+
127+
The Daisy keyboard and its dongle are nRF54LM20A boards whose Zephyr
128+
definitions live in a separate module, [framework-daisy-zephyr][daisy], which
129+
`zephyr-config/west.yml` pulls in -- so `west update` is all the setup there is:
130+
131+
```sh
132+
make BOARD=framework_daisy_kb_dvt1
133+
make BOARD=framework_daisy_dongle_dvt1
134+
```
135+
136+
Unlike the other boards here these two build MCUboot as well, which their
137+
`sysbuild.conf` opts into. Daisy ships with MCUboot in RRAM and the application
138+
is linked into its slot, so an unsigned image would not boot. Two artifacts come
139+
out of a build:
140+
141+
- `build-<board>/firmware.signed.bin`, which installs over the bootloader
142+
already on the hardware -- hold the pairing button (keyboard) or the button
143+
(dongle) at reset to reach its USB serial recovery. No debugger needed.
144+
- `build-<board>/firmware.merged.hex`, bootloader and application together, for
145+
`make BOARD=... flash` over SWD.
146+
147+
The CIRCUITPY drive is 612 KB taken out of the application slot, because Daisy's
148+
flash layout -- fixed by the bootloader already on shipping hardware -- otherwise
149+
fills RRAM exactly. Reinstalling firmware over serial recovery therefore erases
150+
the drive: MCUboot erases the slot as *it* understands it, which is the full
151+
1828 KB. The reasoning and the numbers are in
152+
`boards/framework/daisy_kb_dvt1/board.overlay`.
153+
154+
This port has no `pwmio` yet, so the keyboard overlay disables the PWM instances
155+
and hands their pins back to `digitalio`: `board.WHITE_LED_1` through `_4`,
156+
`board.RED_LED` / `GREEN_LED` / `BLUE_LED` and `board.BACKLIGHT`. The key matrix
157+
is `board.KSI0`-`KSI7` and `board.KSO0`-`KSO17`; it is col2row and has no diodes,
158+
so three keys at the corners of a rectangle read as a fourth and scanning it in
159+
Python needs the same deghosting ZMK does. Of the three I2C buses `board.I2C` is
160+
the pin header, `board.TP_I2C` the touchpad -- already powered, the board brings
161+
its rail up at boot -- and `board.NPMC_I2C` the nPM1300, which is also the PMIC
162+
supplying the rail the SoC runs on.
163+
164+
[daisy]: https://github.com/FrameworkComputer/framework-daisy-zephyr
165+
102166
## Testing other boards
103167

104168
[Any Zephyr board](https://docs.zephyrproject.org/latest/boards/index.html#) can

ports/zephyr-cp/boards/board_aliases.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ cp_board_alias(nordic_nrf54l15dk nrf54l15dk/nrf54l15/cpuapp)
4141
cp_board_alias(nordic_nrf54l15tag nrf54l15tag/nrf54l15/cpuapp)
4242
cp_board_alias(nordic_nrf54lm20dk nrf54lm20dk/nrf54lm20a/cpuapp)
4343
cp_board_alias(nordic_nrf54h20dk nrf54h20dk/nrf54h20/cpuapp)
44+
cp_board_alias(framework_daisy_kb_dvt1 daisy_kb_dvt1/nrf54lm20a/cpuapp)
45+
cp_board_alias(framework_daisy_dongle_dvt1 daisy_dongle_dvt1/nrf54lm20a/cpuapp)
4446
cp_board_alias(nordic_nrf5340dk nrf5340dk/nrf5340/cpuapp)
4547
cp_board_alias(nordic_nrf7002dk nrf7002dk/nrf5340/cpuapp)
4648
cp_board_alias(nxp_frdm_mcxn947 frdm_mcxn947/mcxn947/cpu0)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Reading and writing the adapter's TX power from _bleio goes through the
2+
# controller's vendor-specific HCI commands, which only exist with this set.
3+
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
4+
5+
# Work around a latent bug in soc_flash_nrf_ticker.c: with the default
6+
# NRF_RRAM_WRITE_BUFFER_SIZE=1, FLASH_SLOT_WRITE (500 us) <
7+
# FLASH_SYNC_SWITCHING_TIME (1700 us), so `interval = duration -
8+
# FLASH_SYNC_SWITCHING_TIME` underflows and the next flash slot is scheduled
9+
# ~71 minutes out. Any write larger than a single 16-byte slot then times out
10+
# (-ETIMEDOUT / -116), e.g. persisting bond keys right after pairing.
11+
# See zephyr/tests/boards/nrf/rram/overlay-radio_sync.conf.
12+
CONFIG_NRF_RRAM_WRITE_BUFFER_SIZE=32
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Framework Daisy USB dongle, DVT1.
2+
//
3+
// The Zephyr board itself lives in the framework-daisy-zephyr module (a west
4+
// project, see zephyr-config/west.yml), so everything here is a delta on top of
5+
// it: what CircuitPython needs and the product firmware does not.
6+
7+
// USB CDC console and data plus the HID device. The dongle's USBHS is already
8+
// enabled and labeled zephyr_udc0 by the board -- and USB is the whole point of
9+
// this board, since the only other way in is the debug UART on P2.00/P2.02.
10+
#include "../../../app.overlay"
11+
12+
// Same reasoning, and the same layout, as the keyboard: daisy_partitions.dtsi
13+
// fills the 1940K of RRAM exactly, so a CIRCUITPY drive comes out of the
14+
// application slot. mcuboot's offset and the storage partition keep the
15+
// addresses the bootloader on the hardware was built for; only image-0 shrinks.
16+
//
17+
// Reinstalling firmware over MCUboot's serial recovery erases image-0 as *it*
18+
// understands it, the full 1828K, so a recovery flash takes CIRCUITPY with it.
19+
&slot0_partition {
20+
reg = <0x18000 DT_SIZE_K(1216)>;
21+
};
22+
23+
&cpuapp_rram {
24+
partitions {
25+
circuitpy_partition: partition@148000 {
26+
label = "circuitpy";
27+
reg = <0x148000 DT_SIZE_K(612)>;
28+
};
29+
};
30+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
NAME="Daisy Dongle DVT1"
2+
3+
# signed.bin installs over the bootloader already on the hardware;
4+
# merged.hex is bootloader plus application, for flashing over SWD.
5+
CIRCUITPY_BUILD_EXTENSIONS = ["signed.bin", "merged.hex", "elf"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Build MCUboot and sign the application for it. The dongle ships with MCUboot
2+
# in RRAM and prj.conf links the application at zephyr,code-partition, which the
3+
# board points at slot0 -- so without this the image lands at 0x18000 unsigned
4+
# and the bootloader already on the hardware refuses to start it.
5+
#
6+
# Everything about *how* MCUboot is built for this hardware comes from the
7+
# board's own Kconfig.sysbuild and sysbuild.cmake, which sysbuild picks up on
8+
# its own. The dongle signs "pure" where the keyboard signs prehashed, so the
9+
# two images are not interchangeable even though they share a key.
10+
SB_CONFIG_BOOTLOADER_MCUBOOT=y
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Reading and writing the adapter's TX power from _bleio goes through the
2+
# controller's vendor-specific HCI commands, which only exist with this set.
3+
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
4+
5+
# Work around a latent bug in soc_flash_nrf_ticker.c: with the default
6+
# NRF_RRAM_WRITE_BUFFER_SIZE=1, FLASH_SLOT_WRITE (500 us) <
7+
# FLASH_SYNC_SWITCHING_TIME (1700 us), so `interval = duration -
8+
# FLASH_SYNC_SWITCHING_TIME` underflows and the next flash slot is scheduled
9+
# ~71 minutes out. Any write larger than a single 16-byte slot then times out
10+
# (-ETIMEDOUT / -116), e.g. persisting bond keys right after pairing.
11+
# See zephyr/tests/boards/nrf/rram/overlay-radio_sync.conf.
12+
CONFIG_NRF_RRAM_WRITE_BUFFER_SIZE=32
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Framework Daisy keyboard, DVT1.
2+
//
3+
// The Zephyr board itself lives in the framework-daisy-zephyr module (a west
4+
// project, see zephyr-config/west.yml), so everything here is a delta on top of
5+
// it: what CircuitPython needs and the product firmware does not.
6+
7+
// USB CDC console and data plus the HID device. Daisy's USBHS is already
8+
// enabled and labeled zephyr_udc0 by the board.
9+
#include "../../../app.overlay"
10+
11+
// Daisy's RRAM is fully spoken for by daisy_partitions.dtsi -- 96K mcuboot,
12+
// 1828K image-0, 16K storage, exactly the 1940K there is -- so a CIRCUITPY
13+
// drive has to come out of the application slot. mcuboot's offset and the
14+
// storage partition (Bluetooth bond keys) stay exactly where the bootloader
15+
// already on the hardware expects them; only image-0 shrinks.
16+
//
17+
// If a build ever outgrows 1216K the linker says so, and the fix is to move
18+
// this boundary and the circuitpy offset below by the same amount.
19+
//
20+
// Reinstalling firmware over MCUboot's serial recovery erases image-0 as *it*
21+
// understands it, the full 1828K, so a recovery flash takes CIRCUITPY with it.
22+
&slot0_partition {
23+
reg = <0x18000 DT_SIZE_K(1216)>;
24+
};
25+
26+
&cpuapp_rram {
27+
partitions {
28+
circuitpy_partition: partition@148000 {
29+
label = "circuitpy";
30+
reg = <0x148000 DT_SIZE_K(612)>;
31+
};
32+
};
33+
};
34+
35+
// Bus names. Every enabled I2C controller reaches Python as board.I2C2x
36+
// whatever we do here; these labels are what make the names say which bus is
37+
// which, and zephyr_i2c is the label that additionally designates one of the
38+
// three as plain board.I2C.
39+
//
40+
// The header is the bus a user can physically reach, so it gets board.I2C.
41+
zephyr_i2c: &i2c23 {
42+
status = "okay";
43+
};
44+
45+
// Touchpad, behind the tp_power rail the board brings up at boot.
46+
tp_i2c: &i2c22 {
47+
status = "okay";
48+
};
49+
50+
// The nPM1300. Exposed for reading charge state; writing to it controls the
51+
// rails the SoC itself runs on.
52+
npmc_i2c: &i2c21 {
53+
status = "okay";
54+
};
55+
56+
// This port has no pwmio, and an enabled PWM instance takes its pins away from
57+
// digitalio through pinctrl. Give the four white pairing LEDs, the status RGB
58+
// and the backlight back to CircuitPython as plain GPIO -- board.WHITE_LED_1
59+
// through _4, board.RED_LED / GREEN_LED / BLUE_LED, and board.BACKLIGHT.
60+
&pairing_leds {
61+
status = "disabled";
62+
};
63+
64+
&rgb_pwmleds {
65+
status = "disabled";
66+
};
67+
68+
&backlight_pwms {
69+
status = "disabled";
70+
};
71+
72+
&pwm20 {
73+
status = "disabled";
74+
};
75+
76+
&pwm21 {
77+
status = "disabled";
78+
};
79+
80+
&pwm22 {
81+
status = "disabled";
82+
};
83+
84+
/ {
85+
zephyr,user {
86+
// Pins no driver owns still deserve a name in `board`. The key
87+
// matrix is not part of the Zephyr board at all -- ZMK describes
88+
// it in its own shield -- so it is named here, giving
89+
// board.KSI0..KSI7 and board.KSO0..KSO17. Taken from
90+
// framework-daisy-zmk's daisy_kb_dvt1_nrf54lm20a_cpuapp_zmk.dts.
91+
//
92+
// The matrix is col2row and has no diodes, so three keys at the
93+
// corners of a rectangle read as a fourth. KSO1 and KSO2 are on
94+
// pins that power up as NFC; the board's UICR already releases
95+
// them.
96+
//
97+
// Only the controller and pin are read here. How a pin is driven
98+
// is up to the Python code, so the flags are written as the
99+
// board's defaults rather than as the matrix scan needs them.
100+
ksi-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>,
101+
<&gpio0 1 GPIO_ACTIVE_HIGH>,
102+
<&gpio0 2 GPIO_ACTIVE_HIGH>,
103+
<&gpio0 3 GPIO_ACTIVE_HIGH>,
104+
<&gpio0 4 GPIO_ACTIVE_HIGH>,
105+
<&gpio0 5 GPIO_ACTIVE_HIGH>,
106+
<&gpio0 6 GPIO_ACTIVE_HIGH>,
107+
<&gpio0 7 GPIO_ACTIVE_HIGH>;
108+
109+
kso-gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>,
110+
<&gpio1 1 GPIO_ACTIVE_HIGH>,
111+
<&gpio1 18 GPIO_ACTIVE_HIGH>,
112+
<&gpio1 19 GPIO_ACTIVE_HIGH>,
113+
<&gpio1 4 GPIO_ACTIVE_HIGH>,
114+
<&gpio1 5 GPIO_ACTIVE_HIGH>,
115+
<&gpio1 6 GPIO_ACTIVE_HIGH>,
116+
<&gpio1 7 GPIO_ACTIVE_HIGH>,
117+
<&gpio1 8 GPIO_ACTIVE_HIGH>,
118+
<&gpio1 9 GPIO_ACTIVE_HIGH>,
119+
<&gpio1 10 GPIO_ACTIVE_HIGH>,
120+
<&gpio1 11 GPIO_ACTIVE_HIGH>,
121+
<&gpio1 12 GPIO_ACTIVE_HIGH>,
122+
<&gpio1 13 GPIO_ACTIVE_HIGH>,
123+
<&gpio1 14 GPIO_ACTIVE_HIGH>,
124+
<&gpio1 15 GPIO_ACTIVE_HIGH>,
125+
<&gpio1 16 GPIO_ACTIVE_HIGH>,
126+
<&gpio1 17 GPIO_ACTIVE_HIGH>;
127+
128+
// Keyboard backlight, freed from pwm22 above.
129+
backlight-gpios = <&gpio3 11 GPIO_ACTIVE_HIGH>;
130+
};
131+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
NAME="Daisy Keyboard DVT1"
2+
3+
# signed.bin installs over the bootloader already on the hardware;
4+
# merged.hex is bootloader plus application, for flashing over SWD.
5+
CIRCUITPY_BUILD_EXTENSIONS = ["signed.bin", "merged.hex", "elf"]

0 commit comments

Comments
 (0)