ENG | XAIO-nRF52840 (Seeed Studio) and MicroPython
Few MicroPython code snippets for XIAO-nRF52840 and XIAO Expansion board. LED, display, RTC and buzzer examples.
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
LED | Note |
---|---|
1 | Tiny green |
2 | Red |
3 | Green |
4 | Blue |
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 address | 7bit address | Device |
---|---|---|
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 :
wherea1
is alias forconnect /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:
- https://madflex.de/seeed-xiao-ble-nrf52840-with-micropython/
- https://madflex.de/micropython-and-bluetooth-on-nRF/
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.
Links
- XIAO nRF52840 wiki
- XIAO nRF52840 MicroPython
- XIAO Expansion Board Wiki
- XIAO nRF52840 Schema
- TLV733P voltage regulator (U6 in schema)
- NOTE: TLV733xx is series, it does not mean 3.3V output voltage for specified 1.4 input voltage. Sadly it needs higher input voltage. When 2.6V from 2xNiMH batteries is applied to input, output voltage is roughly 2.35V. Chip and SSD1306 OLED display work. Voltage regulator has 0.125V drop at 300mA and Vout=3.3V.
- BQ25101 battery charging circuit
- Charging characteristics of XIAO_nRF52840 and XIAO_ESP32C3
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.