We'll be using CircuitPython for this project. Are you new to using CircuitPython? No worries, there is a full getting started guide here.
Adafruit suggests using the Mu editor to edit your code and have an interactive REPL in CircuitPython. You can learn about Mu and its installation in this tutorial.
In addition, the LSM303 module is required. We have a great guide on that. Make sure you have the latest version of this module.
A Guided Tour
The warm_up
function is used when starting up to make multiple reads from the magnetometer. Experience showed that the initial reads were unreliable; the magnetometer needs to warm up it would seem. The NeoPixels are set to blue during this phase.
def warm_up(): fill(BLUE) for _ in range(100): _, _, _ = compass.magnetic time.sleep(0.010)
Compass Calibration
The compass needs to be calibrated before use. This adapts it to the variation in the chip as well as the local magnetic environment. Simply spin the compass when the NeoPixels are green. If the compass acts erratically, pressing the left button (A) will do an additional calibration phase, which will hopefully improve the calibration. Be sure to spin it a couple full rotations for the widest range of values.
There are a few ways to run a calibration. If the compass has never been calibrated (the raw_mins
array contains all 1000.0), it will be calibrated. Additionally, it will be done if the A/left button is pressed when the power is turned on or reset is pressed. In both these cases, it is assumed that you have a USB serial connection. When calibration completes, two lines are output, similar to:
raw_mins = [-2.45455, 2.81818] raw_maxes = [-1.72727, 3.54545]
Copy these into the code, replacing the two that look the same (other than the values), and load onto the Circuit Playground Express. Subsequently powering on, resetting, or restarting will then skip calibration.
Calibration works by spending 10 seconds making readings, and keeping track of the lowest and highest values along the X and Y axes. At the end, the origin offset is calculated by finding the center point of the readings (averaging the minimum and maximum values). Now we have the offset for readings as well as the expected value ranges.
def calibrate(do_the_readings): values = [0.0, 0.0] start_time = time.monotonic() fill(GREEN) # Update the high and low extremes if do_the_readings: while time.monotonic() - start_time < 10.0: values[0], values[1], _ = compass.magnetic values[1] *= -1 # accel is upside down, so y is reversed if values[0] != 0.0 and values[1] != 0.0: # ignore the random 0 values for i in range(2): if values[i] < raw_mins[i]: raw_mins[i] = values[i] if values[i] > raw_maxes[i]: raw_maxes[i] = values[i] # Recompute the correction and the correct mins/maxes for i in range(2): corrections[i] = (raw_maxes[i] + raw_mins[i]) / 2 mins[i] = raw_mins[i] - corrections[i] maxes[i] = raw_maxes[i] - corrections[i] fill(BLACK)
The main loop runs about 20 times per second. It starts by checking for the left button being pushed. If it is, more calibration is done. Next the magnetometer values are read for X and Y.
if not calibrate_button.value: calibrate(True) x, y, _ = compass.magnetic y = y * -1
Notice that the Y value is negated since the breakout is mounted upside down, flipping the Y axis.
Occasionally there will be a problem and the read values will both be zero. If so the reading is ignored.
if x != 0.0 and y != 0.0:
Assuming the values are good, they are normalized. This is a simple mapping of the value from the range that resulted from calibration, to the range from -100 to +100.
def normalize(value, in_min, in_max): mapped = (value - in_min) * 200 / (in_max - in_min) + -100 return max(min(mapped, 100), -100)
Once we have normalized values the atan2
function is used to compute the angle of the vector made by those values. The angle will be in radians (ranging from -pi to +pi). That gets converted to degrees by multiplying by 180/pi. Now we have a value in degrees that ranges from -180 to +180. That gets 180 added to it to give a value from 0 to 360. This is then divided by 30 to give the number of a wedge which corresponds to 1 of 10 NeoPixel positions. Because the wedge at the top is defined by the angles at +/-15 degrees, 15 is added (and the result clipped to 360 by using the modulus operator %) before the division.
normalized_x = normalize(x - corrections[0], mins[0], maxes[0]) normalized_y = normalize(y - corrections[1], mins[1], maxes[1]) compass_heading = int(math.atan2(normalized_y, normalized_x) * 180.0 / math.pi) # compass_heading is between -180 and +180 since atan2 returns -pi to +pi # this translates it to be between 0 and 360 compass_heading += 180 direction_index = ((compass_heading + 15) % 360) // 30
The final step is to update the NeoPixels. The led_patterns
array that is defined at the top of the code is used to specify which pixels correspond to each wedge. Notice that two of the wedges specify two pixels, while the others use only one. This is because the Circuit Playgrounds only have 10 NeoPixels; the top and bottom positions are used for the USB and battery connectors. To work around this, the two adjacent NeoPixels are used.
pixels.fill(BLACK) for l in led_patterns[direction_index]: pixels[l] = RED pixels.show()
The complete code is below:
# SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries # # SPDX-License-Identifier: MIT """ Circuit Playground Express Compass Adafruit invests time and resources providing this open source code. Please support Adafruit and open source hardware by purchasing products from Adafruit! Written by Dave Astels for Adafruit Industries Copyright (c) 2018 Adafruit Industries Licensed under the MIT license. All text above must be included in any redistribution. """ import time import math import sys import board import busio import adafruit_lsm303 import neopixel import digitalio BLACK = 0x000000 RED = 0xFF0000 GREEN = 0x00FF00 BLUE = 0x0000FF i2c = busio.I2C(board.SCL, board.SDA) compass = adafruit_lsm303.LSM303(i2c) compass.mag_rate = adafruit_lsm303.MAGRATE_30 #compass.mag_gain = adafruit_lsm303.MAGGAIN_8_1 calibrate_button = digitalio.DigitalInOut(board.BUTTON_A) calibrate_button.direction = digitalio.Direction.INPUT calibrate_button.pull = digitalio.Pull.UP #------------------------------------------------------------------------------- # Replace these two lines with the results of calibration #------------------------------------------------------------------------------- raw_mins = [1000.0, 1000.0] raw_maxes = [-1000.0, -1000.0] #------------------------------------------------------------------------------- mins = [0.0, 0.0] maxes = [0.0, 0.0] corrections = [0.0, 0.0] pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=.2, auto_write=False) led_patterns = [[4, 5], [5], [6], [7], [8], [9], [9, 0], [0], [1], [2], [3], [4]] def fill(colour): pixels.fill(colour) pixels.show() def warm_up(): fill(BLUE) for _ in range(100): _, _, _ = compass.magnetic time.sleep(0.010) def calibrate(do_the_readings): values = [0.0, 0.0] start_time = time.monotonic() fill(GREEN) # Update the high and low extremes if do_the_readings: while time.monotonic() - start_time < 10.0: values[0], values[1], _ = compass.magnetic values[1] *= -1 # accel is upside down, so y is reversed if values[0] != 0.0 and values[1] != 0.0: # ignore the random 0 values for i in range(2): if values[i] < raw_mins[i]: raw_mins[i] = values[i] if values[i] > raw_maxes[i]: raw_maxes[i] = values[i] # time.sleep(0.005) # Recompute the correction and the correct mins/maxes for i in range(2): corrections[i] = (raw_maxes[i] + raw_mins[i]) / 2 mins[i] = raw_mins[i] - corrections[i] maxes[i] = raw_maxes[i] - corrections[i] fill(BLACK) def normalize(value, in_min, in_max): mapped = (value - in_min) * 200 / (in_max - in_min) + -100 return max(min(mapped, 100), -100) # Setup warm_up() if not calibrate_button.value or (raw_mins[0] == 1000.0 and raw_mins[1] == 1000.0): print("Compass calibration") raw_mins[0] = 1000.0 raw_mins[1] = 1000.0 raw_maxes[0] = -1000.0 raw_maxes[1] = -1000.0 calibrate(True) print("Calibration results") print("Update the corresponding lines near the top of the code\n") print("raw_mins = [{0}, {1}]".format(raw_mins[0], raw_mins[1])) print("raw_maxes = [{0}, {1}]".format(raw_maxes[0], raw_maxes[1])) sys.exit() else: calibrate(False) while True: if not calibrate_button.value: calibrate(True) x, y, _ = compass.magnetic y = y * -1 if x != 0.0 and y != 0.0: normalized_x = normalize(x - corrections[0], mins[0], maxes[0]) normalized_y = normalize(y - corrections[1], mins[1], maxes[1]) compass_heading = int(math.atan2(normalized_y, normalized_x) * 180.0 / math.pi) # compass_heading is between -180 and +180 since atan2 returns -pi to +pi # this translates it to be between 0 and 360 compass_heading += 180 direction_index = ((compass_heading + 15) % 360) // 30 pixels.fill(BLACK) for l in led_patterns[direction_index]: pixels[l] = RED pixels.show() time.sleep(0.050)
Page last edited January 21, 2025
Text editor powered by tinymce.