To the Home Page

Installing bootloader and CircuitPython on Waveshare Core52840

Published on July 2, 2025 · Reading time: 6 minutes

Waveshare Core52840 is a small (18 × 13.5 mm) breakout board featuring a Nordic nRF52840 microcontroller and some additional components (decoupling capacitors, crystal oscillator, antenna circuitry), all in an easy-to-solder form factor with castellated pads.

Core52840 (source: Waveshare)
Core52840 (source: Waveshare)

The microcontroller itself has a 32-bit ARM CPU running at 64 MHz, 256 kB of RAM, 1 MB of flash memory, basic communication protocols, analog-to-digital converter, cryptography engine and Secure Boot, and a variety of wireless technologies such as Bluetooth, NFC or Zigbee. The total power consumption can be as low as 10 mA with BLE on, according to the datasheet.

My goal is to make the damn thing run CircuitPython, no matter (sic!) how limited. I can figure out all the missing parts once I get to the REPL.

Table of Contents

Problem #1: how to wire it

The module is designed to be tested with Waveshare’s evaluation kit, which is a large carrier board with LEDs, buttons, LDR, buzzer, voltage regulator, USB-to-UART adapter, CR2032 battery retainer, SD card slot, and a lot of expansion headers for compatibility with Arduino shields and Raspberry Pi HATs.

But I need none of that. Since there’s a schematic available, I can eliminate all the unnecessary components one by one.

Full schematic (source: Waveshare)
Full schematic (source: Waveshare)

A bare minimum
A bare minimum

It’s good news because I own both USB-C and voltage regulator breakout boards. Interesting that both 5 V and 3.3 V are supplied; maybe one of these can be removed.

The pads aren’t that small (1.1 mm pitch), so with enough patience, I was able to make this:

The janky wiring setup I’ve made
The janky wiring setup I’ve made

Unfortunately, it didn’t work. No messages shown in dmesg. Voltages looked fine, and there were no shorts. This means there’s no useful firmware preinstalled (for the general audience).

Problem #2: finding the bootloader

Core52840 is not supported by CircuitPython. Well, there are no compatible Waveshare nRF52 boards as of now (July 2025).

The bearer of bad news?
The bearer of bad news?

But, when browsing through the list of all nRF52 boards, one of these caught my attention: ADM_52840 Breakout board for Holyiot_18010. I’ve found some 18010 modules on AliExpress, and they look suspiciously similar to the Core52840. I don’t know who cloned who, but I think you may know the answer.

Spot the difference
Spot the difference

Okay, so now I have a (potentially working) CircuitPython UF2 image, but there’s still no UF2 bootloader. Luckily, Adafruit has me covered – there’s a GitHub repo with bootloader binaries for all nRF52 boards that officially run CircuitPython.

Problem #3: installing the bootloader

If only I had a hardware debugger… I don’t want to buy anything for a one-time operation.

Previously, I was using Raspberry Pi with OpenOCD, and decided to give it a try once again.

Raspberry Pi 5 is incompatible because it uses the RP1 coprocessor for GPIO.

This adds three more wires to my cable spaghetti. Pi’s GPIO24 is connected to nRF’s SWDIO, GPIO25 to SWCLK, and there’s a common ground.

More cables!
More cables!

There’s an ongoing myth that OpenOCD needs to be built from source, but it’s no longer true. All the necessary packages can be installed directly from Raspberry Pi OS repositories:

sudo apt install openocd telnet wget

Download the firmware:

wget -O bootloader.hex https://github.com/adafruit/Adafruit_nRF52_Bootloader/releases/download/0.9.2/adm_b_nrf52840_1_bootloader-0.9.2_s140_6.1.1.hex

Start the OpenOCD. This configuration will emulate a hardware debugger (OpenOCD can also work with commercially available debuggers), set the nRF52 target, and choose valid pins:

sudo openocd \
    -f "interface/raspberrypi-native.cfg" \
    -c "transport select swd" \
    -f "target/nrf52.cfg" \
    -c "adapter gpio swclk 25" \
    -c "adapter gpio swdio 24"
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
swd
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : BCM2835 GPIO JTAG/SWD bitbang driver
Info : clock speed 1006 kHz
Info : SWD DPIDR 0x2ba01477
Info : [nrf52.cpu] Cortex-M4 r0p1 processor detected
Info : [nrf52.cpu] target has 6 breakpoints, 4 watchpoints
Info : starting gdb server for nrf52.cpu on 3333
Info : Listening on port 3333 for gdb connections

Open another terminal and connect to the debugger session via telnet:

telnet localhost 4444
Trying ::1...
Connection failed: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
>

Stop the execution of the currently running program:

> halt
[nrf52.cpu] halted due to breakpoint, current mode: Handler External Interrupt(22)
xPSR: 0x81000026 pc: 0x00026c24 msp: 0x20005938

Create a backup of existing data, just in case:

> dump_image factory.bin 0 0x100000
dumped 1048576 bytes in 11.661472s (87.811 KiB/s)

As a side quest, I’ve opened the file in hex editor. The original data is 0x2CAC0 bytes long (everything else is 0xFF) and contains some strings, such as “The requested TX/RX packet length is too long by %u/%u octets”. Maybe there is some UART echo program preinstalled?

Erase the flash memory:

> nrf5 mass_erase
nRF52840-xxAA(build code: D0) 1024kB Flash, 256kB RAM
Mass erase completed.

Finally, flash the bootloader:

> program bootloader.hex verify
[nrf52.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0xfffffffe msp: 0xfffffffc
** Programming Started **
Padding image section 0 at 0x00000b00 with 1280 bytes
Flash write discontinued at 0x00025de8, next section at 0x000f4000
Adding extra erase range, 0x00025de8 .. 0x00025fff
Flash write discontinued at 0x000fbd10, next section at 0x000fd800
Adding extra erase range, 0x000fbd10 .. 0x000fbfff
Adding extra erase range, 0x000fd000 .. 0x000fd7ff
Adding extra erase range, 0x000fd858 .. 0x000fdfff
Adding extra erase range, 0x10001000 .. 0x10001013
Adding extra erase range, 0x1000101c .. 0x10001fff
** Programming Finished **
** Verify Started **
** Verified OK **

And reset the device:

> reset

That’s it. A new USB device will be detected, and CircuitPython can be installed as usual by dragging a UF2 file onto the virtual drive.

Board in a bootloader mode, with ADM840BOOT disk mounted
Board in a bootloader mode, with ADM840BOOT disk mounted

Problem #4: actually using it

This is not really a problem, but I’d like to keep the headings consistent :)

CircuitPython’s REPL works great, feels snappier than on SAMD21 and comparable to RP2040. The write speeds are kinda slow, which is very noticeable when copying new files onto a 256 kB user partition. I’ve read in the nRF52840 datasheet that erasing a single 4 kB block can take as long as 85 ms, maybe that’s the cause.

The standard library includes all the necessary bits (displayio, json, keypad, etc.). Bluetooth support is provided by the first-party adafruit_ble package. Every single pin seems to be available, so I can start testing the board with extra peripherals, both wired and wireless.

0;🐍BLE:Off | REPL | 10.0.0-alpha.7
Adafruit CircuitPython 10.0.0-alpha.7 on 2025-06-17; AtelierDuMaker nRF52840 Breakout with nRF52840
>>> import board
>>> for p in dir(board): print(p)
...
__class__ __name__  P0_02     P0_03     P0_04     P0_05     P0_06     P0_07
P0_08     P0_09     P0_10     P0_11     P0_12     P0_13     P0_14     P0_15
P0_16     P0_17     P0_19     P0_20     P0_21     P0_22     P0_23     P0_24
P0_25     P0_26     P0_27     P0_28     P0_29     P0_30     P0_31     P1_00
P1_01     P1_02     P1_03     P1_04     P1_05     P1_06     P1_07     P1_08
P1_09     P1_10     P1_11     P1_12     P1_13     P1_14     P1_15     __dict__
board_id
>>>

Check out other blog posts: