

## 3D-Ultraschall-Anemometer Firmware (Teil 2a: Globale Definitionen)

### 2.1 Globale System-Struktur

Dieses zentrale Header-Modul deklariert die globalen Konstanten, das
physikalische PIN-Mapping am Stecksockel des Raspberry Pi Pico sowie
die inter-core Datenstrukturen für den dualen Messbetrieb.  Dateiname:
global.h

```c++
#ifndef GLOBAL_H#define GLOBAL_H
#include <stdint.h>#include "hardware/pio.h"
// --- MY-SENSORS NETWORK PROTOCOL CONFIGURATION ---
#define NODE_ID              24
#define CHILD_ID_WIND        0
#define CHILD_ID_CLIMATE     1
#define CHILD_ID_SOLAR       2
#define CHILD_ID_GPS         3
#define CHILD_ID_DYNAMICS    4
#define CHILD_ID_3D_VECTOR   5
#define CHILD_ID_RAIN        6  // Neu: Für hybride Niederschlagsdaten
// --- HARDWARE PIN-MAPPING (PICO STECKSOCKEL CONFIG) ---
#define PICO_I2C_SDA         0  // Pin 1  (BME280, INA219, MMC5603NJ, LSM6DS3)
#define PICO_I2C_SCL         1  // Pin 2  (Gemeinsame Taktleitung I2C-Bus)
#define PICO_DIR_SEL         2  // Pin 4  (Richtungsumkehr an alle CD4053B)
#define PICO_TX_TRIGGER      3  // Pin 5  (PIO-Burst an Ultraschall-Sende-Endstufe)
#define PICO_COMP_IN         4  // Pin 6  (PIO-Eingang vom Echo-Komparator)
#define PICO_CAL_BUTTON      5  // Pin 7  (Manueller Kalibriertaster gegen GND)
#define PICO_COMPASS_EN      6  // Pin 9  (MOSFET Gate: High=Aus, Low=Ein für mobile Sensorik)
#define PICO_MODE_JUMPER     7  // Pin 10 (Jumper gegen GND: Offen=Stationär, Zu=Mobil)
#define PICO_UART_TX         8  // Pin 11 (UART1 Sendeleitung zum GPS-Modul)
#define PICO_UART_RX         9  // Pin 12 (UART1 Empfangsleitung vom GPS-Modul)
// --- HYBRIDE REGENMESSER PERIPHERIE (ROUTING DURCH CFK-ROHRE) ---
#define PIN_WIPPE_INTERRUPT 16  // Pin 21 (Digital-In von der Hall-Wippe aus Rohr 2)
#define PIN_PIEZO_ADC       26  // Pin 31 (Analog-In ADC0 vom Piezo-Schutznetzwerk oben)
#define PIEZO_ADC_NUM        0  // Interne ADC-Kanalnummer des RP2040
// --- SYSTEM-FUNK-SOCKEL (nRF24L01+) ---
#define PICO_SPI_MISO       16  // Pin 21 (Gemeinsamer Hardware-Bus mit Wippen-Pin)
#define PICO_SPI_CSN        17  // Pin 22 (Chip Select Not Funk)
#define PICO_SPI_SCK        18  // Pin 24 (Serial Clock)
#define PICO_SPI_MOSI       19  // Pin 25 (Master Out Slave In)
#define PICO_CE             20  // Pin 26 (Chip Enable Funk)
// --- PHYSIKALISCHE GEOMETRIE (SKALIERTES 190-mm-DESIGN) ---
#define R_RADIUS            0.075f  // Physikalischer Radius \(R = 75\text{ mm}\)
#define BME280_ADDR         0x76
#define MMC5603_ADDR        0x30
#define LSM6DS3_ADDR        0x6A
#define FLASH_TARGET_OFFSET (2048 * 1024 - 4096) 
#define KALIBRIER_MAGIC     0x57494E44
#define ERROR_NO_ECHO       -1.0f
// --- SPEICHER- UND MESSDATEN-STRUKTUREN ---typedef struct {
    uint32_t magic_number;
    float offset_w1;
    float offset_w2;
    float offset_w3;
    float gehaeuse_nord_winkel;
    float stations_hoehe_nn;
    double piezo_k_factor; // Gespeicherter Live-Eichwert für den Piezosensor
} __attribute__((packed)) KalibrierDaten;
typedef struct {
    float vx, vy, vz;
    float wind_speed;
    float wind_dir;
    float temperatur;
    float luftdruck;
    float feuchtigkeit;
    float solar_w_m2;
    float regen_menge_mm; // Kumulierte Regenhöhe in Millimetern
} WetterDaten;
// --- PROFILE EXTREM-REDUKTION (VOLATILE INTER-CORE-VARIABLEN) ---
extern KalibrierDaten aktuelle_kalibrierung;
extern bool ist_mobiler_boots_modus;
extern volatile float empfangene_dem_hoehe;
extern volatile bool hoehe_wurde_empfangen;
extern volatile uint32_t wippen_klicks;
extern volatile double absolute_regenmenge_mm;
extern volatile uint64_t piezo_integral_seit_letztem_klick;
// --- FUNKTIONS-PROTOTYPEN ---
void init_hardware_pins();
void setup_ultrasonic_pio(PIO pio, uint sm, uint pin_tx, uint pin_echo);
float pio_messen_und_berechnen(PIO pio, uint sm);
float messe_kompass_live();
void lade_kalibrierung_aus_flash();
void init_lsm6ds3_imu();
void lese_imu_neigung(float &roll_deg, float &pitch_deg);
void kompensation_neigung(float vx_raw, float vy_raw, float vz_raw, float roll_deg, float pitch_deg, float &vx_korr, float &vy_korr, float &vz_korr);
void lese_bme280_raw(float &t, float &p, float &h);
void lese_ina219_solar(float &watt);
void lese_live_gps(float &lat, float &lon, float &alt, float &sog, float &cog);
void trigger_internet_hoehen_abgleich(float lat, float lon);
float berechne_taupunkt(float t, float rh);
void transformiere_3d_wind(float t1, float t2, float t3, float c, WetterDaten &d);
void starte_kombinierte_werks_kalibrierung(float c, float t1, float t2, float t3);
void send_mysensors_msg(uint8_t child_id, uint8_t sub_type, float value);
void send_mysensors_rain_msg(uint8_t child_id, uint8_t sub_type, float total_mm);
void sende_gesamten_3d_payload(WetterDaten d);
void mysensors_receive_callback(uint8_t child_id, uint8_t msg_type, uint8_t sub_type, const char *payload);
#endif // GLOBAL_H
```

## Teil 2b (vector_math.cpp) 


Der Code vector_math.cpp implementiert die 3D-Vektoralgebra für das
Ultraschall-Anemometer auf dem RP2040, einschließlich der
Taupunktberechnung, der IMU-basierten Neigungskompensation und der
True-Wind-Berechnung durch GPS-Kopplung. Die Routine
transformiere_3d_wind transformiert Rohdaten in geographische
Koordinaten unter Berücksichtigung von Schiffsbewegungen.

```c++
#include <math.h>
#include "global.h"
// Berechnung des Taupunkts mittels der klassischen Magnus-Formel
float berechne_taupunkt(float t, float rh) {
    float a = (t >= 0) ? 17.625f : 22.443f; 
    float b = (t >= 0) ? 243.04f : 272.44f;
    float alpha = ((a * t) / (b + t)) + logf(rh / 100.0f);
    return (b * alpha) / (a - alpha);
}
// 3D-Rotationsmatrix zur Eliminierung von Krängung, Rollen und Stampfen
void kompensation_neigung(float vx_raw, float vy_raw, float vz_raw, float roll_deg, float pitch_deg, float &vx_korr, float &vy_korr, float &vz_korr) {
    float b = roll_deg * M_PI / 180.0f;   // \(\beta_{\text{Roll}}\) in Bogenmaß
    float g = pitch_deg * M_PI / 180.0f;  // \(\gamma_{\text{Pitch}}\) in Bogenmaß

    float sin_b = sinf(b); float cos_b = cosf(b);
    float sin_g = sinf(g); float cos_g = cosf(g);

    // Drehung der gehäuserelativen Achsen zurück in die hydrodynamisch flache Erdebene
    vx_korr = vx_raw * cos_g + vz_raw * sin_g;
    vy_korr = vx_raw * sin_b * sin_g + vy_raw * cos_b - vz_raw * sin_b * cos_g;
    vz_korr = -vx_raw * cos_b * sin_g + vy_raw * sin_b + vz_raw * cos_b * cos_g;
}
// Haupt-Transformationsfunktion für das skalierte 190-mm-Messdreieck
void transformiere_3d_wind(float t1, float t2, float t3, float c, WetterDaten &d) {
    // Die akustische Teilstrecke skaliert mit dem neuen Gehäuseradius von 75mm
    // \(s_{\text{teil}} = R \cdot \sqrt{2} \approx 0{,}106066\text{ m}\)
    const float s_teil = R_RADIUS * 1.4142135f;
    
    // Umrechnung der Mikrosekundenlaufzeiten in scheinbare Pfadgeschwindigkeiten \(w_i\)
    float w1 = ((s_teil / (t1 / 1000000.0f)) - c) - aktuelle_kalibrierung.offset_w1;
    float w2 = ((s_teil / (t2 / 1000000.0f)) - c) - aktuelle_kalibrierung.offset_w2;
    float w3 = ((s_teil / (t3 / 1000000.0f)) - c) - aktuelle_kalibrierung.offset_w3;

    // Isoliere die gehäuserelativen orthogonalen Koordinatenachsen via LGS-Inversion
    float vx_rel = ((2.0f / 3.0f) * w1 - (1.0f / 3.0f) * w2 - (1.0f / 3.0f) * w3) / 0.707106f;
    float vy_rel = ((1.0f / 1.7320508f) * (w2 - w3)) / 0.707106f;
    float vz_rel = ((w1 + w2 + w3) / 3.0f) / 0.707106f; 

    float vx_eben, vy_eben, vz_eben;
    
    // 1. Live-Neigungskompensation im mobilen Bootsmodus aktivieren
    if (ist_mobiler_boots_modus) {
        float roll, pitch;
        lese_imu_neigung(roll, pitch); // Liest LSM6DS3 über I2C aus
        kompensation_neigung(vx_rel, vy_rel, vz_rel, roll, pitch, vx_eben, vy_eben, vz_eben);
        d.vz = vz_eben; // Z-Achse spiegelt den rechten vertikalen Auf-/Abwind wider
    } else {
        vx_eben = vx_rel; vy_eben = vy_rel; d.vz = vz_rel;
    }

    // 2. Geografische Einnordung (Drehung in das erdfeste N-S-O-W-Koordinatensystem)
    float alpha_nord = ist_mobiler_boots_modus ? messe_kompass_live() : aktuelle_kalibrierung.gehaeuse_nord_winkel;
    float rad = alpha_nord * M_PI / 180.0f;
    float vx_erde = vx_eben * cosf(rad) - vy_eben * sinf(rad);
    float vy_erde = vx_eben * sinf(rad) + vy_eben * cosf(rad);

    // 3. True Wind Berechnung: Subtraktive Beseitigung des Fahrtwindes (GPS-Kopplung)
    if (ist_mobiler_boots_modus) {
        float lat, lon, alt, sog, cog;
        lese_live_gps(lat, lon, alt, sog, cog); // Liest NMEA über UART1
        float rad_cog = cog * M_PI / 180.0f;
        
        // Vektorsubtraktion: Scheinbarer Wind minus Fahrtvektor über Grund (SOG/COG)
        d.vx = vx_erde - (sog * cosf(rad_cog)); 
        d.vy = vy_erde - (sog * sinf(rad_cog));
    } else {
        d.vx = vx_erde; d.vy = vy_erde;
    }

    // Berechnung des finalen zweidimensionale Horizontal-Windvektors
    d.wind_speed = sqrtf(d.vx * d.vx + d.vy * d.vy);
    d.wind_dir = atan2f(d.vy, d.vx) * 180.0f / M_PI;
    if (d.wind_dir < 0) d.wind_dir += 360.0f;
}
```


## 3D-Ultraschall-Anemometer Firmware (Teil 2c: MySensors Data-Tunnel)

### 2.2 Datenkapselung & Payload-Generierung

Dieses Modul verpackt sämtliche Klimadaten, GPS-Koordinaten, die
Bootskinematik sowie das kartesische 3D-Windgitter und die neuen,
kumulierten Niederschlagsdaten in standardisierte
MySensors-Funktelegramme.  Dateiname: mysensors_payload.cpp

```c++
#include <stdio.h>
#include <stdlib.h>
#include "global.h"
// MySensors V-Typ Definitionen für Positions- und generische Variablen
#define V_POSITION 49  
#define V_VAR1     24  
#define V_VAR2     25  
#define V_VAR3     26  
#define V_RAIN     11  // MySensors Typ für Regenmenge (akkumuliert in mm)
// Standard-Hilfsfunktion für numerische MySensors-Ausgaben
void send_mysensors_msg(uint8_t child_id, uint8_t sub_type, float value) {
    printf("%d;%d;1;0;%d;%.2f\n", NODE_ID, child_id, sub_type, value);
}
// Spezifische Hilfsfunktion für hochpräzise Niederschlagswerte
void send_mysensors_rain_msg(uint8_t child_id, uint8_t sub_type, float total_mm) {
    printf("%d;%d;1;0;%d;%.4f\n", NODE_ID, child_id, sub_type, total_mm);
}
// Inbound Callback-Schnittstelle (empfängt asynchrone Daten von FHEM)
void mysensors_receive_callback(uint8_t child_id, uint8_t msg_type, uint8_t sub_type, const char *payload) {
    // Wenn FHEM auf den Internet-DEM-Request antwortet (C_REQ = Typ 2)
    if (child_id == CHILD_ID_GPS && msg_type == 1 && sub_type == 24) {
        empfangene_dem_hoehe = strtof(payload, NULL);
        hoehe_wurde_empfangen = true;
    }
}
// Hauptfunktion zur Übertragung des gesamten 3D-Wetterdatensatzes
void sende_gesamten_3d_payload(WetterDaten d) {
    char gps_string[64]; 
    float boot_speed = 0.0f;
    float boot_course = 0.0f;

    // 1. Standard Klima-, Horizontalwind- und Solarertragsdaten übertragen
    send_mysensors_msg(CHILD_ID_WIND, 4, d.wind_speed);       
    send_mysensors_msg(CHILD_ID_WIND, 5, d.wind_dir);         
    send_mysensors_msg(CHILD_ID_CLIMATE, 0, d.temperatur);    
    send_mysensors_msg(CHILD_ID_CLIMATE, 1, d.feuchtigkeit);  
    send_mysensors_msg(CHILD_ID_CLIMATE, 11, d.luftdruck);    
    send_mysensors_msg(CHILD_ID_SOLAR, 37, d.solar_w_m2);     

    // 2. NEU: Hybride Niederschlagsdaten (4 Nachkommastellen für Piezo-Auflösung)
    send_mysensors_rain_msg(CHILD_ID_RAIN, V_RAIN, d.regen_menge_mm);

    // 3. Positionsdaten (GPS) und Bootskinematik aufbereiten
    if (ist_mobiler_boots_modus) {
        float lat, lon, alt;
        lese_live_gps(lat, lon, alt, boot_speed, boot_course);
        snprintf(gps_string, sizeof(gps_string), "%.6f,%.6f,%.1f", lat, lon, alt);
    } else {
        // Stationärer Standard-Fall: Fest hinterlegte Werks-Koordinaten
        float stat_lat = 48.1351f; 
        float stat_lon = 11.5820f; 
        snprintf(gps_string, sizeof(gps_string), "%.6f,%.6f,%.1f", stat_lat, stat_lon, aktuelle_kalibrierung.stations_hoehe_nn);
        boot_speed = 0.0f; 
        boot_course = 0.0f;
    }

    // 4. GPS-String an FHEM rausschicken (Typ 49 = V_POSITION)
    printf("%d;%d;1;0;%d;%s\n", NODE_ID, CHILD_ID_GPS, V_POSITION, gps_string);
    
    // 5. Kinematische Zusatzdaten und rohe kartesische Windkomponenten tunneln
    send_mysensors_msg(CHILD_ID_DYNAMICS, V_VAR1, boot_speed);  
    send_mysensors_msg(CHILD_ID_DYNAMICS, V_VAR2, boot_course); 

    send_mysensors_msg(CHILD_ID_3D_VECTOR, V_VAR1, d.vx); 
    send_mysensors_msg(CHILD_ID_3D_VECTOR, V_VAR2, d.vy); 
    send_mysensors_msg(CHILD_ID_3D_VECTOR, V_VAR3, d.vz); 
}
```


## 3D-Ultraschall-Anemometer Firmware (Teil 2d: IMU-Lageberechnung)

### 2.3 Peripherie-Treiber & Tilt-Kompensation

Dieses Modul übernimmt das Aufwachen und die registerbasierte
Rohdatenauswertung des hocheffizienten 6-Achsen-Beschleunigungssensors
am I2C-Bus des RP2040.  Dateiname: imu_setup.cpp

```c++
#include <stdio.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include "global.h"
// Registerdefinitionen aus dem Datenblatt des LSM6DS3
#define LSM6DS3_REG_CTRL1_XL  0x10  // Steuerregister 1 für den Beschleunigungssensor
#define LSM6DS3_REG_OUTX_L_XL 0x28  // Startadresse der sequentiellen 3-Achsen-Daten
// Initialisierung des Lagesensors im stromsparenden On-Demand-Modus
void init_lsm6ds3_imu() {
    uint8_t config[2];
    config[0] = LSM6DS3_REG_CTRL1_XL;
    config[1] = 0x40; // Konfiguration: 104 Hz Abtastrate, +/- 2g Messbereich
    i2c_write_blocking(i2c0, LSM6DS3_ADDR, config, 2, false);
    sleep_ms(10);     // Einschwingzeit des Sensor-Cores abwarten
}
// Auslesen der aktuellen Roll- und Stampfwinkel
void lese_imu_neigung(float &roll_deg, float &pitch_deg) {
    // 0-Watt-Ruhestrom-Logik prüfen: War die mobile Schiene über den BSS84-MOSFET abgeschaltet?
    bool pwr_was_off = (gpio_get(PICO_COMPASS_EN) == 1);
    if (pwr_was_off) {
        gpio_put(PICO_COMPASS_EN, 0); // MOSFET einschalten (Gate auf LOW)
        sleep_ms(30);                 // Hardware-Power-Up-Sicherheitszeit abwarten
        init_lsm6ds3_imu();           // Register neu spiegeln
    }

    uint8_t start_reg = LSM6DS3_REG_OUTX_L_XL;
    uint8_t raw_data[6];
    
    // Sequentielles Auslesen von 6 Datenbytes (X, Y, Z jeweils Low- und High-Byte)
    i2c_write_blocking(i2c0, LSM6DS3_ADDR, &start_reg, 1, true); // Repeated Start erzwingen
    i2c_read_blocking(i2c0, LSM6DS3_ADDR, raw_data, 6, false);

    // 16-Bit Vorzeichenbehaftete Rohwerte zusammensetzen
    int16_t acc_x_raw = (raw_data[1] << 8) | raw_data[0];
    int16_t acc_y_raw = (raw_data[3] << 8) | raw_data[2];
    int16_t acc_z_raw = (raw_data[5] << 8) | raw_data[4];

    // Umrechnung der Rohwerte in g-Kräfte (16384 LSB/g bei einem +/- 2g Bereich)
    float ax = (float)acc_x_raw / 16384.0f;
    float ay = (float)acc_y_raw / 16384.0f;
    float az = (float)acc_z_raw / 16384.0f;

    // Trigonometrische Berechnung der statischen Neigungskomponenten via Arcustangens (in Grad)
    pitch_deg = atan2f(-ax, sqrtf(ay * ay + az * az)) * 180.0f / M_PI;
    roll_deg = atan2f(ay, az) * 180.0f / M_PI;

    // Wenn der stationäre Dachmodus aktiv ist, Peripherieschiene sofort wieder trennen (Energie sparen)
    if (pwr_was_off) {
        gpio_put(PICO_COMPASS_EN, 1); // MOSFET abschalten (Gate auf HIGH)
    }
}
```



## 3D-Ultraschall-Anemometer Firmware (Teil 2e: Hardware-Initialisierung)

### 2.4.1 Programmable I/O (PIO) Assemblerprogramm

Hochpräzisions-Timer Code (`ultrasonic.pio`)

Dieser ASM-Code läuft autark in den programmierbaren I/O-Blöcken (PIO) des Raspberry Pi Pico. Er generiert den 40-kHz-Burst für die Sender und misst die Laufzeit mit einer Auflösung von **16 Nanosekunden** (bei 2 MHz PIO-Taktfrequenz, abgeleitet vom Systemtakt).

```pasm
.program ultrasonic_timer
.side_set 1             ; 1 Pin für den physischen Sende-Burst (Side-Set)

push_wait:
    pull block          ; Warte auf den Startbefehl von der Haupt-CPU via FIFO
    set x, 31           ; Setze Zähler auf 31 (erzeugt exakt 32 Pulse für den Burst)

send_burst:
    set pins, 1 side 1 ; Sende-Pin HIGH für 25 Takte (1 + 24)
    set pins, 0 side 0 ; Sende-Pin LOW für 25 Takte (Mittenfrequenz genau 40 kHz)
    jmp x-- send_burst  ; Schleife wiederholen, die Mittenfrequenz beträgt 40 kHz

; --- BURST BEENDET - START DER HOCHPRÄZISEN ZEITMESSUNG ---
    set x, 0xFFFFFFFF   ; Initialisiere den 32-Bit Counter mit dem Maximalwert
count_loop:
    jmp pin trigger_hit ; Wenn der Komparator-Eingangspin HIGH geht -> Echo detektiert!
    jmp x-- count_loop  ; Dekrementiere Zähler und loope weiter (2 PIO-Takte pro Durchlauf)

trigger_hit:
    mov isr, x          ; Kopiere den verbleibenden Zählerstand in das Input Shift Register
    push block          ; Schiebe den Wert blockierend in den FIFO zur CPU-Auswertung
```


### 2.4.2 Programmable I/O (PIO) Bus-Injektion




Dieses Hardware-Modul koppelt die internen State-Machines
RP2040/RP2350 des Pico an die physikalischen Schaltpins des
Stecksockels und konfiguriert die mathematischen Taktteiler für die
Sub-Mikrosekunden-Laufzeitmessung.  Dateiname: hardware_setup.cpp

```c++
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "global.h"
// Hinweis zur Einbindung: Die automatisch aus der ultrasonic.pio generierte 
// Headerdatei wird hier vorausgesetzt (wird vom CMake-Compiler im Build-Ordner abgelegt)
#include "ultrasonic.pio.h"
void setup_ultrasonic_pio(PIO pio, uint sm, uint pin_tx, uint pin_echo) {
    // 1. Das compilierte PIO-Assemblerprogramm in den Befehlsspeicher des PIO-Blocks laden
    uint offset = pio_add_program(pio, &ultrasonic_timer_program);
    pio_sm_config config = ultrasonic_timer_program_get_default_config(offset);

    // 2. Hardware-Pins der State-Machine zuweisen
    sm_config_set_set_pins(&config, pin_tx, 1);       // SET-Befehl triggert Sende-Burst
    sm_config_set_sideset_pins(&config, pin_tx);      // Side-Set steuert synchrone Signalflanken
    sm_config_set_jmp_pin(&config, pin_echo);         // JMP-Befehl wartet auf Echo-Flanke des Komparators

    // 3. GPIO-Schnittstelle auf PIO-Betriebsmodus umschalten
    pio_gpio_init(pio, pin_tx);
    pio_gpio_init(pio, pin_echo);
    
    // 4. Physische Datenfluss-Richtungen festlegen (TX = Ausgang, Echo = Eingang)
    pio_sm_set_consecutive_pindirs(pio, sm, pin_tx, 1, true);
    pio_sm_set_consecutive_pindirs(pio, sm, pin_echo, 1, false);

    // 5. Mathematische Frequenzberechnung für eine ultrastabile 2-MHz-PIO-Taktbasis
    // Jedes PIO-Inkrement entspricht dadurch exakt 0,5 Mikrosekunden Auflösung.
    float ziel_frequenz_hz = 2000000.0f; 
    uint32_t system_clock_hz = clock_get_hz(clk_sys); 
    float clkdiv = (float)system_clock_hz / ziel_frequenz_hz;

    // 6. Taktteiler (Clock Divider) einspielen, SM initialisieren und scharfschalten
    sm_config_set_clkdiv(&config, clkdiv);
    pio_sm_init(pio, sm, offset, &config);
    pio_sm_set_enabled(pio, sm, true);
}
```

### 2.4.3 # 3D-Ultraschall-Anemometer Build-System (CMake)

**Dateiname:** `CMakeLists.txt`

**Zweck:** Steuerung des Compilers, automatische Generierung von `ultrasonic.pio.h` und Verknüpfung aller Hardware-Bibliotheken.

```cmake
# ====================================================================
# CMAKE BUILD-KONFIGURATION FÜR MODULARISIERTES 3D-ANEMOMETER
# ====================================================================

cmake_minimum_required(VERSION 3.13)

# Pico-SDK Standard-Import-Skript laden
include(pico_sdk_import.cmake)

project(3d_anemometer C CXX ASM)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_C_STANDARD 11)

# Initialisiere die SDK-Umgebung
pico_sdk_init()

# Haupt-Executable definieren (Alle modularisierten C++ Quelldateien auflisten)
add_executable(3d_anemometer
    main.cpp
    vector_math.cpp
    mysensors_payload.cpp
    hardware_setup.cpp
    imu_setup.cpp
)

# --- AUTOMATISCHE PIO-HEADER GENERIERUNG ---
# Dieser Befehl übersetzt 'ultrasonic.pio' im Build-Ordner vollautomatisch zu 'ultrasonic.pio.h'
pico_generate_pio_header(3d_anemometer \${CMAKE_CURRENT_LIST_DIR}/ultrasonic.pio)

# Verknüpfung aller erforderlichen Hardware-Treiberbibliotheken des Picos
target_link_libraries(3d_anemometer 
    pico_stdlib         # Basis-Bibliotheken (GPIO, UART, Standard-Sleep)
    hardware_pio        # Programmable I/O Peripherie-Treiber
    hardware_i2c        # I2C-Treiber (BME280, INA219, MMC5603NJ)
    hardware_spi        # SPI-Treiber (Sockel für nRF24L01+)
    hardware_flash      # Flash-Speicher Operations-Bibliothek (Kalibrierdaten)
    hardware_sync       # Interrupt-Sperrung für sichere Flash-Schreibvorgänge
    hardware_rtc        # Echtzeituhr für das zyklische Wecken aus dem Tiefschlaf
    hardware_clocks     # Taktfrequenz-Abfrage für clk_sys (2-MHz-Berechnung)
)

# Textausgabe über den USB-Port des Picos aktivieren (für Thonny / Terminal)
# Gleichzeitig wird die Ausgabe über die physischen UART-Pins (GPIO 0/1) deaktiviert
pico_enable_stdio_usb(3d_anemometer 1)
pico_enable_stdio_uart(3d_anemometer 0)

# Generiert die ausführbare, per Drag-and-Drop flashende Datei (3d_anemometer.uf2)
pico_add_extra_outputs(3d_anemometer)
```



Hier ist Teil 2f der Firmware (main.cpp) im sauberen Markdown-Format mit vollständigen C++ Codeblöcken.
Dieses Core-Modul koordiniert den gesamten Ablauf. 


## 3D-Ultraschall-Anemometer Firmware (Teil 2f: Hauptsteuerung, Dual-Core-Scheduling & Hybrid-Regenlogik)

## 2.5 Zyklische Ablaufsteuerung & Inter-Core-Synchronisation

Dieses Core-Modul bildet das Zentrum der Station. Es teilt die
Rechenlast asynchron auf beide Prozessorkerne auf und implementiert
die mathematische Feldkalibrierung des Niederschlagssensors.

Es nutzt die Dual-Core-Architektur des RP2040 voll aus: Core 0 steuert
die Ultraschall-Messungen des Windes, die Datenübertragung und die
barometrische Höhenkompensation, während Core 1 völlig unabhängig und
hochfrequent die analogen Impulse des Piezo-Regensensors abtastet,
filtert und über den Interrupt der mechanischen Wippe im Hintergrund
live eicht.

Dateiname: main.cpp

```c++
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "hardware/clocks.h"
#include "hardware/flash.h"
#include "hardware/sync.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
#include "hardware/irq.h"
#include "global.h"
// Globale Systemvariableni
KalibrierDaten aktuelle_kalibrierung;
bool ist_mobiler_boots_modus = false;
volatile float empfangene_dem_hoehe = -999.0f;
volatile bool hoehe_wurde_empfangen = false;
// Volatile Variablen für das Inter-Core-Regenmess-System
volatile uint32_t wippen_klicks = 0;
volatile double absolute_regenmenge_mm = 0.0;
volatile uint64_t piezo_integral_seit_letztem_klick = 0;
volatile uint64_t letzter_wippen_timestamp = 0;
// Hardware-Initialisierung der grundlegenden Pico-Pins und Modi
void init_hardware_pins() {
    gpio_init(PICO_CAL_BUTTON);  gpio_set_dir(PICO_CAL_BUTTON, GPIO_IN);  gpio_pull_up(PICO_CAL_BUTTON);
    gpio_init(PICO_MODE_JUMPER);  gpio_set_dir(PICO_MODE_JUMPER, GPIO_IN);  gpio_pull_up(PICO_MODE_JUMPER);
    gpio_init(PICO_DIR_SEL);      gpio_set_dir(PICO_DIR_SEL, GPIO_OUT);     gpio_put(PICO_DIR_SEL, 0);
    
    // MOSFET-Schalter für die mobile Peripherie (GPS, IMU, Kompass)
    gpio_init(PICO_COMPASS_EN);   gpio_set_dir(PICO_COMPASS_EN, GPIO_OUT);
    gpio_put(PICO_COMPASS_EN, 1); // Standardmäßig hochohmig (0-Watt-Ruhezustand aktiv)
    
    sleep_ms(10);
    // Live-Abfrage des Jumpers an Pin 10 zur Festlegung des Betriebsmodus
    ist_mobiler_boots_modus = (gpio_get(PICO_MODE_JUMPER) == 0);
}
// ISR für die mechanische Kippwippe im geerdeten Unterteil (Hardware-Debounced)
void wippe_isr(uint gpio, uint32_t events) {
    uint64_t jetzt = time_us_64();
    // 250ms Software-Sperrzeit gegen mechanisches Nachfedern der Kippschale
    if ((jetzt - letzter_wippen_timestamp) > 250000) { 
        wippen_klicks++;
        letzter_wippen_timestamp = jetzt;
        
        // Im mobilen Modus wird das Signal der Wippe komplett ignoriert (Schwingungs-Schutz)
        if (!ist_mobiler_boots_modus) {
            // Absolute mathematische Addition basierend auf der skalierten Geometrie
            absolute_regenmenge_mm += MM_PRO_KIPP; 
            
            // AUTOMATISCHE SELD-EICHUNG (Feldkalibrierung des Piezosensors)
            if (piezo_integral_seit_letztem_klick > 5000) {
                // Der Pico berechnet den Skalierungsfaktor live anhand des realen Wippenvolumens
                aktuelle_kalibrierung.piezo_k_factor = MM_PRO_KIPP / (double)piezo_integral_seit_letztem_klick;
                piezo_integral_seit_letztem_klick = 0; // Segment-Zurücksetzen für den nächsten Zyklus
            }
        }
    }
}
// CORE 1 Hauptschleife: Hochfrequente akustische Abtastung (Piezo-Regensensor)void core1_entry() {
    adc_init();
    adc_gpio_init(PIN_PIEZO_ADC);
    adc_select_input(PIEZO_ADC_NUM);

    int16_t last_sample = 0;
    // Rauschfilter gegen rein mechanisches Windrütteln an den Karbonrohren
    const int16_t SCHWELLENWERT_WIND_RAUSCHEN = 45; 

    while (true) {
        uint16_t raw_sample = adc_read();
        // Virtuellen AC-Nullpunkt (2.5V Masse-Vorspannung des OPA2350) rechnerisch entfernen
        int16_t ac_signal = (int16_t)raw_sample - 2048; 
        
        // Digitaler Hochpass-Filter zur harten Kantendetektion des Tropfeneinschlags
        int16_t gefilterter_impuls = ac_signal - last_sample;
        last_sample = ac_signal;

        uint16_t absolut_amplitude = abs(gefilterter_impuls);

        if (absolut_amplitude > SCHWELLENWERT_WIND_RAUSCHEN) {
            // Kontinuierliche Integration der Signalfläche (Energie-Akkumulation)
            piezo_integral_seit_letztem_klick += absolut_amplitude;
            
            // Wenn das System auf dem Boot/Fahrzeug fährt, misst der Piezo autark
            if (ist_mobiler_boots_modus) {
                absolute_regenmenge_mm += (double)absolut_amplitude * aktuelle_kalibrierung.piezo_k_factor;
            }
        }
        sleep_us(100); // Exakte 10 kHz Signal-Abtastrate am Analog-Pin
    }
}
// Zeitmessung und FIFO-Auswertung der PIO-Ultraschall-Strecke
float pio_messen_und_berechnen(PIO pio, uint sm) {
    const uint32_t TIMEOUT_US = 3000;
    uint32_t start_zeit_us = time_us_32();
    bool echo_eingetroffen = false;

    while ((time_us_32() - start_zeit_us) < TIMEOUT_US) {
        if (!pio_sm_is_rx_fifo_empty(pio, sm)) { echo_eingetroffen = true; break; }
        tight_loop_contents();
    }
    if (!echo_eingetroffen) { pio_sm_clear_fifos(pio, sm); return ERROR_NO_ECHO; }

    uint32_t verbleibende_takte = pio_sm_get(pio, sm);
    uint32_t verbrauchte_pio_takte = 0xFFFFFFFF - verbleibende_takte;
    return ((float)(verbrauchte_pio_takte / 2) * 1000000.0f) / (float)clock_get_hz(clk_sys);
}
// Auslesen des MMC5603NJ Magnetometers
float messe_kompass_live() {
    gpio_put(PICO_COMPASS_EN, 0); // Schiene einschalten
    sleep_ms(50);                 
    float live_azimut = 12.5f;    // I2C-Treiber-Dummy für das Magnetometer
    if (!ist_mobiler_boots_modus) gpio_put(PICO_COMPASS_EN, 1); // Stationär direkt wieder ausschalten
    return live_azimut;
}
// Abwicklung des bidirektionalen Internet-DEM-Abgleichs via FHEM-Funkbrücke
void trigger_internet_hoehen_abgleich(float lat, float lon) {
    char pos_string[32];
    snprintf(pos_string, sizeof(pos_string), "%.6f,%.6f", lat, lon);
    
    // 1. Koordinaten per C_SET an das FHEM-Zentralgateway funken
    printf("%d;%d;1;0;49;%s\n", NODE_ID, CHILD_ID_GPS, pos_string);
    sleep_ms(100);
    
    // 2. Asynchronen Request (C_REQ) hinterhersenden
    hoehe_wurde_empfangen = false;
    printf("%d;%d;2;0;24;0\n", NODE_ID, CHILD_ID_GPS);
    
    uint32_t start_wait = time_us_32();
    // 10 Sekunden Hardware-Timeout-Schleife für den Internet-Zentralen-Response
    while (!hoehe_wurde_empfangen && (time_us_32() - start_wait) < 10000000) {
        tight_loop_contents(); 
    }
    
    if (hoehe_wurde_empfangen && empfangene_dem_hoehe > -999.0f) {
        aktuelle_kalibrierung.stations_hoehe_nn = empfangene_dem_hoehe;
    } else {
        aktuelle_kalibrierung.stations_hoehe_nn = 0.0f; // Bei Timeout auf Meereshöhe fallbacken
    }
}
// Laden/Initialisieren des nicht-flüchtigen Flash-Speichers am Pico-Heck
void lade_kalibrierung_aus_flash() {
    const uint8_t *flash_target_contents = (const uint8_t *) (XIP_BASE + FLASH_TARGET_OFFSET);
    memcpy(&aktuelle_kalibrierung, flash_target_contents, sizeof(KalibrierDaten));
    if (aktuelle_kalibrierung.magic_number != KALIBRIER_MAGIC) {
        aktuelle_kalibrierung.magic_number = KALIBRIER_MAGIC;
        aktuelle_kalibrierung.offset_w1 = 0.0f; aktuelle_kalibrierung.offset_w2 = 0.0f; aktuelle_kalibrierung.offset_w3 = 0.0f;
        aktuelle_kalibrierung.gehaeuse_nord_winkel = 0.0f; aktuelle_kalibrierung.stations_hoehe_nn = 0.0f;
        aktuelle_kalibrierung.piezo_k_factor = 0.000042; // Standard-Werkskoeffizient
    }
}
// Werks-Kalibrierungsroutine bei gedrücktem Taster (3 Sekunden)
void starte_kombinierte_werks_kalibrierung(float c, float t1, float t2, float t3) {
    const float s_teil = R_RADIUS * 1.4142135f;
    aktuelle_kalibrierung.offset_w1 = (s_teil / (t1 / 1000000.0f)) - c;
    aktuelle_kalibrierung.offset_w2 = (s_teil / (t2 / 1000000.0f)) - c;
    aktuelle_kalibrierung.offset_w3 = (s_teil / (t3 / 1000000.0f)) - c;
    
    aktuelle_kalibrierung.gehaeuse_nord_winkel = messe_kompass_live();
    
    if (!ist_mobiler_boots_modus) {
        trigger_internet_hoehen_abgleich(48.1351f, 11.5820f); // Beispiel-Koordinaten für den Abgleich
    } else {
        aktuelle_kalibrierung.stations_hoehe_nn = 0.0f;
    }

    uint8_t schreib_puffer[FLASH_PAGE_SIZE] = {0};
    memcpy(schreib_puffer, &aktuelle_kalibrierung, sizeof(KalibrierDaten));
    
    // Core 0 Interrupts einfrieren, Sektor löschen und Parameter dauerhaft einbrennen
    uint32_t ints = save_and_disable_interrupts();
    flash_range_erase(FLASH_TARGET_OFFSET, FLASH_SECTOR_SIZE);
    flash_range_program(FLASH_TARGET_OFFSET, schreib_puffer, FLASH_PAGE_SIZE);
    restore_interrupts(ints);
}
// Hardware-Dummies für Klimasensorik, Solar-INA und Satellitendaten
void lese_bme280_raw(float &t, float &p, float &h) { 
    t = 20.0f; 
    p = 1013.25f; 
    h = 60.0f; 
}
void lese_ina219_solar(float &watt) { 
    watt = 520.0f; 
}
void lese_live_gps(float &lat, float &lon, float &alt, float &sog, float &cog) { 
    lat = 48.1351f; 
    lon = 11.5820f; 
    alt = 0.0f; 
    sog = 5.14f; 
    cog = 45.0f; 
}
// CORE 0: Hauptprogramm, Ultraschall-Zyklen & Übertragungsschleife
int main() {
    stdio_init_all();
    init_hardware_pins();
    
    // 1. Scharfschalten des akustischen Regensignal-Prozessors auf Core 1
    multicore_launch_core1(core1_entry);

    // 2. Hardware-Konfiguration des Hall-Wippen-Interrupts an Pin 16 (Fallende Flanke)
    gpio_init(PIN_WIPPE_INTERRUPT);
    gpio_set_dir(PIN_WIPPE_INTERRUPT, GPIO_IN);
    gpio_pull_up(PIN_WIPPE_INTERRUPT);
    gpio_set_irq_enabled_with_callback(PIN_WIPPE_INTERRUPT, GPIO_IRQ_EDGE_FALLING, true, &wippe_isr);

    setup_ultrasonic_pio(pio0, 0, PICO_TX_TRIGGER, PICO_COMP_IN);
    lade_kalibrierung_aus_flash();
    WetterDaten daten;

    while (true) {
        float t_raw, p_raw, h_raw;
        lese_bme280_raw(t_raw, p_raw, h_raw); // Liest Außen-Lamellenzylinder aus
        daten.temperatur = t_raw; daten.feuchtigkeit = h_raw;
        lese_ina219_solar(daten.solar_w_m2);
        
        // Zuweisung der kumulierten Regenhöhe aus dem Inter-Core-Netzwerk
        daten.regen_menge_mm = (float)absolute_regenmenge_mm;
        
        // Mathematische Echtzeit-Schallgeschwindigkeits-Kompensation via Klimadaten
        float c_aktuell = 331.3f * sqrtf(1.0f + (t_raw / 273.15f));
        float h = aktuelle_kalibrierung.stations_hoehe_nn;
        
        // Exakte barometrische Reduktion mittels der geladenen Internet-Geländehöhe
        daten.luftdruck = (h > 0.0f) ? p_raw * powf(1.0f - ((0.0065f * h)/(t_raw + (0.0065f * h) + 273.15f)), -5.255f) : p_raw;


// Triggern der PIO-State-Machine für den Ultraschall-Messzyklus
pio_sm_put_blocking(pio0, 0, 1);
float t1 = pio_messen_und_berechnen(pio0, 0);
float t2 = 412.2f, t3 = 412.8f; // Dummies für Achsenpaare 2 und 3
// Überprüfung des 3-Sekunden-Kalibriertasters
if (gpio_get(PICO_CAL_BUTTON) == 0) {
uint8_t hold = 0;
while (gpio_get(PICO_CAL_BUTTON) == 0 && hold < 30) { sleep_ms(100); hold++; }
if (hold >= 30) starte_kombinierte_werks_kalibrierung(c_aktuell, t1, t2, t3);
}
// 3D-Vektor-Transformation und MySensors-Datenversand
transformiere_3d_wind(t1, t2, t3, c_aktuell, daten);
sende_gesamten_3d_payload(daten);
// Intelligente Energiespar-Taktung: 2 Min. im mobilen Modus, 15 Min. im stationären Dachmodus
uint32_t sleep_time_ms = ist_mobiler_boots_modus ? 120000 : 900000;
sleep_ms(sleep_time_ms);
}
}
```

#### Der fertige Projekt-Struktur-Check

Ihr Software-Projektordner ist damit jetzt modular und stabil
aufgebaut. Er muss vor dem Kompilieren exakt diese Dateien
enthalten:text

```
3D_Anemometer_Software/
+-- pico_sdk_import.cmake   # Die Standard-Importdatei aus dem Pico-SDK
+-- CMakeLists.txt          # Dieses soeben erzeugte Build-File
+-- ultrasonic.pio          # Ihr PIO-Assembler Zeitmessungscode
+-- global.h                # Zentrale Pin- und Strukturdefinitionen
+-- hardware_setup.cpp      # Die 2-MHz-PIO-Setup-Definition
+-- vector_math.cpp         # Die 3D-Transformationsmatrizen & Vektoralgebra
+-- mysensors_payload.cpp   # Der Datentunnel-Konstruktor für FHEM
+--  main.cpp                # Die Hauptschleife und Taster-Abfrage
```




