Victor Nóvoa's theatrical show "Macuco" directed by Lubi Marques needed a boat sail structure that could be remotely controlled in the backstage or theater booth. A DIGNA theater company already had the three -phase engine Weg W22. Initially, we forwarded the solution to use a VFD Schneider ATV12 with stop buttons, starting, steering reversal and velocity control via potentiometer. But this has not yet met the need for organic movements and accuracy in speed adjustment.
The final solution was to use a DMX light table, and create an interface with 4 channels (ON/OFF, Reverse, Speed Integer, Speed Tenths) to access the phase inverter through the modbus.
The two versions of the code differ only in modularity and the ability to configure other vfd models equivalent to the one we used in the show. I adapted the final version, including a library with the commands and registers for six different vfd models. It's possible to include more models in the library while maintaining compatibility.
The system algorithm is as follows: one of the RS485 adapters receives the DMX signal and interprets the four channels, mapping the values of each. The second RS485 adapter provides the complete MODBUS connection to the VFD. It can receive system status, speed, direction, and errors. At the same time, we can send commands and activate the phase inverter status.
To meet a request from lighting technicians, I added a configuration system for the first DMX channel to be used by the system. Later, to meet the request of a fellow technician who owned a phase inverter from another manufacturer, I included the configuration for models similar to the one we're using in the project.
I also changed the DMX and rotary encoder interfacing libraries to meet compatibility with Arduino Mega and ESP32.
/* ARDUINO MEGA VERSION 1.0 */
#include <DMXSerial.h> // Arduino Mega compatible
#include <ModbusMaster.h>
#include <Wire.h>
#include <Adafruit_GFX.h> // Oled display I2C
#include <Adafruit_SSD1306.h> // Oled display I2C
#include <EEPROM.h>
#include <Encoder.h> // Encoder Library
#include "logo.h" // Just a logo header
// Oled display setup
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// DMX 4 channel
#define DMX_FIRST_CHANNEL 1
#define DMX_ENABLE_CHANNEL (DMX_FIRST_CHANNEL + 0) // Canal 1 - STOP/RUN
#define DMX_DIR_CHANNEL (DMX_FIRST_CHANNEL + 1) // Canal 2 - Direção
#define DMX_SPEED_CHANNEL (DMX_FIRST_CHANNEL + 2) // Canal 3 - Velocidade (0-60)
#define DMX_TENTHS_CHANNEL (DMX_FIRST_CHANNEL + 3) // Canal 4 - Décimos (0.0-0.9)
// Rotary Encoder
#define VCC_ENCODER 15
#define GND_ENCODER 14
#define DT_ENCODER 13
#define CLK_ENCODER 12
#define SW_ENCODER 11
#define ENCODER_STEPS_PER_NOTCH 4
#define ENCODER_DEBOUNCE_MS 5
// ModbusMaster
ModbusMaster node;
// Encoder rotativo (CLK, DT)
Encoder encoder(CLK_ENCODER, DT_ENCODER);
// DMX variables
uint8_t dmxEnable = 0; // Canal 1 - STOP/RUN
uint8_t dmxDirection = 0; // Canal 2 - Direção
uint8_t dmxSpeed = 0; // Canal 3 - Velocidade (0-60)
uint8_t dmxTenths = 0; // Canal 4 - Décimos (0.0-0.9)
uint8_t lastDmxEnable = 0;
uint8_t lastDmxDirection = 0;
uint8_t lastDmxSpeed = 0;
uint8_t lastDmxTenths = 0;
// Motor Variables
uint16_t motorSpeed = 0; // MODBUS (MB_ACTUAL_SPEED_REG)
uint16_t motorDirection = 0;
uint16_t motorStatus = 0;
uint16_t motorErrors = 0;
// Motor Limits
uint16_t minFrequency = 0; // LSP (Low speed) - valor inicial
uint16_t maxFrequency = 600; // tFr (Max frequency) - valor inicial (60.0Hz)
uint16_t highSpeed = 600; // HSP (High speed) - valor inicial (60.0Hz)
// Config/Running
bool configMode = false;
// EEPROM
#define EEPROM_INIT_FLAG_ADDR 0
#define EEPROM_DMX_CHANNEL_ADDR 1
int16_t dmxBaseChannel = 1;
long lastEncoderPosition = 0;
bool buttonPressed = false;
unsigned long lastEncoderCheck = 0;
// =============================================
// Modbus Commands for different manufacturers and models
// (SCHNEIDER_ATV12, SIEMENS_V20, WEG_CFW500,
// ABB_ACS550, DANFOSS_FC302, YASKAWA_V1000)
// =============================================
#define SCHNEIDER_ATV12
#ifdef SCHNEIDER_ATV12
// ==================== SCHNEIDER ATV12 (ORIGINAL COMPLETO) ====================
#define MB_CMD_REG 8501
#define MB_SPEED_REG 8602
#define MB_ACTUAL_SPEED_REG 8604
#define MB_STATUS_REG 3201
#define MB_ERRORS_REG 7200
#define MB_MAX_FREQ_REG 3103
#define MB_MIN_FREQ_REG 3105
#define MB_HIGH_SPEED_REG 3104
// Estados
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x0040
#define STATE_READY 0x0021
#define STATE_SWITCHED_ON 0x0033
#define STATE_OPERATION_ENABLED 0x0037
#define STATE_QUICK_STOP 0x0017
#define STATE_FAULT 0x0008
// Comandos
#define CMD_SHUTDOWN 0x0006
#define CMD_SWITCH_ON 0x0007
#define CMD_ENABLE_OPERATION 0x000F
#define CMD_DISABLE_OPERATION 0x0007
#define CMD_DISABLE_VOLTAGE 0x0000
#define CMD_RESET_FAULT 0x0080
#define CMD_RUN_FORWARD 0x000F
#define CMD_RUN_REVERSE 0x080F
#elif defined(SIEMENS_V20)
// ==================== SIEMENS SINAMICS V20 (COMPLETO) ====================
#define MB_CMD_REG 0x047E
#define MB_SPEED_REG 0x047F
#define MB_ACTUAL_SPEED_REG 0x0481
#define MB_STATUS_REG 0x0402
#define MB_ERRORS_REG 0x0500
#define MB_MAX_FREQ_REG 0x0600
#define MB_MIN_FREQ_REG 0x0601
// Estados
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x0400
#define STATE_READY 0x0400
#define STATE_SWITCHED_ON 0x0421
#define STATE_OPERATION_ENABLED 0x0437
#define STATE_QUICK_STOP 0x0417
#define STATE_FAULT 0x0800
// Comandos
#define CMD_SHUTDOWN 0x0406
#define CMD_SWITCH_ON 0x0407
#define CMD_ENABLE_OPERATION 0x047E
#define CMD_DISABLE_OPERATION 0x0407
#define CMD_DISABLE_VOLTAGE 0x0400
#define CMD_RESET_FAULT 0x0C80
#define CMD_RUN_FORWARD 0x047E
#define CMD_RUN_REVERSE 0x0C7E
#elif defined(WEG_CFW500)
// ==================== WEG CFW500 (COMPLETO) ====================
#define MB_CMD_REG 0x2000
#define MB_SPEED_REG 0x2001
#define MB_ACTUAL_SPEED_REG 0x3001
#define MB_STATUS_REG 0x1000
#define MB_ERRORS_REG 0x1001
#define MB_MAX_FREQ_REG 0x3002
#define MB_MIN_FREQ_REG 0x3003
// Estados
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x1000
#define STATE_READY 0x1000
#define STATE_SWITCHED_ON 0x2000
#define STATE_OPERATION_ENABLED 0x2000
#define STATE_QUICK_STOP 0x3000
#define STATE_FAULT 0x8000
// Comandos
#define CMD_SHUTDOWN 0x0006
#define CMD_SWITCH_ON 0x0007
#define CMD_ENABLE_OPERATION 0x000F
#define CMD_DISABLE_OPERATION 0x0007
#define CMD_DISABLE_VOLTAGE 0x0000
#define CMD_RESET_FAULT 0x0080
#define CMD_RUN_FORWARD 0x0001
#define CMD_RUN_REVERSE 0x0002
#elif defined(ABB_ACS550)
// ==================== ABB ACS550 (COMPLETO) ====================
#define MB_CMD_REG 0x2000
#define MB_SPEED_REG 0x2001
#define MB_ACTUAL_SPEED_REG 0x2002
#define MB_STATUS_REG 0x2100
#define MB_ERRORS_REG 0x2200
#define MB_MAX_FREQ_REG 0x2300
#define MB_MIN_FREQ_REG 0x2301
// Estados
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x2100
#define STATE_READY 0x2100
#define STATE_SWITCHED_ON 0x2101
#define STATE_OPERATION_ENABLED 0x2101
#define STATE_QUICK_STOP 0x2102
#define STATE_FAULT 0x2102
// Comandos
#define CMD_SHUTDOWN 0x0006
#define CMD_SWITCH_ON 0x0007
#define CMD_ENABLE_OPERATION 0x000F
#define CMD_DISABLE_OPERATION 0x0007
#define CMD_DISABLE_VOLTAGE 0x0000
#define CMD_RESET_FAULT 0x0080
#define CMD_RUN_FORWARD 0x0001
#define CMD_RUN_REVERSE 0x0002
#elif defined(DANFOSS_FC302)
// ==================== DANFOSS FC302 (COMPLETO) ====================
#define MB_CMD_REG 0x2000
#define MB_SPEED_REG 0x2001
#define MB_ACTUAL_SPEED_REG 0x2002
#define MB_STATUS_REG 0x3000
#define MB_ERRORS_REG 0x3001
#define MB_MAX_FREQ_REG 0x3002
#define MB_MIN_FREQ_REG 0x3003
// Estados
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x0700
#define STATE_READY 0x0700
#define STATE_SWITCHED_ON 0x0701
#define STATE_OPERATION_ENABLED 0x0701
#define STATE_QUICK_STOP 0x0702
#define STATE_FAULT 0x0703
// Comandos
#define CMD_SHUTDOWN 0x0006
#define CMD_SWITCH_ON 0x0007
#define CMD_ENABLE_OPERATION 0x000F
#define CMD_DISABLE_OPERATION 0x0007
#define CMD_DISABLE_VOLTAGE 0x0000
#define CMD_RESET_FAULT 0x0080
#define CMD_RUN_FORWARD 0x0001
#define CMD_RUN_REVERSE 0x0002
#elif defined(YASKAWA_V1000)
// ==================== YASKAWA V1000 (COMPLETO) ====================
#define MB_CMD_REG 0x0400
#define MB_SPEED_REG 0x0401
#define MB_ACTUAL_SPEED_REG 0x0402
#define MB_STATUS_REG 0x0500
#define MB_ERRORS_REG 0x0501
#define MB_MAX_FREQ_REG 0x0600
#define MB_MIN_FREQ_REG 0x0601
// Estados
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x0040
#define STATE_READY 0x0040
#define STATE_SWITCHED_ON 0x0080
#define STATE_OPERATION_ENABLED 0x0080
#define STATE_QUICK_STOP 0x0100
#define STATE_FAULT 0x8000
// Comandos
#define CMD_SHUTDOWN 0x0006
#define CMD_SWITCH_ON 0x0007
#define CMD_ENABLE_OPERATION 0x000F
#define CMD_DISABLE_OPERATION 0x0007
#define CMD_DISABLE_VOLTAGE 0x0000
#define CMD_RESET_FAULT 0x0080
#define CMD_RUN_FORWARD 0x0002
#define CMD_RUN_REVERSE 0x0004
#else
#error "Defina um fabricante válido: SCHNEIDER_ATV12, SIEMENS_V20, WEG_CFW500, ABB_ACS550, DANFOSS_FC302, YASKAWA_V1000"
#endif
// =============================================
// CONFIGURAÇÕES COMUNS (todos os fabricantes)
// =============================================
#define MODBUS_RESPONSE_TIMEOUT 1000 // ms
#define STATE_TRANSITION_DELAY 500 // ms
#define RETRY_COUNT 3
void setup() {
Serial.begin(115200);
Serial.println("Inicializando hardware...");
pinMode(VCC_ENCODER, OUTPUT);
pinMode(GND_ENCODER, OUTPUT);
digitalWrite(VCC_ENCODER, HIGH);
digitalWrite(GND_ENCODER, LOW);
pinMode(SW_ENCODER, INPUT_PULLUP);
loadEEPROMConfig();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("Falha no display OLED"));
for (;;);
}
display.clearDisplay();
display.drawBitmap(0, 0, interface_logo_bits, interface_logo_width, interface_logo_height, SSD1306_WHITE);
display.display();
delay(2000);
DMXSerial.init(DMXReceiver);
DMXSerial.maxChannel(4); // Atualizado para 4 canais
Serial.println("Interface DMX Inicializada");
Serial2.begin(19200, SERIAL_8E1);
node.begin(1, Serial2);
Serial.println("Checando conexão MODBUS");
checkModbusConnection();
Serial.println("Checando parâmetros MODBUS críticos");
verifyCriticalParameters();
Serial.println("Checando limites de frequência MODBUS");
readFrequencyLimits();
Serial.println("Interface MODBUS inicializada");
readMotorStatus();
}
void loop() {
static unsigned long lastUpdate = 0;
unsigned long currentMillis = millis();
checkEncoder();
if (currentMillis - lastUpdate >= 20) {
if (!configMode && readEncoderButton()) {
enterConfigMode();
lastUpdate = currentMillis;
return;
}
if (configMode && readEncoderButton()) {
saveEEPROMConfig();
exitConfigMode();
lastUpdate = currentMillis;
return;
}
if (!configMode) {
readDMX();
if (dmxChanged()) {
lastDmxEnable = dmxEnable;
lastDmxDirection = dmxDirection;
lastDmxSpeed = dmxSpeed;
lastDmxTenths = dmxTenths;
processDMXCommands();
updateDisplayData();
}
static unsigned long lastStatusRead = 0;
if (currentMillis - lastStatusRead > 500) {
readMotorStatus();
lastStatusRead = currentMillis;
updateDisplayData();
}
}
lastUpdate = currentMillis;
}
}
// =============================================
// FUNÇÕES DO ENCODER ROTATIVO (MANTIDAS)
// =============================================
void checkEncoder() {
static long lastPos = 0;
static unsigned long lastChangeTime = 0;
long newPos = encoder.read();
if (newPos != lastPos && millis() - lastChangeTime > ENCODER_DEBOUNCE_MS) {
long diff = newPos - lastPos;
int increment = (diff > 0) ? 1 : -1;
if (configMode) {
dmxBaseChannel = constrain(dmxBaseChannel + increment, 1, 510);
static int16_t lastDisplayChannel = -1;
if (dmxBaseChannel != lastDisplayChannel) {
display.clearDisplay();
display.setCursor(0, 0);
display.println("Configurando DMX");
display.print("Canal: ");
display.println(dmxBaseChannel);
display.println("Pressione p/ salvar");
display.display();
lastDisplayChannel = dmxBaseChannel;
}
}
lastPos = newPos;
lastChangeTime = millis();
}
}
bool readEncoderButton() {
static bool lastReading = HIGH;
static bool lastStableState = HIGH;
static unsigned long lastDebounceTime = 0;
bool currentReading = digitalRead(SW_ENCODER);
if (currentReading != lastReading) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > 50) {
if (currentReading != lastStableState) {
lastStableState = currentReading;
if (currentReading == LOW) {
return true;
}
}
}
lastReading = currentReading;
return false;
}
void enterConfigMode() {
configMode = true;
encoder.write(0);
lastEncoderPosition = 0;
display.clearDisplay();
display.setCursor(0, 0);
display.println("Modo Config:");
display.println("Gire p/ alterar");
display.println("Pressione p/ salvar");
display.display();
}
void exitConfigMode() {
configMode = false;
display.clearDisplay();
display.setCursor(0, 0);
display.println("Config salva!");
display.display();
delay(800);
display.clearDisplay();
display.setCursor(0, 0);
display.println("Modo Execucao");
display.print("Canal DMX base: ");
display.println(dmxBaseChannel);
display.display();
}
// =============================================
// FUNÇÕES PRINCIPAIS ATUALIZADAS
// =============================================
void readDMX() {
dmxEnable = DMXSerial.read(dmxBaseChannel);
dmxDirection = DMXSerial.read(dmxBaseChannel + 1);
dmxSpeed = DMXSerial.read(dmxBaseChannel + 2);
dmxTenths = DMXSerial.read(dmxBaseChannel + 3);
Serial.print("Lendo DMX - Canais: ");
Serial.print(dmxBaseChannel); Serial.print("="); Serial.print(dmxEnable); Serial.print(", ");
Serial.print(dmxBaseChannel + 1); Serial.print("="); Serial.print(dmxDirection); Serial.print(", ");
Serial.print(dmxBaseChannel + 2); Serial.print("="); Serial.print(dmxSpeed); Serial.print(", ");
Serial.print(dmxBaseChannel + 3); Serial.print("="); Serial.println(dmxTenths);
}
bool dmxChanged() {
return (dmxEnable != lastDmxEnable) ||
(dmxDirection != lastDmxDirection) ||
(dmxSpeed != lastDmxSpeed) ||
(dmxTenths != lastDmxTenths);
}
void processDMXCommands() {
if (!verifyModbusConnection()) {
Serial.println("Erro: Sem comunicação MODBUS - Verifique ATV12");
return;
}
readFrequencyLimits();
bool enableMotor = (dmxEnable >= 128);
uint16_t targetSpeed = 0;
if (enableMotor) {
// Processamento do Canal 3 (velocidade inteira 0-60)
// Divisão em 61 zonas estáveis (0-60) com histerese
static uint8_t lastSpeedValue = 0;
uint8_t newSpeedValue = dmxSpeed * 61 / 256;
// Aplica histerese para evitar oscilações
if (abs(newSpeedValue - lastSpeedValue) >= 1 || dmxSpeed == 0 || dmxSpeed == 255) {
lastSpeedValue = newSpeedValue;
}
uint8_t speedValue = constrain(lastSpeedValue, 0, 60);
// Processamento do Canal 4 (décimos 0-9)
// Divisão em 10 zonas estáveis com histerese
static uint8_t lastTenthsValue = 0;
uint8_t newTenthsValue = dmxTenths * 10 / 256;
// Aplica histerese para evitar oscilações
if (abs(newTenthsValue - lastTenthsValue) >= 1 || dmxTenths == 0 || dmxTenths == 255) {
lastTenthsValue = newTenthsValue;
}
uint8_t tenthsValue = constrain(lastTenthsValue, 0, 9);
// Combinação final para MODBUS
targetSpeed = (speedValue * 10) + tenthsValue;
targetSpeed = constrain(targetSpeed, minFrequency, maxFrequency);
}
bool newDirection = (dmxDirection >= 128);
bool directionChanged = newDirection != (motorDirection == 1);
// Exibição dos valores DMX brutos (sem processamento)
Serial.print("DMX1 (Enable): "); Serial.println(dmxEnable);
Serial.print("DMX2 (Direção): "); Serial.println(dmxDirection);
Serial.print("DMX3 (Velocidade): "); Serial.println(dmxSpeed);
Serial.print("DMX4 (Décimos): "); Serial.println(dmxTenths);
// Exibição do valor processado estável
Serial.print("Velocidade MODBUS: ");
Serial.print(targetSpeed / 10); Serial.print(".");
Serial.print(targetSpeed % 10); Serial.println(" Hz");
// Restante da função permanece igual...
readMotorStatus();
if ((motorStatus & 0x004F) == STATE_FAULT) {
if (!resetFault()) {
Serial.println("Falha: Reset de falha não realizado");
return;
}
delay(1000);
readMotorStatus();
}
if (!setMotorSpeed(targetSpeed)) {
Serial.println("Erro: Velocidade não configurada");
}
if (!enableMotor) {
switch (motorStatus & 0x004F) {
case STATE_OPERATION_ENABLED:
sendCommandWithRetry(CMD_DISABLE_OPERATION);
delay(STATE_TRANSITION_DELAY);
sendCommandWithRetry(CMD_SHUTDOWN);
break;
case STATE_SWITCHED_ON:
sendCommandWithRetry(CMD_SHUTDOWN);
break;
}
return;
}
if (directionChanged && (motorStatus & 0x004F) == STATE_OPERATION_ENABLED) {
sendCommandWithRetry(CMD_DISABLE_OPERATION);
delay(500);
}
if (!setMotorDirection(newDirection)) {
Serial.println("Erro: Direção não configurada");
}
if (enableMotor) {
switch (motorStatus & 0x004F) {
case STATE_SWITCH_DISABLED:
sendCommandWithRetry(CMD_SHUTDOWN);
delay(STATE_TRANSITION_DELAY);
sendCommandWithRetry(CMD_SWITCH_ON);
delay(STATE_TRANSITION_DELAY);
sendCommandWithRetry(newDirection ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
case STATE_READY:
sendCommandWithRetry(CMD_SWITCH_ON);
delay(STATE_TRANSITION_DELAY);
sendCommandWithRetry(newDirection ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
case STATE_SWITCHED_ON:
sendCommandWithRetry(newDirection ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
case STATE_OPERATION_ENABLED:
if (directionChanged) {
sendCommandWithRetry(newDirection ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
}
break;
}
}
}
void updateDisplayData() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
// Cabeçalho com canais DMX
display.setCursor(0, 0);
display.print("DMX"); display.print(dmxBaseChannel); display.println("> STATE");
display.setCursor(90, 0);
display.println(dmxEnable);
display.setCursor(0, 10);
display.print("DMX"); display.print(dmxBaseChannel + 1); display.println("> DIR");
display.setCursor(90, 10);
display.println(dmxDirection);
display.setCursor(0, 20);
display.print("DMX"); display.print(dmxBaseChannel + 2); display.println("> SPEED");
display.setCursor(90, 20);
display.println(dmxSpeed);
display.setCursor(0, 30);
display.print("DMX"); display.print(dmxBaseChannel + 3); display.println("> .DEC");
display.setCursor(90, 30);
display.println(dmxTenths);
// Status do motor (valores lidos do MODBUS)
display.setCursor(0, 45);
display.print("Motor: ");
display.println((motorStatus & 0x004F) == STATE_OPERATION_ENABLED ? "RUN" : "STOP");
display.setCursor(0, 55);
display.print("Speed: ");
// Formata a velocidade lida do MODBUS
float displayedSpeed = motorSpeed / 10.0f;
if (motorDirection == 1) {
displayedSpeed = (motorSpeed > 32767) ?
((int16_t)(65535 - motorSpeed + 1)) / -10.0f :
-displayedSpeed;
display.print("-");
}
display.print(abs(displayedSpeed), 1);
display.println(" Hz");
display.display();
}
// =============================================
// FUNÇÕES MODBUS (MANTIDAS)
// =============================================
bool readModbusRegister(uint16_t reg, uint16_t* value) {
unsigned long startTime = millis();
uint8_t result;
uint8_t attempts = 0;
do {
attempts++;
result = node.readHoldingRegisters(reg, 1);
if (result == node.ku8MBSuccess) {
*value = node.getResponseBuffer(0);
return true;
}
delay(50);
} while (millis() - startTime < MODBUS_RESPONSE_TIMEOUT);
return false;
}
bool writeModbusRegister(uint16_t reg, uint16_t value) {
unsigned long startTime = millis();
uint8_t result;
do {
result = node.writeSingleRegister(reg, value);
if (result == node.ku8MBSuccess) {
return true;
}
delay(50);
} while (millis() - startTime < MODBUS_RESPONSE_TIMEOUT);
return false;
}
void readMotorStatus() {
uint16_t value;
if (readModbusRegister(MB_STATUS_REG, &value)) {
motorStatus = value;
}
if (readModbusRegister(MB_ACTUAL_SPEED_REG, &value)) {
motorSpeed = value;
}
if (readModbusRegister(MB_ERRORS_REG, &value)) {
motorErrors = value;
}
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
bool sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (writeModbusRegister(MB_CMD_REG, command)) {
return true;
}
delay(100);
}
return false;
}
bool setMotorSpeed(uint16_t speed) {
return writeModbusRegister(MB_SPEED_REG, speed);
}
bool setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!readModbusRegister(MB_CMD_REG, ¤tCmd)) {
return false;
}
if (direction) {
currentCmd |= (1 << 11);
} else {
currentCmd &= ~(1 << 11);
}
return writeModbusRegister(MB_CMD_REG, currentCmd);
}
bool resetFault() {
return sendCommandWithRetry(CMD_RESET_FAULT);
}
bool verifyModbusConnection() {
static uint32_t lastCheck = 0;
static bool lastStatus = false;
if (millis() - lastCheck < 2000) return lastStatus;
lastCheck = millis();
uint16_t value;
lastStatus = readModbusRegister(MB_STATUS_REG, &value);
if (!lastStatus) {
node.begin(1, Serial2);
}
return lastStatus;
}
void readFrequencyLimits() {
static unsigned long lastCheck = 0;
const unsigned long checkInterval = 30000;
if (millis() - lastCheck > checkInterval) {
uint16_t newMax, newMin, newHigh;
bool success = true;
success &= readModbusRegister(MB_MAX_FREQ_REG, &newMax);
success &= readModbusRegister(MB_MIN_FREQ_REG, &newMin);
success &= readModbusRegister(MB_HIGH_SPEED_REG, &newHigh);
if (success && newMin < newMax && newHigh <= newMax) {
minFrequency = newMin;
maxFrequency = newMax;
highSpeed = newHigh;
}
lastCheck = millis();
}
}
void loadEEPROMConfig() {
if (EEPROM.read(EEPROM_INIT_FLAG_ADDR) == 1) {
dmxBaseChannel = EEPROM.read(EEPROM_DMX_CHANNEL_ADDR);
} else {
dmxBaseChannel = 1;
EEPROM.write(EEPROM_INIT_FLAG_ADDR, 1);
EEPROM.write(EEPROM_DMX_CHANNEL_ADDR, dmxBaseChannel);
}
}
void saveEEPROMConfig() {
EEPROM.write(EEPROM_DMX_CHANNEL_ADDR, dmxBaseChannel);
}
void checkModbusConnection() {
uint16_t value;
if (readModbusRegister(6001, &value)) {
Serial.print("Endereço MODBUS atual: ");
Serial.println(value);
}
}
void verifyCriticalParameters() {
uint16_t add, tbr, tfo, tto;
if (readModbusRegister(6001, &add) &&
readModbusRegister(6003, &tbr) &&
readModbusRegister(6004, &tfo) &&
readModbusRegister(6005, &tto)) {
if (add != 1 || tbr != 19 || tfo != 1) {
Serial.println("ATENÇÃO: Parâmetros MODBUS incorretos!");
}
}
}
The logo.h file is just Modbus DMX text. However, it can be customized as desired.
The second version allows VFD configuration from the configuration menu. This should meet the needs of those with other types of VFDs or motors.
/* ESP32 TTGO T Display - ino file */
#include <esp_dmx.h> // DMX interface
#include <Preferences.h>
#include <ModbusMaster.h>
#include <ESP32Encoder.h> // Encoder
#include <Wire.h>
#include <TFT_eSPI.h>
#include <SPI.h>
#include "vfd.h" // My Custom VFD Library
TFT_eSPI tft = TFT_eSPI();
#define TFT_BL 4
/*============================= DMX =============================*/
dmx_port_t dmxPort = DMX_NUM_1;
#define DMX_RX1_PIN 21
#define DMX_TX1_PIN 22
#define DMX_FIRST_CHANNEL 1
#define DMX_TOTAL_CHANNELS 4
#define DMX_MAX_CHANNEL (512 - DMX_TOTAL_CHANNELS)
int16_t dmxBaseChannel = DMX_FIRST_CHANNEL;
uint8_t dmxEnable = 0;
uint8_t dmxDirection = 0;
uint8_t dmxSpeed = 0;
uint8_t dmxTenths = 0;
uint8_t lastDmxEnable = 0;
uint8_t lastDmxDirection = 0;
uint8_t lastDmxSpeed = 0;
uint8_t lastDmxTenths = 0;
bool dmxOnline = false;
uint8_t dmxData[DMX_PACKET_SIZE];
/*============================= MODBUS =============================*/
#define MODBUS_RESPONSE_TIMEOUT 2000 // ms
#define STATE_TRANSITION_DELAY 500 // ms
#define MODBUS_TX 15
#define MODBUS_RX 17
ModbusMaster node;
/*============================= ENCODER =============================*/
#define DT_ENCODER 25
#define CLK_ENCODER 26
#define SW_ENCODER 27
#define ENCODER_DEBOUNCE_MS 5
ESP32Encoder encoder;
volatile bool encoderButtonFlag = false;
/*============================= VFD =============================*/
#define SCHNEIDER_ATV12 // Default
// #define SIEMENS_V20
// #define WEG_CFW500
// #define ABB_ACS550
// #define DANFOSS_FC302
// #define YASKAWA_V1000
#ifdef SCHNEIDER_ATV12
SchneiderATV12 vfd(&node, 1);
#elif defined(SIEMENS_V20)
SiemensV20 vfd(&node, 1);
#elif defined(WEG_CFW500)
WEGCFW500 vfd(&node, 1);
#elif defined(ABB_ACS550)
ABBACS550 vfd(&node, 1);
#elif defined(DANFOSS_FC302)
DanfossFC302 vfd(&node, 1);
#elif defined(YASKAWA_V1000)
YaskawaV1000 vfd(&node, 1);
#else
#error "Defina um fabricante válido."
#endif
/*============================== VFD =================================*/
uint16_t getMotorState() {
return vfd.getMotorStatus() & 0x004F;
}
/*============================= CONFIG =============================*/
enum ConfigStep : uint8_t {
IDLE = 0,
SELECT_INVERTER = 1,
SELECT_CHANNEL = 2,
SAVE_AND_REBOOT = 3
};
enum InverterBrand : uint8_t {
INV_SCHNEIDER_ATV12 = 0,
INV_SIEMENS_V20,
INV_WEG_CFW500,
INV_ABB_ACS550,
INV_DANFOSS_FC302,
INV_YASKAWA_V1000,
INV_COUNT
};
const char* brandNames[INV_COUNT] = {
"Schneider ATV12",
"Siemens V20",
"WEG CFW500",
"ABB ACS550",
"Danfoss FC302",
"Yaskawa V1000"
};
InverterBrand inverterBrand = INV_SCHNEIDER_ATV12;
ConfigStep configStep = IDLE;
/*============================= EEPROM =============================*/
Preferences prefs;
/*============================= PROTÓTIPOS =============================*/
void setup();
void loop();
void readDMX();
bool dmxChanged();
void processDMXCommands();
void checkEncoder();
void IRAM_ATTR onEncoderButton();
bool readEncoderButton();
void drawInverterScreen();
void drawChannelScreen();
void enterConfigMode();
void exitConfigMode();
void updateDisplayData();
/*======================================================================*/
/* SETUP */
/*======================================================================*/
void setup() {
Serial.begin(115200);
delay(2000);
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
pinMode(SW_ENCODER, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SW_ENCODER), onEncoderButton, FALLING);
ESP32Encoder::useInternalWeakPullResistors = puType::up;
encoder.attachSingleEdge(CLK_ENCODER, DT_ENCODER);
encoder.clearCount();
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
// Título principal "DMX MODBUS" em destaque
tft.setTextFont(4); // Fonte maior para destaque
tft.setTextColor(TFT_CYAN);
tft.setTextDatum(MC_DATUM); // Centraliza o texto no meio da tela
tft.drawString("DMX MODBUS", tft.width() / 2, 50);
tft.setTextFont(2); // Fonte menor para o subtítulo
tft.setTextColor(TFT_WHITE);
tft.drawString("@nicolaudosbrinquedos", tft.width() / 2, 100);
delay(500);
// EEPROM
prefs.begin("dmx-config", false);
inverterBrand = static_cast<InverterBrand>(prefs.getUChar("inverter", INV_SCHNEIDER_ATV12));
dmxBaseChannel = prefs.getInt("baseChannel", DMX_FIRST_CHANNEL);
prefs.end();
dmxBaseChannel = constrain(dmxBaseChannel, DMX_FIRST_CHANNEL, DMX_MAX_CHANNEL);
// DMX
dmx_config_t cfg = DMX_CONFIG_DEFAULT;
dmx_driver_install(dmxPort, &cfg, NULL, 0);
dmx_set_pin(dmxPort, DMX_TX1_PIN, DMX_RX1_PIN, DMX_PIN_NO_CHANGE);
// MODBUS
Serial2.begin(19200, SERIAL_8E1, MODBUS_TX, MODBUS_RX);
node.begin(1, Serial2);
// VFD
vfd.readFrequencyLimits();
vfd.readMotorStatus();
updateDisplayData();
}
/*======================================================================*/
/* LOOP */
/*======================================================================*/
void loop() {
static unsigned long lastUpdate = 0;
unsigned long now = millis();
checkEncoder();
if (now - lastUpdate >= 20) {
if (configStep == IDLE && readEncoderButton()) {
configStep = SELECT_INVERTER;
encoder.clearCount();
drawInverterScreen();
lastUpdate = now;
return;
}
if (configStep == SELECT_INVERTER && readEncoderButton()) {
configStep = SELECT_CHANNEL;
encoder.clearCount();
encoder.setCount(dmxBaseChannel);
drawChannelScreen();
lastUpdate = now;
return;
}
if (configStep == SELECT_CHANNEL && readEncoderButton()) {
prefs.begin("dmx-config", false);
prefs.putUChar("inverter", inverterBrand);
prefs.putInt("baseChannel", dmxBaseChannel);
prefs.end();
ESP.restart();
lastUpdate = now;
return;
}
if (configStep == IDLE) {
readDMX();
if (dmxChanged()) {
lastDmxEnable = dmxEnable;
lastDmxDirection = dmxDirection;
lastDmxSpeed = dmxSpeed;
lastDmxTenths = dmxTenths;
processDMXCommands();
}
static unsigned long lastStatus = 0;
if (now - lastStatus > 500) {
vfd.readMotorStatus();
lastStatus = now;
}
}
updateDisplayData();
lastUpdate = now;
}
}
/*======================================================================*/
/* FUNÇÕES */
/*======================================================================*/
void readDMX() {
dmx_packet_t packet;
if (dmx_receive(dmxPort, &packet, 0)) {
dmxOnline = (packet.err == DMX_OK);
if (dmxOnline) {
dmx_read(dmxPort, dmxData, packet.size);
int idx = dmxBaseChannel;
if (packet.size >= (idx + DMX_TOTAL_CHANNELS)) {
dmxEnable = dmxData[idx];
dmxDirection = dmxData[idx + 1];
dmxSpeed = dmxData[idx + 2];
dmxTenths = dmxData[idx + 3];
} else {
dmxEnable = dmxDirection = dmxSpeed = dmxTenths = 0;
}
}
}
}
bool dmxChanged() {
return lastDmxEnable != dmxEnable ||
lastDmxDirection != dmxDirection ||
lastDmxSpeed != dmxSpeed ||
lastDmxTenths != dmxTenths;
}
void processDMXCommands() {
bool enableMotor = (dmxEnable >= 128);
uint16_t targetSpeed = 0;
if (enableMotor) {
static uint8_t lastSpeed = 0, lastTenths = 0;
uint8_t speedVal = (dmxSpeed * 61) / 256;
if (abs(speedVal - lastSpeed) >= 1 || dmxSpeed == 0 || dmxSpeed == 255) lastSpeed = speedVal;
uint8_t tenthsVal = (dmxTenths * 10) / 256;
if (abs(tenthsVal - lastTenths) >= 1 || dmxTenths == 0 || dmxTenths == 255) lastTenths = tenthsVal;
targetSpeed = constrain(lastSpeed, 0, 60) * 10 + constrain(lastTenths, 0, 9);
targetSpeed = constrain(targetSpeed, vfd.getMinFrequency(), vfd.getMaxFrequency());
}
bool newDir = (dmxDirection >= 128);
bool dirChanged = newDir != (vfd.getMotorDirection() == 1);
uint16_t st = vfd.getMotorStatus() & 0x004F;
if (st == STATE_FAULT) {
vfd.sendCommandWithRetry(CMD_RESET_FAULT);
delay(1000);
vfd.readMotorStatus();
return;
}
vfd.setMotorSpeed(targetSpeed);
if (!enableMotor) {
switch (st) {
case STATE_OPERATION_ENABLED:
vfd.sendCommandWithRetry(CMD_DISABLE_OPERATION);
delay(STATE_TRANSITION_DELAY);
vfd.sendCommandWithRetry(CMD_SHUTDOWN);
break;
case STATE_SWITCHED_ON:
vfd.sendCommandWithRetry(CMD_SHUTDOWN);
break;
}
return;
}
if (dirChanged && st == STATE_OPERATION_ENABLED) {
vfd.sendCommandWithRetry(CMD_DISABLE_OPERATION);
delay(500);
}
vfd.setMotorDirection(newDir);
switch (st) {
case STATE_SWITCH_DISABLED:
vfd.sendCommandWithRetry(CMD_SHUTDOWN);
delay(STATE_TRANSITION_DELAY);
vfd.sendCommandWithRetry(CMD_SWITCH_ON);
delay(STATE_TRANSITION_DELAY);
vfd.sendCommandWithRetry(newDir ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
case STATE_READY:
vfd.sendCommandWithRetry(CMD_SWITCH_ON);
delay(STATE_TRANSITION_DELAY);
vfd.sendCommandWithRetry(newDir ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
case STATE_SWITCHED_ON:
vfd.sendCommandWithRetry(newDir ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
case STATE_OPERATION_ENABLED:
if (dirChanged) vfd.sendCommandWithRetry(newDir ? CMD_RUN_REVERSE : CMD_RUN_FORWARD);
break;
}
}
/*======================================================================*/
/* ENCODER / CONFIG */
/*======================================================================*/
void IRAM_ATTR onEncoderButton() {
static unsigned long last = 0;
if (millis() - last > 100) {
encoderButtonFlag = true;
last = millis();
}
}
bool readEncoderButton() {
bool ret = encoderButtonFlag;
encoderButtonFlag = false;
return ret;
}
void checkEncoder() {
static long lastPos = 0;
static unsigned long lastChange = 0;
long newPos = encoder.getCount();
if (newPos != lastPos && millis() - lastChange > ENCODER_DEBOUNCE_MS) {
long diff = newPos - lastPos;
int inc = (diff > 0) ? 1 : -1;
if (configStep == SELECT_INVERTER) {
inverterBrand = static_cast<InverterBrand>((inverterBrand + INV_COUNT + inc) % INV_COUNT);
drawInverterScreen();
} else if (configStep == SELECT_CHANNEL) {
dmxBaseChannel = constrain(dmxBaseChannel + inc, DMX_FIRST_CHANNEL, DMX_MAX_CHANNEL);
drawChannelScreen();
}
lastPos = newPos;
lastChange = millis();
}
}
void drawInverterScreen() {
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(2);
tft.setTextColor(TFT_WHITE);
tft.drawString("Choose Inverter", tft.width() / 2, 15);
tft.setTextFont(4);
tft.drawString(brandNames[inverterBrand], tft.width() / 2, 85);
tft.setTextFont(1);
tft.setTextColor(TFT_YELLOW);
tft.drawString("Press to Confirm", tft.width() / 2, 130);
}
void drawChannelScreen() {
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(2);
tft.setTextColor(TFT_WHITE);
tft.drawString("DMX Channel", tft.width() / 2, 15);
tft.setTextFont(7);
tft.setTextColor(TFT_CYAN);
tft.drawNumber(dmxBaseChannel, tft.width() / 2, 85);
tft.setTextFont(1);
tft.setTextColor(TFT_YELLOW);
tft.drawString("Press to Save & Reboot", tft.width() / 2, 130);
}
/*======================================================================*/
/* DISPLAY */
/*======================================================================*/
void updateDisplayData() {
if (configStep != IDLE) return;
tft.fillScreen(TFT_BLACK);
tft.setTextFont(2);
tft.setTextSize(1);
tft.setTextDatum(TL_DATUM);
int xLabel = 5, xValue = 120, xBar = 170;
int barW = 60, barH = 10, yStart = 10, lineH = 15;
const char* labels[4] = {"STATE", "DIR", "SPEED", ".DEC"};
uint8_t vals[4] = {dmxEnable, dmxDirection, dmxSpeed, dmxTenths};
uint16_t colors[4] = {TFT_CYAN, TFT_MAGENTA, TFT_ORANGE, TFT_YELLOW};
for (int i = 0; i < 4; ++i) {
int y = yStart + i * lineH;
tft.setCursor(xLabel, y);
tft.setTextColor(colors[i], TFT_BLACK);
tft.printf("DMX %d > %s >", dmxBaseChannel + i, labels[i]);
tft.setCursor(xValue, y);
tft.printf("%3d", vals[i]);
int len = map(vals[i], 0, 255, 0, barW);
tft.fillRect(xBar, y + 2, len, barH, colors[i]);
tft.drawRect(xBar, y + 2, barW, barH, TFT_DARKGREY);
}
int modY = yStart + 4 * lineH + 10;
float dsp = vfd.getMotorSpeed() / 10.0f;
if (vfd.getMotorDirection() == 1 && vfd.getMotorSpeed() > 32767)
dsp = ((int16_t)(65535 - vfd.getMotorSpeed() + 1)) / -10.0f;
tft.setCursor(xLabel, modY);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.print("Motor: ");
bool running = (vfd.getMotorStatus() & 0x004F) == STATE_OPERATION_ENABLED;
tft.setTextColor(running ? TFT_GREEN : TFT_RED, TFT_BLACK);
tft.println(running ? "ON" : "OFF");
tft.setCursor(xValue, modY);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.printf("Speed: %.1f Hz", abs(dsp));
int r = 4, ySt = modY + 25;
tft.setCursor(xLabel, ySt - 8);
tft.print("DMX");
tft.fillCircle(xLabel + 72, ySt - 2, r, dmxOnline ? TFT_GREEN : TFT_RED);
tft.setCursor(xValue, ySt - 8);
tft.print("MODBUS");
tft.fillCircle(xValue + 72, ySt - 2, r, vfd.getMotorStatus() != 0 ? TFT_GREEN : TFT_RED);
// Inverter status
int inverterY = ySt + 5; // 20px abaixo dos status de DMX e MODBUS
tft.setCursor(xLabel, inverterY);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.print("Inverter:");
tft.setCursor(xLabel + 100, inverterY);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.print(brandNames[inverterBrand]);
}
// =============================================
// FUNÇÕES CONFIGURAÇÕES SALVAS E LIDAS NA EEPROM
// =============================================
void loadEEPROMConfig() {
prefs.begin("dmx-config", false);
inverterBrand = static_cast<InverterBrand>(prefs.getUChar("inverter", INV_SCHNEIDER_ATV12));
dmxBaseChannel = prefs.getInt("baseChannel", DMX_FIRST_CHANNEL);
prefs.end();
}
void saveEEPROMConfig() {
prefs.begin("dmx-config", false);
prefs.putUChar("inverter", inverterBrand);
prefs.putInt("baseChannel", dmxBaseChannel);
prefs.end();
}
Finally, the files concerning the library with the six VFD models on the market, which are compatible with the system we use.
First, vfd.h file:
/* vfd.h */
#ifndef VFD_H
#define VFD_H
#include <ModbusMaster.h>
#define STATE_NOT_READY 0x0000
#define STATE_SWITCH_DISABLED 0x0040
#define STATE_READY 0x0021
#define STATE_SWITCHED_ON 0x0033
#define STATE_OPERATION_ENABLED 0x0037
#define STATE_FAULT 0x0008
#define CMD_SHUTDOWN 0x0006
#define CMD_SWITCH_ON 0x0007
#define CMD_RESET_FAULT 0x0080
#define CMD_DISABLE_OPERATION 0x0007
#define CMD_RUN_FORWARD 0x000F
#define CMD_RUN_REVERSE 0x080F
class VFD {
public:
VFD(ModbusMaster* node, uint8_t address = 1);
virtual bool setMotorSpeed(uint16_t speed) = 0;
virtual bool setMotorDirection(uint16_t direction) = 0;
virtual bool sendCommandWithRetry(uint16_t command) = 0;
virtual void readFrequencyLimits() = 0;
virtual void readMotorStatus() = 0;
uint16_t getMotorSpeed() const { return motorSpeed; }
uint16_t getMotorStatus() const { return motorStatus; }
uint16_t getMotorErrors() const { return motorErrors; }
uint16_t getMotorDirection() const { return motorDirection; }
uint16_t getMinFrequency() const { return minFrequency; }
uint16_t getMaxFrequency() const { return maxFrequency; }
uint16_t getHighSpeed() const { return highSpeed; }
protected:
ModbusMaster* node;
uint8_t address;
uint16_t MB_CMD_REG;
uint16_t MB_SPEED_REG;
uint16_t MB_ACTUAL_SPEED_REG;
uint16_t MB_STATUS_REG;
uint16_t MB_ERRORS_REG;
uint16_t MB_MAX_FREQ_REG;
uint16_t MB_MIN_FREQ_REG;
uint16_t MB_HIGH_SPEED_REG;
uint16_t motorSpeed = 0;
uint16_t motorDirection = 0;
uint16_t motorStatus = 0;
uint16_t motorErrors = 0;
uint16_t minFrequency = 0;
uint16_t maxFrequency = 600;
uint16_t highSpeed = 600;
};
class SchneiderATV12 : public VFD {
public:
SchneiderATV12(ModbusMaster* node, uint8_t address = 1);
bool setMotorSpeed(uint16_t speed) override;
bool setMotorDirection(uint16_t direction) override;
bool sendCommandWithRetry(uint16_t command) override;
void readFrequencyLimits() override;
void readMotorStatus() override;
};
class SiemensV20 : public VFD {
public:
SiemensV20(ModbusMaster* node, uint8_t address = 1);
bool setMotorSpeed(uint16_t speed) override;
bool setMotorDirection(uint16_t direction) override;
bool sendCommandWithRetry(uint16_t command) override;
void readFrequencyLimits() override;
void readMotorStatus() override;
};
class WEGCFW500 : public VFD {
public:
WEGCFW500(ModbusMaster* node, uint8_t address = 1);
bool setMotorSpeed(uint16_t speed) override;
bool setMotorDirection(uint16_t direction) override;
bool sendCommandWithRetry(uint16_t command) override;
void readFrequencyLimits() override;
void readMotorStatus() override;
};
class ABBACS550 : public VFD {
public:
ABBACS550(ModbusMaster* node, uint8_t address = 1);
bool setMotorSpeed(uint16_t speed) override;
bool setMotorDirection(uint16_t direction) override;
bool sendCommandWithRetry(uint16_t command) override;
void readFrequencyLimits() override;
void readMotorStatus() override;
};
class DanfossFC302 : public VFD {
public:
DanfossFC302(ModbusMaster* node, uint8_t address = 1);
bool setMotorSpeed(uint16_t speed) override;
bool setMotorDirection(uint16_t direction) override;
bool sendCommandWithRetry(uint16_t command) override;
void readFrequencyLimits() override;
void readMotorStatus() override;
};
class YaskawaV1000 : public VFD {
public:
YaskawaV1000(ModbusMaster* node, uint8_t address = 1);
bool setMotorSpeed(uint16_t speed) override;
bool setMotorDirection(uint16_t direction) override;
bool sendCommandWithRetry(uint16_t command) override;
void readFrequencyLimits() override;
void readMotorStatus() override;
};
#endif
And finally vfd.cpp file:
#include "vfd.h"
VFD::VFD(ModbusMaster* node, uint8_t address)
: node(node), address(address) {
node->begin(address, Serial2);
}
// ---------- Schneider ATV12 ----------
SchneiderATV12::SchneiderATV12(ModbusMaster* node, uint8_t address)
: VFD(node, address) {
MB_CMD_REG = 8501;
MB_SPEED_REG = 8602;
MB_ACTUAL_SPEED_REG = 8604;
MB_STATUS_REG = 3201;
MB_ERRORS_REG = 7200;
MB_MAX_FREQ_REG = 3103;
MB_MIN_FREQ_REG = 3105;
MB_HIGH_SPEED_REG = 3104;
}
bool SchneiderATV12::setMotorSpeed(uint16_t speed) {
return node->writeSingleRegister(MB_SPEED_REG, speed);
}
bool SchneiderATV12::setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!node->readHoldingRegisters(MB_CMD_REG, 1)) return false;
currentCmd = node->getResponseBuffer(0);
if (direction) currentCmd |= (1 << 11);
else currentCmd &= ~(1 << 11);
return node->writeSingleRegister(MB_CMD_REG, currentCmd);
}
bool SchneiderATV12::sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (node->writeSingleRegister(MB_CMD_REG, command)) return true;
delay(100);
}
return false;
}
void SchneiderATV12::readFrequencyLimits() {
node->readHoldingRegisters(MB_MAX_FREQ_REG, 1);
maxFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_MIN_FREQ_REG, 1);
minFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_HIGH_SPEED_REG, 1);
highSpeed = node->getResponseBuffer(0);
}
void SchneiderATV12::readMotorStatus() {
node->readHoldingRegisters(MB_STATUS_REG, 1);
motorStatus = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ACTUAL_SPEED_REG, 1);
motorSpeed = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ERRORS_REG, 1);
motorErrors = node->getResponseBuffer(0);
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
// ---------- Siemens V20 ----------
SiemensV20::SiemensV20(ModbusMaster* node, uint8_t address)
: VFD(node, address) {
MB_CMD_REG = 0x047E;
MB_SPEED_REG = 0x047F;
MB_ACTUAL_SPEED_REG = 0x0481;
MB_STATUS_REG = 0x0402;
MB_ERRORS_REG = 0x0500;
MB_MAX_FREQ_REG = 0x0600;
MB_MIN_FREQ_REG = 0x0601;
}
bool SiemensV20::setMotorSpeed(uint16_t speed) {
return node->writeSingleRegister(MB_SPEED_REG, speed);
}
bool SiemensV20::setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!node->readHoldingRegisters(MB_CMD_REG, 1)) return false;
currentCmd = node->getResponseBuffer(0);
if (direction) currentCmd |= (1 << 11);
else currentCmd &= ~(1 << 11);
return node->writeSingleRegister(MB_CMD_REG, currentCmd);
}
bool SiemensV20::sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (node->writeSingleRegister(MB_CMD_REG, command)) return true;
delay(100);
}
return false;
}
void SiemensV20::readFrequencyLimits() {
node->readHoldingRegisters(MB_MAX_FREQ_REG, 1);
maxFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_MIN_FREQ_REG, 1);
minFrequency = node->getResponseBuffer(0);
}
void SiemensV20::readMotorStatus() {
node->readHoldingRegisters(MB_STATUS_REG, 1);
motorStatus = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ACTUAL_SPEED_REG, 1);
motorSpeed = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ERRORS_REG, 1);
motorErrors = node->getResponseBuffer(0);
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
// ---------- WEG CFW500 ----------
WEGCFW500::WEGCFW500(ModbusMaster* node, uint8_t address)
: VFD(node, address) {
MB_CMD_REG = 0x2000;
MB_SPEED_REG = 0x2001;
MB_ACTUAL_SPEED_REG = 0x3001;
MB_STATUS_REG = 0x1000;
MB_ERRORS_REG = 0x1001;
MB_MAX_FREQ_REG = 0x3002;
MB_MIN_FREQ_REG = 0x3003;
}
bool WEGCFW500::setMotorSpeed(uint16_t speed) {
return node->writeSingleRegister(MB_SPEED_REG, speed);
}
bool WEGCFW500::setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!node->readHoldingRegisters(MB_CMD_REG, 1)) return false;
currentCmd = node->getResponseBuffer(0);
if (direction) currentCmd |= (1 << 11);
else currentCmd &= ~(1 << 11);
return node->writeSingleRegister(MB_CMD_REG, currentCmd);
}
bool WEGCFW500::sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (node->writeSingleRegister(MB_CMD_REG, command)) return true;
delay(100);
}
return false;
}
void WEGCFW500::readFrequencyLimits() {
node->readHoldingRegisters(MB_MAX_FREQ_REG, 1);
maxFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_MIN_FREQ_REG, 1);
minFrequency = node->getResponseBuffer(0);
}
void WEGCFW500::readMotorStatus() {
node->readHoldingRegisters(MB_STATUS_REG, 1);
motorStatus = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ACTUAL_SPEED_REG, 1);
motorSpeed = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ERRORS_REG, 1);
motorErrors = node->getResponseBuffer(0);
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
// ---------- ABB ACS550 ----------
ABBACS550::ABBACS550(ModbusMaster* node, uint8_t address)
: VFD(node, address) {
MB_CMD_REG = 0x2000;
MB_SPEED_REG = 0x2001;
MB_ACTUAL_SPEED_REG = 0x2002;
MB_STATUS_REG = 0x2100;
MB_ERRORS_REG = 0x2200;
MB_MAX_FREQ_REG = 0x2300;
MB_MIN_FREQ_REG = 0x2301;
}
bool ABBACS550::setMotorSpeed(uint16_t speed) {
return node->writeSingleRegister(MB_SPEED_REG, speed);
}
bool ABBACS550::setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!node->readHoldingRegisters(MB_CMD_REG, 1)) return false;
currentCmd = node->getResponseBuffer(0);
if (direction) currentCmd |= (1 << 11);
else currentCmd &= ~(1 << 11);
return node->writeSingleRegister(MB_CMD_REG, currentCmd);
}
bool ABBACS550::sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (node->writeSingleRegister(MB_CMD_REG, command)) return true;
delay(100);
}
return false;
}
void ABBACS550::readFrequencyLimits() {
node->readHoldingRegisters(MB_MAX_FREQ_REG, 1);
maxFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_MIN_FREQ_REG, 1);
minFrequency = node->getResponseBuffer(0);
}
void ABBACS550::readMotorStatus() {
node->readHoldingRegisters(MB_STATUS_REG, 1);
motorStatus = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ACTUAL_SPEED_REG, 1);
motorSpeed = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ERRORS_REG, 1);
motorErrors = node->getResponseBuffer(0);
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
// ---------- Danfoss FC302 ----------
DanfossFC302::DanfossFC302(ModbusMaster* node, uint8_t address)
: VFD(node, address) {
MB_CMD_REG = 0x2000;
MB_SPEED_REG = 0x2001;
MB_ACTUAL_SPEED_REG = 0x2002;
MB_STATUS_REG = 0x3000;
MB_ERRORS_REG = 0x3001;
MB_MAX_FREQ_REG = 0x3002;
MB_MIN_FREQ_REG = 0x3003;
}
bool DanfossFC302::setMotorSpeed(uint16_t speed) {
return node->writeSingleRegister(MB_SPEED_REG, speed);
}
bool DanfossFC302::setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!node->readHoldingRegisters(MB_CMD_REG, 1)) return false;
currentCmd = node->getResponseBuffer(0);
if (direction) currentCmd |= (1 << 11);
else currentCmd &= ~(1 << 11);
return node->writeSingleRegister(MB_CMD_REG, currentCmd);
}
bool DanfossFC302::sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (node->writeSingleRegister(MB_CMD_REG, command)) return true;
delay(100);
}
return false;
}
void DanfossFC302::readFrequencyLimits() {
node->readHoldingRegisters(MB_MAX_FREQ_REG, 1);
maxFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_MIN_FREQ_REG, 1);
minFrequency = node->getResponseBuffer(0);
}
void DanfossFC302::readMotorStatus() {
node->readHoldingRegisters(MB_STATUS_REG, 1);
motorStatus = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ACTUAL_SPEED_REG, 1);
motorSpeed = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ERRORS_REG, 1);
motorErrors = node->getResponseBuffer(0);
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
// ---------- Yaskawa V1000 ----------
YaskawaV1000::YaskawaV1000(ModbusMaster* node, uint8_t address)
: VFD(node, address) {
MB_CMD_REG = 0x0400;
MB_SPEED_REG = 0x0401;
MB_ACTUAL_SPEED_REG = 0x0402;
MB_STATUS_REG = 0x0500;
MB_ERRORS_REG = 0x0501;
MB_MAX_FREQ_REG = 0x0600;
MB_MIN_FREQ_REG = 0x0601;
}
bool YaskawaV1000::setMotorSpeed(uint16_t speed) {
return node->writeSingleRegister(MB_SPEED_REG, speed);
}
bool YaskawaV1000::setMotorDirection(uint16_t direction) {
uint16_t currentCmd;
if (!node->readHoldingRegisters(MB_CMD_REG, 1)) return false;
currentCmd = node->getResponseBuffer(0);
if (direction) currentCmd |= (1 << 11);
else currentCmd &= ~(1 << 11);
return node->writeSingleRegister(MB_CMD_REG, currentCmd);
}
bool YaskawaV1000::sendCommandWithRetry(uint16_t command) {
for (uint8_t i = 0; i < 3; i++) {
if (node->writeSingleRegister(MB_CMD_REG, command)) return true;
delay(100);
}
return false;
}
void YaskawaV1000::readFrequencyLimits() {
node->readHoldingRegisters(MB_MAX_FREQ_REG, 1);
maxFrequency = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_MIN_FREQ_REG, 1);
minFrequency = node->getResponseBuffer(0);
}
void YaskawaV1000::readMotorStatus() {
node->readHoldingRegisters(MB_STATUS_REG, 1);
motorStatus = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ACTUAL_SPEED_REG, 1);
motorSpeed = node->getResponseBuffer(0);
node->readHoldingRegisters(MB_ERRORS_REG, 1);
motorErrors = node->getResponseBuffer(0);
motorDirection = (motorStatus & 0x8000) ? 1 : 0;
}
It's worth mentioning that before creating this customized version of the interface, we searched for a similar solution on websites like AliExpress. But no interface model offers the refined speed control we were able to achieve with our solution.
In the show, the structure connected to the engine is a boat sail. The control is so precise that the structure feels more like an actor in the cast.
The show runs until August 30th, on Thursdays, Fridays and Saturdays, always at 8pm at SESC Pinheiros, in São Paulo, SP.
Comments