You need to run the calibration script and copy the values that it outputs into the compass script before it will be able to work properly. If you have not already done so, follow the instructions on the Calibrate page.
Drive Structure
After copying the files, your drive should look like the listing below. It can contain other files as well, but must contain these at a minimum.

Code
The code.py for the project is shown below.
Each section of code contains comments about its purpose.
Be sure to replace the calibration value variables near the top of the code with the ones you obtained by following the steps on the calibrate page.
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries # SPDX-License-Identifier: MIT # # Adapted from QualiaS3 Compass Learn Guide by Liz Clark (Adafruit Industries) # https://learn.adafruit.com/qualia-s3-compass/ import time from math import atan2, degrees, radians import adafruit_lis3mdl import board from adafruit_lsm6ds.lsm6dsox import LSM6DSOX from gamblor21_ahrs import mahony import bitmaptools from adafruit_gc9a01a import GC9A01A import adafruit_imageload import displayio from fourwire import FourWire # change these values to your calibration values MAG_MIN = [-75.1973, -22.5665, -34.5221] MAG_MAX = [-1.2131, 68.1379, 20.8126] GYRO_CAL = [-0.0038, -0.0026, -0.0011] # use filter for more accurate, but slightly slower readings # otherwise just reads from magnetometer ahrs = True center_x, center_y = 120, 120 i2c = board.STEMMA_I2C() accel_gyro = LSM6DSOX(i2c) magnetometer = adafruit_lis3mdl.LIS3MDL(i2c) # Create the AHRS filter ahrs_filter = mahony.Mahony(50, 5, 100) # Variable to account for the offset between raw heading values # and the orientation of the display. offset_angle = 90 def map_range(x, in_min, in_max, out_min, out_max): """ Maps a value from one range to another. :param x: The value to map :param in_min: The minimum value of the input range :param in_max: The maximum value of the input range :param out_min: The minimum value of the output range :param out_max: The maximum value of the output range :return: The value mapped to the output range """ mapped = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min if out_min <= out_max: return max(min(mapped, out_max), out_min) return min(max(mapped, out_max), out_min) last_heading = offset_angle heading = offset_angle last_update = time.monotonic() # last time we printed the yaw/pitch/roll values timestamp = time.monotonic_ns() # used to tune the frequency to approx 100 Hz # Display Setup spi = board.SPI() tft_cs = board.TX tft_dc = board.RX displayio.release_displays() display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=None) display = GC9A01A(display_bus, width=240, height=240) display.rotation = 90 # group to hold all of our visual elements main_group = displayio.Group() display.root_group = main_group # load the compass rose background image rose_bmp, rose_palette = adafruit_imageload.load("compass_rose.png") rose_tg = displayio.TileGrid(bitmap=rose_bmp, pixel_shader=rose_palette) # bitmap for the pointer needle pointer = displayio.Bitmap(5, 90, 2) # bitmap for erasing the pointer needle pointer_eraser = displayio.Bitmap(5, 90, 1) # pointer needle palette, red foreground, transparent background pointer_palette = displayio.Palette(2) pointer_palette[0] = 0x000000 pointer_palette[1] = 0xFF0000 pointer_palette.make_transparent(0) pointer.fill(1) # display sized bitmap to paste the rotated needle into rotated_pointer = displayio.Bitmap(240, 240, 2) # tilegrid for the rotated pointer needle pointer_tg = displayio.TileGrid(rotated_pointer, pixel_shader=pointer_palette) # add rose then pointer to the displaying group main_group.append(rose_tg) main_group.append(pointer_tg) while True: # if it's time to take a compass reading from the mag/gyro if (time.monotonic_ns() - timestamp) > 6500000: # read magnetic data mx, my, mz = magnetometer.magnetic # map it to the calibrated values cal_x = map_range(mx, MAG_MIN[0], MAG_MAX[0], -1, 1) cal_y = map_range(my, MAG_MIN[1], MAG_MAX[1], -1, 1) cal_z = map_range(mz, MAG_MIN[2], MAG_MAX[2], -1, 1) # if using ahrs filter if ahrs: # get accel/gyro data ax, ay, az, gx, gy, gz = accel_gyro.acceleration + accel_gyro.gyro # apply callibration offset gx += GYRO_CAL[0] gy += GYRO_CAL[1] gz += GYRO_CAL[2] # update filter ahrs_filter.update(gx, gy, -gz, ax, ay, az, cal_x, -cal_y, cal_z) # get yaw yaw_degree = ahrs_filter.yaw # convert radians to degrees heading = degrees(yaw_degree) else: # not using ahrs filter # calculate heading from calibrated mag data # and convert from radians to degrees heading = degrees(atan2(cal_y, cal_x)) # save time to compare next iteration timestamp = time.monotonic_ns() # if it's time to update the display if time.monotonic() > last_update + 0.2: # wrap negative heading values if heading < 0: heading += 360 # if the heading is sufficiently different from previous heading if abs(last_heading - heading) >= 2: #print(heading) # erase the previous pointer needle bitmaptools.rotozoom(rotated_pointer, pointer_eraser, ox=120, oy=120, px=pointer.width // 2, py=pointer.height, angle=radians(last_heading + offset_angle)) # draw the new pointer needle bitmaptools.rotozoom(rotated_pointer, pointer, ox=120, oy=120, px=pointer.width // 2, py=pointer.height, angle=radians(heading + offset_angle)) # set the previous heading to compare next iteration last_heading = heading # set the last update time to compare next iteration last_update = time.monotonic()
Page last edited February 28, 2025
Text editor powered by tinymce.