17 KiB
Raspberry Pi Pico W – Sensor Projekt
Regeln
- Claude erklärt Konzepte und gibt Hinweise, aber kein fertiger Code
- Ich schreibe den Code selbst
- Bei Fehlern analysieren wir gemeinsam
- Ausnahme: Setup und Boilerplate darf vorgegeben werden
Hardware
| Komponente | Protokoll | Misst |
|---|---|---|
| Raspberry Pi Pico W | – | Mikrocontroller mit WLAN |
| BME280 (GY-BME280) | I2C (Adresse 0x76) | Temperatur + Luftdruck + Luftfeuchtigkeit |
AM2302 und DS18B20 werden nicht verwendet – BME280 liefert alle benötigten Messwerte.
Entwicklungsumgebung
System
- CachyOS (Arch-basiert)
- VS Code
Installierte Tools
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib cmake ninja git python
sudo pacman -S minicom
VS Code Extensions
- C/C++ (ms-vscode.cpptools)
- CMake Tools (ms-vscode.cmake-tools)
- CMake (twxs.cmake)
- Serial Monitor (ms-vscode.vscode-serial-monitor)
- Raspberry Pi Pico (raspberry-pi.raspberry-pi-pico)
Serieller Monitor
- Port:
/dev/ttyACM0 - Baud Rate:
115200 - Gruppe für Zugriff:
uucp
sudo usermod -aG uucp $USER
# Dann neu einloggen!
Projekt Struktur
Das Repo heißt sensor-pico und ist gleichzeitig der Root – keine extra Ebene darüber.
sensor-pico/ ← Git Repo Root
├── pico-sdk/ ← Git Submodule (Raspberry Pi Pico SDK)
├── lib/
│ ├── bme280/ ← Git Submodule (lafftale1999/bme280_driver)
│ └── dhcp_server/ ← DHCP Server (von pico-examples, kein Submodule)
├── src/
│ ├── main.cpp ← Hauptprogramm (WiFi + MQTT Verbindungslogik)
│ ├── webserver.c ← lwIP POST-Handler + Formular-Parsing
│ ├── webserver.h ← Exports: saved_ssid, saved_mqtt_* etc.
│ └── fsdata.c ← Generiert von makefsdata, nicht im Git!
├── fs/
│ ├── index.html ← Navigations-Hub (Links zu den Unterseiten)
│ ├── wlan_config.html ← WLAN SSID + Passwort Formular (POST /config)
│ ├── mqtt_config.html ← MQTT + Frequenz Formular (POST /mqtt-config)
│ └── live_stats.html ← (geplant) Live-Sensordaten
├── build/ ← Von cmake generiert, nicht im Git
├── CMakeLists.txt
├── lwipopts.h ← lwIP Konfiguration
└── prepend_include.cmake ← Hilfsskript für makefsdata-Build-Schritt
Git Submodule
[submodule "lib/bme280"]
path = lib/bme280
url = git@github.com:lafftale1999/bme280_driver.git
[submodule "pico-sdk"]
path = pico-sdk
url = https://github.com/raspberrypi/pico-sdk.git
.gitignore
build/
.vscode/
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.uf2
*.elf
*.bin
*.hex
*.map
*.dis
__pycache__/
*.pyc
src/fsdata.c
src/fsdata.cwird beim Build automatisch generiert und gehört nicht ins Repo.
CMakeLists.txt (aktueller Stand)
cmake_minimum_required(VERSION 3.13)
set(PICO_BOARD pico_w)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(PICO_SDK_PATH ${CMAKE_SOURCE_DIR}/pico-sdk)
include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(sensor-pico C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_subdirectory(lib/bme280 bme280_build)
add_subdirectory(lib/dhcp_server dhcp_server_build)
# makefsdata: fs/ → src/fsdata.c (wird von fs.c via #include eingebunden)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/fsdata.c
COMMAND perl ${PICO_SDK_PATH}/lib/lwip/src/apps/http/makefsdata/makefsdata
COMMAND ${CMAKE_COMMAND} -E rename fsdata.c src/fsdata.c
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/prepend_include.cmake
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${CMAKE_SOURCE_DIR}/fs/index.html
${CMAKE_SOURCE_DIR}/fs/wlan_config.html
${CMAKE_SOURCE_DIR}/fs/mqtt_config.html
)
# fsdata.c wird NICHT in add_executable gelistet – fs.c inkludiert sie via
# #include HTTPD_FSDATA_FILE (definiert in lwipopts.h als "src/fsdata.c")
add_executable(sensor-pico
src/main.cpp
src/webserver.c
)
pico_enable_stdio_usb(sensor-pico 1)
pico_enable_stdio_uart(sensor-pico 0)
target_include_directories(sensor-pico PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(sensor-pico
pico_stdlib
pico_cyw43_arch_lwip_threadsafe_background
pico_lwip
pico_lwip_http
pico_lwip_mqtt
bme280
dhcp_server
)
pico_add_extra_outputs(sensor-pico)
prepend_include.cmake
Dieses Script wird von CMake als Build-Schritt ausgeführt und fügt den fehlenden #include an den Anfang der generierten fsdata.c ein. Nötig weil das alte Perl-Script von lwIP keinen Header einfügt.
file(READ "src/fsdata.c" CONTENT)
file(WRITE "src/fsdata.c" "#include \"lwip/apps/fs.h\"\n${CONTENT}")
Warum nicht direkt Perl dafür? CMake-Scripts haben kein Shell-Quoting-Problem –
file(READ/WRITE)arbeitet direkt mit dem Dateisystem, ohne Shell-Umweg.
Build & Flash Workflow
# Erstes Setup (nur einmal)
mkdir build
# Bauen
cd build
cmake .. -DPICO_BOARD=pico_w # PICO_BOARD muss explizit angegeben werden!
make -j$(nproc)
# Was passiert beim Build automatisch:
# 1. makefsdata liest fs/index.html
# 2. Generiert fsdata.c mit HTML-Inhalt als Byte-Array
# 3. Verschiebt fsdata.c nach src/fsdata.c
# 4. prepend_include.cmake fügt #include "lwip/apps/fs.h" ein
# 5. fsdata.c wird mitkompiliert
# Flashen
# 1. BOOTSEL gedrückt halten
# 2. USB einstecken
# 3. Taste loslassen
# 4. RPI-RP2 erscheint als Laufwerk
cp sensor-pico.uf2 /run/media/$USER/RPI-RP2/
Verkabelung
I2C (BME280):
VCC → 3.3V (Pin 36)
GND → GND
SDA → GPIO 14 (Pin 19) ← Vorgabe der bme280-Bibliothek (i2c1)
SCL → GPIO 15 (Pin 20) ← Vorgabe der bme280-Bibliothek (i2c1)
BME280 Messwerte
Die Bibliothek gibt skalierte Integer zurück – kein Floating Point, üblich auf Embedded-Systemen wegen Speicher und Geschwindigkeit.
| Wert | Skalierung | Beispiel | Umrechnung |
|---|---|---|---|
| Temperatur | × 100 | 2494 | / 100 → 24.94°C |
| Luftfeuchtigkeit | × 1024 | 30759 | / 1024 → 30.0% |
| Luftdruck | × 256 (Pa) | 26074767 | / 256 / 100 → 1018.54 hPa |
JSON-Ausgabe per bme280_get_json(handle):
{"temperature":2494,"humidity":30759,"pressure":26074767}
Die Umrechnung kann auf dem Empfänger (z.B. Home Assistant) gemacht werden – der Pico sendet die Rohwerte.
Lesbare Ausgabe per printf möglich:
printf("Temp: %d.%02d C\n", temperature / 100, temperature % 100);
Netzwerk / Webserver
Architektur
Der Pico hat zwei Betriebsmodi:
Modus 1 – Erststart / Konfiguration (Access Point)
- Pico öffnet einen eigenen Access Point (
SensorAP, WPA2) - DHCP-Server läuft – Clients bekommen automatisch eine IP (
192.168.4.x) - Pico ist erreichbar unter
192.168.4.1 - Weboberfläche zeigt Konfigurationsformular
Modus 2 – Produktionsbetrieb (Station Mode)
- Pico verbindet sich mit bestehendem WLAN (gespeicherte SSID + Passwort)
- Publisht Sensordaten per MQTT an Broker
- Webserver läuft weiterhin: Live-Daten + Einstellungen änderbar
Aktueller Stand
- ✅ Access Point (
SensorAP, WPA2) öffnet sich - ✅ DHCP-Server läuft – Clients bekommen IP
- ✅ Pico erreichbar unter
192.168.4.1 - ✅ lwIP httpd läuft – Seiten werden ausgeliefert
- ✅
makefsdataautomatisch im Build-Prozess eingebunden - ✅ Alle HTML-Seiten aus
fs/werden als Byte-Array in Firmware eingebaut - ✅ WLAN-Konfiguration über Captive Portal (POST Handler)
- ✅ Pico verbindet sich mit bestehendem WLAN (Station Mode)
- ✅ MQTT-Konfiguration über Webformular (Adresse, Port, User, Passwort, Frequenzen)
- ✅ MQTT-Verbindung zum Broker steht
- ✅ BME280 Sensor liest Messwerte aus und gibt JSON per printf aus
- ⬜ Sensordaten per MQTT publishen
- ⬜ Live-Daten Seite (
live_stats.html)
Programm-Ablauf (main.cpp)
1. cyw43_arch_init() + httpd_init()
2. WiFi-Schleife:
└─ ap_init() → warte auf SSID+Passwort → verbinde mit WLAN
└─ bei Erfolg: weiter; bei Fehler: neu versuchen
3. MQTT-Config-Schleife:
└─ warte bis saved_mqtt_address gesetzt ist (Formular abgeschickt)
└─ connect_to_mqtt() → bei Fehler: Config zurücksetzen + neu warten
4. Sensor-Loop (TODO):
└─ BME280 auslesen → per MQTT publishen
Geplante Anforderungen
- ✅ Beim Erststart: Access Point für Erstkonfiguration
- ✅ Weboberfläche zum Einstellen von:
- ✅ WLAN SSID + Passwort
- ✅ MQTT Broker (Adresse, Port, User, Passwort)
- ✅ Messfrequenz + Pushfrequenz
- Im Produktionsbetrieb: Live-Daten + Einstellungen änderbar
makefsdata – wie es funktioniert
makefsdata ist ein Perl-Script im lwIP-Quellcode:
pico-sdk/lib/lwip/src/apps/http/makefsdata/makefsdata
Es liest alle Dateien aus dem fs/-Ordner und erzeugt daraus eine fsdata.c mit dem Dateiinhalt als statische Byte-Arrays. lwIP httpd schaut zur Laufzeit in dieser Datei nach statt auf einem echten Dateisystem.
Problem: Das Script generiert fsdata.c ohne den nötigen #include "lwip/apps/fs.h" – der Compiler kennt dann weder struct fsdata_file noch FS_FILE_FLAGS_*.
Lösung: prepend_include.cmake fügt den Include als CMake-Build-Schritt nach der Generierung ein.
lwipopts.h
// fsdata.c wird von lwIP's fs.c via #include eingebunden – Pfad relativ zum Include-Root
#define HTTPD_FSDATA_FILE "src/fsdata.c"
#define NO_SYS 1
#define LWIP_SOCKET 0
#define LWIP_NETCONN 0
#define LWIP_IPV4 1
#define LWIP_TCP 1
#define LWIP_UDP 1
#define LWIP_DHCP 1
#define LWIP_DNS 1
#define LWIP_RAW 1
#define LWIP_ARP 1
#define LWIP_ETHERNET 1
#define LWIP_ICMP 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETIF_STATUS_CALLBACK 1
#define MEM_ALIGNMENT 4
#define MEM_SIZE 4000
#define MEMP_NUM_TCP_SEG 32
#define PBUF_POOL_SIZE 24
#define TCP_MSS 1460
#define TCP_WND (8 * TCP_MSS)
#define TCP_SND_BUF (8 * TCP_MSS)
#define LWIP_HTTPD 1
#define LWIP_HTTPD_CGI 1
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SUPPORT_POST 1 // POST-Body Callbacks aktivieren
Nächste Schritte
makefsdataautomatisch in cmake einbinden- WLAN Konfigurationsformular + POST-Handler
- WLAN Station Mode
- MQTT Konfigurationsformular + POST-Handler
- MQTT Verbindung zum Broker
- BME280 Sensor auslesen
- BME280 Sensordaten per MQTT publishen
- Live-Daten Seite (
live_stats.html) - Einstellungen im Flash speichern (Neustart ohne Neukonfiguration)
Gelerntes
Konzepte
- I2C – Bus-Protokoll, alle Geräte an 2 Drähten (SDA + SCL), jedes Gerät hat eine Adresse
- Hexadezimal –
0xPrefix, 16 Ziffern (0-9, A-F) - ACK/NACK – Antwort/keine Antwort bei I2C Kommunikation
- Handle – "Ticket" auf ein intern verwaltetes Objekt (Garderobe-Analogie)
- Pull-up Widerstand – nötig bei I2C und OneWire
- printf – Formatstring mit Platzhaltern (
%d,%s,%02X), Zeilenumbruch mit\n - &variable – Adressoperator, gibt Speicheradresse zurück
- uint8_t – 8-bit unsigned integer, genau 1 Byte
- void – Rückgabetyp wenn Funktion nichts zurückgibt
- Embedded – kein
std::cout,printfbevorzugen wegen Speicher - IP-Adresse – eindeutige Adresse im Netzwerk, 4 Zahlen (0-255)
- Netzmaske – definiert welcher Teil der IP das Netzwerk ist (
255.255.255.0) - DHCP – verteilt automatisch IP-Adressen an Geräte im Netzwerk
- lwIP – schlanker TCP/IP Stack für Embedded-Systeme
- fsdata – HTML-Dateien werden beim Build als C-Bytes in die Firmware eingebaut
- makefsdata – Perl-Script das
fs/-Ordner infsdata.cByte-Array konvertiert - extern "C" – verhindert C++ Name Mangling bei C-Bibliotheken
- Name Mangling – C++ kodiert Funktionsnamen mit Typinfo im Symbol (für Overloading); C nicht – das verursacht Linker-Fehler wenn man C-Bibliotheken aus C++ nutzt ohne
extern "C" - -j$(nproc) – paralleles Bauen mit allen CPU-Kernen
- add_custom_command – CMake-Befehl um externe Programme als Build-Schritt auszuführen; mit
OUTPUTweiß CMake welche Datei erzeugt wird und wann neu gebaut werden muss - ${CMAKE_COMMAND} -E – plattformunabhängige CMake-Dateibefehle (rename, copy, remove) ohne Shell-Abhängigkeit
- ${CMAKE_COMMAND} -P – führt ein CMake-Script direkt aus, ohne Shell-Umweg und ohne Quoting-Probleme
- GET vs POST – GET-Parameter landen in der URL (sichtbar, in Logs), POST-Daten im Body – für Passwörter immer POST
- Buffer – Array als temporärer Zwischenspeicher; in C immer manuell null-terminieren (
buf[len] = '\0') - pbuf – lwIPs internes Paket-Buffer-Format; Inhalt mit
pbuf_copy_partial()extrahieren, danachpbuf_free()aufrufen - strstr – sucht Substring in String, gibt Zeiger auf Fundstelle zurück oder NULL
- strchr – sucht einzelnes Zeichen in String, gibt Zeiger darauf zurück
- Zeigerarithmetik – zwei Zeiger auf denselben String subtrahieren gibt die Länge zwischen ihnen
- strlen – Länge eines C-Strings ohne Null-Terminator
- atoi – wandelt C-String (
"30") in Integer (30) um; aus<stdlib.h> - memset – setzt alle Bytes eines Arrays auf einen Wert;
memset(arr, 0, sizeof(arr))zum Nullen - static (lokale Variable) – lebt für die gesamte Programmlaufzeit, wird nicht bei jedem Aufruf neu initialisiert
- Asynchrone Callbacks – Funktion wird nicht direkt aufgerufen sondern von lwIP registriert und später automatisch aufgerufen (z.B. bei MQTT-Verbindung)
- *void arg – generischer Kontext-Zeiger in C-Callbacks; übergib
&deine_variable, caste in Callback zurück mitstatic_cast<typ*>(arg) - static_cast<> – C++ Cast-Operator; typsicherer als C-Style-Cast
(typ) - Ternary Operator –
bedingung ? wert_true : wert_false; Kurzform für einfache if/else-Zuweisungen - mqtt_client_t – lwIP MQTT Client-Objekt; erstellt mit
mqtt_client_new() - mqtt_connect_client_info_t – Struct mit Client-ID, Username, Passwort für MQTT-Verbindung
- MQTT Verbindungsflow –
mqtt_client_connect()kehrt sofort zurück; Ergebnis kommt asynchron im Callback
CMake-Konzepte
- add_subdirectory – bindet einen Unterordner mit eigenem CMakeLists.txt ein; zweiter Parameter ist der Build-Unterordner
- target_include_directories – sagt dem Compiler wo er Header-Dateien suchen soll;
PRIVATE= nur für dieses Target - target_link_libraries – verknüpft Bibliotheken mit dem Executable
- add_custom_command OUTPUT – definiert wie eine generierte Datei erzeugt wird; CMake führt den Befehl aus wenn die Datei fehlt oder Abhängigkeiten neuer sind
- DEPENDS in add_custom_command – wenn diese Dateien sich ändern, wird der Befehl beim nächsten Build neu ausgeführt
Pico W Besonderheiten
- Interne LED hängt am WLAN-Chip (CYW43), nicht an GPIO 25
- Braucht
pico_cyw43_arch_none(ohne Netzwerk) oderpico_cyw43_arch_lwip_threadsafe_background(mit lwIP) PICO_BOARD=pico_wmuss vorpico_sdk_import.cmakegesetzt werdenMEM_LIBC_MALLOCist inkompatibel mitthreadsafe_backgroundlwipopts.hmuss selbst erstellt werden – lwIP hat keine Defaults- lwIP httpd CGI erwartet C-Funktionen – in
.cpp-Dateien mitextern "C"wrappen
Bekannte Fallstricke
makefsdatageneriertfsdata.cohne#include "lwip/apps/fs.h"→ Compiler kenntstruct fsdata_filenicht →prepend_include.cmakeals Workaround- Shell-Quoting in CMake
COMMANDist fehleranfällig bei komplexen Perl-Ausdrücken → CMake-Scripts (-P) verwenden statt Shell-Befehle add_custom_command OUTPUTmit relativem Pfad legt die Datei im Build-Verzeichnis ab, nicht im Source-Verzeichnis → immer absoluten Pfad mit${CMAKE_CURRENT_SOURCE_DIR}angebenpico_lwip_httpist ein INTERFACE-Target –target_include_directoriesmitPRIVATEschlägt fehl; stattdessenHTTPD_FSDATA_FILEinlwipopts.hüberschreiben- lwIP
fs.cinkludiertfsdata.cdirekt via#include HTTPD_FSDATA_FILE– ohne Override wird die Default-Seite aus dem pico-sdk verwendet statt der eigenen strcmpgibt0zurück wenn Strings gleich sind, nichttrue–== trueprüft auf Verschiedenheit- lwIP CGI-Handler parst nur GET-Parameter aus der URL, nicht POST-Body → für POST-Formulare
LWIP_HTTPD_SUPPORT_POST+ die drei Callbacks (httpd_post_begin,httpd_post_receive_data,httpd_post_finished) verwenden dhcp_server_tinap_init()mussstaticsein – sonst wird der Stack-Speicher nach Funktionsrückkehr freigegeben, der DHCP-Server läuft aber weiter und greift auf ungültigen Speicher zustrncpyfügt kein Null-Byte hinzu wenn exakte Länge übergeben wird → immerdest[len] = '\0'danach setzen