318 lines
12 KiB
Markdown
318 lines
12 KiB
Markdown
# awl2scl — AWL/STL → SCL Konvertierung
|
||
|
||
Werkzeuge zur (halb-)automatischen Umwandlung der Siemens-AWL/STL-Bausteine
|
||
dieses Projekts nach SCL für den Umstieg auf TIA Portal V20.
|
||
|
||
- **`lib/stlxml2awl.py`** — wandelt TIA-Openness-XML (SimaticML `StatementList`)
|
||
in lesbaren AWL-Text (`.awl`) um und schätzt je Baustein eine Aufwandskategorie
|
||
A/B/C. Aufruf über `bin\xmls2awl.bat`.
|
||
- **`lib/awl2scl.py`** — wandelt `.awl`-Dateien mechanisch nach `.scl` um, optional
|
||
mit Lesbarkeits- und Parameter-Optimierung. Aufruf über `bin\awl2scl.bat` bzw.
|
||
`bin\awl2scl_deep.bat`.
|
||
- **`lib/sclopt.py`** — SCL-Zwischenstruktur (IR) und die `--improve-gotos`-Durchläufe.
|
||
- **`lib/paramize.py`** — die `--globals2params`-Umwandlung samt persistenter
|
||
toChange-Liste für die Aufrufer.
|
||
|
||
> **Wichtig:** Das Werkzeug erzeugt syntaktisch zielgerichteten, mechanisch
|
||
> verhaltensgleichen SCL-Text — **keine** in TIA verifizierte Korrektheit. Die
|
||
> Freigabe (Kompilat in TIA Portal V20, PLCSIM-Vergleich AWL↔SCL) bleibt ein
|
||
> manueller Schritt pro Baustein (siehe `SCL-Export/README.md`).
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
- [Datenfluss](#datenfluss)
|
||
- [Kategorien A/B/C](#kategorien-abc)
|
||
- [`bin`-Skripte](#bin-skripte)
|
||
- [CLI-Optionen](#cli-optionen-libawl2sclpy)
|
||
- [`--improve-gotos`](#--improve-gotos)
|
||
- [`--globals2params`](#--globals2params)
|
||
- [Schutz handgeschriebener Dateien](#schutz-handgeschriebener-dateien)
|
||
- [Log & Ausgaben](#log--ausgaben)
|
||
|
||
## Datenfluss
|
||
|
||
```
|
||
TIA-Openness-XML lib/stlxml2awl.py lib/awl2scl.py
|
||
(SimaticML StatementList) ───────────────► *.awl ───────────────► *.scl
|
||
(bin\xmls2awl.bat) (bin\awl2scl.bat /
|
||
bin\awl2scl_deep.bat)
|
||
```
|
||
|
||
`awl2scl.py` liest **zwei** `.awl`-Formate transparent ein:
|
||
|
||
1. **gerendert** — von `stlxml2awl.py` erzeugt, Kopf `=== FC 181: Name [STL] ===`.
|
||
2. **nativ** — klassische STEP7/TIA-AWL-Quelle, Kopf `FUNCTION_BLOCK "Name"`,
|
||
`BEGIN`/`NETWORK`, deutsche Mnemonik (`U`/`UN`/`O`/`SPA`/`SPB`/`AUF` …).
|
||
|
||
## Kategorien A/B/C
|
||
|
||
Die Kategorie wird je Datei **selbst** bestimmt (nicht aus einem Log übernommen),
|
||
mit denselben Kriterien wie in `stlxml2awl.py` (Namensmuster aus
|
||
`SCL-Export/README.md` + Sprung-/Pointer-Analyse):
|
||
|
||
| Kat. | Bedeutung | Behandlung |
|
||
|------|-----------|------------|
|
||
| **A** | einfache Verknüpfungslogik, mechanisch übersetzbar | wird übersetzt |
|
||
| **B** | Ablaufsteuerung / viele Sprungmarken | nur mit `--category B` |
|
||
| **C** | Pointer-/Register-Indirektzugriffe | nur mit `--category C` |
|
||
|
||
Bausteine, die trotz passender Kategorie ein technisch nicht sicher übersetzbares
|
||
Konstrukt enthalten (Timer/Zähler-Mnemonik, `OPN DB[var]`, registerindirekte
|
||
Operanden, unbekannte Sprünge), werden **nicht** übersetzt, sondern im Log unter
|
||
**„Manuelle Prüfung nötig"** mit Grund gelistet.
|
||
|
||
## `bin`-Skripte
|
||
|
||
| Skript | Zweck |
|
||
|--------|-------|
|
||
| `bin\xmls2awl.bat` | XML → `.awl` (+ Kategorie-Log) |
|
||
| `bin\awl2scl.bat` | `.awl` → `.scl`, **mechanisch** (Argumente durchgereicht) |
|
||
| `bin\awl2scl_deep.bat` | `.awl` → `.scl` **mit** `--improve-gotos --globals2params` |
|
||
|
||
Beispiele:
|
||
|
||
```bat
|
||
REM Nur Kategorie A, mechanisch nach SCL-Export\<Station>\
|
||
bin\awl2scl.bat --dir . --category A
|
||
|
||
REM Tiefenlauf: Kontrollfluss aufräumen + Globale zu Parametern
|
||
bin\awl2scl_deep.bat --dir . --category A
|
||
```
|
||
|
||
## CLI-Optionen (`lib/awl2scl.py`)
|
||
|
||
| Option | Wirkung |
|
||
|--------|---------|
|
||
| `<datei.awl> …` | einzelne Dateien |
|
||
| `--dir <verz>` | rekursiv alle `.awl` |
|
||
| `--category A[,B,C]` | zu übersetzende Kategorien (Default `A`) |
|
||
| `--target-dir <pfad>` | Zielordner (Default `$PV_SCL_EXPORT` bzw. `<Projekt>\SCL-Export`) |
|
||
| `--improve-gotos` | Kontrollfluss aufräumen (verlustfrei) |
|
||
| `--globals2params` | globale Merker → Parameter + toChange für Aufrufer |
|
||
| `--force` | vorhandene **generierte** `.scl` überschreiben |
|
||
| `--tochange <pfad>` | toChange-Liste (Default `<Projekt>\data\globals2params_tochange.json`) |
|
||
| `--log <pfad>` | Log-Datei |
|
||
|
||
`--improve-gotos` und `--globals2params` implizieren `--force` (Regenerierung).
|
||
Der Zielpfad je Baustein ist `<target-dir>\<Station>\<Name>.scl`.
|
||
|
||
---
|
||
|
||
## `--improve-gotos`
|
||
|
||
Räumt den mechanisch erzeugten Kontrollfluss **verlustfrei** auf. Die Übersetzung
|
||
läuft dazu über eine typisierte Zwischenstruktur (IR, `lib/sclopt.py`); ohne den
|
||
Schalter ist die Ausgabe byte-identisch zur rein mechanischen Variante. Durchläufe:
|
||
|
||
- **`merge_adjacent_ifdo`** — aufeinanderfolgende `IF`-Blöcke mit identischer
|
||
Bedingung zusammenfassen (aus dem AWL-Idiom `S x; S y; JC L` entsteht sonst
|
||
dieselbe Bedingung mehrfach).
|
||
- **`guard_skip_to_if`** — `IF c THEN GOTO L; … ; L:` → `IF NOT c THEN … END_IF`.
|
||
- **`synth_case`** / **`synth_case_across_networks`** — Verteilung auf **eine**
|
||
Variable gegen Konstanten → `CASE` (auch über REGION-Grenzen hinweg; die
|
||
REGION-Titel werden zu Branch-Kommentaren).
|
||
- **`drop_unused_labels`** — nicht mehr angesprungene Marken entfernen.
|
||
- **`normalize_bool`** — überflüssige Klammern und `NOT`-Verschachtelungen
|
||
vereinfachen (`NOT (a <> b)` → `a = b`).
|
||
|
||
Nicht sauber auflösbare Sprünge (Rücksprünge, gemeinsame Sammel-Austrittsmarken)
|
||
bleiben als strukturierter `GOTO` erhalten — die Passes greifen nur beim sicher
|
||
erkannten Muster.
|
||
|
||
### Beispiel 1 — `HWtoAMRZielFC` (FC 181): Guard-Sprünge → `CASE`
|
||
|
||
Zwei Netzwerke prüfen dieselbe Variable `"MW190"` gegen Konstanten. Mechanisch:
|
||
|
||
```scl
|
||
REGION Schleuse 3004: (BST 3)
|
||
IF ("MW190" <> 3004) THEN GOTO X3004E; END_IF;
|
||
"M180.6" := TRUE;
|
||
RETURN;
|
||
X3004E: ;
|
||
END_REGION
|
||
|
||
REGION Schleuse 3565: (BST 4)
|
||
IF ("MW190" <> 3565) THEN GOTO X3565E; END_IF;
|
||
"M180.6" := TRUE;
|
||
RETURN;
|
||
X3565E: ;
|
||
END_REGION
|
||
```
|
||
|
||
Mit `--improve-gotos` (Guard→IF + netzwerkübergreifende `CASE`-Synthese):
|
||
|
||
```scl
|
||
REGION CASE "MW190"
|
||
CASE "MW190" OF
|
||
3004: // Schleuse 3004: (BST 3)
|
||
"M180.6" := TRUE;
|
||
3565: // Schleuse 3565: (BST 4)
|
||
"M180.6" := TRUE;
|
||
END_CASE;
|
||
END_REGION
|
||
```
|
||
|
||
### Beispiel 2 — `MBlinken` (FB): mehrfache Bedingung zusammenfassen
|
||
|
||
Das AWL setzt mit *einer* Verknüpfung drei Merker und springt (`R x; R y; R z; SPB L`).
|
||
Mechanisch entsteht viermal dieselbe Bedingung:
|
||
|
||
```scl
|
||
REGION Anzeige bei Error 1-10 1-10 mal blinken lassen
|
||
IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN #hAnzeige := FALSE; END_IF;
|
||
IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN #Flanke_Ausschalttakt := FALSE; END_IF;
|
||
IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN #Kein_Blinken := FALSE; END_IF;
|
||
IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN GOTO NoAn; END_IF;
|
||
```
|
||
|
||
Mit `--improve-gotos` (`merge_adjacent_ifdo` + Klammer-Normalisierung):
|
||
|
||
```scl
|
||
REGION Anzeige bei Error 1-10 1-10 mal blinken lassen
|
||
IF ((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_9)) AND NOT (#Error_10) THEN
|
||
#hAnzeige := FALSE;
|
||
#Flanke_Ausschalttakt := FALSE;
|
||
#Kein_Blinken := FALSE;
|
||
GOTO NoAn;
|
||
END_IF;
|
||
```
|
||
|
||
(Das verbleibende `GOTO NoAn` zeigt auf eine gemeinsame Austrittsmarke und bleibt
|
||
bewusst als strukturierter Sprung erhalten.)
|
||
|
||
---
|
||
|
||
## `--globals2params`
|
||
|
||
Baut **globale Merker-Zugriffe** eines Bausteins in echte Parameter um und
|
||
verwaltet eine persistente **toChange-Liste** (`data\globals2params_tochange.json`)
|
||
für die Anpassung der Aufrufer — auch solcher, die noch als `.awl` (nicht
|
||
konvertiert) vorliegen.
|
||
|
||
**Ablauf je Baustein** (`lib/paramize.py`):
|
||
|
||
1. **Richtungsanalyse:** jedes Global (`"MW190"`, `"M180.6"`, `Mx.y`, `Ex`, `Ax` …)
|
||
wird als `Input` (nur gelesen), `Output` (nur geschrieben) oder `InOut` (beides,
|
||
bzw. uneindeutig — konservativ) eingestuft.
|
||
2. **Parametername** deterministisch aus dem Global-Token (`"MW190"`→`MW190`,
|
||
`"M180.6"`→`M180_6`), Kommentar `// war "…"`. Semantische Umbenennung bleibt manuell.
|
||
3. **Rumpf umschreiben:** jeder Global-Zugriff wird `#Param`.
|
||
4. **Aufrufer finden** (direkte `CALL "B"` und indirekte `UC/CC FC[..]`) über **alle**
|
||
`.awl`-Quellen und je Aufrufer den parameterbasierten Aufruf vormerken.
|
||
|
||
**Verhaltenserhaltend by construction:** Jeder Aufrufer forwardet exakt dasselbe
|
||
Global an den Parameter (`Input` mit `:=`, `Output`/`InOut` mit `=>`), solange er
|
||
das Global vor/nach dem Aufruf wie bisher nutzt.
|
||
|
||
**Grenze:** indirekte Aufrufe (`UC/CC FC[var]`, z.B. die CASE-Verteiler in
|
||
`MHMOmniSchleuse`) können nicht automatisch umgeschrieben werden → in toChange als
|
||
`manual` markiert.
|
||
|
||
### Beispiel 1 — `HWtoAMRZielFC`: globale Merker werden Parameter
|
||
|
||
`--globals2params` (hier mit `--improve-gotos` kombiniert) erzeugt aus dem Baustein,
|
||
der `"MW190"` liest und `"M180.6"` schreibt:
|
||
|
||
```scl
|
||
FUNCTION "HWtoAMRZielFC" : Void
|
||
VAR_INPUT
|
||
MW190 : Word; // war "MW190"
|
||
END_VAR
|
||
VAR_OUTPUT
|
||
M180_6 : Bool; // war "M180.6"
|
||
END_VAR
|
||
BEGIN
|
||
REGION CASE #MW190
|
||
CASE #MW190 OF
|
||
3004: // Schleuse 3004: (BST 3)
|
||
#M180_6 := TRUE;
|
||
3565: // Schleuse 3565: (BST 4)
|
||
#M180_6 := TRUE;
|
||
END_CASE;
|
||
END_REGION
|
||
…
|
||
```
|
||
|
||
`HWtoAMRZielFC` wird nur **indirekt** (über `UC FC[..]`-Verteiler) aufgerufen —
|
||
daher ein `manual`-Eintrag in der toChange-Liste:
|
||
|
||
```json
|
||
"=A01+UH07-KF00|DatenIn": {
|
||
"path": "…\\DatenIn.awl",
|
||
"changes": [
|
||
{ "func": "HWtoAMRZielFC",
|
||
"call": "// MANUELL: indirekter Aufruf von HWtoAMRZielFC auf Parameter umstellen",
|
||
"status": "manual" }
|
||
]
|
||
}
|
||
```
|
||
|
||
### Beispiel 2 — direkter Aufrufer wird automatisch umgeschrieben (2-Phasen)
|
||
|
||
Ein Baustein `Sub_Callee`, der `"MW50"` liest und `"M60.0"` schreibt, und ein
|
||
direkter Aufrufer `Top_Caller` (`CALL FC "Sub_Callee"`).
|
||
|
||
**Lauf 1** — `Sub_Callee` wird parametrisiert; der Aufruf-Rewrite wird als
|
||
`pending` in `data\globals2params_tochange.json` vermerkt:
|
||
|
||
```scl
|
||
FUNCTION "Sub_Callee" : Void
|
||
VAR_INPUT
|
||
MW50 : Word; // war "MW50"
|
||
END_VAR
|
||
VAR_OUTPUT
|
||
M60_0 : Bool; // war "M60.0"
|
||
END_VAR
|
||
BEGIN
|
||
REGION test
|
||
#M60_0 := (#MW50 >= 100);
|
||
END_REGION
|
||
```
|
||
|
||
```json
|
||
"_|Top_Caller": {
|
||
"changes": [
|
||
{ "func": "Sub_Callee",
|
||
"call": "\"Sub_Callee\"(M60_0 => \"M60.0\", MW50 := \"MW50\");",
|
||
"status": "pending" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**Lauf 2** — beim Konvertieren von `Top_Caller` wird der vorgemerkte Rewrite
|
||
angewandt (Status `pending` → `applied`); aus dem mechanischen `"Sub_Callee"();`
|
||
wird der parameterbasierte Aufruf:
|
||
|
||
```scl
|
||
REGION ruft callee
|
||
"Sub_Callee"(M60_0 => "M60.0", MW50 := "MW50");
|
||
END_REGION
|
||
```
|
||
|
||
So bleibt die toChange-Liste über mehrere Konvertierungsläufe hinweg gültig, bis
|
||
jeder Aufrufer konvertiert und sein Aufruf angepasst wurde.
|
||
|
||
---
|
||
|
||
## Schutz handgeschriebener Dateien
|
||
|
||
Generierte `.scl` tragen die Kopfzeile
|
||
`// Mechanische AWL->SCL-Konvertierung (awl2scl.py)`. Mit `--force` werden **nur**
|
||
Dateien mit diesem Marker überschrieben; handgeschriebene Piloten (z.B.
|
||
`SCL-Export\=A01+UH06-KF00\MZylinder.scl`, `MHMOmniSchleuse.scl`, `FifoIn.scl`,
|
||
`DatenOut.scl`) bleiben unangetastet und erscheinen im Log unter
|
||
**„Geschützt, handgeschriebene .scl nicht überschrieben"**.
|
||
|
||
## Log & Ausgaben
|
||
|
||
Jeder Lauf schreibt `log\awl2scl_<Zeitstempel>.log` mit:
|
||
|
||
- Kopfstatistik (angeforderte Kategorien, aktive Schalter, Zähler),
|
||
- **Manuelle Prüfung nötig** (Baustein + Grund),
|
||
- **Parametrisiert** (neue Parameter je Baustein + Anzahl Aufrufer/davon manuell),
|
||
- **GOTO-lastig** (viele Sprünge, nur ohne `--improve-gotos`),
|
||
- **Geschützt** / **Übersprungen** (Kategorie bzw. Ziel existiert),
|
||
- **Fehler**.
|
||
|
||
Bei `--globals2params` zusätzlich der Pfad der toChange-Liste.
|