Post

ENG | XAIO-nRF52840 (Seeed Studio) and MicroPython

Few MicroPython code snippets for XIAO-nRF52840 and XIAO Expansion board. LED, display, RTC and buzzer examples.

ENG | XAIO-nRF52840 (Seeed Studio) and MicroPython

This somewhat copies article about [nRF52840 Dongle](This was docummented for a dongle

Download and flash MicroPython firmware

Note that board has preloaded firmware for Sense version.

1
2
3
4
5
6
[pavel@marten -=- ~]$ cd Downloads
[pavel@marten -=- ~/Downloads]$ wget https://micropython.org/resources/firmware/SEEED_XIAO_NRF52-20250415-v1.25.0.uf2
Saving 'SEEED_XIAO_NRF52-20250415-v1.25.0.uf2'
HTTP response 200  [https://micropython.org/resources/firmware/SEEED_XIAO_NRF52-20250415-v1.25.0.uf2]
SEEED_XIAO_NRF52-202 100% [==============================================================================================================>]  511.00K    --.-KB/s
                          [Files: 1  Bytes: 511.00K [805.99KB/s] Redirects: 0  Todo: 0  Errors: 0

Now press reset button twice to enter bootloader mode and upload firmware

1
[pavel@marten -=- ~/Downloads]$ cp ~/Downloads/SEEED_XIAO_NRF52-20250415-v1.25.0.uf2 /run/media/pavel/XIAO-SENSE

On Linux, drive is not always automatically mounted - I guess it’s handled by KDE Plasma. To mount it, try the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
(.venv) [pavel@marten -=- ~]$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 025: ID 2357:0604 TP-Link TP-Link UB500 Adapter
Bus 001 Device 052: ID 2886:0045 Seeed Technology Co., Ltd. XIAO nRF52840 Sense
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub

(.venv) [pavel@marten -=- ~]$ udisksctl mount -b /dev/sda
==== AUTHENTICATING FOR org.freedesktop.udisks2.filesystem-mount ====
Authentication is required to mount Adafruit nRF UF2 (/dev/sda)
Authenticating as: Pavel Perina (pavel)
Password: 
==== AUTHENTICATION COMPLETE ====
Mounted /dev/sda at /run/media/pavel/XIAO-SENSE

Command above has /dev/sda. This is correct for NRF52 development board, for Raspberry Pi Pico and clones, it’s /dev/sda1.

Test basics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[pavel@marten -=- /home/pavel/Downloads]$ mpremote a1
Connected to MicroPython at /dev/ttyACM1
Use Ctrl-] or Ctrl-x to exit this shell
MicroPython v1.25.0 on 2025-04-15; XIAO nRF52840 Sense with NRF52840
Type "help()" for more information.
>>> help()
Welcome to MicroPython!

For online docs please visit http://docs.micropython.org/

Quick overview of commands for the board:
  board.LED(n)    -- create an LED object for LED n (n=1,2,3,4)

If compiled with SD=<softdevice> the additional commands are
available:
  ble.enable()    -- enable bluetooth stack
  ble.disable()   -- disable bluetooth stack
  ble.enabled()   -- check whether bluetooth stack is enabled
  ble.address()   -- return device address as text string

Control commands:
  CTRL-A        -- on a blank line, enter raw REPL mode
  CTRL-B        -- on a blank line, enter normal REPL mode
  CTRL-D        -- on a blank line, do a soft reset of the board
  CTRL-E        -- on a blank line, enter paste mode

For further help on a specific object, type help(obj)
>>> import board
>>> l1=board.LED(1)
>>> l1.on()
>>> l1.off()
>>> l1.on()
>>> l2=board.LED(2)
>>> l2.on()
>>> l3=board.LED(3)
>>> l3.on()
>>> board.LED(4).on()
>>> import sys
>>> sys.platform
'nrf52'
>>> sys.implementation
(name='micropython', version=(1, 25, 0, ''), _machine='XIAO nRF52840 Sense with NRF52840', _mpy=7942, _build='SEEED_XIAO_NRF52')
>>> import time
>>> time.ticks_ms()
306696
LEDNote
1Tiny green
2Red
3Green
4Blue

When you have XIAO Expansion Board, you may try I2C scan:

1
2
3
4
>>> from machine import Pin, I2C
>>> i2c_0 = I2C(0, sda=Pin(4), scl=Pin(5))
>>> i2c_0.scan()
[60, 81]
8bit address7bit addressDevice
0x78 (120)0x3c (60)128x64 0.96” OLED (ZJY-2864KSWPG01) with SSD1306 controller
0xA2 (162)0x51 (81)RTC PCF8563T

MicroPython uses 7bit address, while documentation usually mentions 8bit address (or pair), lowest bits is zero for write and one for read operation.

Test RTC

1
2
3
>>> data = i2c_0.readfrom_mem(81, 0x02, 7)
>>> data
b'\xa6\x88\x80\x8c\xa0\xa7p'

Hmm. We got something.

Test BLE functionality

1
2
3
4
>>> import bluetooth
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'bluetooth'

Actually it has very weird BLE module with no useable functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import ble
>>> help(ble)
object <module 'ble'> is of type module
  __name__ -- ble
  enable -- <function>
  disable -- <function>
  enabled -- <function>
  address -- <function>
>>> print(dir(ble))
['__class__', '__name__', '__dict__', 'address', 'disable', 'enable', 'enabled']
>>> ble.enable()
SoftDevice enabled
>>> ble.address()
'fb:58:6b:52:8b:b7'
>>> ble.enabled()
1

Then, there is weird ubluepy module with basically no documentation. There are like three examples on internet. Good luck using it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>>> import ubluepy
>>> print(dir(ubluepy))
['__class__', '__name__', 'Characteristic', 'DefaultDelegate', 'Peripheral', 'ScanEntry', 'Scanner', 'Service', 'UUID', '__dict__', 'constants']
>>> help(ubluepy)
object <module 'ubluepy'> is of type module
  __name__ -- ubluepy
  Peripheral -- <class 'Peripheral'>
  Scanner -- <class 'Scanner'>
  ScanEntry -- <class 'ScanEntry'>
  DefaultDelegate -- <class 'DefaultDelegate'>
  UUID -- <class 'UUID'>
  Service -- <class 'Service'>
  Characteristic -- <class 'Characteristic'>
  constants -- <class 'constants'>
>>> help(ubluepy.Scanner)
object <class 'Scanner'> is of type type
  scan -- <function>
>>> help(ubluepy.Peripheral)
object <class 'Peripheral'> is of type type
  withDelegate -- <function>
  setNotificationHandler -- <function>
  setConnectionHandler -- <function>
  getServices -- <function>
  connect -- <function>
  advertise -- <function>
  advertise_stop -- <function>
  disconnect -- <function>
  addService -- <function>
>>> help(ubluepy.Peripheral.advertise)
object <function> is of type function  

But I found two files on github that are somewhat relevant:

1
2
3
4
5
>>> import ubluepy
>>> p = ubluepy.Peripheral()
>>> p.advertise(device_name="BleTest")
>>> # Beyond this, good luck and have fun!
>>> # I was not even able to put custom data to the advertisment packet.

Uploading program

This was docummented for a dongle

Now, let’s have fun with expansion board!

Buzzer demo

NOTE: port A3/D3 is actually P0.29

1
2
3
4
5
6
>>> buzzer = PWM(Pin(29))
>>> buzzer
<PWM: Pin=29 freq=0Hz duty=0 invert=0 id=1 channel=0>
>>> buzzer.freq(500)
>>> buzzer.duty_u16(30000)
>>> buzzer.deinit()

Display demo

See SSD1306 Tutorial, driver is on github.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[pavel@marten -=- /home/pavel/Downloads]$ wget https://raw.githubusercontent.com/micropython/micropython-lib/refs/heads/master/micropython/drivers/display/ssd1306/ssd1306.py
Saving 'ssd1306.py'
HTTP response 200  [https://raw.githubusercontent.com/micropython/micropython-lib/refs/heads/master/micropython/drivers/display/ssd1306/ssd1306.py]
ssd1306.py           100% [====================================================================>]    1.51K    --.-KB/s
                          [Files: 1  Bytes: 1.51K [5.15KB/s] Redirects: 0  Todo: 0  Errors: 0   ]

[pavel@marten -=- /home/pavel/Downloads]$ ls -la ssd1306.py
-rw-r--r--. 1 pavel pavel 4922 Jun  8 21:35 ssd1306.py
[pavel@marten -=- /home/pavel/Downloads]$ mpremote a1 fs cp ssd1306.py :

cp ssd1306.py :
[pavel@marten -=- /home/pavel/Downloads]$ mpremote a1
Connected to MicroPython at /dev/ttyACM1
Use Ctrl-] or Ctrl-x to exit this shell
>>> from machine import Pin, I2C
>>> import ssd1306
>>> i2c = I2C(0, sda=Pin(4), scl=Pin(5))
>>> oled = ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
>>> oled.fill(1)
>>> oled.show()
>>> oled.fill(0)
>>> oled.show()
>>> oled.text("Goodnight world!", 0,0)
>>> oled.show()

This should

  • Download display driver wget ...
  • Upload it to development board mpremote a1 fs cp ssd1306.py : where a1 is alias for connect /dev/ttyACM1
  • Run serial console
  • fill screen with white
  • update screen (screen is lit now)
  • … the rest is quite obvious

RTC demo

RTC requires CR1220 3V battery.

Create helper (this command creates file rtc_pcf8563.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cat > rtc_pcf8563.py << EOF
# rtc_pcf8563.py
from machine import I2C

__all__ = ['read_rtc', 'set_rtc', 'init_rtc']

def _bcd_to_dec(bcd):
    return (bcd >> 4) * 10 + (bcd & 0x0f)

def _dec_to_bcd(dec):
    return ((dec // 10) << 4) + (dec % 10)

def init_rtc(i2c_bus, address=0x51):
    global _i2c, _addr
    _i2c = i2c_bus
    _addr = address

def read_rtc():
    data = _i2c.readfrom_mem(_addr, 0x02, 7)
    seconds = _bcd_to_dec(data[0] & 0x7f)
    minutes = _bcd_to_dec(data[1] & 0x7f)
    hours = _bcd_to_dec(data[2] & 0x3f)
    day = _bcd_to_dec(data[3] & 0x3f)
    weekday = data[4] & 0x07
    month = _bcd_to_dec(data[5] & 0x1f)
    year = _bcd_to_dec(data[6]) + 2000
    return (year, month, day, hours, minutes, seconds, weekday)

def set_rtc(year, month, day, hours, minutes, seconds, weekday=0):
    data = bytearray([
        _dec_to_bcd(seconds),
        _dec_to_bcd(minutes),
        _dec_to_bcd(hours),
        _dec_to_bcd(day),
        weekday,
        _dec_to_bcd(month),
        _dec_to_bcd(year - 2000)
    ])
    _i2c.writeto_mem(_addr, 0x02, data)
EOF

Copy it to device

1
mpremote a1 fs cp rtc_pcf8563.py :

Play with it

1
2
3
4
5
6
7
8
9
Connected to MicroPython at /dev/ttyACM1
Use Ctrl-] or Ctrl-x to exit this shell
>>> from machine import Pin, I2C
>>> import rtc_pcf8563
>>> i2c = I2C(0, sda=Pin(4), scl=Pin(5))
>>> rtc_pcf8563.init_rtc(i2c)
>>> rtc_pcf8563.read_rtc()
(2025, 6, 8, 15, 5, 35, 0)
>>> rtc_pcf8563.set_rtc(2025, 6, 8, 14, 30, 0, 0)

NOTES:

  • Weekday 0 is sunday
  • Time was set previously
  • RTC needs CR1220 battery (D=12mm, h=2.0mm) to keep time without USB power.

Conclusion

This device is very nice and I’m happy I bough expansion board.

What I found problematic is ubluepy module which is dead, undocumented and almost impossible to find (try “import ubluepy” filetype:py in google or similar), but similar bluepy exists, so maybe with some guesses and effort it’s possible to use it, but my hopes are low.

I found two blog posts:

with the very same conclusion.

However, for MicroPython, I’d recommend sticking with Raspberry Pi Pico family, which is very cheap, well documented, well supported, and with large user community. I haven’t tested ESP32 thou. But I’m afraid that combination of nRF52840 and MicroPython does not make sense, as boards are more expensive and selling point is BLE and/or low-power applications. For use with expansion board, XIAO Seeed RP2040 will work as well. This does not mean board is useless, as it can be programmed in C+Zephyr RTOS, where BLE works and MicroPython is still good for fast checks if other stuff works.

TODO: test SPI flash

Notes

Expansion board has charging current set by 2K resistors connected to ISET pins of ETA6003 chip to 1V/2KΩ=500mA, whereas XIAO nRF82840 has charging current either 50mA (P0.13 high) or 100mA (P0.13 low) so it can use smaller capacity batteries.

This post is licensed under CC BY 4.0 by the author.