UNO любой ценой!

Секрет транспилера из 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 языках это называлось "функциональный блок". Что мы делали?

  1. создавали объект типа FB (FAKE).

  2. создавали его экземпляр (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.

Замечания:

  1. созданные .c и .h файлы в папке build проекта
  2. в cmd, powershell команда ie2c работает частично
  3. 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артинки ниже:

симуляция:

реальность:

Проект здесь

Ссылки

  1. http://codesys.ru/docs/st_c.pdf
  2. https://stackoverflow.com/questions/1088290/define-statements-within-a-namespace
  3. https://www.youtube.com/watch?v=Lw1WrubK5fk