#include /** * CMSIS-Header für STM32F10x. * Enthält: * - Registerdefinitionen * - Peripheral-Strukturen * - Basisadressen */ #include /** * armv30_std Bibliothek. */ #include /** * Nextion-HMI-Bibliothek. * Stellt UI-Kommunikation über UART bereit. */ #include /** * Stellt bool, true, false bereit. */ /** * @brief Globale Nextion-UI-Objekte * * @details * Jedes Objekt repräsentiert ein UI-Element im HMI. * Diese Strukturen enthalten: */ NexEventObj fuellstandInd; /**< ProgressBar für Füllstand */ NexEventObj pumpeInd; /**< Anzeige Pumpenzustand */ NexEventObj leerInd; /**< Anzeige Leerblasen */ NexEventObj feinInd; /**< Anzeige Feinventil */ NexEventObj grobInd; /**< Anzeige Grobventil */ NexEventObj fehlerInd; /**< Fehleranzeige */ NexEventObj fuellstandText; /**< Textfeld für Literanzeige */ /** * @brief Berechnet Bit-Banding Alias-Adresse * * @param a Basisadresse des Registers * @param b Bitnummer * * @return Alias-Adresse für atomaren Bitzugriff * * @details * Bit-Banding mappt jedes einzelne Bit * auf eine 32-Bit Alias-Adresse. * * Formel: * Alias = PERIPH_BB_BASE + (ByteOffset * 32) + (Bit * 4) */ #define BITBAND_PERI(a,b) ((PERIPH_BB_BASE + (a-PERIPH_BASE)*32 + (b*4))) /** * @brief Atomare GPIO-Ausgänge via Bit-Banding * * ODR = Output Data Register */ #define FEHLER *((volatile unsigned long *)(BITBAND_PERI(GPIOC_ODR,1))) /**< PC1, V3 */ #define LEERBLASEN *((volatile unsigned long *)(BITBAND_PERI(GPIOC_ODR,2))) /**< PC2, V4 */ #define FEIN *((volatile unsigned long *)(BITBAND_PERI(GPIOC_ODR,13))) /**< PC13, V5 */ #define GROB *((volatile unsigned long *)(BITBAND_PERI(GPIOB_ODR,7))) /**< PB7, V6 */ #define PUMPE *((volatile unsigned long *)(BITBAND_PERI(GPIOB_ODR,6))) /**< PB6, V7 */ /** * @brief DIP-Schalter via Bit-Banding * * IDR = Input Data Register */ #define Switch5 *((volatile unsigned long *)(BITBAND_PERI(GPIOB_IDR,12))) // DIL5 #define Switch4 *((volatile unsigned long *)(BITBAND_PERI(GPIOB_IDR,15))) // DIL4 #define Switch6 *((volatile unsigned long *)(BITBAND_PERI(GPIOB_IDR,14))) // DIL6 #define Switch7 *((volatile unsigned long *)(BITBAND_PERI(GPIOB_IDR,13))) // DIL7 #define Switch1 *((volatile unsigned long *)(BITBAND_PERI(GPIOC_IDR,10))) // DIL1 #define Switch0 *((volatile unsigned long *)(BITBAND_PERI(GPIOC_IDR,6))) // DIL0 #define Switch2 *((volatile unsigned long *)(BITBAND_PERI(GPIOA_IDR,11))) // DIL2 #define Switch3 *((volatile unsigned long *)(BITBAND_PERI(GPIOA_IDR,12))) // DIL3 /** * @brief Nextion-Konstanten */ #define NEX_PID_MAIN 0 /**< Hauptseite */ #define NEX_CID_PROG 1 /**< Komponenten-ID */ /** * @brief Regelparameter / Konstanten */ #define SOLLWERT 0x7F /**< Sollwert = 127d */ #define GAP 0x5F /**< Grobabschaltpunkt = 95d */ #define NACHLAUF 0x5 /**< Nachlaufmenge = 5l */ /** * @brief Array mit Zeigern auf alle DIP-Schalter * * Vorteil: * - Schleifenbasierte Auswertung möglich * - Kein mehrfacher Code */ volatile unsigned long* Switch[8] = { &Switch0, &Switch1, &Switch2, &Switch3, &Switch4, &Switch5, &Switch6, &Switch7 }; /** * @brief Software-Nachlaufkompensations Variable */ int nachlaufOffset = 0; /** * @brief SysTick Interrupt Service Routine * */ void SysTick_Handler() { STD_IncTick(); /**< Erhöht / Incrementiert den globalen System-Tick-Zähler */ } /** * @brief Initialisiert und konfiguriert alle verwendeten GPIO-Ports (A, B, C) * * @details * Diese Funktion aktiviert zunächst die Taktversorgung der GPIO-Peripherie * über das RCC (Reset and Clock Control). * * Anschließend werden die Konfigurationsregister CRL (Pins 0–7) * und CRH (Pins 8–15) beschrieben. * * STM32F1 GPIO-Konfigurationsstruktur: * --------------------------------------------------------- * Pro Pin existieren 4 Konfigurationsbits: * * MODE ... Ausgangsgeschwindigkeit * CNF ... Betriebsart (Push-Pull, Open-Drain, Input...) * * MODE: * 00 = Input * 01 = Output 10 MHz * 10 = Output 2 MHz * 11 = Output 50 MHz * * CNF bei Output: * 00 = Push-Pull * 01 = Open-Drain * 10 = Alternate Function Push-Pull * 11 = Alternate Function Open-Drain * * CNF bei Input: * 00 = Analog * 01 = Floating * 10 = Pull-Up / Pull-Down * * Wichtig: * Bei Input Pull-Up/Pull-Down bestimmt das ODR-Bit: * ODR = 1 ... Pull-Up * ODR = 0 ... Pull-Down */ void PortConfig(void) { /** * @brief Aktivierung der GPIO-Takte * * APB2ENR = Advanced Peripheral Bus 2 Enable Register * GPIOA/B/C liegen auf APB2. */ RCC -> APB2ENR |= RCC_APB2ENR_IOPCEN; /**< GPIOC Clock Enable */ RCC -> APB2ENR |= RCC_APB2ENR_IOPBEN; /**< GPIOB Clock Enable */ RCC -> APB2ENR |= RCC_APB2ENR_IOPAEN; /**< GPIOA Clock Enable */ /** * ============================ * Konfiguration GPIOB * ============================ */ GPIOB -> CRL &= 0x00000000; /** * CRL Reset (Pins 0–7). * &= 0 löscht alle 32 Bits. */ GPIOB -> CRL = 0x22222222; /** * 0x2 = 0010b pro Pin * MODE=10 (2 MHz Output) * CNF=00 (Push-Pull) * * Ergebnis: * PB0–PB7 = 2 MHz Push-Pull Ausgang */ GPIOB -> CRH &= 0x00000000; /** * Reset von PB8–PB15 */ GPIOB -> CRH |= 0x88880000; /** * 0x8 = 1000b pro Pin * MODE=00 (Input) * CNF=10 (Pull-Up/Pull-Down) * * PB12–PB15 werden als Eingang mit Pull-Widerstand konfiguriert. */ /** * ============================ * Konfiguration GPIOA * ============================ */ GPIOA -> CRL &= 0x00000000; /**< Reset Pins PA0–PA7 */ GPIOA -> CRL = 0x22222222; /** * PA0–PA7 = 2 MHz Push-Pull Outputs */ GPIOA -> CRH &= 0x00000000; /**< Reset Pins PA8–PA15 */ GPIOA -> CRH |= 0x00088000; /** * 0x8 entspricht wieder: * Input mit Pull-Up/Pull-Down */ /** * ============================ * Konfiguration GPIOC * ============================ */ GPIOC -> CRL &= 0x00000000; /**< Reset PC0–PC7 */ GPIOC -> CRL |= 0x28222222; /** * Mischung aus: * 0x2 ... Output 2 MHz Push-Pull * 0x8 ... Input Pull-Up/Pull-Down */ GPIOC -> CRH &= 0x00000000; /**< Reset PC8–PC15 */ GPIOC -> CRH |= 0x22222822; /** * Mischung aus: * 0x2 ... Output 2 MHz Push-Pull * 0x8 ... Input Pull-Up/Pull-Down */ /** * ============================ * Initialisierung der Ausgangspegel * ============================ * * ODR = Output Data Register * * Bei Output: * 1 = High * 0 = Low * * Bei Input Pull: * 1 = Pull-Up * 0 = Pull-Down */ GPIOA -> ODR &= 0x0000; GPIOB -> ODR &= 0x0000; GPIOC -> ODR &= 0x0000; /** * Setzt alle Ausgänge zunächst auf LOW. */ GPIOA -> ODR |= 0x1800; /** * Aktiviert Pull-Ups bzw. setzt bestimmte Pins High. */ GPIOB -> ODR |= 0xF000; /** * Setzt PB12–PB15 High ... Pull-Up aktiv. */ GPIOC -> ODR |= 0x0440; /** * Setzt definierte Pins auf High. */ } /** * @brief Initialisiert die serielle Schnittstelle USART2 (19200 Baud, n, 8, 1) * * @details * Diese Funktion konfiguriert: * - Taktversorgung (RCC) * - GPIO-Pins für TX und RX * - USART2 Parameter (Datenformat, Baudrate) * - Aktivierung von Sender und Empfänger * * Frame-Format (8N1): * -> | Start | 8 Datenbits | Stop | -> * * Keine Parität */ void uart2_init(void) { /** * ========================== * 1. Taktaktivierung * ========================== */ RCC->APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN; /** * AFIOEN ... Aktiviert Alternate Function IO * IOPAEN ... Aktiviert GPIOA */ RCC->APB1ENR |= 0x20000; /** * Bit 17 = USART2EN * * Aktiviert Taktversorgung für USART2 auf APB1. */ /** * ========================== * 2. GPIO Konfiguration * ========================== * * PA2 ... TX * PA3 ... RX * * CRL steuert Pins 0–7. * Jeder Pin besitzt 4 Konfigurationsbits. */ GPIOA->CRL &= 0xFFFFF0FF; /** * Löscht Konfiguration von PA2 (Bits 8-11). * Maske: 1111 1111 1111 1111 1111 0000 1111 1111 */ GPIOA->CRL |= 0xB00; /** * 0xB = 1011b * * MODE=11 ... Output 50 MHz * CNF=10 ... Alternate Function Push-Pull * * Ergebnis: * PA2 = USART2_TX */ GPIOA->CRL &= 0xFFFF0FFF; /** * Löscht Konfiguration von PA3 (Bits 12-15). */ GPIOA->CRL |= 0x4000; /** * 0x4 = 0100b * * MODE=00 ... Input * CNF=01 ... Floating Input * * Ergebnis: * PA3 = USART2_RX */ /** * ========================== * 3. USART Konfiguration * ========================== */ USART2->CR1 &= ~0x1000; /** * M-Bit = 0 * 8 Datenbits * * M=1 ... 9 Bit */ USART2->CR1 &= ~0x0400; /** * PCE = 0 * Parity Control deaktiviert * * Kein Paritätsbit */ USART2->CR2 &= ~0x3000; /** * STOP[12-13] = 00 * 1 Stopbit */ USART2->BRR = 0x0753; /** * Baudrate Register * * Mantisse + Fraction ergeben 0x1D4C */ USART2->CR1 |= 0x0C; /** * Bit 3 = TE (Transmitter Enable) * Bit 2 = RE (Receiver Enable) * * Aktiviert Sende- und Empfangseinheit. */ USART2->CR1 |= 0x2000; /** * UE Bit = USART Enable * * Startet die USART-Hardware. */ } /** * @brief Sendet ein einzelnes Zeichen über USART2 * * @param zeichen ASCII-Zeichen, das übertragen werden soll * * @details * Diese Funktion implementiert eine blockierende UART-Übertragung. * Es wird solange gewartet, bis das TXE-Bit (Transmit Data Register Empty) * im Statusregister (SR) gesetzt ist. * * Ablauf: * 1. Prüfen, ob Datenregister leer ist (TXE = 1) * 2. Schreiben des Zeichens in das DR-Register * 3. Hardware übernimmt serielle Ausgabe automatisch */ void uart2_putChar(char zeichen) { /** * Warten bis TXE (Bit 7 im Statusregister) gesetzt ist. * * USART2->SR & 0x80: * 0x80 = 1000 0000b ... Bit 7 auf 1 * * Solange TXE = 0 bleibt, * befindet sich noch ein Zeichen im Datenregister. */ while(!((USART2->SR & 0x80) == 0x80)); /** * Schreiben des Zeichens in das Datenregister. * * Das Schreiben startet sofort: * - Startbit wird generiert * - 8 Datenbits werden seriell ausgegeben * - Stopbit folgt */ USART2->DR = zeichen; } /** * @brief Sendet eine Zeichenkette über USART2 * * @param string Zeiger auf ein char-Array (char*) * * @details * Diese Funktion überträgt eine ASCII-Zeichenkette, * * char-Array ist mit (\n) nullterminiert! * Dieses wird nicht mitgesendet. * * Die Funktion arbeitet blockierend und ruft * für jedes Zeichen uart2_putChar() auf. */ void uart2_putString(char* string) { /** * Schleife läuft solange: * *string != 0 * * Das Dereferenzieren (*string) liest * das aktuelle Zeichen im Speicher. */ while(*string) { /** * Übergibt aktuelles Zeichen * an uart2_putChar(). * * *string liest das Zeichen. * string++ erhöht den Pointer * auf das nächste Zeichen. */ uart2_putChar(*string++); } } /** * @brief Initialisiert ein Nextion-Event-Objekt * * @param obj Zeiger auf das zu initialisierende NexEventObj * @param pid Page-ID (Seitenkennung im Nextion-Display) * @param cid Component-ID (Objektkennung auf der Seite) * @param name Name des UI-Elements (z.B. "pumpeInd") * * @details * Diese Funktion setzt die Grundparameter eines * Nextion-Objekts und versetzt es in einen definierten * Startzustand. */ static void NexEventObj_Init(NexEventObj *obj, uint8_t pid, uint8_t cid, const char *name) { /** * Speichert die Seiten-ID. * * pid referenziert die Seite im Nextion-HMI-Editor. * Beispiel: * page0 ... pid = 0 */ obj->pid = pid; /** * Speichert die Component-ID. * * Jede Komponente auf einer Seite * besitzt eine eindeutige cid. */ obj->cid = cid; /** * Speichert den Namen des UI-Elements. */ obj->name = name; /** * Setzt den Pop-Handler auf NULL. * * pophandler wird ausgelöst, * wenn ein Button losgelassen wird. */ obj->pophandler = NULL; /** * Setzt den Push-Handler auf NULL. * * pushhandler wird ausgelöst, * wenn ein Button gedrückt wird. * * Initial NULL ... sicherer Zustand. */ obj->pushhandler = NULL; } /** * @brief Initialisiert alle verwendeten Nextion-UI-Objekte * * @details * Diese Funktion ruft für jedes globale NexEventObj * die Initialisierungsroutine NexEventObj_Init() auf. * * @note * Alle Objekte liegen auf derselben Page (NEX_PID_MAIN) */ void configNexObjects(void) { /** * Füllstandsanzeige (ProgressBar) */ NexEventObj_Init(&fuellstandInd, NEX_PID_MAIN, NEX_CID_PROG, "fuellstand"); /** * Fehleranzeige (ProgressBar oder LED) */ NexEventObj_Init(&fehlerInd, NEX_PID_MAIN, NEX_CID_PROG, "fehlerInd"); /** * Anzeige für Leerblas-Ventil */ NexEventObj_Init(&leerInd, NEX_PID_MAIN, NEX_CID_PROG, "leerInd"); /** * Anzeige für Feinventil */ NexEventObj_Init(&feinInd, NEX_PID_MAIN, NEX_CID_PROG, "feinInd"); /** * Anzeige für Grobventil */ NexEventObj_Init(&grobInd, NEX_PID_MAIN, NEX_CID_PROG, "grobInd"); /** * Anzeige für Pumpenzustand */ NexEventObj_Init(&pumpeInd, NEX_PID_MAIN, NEX_CID_PROG, "pumpeInd"); /** * Textfeld für numerische Füllstandsausgabe */ NexEventObj_Init(&fuellstandText, NEX_PID_MAIN, NEX_CID_PROG, "fuellstandn"); } /** * @brief Setzt oder löscht den globalen Fehlerzustand * * @param state true ... Fehler aktiv * false ... Fehler behoben * * @details * Diese Funktion realisiert eine zustandsabhängige * Fehleranzeige mit folgenden Eigenschaften: * * - Entprellung auf Software-Ebene (keine Doppelupdates) * - Initialisierung beim ersten Aufruf * - Synchronisierung von: * • HMI-Anzeige (Nextion ProgressBar) * • Hardware-Ausgang (Bit-Banding GPIO) * • UART-Debugausgabe * * Zustände: * 0 ... Kein Fehler * 1 ... Fehler aktiv */ void fehler(bool state) { /** * Speichert den letzten gesetzten Zustand. * static ... bleibt über Funktionsaufrufe erhalten. * * Initialwert = true */ static bool lastStateF = true; /** * Kennzeichnet den ersten Funktionsaufruf. * Dient zur Initialisierung der Anzeige. */ static bool firstCallF = true; /** * Initialisierungsblock * * Beim allerersten Aufruf: * - Anzeige wird auf 0 gesetzt * - Keine Hardwareaktion * - Keine UART-Meldung */ if (firstCallF) { firstCallF = false; /**< Initialisierung abgeschlossen */ /** * Setzt ProgressBar auf 0%. * Anzeige: kein Fehler. */ NexProgressBar_setValue(&fehlerInd, 0); return; /**< Funktion verlassen */ } /** * Falls sich der Zustand nicht geändert hat ... Keine Aktion notwendig. * * Verhindert: * - unnötige UART-Ausgaben * - unnötige HMI-Kommandos * - unnötige GPIO-Schaltvorgänge */ else if (state == lastStateF) { return; } /** * Aktualisiert gespeicherten Zustand. */ lastStateF = state; /** * Fehler aktiv */ if(state) { /** * HMI: Fehleranzeige auf 100% */ NexProgressBar_setValue(&fehlerInd, 100); /** * Hardware-Ausgang setzen. * * FEHLER ist ein Bit-Banding-Makro ... Direkter atomarer Zugriff auf ein einzelnes ODR-Bit. */ FEHLER = 1; /** * UART-Debugmeldung. */ uart2_putString("Fehler aufgetreten!\r\n"); } else { /** * HMI: Anzeige zurücksetzen */ NexProgressBar_setValue(&fehlerInd, 0); /** * Hardware-Ausgang löschen. */ FEHLER = 0; /** * UART-Meldung */ uart2_putString("Fehler behoben!\r\n"); } return; } /** * @brief Steuert das Leerblasventil (Ein/Aus) mit Zustandsüberwachung * * @param state true ... Ventil öffnen * false ... Ventil schließen * * @details * Diese Funktion implementiert eine zustandsbasierte * Aktorsteuerung für das Leerblasventil. * * Eigenschaften: * - Initialisierung beim ersten Aufruf * - Keine Mehrfachschaltungen bei gleichem Zustand * - Synchronisierung von: * • HMI-Anzeige (Nextion ProgressBar) * • GPIO-Ausgang (Bit-Banding) * • UART-Debugmeldung * * Zustand 0 ... Ventil geschlossen * Zustand 1 ... Ventil geöffnet */ void leerBlasen(bool state) { /** * Speichert den zuletzt gesetzten Zustand. * static ... bleibt über Funktionsaufrufe erhalten. */ static bool lastStateB; /** * Kennzeichnet ersten Funktionsaufruf. * Dient zur Anzeigeinitialisierung. */ static bool firstCallB = true; /** * Initialisierung beim ersten Aufruf. * * Verhindert: * - unerwünschtes Einschalten beim Systemstart * - falsche Anzeigezustände */ if (firstCallB) { firstCallB = false; /** * HMI-Anzeige auf 0% * ? Ventil geschlossen */ NexProgressBar_setValue(&leerInd, 0); return; } /** * Wenn Zustand unverändert ... keine Aktion. * * Verhindert: * - unnötige UART-Ausgaben * - unnötiges GPIO-Toggeln * - unnötige HMI-Kommandos */ else if (state == lastStateB) { return; } /** * Zustand aktualisieren. */ lastStateB = state; /** * Ventil öffnen */ if(state) { /** * HMI: Anzeige 100% */ NexProgressBar_setValue(&leerInd, 100); /** * GPIO setzen via Bit-Banding. * * Atomarer Zugriff auf einzelnes ODR-Bit. */ LEERBLASEN = 1; /** * Debugmeldung über UART. */ uart2_putString("Leerblasventil geoeffnet!\r\n"); } else { /** * HMI zurücksetzen. */ NexProgressBar_setValue(&leerInd, 0); /** * GPIO löschen. */ LEERBLASEN = 0; /** * Debugmeldung. */ uart2_putString("Leerblasventil geschlossen!\r\n"); } return; } /** * @brief Steuert das Fein-Dosierventil (Ein/Aus) mit Zustandsüberwachung * * @param state true ... Feinventil öffnen * false ... Feinventil schließen * * @details * Diese Funktion implementiert eine einfache Aktorsteuerung für das Feinventil der Dosieranlage. * * Aufgaben: * - Initialisierung beim ersten Funktionsaufruf * - Vermeidung redundanter Schaltvorgänge * - Synchronisierung von: * • Nextion-HMI-Anzeige * • GPIO-Ausgang (Bit-Banding) * • UART-Debugausgabe * * Zustände: * 0 ... Ventil geschlossen * 1 ... Ventil geöffnet */ void fein(bool state) { /** * Speichert den letzten Ventilzustand. * static ... persistiert über Funktionsaufrufe. */ static bool lastStateFein; /** * Kennzeichnet den ersten Aufruf der Funktion. * Dient zur sauberen Initialisierung der Anzeige. */ static bool firstCallFein = true; /** * Initialisierungsblock. * * Beim ersten Aufruf: * - Anzeige wird auf 0 gesetzt * - Keine Hardwareänderung */ if (firstCallFein) { firstCallFein = false; /** * ProgressBar = 0% * ? Ventil geschlossen anzeigen. */ NexProgressBar_setValue(&feinInd, 0); return; } /** * Falls sich der gewünschte Zustand * nicht geändert hat ... Keine Aktion durchführen. * * Verhindert: * - unnötiges Toggeln der Ventilspule * - unnötige UART-Ausgaben * - unnötige HMI-Kommandos */ else if (state == lastStateFein) { return; } /** * Zustand aktualisieren. */ lastStateFein = state; /** * Ventil öffnen */ if(state) { /** * HMI-Anzeige auf 100%. */ NexProgressBar_setValue(&feinInd, 100); /** * GPIO-Bit setzen (Bit-Banding). * * Atomarer Zugriff auf einzelnes ODR-Bit. */ FEIN = 1; /** * UART-Debugmeldung. */ uart2_putString("Feinventil geoeffnet!\r\n"); } else { /** * Anzeige zurücksetzen. */ NexProgressBar_setValue(&feinInd, 0); /** * Feinventil abschalten. */ FEIN = 0; /** * Debugmeldung. */ uart2_putString("Feinventil geschlossen!\r\n"); } return; } /** * @brief Steuert das Grobventil (Ein/Aus) mit Zustandsüberwachung * * @param state true ... Grobventil öffnen * false ... Grobventil schließen * * @details * Diese Funktion implementiert eine einfache Aktorsteuerung für das Grobventil der Dosieranlage. * * Eigenschaften: * - Initialisierung beim ersten Aufruf * - Keine redundanten Schaltvorgänge * - Synchronisierung von: * • Nextion-HMI-Anzeige * • GPIO-Ausgang (Bit-Banding) * • UART-Debugausgabe */ void grob(bool state) { /** * Speichert den zuletzt gesetzten Zustand. * static ... Wert bleibt zwischen Funktionsaufrufen erhalten. */ static bool lastStateGrob; /** * Kennzeichnet den ersten Funktionsaufruf. * Dient der Anzeigeinitialisierung. */ static bool firstCallGrob = true; /** * Initialisierungsblock. * * Beim ersten Aufruf: * - Anzeige wird auf 0 gesetzt * - Keine Hardwareänderung */ if (firstCallGrob) { firstCallGrob = false; /** * HMI-Anzeige ? 0% * Ventil geschlossen darstellen. */ NexProgressBar_setValue(&grobInd, 0); return; } /** * Falls gewünschter Zustand identisch * mit dem letzten Zustand ist ... Keine Aktion durchführen. */ else if (state == lastStateGrob) { return; } /** * Aktualisierung des gespeicherten Zustands. */ lastStateGrob = state; /** * Grobventil öffnen */ if(state) { /** * Anzeige 100% */ NexProgressBar_setValue(&grobInd, 100); /** * GPIO setzen via Bit-Banding. * * Atomarer Zugriff auf ein einzelnes ODR-Bit. */ GROB = 1; /** * Debugmeldung. */ uart2_putString("Grobventil geoeffnet!\r\n"); } else { /** * Anzeige zurücksetzen. */ NexProgressBar_setValue(&grobInd, 0); /** * Ventil deaktivieren. */ GROB = 0; /** * Debugmeldung. */ uart2_putString("Grobventil geschlossen!\r\n"); } return; } /** * @brief Steuert die Förderpumpe der Dosieranlage (Ein/Aus) * * @param state true ... Pumpe einschalten * false ... Pumpe ausschalten * * @details * Diese Funktion implementiert eine einfache Steuerlogik (Finite State Machine) für die Hauptpumpe. * * Eigenschaften: * - Initialisierung beim ersten Aufruf * - Keine mehrfachen Schaltvorgänge bei gleichem Zustand * - Synchronisierung von: * • Nextion-HMI-Anzeige (ProgressBar) * • GPIO-Ausgang (Bit-Banding) * • UART-Debugausgabe */ void pumpe(bool state) { /** * Speichert den zuletzt gesetzten Pumpenzustand. * static ... persistiert über Funktionsaufrufe. */ static bool lastState; /** * Kennzeichnet ersten Funktionsaufruf. * Dient zur Initialisierung der Anzeige. */ static bool firstCall = true; /** * Initialisierungsphase. * * Beim ersten Aufruf: * - Anzeige wird auf 0 gesetzt * - Keine GPIO-Aktion */ if (firstCall) { firstCall = false; /** * ProgressBar auf 0% ... Pumpe aus anzeigen. */ NexProgressBar_setValue(&pumpeInd, 0); return; } /** * Falls der gewünschte Zustand * identisch mit dem letzten Zustand ist ... Keine Aktion. */ else if (state == lastState) { return; } /** * Zustand aktualisieren. */ lastState = state; /** * Einschalten der Pumpe */ if(state) { /** * Anzeige 100% */ NexProgressBar_setValue(&pumpeInd, 100); /** * GPIO-Bit setzen (Bit-Banding). */ PUMPE = 1; /** * Debugmeldung. */ uart2_putString("Pumpe eingeschaltet!\r\n"); } else { /** * Anzeige zurücksetzen. */ NexProgressBar_setValue(&pumpeInd, 0); /** * GPIO löschen. */ PUMPE = 0; /** * Debugmeldung. */ uart2_putString("Pumpe ausgeschaltet!\r\n"); } return; } /** * @brief Aktualisiert die Füllstandsanzeige (0–100 %) * * @param value Füllstandswert in Prozent (0–100) * * @details * Diese Funktion übernimmt die Anzeige des aktuellen * Füllstands auf dem Nextion-Display. * * Eigenschaften: * - Initialisierung beim ersten Aufruf * - Keine redundante Aktualisierung bei gleichem Wert * - Plausibilitätsprüfung des Wertebereichs * - Debug-Ausgabe bei Änderung * * Wertebereich: * 0 ... leer * 100 ... voll * * @note * Datentyp int8_t erlaubt Werte von -128 bis +127. * Daher ist eine Bereichsprüfung notwendig. */ void fuellstand(int8_t value) { /** * Kennzeichnet den ersten Funktionsaufruf. * Dient zur Initialisierung der Anzeige. */ static bool firstCall = true; /** * Speichert den zuletzt angezeigten Wert. * * Initialwert -1 dient als ungültiger Marker, * da gültiger Bereich 0–100 ist. */ static int8_t lastValue = -1; /** * Initialisierungsphase. * * Beim ersten Aufruf: * - Anzeige auf 0 setzen * - Keine weitere Aktion */ if(firstCall) { firstCall = false; /** * ProgressBar auf 0% */ NexProgressBar_setValue(&fuellstandInd, 0); return; } /** * Falls sich der Wert nicht geändert hat ... Keine Aktualisierung durchführen. */ else if (value == lastValue) { return; } /** * Speichert neuen Wert. */ lastValue = value; /** * Plausibilitätsprüfung. * * Nur Werte zwischen 0 und 100 % * werden akzeptiert. */ if(value >= 0 && value <= 100) { /** * Anzeige des Füllstands. * * Übergabe direkt als Prozentwert. */ NexProgressBar_setValue(&fuellstandInd, value); /** * Debugmeldung. */ uart2_putString("Fuellstand geaendert!\r\n"); } else { /** * Ungültiger Wert ... Anzeige zurücksetzen. */ NexProgressBar_setValue(&fuellstandInd, 0); /** * Debugmeldung bei Fehler. */ uart2_putString("Uengueltiger Fuellstandswert!\r\n"); } } /** * @brief Skaliert einen Messwert auf Prozent (0–100 %) * * @param value Eingangsgröße * @param max Maximalwert der Eingangsgröße * * @return Prozentwert (0–100) * * @details * Diese Funktion führt eine lineare Skalierung durch: * * Prozent = value / max * 100 * * Um Rundungsfehler bei Ganzzahlarithmetik zu vermeiden, * wird vor der Division max/2 addiert. * * Formel mit Rundung: * * (value * 100 + max/2) / max * * Dadurch wird mathematisch korrekt gerundet * (statt immer abzurunden). * * @note * max darf nicht 0 sein (Division durch 0!). */ uint8_t toPercent(uint8_t value, uint8_t max) { /** * Falls value größer als max ist, * wird value auf max begrenzt. * * Verhindert: * - Prozentwerte > 100 */ if (value > max) value = max; /** * Prozentberechnung mit Rundung. * * Schritt 1: * value * 100 ... Skalierung auf Prozentbasis * * Schritt 2: * + max/2 ... Rundung auf nächstes Ganzzahl-Ergebnis * * Schritt 3: * Division durch max * * Ganzzahl-Division rundet normalerweise ab. * Durch + max/2 wird korrekt gerundet. */ return (value * 100 + max / 2) / max; } /** * @brief Setzt den numerischen Füllstandstext und aktualisiert die Prozentanzeige * * @param value Absoluter Füllwert (0–255) * * @details * Diese Funktion übernimmt zwei Aufgaben: * * 1. Formatierung des absoluten Füllwerts als Text ... Anzeige in Litern auf dem Nextion-Display * * 2. Umrechnung des Wertes in Prozent ... Aktualisierung der ProgressBar */ void setFuellstand(uint8_t value) { /** * Lokaler Zeichenpuffer. * * Größe: 20 Byte */ char buf[20]; /** * Formatierung des Wertes als String. * * "%u l": * - %u ... unsigned Integer * - " l" ... Einheit Liter * * sprintf: * - erzeugt Null-terminierten String */ sprintf(buf, "%u l", value); /** * Übergibt den formatierten Text * an das Nextion-Textobjekt. */ NexText_setText(&fuellstandText, buf); /** * Aktualisiert zusätzlich die Prozentanzeige. * * value (0–255) wird linear auf * 0–100 % skaliert. */ fuellstand(toPercent(value, 255)); } /** * @brief Wandelt den Zustand der DIP-Schalter in einen Integerwert um. * * @return Ganzzahlige Bitkodierung der Schalterzustände * * @details * Diese Funktion liest die digitalen Eingänge aus dem * globalen Pointer-Array "Switch[]" ein und erzeugt daraus * eine Bitmaske. * * Logik: * - Jeder Schalter repräsentiert ein Bit. * - Schalter gedrückt (Low-Pegel) ... Bit wird gesetzt. * - Schalter offen (High-Pegel) ... Bit bleibt 0. * * Daraus ergibt sich: * result = Summe( 2^i ) für alle aktiven Schalter. * * Beispiel: * Switch0 = aktiv * Switch1 = inaktiv * Switch2 = aktiv * - result = 0000 0101b = 5 */ int SwitchesToInt(void) { /** * Ergebnisvariable. * * Initialwert = 0 * Enthält am Ende die Bitmaske. */ int result = 0; /** * Zählvariable (0–6). */ uint8_t i = 0; /** * Schleife über 7 Schalter. * * Hinweis: * Switch[7] wird hier NICHT berücksichtigt. */ for (i = 0; i < 7; i++) { /** * Dereferenzieren des Zeigers: * * Switch[i] ... Zeiger auf ein Bit-Banding-Register * *Switch[i] ... aktueller GPIO-Pegel (0 oder 1) * * Active-Low-Logik: * 0 ... Schalter gedrückt */ if (!*Switch[i]) { /** * Setzt Bit i im Ergebnis. * * (1 << i) * erzeugt eine Maske: * * i = 0 ... 0000 0001 * i = 1 ... 0000 0010 * i = 2 ... 0000 0100 * ... * * result |= Maske ... Bit wird gesetzt, andere bleiben erhalten. */ result |= (1 << i); } } /** * Rückgabe der gebildeten Bitmaske. */ return result; } /** * @brief Hauptprogramm der Dosieranlage. * * @details * Implementiert: * - Initialisierung aller Peripherien * - HMI-Anbindung (Nextion) * - 2-Stufen-Dosierstrategie (Grob + Fein) * - Nachlaufkompensation * - Leitungsleerblasen nach Dosierende * * Regelprinzip: * - Grobventil ... hoher Durchfluss (Vorregelung) * - Feinventil ... präzise Enddosierung * - Pumpe ... Systemdruck * - Nachlaufmodell ... Kompensation von Restvolumen */ int main(void) { /* ============================= * Hardware-Initialisierung * ============================= */ PortConfig(); /**< GPIO-Konfiguration */ uart2_init(); /**< UART2 initialisieren */ /** * Nextion-Eventliste. * Hier leer ... keine Touch-Events registriert. */ NexEventObj *nextlisten_list[] = {NULL}; configNexObjects(); /**< UI-Objekte initialisieren */ Nex_Init(0); /**< Nextion initialisieren */ Init_Nex_UI(); /**< UI-Startzustand */ /** * Alle Anzeigen und Aktoren in definierten Startzustand setzen. */ fuellstand(0); fehler(false); leerBlasen(false); fein(false); grob(false); pumpe(false); wait_sys_ms(20); /**< kurze Stabilisierungspause */ /** * Statusvariablen */ bool leerGeblasen = false; /**< merkt, ob Dosierung stattfand */ bool leitungBenutzt = false; /**< merkt, ob Durchfluss stattfand */ uart2_putString("Dosieranlage V01\r\n"); /* ============================= * Hauptschleife * ============================= */ while(1) { /** * Nextion Event-Handling */ Nex_Event_Loop(nextlisten_list); wait_sys_ms(5); /**< Entprell-/Taktintervall */ /** * Istwert aus DIP-Schaltern lesen * (0–127) */ uint8_t istWert = SwitchesToInt(); /** * Effektiver Wert inklusive Nachlaufkompensation */ uint16_t effektiverWert = istWert + nachlaufOffset; /** * Start-Schalter (Switch[7]) Low-Active? ... Dosierung aktiv */ if(*Switch[7] == 0) { leerGeblasen = true; leitungBenutzt = false; pumpe(true); /**< Pumpe einschalten */ /* ============================= * Feinregelung * ============================= */ if(effektiverWert < SOLLWERT - NACHLAUF) { fein(true); /**< Feinventil öffnen */ leitungBenutzt = true; } else { fein(false); } /* ============================= * Grobregelung * ============================= */ if(effektiverWert < GAP) { grob(true); /**< Grobventil öffnen */ leitungBenutzt = true; } else { grob(false); } /** * Anzeige aktualisieren */ setFuellstand(effektiverWert); wait_sys_ms(5); } else { /** * Dosierung wurde beendet */ if(leerGeblasen) { pumpe(false); grob(false); fein(false); /** * Leitung leerblasen (Restvolumen entfernen) */ leerBlasen(true); if(leitungBenutzt) { /** * 3 Sekunden Nachlaufkompensation * Aufgeteilt in NACHLAUF Schritte */ int time = 3000 / NACHLAUF; /**< 3 Sekunden in passende Teile dividieren */ int rest = 3000 % NACHLAUF; /**< Rest berechnen */ fein(true); grob(true); uint8_t i = 0; for(i = 0; i < NACHLAUF; i++) { wait_sys_ms(time); /** * Restzeit verteilen */ if(rest > 0) { wait_sys_ms(1); rest -= 1; } /** * Offset erhöhen ... simuliert Restvolumen */ nachlaufOffset += 1; setFuellstand(istWert + nachlaufOffset); } } else { wait_sys_ms(3000); } fein(false); grob(false); leerBlasen(false); leerGeblasen = false; } } } }