Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Comment on lines +127 to +131

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Arrr, add the missing UF2 workflow steps.

This section says to copy an “exported” .uf2 file but does not explain how to export it or how to trigger the board’s UF2 bootloader. Add the exact IDE action and reset sequence so users can complete the documented workflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 127 - 131, Expand the “Uploading via UF2” section
with the exact IDE action used to export the .uf2 file, then document how to
reset the board into its UF2 bootloader, including the required reset-button or
reset-sequence steps before copying the file to the mounted drive.


### 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.
Expand Down
86 changes: 86 additions & 0 deletions boards.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion platform.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
tools.adafruit-nrfutil.program.pattern="{cmd}" {program.verbose} dfu serial -pkg "{build.path}/{build.project_name}.zip" -p {serial.port} -b 115200 --singlebank
125 changes: 125 additions & 0 deletions tools/uf2conv.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +31 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Arrr, reject malformed HEX records before generating firmware.

The parser accepts invalid type-specific lengths—for example, extended-address records with payloads other than 2 bytes—and accepts malformed EOF records. It also stops at the first EOF and silently ignores any subsequent records, which can produce a partial or incorrectly addressed UF2 image.

Validate each record type’s required byte count and address, require exactly one valid EOF record, and reject non-empty content after EOF.

Also applies to: 57-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/uf2conv.py` around lines 31 - 44, The HEX parser must reject
type-specific structural errors and trailing data after EOF. Update the record
parser function containing the length/checksum validation to enforce required
payload lengths and addresses for each supported record type, including a valid
zero-length EOF record; then update the main record-processing loop to require
exactly one EOF, reject any non-empty records after it, and fail when EOF is
missing instead of silently stopping.



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(
"<IIIIIIII",
UF2_MAGIC_START0,
UF2_MAGIC_START1,
UF2_FLAG_FAMILY_ID_PRESENT,
address,
UF2_PAYLOAD_SIZE,
index,
total,
family_id,
)
block = header + bytes(payload)
block += bytes(512 - len(block) - 4)
block += struct.pack("<I", UF2_MAGIC_END)
uf2_file.write(block)


def main(argv):
parser = argparse.ArgumentParser(description="Convert an Intel HEX file to UF2.")
parser.add_argument("-f", "--family", type=parse_int, required=True, help="UF2 family ID")
parser.add_argument("-o", "--output", required=True, help="output UF2 file")
parser.add_argument("input", help="input Intel HEX file")
args = parser.parse_args(argv)

data = read_ihex(args.input)
blocks = build_blocks(data)
write_uf2(blocks, args.family, args.output)
print(f"Wrote {args.output} ({len(blocks)} UF2 blocks)")


if __name__ == "__main__":
main(sys.argv[1:])
45 changes: 45 additions & 0 deletions variants/nice_nano_v2/variant.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
#include "nrf.h"

const uint32_t g_ADigitalPinMap[] =
{
// nice!nano v2 edge castellations
8, // D0 is P0.08
6, // D1 is P0.06
17, // D2 is P0.17
20, // D3 is P0.20
22, // D4 is P0.22
24, // D5 is P0.24
32, // D6 is P1.00
11, // D7 is P0.11
36, // D8 is P1.04
38, // D9 is P1.06
9, // D10 is P0.09

// Exposed bottom pads
33, // D11 is P1.01
34, // D12 is P1.02
39, // D13 is P1.07

// nice!nano v2 edge castellations
43, // D14 is P1.11
45, // D15 is P1.13
10, // D16 is P0.10
42, // D17 is P1.10
47, // D18 is P1.15
2, // D19 is P0.02 (AIN0)
29, // D20 is P0.29 (AIN5)
31, // D21 is P0.31 (AIN7)

15, // D22 is P0.15 (status LED)
4, // D23 is P0.04 (battery voltage)
13, // D24 is P0.13 (VCC cutoff)
};

void initVariant()
{
pinMode(PIN_LED, OUTPUT);
ledOff(PIN_LED);
}
Loading