Drohnen-Telemetrie einfach gemacht: Feather RP2040 + CircuitPython Logger
Build, Create & Learn — A Maker’s Journey Episode 5 In der letzten Episode habe ich entschieden, mein Drohnen-Telemetrieprojekt auf CircuitPython mit dem Adafruit Feather RP2040 zu verlagern.
Hinweis: Der Podcast ist ausschließlich auf Englisch verfügbar.
Erste Schritte mit dem Feather RP2040
Das Einrichten des RP2040 mit CircuitPython war fast schockierend einfach. Anstatt wie beim STM32 zu kompilieren und zu flashen, kopiert man einfach die UF2-Datei auf das Board. Nach dem Neustart erscheint es als USB-Laufwerk mit dem Namen CIRCUITPY
.
Von dort bearbeitet man lediglich code.py
— speichern drücken — und das Board führt den Code sofort aus. Keine Toolchains, keine Flashing-Schritte. Ehrlich gesagt fühlte es sich fast wie Schummeln an, verglichen mit meinen früheren Kämpfen.
Die Adafruit-Dokumentation verdient hier ein großes Lob. Sie ist extrem einsteigerfreundlich: Verdrahtungsdiagramme, Beispielcode, Fotos — alles Schritt für Schritt erklärt. Es fühlte sich an wie LEGO-Bauen. Das macht es nicht nur für mich großartig, sondern auch zum perfekten Einstiegspunkt für alle, die mit Elektronik beginnen wollen.
Blinky, NeoPixel & Erste Eindrücke
Natürlich startete ich mit einem Blinky-Skript — und entdeckte dann den großen RGB-NeoPixel in der Mitte des Boards. Ein Beispiel später leuchteten Regenbogenfarben über meinen Schreibtisch. Meine Tochter war begeistert — und ich auch. Manchmal sind es die kleinen Dinge, die einem den Schub geben, weiterzumachen.
Den BME280-Sensor anschließen
Als nächstes kam der BME280-Umweltsensor. Dank der Adafruit-Bibliothek war es so einfach wie das .mpy
-File in den lib
-Ordner zu legen und das Beispiel zu starten.
import board, busio
i2c = busio.I2C(board.SCL, board.SDA)
while not i2c.try_lock():
pass
print([hex(x) for x in i2c.scan()])
i2c.unlock()
Zuerst hatte ich einen Pin-Zuweisungsfehler, aber mit einer kleinen Hilfsfunktion zur expliziten Definition der SPI-Pins konnte ich schnell Temperatur, Luftfeuchtigkeit und Luftdruck über die serielle Konsole auslesen.
Endlich: SD-Karten-Logging, das funktioniert
Die größte Erleichterung brachte das microSD-Breakout-Board. Nach endlosen Fehlschlägen mit dem STM32 funktionierte die Kombination Feather RP2040 + CircuitPython fast sofort, nachdem ich auf die Bibliothek sdcardio umgestiegen war.
Nicht nur konnte ich Testdateien schreiben, bald loggte ich auch BME280-Daten in CSV-Dateien. Anfangs alles in eine Datei, später stellte ich auf tägliche Logfiles um — viel besser zur Organisation von Drohnenflugdaten.
Hier ist der vollständige Code, den ich am Ende (inkl. GPS) genutzt habe:
import time, os, board, storage, sdcardio, busio, supervisor, neopixel, digitalio, rtc
from adafruit_bme280 import basic as adafruit_bme280
import adafruit_gps
# ---------------- Settings ----------------
SEA_LEVEL_HPA = 1016.0
SD_CS = board.D5
LOG_DIR = "/sd"
FLUSH_EVERY = 10
GPS_BAUD = 9600
# Avoid restarts while logging if you save files over USB
supervisor.runtime.autoreload = False
# --------------- NeoPixel -----------------
try:
np_power = digitalio.DigitalInOut(getattr(board, "NEOPIXEL_POWER"))
np_power.switch_to_output(True)
except Exception:
pass
PIX = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2, auto_write=True)
OFF = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 180, 0)
TEAL = (0, 170, 120)
PURPLE = (160, 0, 255)
ORANGE = (255, 80, 0)
def set_led(c): PIX[0] = c
flash_until = 0
def flash(color, duration=0.08):
global flash_until
set_led(color)
flash_until = time.monotonic() + duration
def apply_base_color(has_fix):
set_led(TEAL if has_fix else YELLOW)
# ---------------- SD init -----------------
set_led(PURPLE)
spi = board.SPI()
sd = sdcardio.SDCard(spi, SD_CS, baudrate=1_000_000)
vfs = storage.VfsFat(sd)
storage.mount(vfs, LOG_DIR)
for _ in range(2):
set_led(BLUE); time.sleep(0.07)
set_led(OFF); time.sleep(0.07)
def path_exists(p):
try: os.stat(p); return True
except OSError: return False
def datestr_from_rtc():
y, m, d = rtc.RTC().datetime[0:3]
return f"{y:04d}{m:02d}{d:02d}"
def log_path_for_today():
return f"{LOG_DIR}/log_{datestr_from_rtc()}.csv"
def open_log_for_today():
path = log_path_for_today()
new = not path_exists(path)
f = open(path, "a")
if new:
header = "t_s,temp_c,rel_hum_pct,press_hpa,alt_bme_m,lat,lon,spd_kmh,fix_q,sats,hdop,alt_gps_m\n"
f.write(header); f.flush(); os.sync()
return f, path
log_f, current_path = open_log_for_today()
lines_since_sync = 0
# ------------- BME280 (I2C) --------------
i2c = busio.I2C(board.SCL, board.SDA)
bme = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
bme.sea_level_pressure = SEA_LEVEL_HPA
# --------------- GPS (UART) --------------
uart = busio.UART(board.TX, board.RX, baudrate=GPS_BAUD, timeout=10)
gps = adafruit_gps.GPS(uart, debug=False)
gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") # RMC + GGA
gps.send_command(b"PMTK220,1000") # 1 Hz
last_sample = time.monotonic()
flash_until = 0.0
apply_base_color(False) # start "no fix"
def fmt(x, prec=6):
if x is None: return ""
if isinstance(x, float): return f"{x:.{prec}f}"
return str(x)
def rtc_is_reasonable():
y = rtc.RTC().datetime[0]
return y >= 2020
def try_set_rtc_from_gps():
"""If GPS has a timestamp, set the RTC so filenames reflect real date."""
if getattr(gps, "timestamp_utc", None):
t = gps.timestamp_utc
try:
rtc.RTC().datetime = (t.tm_year, t.tm_mon, t.tm_mday, t.tm_wday,
t.tm_hour, t.tm_min, t.tm_sec, 0)
return True
except Exception:
pass
return False
try:
while True:
gps.update()
# handle LED flash timeout / base color
now = time.monotonic()
if flash_until and now >= flash_until:
flash_until = 0
apply_base_color(gps.has_fix)
# if first valid GPS time, set RTC and rotate file once
if gps.has_fix and not rtc_is_reasonable():
if try_set_rtc_from_gps():
# Rotate to correct-dated file
try:
log_f.flush(); os.sync(); log_f.close()
except Exception:
pass
log_f, current_path = open_log_for_today()
lines_since_sync = 0
apply_base_color(True)
# sample & log each second
if now - last_sample >= 1.0:
last_sample = now
# rotate if date changed
wanted_path = log_path_for_today()
if wanted_path != current_path:
try:
log_f.flush(); os.sync(); log_f.close()
except Exception:
pass
log_f, current_path = open_log_for_today()
lines_since_sync = 0
# BME read
try:
t_c = bme.temperature
rh = bme.relative_humidity
p_hpa= bme.pressure
altb = bme.altitude
except Exception as e:
t_c, rh, p_hpa, altb = None, None, None, None
flash(ORANGE, 0.12)
print("BME read err:", repr(e))
# GPS fields (may be None until fix)
lat = gps.latitude
lon = gps.longitude
spd_kmh = getattr(gps, "speed_kmh", None)
if spd_kmh is None and gps.speed_knots is not None:
spd_kmh = gps.speed_knots * 1.852
fix_q = gps.fix_quality
sats = gps.satellites
hdop = gps.horizontal_dilution
altg = gps.altitude_m
if not flash_until:
apply_base_color(gps.has_fix)
row = ",".join([
f"{now:.1f}",
fmt(t_c, 2),
fmt(rh, 2),
fmt(p_hpa, 2),
fmt(altb, 2),
fmt(lat, 6),
fmt(lon, 6),
fmt(spd_kmh, 2),
fmt(fix_q, 0) if fix_q is not None else "",
fmt(sats, 0) if sats is not None else "",
fmt(hdop, 2),
fmt(altg, 2),
]) + "\n"
try:
log_f.write(row)
lines_since_sync += 1
flash(GREEN, 0.05)
if lines_since_sync >= FLUSH_EVERY:
log_f.flush(); os.sync()
lines_since_sync = 0
except Exception as e:
flash(RED, 0.2)
print("SD write err:", repr(e))
time.sleep(0.05)
except Exception as e:
set_led(RED)
print("FATAL:", repr(e))
finally:
try:
log_f.flush(); os.sync(); log_f.close()
except Exception:
pass
try:
storage.umount(LOG_DIR)
except Exception:
pass
set_led(OFF)
GPS hinzufügen und nächste Schritte
Mit funktionierendem SD- und BME280-Setup verdrahtete ich das GPS-Breakout. Zuerst kam nichts — bis ich ein fehlendes Kabel entdeckte. Danach erschienen die NMEA-Strings sauber, und die Adafruit-GPS-Bibliothek konvertierte sie sofort in nutzbare Daten.
Mit der Integration in den Logger habe ich jetzt Umweltdaten + GPS-Koordinaten auf der microSD gespeichert. Endlich ein vollständiger, funktionierender Telemetrie-Prototyp.
Überlegungen zum Gehäuse
Die Elektronik war soweit, also ging mein Fokus auf die Mechanik. Breadboards sind toll fürs Testen, aber nicht, um sie an eine Drohne zu schnallen.
Anstatt alles auf eine Lochrasterplatine zu löten, habe ich die bewusste Maker-Entscheidung getroffen, zuerst ein Gehäuse zu entwickeln. Da ich fast nur Breakout-Boards mit Befestigungslöchern nutze, ist dieser Ansatz schneller und praktischer für frühe Testflüge.
Ich begann mit Skizzen, druckte dann einfache Standoffs mit Gewindeeinsätzen und ein grobes Gehäuse, um mit Layouts zu spielen. Es fühlt sich gut an, hier auf meinen Maschinenbau-Hintergrund zurückzugreifen — und es ist etwas, womit viele Elektronik-Maker kämpfen. Gehäuse sind genauso wichtig wie Verdrahtung, und ich mag diese Schnittstelle.
Bleib dran — in der nächsten Episode ist das Ziel klar: der erste echte Testflug.
Fazit
Das habe ich in dieser Phase erreicht:
- Feather RP2040 mit CircuitPython am Laufen
- BME280-Sensordaten in CSV geloggt
- microSD-Breakout arbeitet zuverlässig mit sdcardio
- GPS-Koordinaten in den Logger integriert
- Erste CAD-Modelle und 3D-gedruckte Standoffs für das Gehäuse
Es fühlt sich großartig an, wie dieses Projekt endlich zusammenwächst.
🎧 Die ganze Geschichte mit allen Höhen und Tiefen findest du in Podcast Episode 5.
📬 Und wenn du wöchentliche Einblicke direkt von meiner Werkbank willst — Projekte und Experimente, die es nicht immer in den Podcast schaffen — dann abonniere meinen Maker’s Logbook Newsletter.
Lasst uns weiter bauen, erschaffen und lernen — gemeinsam.