From 1f082f7204486a397435931a595441bf5d58646a Mon Sep 17 00:00:00 2001 From: RandoSY Date: Thu, 9 Jul 2026 23:39:58 -0400 Subject: [PATCH] Add nice!nano v2 board support --- README.md | 7 ++ boards.txt | 86 ++++++++++++++++++++ platform.txt | 4 +- tools/uf2conv.py | 125 ++++++++++++++++++++++++++++++ variants/nice_nano_v2/variant.cpp | 45 +++++++++++ variants/nice_nano_v2/variant.h | 115 +++++++++++++++++++++++++++ 6 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 tools/uf2conv.py create mode 100644 variants/nice_nano_v2/variant.cpp create mode 100644 variants/nice_nano_v2/variant.h diff --git a/README.md b/README.md index f327643..357746a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This Arduino Core does **not** contain any BLE functionality. It has been design * [Adafruit Feather nRF52840 Sense](https://www.adafruit.com/product/4516) * [Adafruit ItsyBitsy nRF52840 Express](https://www.adafruit.com/product/4481) * [Ebyte E104-BT5040U](https://www.ebyte.com/en/product-view-news.html?id=1185) + * [nice!nano v2](https://nicekeyboards.com/docs/nice-nano/) * [XIAO nRF52840 Sense](https://wiki.seeedstudio.com/XIAO_BLE/) ### nRF52833 @@ -123,6 +124,12 @@ board = ... 1. Install adafruit-nrfutil if not already installed `pip install adafruit-nrfutil` 2. Select adafruit-nrfutil as the firmware uploader in the tools menu. +### Uploading via UF2 +Boards with an Adafruit UF2 bootloader, such as the nice!nano v2, can be reset +into the bootloader and programmed by copying the exported `.uf2` file to the +mounted bootloader drive. UF2 export requires Python 3 to be available on the +system path. + ### Uploading via (Nordic) nrfutil (for Nordic bootloader devices only) 1. Install nrfutil if not already installed `pip install nrfutil` 2. Select nrfutil as the firmware uploader in the tools menu. diff --git a/boards.txt b/boards.txt index f78ca62..da2b07e 100644 --- a/boards.txt +++ b/boards.txt @@ -989,6 +989,92 @@ BT5040.menu.bootloader.none.bootloader.flags=-DUSE_NORDIC_BL BT5040.menu.bootloader.none.build.bootloader.file= +# Nice Keyboards + +nicenano_v2.name=nice!nano v2 + +nicenano_v2.vid.0=0x239A +nicenano_v2.pid.0=0x80B3 +nicenano_v2.vid.1=0x239A +nicenano_v2.pid.1=0x00B3 + +nicenano_v2.upload.tool=openocd +nicenano_v2.upload.target=nrf52 +nicenano_v2.upload.jlink_device=nRF52840_xxAA +nicenano_v2.upload.use_1200bps_touch=true +nicenano_v2.upload.wait_for_upload_port=true +nicenano_v2.upload.maximum_size=1032192 +nicenano_v2.upload.maximum_data_size=262144 +nicenano_v2.bootloader.tool=openocd + +nicenano_v2.build.dfu_pattern={adafruit-nrfutil.dfu_pattern} +nicenano_v2.build.uf2_family=0xADA52840 +nicenano_v2.build.mcu=cortex-m4 +nicenano_v2.build.f_cpu=64000000 +nicenano_v2.build.board=NICE_NANO_V2 +nicenano_v2.build.core=nRF5 +nicenano_v2.build.variant=nice_nano_v2 +nicenano_v2.build.variant_system_lib= +nicenano_v2.build.usb_manufacturer="Nice Keyboards" +nicenano_v2.build.usb_product="nice!nano v2" +nicenano_v2.build.extra_flags=-DNRF52_SERIES -DNRF52840_XXAA {bootloader.flags} {build.flags.usb} -DCONFIG_NIMBLE_CPP_LOG_LEVEL={build.cpp_debug} {build.nimble_flags} +nicenano_v2.build.ldscript=nrf52840_s140_v6_adabl.ld +nicenano_v2.build.lfclk_flags=-DUSE_LFXO +nicenano_v2.build.vid=0x239A +nicenano_v2.build.pid=0x80B3 +nicenano_v2.build.float_flags=-mfloat-abi=hard -mfpu=fpv4-sp-d16 +nicenano_v2.recipe.objcopy.uf2.pattern="{tools.uf2conv.cmd}" "{runtime.platform.path}/tools/uf2conv.py" -f {build.uf2_family} -o "{build.path}/{build.project_name}.uf2" "{build.path}/{build.project_name}.hex" +nicenano_v2.recipe.output.tmp_file={build.project_name}.uf2 +nicenano_v2.recipe.output.save_file={build.project_name}.{build.variant}.uf2 + +nicenano_v2.menu.role.all=All +nicenano_v2.menu.role.all.build.nimble_flags= +nicenano_v2.menu.role.periph=Peripheral +nicenano_v2.menu.role.periph.build.nimble_flags=-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED -DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED +nicenano_v2.menu.role.adv=Advertiser +nicenano_v2.menu.role.adv.build.nimble_flags=-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED -DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED -DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL_DISABLED +nicenano_v2.menu.role.scan=Scanner +nicenano_v2.menu.role.scan.build.nimble_flags=-DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED -DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL_DISABLED -DCONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED +nicenano_v2.menu.role.central=Client +nicenano_v2.menu.role.central.build.nimble_flags=-DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL_DISABLED -DCONFIG_BT_NIMBLE_ROLE_BROADCASTER_DISABLED + +nicenano_v2.menu.cppDbgLvl.none=None +nicenano_v2.menu.cppDbgLvl.none.build.cpp_debug=0 +nicenano_v2.menu.cppDbgLvl.error=Error +nicenano_v2.menu.cppDbgLvl.error.build.cpp_debug=1 +nicenano_v2.menu.cppDbgLvl.warn=Warn +nicenano_v2.menu.cppDbgLvl.warn.build.cpp_debug=2 +nicenano_v2.menu.cppDbgLvl.info=Info +nicenano_v2.menu.cppDbgLvl.info.build.cpp_debug=3 +nicenano_v2.menu.cppDbgLvl.debug=Debug +nicenano_v2.menu.cppDbgLvl.debug.build.cpp_debug=4 + +nicenano_v2.menu.bootloader.adafruit_ble=Adafruit BLE (s140 v6 softdevice) +nicenano_v2.menu.bootloader.adafruit_ble.bootloader.flags=-DUSE_ADA_BL -DADA_SD_BL +nicenano_v2.menu.bootloader.adafruit_ble.upload.tool=adafruit-nrfutil +nicenano_v2.menu.bootloader.adafruit_ble.upload.protocol=adafruit-nrfutil +nicenano_v2.menu.bootloader.adafruit_ble.upload.maximum_size=827392 +nicenano_v2.menu.bootloader.adafruit_ble.upload.maximum_data_size=237568 +nicenano_v2.menu.bootloader.adafruit_ble.build.dfu_pattern={adafruit-nrfutil.dfu_pattern} +nicenano_v2.menu.bootloader.adafruit_ble.build.bootloader.file=tools/nrf52840_s140_v6_adabl.hex +nicenano_v2.menu.bootloader.adafruit_ble.build.ldscript=nrf52840_s140_v6_adabl.ld + +nicenano_v2.menu.bootloader.adafruit_serial=Adafruit Serial (no softdevice) +nicenano_v2.menu.bootloader.adafruit_serial.bootloader.flags=-DUSE_ADA_BL +nicenano_v2.menu.bootloader.adafruit_serial.upload.tool=adafruit-nrfutil +nicenano_v2.menu.bootloader.adafruit_serial.upload.protocol=adafruit-nrfutil +nicenano_v2.menu.bootloader.adafruit_serial.upload.maximum_size=978944 +nicenano_v2.menu.bootloader.adafruit_serial.upload.maximum_data_size=262136 +nicenano_v2.menu.bootloader.adafruit_serial.build.dfu_pattern={adafruit-nrfutil.dfu_pattern} +nicenano_v2.menu.bootloader.adafruit_serial.build.bootloader.file=tools/nrf52840_serial_adabl.hex +nicenano_v2.menu.bootloader.adafruit_serial.build.ldscript=nrf52840_serial_adabl.ld + +nicenano_v2.menu.bootloader.none=No bootloader +nicenano_v2.menu.bootloader.none.bootloader.flags= +nicenano_v2.menu.bootloader.none.build.bootloader.file=tools/none_bootloader.hex +nicenano_v2.menu.bootloader.none.build.ldscript=nrf52840_xxaa.ld + + # Seeedstudio seeed52840sense.name=Seeed XIAO nRF52840 Sense diff --git a/platform.txt b/platform.txt index bf913d7..3e719ab 100644 --- a/platform.txt +++ b/platform.txt @@ -61,6 +61,8 @@ adafruit-nrfutil.dfu_pattern="{tools.adafruit-nrfutil.cmd}" dfu genpkg --dev-typ none.dfu_pattern.cmd=no_dfu none.dfu_pattern.cmd.windows=no_dfu.bat none.dfu_pattern.path={runtime.platform.path}/tools +tools.uf2conv.cmd=python3 +tools.uf2conv.cmd.windows=python nordic.path={build.core.path}/nordic @@ -229,4 +231,4 @@ tools.adafruit-nrfutil.upload.pattern="{cmd}" {upload.verbose} dfu serial -pkg " tools.adafruit-nrfutil.program.params.verbose=--verbose tools.adafruit-nrfutil.program.params.quiet= -tools.adafruit-nrfutil.program.pattern="{cmd}" {program.verbose} dfu serial -pkg "{build.path}/{build.project_name}.zip" -p {serial.port} -b 115200 --singlebank \ No newline at end of file +tools.adafruit-nrfutil.program.pattern="{cmd}" {program.verbose} dfu serial -pkg "{build.path}/{build.project_name}.zip" -p {serial.port} -b 115200 --singlebank diff --git a/tools/uf2conv.py b/tools/uf2conv.py new file mode 100644 index 0000000..8a4aa2a --- /dev/null +++ b/tools/uf2conv.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import argparse +import struct +import sys + + +UF2_MAGIC_START0 = 0x0A324655 +UF2_MAGIC_START1 = 0x9E5D5157 +UF2_MAGIC_END = 0x0AB16F30 +UF2_FLAG_FAMILY_ID_PRESENT = 0x00002000 +UF2_PAYLOAD_SIZE = 256 + + +def parse_int(value): + return int(value, 0) + + +def parse_hex_line(line, line_number): + line = line.strip() + if not line: + return None + if not line.startswith(":"): + raise ValueError(f"line {line_number}: missing ':'") + + try: + raw = bytes.fromhex(line[1:]) + except ValueError as exc: + raise ValueError(f"line {line_number}: invalid hex") from exc + + if len(raw) < 5: + raise ValueError(f"line {line_number}: record is too short") + + count = raw[0] + if len(raw) != count + 5: + raise ValueError(f"line {line_number}: record length mismatch") + + if (sum(raw) & 0xFF) != 0: + raise ValueError(f"line {line_number}: checksum mismatch") + + address = (raw[1] << 8) | raw[2] + record_type = raw[3] + payload = raw[4:4 + count] + return record_type, address, payload + + +def read_ihex(filename): + data = {} + upper_address = 0 + + with open(filename, "r", encoding="ascii") as hex_file: + for line_number, line in enumerate(hex_file, 1): + record = parse_hex_line(line, line_number) + if record is None: + continue + + record_type, address, payload = record + if record_type == 0x00: + absolute = upper_address + address + for offset, value in enumerate(payload): + data[absolute + offset] = value + elif record_type == 0x01: + break + elif record_type == 0x02: + upper_address = int.from_bytes(payload, "big") << 4 + elif record_type == 0x04: + upper_address = int.from_bytes(payload, "big") << 16 + elif record_type in (0x03, 0x05): + continue + else: + raise ValueError(f"line {line_number}: unsupported record type {record_type:#x}") + + if not data: + raise ValueError("input HEX contains no data records") + + return data + + +def build_blocks(data): + blocks = {} + for address, value in data.items(): + block_address = address & ~(UF2_PAYLOAD_SIZE - 1) + if block_address not in blocks: + blocks[block_address] = bytearray([0xFF] * UF2_PAYLOAD_SIZE) + blocks[block_address][address - block_address] = value + + return sorted(blocks.items()) + + +def write_uf2(blocks, family_id, output): + total = len(blocks) + with open(output, "wb") as uf2_file: + for index, (address, payload) in enumerate(blocks): + header = struct.pack( + "