MicroPython on the Seeed Studio Wio Terminal: it works!

A while back, Seeed Studio sent me one of their Wio Terminal devices to review. It was pretty neat, but being limited to using Arduino to access all of it features was a little limiting. I still liked it, though, and wrote about it here: SeeedStudio Wio Terminal

Small screen device showing geometric pattern
Wio Terminal, doing a thing

There wasn’t any proper MicroPython support for the device as it used a MicroChip/Atmel SAMD51 ARM® Cortex®-M4 micro-controller. But since I wrote the review, one developer (robert-hh) has worked almost entirely solo to make SAMD51 and SAMD21 support useful in mainline MicroPython.

Hey! Development is still somewhere between “not quite ready for prime time” and “beware of the leopard”. MicroPython on the SAMD51 works remarkably well for supported boards, but don’t expect this to be beginner-friendly yet.

I thought I’d revisit the Wio Terminal and see what I could do using a nightly build (downloaded from Downloads – Wio Terminal D51R – MicroPython). Turns out, most of the board works really well!

What doesn’t work yet

  • Networking/Bluetooth – this is never going to be easy, especially with Seeed Studio using a separate RTL8720 SoC. It may not be entirely impossible, as previously thought, but so far, wifi support seems quite far away
  • QSPI flash for program storagethis is not impossible, just not implemented yet this works now too, but it’s quite slow since it relies on a software SPI driver. More details: samd51: MicroPython on the Seeed Wio Terminal · Discussion #9838 · micropython
  • RTCthis is a compile-time option, but isn’t available on the stock images. Not all SAMD51 boards have a separate RTC oscillator, and deriving the RTC from the system oscillator would be quite wobbly. RTC works now! It may even be possible to provide backup battery power and have it keep time when powered off. VBAT / PB03 / SPI_SCK is broken out to the 40-pin connector.

What does work

  • Display – ILI9341 320×240 px, RGB565 via SPI
  • Accelerometer – LIS3DHTR via I²C
  • Microphone – analogue
  • Speaker – more like a buzzer, but this little PWM speaker element does allow you to play sounds
  • Light Sensor – via analogue photo diode
  • IR emitter – PWM, not tied to any hardware protocol
  • Internal LED – a rather faint blue thing, but useful for low-key signalling
  • Micro SD Card – vi SPI. Works well with MicroPython’s built-in virtual file systems
  • Switches and buttons – three buttons on the top, and a five-way mini-joystick
  • I²C via Grove Connector – a second, separate I²C channel.

I’ll go through each of these here, complete with a small working example.

Wio Terminal main board
Inside the remarkably hard-to-open Wio Terminal

LED

Let’s start with the simplest feature: the tiny blue LED hidden inside the case. You can barely see this, but it glows out around the USB C connector on the bottom of the case.

  • MicroPython interfaces: machine.Pin, machine.PWM
  • Control pin: Pin(“LED_BLUE”) or Pin(15), or Pin(“PA15”): any one of these would work.

Example: Wio-Terminal-LED.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-LED.py - blink the internal blue LED
# scruss, 2022-10
# -*- coding: utf-8 -*-

from machine import Pin
from time import sleep_ms

led = Pin("LED_BLUE", Pin.OUT)  # or Pin(15) or Pin("PA15")

try:
    while True:
        led.value(not led.value())
        sleep_ms(1200)
except:
    led.value(0)  # turn it off if user quits
    exit()

IR LED

I don’t have any useful applications of the IR LED for device control, so check out Awesome MicroPython’s IR section for a library that would work for you.

  • MicroPython interfaces: machine.PWM
  • Control pin: Pin(“PB31”)

Example: Wio-Terminal-IR_LED.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-IR_LED.py - blink the internal IR LED
# scruss, 2022-10
# -*- coding: utf-8 -*-

# Hey! This is a completely futile exercise, unless you're able
# to see into the IR spectrum. But we're here to show you the pin
# specification to use. For actual useful libraries to do stuff with
# IR, take a look on https://awesome-micropython.com/#ir

# So this is a boring blink, 'cos we're keeping it short here.
# You might be able to see the LED (faintly) with your phone camera

from machine import Pin, PWM
from time import sleep_ms

ir = PWM(Pin("PB31"))  # "IR_CTL" not currently defined

try:
    while True:
        ir.duty_u16(32767)  # 50% duty
        ir.freq(38000)  # fast flicker
        sleep_ms(1200)
        ir.duty_u16(0)  # off
        sleep_ms(1200)
except:
    ir.duty_u16(0)  # turn it off if user quits
    exit()

Buttons

There are three buttons on top, plus a 5-way joystick on the front. Their logic is inverted, so they read 0 when pressed, 1 when not. It’s probably best to use machine.Signal with these to make operation more, well, logical.

  • MicroPython interface: machine.Signal (or machine.Pin)
  • Control pins: Pin(“BUTTON_3”) or Pin(92) or Pin(PC28) – top left; Pin(“BUTTON_2”) or Pin(91) or Pin(PC27) – top middle; Pin(“BUTTON_1”) or Pin(90) or Pin(PC26) – top right; Pin(“SWITCH_B”) or Pin(108) or Pin(PD12) – joystick left; Pin(“SWITCH_Y”) or Pin(105) or Pin(PD09) – joystick right; Pin(“SWITCH_U”) or Pin(116) or Pin(PD20) – joystick up; Pin(“SWITCH_X”) or Pin(104) or Pin(PD08) – joystick down; Pin(“SWITCH_Z”) or Pin(106) or Pin(PD10) – joystick button

Example: Wio-Terminal-Buttons.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Buttons.py - test the buttons
# scruss, 2022-10
# -*- coding: utf-8 -*-

# using Signal because button sense is inverted: 1 = off, 0 = on
from machine import Pin, Signal
from time import sleep_ms

pin_names = [
    "BUTTON_3",  # Pin(92)  or Pin(PC28) - top left
    "BUTTON_2",  # Pin(91)  or Pin(PC27) - top middle
    "BUTTON_1",  # Pin(90)  or Pin(PC26) - top right
    "SWITCH_B",  # Pin(108) or Pin(PD12) - joystick left
    "SWITCH_Y",  # Pin(105) or Pin(PD09) - joystick right
    "SWITCH_U",  # Pin(116) or Pin(PD20) - joystick up
    "SWITCH_X",  # Pin(104) or Pin(PD08) - joystick down
    "SWITCH_Z",  # Pin(106) or Pin(PD10) - joystick button
]

pins = [None] * len(pin_names)
for i, name in enumerate(pin_names):
    pins[i] = Signal(Pin(name, Pin.IN), invert=True)

while True:
    for i in range(len(pin_names)):
        print(pins[i].value(), end="")
    print()
    sleep_ms(100)

Buzzer

A very quiet little PWM speaker.

  • MicroPython interfaces: machine.PWM
  • Control pin: Pin(“BUZZER”) or Pin(107) or Pin(“PD11”)

Example: Wio-Terminal-Buzzer.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Buzzer.py - play a scale on the buzzer with PWM
# scruss, 2022-10
# -*- coding: utf-8 -*-

from time import sleep_ms
from machine import Pin, PWM

pwm = PWM(Pin("BUZZER", Pin.OUT))  # or Pin(107) or Pin("PD11")
cmaj = [262, 294, 330, 349, 392, 440, 494, 523]  # C Major Scale frequencies

for note in cmaj:
    print(note, "Hz")
    pwm.duty_u16(32767)  # 50% duty
    pwm.freq(note)
    sleep_ms(225)
    pwm.duty_u16(0)  # 0% duty - silent
    sleep_ms(25)

Light Sensor

This is a simple photo diode. It doesn’t seem to return any kind of calibrated value. Reads through the back of the case.

  • MicroPython interfaces: machine.ADC
  • Control pin: machine.ADC(“PD01”)

Example code: Wio-Terminal-LightSensor.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-LightSensor.py - print values from the light sensor
# scruss, 2022-10
# -*- coding: utf-8 -*-

from time import sleep_ms
from machine import ADC

# PD15-22C/TR8 photodiode
light_sensor = ADC("PD01")

while True:
    print([light_sensor.read_u16()])
    sleep_ms(50)

Microphone

Again, a simple analogue sensor:

  • MicroPython interfaces: machine.ADC
  • Control pin: machine.ADC(“MIC”)

Example: Wio-Terminal-Microphone.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Microphone.py - print values from the microphone
# scruss, 2022-10
# -*- coding: utf-8 -*-

from time import sleep_ms
from machine import ADC

mic = ADC("MIC")

while True:
    print([mic.read_u16()])
    sleep_ms(5)

Grove I²C Port

The Wio Terminal has two Grove ports: the one on the left (under the speaker port) is an I²C port. As I don’t know what you’ll be plugging in there, this example does a simple bus scan. You could make a, appalling typewriter if you really wanted.

  • MicroPython interfaces: machine.I2C (channel 3), machine. Pin
  • Control pins: scl=Pin(“SCL1”), sda=Pin(“SDA1”)

Example: Wio-Terminal-Grove-I2C.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Grove-I2C.py - show how to connect on Grove I2C
# scruss, 2022-10
# -*- coding: utf-8 -*-

from machine import Pin, I2C

# NB: This doesn't do much of anything except list what's
# connected to the left (I²C) Grove connector on the Wio Terminal

i2c = I2C(3, scl=Pin("SCL1"), sda=Pin("SDA1"))
devices = i2c.scan()

if len(devices) == 0:
    print("No I²C devices connected to Grove port.")
else:
    print("Found these I²C devices on the Grove port:")
    for n, id in enumerate(devices):
        print(" device", n, ": ID", id, "(hex:", hex(id) + ")")

LIS3DH Accelerometer

This is also an I²C device, but connected to a different port (both logically and physically) than the Grove one.

  • MicroPython interfaces: machine.I2C (channel 4), machine. Pin
  • Control pins: scl=Pin(“SCL0”), sda=Pin(“SDA0”)
  • Library: from MicroPython-LIS3DH, copy lis3dh.py to the Wio Terminal’s small file system. Better yet, compile it to mpy using mpy-cross to save even more space before you copy it across

Example: Wio-Terminal-Accelerometer.py (based on tinypico-micropython/lis3dh library/example.py)

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Accelerometer.py - test out accelerometer
# scruss, 2022-10
# -*- coding: utf-8 -*-
# based on
#  https://github.com/tinypico/tinypico-micropython/tree/master/lis3dh%20library

import lis3dh, time, math
from machine import Pin, I2C

i2c = I2C(4, scl=Pin("SCL0"), sda=Pin("SDA0"))
imu = lis3dh.LIS3DH_I2C(i2c)

last_convert_time = 0
convert_interval = 100  # ms
pitch = 0
roll = 0

# Convert acceleration to Pitch and Roll
def convert_accell_rotation(vec):
    x_Buff = vec[0]  # x
    y_Buff = vec[1]  # y
    z_Buff = vec[2]  # z

    global last_convert_time, convert_interval, roll, pitch

    # We only want to re-process the values every 100 ms
    if last_convert_time < time.ticks_ms():
        last_convert_time = time.ticks_ms() + convert_interval

        roll = math.atan2(y_Buff, z_Buff) * 57.3
        pitch = (
            math.atan2((-x_Buff), math.sqrt(y_Buff * y_Buff + z_Buff * z_Buff)) * 57.3
        )

    # Return the current values in roll and pitch
    return (roll, pitch)


# If we have found the LIS3DH
if imu.device_check():
    # Set range of accelerometer (can be RANGE_2_G, RANGE_4_G, RANGE_8_G or RANGE_16_G).
    imu.range = lis3dh.RANGE_2_G

    # Loop forever printing values
    while True:
        # Read accelerometer values (in m / s ^ 2).  Returns a 3-tuple of x, y,
        # z axis values.  Divide them by 9.806 to convert to Gs.
        x, y, z = [value / lis3dh.STANDARD_GRAVITY for value in imu.acceleration]
        print("x = %0.3f G, y = %0.3f G, z = %0.3f G" % (x, y, z))

        # Convert acceleration to Pitch and Roll and print values
        p, r = convert_accell_rotation(imu.acceleration)
        print("pitch = %0.2f, roll = %0.2f" % (p, r))

        # Small delay to keep things responsive but give time for interrupt processing.
        time.sleep(0.1)

SD Card

  • MicroPython interfaces: machine.SPI (channel 6), machine.Pin, machine.Signal
  • Control Pins: Pin(“SD_SCK”), Pin(“SD_MOSI”), Pin(“SD_MISO”) for SD access. Pin(“SD_DET”) is low if an SD card is inserted, otherwise high
  • Library: copy sdcard.py from micropython-lib to the Wio Terminal’s file system.

Rather than provide a small canned example (there’s one here, if you must: Wio-Terminal-SDCard.py) here’s my boot.py startup file, showing how I safely mount an SD card if there’s one inserted, but keep booting even if it’s missing:

# boot.py - MicroPython / Seeed Wio Terminal / SAMD51

import sys

sys.path.append("/lib")

import machine
import gc
import os
import sdcard

machine.freq(160000000)  # fast but slightly jittery clock
gc.enable()

# mount SD card if there's one inserted
try:
    sd_detected = machine.Signal(
        machine.Pin("SD_DET", machine.Pin.IN),
        invert=True,
    )
    sd_spi = machine.SPI(
        6,
        sck=machine.Pin("SD_SCK"),
        mosi=machine.Pin("SD_MOSI"),
        miso=machine.Pin("SD_MISO"),
        baudrate=40000000,
    )
    sd = sdcard.SDCard(sd_spi, machine.Pin("SD_CS"))
    if sd_detected.value() == True:
        os.mount(sd, "/SD")
        print("SD card mounted on /SD")
    else:
        raise Exception("SD card not inserted, can't mount /SD")
except:
    print("SD card not found")


ILI9341 Display

I’m going to use the library rdagger/micropython-ili9341: MicroPython ILI9341Display & XPT2046 Touch Screen Driver because it’s reliable, and since it’s written entirely in MicroPython, it’s easy to install. It’s not particularly fast, though.

The Wio Terminal may have an XPT2046 resistive touch controller installed, but I haven’t been able to test it. There are LCD_XL, LCD_YU, LCD_XR and LCD_YD lines on the schematic that might indicate it’s there, though.

  • MicroPython interfaces: machine.SPI (channel 7), machine.Pin.
  • Control Pins: Pin(“LCD_SCK”), Pin(“LCD_MOSI”), Pin(“LCD_MISO”). Pin(“LED_LCD”) is the backlight control
  • Library: copy ili9341.py from rdagger /micropython-ili9341 to the Wio Terminal’s file system.

This demo draws rainbow-coloured diamond shapes that change continuously.

Example: Wio-Terminal-Screen.py

# MicroPython / Seeed Wio Terminal / SAMD51
# Wio-Terminal-Screen.py - output something on the ILI9341 screen
# scruss, 2022-10
# -*- coding: utf-8 -*-


from time import sleep
from ili9341 import Display, color565
from machine import Pin, SPI


def wheel565(pos):
    # Input a value 0 to 255 to get a colour value.
    # The colours are a transition r - g - b - back to r.
    # modified to return RGB565 value for ili9341 - scruss
    (r, g, b) = (0, 0, 0)
    if (pos < 0) or (pos > 255):
        (r, g, b) = (0, 0, 0)
    if pos < 85:
        (r, g, b) = (int(pos * 3), int(255 - (pos * 3)), 0)
    elif pos < 170:
        pos -= 85
        (r, g, b) = (int(255 - pos * 3), 0, int(pos * 3))
    else:
        pos -= 170
        (r, g, b) = (0, int(pos * 3), int(255 - pos * 3))
    return (r & 0xF8) << 8 | (g & 0xFC) << 3 | b >> 3


# screen can be a little slow to turn on, so use built-in
# LED to signal all is well
led = Pin("LED_BLUE", Pin.OUT)

backlight = Pin("LED_LCD", Pin.OUT)  # backlight is not a PWM pin
spi = SPI(
    7, sck=Pin("LCD_SCK"), mosi=Pin("LCD_MOSI"), miso=Pin("LCD_MISO"), baudrate=4000000
)
display = Display(spi, dc=Pin("LCD_D/C"), cs=Pin("LCD_CS"), rst=Pin("LCD_RESET"))
display.display_on()
display.clear()
led.on()  # shotgun debugging, embedded style
backlight.on()

# use default portrait settings: x = 0..239, y = 0..319
dx = 3
dy = 4
x = 3
y = 4
i = 0

try:
    while True:
        # display.draw_pixel(x, y, wheel565(i))
        display.fill_hrect(x, y, 3, 4, wheel565(i))
        i = (i + 1) % 256
        x = x + dx
        y = y + dy
        if x <= 4:
            dx = -dx
        if x >= 234:
            dx = -dx
        if y <= 5:
            dy = -dy
        if y >= 313:
            dy = -dy
except:
    backlight.off()
    led.off()
    display.display_off()

Seeeduino XIAO simple USB volume control with CircuitPython

round computer device with USB cable exiting at left. Small microcontroller at centreshowing wiring to LED ring and rotary encoder
Slightly blurry image of the underside of the device, showing the Seeeduino XIAO and the glow from the NeoPixel ring. And yes, the XIAO is really that small

Tod Kurt’s QTPy-knob: Simple USB knob w/ CircuitPython is a fairly simple USB input project that relies on the pin spacing of an Adafruit QT Py development board being the same as that on a Bourns Rotary Encoder. If you want to get fancy (and who wouldn’t?) you can add a NeoPixel Ring to get an RGB glow.

The QT Py is based on the Seeeduino XIAO, which is a slightly simpler device than the Adafruit derivative. It still runs CircuitPython, though, and is about the least expensive way of doing so. The XIAO is drop-in replacement for the Qt Py in this project, and it works really well! Everything you need for the project is described here: todbot/qtpy-knob: QT Py Media Knob using rotary encoder & neopixel ring

I found a couple of tiny glitches in the 3d printed parts, though:

  1. The diffuser ring for the LED ring is too thick for the encoder lock nut to fasten. It’s 2 mm thick, and there’s exactly 2 mm of thread left on the encoder.
  2. The D-shaft cutout in the top is too deep to allow the encoder shaft switch to trigger.

I bodged these by putting an indent in the middle of the diffuser, and filling the top D-shaft cutout with just enough Blu Tack.

Tod’s got a bunch of other projects for the Qt Py that I’m sure would work well with the XIAO: QT Py Tricks. And yes, there’s an “Output Farty Noises to DAC” one that, regrettably, does just that.

Maybe I’ll add some mass to the dial to make it scroll more smoothly like those buttery shuttle dials from old video editing consoles. The base could use a bit more weight to stop it skiting about the desk, so maybe I’ll use Vik’s trick of embedding BB gun shot into hot glue. For now, I’ve put some rubber feet on it, and it mostly stays put.


Hey! Unlike my last Seeed Studio device post, I paid for all the bits mentioned here.

SeeedStudio Wio Terminal

Small screen device showing geometric pattern
Wio Terminal displaying … some kind of nonsense of mine

Some months ago, when Chloe from Seeed Studio got in touch and asked me if I’d like to write about their new Wio Terminal device, I didn’t waste any time in saying yes. I mean, would you say no to all of this?

  • 120 / 200 MHz ARM Cortex-M4F core (MicroChip ATSAMD51P19: 512 KB Flash, 192 KB RAM) with additional 4 MB Flash program/data storage and micro-SD card slot;
  • 2.4″ 320 × 240 colour screen;
  • Realtek RTL8720DN wifi / Bluetooth transceiver;
  • buttons, joystick, accelerometer, RGB LED, light sensor and IR transmitter;
  • neat case (72 × 57 × 10.4 mm) with magnetic and screw mounts;
  • Grove connectors for wiring free sensor mounting.
Wio Terminal internals
Wio Terminal internals (from Wio Terminal User Manual)

It’s got a Raspberry Pi-type header that claims compatibility. The documentation for all the ports is a cut above the usual no-name ESP8266 / STM32 stuff:

Wio Terminal pinout
Wio Terminal pinout (from Wio Terminal User Manual)

The device is in a really tidy package. Its screen, although not a touchscreen, is super sharp.

individual pixels magnified from the Wio Terminal screen
The screen is very nice: individual pixels zoomed in

There are three ways of programming the Wio Terminal:

  1. Arduino
  2. SeeedStudio’s own ArduPy
  3. CircuitPython

Each of these have pros and cons.

Arduino (get started)

  • the fastest code execution: compiled ARM binary code
  • the only way to access wifi and Bluetooth (currently)
  • slow development cycle
    (… is it just me, or has the Arduino IDE got really 🦥🦥🦥 recently?)

ArduPy (get started)

  • SeeedStudio’s own ingenious port of MicroPython to the Arduino API, as MicroPython doesn’t (yet) support the SAMD51 chip
  • Works almost, but not quite, exactly like you’d expect MicroPython to work
  • It’s a great and amazing effort, but it’s essentially a solo project, so documentation and examples are few.

CircuitPython (install)

  • developed and maintained by Adafruit as a fork of an earlier version of MicroPython
  • very actively developed, with a huge library of supported devices.
CircuitPython text mode Mandelbrot set: about all I managed with that system. And yes, I did eventually take the screen protector sheet off

Here’s the major problem I have with all of these development toolkits for the Wio Terminal: none of them provide high-level access to the device’s sensors and outputs. Compare this with Adafruit, who create things like the Adafruit_CircuitPython_CircuitPlayground module. On that board, you can access the LEDs, speaker, etc without having to go back to the schematic to find out which pin each of them is connected to. Because of this, I’ve only been able to scratch the surface of what the Wio Terminal can do.

In summary:

  • It’s really nicely made, and the µC inside is very powerful
  • It’s not too expensive: US $29
  • All of the software stacks aren’t particularly mature
    (but it’s only been available since March 2020)
  • Documentation is at the “datasheet + trial/error” stage
  • The 40-pin connector isn’t completely compatible with Raspberry Pi:
    • Serial RX/TX aren’t crossed
    • ILI9341 display isn’t broken out to header
  • … although you can (and I verified this in a live demo at a user group) use a Wio Terminal as a tiny HMI (Human Machine Interface) USB display for Linux machines

The Wio Terminal is a little too powerful to be thought of as a simple micro-controller platform, but not quite powerful enough to be a standalone general purpose computer. I wish I could find a great application for it, though.


This post is modified from the talk I gave to the Toronto Raspberry Pi Meetup group in December 2020: SeeedStudio Wio Terminal: Applications with the Raspberry Pi. Thanks to Chloe and all at SeeedStudio for sending it to me.

Seeed is the IoT hardware enabler providing services over 10 years that empower makers to realize their projects and products. Seeed offers a wide array of hardware platforms and sensor modules ready to be integrated with existing IoT platforms and one-stop PCB fabrication and PCB assembly service. Seeed Studio provides a wide selection of electronic parts including Arduino  Raspberry Pi and many different development board platforms  Especially the Grove System help engineers and makers to avoid jumper wires problems. Seeed Studio has developed more than 280 Grove modules covering a wide range of applications that can fulfill a variety of needs. 

https://www.seeedstudio.com/

Disclosure: SeedStudio sent me this unit free of charge.