Dosieranlage/main_kommentiert.c

1683 lines
33 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <stm32f10x.h>
/**
* CMSIS-Header für STM32F10x.
* Enthält:
* - Registerdefinitionen
* - Peripheral-Strukturen
* - Basisadressen
*/
#include <armv30_std.h>
/**
* armv30_std Bibliothek.
*/
#include <Nextion.h>
/**
* Nextion-HMI-Bibliothek.
* Stellt UI-Kommunikation über UART bereit.
*/
#include <stdbool.h>
/**
* 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 07)
* und CRH (Pins 815) 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 07).
* &= 0 löscht alle 32 Bits.
*/
GPIOB -> CRL = 0x22222222;
/**
* 0x2 = 0010b pro Pin
* MODE=10 (2 MHz Output)
* CNF=00 (Push-Pull)
*
* Ergebnis:
* PB0PB7 = 2 MHz Push-Pull Ausgang
*/
GPIOB -> CRH &= 0x00000000;
/**
* Reset von PB8PB15
*/
GPIOB -> CRH |= 0x88880000;
/**
* 0x8 = 1000b pro Pin
* MODE=00 (Input)
* CNF=10 (Pull-Up/Pull-Down)
*
* PB12PB15 werden als Eingang mit Pull-Widerstand konfiguriert.
*/
/**
* ============================
* Konfiguration GPIOA
* ============================
*/
GPIOA -> CRL &= 0x00000000; /**< Reset Pins PA0PA7 */
GPIOA -> CRL = 0x22222222;
/**
* PA0PA7 = 2 MHz Push-Pull Outputs
*/
GPIOA -> CRH &= 0x00000000; /**< Reset Pins PA8PA15 */
GPIOA -> CRH |= 0x00088000;
/**
* 0x8 entspricht wieder:
* Input mit Pull-Up/Pull-Down
*/
/**
* ============================
* Konfiguration GPIOC
* ============================
*/
GPIOC -> CRL &= 0x00000000; /**< Reset PC0PC7 */
GPIOC -> CRL |= 0x28222222;
/**
* Mischung aus:
* 0x2 ... Output 2 MHz Push-Pull
* 0x8 ... Input Pull-Up/Pull-Down
*/
GPIOC -> CRH &= 0x00000000; /**< Reset PC8PC15 */
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 PB12PB15 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 07.
* 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 (0100 %)
*
* @param value Füllstandswert in Prozent (0100)
*
* @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 0100 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 (0100 %)
*
* @param value Eingangsgröße
* @param max Maximalwert der Eingangsgröße
*
* @return Prozentwert (0100)
*
* @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 (0255)
*
* @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 (0255) wird linear auf
* 0100 % 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 (06).
*/
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
* (0127)
*/
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;
}
}
}
}