Секрет транспилера из C++ в ST замечательно раскрыт в заметке И. Петрова, "Язык ST для C программиста"[1]:
Функции: Память для переменных функции выделяется в стеке. Естественно, значения внутренних переменных не сохраняются между вызовами. Объявления локальных static переменных в функциях не предусмотрено. Для функций и функциональных блоков допускается передача параметров по ссылке (VAR_IN_OUT).
Функциональные блоки: ..В отличие от функции, значения переменных экземпляра функционального блока сохраняются между его вызовами.
Или, в переводе на русский: Функциональный блок это класс, в котором переменные VAR_*
кроме LOCAL
объявлены как public
переменные класса, а VAR_LOCAL
, как private. Локальных же переменных в методах быть не должно.
Кроме того, как остроумно подметил Soo Wei Tan в [2]
The #define statement is not restricted to the namespace:
namespace MyNamespace { #define SOME_VALUE 0xDEADBABE }
The following the "correct" thing to do:
namespace MyNamespace { const unsigned int SOME_VALUE = 0xDEADBABE; }
Проще говоря, избегайте #define
из-за высокой вероятности конфликта имен.
На первых порах PLC-прошивки для Arduino Uno удобнее всего писать на C++ с оглядкой на "дословный" перевод на ST. Автоматическая генерация кода из .st в .hex часто приводит к нерабочему коду, как на симуляторе (simulIDE), так и на реальном устройстве. Все дело в исполнении кода c помощью runtime (openPLC, Snek Python, etc..) в то время как Pascal (предок ST), так и C++ -- языки вполне компилируемые: ("простая задача -- короткий код") и никаких проблем с переполнением памяти до поры до времени не возникает в принципе.
В среде Arduino IDE главное вот это:
Учимся на примерах:
Чтобы познакомиться с языком ST поближе, стоит обратить внимание на перевод "вообще всего" в ST в IDE openPLC
вот здесь:
а из ST вообще "во все, что можно вообразить" (в рамках IEC61131-3) в античной версии CodeSys v.2.3 вот здесь:
Первый пример -- здравствуй мир:
Пишем, строим прошивку:
Строим схему для связи по последовательному порту:
Грузим прошивку, созданную в предыдущем пункте в Arduino:
Первый раз в первый class
На PLC языках это называлось "функциональный блок". Что мы делали?
создавали объект типа FB (FAKE).
создавали его экземпляр (FAKE0):
На ST это будет выглядеть так:
FUNCTION_BLOCK FAKE
VAR_INPUT
A : BOOL;
B : BOOL;
SET : BOOL;
RESET : BOOL;
DATA : BOOL;
CLOCK : BOOL;
TOGGLE : BOOL;
END_VAR
VAR_OUTPUT
A_AND_B : BOOL;
A_OR_B : BOOL;
A_XOR_B : BOOL;
Q_SR : BOOL;
Q_D : BOOL;
Q_T : BOOL;
END_VAR
VAR
TM1 : BOOL;
TM2 : BOOL;
END_VAR
A_AND_B := B AND A;
A_OR_B := A OR B;
A_XOR_B := B AND NOT(A) OR NOT(B) AND A;
Q_SR := NOT(RESET) AND (Q_SR OR SET);
Q_D := CLOCK AND DATA OR NOT(CLOCK) AND Q_D;
TM1 := NOT(TM2) AND TOGGLE;
TM2 := TOGGLE;
Q_T := NOT(TM1) AND Q_T OR TM1 AND NOT(Q_T);
END_FUNCTION_BLOCK
PROGRAM program0
VAR
A AT %IX0.0 : BOOL;
B AT %IX0.1 : BOOL;
SET AT %IX0.2 : BOOL;
RESET AT %IX0.3 : BOOL;
DATA AT %IX0.4 : BOOL;
CLOCK AT %IW0 : INT;
TOGGLE AT %IW1 : INT;
A_AND_B AT %QX0.0 : BOOL;
A_OR_B AT %QX0.1 : BOOL;
A_XOR_B AT %QX0.2 : BOOL;
Q_SR AT %QX0.3 : BOOL;
Q_D AT %QW0 : INT;
Q_T AT %QW1 : INT;
END_VAR
VAR
FAKE0 : FAKE;
_TMP_INT_TO_BOOL25_OUT : BOOL;
_TMP_INT_TO_BOOL24_OUT : BOOL;
_TMP_BOOL_TO_INT23_OUT : INT;
_TMP_MUL21_OUT : INT;
_TMP_BOOL_TO_INT14_OUT : INT;
_TMP_MUL8_OUT : INT;
END_VAR
_TMP_INT_TO_BOOL25_OUT := INT_TO_BOOL(CLOCK);
_TMP_INT_TO_BOOL24_OUT := INT_TO_BOOL(TOGGLE);
FAKE0(
A := A, B := B, SET := SET, RESET := RESET,
DATA := DATA, CLOCK := _TMP_INT_TO_BOOL25_OUT,
TOGGLE := _TMP_INT_TO_BOOL24_OUT
);
A_AND_B := FAKE0.A_AND_B;
A_OR_B := FAKE0.A_OR_B;
A_XOR_B := FAKE0.A_XOR_B;
Q_SR := FAKE0.Q_SR;
_TMP_BOOL_TO_INT23_OUT := BOOL_TO_INT(FAKE0.Q_D);
_TMP_MUL21_OUT := MUL(_TMP_BOOL_TO_INT23_OUT, INT#-1);
Q_D := _TMP_MUL21_OUT;
_TMP_BOOL_TO_INT14_OUT := BOOL_TO_INT(FAKE0.Q_T);
_TMP_MUL8_OUT := MUL(_TMP_BOOL_TO_INT14_OUT, INT#-1);
Q_T := _TMP_MUL8_OUT;
END_PROGRAM
CONFIGURATION Config0
RESOURCE Res0 ON PLC
TASK task0(INTERVAL := T#20ms,PRIORITY := 0);
PROGRAM instance0 WITH task0 : program0;
END_RESOURCE
END_CONFIGURATION
В таком виде прошивка на Arduino Unо не работает (вероятно, переполнение Runtime'ом). Но теперь легко сделать построчный перевод в C++. На первых порах ручками..
file fake.h
#ifndef FAKE_h
#define FAKE_h
class FAKE {
public:
bool A = false;
bool B = false;
bool SET = false;
bool RESET = false;
bool DATA = false;
bool CLOCK = false;
bool TOGGLE = false;
bool A_AND_B = false;
bool A_OR_B = false;
bool A_XOR_B = false;
bool Q_SR = false;
bool Q_D = false;
bool Q_T = false;
private:
bool TM1 = false;
bool TM2 = false;
public:
void run( void);
};
#endif
file fake.cpp
#include "FAKE.h"
void FAKE::run() {
A_AND_B = A && B;
A_OR_B = A || B;
A_XOR_B = (A != B);
Q_SR = !RESET && (Q_SR || SET);
Q_D = CLOCK && DATA || !CLOCK && Q_D;
TM1 = !TM2 && TOGGLE;
TM2 = TOGGLE;
Q_T = !TM1 && Q_T || TM1 && !Q_T;
}
step.ino
#include "FAKE.h"
namespace step {
const int _IX0_0 = 2;
const int _IX0_1 = 3;
const int _IX0_2 = 4;
const int _IX0_3 = 5;
const int _IX0_4 = 6;
const int _IW0 = A0;
const int _IW1 = A1;
const int _QX0_0 = 7;
const int _QX0_1 = 8;
const int _QX0_2 = 12;
const int _QX0_3 = 13;
const int _QW0 = 9;
const int _QW1 = 10;
}
bool A = false; // %IX0.0 2
bool B = false; // %IX0.1 3
bool SET = false; // %IX0.2 4
bool RESET = false; // %IX0.3 5
bool DATA = false; // %IX0.4 6
bool CLOCK = false; // %IW0 A0
bool TOGGLE = false; // %IW1 A1
bool A_AND_B = false; // %QX0.0 7
bool A_OR_B = false; // %QX0.1 8
bool A_XOR_B = false; // %QX0.2 12
bool Q_SR = false; // %QX0.3 13
bool Q_D = false; // %QW0 9
bool Q_T = false; // %QW1 10
FAKE FAKE0;
using namespace step;
void setup() {
pinMode(_IX0_0, INPUT);
pinMode(_IX0_1, INPUT);
pinMode(_IX0_2, INPUT);
pinMode(_IX0_3, INPUT);
pinMode(_IX0_4, INPUT);
pinMode(_IW0, INPUT);
pinMode(_IW1, INPUT);
pinMode(_QX0_0, OUTPUT);
pinMode(_QX0_1, OUTPUT);
pinMode(_QX0_2, OUTPUT);
pinMode(_QX0_3, OUTPUT);
pinMode(_QW0, OUTPUT);
pinMode(_QW1, OUTPUT);
// Serial.begin(9600);
}
void loop() {
A = digitalRead(_IX0_0);
B = digitalRead(_IX0_1);
SET = digitalRead(_IX0_2);
RESET = digitalRead(_IX0_3);
DATA = digitalRead(_IX0_4);
CLOCK = digitalRead(_IW0);
TOGGLE = digitalRead(_IW1);
FAKE0.A = A;
FAKE0.B = B;
FAKE0.SET = SET;
FAKE0.RESET = RESET;
FAKE0.DATA = DATA;
FAKE0.CLOCK = CLOCK;
FAKE0.TOGGLE = TOGGLE;
FAKE0.run();
A_AND_B = FAKE0.A_AND_B;
A_OR_B = FAKE0.A_OR_B;
A_XOR_B = FAKE0.A_XOR_B;
Q_SR = FAKE0.Q_SR;
Q_D = FAKE0.Q_D;
Q_T = FAKE0.Q_T;
digitalWrite(_QX0_0, A_AND_B);
digitalWrite(_QX0_1, A_OR_B);
digitalWrite(_QX0_2, A_XOR_B);
digitalWrite(_QX0_3, Q_SR);
digitalWrite(_QW0, Q_D);
digitalWrite(_QW1, Q_T);
delay(100);
}
Исходник "дословного" перевода почти вдвое длиннее. Что ж, хорошего много не бывает, главное что работает.. А суть можно высказать и более лаконично, это если сразу думать на C:
bool A = false; // %IX0.0 2
bool B = false; // %IX0.1 3
bool SET = false; // %IX0.2 4
bool RESET = false; // %IX0.3 5
bool DATA = false; // %IX0.4 6
bool CLOCK = false; // %IW0 A0
bool TOGGLE = false; // %IW1 A1
bool A_AND_B = false; // %QX0.0 7
bool A_OR_B = false; // %QX0.1 8
bool A_XOR_B = false; // %QX0.2 12
bool Q_SR = false; // %QX0.3 13
bool Q_D = false; // %QW0 9
bool Q_T = false; // %QW1 10
bool TM1 = false;
bool TM2 = false;
void setup() {
pinMode(2, INPUT);
pinMode(3, INPUT);
pinMode(4, INPUT);
pinMode(5, INPUT);
pinMode(6, INPUT);
pinMode(A0, INPUT);
pinMode(A1, INPUT);
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
}
void loop() {
A = digitalRead(2);
B = digitalRead(3);
SET = digitalRead(4);
RESET = digitalRead(5);
DATA = digitalRead(6);
CLOCK = digitalRead(A0);
TOGGLE = digitalRead(A1);
A_AND_B = A && B;
A_OR_B = A || B;
A_XOR_B = (A != B);
Q_SR = !RESET && (Q_SR || SET);
Q_D = CLOCK && DATA || !CLOCK && Q_D;
TM1 = !TM2 && TOGGLE;
TM2 = TOGGLE;
Q_T = !TM1 && Q_T || TM1 && !Q_T;
digitalWrite(7, A_AND_B);
digitalWrite(8, A_OR_B);
digitalWrite(12, A_XOR_B);
digitalWrite(13, Q_SR);
digitalWrite(9, Q_D);
digitalWrite(10, Q_T);
delay(10);
}
Да, и чуть не забыл, испытываем вот на такой схеме:
Любой ценой! (UNO+ST)
Обнаружив наутро обновление openPLC
до версии 2023-04-14 возникло желание покопаться еще, чтобы обойтись и вовсе без C++-ного кода.
Схема в SimulIDE не изменилась, можно шить вчерашние C-шные прошивки. Так зачем был нужен C++? Для танца нужна партнерша. В случае "танцев с бубном" это не бубен, а дух (сравнил размеры прошивок Beremiz-baremetal (9K) и Arduino IDE (5K с классом, 7K без) и понял, что дело тут не в memory overflow):)
На последок ценная строчка из лог-окна openPLC, (видно, только когда лесенка с ошибкой, и куда только раньше глядел?)
Start build in C:\src\step\build
Generating SoftPLC IEC-61131 ST/IL/SFC code...
Collecting data types
Collecting POUs
Generate POU program0
Generate POU FAKE
Generate Config(s)
Compiling IEC Program into C code...
.\iec2c.exe
-f -l -p -I "C:\Users\alien\OpenPLC_Editor\matiec\lib"
-T "C:\target\step\build"
"C:\src\step\build\plc.st"
Extracting Located Variables...
C code generated successfully.
PLC :
[CC] plc_main.c -> plc_main.o
[CC] plc_debugger.c -> plc_debugger.o
py_ext :
[CC] py_ext.c -> py_ext.o
PLC :
[CC] Config0.c -> Config0.o
[CC] Res0.c -> Res0.o
Linking :
[CC] plc_main.o plc_debugger.o py_ext.o \
Config0.o Res0.o -> step.dll
Successfully built.
Замечания:
.c
и .h
файлы в папке build
проектаcmd
, powershell
команда ie2c работает частичноplc.st
= step.st
+ (что-то про Питона)все здесь:
Но на этом ничего не закончилось
Посмотрев на ютубе как лихо chatGPT пишет прошивки для Arduino
https://www.youtube.com/watch?v=Lw1WrubK5fk
меня снова потянуло в ST. И тут я столкнулся с главным сюрпризом: openPLC ST это не ST, теперь стало понятно, откуда там появилась идея автогенерации "красивого" кода:
VAR
FAKE0 : FAKE;
_TMP_INT_TO_BOOL25_OUT : BOOL;
_TMP_INT_TO_BOOL24_OUT : BOOL;
_TMP_BOOL_TO_INT23_OUT : INT;
_TMP_MUL21_OUT : INT;
_TMP_BOOL_TO_INT14_OUT : INT;
_TMP_MUL8_OUT : INT;
END_VAR
_TMP_INT_TO_BOOL25_OUT := INT_TO_BOOL(CLOCK);
_TMP_INT_TO_BOOL24_OUT := INT_TO_BOOL(TOGGLE);
..
ну и дальше в том же духе. Оказывается двойной вызов, типа
y = f(g(x))
не работает. То есть никто не ругается, просто молча выдается нерабочая прошивка. Поняв это движение, освоить новый танец было уже не сложно. Следующий пример работает как на SimulIDE
так и на реальной Arduino UNO R3
и является адаптацией SR-D-T
части примера STEP
под популярный некогда Multifunctional Shield. Kартинки ниже:
симуляция:
реальность:
Проект здесь
Ссылки