# Sistema de Control de Cultivo de Champiñón ## Documentación técnica del programa PLC > **PLC:** Panasonic (lenguaje Structured Text, IEC 61131-3) > **Archivos fuente:** `Funciones.st` (53 funciones), `Programas.st` (16 programas) > **Arquitectura:** Dual-zona simétrica (Zona A · Zona B) con comunicación Modbus RTU maestro > **Última modificación del código:** 11/05/2026 --- ## Índice 1. [Resumen ejecutivo](#1-resumen-ejecutivo) 2. [Síntesis conceptual: sistema experto de climatización adaptativa](#2-síntesis-conceptual-sistema-experto-de-climatización-adaptativa) 3. [Arquitectura general](#3-arquitectura-general) 4. [Convenciones de nomenclatura de variables](#4-convenciones-de-nomenclatura-de-variables) 5. [Configuraciones físicas del sistema](#5-configuraciones-físicas-del-sistema) 6. [Conceptos clave del proceso](#6-conceptos-clave-del-proceso) 7. [Modelo de Procesos y Etapas](#7-modelo-de-procesos-y-etapas) 8. [Ciclo de scan del PLC (flujo maestro)](#8-ciclo-de-scan-del-plc) 9. [Referencia de Programas](#9-referencia-de-programas) 10. [Referencia de Funciones de control](#10-referencia-de-funciones-de-control) 11. [Diagramas de flujo por función](#11-diagramas-de-flujo-por-función) 12. [Bucles de control (matemática y PID)](#12-bucles-de-control) 13. [Cálculo psicrométrico (entalpía / humedad absoluta)](#13-cálculo-psicrométrico) 14. [Sistema de riego](#14-sistema-de-riego) 15. [Comunicaciones Modbus](#15-comunicaciones-modbus) 16. [Gestión de fechas y procesos secuenciales](#16-gestión-de-fechas-y-procesos-secuenciales) 17. [Alarmas y seguridad](#17-alarmas-y-seguridad) 18. [Apéndices](#18-apéndices) 19. [Índice alfabético de funciones y programas](#19-índice-alfabético-de-funciones-y-programas) --- ## 1. Resumen ejecutivo El programa controla **dos salas independientes de cultivo de champiñón** (Zona A y Zona B). Cada zona dispone de su propio conjunto de variables, sondas y actuadores; el código está duplicado de forma simétrica con sufijo `1` (Zona A) y `2` (Zona B). Cada zona regula simultáneamente **seis magnitudes**: | # | Magnitud | Variable proceso | Actuador | |---|----------|------------------|----------| | 1 | Temperatura del aire (TA) | `g_rTemperaturaAire1` | Baterías frío/calor, válvulas | | 2 | Temperatura del compost (TC) | `g_rTemperaturaCompos1` | Ventilación (indirecto) | | 3 | Humedad relativa (HR) | `g_iHumedad1` | Humectadores, deshumectación | | 4 | CO₂ | `g_iCO21` | Compuerta de aire exterior | | 5 | Ventilación (V) | `g_iVentilacion1` (% calculado) | Variador del ventilador | | 6 | Compuerta de aire exterior (C) | `g_iCompuerta1` (% calculado) | Servomotor compuerta | Adicionalmente gestiona **el riego programado** (mediante un grupo motorizado con sensores de inicio/final de carrera, contador de litros y velocidad regulable) y la **lectura de alarmas digitales** (fallos de ventilador, válvulas, riego). La estrategia de control no es un PID puro sobre cada variable, sino un **planificador por etapas (`g_iEtapa1`)** que decide en qué orden actúan los controles cuando hay conflicto (por ejemplo, deshumectar puede entrar en conflicto con CO₂ alto si se hace abriendo la compuerta). El operador selecciona una etapa según la fase del cultivo (incubación, fructificación, etc.) y el programa orquesta los lazos PID/proporcionales de forma coherente. --- ## 2. Síntesis conceptual: sistema experto de climatización adaptativa > Esta sección, **complementaria al resumen ejecutivo**, ofrece el modelo mental que conviene tener antes de leer el resto. Si el documento se va a usar como referencia para mantenimiento, leer esto primero acorta mucho la curva. El PLC implementa un **sistema experto de climatización adaptativo** específico para cultivo de champiñón. No es un controlador genérico de HVAC: tiene tres rasgos diseñados explícitamente para el negocio del champiñón: 1. **Receta multifase planificada por días/horas** (de relleno a recolección). 2. **Jerarquía energética** que prefiere métodos baratos (aire exterior) antes que costosos (compresor). 3. **Control prioritario por Etapas** que evita conflictos entre lazos por el actuador compartido (compuerta). ### 2.1. La consigna real del aire NO es la del operador Una decisión clave del sistema es que la consigna efectiva del lazo de temperatura del aire (`g_rTACalculada1`) se **recalcula en cada scan** en función del desvío del compost: ``` TACalc = TempAireCTR − (TempCompos − (TempCompoCTR + Margen)) × FCorr ``` Lectura: - Si el compost está **más caliente** que su consigna → **se baja la consigna del aire** para enfriar más rápido al compost. - Si el compost está **más frío** → se sube la consigna del aire. La consigna calculada se acota entre `Min` y `Max` para que la corrección nunca empuje fuera de los límites de seguridad. Esta es **la primera capa de inteligencia** del sistema: hace que el aire sirva al compost, no al revés. ### 2.2. Jerarquía energética de enfriamiento Cuando el sistema necesita enfriar, evalúa tres mecanismos en orden de menor a mayor consumo: ```mermaid flowchart LR A[¿Hay que enfriar?
TA > TACalc] --> B{¿Evaporativo viable?
Entalpia ext < 50 kJ/kg
y Text ≤ 40 °C
y Config = 0} B -- Sí --> EVAP[Activar EVAPORATIVO
· Bombear agua a panel
· Compuerta abierta] B -- No --> C{¿Freecooling viable?
Text + 1 ≤ TACTR
y Entalpia ext < Entalpia int
y Text ≤ TA} C -- Sí --> FREE[Activar FREECOOLING
· Solo abrir compuerta
· Sin compresor] C -- No --> BAT[Activar BATERIA FRIO
· PID sobre compresor
· Mayor consumo] EVAP -. tras 1 h si no funciona .-> BAT FREE -. si T no baja .-> BAT style EVAP fill:#0f766e,stroke:#000,color:#fff style FREE fill:#0ea5e9,stroke:#000,color:#fff style BAT fill:#c43c2a,stroke:#000,color:#fff ``` **Por qué este orden:** - **Evaporativo:** consume agua y un poco de electricidad del ventilador. Eficaz si el aire exterior es seco. La condición `EntalpiaExterior < 50 kJ/kg` garantiza que el aire exterior puede absorber agua sin saturarse. - **Freecooling:** solo electricidad del servomotor de la compuerta. Eficaz si el aire exterior es más frío Y de menor entalpía (no aporta humedad). - **Batería de frío (compresor):** último recurso. Es el más caro pero el único que funciona siempre. Los dos primeros tienen **fallback automático**: si tras 1 hora no consiguen bajar la temperatura, ceden el control a la batería de frío. ### 2.3. Jerarquía energética de deshumectación Análoga para humedad: ```mermaid flowchart LR A[¿Hay que deshumectar?
HR > HRctr] --> B{¿Aire ext más seco?
Habs ext < Habs int
y Text+1 ≤ TCompoCTR} B -- Sí --> EXT[FREECOOLING HR
Abrir compuerta] B -- No --> C[BATERIA FRIO
condensar agua] C --> D{¿T baja demasiado?} D -- Sí --> E[Activar BATERIA CALOR
recalentar el aire
seco impulsado] D -- No --> F[Continuar] style EXT fill:#0ea5e9,stroke:#000,color:#fff style C fill:#c43c2a,stroke:#000,color:#fff style E fill:#d97706,stroke:#000,color:#fff ``` **Detalle del refresco compensador (E):** cuando la batería de frío condensa agua, el aire de impulsión sale frío. Si la temperatura ambiente cae por debajo de la consigna, se activa la batería de calor (`ControlHumedadCalorZonaA`) en paralelo para recalentar el aire deshumectado. Es un mecanismo de **deshumectación isotermal**. ### 2.4. El actuador compartido y las Etapas La **compuerta** (`g_iCompuerta1`) es el actuador físicamente compartido por tres lazos potencialmente competidores: - **Lazo CO₂**: la abre para bajar CO₂ (renueva aire). - **Lazo HR (freecooling de humedad)**: la abre para deshumectar. - **Lazo Temperatura (freecooling)**: la abre para enfriar. Si los tres compiten en un mismo scan, hay conflicto. Las **Etapas (0–16)** definen la prioridad y los gating cruzados que evitan ese conflicto. Por eso una receta de champiñón en fase incubación usa una Etapa distinta a otra en fructificación: las prioridades son diferentes según la fase. ### 2.5. Diferencia operativa con un PID convencional | Aspecto | PID convencional | Este sistema | |---------|------------------|--------------| | Consigna | Fija | Recalculada cada scan según compost | | Modo de enfriar | Siempre compresor | Cascada evaporativo → freecool → compresor | | Conflictos entre lazos | No considera | Resueltos por Etapas y gating cruzado | | Recetas | Una | Hasta 15 fases encadenadas por fecha | | Bandas muertas | Únicas | Doble: estrecha (alcance) y ancha (acción) | | Rampas | No | Por consigna, por hora/minuto | | Decisiones psicrométricas | No | Entalpía/H. absoluta exterior vs. interior | | Seguridad | Sobre PV | Tope de impulsión + topes de temperatura exterior | Una vez asimilado este modelo, las 53 funciones del código se entienden como **implementación de estos cuatro principios** repetidos por magnitud y por zona. --- ## 3. Arquitectura general ### 2.1. Capas del software ```mermaid flowchart TB subgraph HMI["HMI / Pantalla / SCADA"] UI[Pantallas, botones, valores] end subgraph PLC["PLC Panasonic (Structured Text)"] direction TB subgraph PRG["Programas (orquestadores cíclicos)"] CALCT[CalculoTemperaturasZonaA/B
Planificador por Etapas] CALCE[CalculoEntalpiaZonaA/B
Lectura sondas + psicrometría] RIEGO[ControlRiegoZonaA/B
Máquina de estados de riego] FECHA[ControlFechaFinal*
Procesos temporales y secuenciales] UICOL[ColoresCirculosCentrales*
RelojyColoresChivatos*
Lógica visual HMI] MODB[Modbus
Master serie a I/O remotas] end subgraph FUN["Funciones (librería de control)"] ACT[ActivarProcesosZonaA/B] CTRL[ControlCO2 / ControlTemperatura /
ControlHumectar / ControlFreecooling*
ControlEvaporativo* / ControlVentilacion] WRAP["Wrappers X*ClimatiZona
(gating + dispatch)"] UTIL[TablaEntalpias, calcularFechaFinal,
FlancoAlto, LecturaDeAlarmas,
ContadorHumectacion, PulsoContador] end end subgraph IO["Hardware de campo (vía Modbus RTU)"] S1[Sondas PT100 / 4-20 mA
Aire, Compost, Humedad, CO2] S2[Sondas exteriores
T ext, HR ext, CO2 ext] A1[Variador ventilador] A2[Servomotor compuerta] A3[Baterías Frío/Calor] A4[Humectador / Caldera / Evaporimetro] A5[Grupo motorizado de riego] DI[Entradas digitales
alarmas, finales carrera] end UI <-->|registros| PLC PRG --> FUN PLC <-->|"COM1 - Modbus RTU
9600/19200 baud"| IO S1 --> IO S2 --> IO IO --> A1 & A2 & A3 & A4 & A5 DI --> IO ``` ### 2.2. División Zona A / Zona B El sistema replica la misma estructura para dos cámaras de cultivo. Una variable o función con sufijo `1` o `ZonaA` controla la sala A; con sufijo `2` o `ZonaB`, la sala B. La duplicación es **exacta a nivel de lógica**; solo cambian los nombres de variables. Esto es coherente con instalaciones de doble cámara donde se desea aislar climáticamente cada lote de cultivo sin que un cambio en una afecte al otro. ```mermaid flowchart LR subgraph A[Zona A] A_PRG[Programas con sufijo ZonaA] A_VAR[Variables con sufijo 1
g_rTemperaturaAire1, g_iCO21, ...] end subgraph B[Zona B] B_PRG[Programas con sufijo ZonaB] B_VAR[Variables con sufijo 2
g_rTemperaturaAire2, g_iCO22, ...] end subgraph SHARED[Variables compartidas] EXT[g_rTemperaturaExterior
g_iHumedadExterior
g_iCO2Exterior] RTC[g_iDTHora, g_iDTMinutos, ...
Reloj tiempo real] end A --> SHARED B --> SHARED ``` --- ## 4. Convenciones de nomenclatura de variables Todo el código sigue una convención **Hungarian Notation** (prefijo de tipo) combinada con un sufijo numérico que identifica la zona. ### 4.1. Prefijos de tipo | Prefijo | Tipo | Ejemplo | |---------|------|---------| | `b` | BOOL | `g_bValorVentilador1` | | `i` | INT | `g_iCO21`, `g_iVentilacion1` | | `r` | REAL | `g_rTemperaturaAire1` | | `s` | STRING | `g_sNombreProcesoFijo` | | `a` + tipo | ARRAY OF tipo | `g_arTemperaturaAireCTR1` (array of real), `g_aiCO2CTR1` (array of int), `g_asNombreProcesoFijo1` (array of string) | | `d` | DATE_AND_TIME | `dFechayHora` | | `Dut_` | DUT (struct) | `Dut_pid_Calor1` (PID_DUT_31) | ### 4.2. Prefijos de ámbito | Prefijo | Significado | |---------|-------------| | `g_` | Variable **GLOBAL** (declarada como `VAR_EXTERNAL` o `VAR_EXTERNAL RETAIN`). Toda variable global representa estado compartido entre programas y funciones. | | *(sin `g_`)* | Variable local del POU | | `sys_` | Variable del sistema (proporcionada por el runtime: `sys_bPulse1s`, `sys_bIsFirstScan`, etc.) | ### 4.3. Sufijos de zona | Sufijo | Significado | |--------|-------------| | `1` | Zona A | | `2` | Zona B | | *(ninguno)* | Variable compartida o transitoria de selección desde HMI | ### 4.4. Sufijos semánticos Para cada magnitud controlada existen normalmente **cuatro versiones** de la misma consigna, lo que permite gestionar simultáneamente una receta seleccionada, una receta en curso y un override temporal: | Sufijo | Significado | Ejemplo | |--------|-------------|---------| | `Maxima` | Límite superior absoluto (la consigna nunca podrá subir más) | `g_rTemperaturaAireMaxima1` | | `Minima` | Límite inferior absoluto | `g_rTemperaturaAireMinima1` | | `CTR` | **Consigna activa** (Control). Es la que efectivamente se aplica al lazo PID | `g_rTemperaturaAireCTR1` | | `EnCurso` | Copia de la consigna actualmente ejecutándose. Se utiliza para restaurar tras un proceso temporal | `g_rTACTREnCurso1` | | `T` | Versión "Temporal" — valor seleccionado en pantalla antes de aplicarse | `g_rTACTRT` | | sin sufijo | Versión "Seleccionada" en HMI antes de activarse | `g_rTACTR` | **Diagrama del ciclo de vida de una consigna** (ejemplo: temperatura aire): ```mermaid stateDiagram-v2 [*] --> Seleccionado: Operador edita en HMI
g_rTACTR Seleccionado --> Activo: ActivarProcesosZonaA()
copia → g_rTemperaturaAireCTR1 Activo --> EnCurso: copia → g_rTACTREnCurso1 EnCurso --> TemporalSel: Operador edita temporal
g_rTACTRT + g_iPasarTA=1 TemporalSel --> Activo: ActivarProcesosZonaA()
override → g_rTemperaturaAireCTR1 Activo --> Activo: Reaplicar / restaurar
desde g_rTACTREnCurso1 Activo --> [*]: Fin de proceso
(g_iEtapa1 = 0) ``` ### 4.5. Mapa rápido de prefijos de magnitud | Token | Magnitud | |-------|----------| | `TA` | Temperatura del **A**ire | | `TC` | Temperatura del **C**ompost (champiñonera) | | `HR` o `H` | **H**umedad **R**elativa | | `CO2` | CO₂ | | `V` | **V**entilación | | `C` (en contexto de consigna como `CMaxima`) | **C**ompuerta de aire exterior | | `Ramp*` | **Rampa** (variación máxima por hora) | ### 4.6. Flags `g_iPasar*` (override temporal) Cuando un operador define un **proceso temporal**, no necesariamente toca todas las consignas: por ejemplo, puede querer subir solo el CO₂ durante 4 horas sin tocar la humedad. Para soportar esto, el sistema usa flags por magnitud que indican qué campos del temporal sustituyen al activo: | Flag | Si vale 1, al activar temporal se sobrescribe... | |------|---------------------------------------------------| | `g_iPasarTA` | Consignas de temperatura de aire | | `g_iPasarHR` | Consignas de humedad | | `g_iPasarCO2` | Consignas de CO₂ | | `g_iPasarCompost` | Consignas de temperatura de compost | | `g_iPasarVentilacion` | Consignas de ventilación | | `g_iPasarCompuerta` | Consignas de apertura de compuerta | | `g_iPasarDeshumet` | Activación de deshumectación | | `g_iPasarHumectar` | Activación de humectación | | `g_iPasarRampaTA` | Rampa de temperatura | | `g_iPasarRampaHR` | Rampa de humedad | | `g_iPasarRampaCO2` | Rampa de CO₂ | Estos flags se leen exclusivamente en `ActivarProcesosZonaA/B()` cuando `g_iActivarProcesosTemporales1 = 1`. --- ## 5. Configuraciones físicas del sistema La variable global retentiva **`g_iConfiguracion1`** (y `g_iConfiguracion2`) define el equipamiento físico instalado en la zona. Determina qué controles tienen sentido y, por tanto, cuáles se ejecutan. | Valor | Equipamiento | Controles habilitados | |-------|--------------|------------------------| | `0` | Climatizador (baterías frío/calor) **+ Evaporativo** | Todos: baterías, evaporativo, freecooling, humectador, compuerta | | `1` | **Solo climatizador** (baterías frío/calor) | Baterías, freecooling, humectador, compuerta. Sin evaporativo | | `2` | **Solo evaporativo** (cooler + caldera) | Evaporativo, caldera, compuerta. Sin baterías ni freecooling | Los wrappers `X*ClimatiZona*` filtran su ejecución según esta variable: ```text if (g_rTemperaturaAireCTR1 > 0) AND (g_iConfiguracion1 = 0 OR g_iConfiguracion1 = 1) then [...control con baterías...] end_if; ``` Es decir, cuando `g_iConfiguracion = 2` (solo evaporativo) los wrappers de baterías no ejecutan nada y la lógica activa es `ControlSoloEvaporativoTemperaturaZonaA()` (invocada desde `CalculoTemperaturasZonaA` cuando la Etapa es 16). ### 5.1. Marcha de equipos (HMI feedback) | Variable | Sentido | Color HMI | |----------|---------|-----------| | `g_iMarchaFrio1` | Baterías de frío en marcha | Verde si 1, Rojo si 0 | | `g_iMarchaCalor1` | Baterías de calor en marcha | Verde si 1, Rojo si 0 | | `g_iMarchaEvaporativo1` | Evaporativo en marcha | Verde si 1, Rojo si 0 | | `g_iVisibleClimatizador1` | Mostrar widget climatizador en pantalla | 0/1 según `g_iConfiguracion1` | | `g_iVisibleEvaporimetro1` | Mostrar widget evaporativo en pantalla | 0/1 según `g_iConfiguracion1` | --- ## 6. Conceptos clave del proceso ### 6.1. Receta = Proceso Una **receta** (o "proceso" en la terminología del código) es un conjunto coherente de consignas y márgenes que definen el clima objetivo para una fase del cultivo. Por ejemplo: "Incubación día 7", "Inducción de fructificación", "Cosecha tercer flush". Un proceso contiene como mínimo: - Nombre (`g_sNombreProcesoFijo`, STRING[32]) - Consignas CTR / Max / Min para TA, TC, HR, CO₂, V, C - Rampas: `RampaTA`, `RampaHR`, `RampaCO2` - Etapa de prioridad (`g_iEtapa`, 0–16) Existen **tres tipos**: | Tipo | Identificador | Características | |------|---------------|-----------------| | **Fijo** | `g_iActivarProcesosFijos1 = 1` | Receta completa guardada. Al activarse, sobrescribe **todas** las consignas en curso | | **Temporal** | `g_iActivarProcesosTemporales1 = 1` | Override parcial. Solo modifica las magnitudes marcadas en los flags `g_iPasar*` | | **Secuencial** | `g_iSecuActivado1 = 1` | Encadenamiento de hasta **15 procesos fijos**, cada uno con duración fija (días/horas). Al expirar uno, salta al siguiente automáticamente | ### 6.2. Consignas, márgenes y bandas muertas Todo lazo de control trabaja con una banda muerta para evitar oscilaciones. La lógica para una variable genérica `X` es: ```text SI ABS(X_actual − X_CTR) ≤ X_margen ENTONCES Control en stand-by (no actúa) SINO Activar lazo (PID o lógica específica) FIN_SI ``` | Magnitud | Consigna activa | Margen retentivo | Banda muerta efectiva | |----------|-----------------|------------------|------------------------| | Temperatura aire | `g_rTemperaturaAireCTR1` | `g_rMargenTA1` | ±`g_rMargenTA1`, además de un fast-window de ±0.05 °C que considera "consigna alcanzada" | | Humedad relativa | `g_iHumedadCTR1` | `g_iMargenHR1` | ±`g_iMargenHR1`, fast-window ±0.1 % | | CO₂ | `g_iCO2CTR1` | `g_iMargenCO21` | ±`g_iMargenCO21`, fast-window ±7 ppm | | Temperatura compost | `g_rTemperaturaCompoCTR1` | `g_rMargenTC1` | ±`g_rMargenTC1` | ### 6.3. Rampas Para evitar saltos bruscos en los actuadores cuando se cambia una consigna grande, el sistema implementa **rampas temporizadas**: | Rampa | Variable | Período | |-------|----------|---------| | Rampa TA | `g_rRampaTA1` (°C) | Cada `g_iTiempoRampaTA1` minutos (si `=0`, cada hora) | | Rampa HR | `g_iRampaHR1` (%) | Cada `g_iTiempoRampaHR1` minutos | | Rampa CO₂ | `g_iRampaCO21` (ppm) | Cada `g_iTiempoRampaCO21` minutos | **Funcionamiento interno (ejemplo TA):** 1. Al cambiar la consigna o iniciar un proceso, el sistema captura la temperatura actual en `rTTopeHoraC1`. 2. El PID no usa `g_rTemperaturaAireCTR1` directamente, sino el "tope móvil" `rTTopeHoraC1`. 3. Cada vez que pasa el periodo de rampa, `rTTopeHoraC1` se incrementa (o decrementa) en `g_rRampaTA1` hacia `g_rTemperaturaAireCTR1`. 4. Al alcanzar la consigna, la rampa se autodesactiva (`g_rRampaTA1 := 0` y `rRampaAux1 := 0`). Esta arquitectura garantiza que la velocidad de cambio del clima sea suave, algo crítico para el cultivo de champiñón donde un choque térmico puede arruinar el flush. ### 6.4. Cálculo de la temperatura de aire compensada (`g_rTACalculada1`) El control de temperatura del aire **no usa directamente** `g_rTemperaturaAireCTR1`. En su lugar, calcula una consigna corregida que tiene en cuenta el calentamiento natural del compost: ```text g_rTACalculada1 := g_rTemperaturaAireCTR1 − ((g_rTemperaturaCompos1 − (g_rTemperaturaCompoCTR1 + g_rMargenTC1)) * g_rFCorrecion1); ``` Donde `g_rFCorrecion1` es el **factor de corrección** (parámetro de planta) que pondera cuánto influye la desviación del compost sobre el aire. - Si el compost está **más caliente** que su consigna + margen → la consigna de aire **baja** (enfriamos preventivamente) - Si el compost está **más frío** → la consigna de aire **sube** El valor se acota entre `g_rTemperaturaAireMinima1` y `g_rTemperaturaAireMaxima1` para que la corrección nunca empuje fuera de los límites de seguridad del proceso. ### 6.5. Detección manual / automático (`g_bAutoXX`) Cada actuador puede estar en modo automático o manual. El flag se llama `g_bAutoNN` donde NN identifica el actuador: | Flag | Actuador (Zona A) | Significado del valor | |------|--------------------|----------------------| | `g_bAuto11` | Frío (baterías) | 0=Auto, 1=Manual, 2=Parado | | `g_bAuto12` | Calor (baterías) | 0=Auto, 1=Manual, 2=Parado | | `g_bAuto13` | Ventilación | 0=Auto, 1=Manual | | `g_bAuto14` | Compuerta | 0=Auto, 1=Manual | | `g_bAuto15` | Humectador | 0=Auto, 1=Manual (parado) | Las funciones de control comprueban estos flags antes de escribir en el actuador: ```text if (g_bAuto11 = 0) then g_iFrio := 0; end_if; ``` > Nota: el código asume `g_bAuto11 = 0` (auto) para **poder actuar**. Si está en manual, el valor lo escribe la pantalla directamente y la función de control NO lo toca. --- ## 7. Modelo de Procesos y Etapas ### 7.1. La variable `g_iEtapa1` como planificador de prioridades `g_iEtapa1` (INT, retentiva) es **el corazón del sistema**. Define la estrategia de control activa: qué magnitudes se regulan, en qué orden, y cómo se resuelven los conflictos cuando varios controles compiten por el mismo actuador (típicamente la compuerta de aire exterior). **Lógica de orquestación dentro de `CalculoTemperaturasZonaA`:** ```mermaid flowchart TD INI[Inicio scan] SIM[WSimuladorPID] ACT[ActivarProcesosZonaA
aplica receta fija/temporal/secuencial] DEC{g_iEtapa1 = ?} INI --> SIM --> ACT --> DEC DEC -->|0| E0[Sala vacía
Todo parado] DEC -->|1| E1[TEMPERATURA] DEC -->|2| E2[HUMEDAD RELATIVA] DEC -->|3| E3[CO2] DEC -->|4| E4[TEMPERATURA + CO2] DEC -->|5| E5[TEMPERATURA + HUMEDAD] DEC -->|6| E6[CO2 → TEMPERATURA] DEC -->|7| E7[CO2 → HUMEDAD] DEC -->|8| E8[HUMEDAD → TEMPERATURA] DEC -->|9| E9[HUMEDAD → CO2] DEC -->|10| E10[TEMP + CO2 + HUMEDAD] DEC -->|11| E11[TEMP + HUMEDAD + CO2] DEC -->|12| E12[CO2 → HUMEDAD → TEMP] DEC -->|13| E13[CO2 → TEMP → HUMEDAD] DEC -->|14| E14[HUMEDAD → TEMP → CO2] DEC -->|15| E15[HUMEDAD → CO2 → TEMP] DEC -->|16| E16[SOLO EVAPORATIVO] style E0 fill:#444,stroke:#000,color:#fff style E16 fill:#777,stroke:#000,color:#fff ``` > **Convención de notación de etapas:** > - `A + B` significa que ambos controles se ejecutan, en paralelo, sin condicionarse. > - `A → B` significa que B **solo se ejecuta si A no está activamente actuando** (es la forma de resolver conflictos por la compuerta). ### 7.2. Tabla detallada de Etapas | Etapa | `sPrioridades1` | Controles que ejecuta | Gating | |-------|------------------|-----------------------|--------| | **0** | `CONTROLES PARADOS` | Solo `g_iVentilacion1` se fuerza a 0 (en auto). Todos los actuadores en mínimos | "Sala vacía", sistema desactivado | | **1** | `TEMPERATURA` | `XEvapoTemperaturaClimati` → `XFreecoolingTemperaturaClimati` → `XTemperaturaClimati` | Sin gating, los tres siempre | | **2** | `HUMEDAD RELATIVA` | `XDesHumectarAireExterior` → `XDesHumectarBateriaFrio` → `XDesHumectarBateriaCalor` → `XHumectar` | Sin gating | | **3** | `CO2` | `XCO2Climati` | Solo CO₂ | | **4** | `TEMPERATURA - CO2` | Temp en bloque + `XHumectar`. `XCO2Climati` **solo si** evaporativo y freecooling no están activos | TempActivo → bloquea CO₂ | | **5** | `TEMPERATURA - HUMEDAD` | Temp en bloque + `XHumectar`. Cada deshumect. **solo si** la equivalencia de temp no está activa | TempActivo → bloquea su deshumect. | | **6** | `CO2 → Temperatura` | `XCO2Climati`. Temp **solo si** CO₂ no actuó | CO₂Activo → bloquea Temp | | **7** | `CO2 → HUMEDAD` | `XCO2Climati`. Deshum. aire ext **solo si** CO₂ no actuó. Resto deshumect. + humect siempre | CO₂Activo → bloquea deshum.aire-ext | | **8** | `HUMEDAD → TEMPERATURA` | Todos los Hum. Temp **solo si** Hum aire-ext, bate-frío y bate-calor no actuaron | HumActivo → bloquea Temp | | **9** | `HUMEDAD → CO2` | Todos los Hum. CO₂ **solo si** Hum aire-ext no actuó | HumActivo → bloquea CO₂ | | **10** | `TEMP - CO2 - HUMEDAD` | Temp + bloque Hum siempre. CO₂ y Deshum.aire-ext condicionales en cascada | Cascada Temp → CO₂ → Hum | | **11** | `TEMP - HUMEDAD - CO2` | Temp + bloque Hum siempre. CO₂ requiere que ni Evapo, ni Freecool, ni Hum.aire-ext actúen | Cascada Temp → Hum → CO₂ | | **12** | `CO2 - HUMEDAD - TEMP` | CO₂ + bloque Hum (con gating CO₂). Temp solo si Hum.aire-ext y CO₂ ya están parados | Cascada CO₂ → Hum → Temp | | **13** | `CO2 - TEMP - HUMEDAD` | CO₂. Temp **solo si** CO₂ no actuó. Hum.aire-ext si todo lo anterior está parado | Cascada CO₂ → Temp → Hum | | **14** | `HUMEDAD - TEMP - CO2` | Bloque Hum + Humectar siempre. Temp solo si Hum aire-ext está parado. CO₂ solo si nada de temp ni Hum aire-ext actúa | Cascada Hum → Temp → CO₂ | | **15** | `HUMEDAD - CO2 - TEMP` | Bloque Hum siempre. CO₂ si Hum aire-ext parado. Temp si Hum aire-ext y CO₂ parados | Cascada Hum → CO₂ → Temp | | **16** | `SOLO EVAPORATIVO` | `ControlSoloEvaporativoTemperaturaZonaA` directo (sin PID) | Requiere `g_iConfiguracion1 = 2` | ### 7.3. Etapas durante el riego Cuando `g_iRegando1 = 1`, **se fuerza `g_iEtapa1 := 4`** independientemente de lo programado, y se activa el simulador PID (`g_iActivarSimulador := 1`). Al finalizar el riego se restaura la etapa original desde `g_iEtapaEnCurso1`. Esto se hace porque el riego perturba significativamente humedad, temperatura del compost y CO₂, y necesita un modo de control específico que regule conjuntamente TA y CO₂. ```text if (g_iRegando1 = 1) then g_iEtapa1 := 4; g_iActivarSimulador := 1; else if (g_iActivarSimulador = 1) then g_iActivarSimulador := 0; g_iEtapa1 := g_iEtapaEnCurso1; end_if; end_if; ``` ### 7.4. Banderas de influencia (`g_iInfluencia*`) Permiten al operador deshabilitar específicamente la **influencia** de una magnitud sobre los actuadores compartidos, sin desactivar la lectura ni el cálculo de la consigna: | Variable | Si vale 0... | |----------|--------------| | `g_iInfluenciaTA1` | El freecooling no actuará por temperatura | | `g_iInfluenciaHR1` | La deshumectación por aire exterior no actuará | | `g_iInfluenciaCO21` | El control de CO₂ no actuará sobre la compuerta | | `g_iInfluenciaFRIO1` | Se inhibe el frío cuando hay activación de humedad | --- ## 8. Ciclo de scan del PLC El PLC ejecuta cíclicamente todos los `PROGRAM`. El orden de ejecución dentro del scan no está explicitado en el código (depende de la tabla de tareas en FPWIN Pro), pero las dependencias funcionales **exigen** la siguiente secuencia lógica para que las variables estén actualizadas en cada paso: ```mermaid flowchart TD A[Inicio scan] A --> M[Modbus
· Lee sondas e inputs
· Escribe consignas de actuadores] M --> CE[CalculoEntalpiaZonaA/B
· Promedia sondas válidas
· Calcula H absoluta, H saturación, entalpía
· Calcula TACalculada] CE --> CT[CalculoTemperaturasZonaA/B
· ActivarProcesosZonaA
· Despacha lazos según Etapa] CT --> FF[ControlFechaFinalTemporales*
ControlFechaFinalSecuenciales*
· Comprueba fin de proceso
· Encadena el siguiente] FF --> R[ControlRiegoZonaA/B
· Programación / ejecución riegos] R --> UI[ColoresCirculosCentrales*
RelojyColoresChivatos*
· Calcula colores HMI
· Actualiza reloj] UI --> Z[Fin scan] Z -.->|nuevo ciclo| A ``` ### 8.1. Punteros temporales del sistema | Variable de sistema | Tipo | Sentido | |---------------------|------|---------| | `sys_bPulse1s` | BOOL | Tren de pulsos de 1 s (50% duty) | | `sys_bPulse2s` | BOOL | Tren de pulsos de 2 s | | `sys_bPulse1min` | BOOL | Tren de pulsos de 1 min | | `sys_bPulse10ms` | BOOL | Tren de pulsos de 10 ms | | `sys_bIsFirstScan` | BOOL | TRUE solo en el primer scan tras arranque | | `sys_bIsNotFirstScan` | BOOL | Negación del anterior | | `sys_bIsComPort1MasterCommunicationActive` | BOOL | Hay comunicación Modbus en marcha | | `sys_bIsComPort1CommunicationError` | BOOL | Error de comunicación en el último ciclo | Las funciones de control usan estos pulsos para implementar timers software sin recurrir a temporizadores hardware (que serían limitados). ### 8.2. Lectura del reloj tiempo real Casi todas las funciones que necesitan tiempo extraen la hora con el patrón: ```text dFechayHora := GET_RTC_DT(); SPLIT_DT_INT(IN := dFechayHora, YEAR => iA, MONTH => iM, DAY => iD, HOUR => iH, MINUTE => iMi, SECOND => g_iDTSegundos, MILLISECOND => g_iDTMilisegundos); ``` Las variables `g_iDTSegundos`, `g_iDTMilisegundos` quedan disponibles globalmente y se reutilizan en muchos cálculos. `RelojyColoresChivatosZonaA` permite también **escribir** la hora del PLC vía `SET_RTC_DT()` cuando `g_iCambiarHora = 1`, validando previamente que el día sea correcto para el mes y aplicando lógica de año bisiesto. --- ## 9. Referencia de Programas > Cada programa ejecuta cíclicamente. Aquí se documenta su **propósito**, las **entradas globales** críticas que lee, las **salidas globales** que escribe, y las **funciones que invoca**. Salvo indicación contraria, todos están duplicados para Zona B con sufijo `2`. ### 9.1. `CalculoEntalpiaZonaA` **Propósito.** Es la **puerta de entrada de datos** al sistema de control. Se encarga de: 1. Promediar las lecturas de las sondas que el operador ha marcado como activas. 2. Detectar sondas fallidas y colorearlas en HMI (rojo/verde). 3. Convertir las lecturas crudas (Modbus) en variables de proceso (`g_rTemperaturaAire1`, `g_iHumedad1`, …). 4. Calcular humedad absoluta interior y exterior usando la `TablaEntalpias`. 5. Calcular entalpía interior y exterior. 6. Calcular la consigna corregida de aire (`g_rTACalculada1`). 7. Actualizar los colores HMI de los flags `g_iPasar*` (verde/rojo). **Entradas principales.** | Variable | Tipo | Origen | |----------|------|--------| | `g_rTemperaturaAireaux11`, `aux12` | REAL | Sondas PT100 de aire ambiente (Modbus) | | `g_rTemperaturaAireHumeda11`, `aux12` | REAL | Sondas PT100 con bulbo húmedo | | `g_rTemperaturaComposAux11..14` | REAL | Hasta 4 sondas de compost | | `g_iHumedadAmbiente11`, `12` | REAL | Sondas HR | | `g_iCO2Ambiente11`, `12` | REAL | Sondas CO₂ | | `g_iLeerSonsa*1` | INT | Habilitación de cada sonda (1=usar) | | `g_iConectarSondas1` | INT | Master enable lectura sondas | | `g_iHumedaOAmbiente1` | INT | 1=HR por psicrómetro de bulbo húmedo, 0=por sonda HR directa | **Salidas principales.** | Variable | Tipo | Sentido | |----------|------|---------| | `g_rTemperaturaAire1` | REAL | Media de sondas válidas | | `g_rTemperaturaCompos1` | REAL | Media de sondas de compost | | `g_iHumedad1` | REAL | HR efectiva | | `g_iCO21` | INT | CO₂ ambiente | | `g_rEntalpiaInte1`, `g_rEntalpiaExte1` | REAL | Entalpía interior/exterior (kJ/kg) | | `rHAbsolutaInte1`, `rHAbsolutaExte1` | REAL | Humedad absoluta interior/exterior (kg/kg) | | `g_rLimiteHumeAdsoluta1` | REAL | Margen hasta saturación | | `g_rTACalculada1` | REAL | Consigna de TA corregida por desviación del compost | | `g_sColorSonda*1` | STRING | Color HMI por sonda | | `g_wMandarTemperaturaEXT`, `g_wMandarHumedadEXT`, `g_wMandarCO2EXT` | WORD | Replicación de sondas exteriores por EtherNet/IP | **Lógica de promediado de sondas:** ```text contador := 0 suma := 0 si sonda1_habilitada entonces contador++; suma += sonda1 color_sonda1 := verde sino color_sonda1 := rojo fin_si // repetir para cada sonda... si contador > 0 entonces media := suma / contador fin_si ``` Esto significa que **el sistema tolera el fallo de N–1 sondas** y mantiene control con las que sobreviven (siempre que el operador no las haya marcado como activas manualmente las defectuosas). --- ### 9.2. `CalculoTemperaturasZonaA` **Propósito.** Núcleo de la orquestación: aplica el proceso seleccionado (fijo/temporal/secuencial) y dispara los lazos de control según la Etapa. **Llamadas internas:** ```mermaid graph TD A[CalculoTemperaturasZonaA] --> B[WSimuladorPID] A --> C[ActivarProcesosZonaA] A --> D{g_iEtapa1} D --> V[ControlVentilacionZonaA
siempre que Etapa ≠ 0] D --> XE[XEvapoTemperaturaClimatiZonaA] D --> XF[XFreecoolingTemperaturaClimatiZonaA] D --> XT[XTemperaturaClimatiZonaA] D --> XC[XCO2ClimatiZonaA] D --> XH[XHumectarClimatiZonaA] D --> XDA[XDesHumectarAireExteriorClimatiZonaA] D --> XDF[XDesHumectarBateriaFrioClimatiZonaA] D --> XDC[XDesHumectarBateriaCalorClimatiZonaA] D --> SE[ControlSoloEvaporativoTemperaturaZonaA
solo si Etapa=16 y Config=2] ``` **Reglas duras independientes de Etapa:** ```text // Saturación de compuerta if (g_iCompuerta1 < g_iCompuertaMinima1) AND (g_bAuto14 = 0) then g_iCompuerta1 := g_iCompuertaMinima1; end_if; if (g_iCompuerta1 > g_iCompuertaMaxima1) AND (g_bAuto14 = 0) then g_iCompuerta1 := g_iCompuertaMaxima1; end_if; // Si la ventilación está parada, no hay humedad ni se influencia if (g_iInfluenciaFRIO1 = 0) AND (g_iActivarClimaHumedad1 ≠ 0) then g_iFrio := 0; g_iActivarClimaHumedad1 := 0; end_if; // Humectador en manual if (g_bAuto15 = 1) then g_bValorHumectar1 := FALSE; g_iHumectar1 := 0; end_if; ``` Al final también actualiza los colores HMI de los pilotos: - `g_sColorFrioClima` (verde si `g_iMarchaFrio1=1`, rojo si 0) - `g_sColorCalorClima` (idem para `g_iMarchaCalor1`) - `g_sColorEvaporativo` (idem para `g_iMarchaEvaporativo1`) --- ### 9.3. `ControlRiegoZonaA` **Propósito.** Implementa la **máquina de estados** del riego programado. Gestiona simultáneamente: - Programación de hasta **200 puntos de riego** (arrays `g_aiProgramaRiegoAno1` a `g_aiProgramaRiegoMinutos1`) - Ejecución manual o automática - Motor de la rampa de riego (bidireccional, con finales de carrera) - Conteo de litros consumidos por pulsos - Cálculo de velocidad para que el riego dure lo programado **Variables principales.** | Variable | Tipo | Sentido | |----------|------|---------| | `g_iRegando1` | INT | 1 si hay un riego en ejecución | | `g_iPreparativosRiego1` | INT | 1 si está en fase de preparativos (rampa hacia inicio carrera) | | `g_iInicioCarrera1`, `g_iFinalCarrera1` | INT | 1 si el motor llegó al final mecánico | | `g_iSentidoMotor1` | INT | 0=adelante, 1=atrás | | `g_iAbrirGrifoSuelo1`, `g_iAbrirGrifoAmbiente1` | INT | Activación de electroválvulas | | `g_iAbrirGrifoSueloCa1` | INT | Grifo suelo caliente | | `g_rLitrosConsumidos1` | REAL | Litros totales del riego en curso | | `g_rLitrosConsumidosTramo1` | REAL | Litros por tramo | | `g_iNumeroTramo1` | INT | Tramo actual | | `g_rContadorAgua1` | REAL | Pulsos del caudalímetro | | `g_iCantidadRiegos1` | INT | Cuántos riegos quedan en la programación periódica | | `g_iVelocidadRiego1` | INT | Velocidad enviada al variador del motor | **Recetas de riego (parámetros configurables por punto):** | Variable | Sentido | |----------|---------| | `g_sOperacionRiego1` | `.RIEGO AMBIENTE.` / `.RIEGO SUELO.` / `.RIEGO SUELO CALIENTE.` | | `g_sModoRiego1` | `.AUTOMATICO.` / `.MANUAL.` | | `g_iLitrosRiego1` | Litros a aplicar | | `g_iTramosRiego1` | Número de tramos del riego | | `g_iVelocidadRiego1` | Velocidad de la rampa | | `g_rCadaXMinutos1` | Periodicidad cuando es repetitivo | **Flujo cuando se graba un riego en programación:** ```mermaid sequenceDiagram actor Op as Operador HMI participant Prog as ControlRiegoZonaA participant Func as ControlRiegoLineaDeTexto/AsignarArray Op->>Prog: g_iGrabarRiego1 := 1
(o 2 con repetición, 3 manual) Prog->>Prog: Espera g_iSemaforoRiego1 = 1 Prog->>Prog: g_sEstadoRiego1 := 'PEN' Prog->>Func: ControlRiegoLineaDeTextoZonaA
(formatea g_sNombreLineaRiego1) Prog->>Prog: g_iContadorIndiceRiego1++ Prog->>Func: ControlRiegoAsignarArrayZonaA
(graba en arrays con índice) Note over Prog: Si modo=2 (repetir):
calcula siguiente fecha
con ADD_DT_TIME y la añade Prog->>Prog: g_iGrabarRiego1 := 0
g_iSemaforoRiego1 := 0 ``` **Códigos de color en gota de riego:** | Estado | `g_sColorGota1` | Cuando | |--------|-----------------|--------| | Regando, parpadeo lento | `#0000FF` / `#00FFEF` | `sys_bPulse1s` alterna | | Parado | `#FF0000` (rojo) | Sin riego | --- ### 9.4. `ControlFechaFinalTemporalesZonaA` **Propósito.** Gestiona la **fecha de finalización** de los procesos temporales. **Comportamiento:** 1. Si `g_iFunActivado = 1` (operador acaba de definir un temporal de N horas/días), calcula la fecha de fin llamando a `calcularFechaFinal()` y la guarda en `g_iFechaFinAno1..Minutos1`. 2. En cada scan, compara la fecha actual con la fecha de fin. Si se ha alcanzado: - Restaura todas las consignas `EnCurso` (TA, TC, HR, CO₂, V, C) sobre las consignas activas. - Limpia las rampas y los flags auxiliares. 3. Si `g_iAbortarProcesoTemporal1 = 1`, el operador puede cancelar el temporal manualmente y el comportamiento es igual al expirar el tiempo. 4. Si `g_iSecuActivado1 = 1` al expirar, **no** restaura: deja el campo libre para que `ControlFechaFinalSecuencialesZonaA` cargue la siguiente etapa. --- ### 9.5. `ControlFechaFinalSecuencialesZonaA` **Propósito.** Gestiona **secuencias encadenadas de hasta 15 procesos fijos**. Cuando un proceso de la secuencia termina (por fecha), carga automáticamente todas las consignas del siguiente. **Arrays de la secuencia (índice 1..15):** | Array | Tipo | Contenido | |-------|------|-----------| | `g_asNombreProcesoFijo1` | ARRAY[1..15] OF STRING[32] | Nombre del proceso | | `g_arTemperaturaAireCTR1`, `Maxima`, `Minima` | REAL | Consignas TA | | `g_arTemperaturaCompoCTR1`, `Maxima`, `Minima` | REAL | Consignas TC | | `g_aiHumedadCTR1`, `Maxima`, `Minima` | REAL | Consignas HR | | `g_aiCO2CTR1`, `Maxima`, `Minima` | INT | Consignas CO₂ | | `g_aiVentilacionCTR1`, `Maxima`, `Minima` | INT | Consignas V | | `g_aiCompuertaCTR1`, `Maxima`, `Minima` | INT | Consignas C | | `g_arRampaTemperatura1`, `g_arRampahumedad1`, `g_aiRampaCO21` | REAL/INT | Rampas | | `g_aiDiasProceso1`, `g_aiHorasProceso1` | INT | Duración | | `g_aiEtapa1` | INT | Etapa de prioridad | **Variables de control de la secuencia:** | Variable | Sentido | |----------|---------| | `g_iSecuActivado1` | 1 si hay secuencia activa | | `g_iSecuFunActivado` | 1 si toca avanzar al siguiente paso | | `g_iIndice` | Índice del paso actual (1..15) | | `g_iAbortarSecuencial1` | 1 para abortar la secuencia | **Avance entre pasos:** ```mermaid flowchart TD A[Fin de scan] --> B{¿Llegó fecha fin?} B -->|No| Z[Continuar] B -->|Sí| C{g_iSecuActivado1 = 1?} C -->|No| Z C -->|Sí| D[g_iSecuFunActivado := 1] D --> E[Próximo scan] E --> F{índice ≤ 15?} F -->|No| G[Fin secuencia
Reset fechas] F -->|Sí| H{Días/Horas > 0?} H -->|No| G H -->|Sí| I[Cargar arrays
al índice] I --> J[calcularFechaFinal] J --> K[Guardar fecha fin nueva] K --> Z ``` --- ### 9.6. `ColoresCirculosCentralesZonaA` **Propósito.** Calcula los colores de los círculos centrales del sinóptico HMI y dispara las alarmas por exceso. **Lógica del color para cada magnitud (ej. temperatura aire):** ```text si TemperaturaAire1 > AireMaxima1 + AlarmaTA1 entonces color := negro (alarma) MailAlarma1 := 1 sino si TemperaturaAire1 > AireMaxima1 entonces color := naranja sino si TemperaturaAire1 > AireCTR1 + MargenTA entonces color := azul-claro sino si TemperaturaAire1 < AireMinima1 entonces color := rojo sino color := verde fin_si ``` Las **alarmas por desviación** (`g_rAlarmaTA1`, `g_rAlarmaTC1`, `g_rAlarmaHR1`, `g_iAlarmaCO21`) son umbrales adicionales que cuando se superan disparan `g_bMandarAlarma := TRUE` y `g_iMailAlarma1 := 1` para enviar notificaciones por email. También aplica saneamiento de consignas (las pinza entre min/max para que un valor mal introducido nunca rompa el control). --- ### 9.7. `RelojyColoresChivatosZonaA` **Propósito.** Tareas misceláneas de HMI: 1. **Cálculo de velocidades de animación** (`g_iVelocidad1`, `g_iVelociFrio1`, `g_iVelociCalor1`, etc.) — los círculos del HMI giran a una velocidad proporcional al % de actuación. 2. **Mantenimiento del reloj**: cambio de hora desde HMI (`g_iCambiarHora`) y carga de hora actual (`g_iCargarHora`). 3. **Cálculo del día de la semana** (`g_sDiaSemanaLetra`: D/L/M/Mx/J/V/S). 4. **Composición del nombre del cultivo actual**: `fecha-zona-ciclo` (`g_sNombreUltimoCultivo1`). 5. **Asignación de colores a los chivatos de manual/automático**: | Estado `g_bAutoXX` | Color piloto | |--------------------|--------------| | 0 (Auto) | Verde `#00FF00` | | 1 (Manual) | Variable: azul oscuro (frío), rojo (calor), rojo (ventilación)... | | 2 (Parado) | Negro `#000000` | --- ### 9.8. `Modbus` **Propósito.** Maestro Modbus RTU sobre `SYS_COM1_PORT`, esclavo único con `SlaveAddress = 1`. Ver sección [13. Comunicaciones Modbus](#13-comunicaciones-modbus) para detalle de mapa de registros. --- ### 9.9. `ModbusPrueba1` Programa **auxiliar de pruebas**, no productivo. No se documenta más allá de mencionarlo. --- ## 10. Referencia de Funciones de control Las funciones (FUNCTION) son la **librería de control** que llaman los programas. Conceptualmente se agrupan en cuatro grupos: | Grupo | Prefijo | Misión | |-------|---------|--------| | **Wrappers de despacho** | `X*ClimatiZona*` | Gating por configuración y por estado de otros lazos. Llaman a la función `Control*` real solo si procede | | **Lazos de control** | `Control*` | Lógica real: PID, rampas, bandas muertas, escritura de actuadores | | **Activación de procesos** | `ActivarProcesos*`, `controlarFechaFinal` | Carga de recetas, cálculo de fechas | | **Utilidades** | `TablaEntalpias`, `FlancoAlto`, `LecturaDeAlarmas*`, `ContadorHumectacion*`, `PulsoContador*`, `W*` | Funciones puras o de soporte | > Las versiones para Zona B (sufijo `B` en el nombre, sufijo `2` en variables) son **idénticas en estructura y lógica** a las de Zona A; en lo sucesivo solo se documenta la versión A. --- ### 10.1. Wrappers `X*ClimatiZonaA` Cada wrapper sigue el patrón: ```text FUNCTION X[Nombre]ClimatiZonaA: VOID si (consigna_de_la_magnitud > 0) AND (configuración_compatible) AND (condiciones_de_freecooling/evaporativo/etc) AND (banderas_de_influencia) entonces llamar a Control[Nombre]ZonaA() sino anular salidas asociadas actualizar mensaje de acción fin_si END_FUNCTION ``` #### `XTemperaturaClimatiZonaA` | Condición para ejecutar `ControlTemperaturaZonaA` | |----------------------------------------------------| | `g_rTemperaturaAireCTR1 > 0` | | `g_iConfiguracion1 ∈ {0, 1}` | | `g_iActivadoEvapo1 = 0` (evaporativo no activo) | | `g_iActivadoClimat ∈ {1, 2}` | Si no se cumplen, anula `g_iTempeBateFrio1`, `g_iTempeBateCalor1` y limpia `g_sAccionCT1 := 'CLIMA-1'`. #### `XEvapoTemperaturaClimatiZonaA` | Condición para ejecutar `ControlEvaporativoTemperaturaZonaA` | |---------------------------------------------------------------| | `g_rTemperaturaAireCTR1 > 0` | | `g_iConfiguracion1 ∈ {0, 1}` | | `g_iActivadoEvapo1 = 1` | | `g_iConfiguracion1 = 0` (climatizador + evaporativo) | | `g_rEntalpiaExte1 < 50` kJ/kg | | `g_rTemperaturaExterior ≤ 40 °C` | Es decir: **el evaporativo solo entra si el aire exterior no aporta demasiado calor/humedad**. #### `XFreecoolingTemperaturaClimatiZonaA` | Condición para ejecutar `ControlFreecoolingTemperaturaZonaA` | |---------------------------------------------------------------| | `g_rTemperaturaAireCTR1 > 0` | | `g_iConfiguracion1 ∈ {0, 1}` | | `g_iActivadoEvapo1 = 0` | | `g_rTemperaturaAire1 ≥ g_rTACalculada1` (hace falta enfriar) | | `g_rTemperaturaExterior + 1 ≤ g_rTemperaturaAireCTR1` (exterior aporta frío) | | `g_rEntalpiaExte1 < g_rEntalpiaInte1` (exterior aporta menos energía latente) | | `g_rTemperaturaExterior ≤ g_rTemperaturaAire1` | | `g_iInfluenciaTA1 = 1` | Si no se cumple, además de no actuar, fuerza la compuerta al mínimo cuando no hay otras influencias activas (CO₂, HR). #### `XCO2ClimatiZonaA` | Condición | |-----------| | `g_iCO2CTR1 > 0` | | `g_iConfiguracion1 ∈ {0, 1}` | | `g_iInfluenciaCO21 = 1` | #### `XHumectarClimatiZonaA` | Condición | |-----------| | `g_bAuto15 = 0` (humectador en auto) | | `g_iCompuertaCTR1 = 1` (proceso permite humectar) | Si no se cumple, fuerza `g_iHumectar1 := 0` y `g_bValorHumectar1 := FALSE`. #### `XDesHumectarAireExteriorClimatiZonaA` Llama a `ControlFreecoolingHumedadZonaA` si: | Condición | |-----------| | `g_iHumedadCTR1 > 0` | | `g_iConfiguracion1 ∈ {0, 1}` | | `g_rTemperaturaExterior + 1 ≤ g_rTemperaturaCompoCTR1` | | `rHAbsolutaExte1 < rHAbsolutaInte1` (aire exterior más seco en absoluto) | | `g_iInfluenciaHR1 = 1` | Es decir, se deshumecta con aire exterior **solo si el aire exterior aporta menos humedad absoluta y no calienta el compost**. #### `XDesHumectarBateriaFrioClimatiZonaA` / `XDesHumectarBateriaCalorClimatiZonaA` Llaman a `ControlHumedadFrioZonaA` / `ControlHumedadCalorZonaA`, que deshumectan por condensación en la batería de frío o evaporando con calor. --- ### 10.2. Funciones `Control*` #### 10.2.1. `ControlTemperaturaZonaA` **Lazo PID de temperatura del aire con baterías frío/calor.** **Algoritmo:** ```mermaid flowchart TD A[Calcular TACalculada = TACTR − corrección por TC] A --> B{¿Dentro de banda muerta ±0.05 °C?} B -->|Sí| C[Anular Frio y Calor en auto
Reset rampa
Acción = STANDBY] B -->|No| D[Aplicar rampa
actualizar rTTopeHoraC1] D --> E{TemperaturaAire < rTTopeHoraC1?} E -->|Sí| F[CALENTAR
PID inverso
g_iCalor := MV] E -->|No| G[ENFRIAR
PID directo
g_iFrio := MV] F --> H[Limitar por temperatura impulsión
TopeCalorImpulsion] G --> I[Limitar por temperatura impulsión
TopeFrioImpulsion] H --> Z[Fin] I --> Z ``` **Bloque PID por modo.** Hay dos instancias separadas: | Instancia | Modo | Palabra de control inicial | Cambia a | |-----------|------|----------------------------|----------| | `Dut_pid_Calor1` | Auto-tuning | `16#8000` | `16#0000` (PI-D inverso) cuando `AT_Progress ≥ 5` | | `Dut_pid_Frio1` | Auto-tuning | `16#8001` | `16#0003` (I-PD directo) cuando `AT_Progress ≥ 5` | **Estructura del PID (`PID_DUT_31` de Panasonic):** - `pv`: variable de proceso (temperatura aire escalada a 0..10000) - `SP`: set point (rTTopeHoraC1 escalado a 0..10000) - `Control`: palabra de modo - `UpperLimit`: 100 (% de actuación) - `Kp`, `Ti`, `Td`, `Ts`: parámetros tuning (defaults: Kp=10, Ti=1, Td=1, Ts=10) - `MV`: salida (0–100 %) **Función PID ejecutada:** `F355_PID_DUT(s := Dut_pid_Calor1)` (o Frio). **Limitación por temperatura de impulsión.** Si la temperatura de impulsión del clima (`g_rTempImpulsion1`) se acerca a `g_rTopeCalorImpulsion` (por defecto 40 °C), el calor se reduce proporcionalmente: ```text g_iCalor := TO_INT((TopeCalorImpulsion − TempImpulsion) * (Calor_anterior / MargenTopeCalorImpulsion)); ``` Análogo para frío con `g_rTopeFrioImpulsion` (por defecto 5 °C). #### 10.2.2. `ControlCO2ZonaA` **Lazo PID de CO₂ actuando sobre la compuerta de aire exterior.** Estructura igual a `ControlTemperaturaZonaA` pero más simple: - **Banda muerta**: ±7 ppm en torno a `g_iCO2CTR1`. - **Rampa**: `g_iRampaCO21` (ppm/periodo). - **PID único**: `Dut_pid_CO21` con palabra de control `16#0001`/`16#0003`. - **Salida**: `g_iCompuerta1 := Dut_pid_CO21.MV` (limitado entre `g_iCompuertaMinima1` y `g_iCompuertaMaxima1`). **Compatibilidad con frío exterior** (`g_iTemporizador0Grados`): cuando la temperatura exterior es ≤ 0 °C, la apertura máxima de la compuerta se limita a `g_iTemporizador0Grados` (por defecto 50 %). #### 10.2.3. `ControlHumectarZonaA` **Lazo PID para humectar el aire.** - Activa la bomba de niebla / humectador cuando `g_iHumedad1 < g_iHumedadCTR1 − iHRMiMargen1`. - Banda muerta: ±0.1 % en torno a la consigna. - PID: `Dut_pid_Humectar1`. #### 10.2.4. `ControlFreecoolingHumedadZonaA` **Deshumectación abriendo compuerta de aire exterior cuando este es más seco.** - Sale de `XDesHumectarAireExteriorClimatiZonaA` (con condiciones psicrométricas ya verificadas). - Lleva un **contador propio** (`g_iContadorMinutosHRAire1`) que se incrementa cada minuto mientras `g_iHumedadAireExterior1 = 1` para limitar el tiempo continuo de deshumectación por aire exterior y evitar enfriar en exceso. #### 10.2.5. `ControlHumedadFrioZonaA` **Deshumectación por condensación en la batería de frío.** Cuando la HR es alta y el aire interior está cerca de saturación (`g_rLimiteHumeAdsoluta1`), activa la batería de frío para condensar agua y bajar la HR. Usa la consigna de temperatura de aire menos un offset. #### 10.2.6. `ControlHumedadCalorZonaA` **Deshumectación calentando el aire mezclado.** Si la **temperatura de impulsión** (`g_rTempImpulsion1`) está por debajo de la **temperatura ambiente** (`g_rTemperaturaAire1`), activa la batería de calor para que el aire impulsado, al calentarse, baje su HR. Lleva su propio PID (`Dut_pid_CalorHR1`) con tope de impulsión. #### 10.2.7. `ControlEvaporativoTemperaturaZonaA` **Lazo de control simplificado para sistemas con evaporativo (configuración 0).** No es un PID sino **ON/OFF con histéresis horaria**: ```mermaid flowchart TD A{TA > TACalculada?} -->|Sí| B[Activar evaporimetro 100%
Abrir compuerta máxima] B --> C[Esperar 1 hora] C --> D{TA sigue > TACalculada?} D -->|Sí| E[Apagar evaporativo
compuerta a mínima] D -->|No| F[Mantener evaporativo
hasta consigna] A -->|No| G[Standby
g_iEvaporimetroFrio1 := 0] ``` #### 10.2.8. `ControlSoloEvaporativoTemperaturaZonaA` **Para configuración `g_iConfiguracion1 = 2` (solo evaporativo, sin baterías).** ON/OFF simple sin temporización: - TA > TACalculada → evaporimetro 100 %, caldera 0 %, compuerta máxima - TA < TACalculada → evaporimetro 0 %, **caldera 100 %**, compuerta mínima - TA = TACalculada → todo a 0, compuerta mínima #### 10.2.9. `ControlVentilacionZonaA` **Control proporcional puro de ventilación según diferencia de temperatura compost.** ```text Δ = g_rTemperaturaCompos1 − g_rTemperaturaCompoCTR1 si |Δ| ≤ g_rMargenMinimoDiferencia entonces Ventilación := Mínima sino si |Δ| ≥ g_rMargenMaximoDiferencia entonces Ventilación := Máxima sino pendiente := (Vmax − Vmin) / (MargenMax − MargenMin) Ventilación := Vmin + (|Δ| − MargenMin) * pendiente fin_si ``` La ventilación se ajusta **proporcionalmente** entre `g_iVentilacionMinima1` y `g_iVentilacionMaxima1` cuando la temperatura del compost se desvía de su consigna entre los márgenes mínimo y máximo. Esto es coherente con cultivos de champiñón donde la ventilación es la herramienta principal para evacuar el calor metabólico del micelio. --- ### 10.3. `ActivarProcesosZonaA` **Punto único de aplicación de una receta a las consignas activas.** **Dos caminos:** #### Camino 1: Activar Proceso Fijo Se activa cuando `g_iActivarProcesosFijos1 = 1`. Realiza: 1. Copia **todas** las consignas de la receta seleccionada (sufijo sin número) a las consignas activas (sufijo `1`) y de paso a las "EnCurso" (`EnCurso1`). 2. Copia las rampas. 3. Resetea los auxiliares de rampa (`rRampaAux1`, `rCO2RampaAux1`, `iHRRampaAux1`). 4. Reset de `g_iActivarProcesosFijos1` a 0. ```mermaid flowchart LR SEL[Variables Seleccionadas
g_rTAMaxima, g_iHMaxima, ...] ACT[Variables Activas
g_rTemperaturaAireMaxima1, ...] CURSO[Variables EnCurso
g_rTAMaximaEnCurso1, ...] SEL -->|copia completa| ACT SEL -->|copia completa| CURSO ``` #### Camino 2: Activar Proceso Temporal Se activa cuando `g_iActivarProcesosTemporales1 = 1`. Realiza: 1. Solo copia consignas marcadas por los flags `g_iPasar*`. Por cada magnitud, comprueba el flag y solo si vale 1, copia la versión `T` a la versión activa. 2. **No toca las EnCurso**, para poder restaurar al expirar. 3. Aplica duración (`g_iHorasT`, `g_iMinutosT`) y `g_iEtapaT`. ```text si g_iPasarTA = 1 entonces g_rTemperaturaAireCTR1 := g_rTACTRT; g_rTemperaturaAireMaxima1 := g_rTAMaximaT; g_rTemperaturaAireMinima1 := g_rTAMinimaT; fin_si // ... repetir para HR, CO2, Compost, Ventilación, Compuerta, Rampas ``` --- ### 10.4. Utilidades #### 10.4.1. `TablaEntalpias` **Función puramente combinacional**: dado `g_rTemperaturaAireHumedad1` (temperatura del aire, °C), devuelve `rHsaturacionxyz` (kg de agua / kg de aire seco), que es la **humedad de saturación a esa temperatura**. Implementa una **tabla psicrométrica entre −5 y 45 °C**: - Entre 12 y 26 °C, paso de 0.1 °C. - Resto, paso de 0.5 °C. Al final aplica una pequeña interpolación lineal para décimas: ```text rSuma := g_rTemperaturaAireHumedad1 * 10; rMultiplicada := rSuma − TO_REAL(TRUNC_TO_INT(rSuma)); rSuma := rMultiplicada / 10000; rHsaturacionxyz := rHsaturacionxyz + rSuma; ``` Se invoca múltiples veces desde `CalculoEntalpiaZonaA` para calcular humedad de saturación interior, exterior y para el bulbo húmedo. #### 10.4.2. `calcularFechaFinal` **Suma dias + horas a una fecha de partida**, contemplando años bisiestos. **Entradas:** - `g_iFunAno`, `g_iFunMes`, `g_iFunDia`, `g_iFunHora`, `g_iFunMinutos`: fecha de partida - `g_iDiaDuracion`: días a añadir - `g_iDuracionHoras`: horas a añadir **Algoritmo:** bucle `repeat ... until` que va decrementando los días por cada mes (28/29/30/31 según año bisiesto) hasta agotar `g_iDiaAuxiliar`. Cuando solo quedan horas, usa `CONCAT_DT_INT + ADD_DT_TIME` para sumar el resto. **Salida:** los mismos `g_iFunAno..Minutos` reescritos con la fecha final. > Nota: el cálculo de año bisiesto parte de 2024 y va sumando 4 (no usa la fórmula correcta de bisiesto: 4 con excepción 100 con excepción 400). Es **incorrecto a partir del año 2100**, pero válido para los próximos 70+ años. Estilísticamente refleja que el código se concibió para uso operativo a medio plazo. #### 10.4.3. `FlancoAlto` **Función de doble propósito:** 1. **Validación de clave horaria para entrar al panel de control:** concatena `mm` + `hh` actuales como string, lo compara con `g_sClavePanelControl`. Si coinciden, abre `g_iEntrarPanelControl := 1`. Es una **clave dependiente de la hora** (cambia cada minuto). 2. **Generador de huella para detección de modificaciones**: cuando `g_iSolocitar = 1`, genera 4 enteros pseudoaleatorios a partir de la hora y un contador `g_iCuentaVariable`, los expone en `g_iResPrimero..Cuarto`, y los compara con `g_iResPrimeroPan..CuartoPan` (que tendría el HMI). Si coinciden, activa `g_iVisibleSiGue := 1`. Es un mecanismo simple de **anti-duplicación** o **comprobación de pareja PLC/HMI**. #### 10.4.4. `LecturaDeAlarmasZonaA` **Mapea alarmas digitales de entrada a banderas globales:** | Entrada | Salida | |---------|--------| | `g_bAlarmaEntradaVentilador1` | `g_bValorAlarmaVentilador1` + `g_bValorAlarma1` | | `g_bAlarmaEntradaValvulaFrio1` | `g_bValorAlarmaValvulaFrio1` | | `g_bAlarmaEntradaValvulaCalor1` | `g_bValorAlarmaValvulaCalor1` | | `g_bAlarmaEntradaValvulaCompuerta1` | `g_bValorAlarmaValvulaCompuerta1` | > ⚠️ **Bug confirmado:** cada `if/else` **sobrescribe** `g_bValorAlarma1` en lugar de hacer OR. Solo la última condición evaluada (Compuerta) determina el valor final. Lo correcto sería: > > ```text > g_bValorAlarma1 := g_bValorAlarma1 OR g_bAlarmaEntradaXxx; > ``` > > Ver representación visual del bug en el [diagrama 11.10](#1110-lecturadealarmaszonaa). Los indicadores específicos por alarma sí funcionan; solo el indicador agregado `g_bValorAlarma1` está afectado. #### 10.4.5. `ContadorHumectacionZonaA` **Ciclo on/off del humectador con compensación por diferencia de humedad y ventilación.** ```text tiempo_humectando := SegundosHumectando + V_actual * pendiente si g_iParadoHumectador = 0 AND contador >= tiempo_humectando entonces pausar humectador sino si g_iParadoHumectador = 1 AND contador >= ContadorHmeParado/Diferencia entonces reanudar humectador fin_si ``` La **pausa** es más corta cuanto mayor es la diferencia entre la HR actual y la consigna (`rDiferencia = HumedadCTR − Humedad`, acotada a [1..4]), de modo que **si estamos muy lejos de la consigna, humecta más rápido**. #### 10.4.6. `PulsoContadorZonaA` Cuenta pulsos del caudalímetro del riego (`g_bPulsoContador1`) y los acumula en `g_rContadorAgua1` (REAL). #### 10.4.7. `WSimuladorPID` / `WSimuladorPIDB` **Modos de simulación** para probar los lazos sin actuadores conectados. Cuando `g_iActivarSimulador = 1`, calcula evolución teórica de TA y HR con ecuaciones de balance térmico simplificadas, permitiendo validar tuning PID en pantalla. --- ## 11. Diagramas de flujo por función > Esta sección visualiza los algoritmos internos de las funciones más complejas. Sirve como complemento gráfico de la sección 10. Los diagramas describen Zona A; Zona B es simétrica. ### 11.1. `CalculoEntalpiaZonaA` — lectura de sondas + cálculo psicrométrico ```mermaid flowchart TD Inicio([Inicio del scan]) --> Compos[Suma hasta 4 sondas
de compost activas
g_iLeerSonsaCompos1..41] Compos --> PromCompos[Promedio
g_rTemperaturaComposAux1] PromCompos --> Aire[Suma sondas PT100 aire
g_iLeerSonsaTA11/21
si g_iHumedaOAmbiente=0] Aire --> Cond4{"¿PT100 o 4-20mA?
g_iPT100O420"} Cond4 -- 4-20mA --> Aire420[Lee sondas 4-20mA
TA114, TA124] Cond4 -- PT100 --> PromAire[Promedio aire] Aire420 --> PromAire PromAire --> HumPT[Suma sondas PT100 húmedas
si g_iHumedaOAmbiente=1] HumPT --> HumHR[Lee sondas HR 1/2] HumHR --> ConCo2[Lee sondas CO2 1/2] ConCo2 --> CondExt{"¿g_iConectarSondas1=1?"} CondExt -- Sí --> Asigna[Asigna valores reales:
g_rTemperaturaAire1, g_iHumedad1,
g_iCO21, g_rTempImpulsion1...] CondExt -- No --> SimDatos[Mantener
valores simulados/fijos] Asigna --> Eth[Envía por Ethernet-IP:
g_wMandarTemperaturaEXT
g_wMandarHumedadEXT
g_wMandarCO2EXT] SimDatos --> Eth Eth --> HAbs[Humedad absoluta interior:
TablaEntalpias TempAire
→ rHsaturacion
rHAbsolutaInte1 = rHsat × HR/100] HAbs --> EntInt[Entalpía interior:
g_rEntalpiaInte1 =
1.005·T + Habs·1.88·T+2501] EntInt --> HAbsExt[Humedad absoluta exterior
+ entalpía exterior análogamente] HAbsExt --> LimHabs[g_rLimiteHumeAdsoluta1 =
rHsat – rHAbsolutaInte1] LimHabs --> HRSondas[Calcular HR sondas húmedas:
HR = HABAM / rHsat × 100] HRSondas --> Consigna[CONSIGNA CALCULADA DEL AIRE
g_rTACalculada1 =
TempAireCTR – TempCompos – TempCompoCTR+Margen × FCorr] Consigna --> Saturar{"Saturar:
min ≤ TACalc ≤ max"} Saturar --> Fin([Fin del ciclo]) ``` --- ### 11.2. `CalculoTemperaturasZonaA` — bucle principal de control ```mermaid flowchart TD Inicio([Inicio scan]) --> Sat[Saturar Compuerta1
entre min y max] Sat --> Act[ActivarProcesosZonaA
copiar consignas fijo/temporal] Act --> Riego{"¿g_iRegando1=1?"} Riego -- Sí --> ForzarE4[Forzar g_iEtapa1 = 4
g_iActivarSimulador = 1] Riego -- No --> Cfg{"¿g_iConfiguracion1=1?
solo climatizador"} ForzarE4 --> Cfg Cfg -- Sí --> SinEvapo[Desactivar evaporativo
= 0] Cfg -- No --> InfFrio SinEvapo --> InfFrio{"g_iInfluenciaFRIO1=0 y
clima-humedad activado?"} InfFrio -- Sí --> FrioOff[g_iFrio=0, desactivar] InfFrio -- No --> InfAll{"TA=0, HR=0, CO2=0
todas influencias off?"} FrioOff --> InfAll InfAll -- Sí --> CompMin[g_iCompuerta1 = Mín] InfAll -- No --> CheckEt CompMin --> CheckEt[Si g_bAuto15=1
→ humectar OFF] CheckEt --> Etapa{"Etapa g_iEtapa1"} Etapa -- 0 SALA VACIA --> Apagar[Todo OFF
'CONTROLES PARADOS'] Apagar --> Fin([Fin scan]) Etapa -- 1 TEMP --> T1[XEvapoTemp → XFreecool → XClima] T1 --> Vent Etapa -- 2 HR --> HR2[XDesHumAireExt → XDesHumFrio
→ XDesHumCalor → XHumectar] HR2 --> Vent Etapa -- 3 CO2 --> C3[XCO2Climati] C3 --> Vent Etapa -- 4..15 --> Tx[Combinaciones T/HR/CO2
en distintos órdenes] Tx --> Vent Etapa -- 16 SOLO EVAP --> Te16{"¿g_iConfiguracion1=2?"} Te16 -- Sí --> SoloEvap[ControlSoloEvaporativoTemperaturaZonaA] SoloEvap --> Vent Te16 -- No --> Vent Vent[ControlVentilacionZonaA
siempre que Etapa ≠ 0] Vent --> UIcfg[Configurar UI según
g_iConfiguracion1] UIcfg --> Fin ``` > **Clave:** Las Etapas definen la **prioridad** de control. En las combinadas, primero se mira temperatura y, si no requiere acción, se permite que HR o CO2 actúen sobre la compuerta. Así se evitan conflictos en el único actuador compartido. --- ### 11.3. `ControlTemperaturaZonaA` — lazo PID frío/calor ```mermaid flowchart TD Inicio([Inicio]) --> Lim[Saturar g_rTACalculada1
entre min y max] Lim --> Hora[GET_RTC_DT → iH, iMi] Hora --> Anem{"Anemómetro
g_iAnemometro1=1?
ventilador parado"} Anem -- No --> NoOp[Permite calentar/enfriar] Anem -- Sí --> Forzar0[Si en auto: Frío=0, Calor=0
NO se climatiza sin caudal] NoOp --> InRange Forzar0 --> InRange{"¿TempAire ≈ TACalc ±0.05?"} InRange -- Sí --> StandBy[STANDBY:
Margen=g_rMargenTA1
g_sAccionCT1='CLIMA STANDBY'
Reset auxiliares de rampa] InRange -- No --> Margen{"¿TempAire fuera
de TACalc ± Margen?"} Margen -- No --> StandBy StandBy --> Fin([Fin]) Margen -- Sí --> Rampa{"¿g_rRampaTA1 = 0?
(sin rampa)"} Rampa -- Sí --> TopeDirecto[rTTopeHoraC1 = TACalc
setpoint directo] Rampa -- No --> CambioCfg{"¿Cambió rampa,
tiempo o consigna?"} CambioCfg -- Sí --> InicRampa[Reiniciar:
Si TempAire ≤ TACalc:
TopeC = TempAire+RampaTA
Si no:
TopeC = TempAire–RampaTA
Calcular hora/min fin] CambioCfg -- No --> Avanzar InicRampa --> Avanzar Avanzar{"¿Llegó la hora/min fin?"} Avanzar -- Sí --> Sumar[Tope = Tope ± RampaTA
Si Tope alcanza TACalc:
fija Tope=TACalc, RampaTA=0
Recalcula nueva hora/min fin] Avanzar -- No --> Decidir Sumar --> Decidir TopeDirecto --> Decidir Decidir{"¿TempAire < TopeC
o Anemometro=0?"} Decidir -- Sí --> Calentar Decidir -- No --> Enfriar Calentar[Auto Calor:
PID inverso 16#0000
F355_PID_DUT
g_iCalor = MV
Limitar por TopeCalorImpulsion] Enfriar[Auto Frío:
PID I-PD directo 16#0003
F355_PID_DUT
g_iFrio = MV
Limitar por TopeFrioImpulsion] Calentar --> Fin Enfriar --> Fin ``` > **El anemómetro inhibe la climatización.** Si `g_iAnemometro1=1` indica que el ventilador no está moviendo aire — calentar o enfriar sin caudal sobrecalentaría/congelaría la batería. El lazo se inhibe automáticamente hasta que el ventilador arranque. --- ### 11.4. `ControlCO2ZonaA` — control de CO₂ por compuerta ```mermaid flowchart TD Inicio([Inicio]) --> Hora[GET_RTC_DT] Hora --> Range{"¿CO2 ≈ Consigna ±7 ppm?"} Range -- Sí --> SB[STANDBY:
g_iCO2Activada1=0] SB --> Fin([Fin]) Range -- No --> Mayor{"¿CO2 > Consigna + Margen?"} Mayor -- Sí --> Disminuir[Acción: 'DISMINUIR'
Abrir compuerta] Mayor -- No --> Menor{"¿CO2 < Consigna – Margen?"} Menor -- Sí --> Aumentar[Acción: 'AUMENTAR'
Cerrar compuerta] Menor -- No --> Fin Disminuir --> Rampa1{"¿Rampa CO2=0?"} Aumentar --> Rampa1 Rampa1 -- Sí --> SP[Tope = Consigna directo] Rampa1 -- No --> Avanzar[Gestión rampa con
iCO2HoraFin, iCO2MinutoFin] SP --> PID Avanzar --> PID PID[Escalar CO2: 0-5000 → 0-25000 → 0-10000
PID I-PD directo 16#0003
F355_PID_DUT
g_iCompuerta1 = MV] PID --> Sat[Saturar: min ≤ Compuerta ≤ max] Sat --> Frio{"¿TempExterior ≤ 0°C?"} Frio -- Sí --> LimFrio[Compuerta1 ≤ g_iTemporizador0Grados
protección anti-heladas] Frio -- No --> Fin LimFrio --> Fin ``` > **Especificidad del cultivo de champiñón:** la incubación requiere CO₂ alto (>5000 ppm) y la fructificación bajo (<1000 ppm). El mismo lazo gestiona ambos modos cambiando solo la consigna. --- ### 11.5. `ControlFreecoolingTemperaturaZonaA` — refrigeración gratuita ```mermaid flowchart TD Inicio([Inicio]) --> CntMin[Si pulso 1 min:
contador Freecool +0.5] CntMin --> Calc[TACalc = TACTR – corrección por compost] Calc --> Sat[Saturar min ≤ TACalc ≤ max] Sat --> ResetClima{"¿g_iActivadoClimat = 2?"} ResetClima -- Sí --> R0[g_iActivadoClimat=0
g_iFrio=0] ResetClima -- No --> Range R0 --> Range Range{"¿TempAire ≈ TACalc ±0.05?"} Range -- Sí --> SB[STANDBY Freecool:
Compuerta=Mín
g_iTempeFreecooling1=0] SB --> Fin([Fin]) Range -- No --> Margen{"¿TempAire > TACalc + Margen?
necesita enfriar"} Margen -- No --> Fin Margen -- Sí --> Rampa[Gestión rampa de TA] Rampa --> Calor{"¿TempAire < TopeC?
ya está enfriando"} Calor -- Sí --> RetornoClima[g_iActivadoClimat=1
cede a Climatizador] Calor -- No --> AbrirComp[g_iTempeFreecooling1=1
Abrir compuerta proporcional
al exceso de temperatura] AbrirComp --> SubeImp[Si TempImpulsion baja del límite:
limitar apertura] SubeImp --> Fin RetornoClima --> Fin ``` **Decisor `XFreecoolingTemperaturaClimatiZonaA` (gating):** ``` Activa freecooling SOLO si: TempAire ≥ TACalc (necesita enfriar) Y (TempExt + 1) ≤ TempAireCTR (exterior es frío) Y EntalpiaExt < EntalpiaInt (exterior aporta menos energía) Y TempExt ≤ TempAire Y g_iInfluenciaTA1 = 1 ``` Si no se cumple, devuelve el control a la batería de frío (`g_iActivadoClimat:=2`). --- ### 11.6. `ControlFreecoolingHumedadZonaA` — deshumectación con aire exterior ```mermaid flowchart TD Inicio([Inicio]) --> Cnt[Si pulso 1 min:
contador HR aire +0.5] Cnt --> Range{"¿HR ≈ Consigna ±0.1%?"} Range -- Sí --> SB[STANDBY:
g_iHumedadAireExterior1=0
'AIRE EXTERIOR STANDBYT'
Compuerta=CompuertaCO2] SB --> Fin([Fin]) Range -- No --> Mayor{"¿HR > Consigna + Margen?"} Mayor -- No --> Fin Mayor -- Sí --> Rampa[Gestión rampa HR] Rampa --> Compensar{"¿Necesita compensar T?"} Compensar -- Sí --> CompCalor[Abrir compuerta
+ activar calor para mantener T] Compensar -- No --> SoloComp[Sólo abrir compuerta
g_iHumedadAireExterior1=1] CompCalor --> Fin SoloComp --> Fin ``` **Decisor `XDesHumectarAireExteriorClimatiZonaA`:** ``` Activa deshumectación por aire exterior SOLO si: (TempExt + 1) ≤ TempCompoCTR Y rHAbsolutaExte1 < rHAbsolutaInte1 (exterior absolutamente más seco) Y g_iInfluenciaHR1 = 1 ``` --- ### 11.7. `ControlEvaporativoTemperaturaZonaA` — refrigeración evaporativa ```mermaid flowchart TD Inicio([Inicio]) --> Sat[Saturar TACalc] Sat --> Hora[GET_RTC_DT] Hora --> Range{"¿TempAire ≤ TACalc?
consigna alcanzada"} Range -- Sí --> SB[STANDBY:
g_iEvaporimetroFrio1=0
g_iActivadoEvapo1=1
'EVAPO STANDBY'] SB --> Fin([Fin]) Range -- No --> Enfriar[ENFRIAR EVAPO
g_iTempeEvaporativo1=1] Enfriar --> Activos{"¿1ª vez o cambió consigna?"} Activos -- Sí --> Inicio2[Marca iHoraFin = iH+1] Activos -- No --> SetEv Inicio2 --> SetEv[g_iEvaporimetroFrio1=100
g_iEvaporativoCompuerta1=Máx
g_iCompuerta1=Máx] SetEv --> WaitHora{"¿Pasada 1 hora
y aún caliente?"} WaitHora -- Sí --> Falla[Tras hora sigue alto:
desactivar evapo,
devolver a climatizador
g_iActivadoEvapo1=0] WaitHora -- No --> Fin Falla --> Fin ``` **Decisor `XEvapoTemperaturaClimatiZonaA`:** ``` Activa evaporativo SOLO si: g_iConfiguracion1 = 0 (climatizador + evaporativo) Y EntalpiaExt < 50 kJ/kg (aire ext con potencial evaporativo) Y TempExt ≤ 40 °C ``` > **Fallback temporal automático:** si tras 1 hora no consigue bajar la temperatura, marca `g_iActivadoEvapo1=0` para ceder al climatizador convencional. --- ### 11.8. `ControlHumectarZonaA` + `ContadorHumectacion` — humectación PWM ```mermaid flowchart TD subgraph CH ["ControlHumectarZonaA"] I1([Inicio]) --> R1{"¿HR ≈ Consigna ±0.1?"} R1 -- Sí --> SB1[Si Auto: Humectar=0
'HUMECTAR STANDBYT'] R1 -- No --> Mar{"¿HR < Consigna – Margen?"} Mar -- No --> Fin1([Fin función]) Mar -- Sí --> Rampa[Gestión rampa HR] Rampa --> CompMin{"¿Compuerta = Mín?"} CompMin -- No --> Fin1 CompMin -- Sí --> Act[g_iHumectar1 = 1
'HUMECTAR'] Act --> CallCnt[Llamar a ContadorHumectacion] SB1 --> Fin1 end subgraph CC ["ContadorHumectacion"] I2([Pulso 2s]) --> Cont[g_iContadorhumectando +1] Cont --> TVent["Calcular tiempo ON según ventilación:
tHum = SegMin + SegMax−SegMin × V/Vmax"] TVent --> Dif[rDiferencia = HRctr – HR
limitada a 4] Dif --> EstadoON{"¿Activo y contador ≥ tHum?"} EstadoON -- Sí --> Apagar[Apagar:
Parado=1, contador=0] EstadoON -- No --> EstadoOFF{"¿Parado y contador ≥
HmeParado / rDif?"} Apagar --> Out EstadoOFF -- Sí --> Encender[Encender:
Parado=0, contador=0] EstadoOFF -- No --> Out Encender --> Out Out{"¿g_iHumectar1>0 y Parado=0?"} Out -- Sí --> ValOn[g_bValorHumectar1 = TRUE] Out -- No --> ValOff[g_bValorHumectar1 = FALSE] ValOn --> Fin2([Fin]) ValOff --> Fin2 end CallCnt --> I2 ``` **Reglas:** - Es un control **PWM** real, no proporcional al PID: ciclos ON/OFF de duración variable. - A mayor ventilación → **más tiempo ON** (el aire se lleva el agua antes). - A mayor déficit (HR muy baja) → **menos tiempo OFF**. - Requiere que la compuerta esté cerrada al mínimo (no humecta con aire entrando). --- ### 11.9. `ControlVentilacionZonaA` — proporcional según compost ```mermaid flowchart TD Inicio([Inicio]) --> Auto{"¿g_bAuto13=0?
ventilación auto"} Auto -- No --> Fin([Fin / mantener manual]) Auto -- Sí --> Margen{"¿MargenMax – MargenMin > 0?"} Margen -- No --> Fin Margen -- Sí --> Compos{"¿TempCompos ≥ TempCompoCTR?"} Compos -- Sí --> Calc1["Δ = TCompos – TCompoCTR
Pendiente = VMax−VMin / MargenMax−MargenMin"] Compos -- No --> Calc2["Δ = TCompoCTR – TCompos
misma pendiente"] Calc1 --> Lim1{"|Δ| ≤ MargenMin?"} Calc2 --> Lim1 Lim1 -- Sí --> Min[Vent = VMin] Lim1 -- No --> Lim2{"|Δ| ≥ MargenMax?"} Lim2 -- Sí --> Max[Vent = VMax] Lim2 -- No --> Sat[Vent = VMin + |Δ|−MargenMin × Pendiente
Saturar entre VMin y VMax] Min --> Sat Max --> Sat Sat --> Fin ``` > Rampa lineal entre `VMin` (con `MargenMinimoDiferencia`, p.ej. 0.5 °C) y `VMax` (con `MargenMaximoDiferencia`, p.ej. 2 °C). **Única estrategia donde el compost manda directamente sobre un actuador** (sin pasar por la TA corregida). --- ### 11.10. `LecturaDeAlarmasZonaA` ```mermaid flowchart TD Inicio([Inicio]) --> AV{"¿AlarmaEntradaVentilador1?"} AV -- Sí --> AvOn[ValorAlarmaVentilador1=T
ValorAlarma1=T] AV -- No --> AvOff[ValorAlarmaVentilador1=F
ValorAlarma1=F ⚠] AvOn --> AF AvOff --> AF AF{"¿AlarmaEntradaValvulaFrio1?"} AF -- Sí --> AfOn[ValorAlarmaValvulaFrio1=T
ValorAlarma1=T] AF -- No --> AfOff[ValorAlarmaValvulaFrio1=F
ValorAlarma1=F ⚠] AfOn --> AC AfOff --> AC AC{"¿AlarmaEntradaValvulaCalor1?"} AC -- Sí --> AcOn[ValorAlarmaValvulaCalor1=T
ValorAlarma1=T] AC -- No --> AcOff[ValorAlarma1=F ⚠] AcOn --> ACo AcOff --> ACo ACo{"¿AlarmaEntradaValvulaCompuerta1?"} ACo -- Sí --> ACoOn[ValorAlarmaValvulaCompuerta1=T
ValorAlarma1=T] ACo -- No --> ACoOff[ValorAlarma1=F ⚠] ACoOn --> Fin([Fin]) ACoOff --> Fin style AvOff fill:#fef3c7,stroke:#d97706 style AfOff fill:#fef3c7,stroke:#d97706 style AcOff fill:#fef3c7,stroke:#d97706 style ACoOff fill:#fef3c7,stroke:#d97706 ``` > ⚠ **Bug confirmado en el código:** cada bloque `if/else` **sobrescribe** `g_bValorAlarma1` en lugar de hacer OR. Solo la última condición evaluada (Compuerta) determina el valor final. Lo correcto sería acumular: > ```text > g_bValorAlarma1 := g_bValorAlarma1 OR g_bAlarmaEntradaXxx; > ``` > En la práctica esto significa que **el indicador general** del HMI puede no encenderse aunque haya una alarma activa en Ventilador, Frío o Calor, si la entrada de Compuerta está OK. Los indicadores específicos sí funcionan correctamente. --- ### 11.11. `ActivarProcesosZonaA` — orquestador de consignas ```mermaid flowchart TD Inicio([Inicio]) --> Fijo{"¿ActivarProcesosFijos1=1?
nueva receta seleccionada"} Fijo -- Sí --> CopiaF[Copiar TODAS las consignas del
proceso FIJO al área 'EnCurso':
· TA, HR, CO2, TC, Compuerta
· Min/Max, Etapa, Rampas
· Nombre del proceso] Fijo -- No --> Temp CopiaF --> ResetAux[Reset auxiliares de rampa] ResetAux --> Temp Temp{"¿ActivarProcesosTemporales1=1?
proceso puntual lanzado"} Temp -- No --> Fin([Fin]) Temp -- Sí --> CopiaTNom[Copiar nombre del proceso temporal] CopiaTNom --> PasarTA{"¿g_iPasarTA=1?"} PasarTA -- Sí --> AsTA[Copia TA temporal] PasarTA -- No --> PasarHR AsTA --> PasarHR{"¿g_iPasarHR=1?"} PasarHR -- Sí --> AsHR[Copia HR temporal] PasarHR -- No --> PasarCO2 AsHR --> PasarCO2{"¿g_iPasarCO2=1?"} PasarCO2 -- Sí --> AsCO2[Copia CO2 temporal] PasarCO2 -- No --> Otros AsCO2 --> Otros[Comprobar resto de flags:
Compost, Ventilación, Compuerta,
Deshumect, Humectar, Rampas] Otros --> Dur[Copiar Duración:
Horas, Minutos, Etapa] Dur --> Fin ``` **Importancia:** el operador define dos clases de procesos: - **Fijos** (planificados): la receta de cultivo (hasta 15 fases por receta). - **Temporales** (puntuales): ajustes "encima" de la fase actual sin perder la receta. Cada flag `g_iPasarXX` permite aplicar **selectivamente** sólo algunos parámetros del temporal. Esto evita que un override temporal de 4 h en CO₂ tenga que duplicar también temperatura, humedad, etc. --- ### 11.12. `ControlFechaFinalSecuencialesZonaA` — avance de receta ```mermaid flowchart TD Inicio([Inicio]) --> Cfg{"¿g_iSecuFunActivado=1?
secuenciador habilitado"} Cfg -- No --> Fin([Fin]) Cfg -- Sí --> Act{"¿g_iSecuActivado1=1?
avanzar al siguiente"} Act -- No --> CalcF[Calcular fecha final
del proceso actual] Act -- Sí --> Inc[g_iIndice += 1] Inc --> Range{"¿Índice ≤ 15?"} Range -- No --> Fin Range -- Sí --> Hay{"¿Días o Horas
del proceso > 0?"} Hay -- No --> Fin Hay -- Sí --> Cargar[Asignar al área 'EnCurso':
· Nombre, TA, HR, CO2, TC
· Min/Max de cada variable
· Ventilación, Compuerta
· Etapa, Rampas TA/HR/CO2
· Duración días/horas] Cargar --> Activar[g_iActivarProcesosFijos1 = 1
→ ActivarProcesosZonaA aplicará] Activar --> CalcF CalcF --> Fin ``` > Una receta típica de champiñón tiene **4–7 fases** (relleno, incubación, choque térmico, fructificación, recolección…). El programa soporta hasta 15 fases por receta para cubrir cultivos largos con flushes múltiples. --- ### 11.13. Programa `Modbus` — comunicación con E/S remotas ```mermaid flowchart TD Inicio([Inicio scan]) --> First{"sys_bIsFirstScan?"} First -- Sí --> Init[iTurnoModbus = 0] First -- No --> Pack Init --> Pack Pack["Empaqueta Holding Registers:
RegistrosSalida 0 = g_iCalor
RegistrosSalida 1 = g_iFrio
RegistrosSalida 2 = g_iCompuerta1
RegistrosSalida 4 = g_iVentilacion1
RegistrosSalida 5 = g_iVelocidadRiego1"] Pack --> Coils["Empaqueta Coils:
Coil 0 = Ventilador
Coil 1 = GiroRiego
Coil 2 = ParoMarchaRiego
Coil 3 = Riego
Coil 6 = Humectar"] Coils --> Turn{"Turno Modbus
iTurnoModbus"} Turn -- 0 --> LeerIR[Leer Input Registers
FC 04 — sondas analógicas] Turn -- 1 --> LeerDI[Leer Discrete Inputs
FC 02 — alarmas, finales carrera] Turn -- 2 --> EscrHR[Escribir Holding Registers
FC 16 — actuadores 0-100] Turn -- 3 --> EscrCoils[Escribir Coils
FC 15 — relés ON/OFF] LeerIR --> Avanzar[iTurnoModbus = +1 mod 4] LeerDI --> Avanzar EscrHR --> Avanzar EscrCoils --> Avanzar Avanzar --> Fin([Fin scan]) ``` --- ## 12. Bucles de control ### 12.1. Estructura general de un lazo Todos los lazos de control siguen exactamente el mismo esqueleto, lo que da consistencia y predictibilidad al sistema. Cualquier `Control*ZonaA` que actúe sobre un PID se puede leer con esta plantilla mental: ```mermaid flowchart TD A[Leer fecha/hora] --> B{¿|PV − SP| ≤ banda_estrecha?} B -->|Sí| C[Margen activo := margen_grande
Reset rampa
Acción := STANDBY] B -->|No| D{¿|PV − SP| > margen_activo?} D -->|No| E[Mantener PID corriendo
con margen reducido] D -->|Sí| F[Margen := 0] F --> G{¿Rampa = 0?} G -->|Sí| H[Tope móvil := SP] G -->|No| I[Calcular tope móvil interpolado
según hora/min de rampa] H --> J[Configurar PID:
pv, sp escalados a 0..10000] I --> J J --> K{¿Autotuning hecho?} K -->|No, primera vez| L[Control = autotuning code] K -->|Sí| M[Control = modo final
16#0000 PI-D inverso
16#0001 directo
16#0003 I-PD directo] L --> N[F355_PID_DUT] M --> N N --> O[Salida MV → actuador
limitar entre mínimo/máximo] O --> P[Aplicar topes por impulsión] ``` ### 12.2. Escalado de variables al PID El bloque `F355_PID_DUT` de Panasonic trabaja con `INT` en rango 0..10000 tanto para PV como para SP. Las variables de proceso se escalan con `SCALE_INT`: | Variable | Rango físico | Escalado al PID | |----------|--------------|-----------------| | Temperatura aire | 0..100 °C | `INT(TA * 100)` directo a 0..10000 | | Humedad relativa | 0..100 % | `INT(HR * 100)` directo | | CO₂ | 0..5000 ppm | Doble escalado: 0..5000 → 0..25000 → 0..10000 | > El doble escalado del CO₂ es **inusual** y probablemente refleja un intento histórico de mejorar la resolución del PID al cuadruplicar el rango intermedio. Funcionalmente es equivalente a un escalado lineal directo 0..5000 → 0..10000. ### 12.3. Modos de control PID utilizados | Hexadecimal | Modo | Uso | |-------------|------|-----| | `16#8000` | Auto-tuning | Modo inicial **PI-D inverso** (Calor) | | `16#0000` | PI-D inverso (acción positiva sube cuando PV baja) | Modo normal de **Calor** | | `16#8001` | Auto-tuning | Modo inicial **I-PD directo** (Frío, CO₂, HR) | | `16#0001` | PID directo (no usado tras AT) | Stand-by | | `16#0003` | I-PD directo (acción positiva sube cuando PV sube) | Modo normal de **Frío**, **CO₂**, **HR** | ### 12.4. Autotuning El autotuning de Panasonic se ejecuta automáticamente la primera vez que cada PID se invoca con palabra de control `16#8000` o `16#8001`. El indicador `Dut_pid_*.AT_Progress` recorre 0..100 %. El código considera el autotuning "completado" cuando `AT_Progress ≥ 5`: ```text if (Dut_pid_Calor1.AT_Progress >= 5) then Dut_pid_Calor1.Control := 16#0000; g_iAutotunigCalor1 := 1; end_if; ``` > El umbral de 5 % parece excesivamente bajo. En la práctica, esto significa que el autotuning **apenas tiene tiempo de caracterizar la planta** antes de pasar al modo operativo. Es posible que sea un mecanismo para forzar el modo PI-D normal de forma temprana cuando el autotuning real ya se hizo offline y los parámetros `Kp`, `Ti`, `Td`, `Ts` fueron ajustados manualmente. ### 12.5. Limitación por temperatura de impulsión Para proteger los conductos y el material de la cámara, las baterías de frío y calor se limitan si la temperatura del aire impulsado (`g_rTempImpulsion1`) se aproxima a los topes definidos: | Tope | Variable | Default | |------|----------|---------| | Tope superior (calor) | `g_rTopeCalorImpulsion` | 40 °C | | Margen superior | `g_rMargenTopeCalorImpulsion` | 1 °C | | Tope inferior (frío) | `g_rTopeFrioImpulsion` | 5 °C | | Margen inferior | `g_rMargenTopeFrioImpulsion` | 1 °C | Cuando `TempImpulsion ≥ TopeCalor − Margen`, el calor se reduce proporcionalmente: ```text g_iCalor := TO_INT((TopeCalor − TempImpulsion) * (Calor_anterior / Margen)); ``` Esto crea una **función de transferencia lineal** entre `TopeCalor − Margen` (calor pleno) y `TopeCalor` (calor cero). El cambio es suave para evitar oscilaciones. --- ## 13. Cálculo psicrométrico Todo el cálculo psicrométrico se hace en `CalculoEntalpiaZonaA`. La cadena es: ```mermaid flowchart LR A[T aire interior] -->|TablaEntalpias| B[Hsat interior] B --> C[Habs interior = Hsat * HR/100] A --> D[Entalpía interior = 1.005·T + Habs·(1.88·T + 2501)] E[T aire exterior] -->|TablaEntalpias| F[Hsat exterior] F --> G[Habs exterior = Hsat * HRext/100] E --> H[Entalpía exterior = 1.005·Text + Habs_ext·(1.88·T + 2501)] I[T bulbo húmedo] -->|TablaEntalpias| J[Hsat psicrómetro] J --> K[HABAM = Hsat_psicro − 0.0004·(T−Tbh)] K --> L[HR calculada = HABAM/Hsat_interior · 100] M[Hsat interior] --> N[Límite humedad absoluta = Hsat − Habs interior] ``` ### 13.1. Fórmulas usadas | Cálculo | Fórmula | |---------|---------| | Humedad absoluta | `Habs = Hsat * (HR / 100)` kg/kg | | Entalpía del aire húmedo | `h = 1.005·T + Habs·(1.88·T + 2501)` kJ/kg | | HR desde psicrómetro | `HR = (Hsat_BH − 0.0004·(T − Tbh)) / Hsat * 100` | | Límite a saturación | `LimitHabs = Hsat − Habs` | ### 13.2. Uso operativo Estos valores se utilizan para: | Variable | Función que la consulta | Decisión | |----------|--------------------------|----------| | `g_rEntalpiaExte1 < g_rEntalpiaInte1` | `XFreecoolingTemperaturaClimatiZonaA` | Permitir freecooling solo si el exterior aporta menos energía | | `rHAbsolutaExte1 < rHAbsolutaInte1` | `XDesHumectarAireExteriorClimatiZonaA` | Permitir deshumectar con aire exterior solo si es más seco en términos absolutos | | `g_rLimiteHumeAdsoluta1` | (visualización) | Avisar al operador de cercanía a saturación (condensación) | | `g_rEntalpiaExte1 < 50` | `XEvapoTemperaturaClimatiZonaA` | Habilitar evaporativo solo si el aire exterior no aporta demasiada energía | Esta capa psicrométrica es la que distingue al sistema de un controlador "todo o nada" y le da capacidad de **decisiones por carga energética y no solo por temperatura nominal**. --- ## 14. Sistema de riego ### 14.1. Arquitectura física El riego utiliza un **grupo motorizado bidireccional** (típicamente una rampa de riego que recorre la cámara longitudinalmente), con: | Elemento | Variable de control | |----------|---------------------| | Motor avance/retroceso | `g_bValorParoMarchaRiego1` + `g_bValorGiroRiego1` | | Variador de velocidad | `g_iVelocidadRiego1` (0..100) | | Detector inicio carrera | `g_bEntradaInicioCarrera1` | | Detector final carrera | `g_bEntradaFinalCarrera1` | | Electroválvula riego ambiente | `g_bValorRiego1` ← `g_iAbrirGrifoAmbiente1` | | Electroválvula riego suelo | `g_bValorRiegoSuelo1` ← `g_iAbrirGrifoSuelo1` | | Electroválvula riego suelo caliente | `g_bValorRiegoSueloCa1` ← `g_iAbrirGrifoSueloCa1` | | Caudalímetro | `g_bPulsoContador1` → `g_rContadorAgua1` | ### 14.2. Modos y operaciones | `g_sModoRiego1` | Significado | |------------------|-------------| | `.AUTOMATICO.` | Ejecución programada por fecha/hora | | `.MANUAL.` | Ejecución inmediata desde HMI | | `g_sOperacionRiego1` | Significado | |------------------------|-------------| | `.RIEGO AMBIENTE.` | Riego desde rampa elevada (humectación) | | `.RIEGO SUELO.` | Riego al suelo (drenaje) | | `.RIEGO SUELO CALIENTE.` | Riego al suelo con agua caliente | ### 14.3. Programación El sistema almacena hasta **200 riegos programados** en arrays paralelos. Cada slot contiene: | Array | Tipo | Contenido | |-------|------|-----------| | `g_aiProgramaRiegoAno1` | INT[0..199] | Año | | `g_aiProgramaRiegoMes1`, `Dia1`, `Hora1`, `Minutos1` | INT | Fecha/hora | | `g_asProgramaRiegoOperacion1` | STRING[32] | Tipo de operación | | `g_asProgramaRiegoModo1` | STRING[32] | Modo | | `g_aiProgramaRiegoLitros1` | INT | Litros objetivo | | `g_aiProgramaRiegoTramos1` | INT | Número de tramos | | `g_aiProgramaRiegoVelocidad1` | INT | Velocidad del motor | El **índice de inserción** se mantiene en `g_iContadorIndiceRiego1`. ### 14.4. Diagrama de estados de un riego ```mermaid stateDiagram-v2 [*] --> Inactivo Inactivo --> Programado: Operador graba riego
g_iGrabarRiego1 = 1 Programado --> Preparativos: Llega fecha/hora
g_iPreparativosRiego1 = 1 Preparativos --> Posicionando: Motor a inicio carrera Posicionando --> Regando: g_bEntradaInicioCarrera1 = TRUE
g_iRegando1 = 1 Regando --> Regando: Conteo litros
g_rContadorAgua1 acumula Regando --> Finalizando: Litros objetivo alcanzados
OR final carrera Finalizando --> Inactivo: Cierre electroválvulas
motor a inicio Programado --> Inactivo: Cancelación operador Regando --> Inactivo: g_iStopRiego1 = 1 ``` ### 14.5. Cálculo de velocidad y tramos El sistema permite definir cuántos **tramos** (pasadas) tiene un riego y cuántos litros totales se quieren aplicar. A partir de: | Parámetro | Variable | |-----------|----------| | Litros objetivo | `g_iLitrosRiego1` | | Tramos | `g_iTramosRiego1` | | Píxeles por segundo (constante de calibración) | `g_iPilxenSegundo1` (default 245) | | Píxeles del recorrido total | `g_iPilxenRecorido1` (default 368) | | Litros para velocidad punto | `g_rLitrosPuntoVelocidad1` | Calcula: - `g_rLitrosConsumidosTramo1`: litros por tramo - `g_rTiempoPorTramo1`: tiempo en segundos por tramo - `g_rTiempoTotal1`: tiempo total estimado - `g_iVelocidadRiego1`: velocidad efectiva (0..100) para el variador Durante el riego, si los litros realmente consumidos divergen de los esperados, el sistema ajusta la velocidad mediante `g_iVelocidadRiegoAux1` y `g_iGuardarVelocidad1` para corregir. ### 14.6. Interacción con el control climático Cuando `g_iRegando1 = 1`: - **`CalculoTemperaturasZonaA` fuerza `g_iEtapa1 := 4`** (TEMPERATURA + CO₂). - Se activa el simulador (`g_iActivarSimulador := 1`). - Las sondas pueden dar lecturas anómalas (mojado, brusca caída de T compost) por lo que el simulador estabiliza el control hasta el final. Al finalizar: - `g_iEtapa1 := g_iEtapaEnCurso1` (restauración). - Las sondas vuelven a tomar el control real. ### 14.7. Contador de cultivo El programa mantiene **estadísticas de litros consumidos por cultivo**: | Variable | Sentido | |----------|---------| | `g_iTotalLitrosCultivo1` | Litros aplicados desde el inicio del cultivo actual | | `g_iTotalLitrosCultivoActual1` | Subtotal del riego actual | | `g_iCicloRiero1` | Número de ciclo de cultivo (forma parte del nombre `g_sNombreUltimoCultivo1`) | --- ## 15. Comunicaciones Modbus ### 15.1. Configuración del bus | Parámetro | Valor | |-----------|-------| | Modo | Maestro Modbus RTU | | Puerto físico | `SYS_COM1_PORT` | | Esclavos | 1 único (`SlaveAddress = 1`) | | Espaciado entre transacciones | 50 ms (timer `TM_Timer_Comunicacion`) | | Detección de error | `sys_bIsComPort1CommunicationError`. Bandera `g_bErrorModbusS1` con auto-reset tras 3000 scans sin error | ### 15.2. Secuenciador de turnos El programa **Modbus** ejecuta **4 turnos cíclicos** (`iTurnoModbus = 0..3`), uno por scan de comunicación, para no solapar transacciones: ```mermaid stateDiagram-v2 [*] --> Turno0 Turno0: TURNO 0
FC 04 Read Input Registers
StartReg 0x0002, Cant. 40
→ aRegistrosEntrada Turno1: TURNO 1
FC 02 Read Discrete Inputs
StartReg 0x0002, Cant. 5
→ aDiscreteInputs Turno2: TURNO 2
FC 16 Preset Multiple Registers
StartReg 0x0001, Cant. 6
← aRegistrosSalida Turno3: TURNO 3
FC 15 Force Multiple Coils
StartReg 0x0002, Cant. 7
← aCoils Turno0 --> Turno1 Turno1 --> Turno2 Turno2 --> Turno3 Turno3 --> Turno0 ``` ### 15.3. Mapa de Input Registers (lectura, FC 04) `aRegistrosEntrada[0..39]` → variables globales: | Índice array | Reg. Modbus | Tipo | Variable destino | Sentido | |--------------|-------------|------|-------------------|---------| | 0 | 0x0002 | DWORD→REAL | `g_rTemperaturaComposAux11` | Sonda Compost 1 | | 1 | 0x0004 | DWORD→REAL | `g_rTemperaturaComposAux12` | Sonda Compost 2 | | 2 | 0x0006 | DWORD→REAL | `g_rTemperaturaComposAux13` | Sonda Compost 3 | | 3 | 0x0008 | DWORD→REAL | `g_rTemperaturaComposAux14` | Sonda Compost 4 | | 4 | 0x000A | DWORD→REAL | `g_rTemperaturaMezclaAux1` | T mezcla | | 5 | 0x000C | DWORD→REAL | `g_rImpulsionFrio1` | T impulsión frío | | 6 | 0x000E | DWORD→REAL | `g_rImpulsionCalor1` | T impulsión calor | | 7 | 0x0010 | DWORD→REAL | `g_rEntradaAFria1` | T entrada agua fría | | 8 | 0x0012 | DWORD→REAL | `g_rSalidaAFria1` | T salida agua fría | | 9 | 0x0014 | DWORD→REAL | `g_rSalidaACaliente1` | T salida agua caliente | | 10 | 0x0016 | DWORD→REAL | `g_rEntradaACaliente1` | T entrada agua caliente | | 11 | 0x0018 | (saltado) | — | Reservado | | 12 | 0x001A | DWORD→INT | `g_iCO2Ambiente11` | Sonda CO₂ ambiente 1 | | 13 | 0x001C | DWORD→INT | `g_iCO2Ambiente12` | Sonda CO₂ ambiente 2 | | 14 | 0x001E | DWORD→REAL | `g_iHumedadAmbiente11` | Sonda HR ambiente 1 | | 15 | 0x0020 | DWORD→REAL | `g_iHumedadAmbiente12` | Sonda HR ambiente 2 | | 16 | 0x0022 | DWORD→REAL | `g_iHumedadSalidaFrio1` | T aire salida frío | | 17 | 0x0024 | DWORD→REAL | `g_rImpulsionBCalor1` | T impulsión BCalor | > Cada registro consume 2 words (porque son DWORD/REAL = 32 bits). Por eso los registros Modbus tienen saltos de 2 (0x0002, 0x0004, 0x0006...). ### 15.4. Mapa de Discrete Inputs (lectura, FC 02) `aDiscreteInputs[0..4]`: | Índice | Variable | Sentido | |--------|----------|---------| | 0 | `g_bAlarmaEntradaVentilador1` | Alarma ventilador | | 1 | `g_bAlarmaRiego1` | Alarma riego | | 2 | `g_bPulsoContador1` | Pulso del caudalímetro | | 3 | `g_bEntradaInicioCarrera1` | Sensor inicio carrera motor riego | | 4 | `g_bEntradaFinalCarrera1` | Sensor final carrera motor riego | ### 15.5. Mapa de Holding Registers (escritura, FC 16) `aRegistrosSalida[0..5]`: | Índice | Variable origen | Sentido | |--------|------------------|---------| | 0 | `g_iCalor` | % calor para baterías | | 1 | `g_iFrio` | % frío para baterías | | 2 | `g_iCompuerta1` | % apertura compuerta | | 3 | *(reservado)* | — | | 4 | `g_iVentilacion1` | % ventilación | | 5 | `g_iVelocidadRiego1` | Velocidad motor riego | ### 15.6. Mapa de Coils (escritura, FC 15) `aCoils[0..6]`: | Índice | Salida | Significado | Origen | |--------|--------|-------------|--------| | 0 | Relé 1 | Ventilador ON/OFF | `g_bSalidaVentilador1 ← g_bValorVentilador1` | | 1 | Relé 2 | Giro motor riego (sentido) | `g_bSalidaGiroRiego1 ← g_bValorGiroRiego1` | | 2 | Relé 3 | Paro/marcha motor riego | `g_bSalidaParoMarchaRiego1 ← g_bValorParoMarchaRiego1` | | 3 | Relé 4 | Electroválvula riego | `g_bSalidaRiego1 ← g_bValorRiego1` | | 4 | Relé 5 | (Válvula de reserva, no asignada) | — | | 5 | Relé 6 | (Consola riego, no asignada) | — | | 6 | Relé 7 | Humectador | `g_bSalidaHumectar1 ← g_bValorHumectar1` | Relés 8–12 reservados (vapor, etc.) sin asignación actual. ### 15.7. EtherNet/IP (sondas exteriores) Adicionalmente, el sistema replica sondas exteriores por EtherNet/IP a otro dispositivo: ```text g_wMandarTemperaturaEXT := TO_WORD(TO_INT(g_rTemperaturaExterior * 100)); g_wMandarHumedadEXT := TO_WORD(g_iHumedadExterior); g_wMandarCO2EXT := TO_WORD(g_iCO2Exterior); ``` Esto sugiere que existe una **estación meteorológica externa** conectada por otro bus, cuyas lecturas se exponen a un sistema superior. --- ## 16. Gestión de fechas y procesos secuenciales ### 16.1. Flujo de un proceso temporal ```mermaid sequenceDiagram actor Op as Operador participant HMI as Pantalla HMI participant PLC as PLC Op->>HMI: Define duración (H, M)
y flags g_iPasar* HMI->>PLC: g_iFunActivado = 1
g_iActivarProcesosTemporales1 = 1 PLC->>PLC: ActivarProcesosZonaA()
aplica overrides PLC->>PLC: calcularFechaFinal()
guarda g_iFechaFinAno1..Min1 Note over PLC: Control ejecuta con consignas temporales PLC->>PLC: Cada scan compara fecha actual con fechafin PLC-->>PLC: Cuando fecha actual ≥ fechafin PLC->>PLC: Restaura *EnCurso* sobre *Activos*
Reset fechafin = 0 Note over PLC: Vuelve al proceso fijo previo ``` ### 16.2. Flujo de procesos secuenciales Los procesos secuenciales encadenan **hasta 15 procesos fijos**, cada uno con su propia duración. Al expirar uno, el siguiente carga automáticamente. ```mermaid sequenceDiagram actor Op as Operador participant HMI as Pantalla HMI participant SEQ as ControlFechaFinalSecuenciales participant ACT as ActivarProcesos Op->>HMI: Define 1..15 etapas
cada una con días/horas HMI->>SEQ: g_iSecuActivado1 = 1
g_iSecuFunActivado = 1
g_iIndice = 0 SEQ->>SEQ: g_iIndice++
(=1) SEQ->>SEQ: Carga arrays[1] → consignas activas SEQ->>SEQ: calcularFechaFinal()
establece fecha fin de paso 1 loop Hasta fecha fin SEQ->>SEQ: Sleep / control normal end SEQ->>SEQ: Llega fecha fin SEQ->>SEQ: g_iSecuFunActivado = 1 SEQ->>SEQ: g_iIndice = 2
(o aborta si días/horas = 0) SEQ->>SEQ: Carga arrays[2] → consignas activas SEQ->>ACT: Nuevas consignas, EnCurso etc. Note over SEQ,ACT: ... repite hasta i=15 o array vacío ``` ### 16.3. Reglas de detección del fin de un proceso La comparación es **literal con `≥`** sobre los 5 componentes de fecha: ```text si (g_iFechaFinAno1 > 0) AND (iA >= g_iFechaFinAno1) AND (iM >= g_iFechaFinMes1) AND (iD >= g_iFechaFinDia1) AND (iH >= g_iFechaFinHora1) AND (iMi >= g_iFechaFinMinutos1) entonces ... fin de proceso fin_si ``` > Esto es **incorrecto** estrictamente: con la lógica `AND` actual, si por ejemplo la fecha fin es `2026/11/15` y la actual es `2026/12/01`, sí dispara (correcto); pero si la fecha fin es `2026/02/28 23:55` y la actual es `2026/03/01 00:05`, **NO dispara** (iMi=05 < 55) — falsamente no detecta fin. En la práctica, como se evalúa en cada scan (≈ varias veces por segundo), al cabo de pocos segundos el minuto avanza por encima y se dispara, por lo que el error práctico es de **unos segundos a un minuto**. Esto es aceptable para procesos de cultivo de mushrooms que duran días, pero conviene documentarlo como pequeño defecto de diseño. ### 16.4. Abortar manualmente | Variable | Efecto | |----------|--------| | `g_iAbortarProcesoTemporal1 = 1` | Aborta temporal y restaura consignas EnCurso (si no hay secuencia activa) | | `g_iAbortarSecuencial1 = 1` | Limpia fechas y posición de la secuencia (se evalúa solo si no hay temporal ni paso en curso) | --- ## 17. Alarmas y seguridad ### 17.1. Alarmas digitales (entradas físicas) Detectadas por `LecturaDeAlarmasZonaA()` desde `aDiscreteInputs`: | Entrada Modbus | Variable | Significado | |----------------|----------|-------------| | DI 0 | `g_bAlarmaEntradaVentilador1` | Térmico del motor ventilador disparado | | DI 1 | `g_bAlarmaRiego1` | Fallo en grupo de riego (presión, motor, etc.) | | DI ? | `g_bAlarmaEntradaValvulaFrio1` | Fallo válvula frío | | DI ? | `g_bAlarmaEntradaValvulaCalor1` | Fallo válvula calor | | DI ? | `g_bAlarmaEntradaValvulaCompuerta1` | Fallo servomotor compuerta | Cada una activa su flag de visualización (`g_bValorAlarma*`) y consolida en `g_bValorAlarma1` (alarma agregada). ### 17.2. Alarmas por desviación de proceso Definidas en `ColoresCirculosCentralesZonaA`: | Magnitud | Umbral de alarma | Variable HMI | |----------|------------------|---------------| | Temperatura aire | `g_rAlarmaTA1` (default 3 °C) | Color negro al sobrepasar máximo + alarma | | Temperatura compost | `g_rAlarmaTC1` (default 3 °C) | idem | | Humedad relativa | `g_rAlarmaHR1` (default 4 %) | idem | | CO₂ | `g_iAlarmaCO21` (default 500 ppm) | idem | Cuando se dispara una alarma de desviación: ```text g_bMandarAlarma := TRUE g_iMailAlarma1 := 1 g_iContadorAlarma1 incrementa ``` El sistema externo es responsable de leer estas variables y enviar la notificación por email/SMS. ### 17.3. Alarma de comunicación Modbus | Condición | Efecto | |-----------|--------| | `sys_bIsComPort1CommunicationError = TRUE` | `g_bErrorModbusS1 := TRUE` | | Tras 3000 scans sin error | `g_bErrorModbusS1 := FALSE` (recuperación) | > Esto significa que si la comunicación cae intermitentemente, el flag se mantiene activo entre fallos cortos, dando estabilidad al diagnóstico. ### 17.4. Saneamiento de consignas `ColoresCirculosCentralesZonaA` aplica al inicio una **limpieza defensiva** de todas las consignas para garantizar coherencia: ```text si Máxima < Mínima entonces Máxima := Mínima si Mínima > Máxima entonces Mínima := Máxima si CTR > Máxima entonces CTR := Máxima si CTR < Mínima entonces CTR := Mínima ``` Esto evita que un error en la pantalla (introducir una consigna fuera de límites) deje el sistema en estado incoherente. ### 17.5. Topes de seguridad por temperatura de impulsión Ya descritos en sección [12.5](#125-limitación-por-temperatura-de-impulsión). Constituyen una **última línea de defensa hardware** para que las baterías no impulsen aire a temperaturas que dañen materiales o producto: - Tope calor: 40 °C - Tope frío: 5 °C ### 17.6. Saturación de compuerta a 0 °C exterior Cuando `g_rTemperaturaExterior ≤ 0`, la apertura máxima de la compuerta se limita a `g_iTemporizador0Grados` (default 50 %) para evitar congelación de la batería de frío al recibir aire muy frío. Aplicado en `ControlCO2ZonaA` y `ControlFreecoolingHumedadZonaA`. --- ## 18. Apéndices ### 18.1. Apéndice A — Inventario de funciones | Función | Líneas (src) | Categoría | Versión B | |---------|--------------|-----------|-----------| | `ActivarProcesosZonaA` | 2–474 | Activación | `ActivarProcesosZonaB` | | `ContadorHumectacion` | 957–1081 | Utilidad | `ContadorHumectacionB` | | `ControlCO2ZonaA` | 1221–1620 | Lazo PID | `ControlCO2ZonaB` | | `ControlEvaporativoTemperaturaZonaA` | 2021–2231 | Lazo proporcional | `ControlEvaporativoTemperaturaZonaB` | | `ControlFreecoolingHumedadZonaA` | 2441–2956 | Lazo PID | `ControlFreecoolingHumedadZonaB` | | `ControlFreecoolingTemperaturaZonaA` | 3474–4045 | Lazo PID | `ControlFreecoolingTemperaturaZonaB` | | `ControlHumectarZonaA` | 4616–4978 | Lazo PID | `ControlHumectarZonaB` | | `ControlHumedadCalorZonaA` | 5341–5504 | Lazo PID | `ControlHumedadCalorZonaB` | | `ControlHumedadFrioZonaA` | 5671–6066 | Lazo PID | `ControlHumedadFrioZonaB` | | `ControlRiegoAsignarArrayZonaA` | 6467–6514 | Riego | `ControlRiegoAsignarArrayZonaB` | | `ControlRiegoLineaDeTextoZonaA` | 6567–6644 | Riego | `ControlRiegoLineaDeTextoZonaB` | | `ControlSoloEvaporativoTemperaturaZonaA` | 6727–6889 | Lazo ON/OFF | `ControlSoloEvaporativoTemperaturaZonaB` | | `ControlTemperaturaZonaA` | 7051–7562 | Lazo PID | `ControlTemperaturaZonaB` | | `ControlVentilacionZonaA` | 8079–8165 | Lazo proporcional | `ControlVentilacionZonaB` | | `FlancoAlto` | 8256–8367 | Utilidad | — (compartida) | | `LecturaDeAlarmasZonaA` | 8370–8419 | Alarmas | `LecturaDeAlarmasZonaB` | | `PulsoContadorZonaA` | 8474–8508 | Riego | `PulsoContadorZonaB` | | `TablaEntalpias` | 8548–8785 | Psicrometría | — (compartida) | | `WSimuladorPID` | 8788–8900 | Simulación | `WSimuladorPIDB` | | `XCO2ClimatiZonaA` | 9016–9049 | Wrapper | `XCO2ClimatiZonaB` | | `XDesHumectarAireExteriorClimatiZonaA` | 9088–9176 | Wrapper | `XDesHumectarAireExteriorClimatiZonaB` | | `XDesHumectarBateriaCalorClimatiZonaA` | 9269–9351 | Wrapper | `XDesHumectarBateriaCalorClimatiZonaB` | | `XDesHumectarBateriaFrioClimatiZonaA` | 9437–9513 | Wrapper | `XDesHumectarBateriaFrioClimatiZonaB` | | `XEvapoTemperaturaClimatiZonaA` | 9594–9660 | Wrapper | `XEvapoTemperaturaClimatiZonaB` | | `XFreecoolingTemperaturaClimatiZonaA` | 9731–9822 | Wrapper | `XFreecoolingTemperaturaClimatiZonaB` | | `XHumectarClimatiZonaA` | 9918–9964 | Wrapper | `XHumectarClimatiZonaB` | | `XTemperaturaClimatiZonaA` | 10016–10086 | Wrapper | `XTemperaturaClimatiZonaB` | | `calcularFechaFinal` | 10161–10382 | Utilidad fecha | — (compartida) | ### 18.2. Apéndice B — Inventario de programas | Programa | Líneas (src) | Frecuencia recomendada | |----------|--------------|-------------------------| | `CalculoEntalpiaZonaA` | 2–633 | Cada scan | | `CalculoEntalpiaZonaB` | 636–1146 | Cada scan | | `CalculoTemperaturasZonaA` | 1149–2087 | Cada scan | | `CalculoTemperaturasZonaB` | 2090–3018 | Cada scan | | `ColoresCirculosCentralesZonaA` | 3021–3750 | Cada scan o lento (HMI) | | `ColoresCirculosCentralesZonaB` | 3753–4478 | Cada scan o lento (HMI) | | `ControlFechaFinalSecuencialesZonaA` | 4481–4829 | Cada scan | | `ControlFechaFinalSecuencialesZonaB` | 4832–5177 | Cada scan | | `ControlFechaFinalTemporalesZonaA` | 5180–5464 | Cada scan | | `ControlFechaFinalTemporalesZonaB` | 5467–5748 | Cada scan | | `ControlRiegoZonaA` | 5751–6907 | Cada scan | | `ControlRiegoZonaB` | 6910–8067 | Cada scan | | `Modbus` | 8070–8365 | Cada scan (con timer interno 50 ms) | | `ModbusPrueba1` | 8368–8493 | (auxiliar pruebas) | | `RelojyColoresChivatosZonaA` | 8496–9013 | Cada scan (animaciones HMI) | | `RelojyColoresChivatosZonaB` | 9016–9117 | Cada scan | ### 18.3. Apéndice C — Glosario de variables globales críticas #### C.1. Consignas activas (lo que controla el PID) | Variable | Tipo | Unidad | |----------|------|--------| | `g_rTemperaturaAireCTR1` | REAL | °C | | `g_rTemperaturaCompoCTR1` | REAL | °C | | `g_iHumedadCTR1` | REAL | % HR | | `g_iCO2CTR1` | INT | ppm | | `g_iVentilacionCTR1` | INT | 0/1 (habilitación) | | `g_iCompuertaCTR1` | INT | 0/1 (habilitación humectar) | #### C.2. Variables de proceso (lo que leen las sondas) | Variable | Tipo | Unidad | |----------|------|--------| | `g_rTemperaturaAire1` | REAL | °C | | `g_rTemperaturaCompos1` | REAL | °C | | `g_iHumedad1` | REAL | % HR | | `g_iCO21` | INT | ppm | | `g_rTemperaturaExterior` | REAL | °C | | `g_iHumedadExterior` | INT | % HR | | `g_iCO2Exterior` | INT | ppm | | `g_rTempImpulsion1` | REAL | °C | | `g_rTempMezcla1` | REAL | °C | #### C.3. Salidas a actuadores | Variable | Tipo | Unidad | Destino | |----------|------|--------|---------| | `g_iFrio` | INT | 0..100 % | Batería frío (Modbus HR[1]) | | `g_iCalor` | INT | 0..100 % | Batería calor (Modbus HR[0]) | | `g_iCompuerta1` | INT | 0..100 % | Servomotor compuerta (Modbus HR[2]) | | `g_iVentilacion1` | INT | 0..100 % | Variador ventilador (Modbus HR[4]) | | `g_iHumectar1` | INT | 0..100 % | Humectador analógico | | `g_bValorVentilador1` | BOOL | ON/OFF | Relé ventilador (Modbus Coil[0]) | | `g_bValorHumectar1` | BOOL | ON/OFF | Relé humectador (Modbus Coil[6]) | | `g_bValorRiego1` | BOOL | ON/OFF | Relé electroválvula riego (Modbus Coil[3]) | | `g_bValorGiroRiego1` | BOOL | ON/OFF | Sentido motor riego (Modbus Coil[1]) | | `g_bValorParoMarchaRiego1` | BOOL | ON/OFF | Paro/marcha motor riego (Modbus Coil[2]) | | `g_iVelocidadRiego1` | INT | 0..100 % | Variador motor riego (Modbus HR[5]) | #### C.4. Variables de estado de proceso | Variable | Tipo | Sentido | |----------|------|---------| | `g_iEtapa1` | INT | Etapa de prioridad (0..16) | | `g_iEtapaEnCurso1` | INT | Etapa anterior, para restaurar | | `g_sNombreProcesoEnCurso1` | STRING[32] | Nombre del proceso activo | | `g_iSecuActivado1` | INT | 1 si hay secuencia activa | | `g_iRegando1` | INT | 1 si hay riego en curso | | `sPrioridades1` | STRING[32] | Texto descriptivo de la Etapa actual | #### C.5. Mensajes de acción (para HMI) Cada lazo expone un string con el estado de la última acción: | Variable | Lazo | |----------|------| | `g_sAccionCT1` | Climatización (calor/frío) | | `g_sAccionCO21` | CO₂ | | `g_sAccionCHH1` | Humectación | | `g_sAccionCHA1` | Deshumectar aire exterior | | `g_sAccionCHF1` | Deshumectar batería frío | | `g_sAccionCHC1` | Deshumectar batería calor | | `g_sAccionCTF1` | Freecooling | | `g_sAccionCTE1` | Evaporativo | Ejemplos de mensajes que el HMI muestra: | String | Significado | |--------|-------------| | `CLIMATIZADOR STANDBYT` | Climatizador parado, dentro de banda muerta | | `CONTROL CLIMATIZADOR: CALOR` | Calentando | | `CONTROL CLIMATIZADOR: FRIO` | Enfriando | | `CLIMATIZADOR: FRIO Y CALOR` | Estado mixto (transitorio) | | `CONTROL CO2 - DISMINUIR:` | CO₂ alto, abriendo compuerta | | `CONTROL CO2 - AUMENTAR` | CO₂ bajo (poco probable en cámaras de cultivo) | | `CONTROL CO2 STANDBYT` | CO₂ dentro de banda | | `HUMECTAR: STANDBYT` | Humectación parada | | `DESHUMECTAR: CLIMATIZADOR CALOR` | Deshumectando con baterías calor | | `AIRE EXTERIOR STANDBYT 0` | Deshumect. aire ext parado | | `CONTROLES PARADOS` | Etapa 0, sala vacía | ### 18.4. Apéndice D — Mapa de archivos en disco ```text /Funciones.st ← 53 FUNCTION (librería de control) /Programas.st ← 16 PROGRAM (orquestadores cíclicos) ``` > Los archivos vienen en codificación **UTF-16 LE con BOM** (típico de FPWIN Pro al exportar a texto). Para procesarlos en herramientas Unix conviene convertir a UTF-8: > > ```bash > iconv -f UTF-16LE -t UTF-8 Funciones.st > Funciones.utf8.st > iconv -f UTF-16LE -t UTF-8 Programas.st > Programas.utf8.st > ``` ### 18.5. Apéndice E — Códigos de color del HMI | Color HEX | Significado típico | |-----------|---------------------| | `#00FF00` Verde | Auto / sonda OK / equipo en marcha | | `#FF0000` Rojo | Manual / sonda KO / fuera de rango / equipo parado | | `#0000FF` Azul oscuro | Manual frío / parpadeo riego | | `#00FFEF` Azul claro | Estado intermedio frío / parpadeo riego | | `#FF5200` Naranja | Aviso / preaviso de alarma | | `#000000` Negro | Alarma activa / parado (frío) | ### 18.6. Apéndice F — Anomalías y mejoras detectadas Resumen de **defectos y mejoras técnicas** identificados durante el análisis. No son críticos para el funcionamiento operativo pero conviene tenerlos en cuenta: | # | Severidad | Ubicación | Descripción | |---|-----------|-----------|-------------| | 1 | Baja | `LecturaDeAlarmasZonaA` | El reset de `g_bValorAlarma1` se hace en cada `else`. Si la primera alarma es TRUE y las siguientes FALSE, queda en FALSE. Mejor acumular con OR. | | 2 | Media | `calcularFechaFinal` | Detección de año bisiesto incorrecta a partir de 2100 (no aplica regla de los 100/400). | | 3 | Media | Comparación de fecha en `ControlFechaFinal*` | Lógica `iA>=AA AND iM>=MM AND iD>=DD AND iH>=HH AND iMi>=MM` falla en cruce de mes/hora. Error práctico: hasta 1 min de retraso. | | 4 | Baja | Bug datos en `calcularFechaFinal` | En el caso `g_iFunMes = 2` con bisiesto = 0, el código resta `20` en vez de `28` al excedente (`g_iDiaAuxiliar := g_iDiaAuxiliar - 20`). Línea 10243. | | 5 | Baja | Autotuning PID | Umbral `AT_Progress ≥ 5` parece muy bajo. Posiblemente se usen parámetros offline y el autotuning se omita. | | 6 | Baja | Escalado CO₂ | El doble `SCALE_INT` con rango intermedio 0..25000 es funcionalmente equivalente a un escalado lineal directo y solo añade overhead. | | 7 | Baja | Comentarios | Errores tipográficos consistentes en comentarios (`tenporal` por `temporal`, `BIDIESTO` por `BISIESTO`, `Compuebra` por `Comprueba`). | | 8 | Baja | Duplicación A/B | El código duplicado A/B es ~4500 líneas. Refactorizable a Function Block parametrizado por zona. Implicaría reescritura mayor; no urgente. | --- ## 19. Índice alfabético de funciones y programas Lista alfabética con la categoría y la sección donde se documenta cada elemento. Total: **16 programas + 53 funciones = 69 elementos**. ### A | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `ActivarProcesosZonaA` | FUNCTION | Gestión de procesos | [10.3](#103-activarprocesoszonaa), [11.11](#1111-activarprocesoszonaa--orquestador-de-consignas) | | `ActivarProcesosZonaB` | FUNCTION | Gestión de procesos | Idem ZonaA | ### C | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `calcularFechaFinal` | FUNCTION | Fechas | [10.4.2](#1042-calcularfechafinal) | | `CalculoEntalpiaZonaA` / `B` | PROGRAM | Lectura sondas + psicrometría | [9.1](#91-calculoentalpiazonaa), [11.1](#111-calculoentalpiazonaa--lectura-de-sondas--cálculo-psicrométrico) | | `CalculoTemperaturasZonaA` / `B` | PROGRAM | Orquestador principal | [9.2](#92-calculotemperaturaszonaa), [11.2](#112-calculotemperaturaszonaa--bucle-principal-de-control) | | `ColoresCirculosCentralesZonaA` / `B` | PROGRAM | HMI / saneamiento consignas | [9.6](#96-colorescirculoscentraleszonaa) | | `ContadorHumectacion` / `B` | FUNCTION | Humectación PWM | [10.4.5](#1045-contadorhumectacionzonaa), [11.8](#118-controlhumectarzonaa--contadorhumectacion--humectación-pwm) | | `ControlCO2ZonaA` / `B` | FUNCTION | Lazo PID CO₂ | [10.2.2](#1022-controlco2zonaa), [11.4](#114-controlco2zonaa--control-de-co-por-compuerta) | | `ControlEvaporativoTemperaturaZonaA` / `B` | FUNCTION | Refrigeración evaporativa | [10.2.7](#1027-controlevaporativotemperaturazonaa), [11.7](#117-controlevaporativotemperaturazonaa--refrigeración-evaporativa) | | `ControlFechaFinalSecuencialesZonaA` / `B` | PROGRAM | Avance de receta | [9.5](#95-controlfechafinalsecuencialeszonaa), [11.12](#1112-controlfechafinalsecuencialeszonaa--avance-de-receta) | | `ControlFechaFinalTemporalesZonaA` / `B` | PROGRAM | Procesos temporales | [9.4](#94-controlfechafinaltemporaleszonaa) | | `ControlFreecoolingHumedadZonaA` / `B` | FUNCTION | Deshumectación por aire ext | [10.2.4](#1024-controlfreecoolinghumedadzonaa), [11.6](#116-controlfreecoolinghumedadzonaa--deshumectación-con-aire-exterior) | | `ControlFreecoolingTemperaturaZonaA` / `B` | FUNCTION | Refrigeración gratuita | [11.5](#115-controlfreecoolingtemperaturazonaa--refrigeración-gratuita) | | `ControlHumectarZonaA` / `B` | FUNCTION | Humectación PID | [10.2.3](#1023-controlhumectarzonaa), [11.8](#118-controlhumectarzonaa--contadorhumectacion--humectación-pwm) | | `ControlHumedadCalorZonaA` / `B` | FUNCTION | Compensación calor durante deshum | [10.2.6](#1026-controlhumedadcalorzonaa) | | `ControlHumedadFrioZonaA` / `B` | FUNCTION | Deshumect. por condensación frío | [10.2.5](#1025-controlhumedadfriozonaa) | | `ControlRiegoAsignarArrayZonaA` / `B` | FUNCTION | Gestión arrays de riego | Sección 14 | | `ControlRiegoLineaDeTextoZonaA` / `B` | FUNCTION | Formato HMI riego | Sección 14 | | `ControlRiegoZonaA` / `B` | PROGRAM | Máquina de estados riego | [9.3](#93-controlriegozonaa), [14](#14-sistema-de-riego) | | `ControlSoloEvaporativoTemperaturaZonaA` / `B` | FUNCTION | Modo solo evaporativo | [10.2.8](#1028-controlsoloevaporativotemperaturazonaa) | | `ControlTemperaturaZonaA` / `B` | FUNCTION | Lazo PID frío/calor | [10.2.1](#1021-controltemperaturazonaa), [11.3](#113-controltemperaturazonaa--lazo-pid-fríocalor) | | `ControlVentilacionZonaA` / `B` | FUNCTION | Ventilación proporcional | [10.2.9](#1029-controlventilacionzonaa), [11.9](#119-controlventilacionzonaa--proporcional-según-compost) | ### F | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `FlancoAlto` | FUNCTION | Clave horaria + huella | [10.4.3](#1043-flancoalto) | ### L | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `LecturaDeAlarmasZonaA` / `B` | FUNCTION | Alarmas digitales | [10.4.4](#1044-lecturadealarmaszonaa), [11.10](#1110-lecturadealarmaszonaa) | ### M | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `Modbus` | PROGRAM | Master Modbus RTU | [9.8](#98-modbus), [11.13](#1113-programa-modbus--comunicación-con-es-remotas), [15](#15-comunicaciones-modbus) | | `ModbusPrueba1` | PROGRAM | Auxiliar pruebas | [9.9](#99-modbusprueba1) | ### P | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `PulsoContadorZonaA` / `B` | FUNCTION | Contador caudalímetro riego | [10.4.6](#1046-pulsocontadorzonaa) | ### R | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `RelojyColoresChivatosZonaA` / `B` | PROGRAM | HMI tiempo real + chivatos | [9.7](#97-relojycoloreschivatoszonaa) | ### T | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `TablaEntalpias` | FUNCTION | Look-up table psicrométrica | [10.4.1](#1041-tablaentalpias), [13](#13-cálculo-psicrométrico) | ### W | Elemento | Tipo | Categoría | Sección | |----------|------|-----------|---------| | `WSimuladorPID` / `WSimuladorPIDB` | FUNCTION | Simulador planta | [10.4.7](#1047-wsimuladorpid--wsimuladorpidb) | ### X (wrappers de despacho) | Elemento | Tipo | Llama a | Sección | |----------|------|---------|---------| | `XCO2ClimatiZonaA` / `B` | FUNCTION | `ControlCO2ZonaA` | [10.1](#101-wrappers-xclimatizonaa) | | `XDesHumectarAireExteriorClimatiZonaA` / `B` | FUNCTION | `ControlFreecoolingHumedadZonaA` | [10.1](#101-wrappers-xclimatizonaa), [11.6](#116-controlfreecoolinghumedadzonaa--deshumectación-con-aire-exterior) | | `XDesHumectarBateriaCalorClimatiZonaA` / `B` | FUNCTION | `ControlHumedadCalorZonaA` | [10.1](#101-wrappers-xclimatizonaa) | | `XDesHumectarBateriaFrioClimatiZonaA` / `B` | FUNCTION | `ControlHumedadFrioZonaA` | [10.1](#101-wrappers-xclimatizonaa) | | `XEvapoTemperaturaClimatiZonaA` / `B` | FUNCTION | `ControlEvaporativoTemperaturaZonaA` | [10.1](#101-wrappers-xclimatizonaa), [11.7](#117-controlevaporativotemperaturazonaa--refrigeración-evaporativa) | | `XFreecoolingTemperaturaClimatiZonaA` / `B` | FUNCTION | `ControlFreecoolingTemperaturaZonaA` | [10.1](#101-wrappers-xclimatizonaa), [11.5](#115-controlfreecoolingtemperaturazonaa--refrigeración-gratuita) | | `XHumectarClimatiZonaA` / `B` | FUNCTION | `ControlHumectarZonaA` | [10.1](#101-wrappers-xclimatizonaa) | | `XTemperaturaClimatiZonaA` / `B` | FUNCTION | `ControlTemperaturaZonaA` | [10.1](#101-wrappers-xclimatizonaa) | --- ## Cierre Esta documentación describe el sistema tal y como aparece en la última versión del código (`Modbus.st` modificado el 11/05/2026). Es **descriptiva**, no normativa: refleja lo que el código hace, no necesariamente lo que el proyecto requiere. Para mantenerla viva conviene: 1. **Cuando se modifique una función**, actualizar la sección 9 correspondiente. 2. **Cuando se cambie la lógica de Etapas**, regenerar la tabla 6.2. 3. **Cuando se añadan/cambien sondas o actuadores**, actualizar las secciones 8.1 (sondas) y 13 (Modbus). 4. **Cuando se añada una nueva alarma**, ampliar las secciones 15.1 y 16.3. Los archivos `.st` son la **fuente única de verdad**. Esta documentación es un mapa, no un sustituto del código.