Pi Pico and Micropython

I bought a Raspberry Pi Pico from Sparkfun; cheap, cheap, cheap, fun, fun, fun. I also bought the experimenter kit with the step by step instructions which got me started but I’ve “advanced” beyond that and am trying to play with sensors. That means I need to use micropython and learn to code python, yikes. I’ve had some luck and got good results from an Adafruit BNO055.

The next sensor up is a cheap chinese junk QMC5883 magnetometer. I got that sensor to work with my Sparkfun pro micro running MultiWii 2.3, but that doesnt run on the Pico. I found a library that someone wrote for the QMC5883 for micropython but the problem is how to use it from the REPL or even a .py file. Here’s the github link: https://github.com/AngelouDi/QMC5883L_l … 83L_lib.py

Here’s the code that I cant run because when I try to instantiate the QMC5883L class, it tells me I gave it 0 arguments, but there are no arguments for the class. How do I use this code?

REG_X_LSB = 0x00
REG_X_MSB = 0x01
REG_Y_LSB = 0x02
REG_Y_MSB = 0x03
REG_Z_LSB = 0x04
REG_Z_MSB = 0x05
REG_STAT1 = 0x06
REG_TMP_LSB = 0x07
REG_TMP_MSB = 0x08
REG_CTRL1 = 0x09
REG_CTRL2 = 0x0A

MODE_SBY = 0b00
MODE_CON = 0b01

ODR_010 = 0b00 << 2
ODR_050 = 0b01 << 2
ODR_100 = 0b10 << 2
ODR_200 = 0b11 << 2

RNG_2G = 0b00 << 4
RNG_8G = 0b01 << 4

OSR_512 = 0b00 << 6
OSR_256 = 0b01 << 6
OSR_128 = 0b10 << 6
OSR_64 = 0b11 << 6

SOFT_RST = 0x80


def print_in_bits(byte):
    bits = []
    if type(byte).__name__ == "bytes":
        byte = int.from_bytes(byte, "little")
    for i in range(0, 8):
        bits.append(byte >> 7 - i & 1)
    print(bits)


def print_in_bits16(byte):
    if type(byte).__name__ == "bytes":
        byte = int.from_bytes(byte, "little")
    bits = []
    for i in range(0, 16):
        try:
            bits.append(byte >> (15 - i) & 1)
        except ValueError:
            pass
    print(bits)


def twoscomplement_to_dec(twos):
    dec = 0
    for i in range(0, 14):
        dec += (twos >> i & 1) * 2 ** i
    return dec


def setup_control_register(i2c, device_id, osr, rng, odr, continuous):
    ctrl1_register = 0b00000000

    if osr == 256:
        ctrl1_register |= OSR_256
    elif osr == 128:
        ctrl1_register |= OSR_128
    elif osr == 64:
        ctrl1_register |= OSR_64
    else:
        ctrl1_register |= OSR_512

    if rng == 2:
        ctrl1_register |= RNG_2G
    else:
        ctrl1_register |= RNG_8G

    if odr == 10:
        ctrl1_register |= ODR_010
    elif odr == 100:
        ctrl1_register |= ODR_100
    elif odr == 200:
        ctrl1_register |= ODR_200
    else:
        ctrl1_register |= ODR_050

    if continuous:
        ctrl1_register |= MODE_CON
    else:
        ctrl1_register |= MODE_SBY

    ctrl1_register = ctrl1_register.to_bytes(2, 'little')

    i2c.writeto_mem(device_id, REG_CTRL1, ctrl1_register)


def get_status(i2c, device_id):
    status = i2c.readfrom_mem(device_id, REG_STAT1, 1)
    status = int.from_bytes(status, "little")
    DRDY = status & 1 == 1
    OVL = status >> 1 & 1 == 1
    DOR = status >> 2 & 1 == 1

    return {"DRDY": DRDY, "OVL": OVL, "DOR": DOR}


def get_data(i2c, device_id, rng):
    data = i2c.readfrom_mem(device_id, REG_X_LSB, 6)

    x_lsb = data[REG_X_LSB]
    x_msb = data[REG_X_MSB]
    y_lsb = data[REG_Y_LSB]
    y_msb = data[REG_Y_MSB]
    z_lsb = data[REG_Z_LSB]
    z_msb = data[REG_Z_MSB]

    x = x_msb << 8 | x_lsb
    y = y_msb << 8 | y_lsb
    z = z_msb << 8 | z_lsb

    x = twoscomplement_to_dec(x)
    y = twoscomplement_to_dec(y)
    z = twoscomplement_to_dec(z)

    if rng == 2:
        x = twoscomplement_to_dec(x) / 12000
        y = twoscomplement_to_dec(y) / 12000
        z = twoscomplement_to_dec(z) / 12000
    else:
        x = twoscomplement_to_dec(x) / 3000
        y = twoscomplement_to_dec(y) / 3000
        z = twoscomplement_to_dec(z) / 3000

    print({"x": x, "y": y, "z": z})
    return {"x": x, "y": y, "z": z}


def get_temp(i2c, id=0xd):
    tmp_lsb = int.from_bytes(i2c.readfrom_mem(id, REG_TMP_LSB, 1), "little")
    tmp_msb = int.from_bytes(i2c.readfrom_mem(id, REG_TMP_MSB, 1), "little")

    print_in_bits(tmp_lsb)
    print_in_bits(tmp_msb)
    tmp = tmp_msb << 8 | tmp_lsb

    tmp = twoscomplement_to_dec(tmp) / 100
    print(tmp)


class QMC5883L:

    def __init__(self, i2c, device_id=0xD, continuous=True, odr=50, rng=8, osr=512, reset=True):
        self.i2c = i2c
        self.device_id = device_id
        self.continuous = continuous
        self.odr = odr
        self.rng = rng
        self.osr = osr
        self.reset = reset
        self.magnet_data = {"x": 0, "y": 0, "z": 0}

    def init(self):
        if self.reset:
            self.soft_reset()
        setup_control_register(i2c=self.i2c, device_id=self.device_id, osr=self.osr, rng=self.rng, odr=self.odr,
                               continuous=self.continuous)

    def soft_reset(self):
        self.i2c.writeto_mem(self.device_id, REG_CTRL2, SOFT_RST.to_bytes(2, 'little'))

    def get_magnet_data(self, wait=False):
        while True:
            if get_status(i2c=self.i2c, device_id=self.device_id)["DRDY"]:
                self.magnet_data = get_data(i2c=self.i2c, device_id=self.rng, rng=self.rng)
                return self.magnet_data
            if not wait:
                return self.magnet_data

Here’s the best I can get out of it:

import machine

import time

i2c = machine.SoftI2C(sda=machine.Pin(18), scl=machine.Pin(19), timeout=1_000)

from qmc5883L import QMC5883L

qmc = QMC5883L

x = qmc.get_magnet_data

print(x)

<function get_magnet_data at 0x20010d00>

I’m gonna be forced to buy my components from AdaFruit where they answer questions and provide tutorials on using the products they sell.

Thanks Sparkfun, you had something good. It can still be good but you have to realize not all your customers are EE’s, so sometimes what seems fundamental has to be explained.

To be fair, you are asking sparkfun to support a “cheap chinese junk” sensor they don’t even carry. I don’t think even adarfuit supports products they don’t make/carry.

Have you asked the chinese vendor for help? Does the chinese vendor even offer help/support?

That’s fair, sorry. I bought the Pico from Sparkfun but not the sensor. It started with the HMC5883L no longer being offered as a breakout. I solved it. It turned out the free github code had the wrong address and one more error. I fixed it and now I get data.

Glad to hear it’s working now, have a great weekend!

@ListerHarry - That is great that you got this figured out. May I suggest that perhaps you can contribute to what you made to make it work on the forum so that other people in the future may benefit from it. That is one of the great things about Sparkfun’s (and Adafruit’s) forums. People make the support a two-way street and therefore it grows organically. If you are so inclined to contribute your solution think about it as “you scratch someone’s back today and hopefully you’ll benefit from someone else scratching yours in the future”. - either way just the mere fact that you reported success is great as it provides someone else using QMC5883 in the future some positive feedback that they could achieve success using Micropython.

Here’s what I did:

I changed device_id=self.rng to device_id=self.device_id

and

“device_id=0xD” to “device_id=0x0D”

Here is the corrected code:

REG_X_LSB = 0x00
REG_X_MSB = 0x01
REG_Y_LSB = 0x02
REG_Y_MSB = 0x03
REG_Z_LSB = 0x04
REG_Z_MSB = 0x05
REG_STAT1 = 0x06
REG_TMP_LSB = 0x07
REG_TMP_MSB = 0x08
REG_CTRL1 = 0x09
REG_CTRL2 = 0x0A

MODE_SBY = 0b00
MODE_CON = 0b01

ODR_010 = 0b00 << 2
ODR_050 = 0b01 << 2
ODR_100 = 0b10 << 2
ODR_200 = 0b11 << 2

RNG_2G = 0b00 << 4
RNG_8G = 0b01 << 4

OSR_512 = 0b00 << 6
OSR_256 = 0b01 << 6
OSR_128 = 0b10 << 6
OSR_64 = 0b11 << 6

SOFT_RST = 0x80


def print_in_bits(byte):
    bits = []
    if type(byte).__name__ == "bytes":
        byte = int.from_bytes(byte, "little")
    for i in range(0, 8):
        bits.append(byte >> 7 - i & 1)
    print(bits)


def print_in_bits16(byte):
    if type(byte).__name__ == "bytes":
        byte = int.from_bytes(byte, "little")
    bits = []
    for i in range(0, 16):
        try:
            bits.append(byte >> (15 - i) & 1)
        except ValueError:
            pass
    print(bits)


def twoscomplement_to_dec(twos):
    dec = 0
    for i in range(0, 14):
        dec += (twos >> i & 1) * 2 ** i
    return dec


def setup_control_register(i2c, device_id, osr, rng, odr, continuous):
    ctrl1_register = 0b00000000

    if osr == 256:
        ctrl1_register |= OSR_256
    elif osr == 128:
        ctrl1_register |= OSR_128
    elif osr == 64:
        ctrl1_register |= OSR_64
    else:
        ctrl1_register |= OSR_512

    if rng == 2:
        ctrl1_register |= RNG_2G
    else:
        ctrl1_register |= RNG_8G

    if odr == 10:
        ctrl1_register |= ODR_010
    elif odr == 100:
        ctrl1_register |= ODR_100
    elif odr == 200:
        ctrl1_register |= ODR_200
    else:
        ctrl1_register |= ODR_050

    if continuous:
        ctrl1_register |= MODE_CON
    else:
        ctrl1_register |= MODE_SBY

    ctrl1_register = ctrl1_register.to_bytes(2, 'little')

    i2c.writeto_mem(device_id, REG_CTRL1, ctrl1_register)


def get_status(i2c, device_id):
    status = i2c.readfrom_mem(device_id, REG_STAT1, 1)
    status = int.from_bytes(status, "little")
    DRDY = status & 1 == 1
    OVL = status >> 1 & 1 == 1
    DOR = status >> 2 & 1 == 1

    return {"DRDY": DRDY, "OVL": OVL, "DOR": DOR}


def get_data(i2c, device_id, rng):
    data = i2c.readfrom_mem(device_id, REG_X_LSB, 6)

    x_lsb = data[REG_X_LSB]
    x_msb = data[REG_X_MSB]
    y_lsb = data[REG_Y_LSB]
    y_msb = data[REG_Y_MSB]
    z_lsb = data[REG_Z_LSB]
    z_msb = data[REG_Z_MSB]

    x = x_msb << 8 | x_lsb
    y = y_msb << 8 | y_lsb
    z = z_msb << 8 | z_lsb

    x = twoscomplement_to_dec(x)
    y = twoscomplement_to_dec(y)
    z = twoscomplement_to_dec(z)

    if rng == 2:
        x = twoscomplement_to_dec(x) / 12000
        y = twoscomplement_to_dec(y) / 12000
        z = twoscomplement_to_dec(z) / 12000
    else:
        x = twoscomplement_to_dec(x) / 3000
        y = twoscomplement_to_dec(y) / 3000
        z = twoscomplement_to_dec(z) / 3000

    print({"x": x, "y": y, "z": z})
    return {"x": x, "y": y, "z": z}


def get_temp(i2c, id=0xd):
    tmp_lsb = int.from_bytes(i2c.readfrom_mem(id, REG_TMP_LSB, 1), "little")
    tmp_msb = int.from_bytes(i2c.readfrom_mem(id, REG_TMP_MSB, 1), "little")

    print_in_bits(tmp_lsb)
    print_in_bits(tmp_msb)
    tmp = tmp_msb << 8 | tmp_lsb

    tmp = twoscomplement_to_dec(tmp) / 100
    print(tmp)


class QMC5883L:

    def __init__(self, i2c, device_id=0x0D, continuous=True, odr=50, rng=8, osr=512, reset=True):
        self.i2c = i2c
        self.device_id = device_id
        self.continuous = continuous
        self.odr = odr
        self.rng = rng
        self.osr = osr
        self.reset = reset
        self.magnet_data = {"x": 0, "y": 0, "z": 0}

    def init(self):
        if self.reset:
            self.soft_reset()
        setup_control_register(i2c=self.i2c, device_id=self.device_id, osr=self.osr, rng=self.rng, odr=self.odr,
                               continuous=self.continuous)

    def soft_reset(self):
        self.i2c.writeto_mem(self.device_id, REG_CTRL2, SOFT_RST.to_bytes(2, 'little'))

    def get_magnet_data(self, wait=False):
        while True:
            if get_status(i2c=self.i2c, device_id=self.device_id)["DRDY"]:
                self.magnet_data = get_data(i2c=self.i2c, device_id=self.device_id, rng=self.rng)
                return self.magnet_data
            if not wait:
                return self.magnet_data

Thank you - on behalf of anyone else that will find this useful in the future.

Quite clever the changes, and subtle.

Just a warning, it’s still not working quite right. One thing I tried was to not have the “if not wait: return self.magnet_data” not actually return self.magnet_data. I changed that to just “if not wait: self.magnet_data = …” and it seemed to help. Thonny will plot anything that gets printed and not returning the data twice, the plot shows wider range of movement in the data. I’ll keep messing with it.

One problem I had with the micropython code above seems related to calibration. I did not have a way to calibrate and without that, the data was confined to a small range. I tried the sensor with an Arduino(Sparkfun Pro Micro) and had the same restricted range, but I had calibration code and used it to get offsets. Once the offsets were applied in Arduino, the sensor data was full range 0-360 degrees. To get it working on the Pico, I found a way to run Arduino code on the Pico.

So,I’ve since temporarily abandoned running micropython on the Pico. With the Arduino IDE, there is a board definition for the RP2040(Pico) that allows running Arduino code. The IDE outputs a .UF2 file(found in temp directory) and that gets dragged and dropped to the Pico while it is in debug(hold down reset and plug into USB) mode. There were some minor additions I had to make to some magnetometer code, like defining the I2C pins. It runs magnetometer code for the QMC5883 written in Arduino.

I’ll eventually try writing some calibration code for micropython, and code that applies the offsets.

Did you ever check or stumble onto https://github.com/jposada202020/MicroPython_QMC5883L ?

I’ll check it out. It looks interesting at first glance.

I got it working, but used different code from this link: https://github.com/robert-hh/QMC5883

I made note eventually of how the good Arduino code setup the QMC5883L and used the same values. That was the problem it seems. All the micropython code examples I tried used different values for oversampling, sensitivity, and update rate. When that was the same in micropython, suddenly I got the 1-2 degree accuracy the data sheet claims and full 360 degree output. Here is my version of micropython code that uses the linked library. There are other libraries imported like nanogui that could be eliminated to just get magnetometer output printed.

# aclock..py Test/demo program for nanogui
# Orinally for ssd1351-based OLED displays but runs on most displays
# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431
# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2018-2020 Peter Hinch
import math
import time
# Initialise hardware and framebuf before importing modules.
from color_setup import ssd  # Create a display instance
from nanogui import refresh  # Color LUT is updated now.
from label import Label
from dial import Dial, Pointer
refresh(ssd, True)  # Initialise and clear display.

# Now import other modules
#from bno055 import BNO055
from qmc5883l import QMC5883L
i2c = machine.SoftI2C(sda=machine.Pin(20), scl=machine.Pin(21), timeout=1_000) 
#imu = BNO055(i2c)
qmc = QMC5883L(i2c)


import cmath
import utime
from writer import CWriter

# Font for CWriter
import arial10 as arial10
from colors import *
declination = 0.191986
#pi          = 3.14159265359

x_min = -956
x_max = 1817
y_min = -750
y_max = 1736
z_min = -32
z_max = 3112

x_offset = (x_min + x_max) / 2 ####(_vCalibration[0][0] + _vCalibration[0][1])/2
y_offset = (y_min + y_max) / 2 #########(_vCalibration[1][0] + _vCalibration[1][1])/2
z_offset = (z_min + z_max) / 2 ##########(_vCalibration[2][0] + _vCalibration[2][1])/2;
x_avg_delta = (x_max - x_min) / 2 #######(_vCalibration[0][1] - _vCalibration[0][0])/2;
y_avg_delta = (y_max - y_min) / 2 #########3(_vCalibration[1][1] - _vCalibration[1][0])/2;
z_avg_delta = (z_max - z_min) / 2 ########(_vCalibration[2][1] - _vCalibration[2][0])/2;

avg_delta = (x_avg_delta + y_avg_delta + z_avg_delta) / 3

x_scale = avg_delta / x_avg_delta
y_scale = avg_delta / y_avg_delta
z_scale = avg_delta / z_avg_delta

#refresh(ssd, True)  # Initialise and clear display.
def acompass():
    uv = lambda phi : cmath.rect(1, phi)  # Return a unit vector of phase phi
    pi = cmath.pi
    # Instantiate CWriter
    CWriter.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
    wri = CWriter(ssd, arial10, GREEN, BLACK)  # Report on fast mode. Or use verbose=False
    wri.set_clip(True, True, False)

    # Instantiate displayable objects
    ######dial = Dial(wri, 5, 5, height = 105, ticks = 24, bdcolor=None,
    dial = Dial(wri, 5, 5, height = 108, ticks = 24, bdcolor=None,
                label='BNO055 Compass', style = Dial.COMPASS, pip=RED)  # Border in fg color
    #lbltim = Label(wri, 5, 85, 35)
    heading = Pointer(dial)
    hstart = 0 + 0.92j # Heading pointer 
    while True:
        #x, y, z = imu.euler()
        #x,y,z, temp = qmc.read_raw()
        x,y,z = qmc.read_raw()
        
        Calx = (x - x_offset) * x_scale
        Caly = (y - y_offset) * y_scale
        Calz = (z - z_offset) * z_scale
        
        headin = math.atan2(Caly, Calx)
        #headin = math.atan2(y, x)
        headin = headin + declination
#Due to declination check for >360 degree

        if(headin > 2*pi):
          headin = headin - 2*pi
#check for sign
        if(headin < 0):
          headin = headin + 2*pi

#convert into angle
        headin = headin * 180/pi
        
        ###heading.value(hstart * uv(-x * pi/180), YELLOW)
        heading.value(hstart * uv(-headin * pi/180), YELLOW)
        #dial.text('Heading: {:5.2f}'.format(*imu.euler()))
        ###dial.text('Heading: {:5.2f}'.format(*qmc.read_raw()))
        dial.text('Heading: {:5.2f}'.format(headin))
        print(Calx,Caly,Calz,headin)
        refresh(ssd)
        #utime.sleep(1)
        time.sleep(0.01)

acompass()

Just for the exercise in micropython, I went to the link offered months ago. https://github.com/jposada202020/MicroPython_QMC5883L

I had to change some file names since I just have everything in one directory on the RP2040 Pico. I’m not sure how bad that is as a practice, but that’s what I’ve done and I can use the class. Anyway, that library works like a champ too.

Glad to see you made it through. And thanks for sharing.

I neglected to mention one detail if using this library: https://github.com/jposada202020/MicroPython_QMC5883L

It works like a champ if it is calibrated. To do that, I used https://github.com/mprograms/QMC5883LCompass/ to get the offsets then I switched back to micropython and added some code to apply the calibration.

If not calibrated, it seems to restrict output to one quadrant. That could vary I guess from sensor to sensor, but if calibrated it works.