Digital Compass
Works with
Any CircuitPython board with I2C and SPI — QT Py ESP32-S2, Feather boards with display
A compass needle that rotates in real time as you turn the board. The BNO055 IMU sensor gives you a heading in degrees; a small round GC9A01A display draws the needle pointing toward magnetic north. Flip the board and the needle follows. This project is based on the Adafruit "QT Py S2 Round Display Compass" guide.
What you'll build
A program that reads the current heading from a BNO055 9-DOF IMU over I2C, converts the heading angle to x/y coordinates using trigonometry, and draws a compass needle on a 240x240 round TFT display. The needle updates continuously as you move the board.
What you'll need
- A CircuitPython board with both I2C and SPI (QT Py ESP32-S2 is ideal; Feather boards work well too)
- Adafruit BNO055 absolute orientation sensor breakout
- Adafruit round GC9A01A 240x240 TFT display (1.28")
- Jumper wires
Wiring
The BNO055 uses I2C. The round GC9A01A display uses SPI. Both connect to the same board simultaneously.
graph LR
subgraph Board
SDA
SCL
SCK
MOSI
CS["CS (GPIO)"]
DC["DC (GPIO)"]
RST["RST (GPIO)"]
P3V["3.3V"]
GND
end
subgraph IMU["BNO055"]
I_SDA["SDA"]
I_SCL["SCL"]
I_VIN["VIN"]
I_GND["GND"]
end
subgraph Display["GC9A01A Round TFT"]
D_SCK["SCK"]
D_MOSI["MOSI / SDA"]
D_CS["CS"]
D_DC["DC"]
D_RST["RST"]
D_VIN["VIN"]
D_GND["GND"]
end
SDA --> I_SDA
SCL --> I_SCL
P3V --> I_VIN
GND --> I_GND
SCK --> D_SCK
MOSI --> D_MOSI
CS --> D_CS
DC --> D_DC
RST --> D_RST
P3V --> D_VIN
GND --> D_GND
The code
import math
import time
import board
import busio
import displayio
import adafruit_bno055
import adafruit_gc9a01a
from adafruit_display_shapes.line import Line
from adafruit_display_shapes.circle import Circle
# --- Display setup ---
displayio.release_displays()
spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI)
display_bus = displayio.FourWire(spi, command=board.D5, chip_select=board.D6, reset=board.D9)
display = adafruit_gc9a01a.GC9A01A(display_bus, width=240, height=240)
# --- IMU setup ---
i2c = busio.I2C(board.SCL, board.SDA)
sensor = adafruit_bno055.BNO055_I2C(i2c)
# Display center and needle length
CX = 120
CY = 120
NEEDLE_LEN = 90
splash = displayio.Group()
display.root_group = splash
# Background circle
bg = Circle(CX, CY, 119, fill=0x001020, outline=0x0055AA)
splash.append(bg)
# Center dot
dot = Circle(CX, CY, 5, fill=0xFF0000)
splash.append(dot)
# Initial needle — will be replaced each update
needle = Line(CX, CY, CX, CY - NEEDLE_LEN, color=0xFF0000)
splash.append(needle)
def heading_to_xy(heading_deg, length):
"""Convert compass heading (0=N, clockwise) to endpoint x, y."""
# Compass: 0 is north (up), increases clockwise
# Math angles: 0 is right (east), increases counter-clockwise
# Convert: math_angle = 90 - heading
rad = math.radians(90 - heading_deg)
x = int(CX + length * math.cos(rad))
y = int(CY - length * math.sin(rad))
return x, y
while True:
euler = sensor.euler
if euler is not None and euler[0] is not None:
heading = euler[0] # degrees, 0–360, 0 = North
# Remove old needle and add a new one
splash.remove(needle)
tx, ty = heading_to_xy(heading, NEEDLE_LEN)
needle = Line(CX, CY, tx, ty, color=0xFF4444)
splash.insert(len(splash) - 1, needle) # keep dot on top
time.sleep(0.05)
How it works
How a magnetometer gives heading. The BNO055 contains a 3-axis magnetometer that measures the strength of the Earth's magnetic field along three axes. By comparing those three readings, it calculates the direction of magnetic north relative to the sensor. The BNO055 goes further than a bare magnetometer: it also contains an accelerometer and gyroscope, and its onboard fusion processor combines all three to produce stable Euler angles — heading, roll, and pitch — even when the board is tilted. That fusion is done in hardware, so your Python code just reads the result.
Euler angles from BNO055. The sensor.euler property returns a tuple of (heading, roll, pitch) in degrees. Heading ranges from 0 to 360, where 0 (and 360) is magnetic north, 90 is east, 180 is south, and 270 is west. This convention — north at zero, increasing clockwise — matches a traditional compass. Roll and pitch describe tilt; they are useful for leveling applications or for correcting heading errors when the board is not held flat, but for a basic compass on a flat surface, only the heading value is needed.
Translating heading angle to x/y coordinates for drawing. Screen coordinates in displayio have their origin at the top-left, with x increasing to the right and y increasing downward. Math angles (as used by math.sin and math.cos) have their origin pointing right, with angles increasing counter-clockwise. A compass heading has its origin pointing up (north), with angles increasing clockwise. To bridge these conventions, the code converts a heading to a math angle with 90 - heading, then uses standard polar-to-Cartesian conversion: x = cx + r * cos(angle), y = cy - r * sin(angle) (note the minus sign on y to flip the vertical axis). The result is the pixel coordinates of the needle's tip.
Installing libraries
Copy the following to the lib/ folder on your CIRCUITPY drive. Get them from the Adafruit CircuitPython Bundle.
adafruit_bno055.mpyadafruit_gc9a01a.mpyadafruit_display_shapes/(folder)adafruit_bus_device/(folder)adafruit_register/(folder) — required byadafruit_bno055
Remix it
Remix idea
Use the roll and pitch data from the BNO055 to add tilt correction or a bubble-level indicator. The BNO055 reference explains all the fusion output modes and how to enable tilt-compensated heading.
Remix idea
Replace the round TFT with a NeoPixel ring. Map the heading to a single lit pixel on the ring to show direction. The First NeoPixel project covers NeoPixel basics and animation patterns.
Remix idea
Log heading and location data to Adafruit IO over WiFi to track which direction something is pointing over time. The Adafruit IO Basics project covers sending sensor data to the cloud.
Go deeper
- BNO055 reference
- QT Py S2 Round Display Compass — Credit: Adafruit Learning System