Files
arduino-pwm-modul/pwm-modul/pwm-modul.ino

537 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*************************************************************
* ARDUINO NANO MODBUS SLAVE 6× PWM + LED + EEPROM + CRC
* -----------------------------------------------------------
* Verze FW: 2.2
*
* TODO:
* - případné rozšíření registrů a chybových kódů
*
* Funkce:
* - 6× slow PWM pro termo hlavice (SSR)
* - LED D13 řízená přes Modbus (LED_SWITCH)
* - Alive LED (pin 10)
* - EEPROM uložení konfigurace (perioda, duty, typy, skupiny)
* - CRC kontrola konfigurace
* - Versioning konfigurace (CONFIG_VERSION)
* - Last-Modbus-Age registr (čas od poslední OK Modbus komunikace)
* - Test mode (sekvenční blikání 6 PWM)
* - Skupiny PWM:
* - Každý PWM kanál má svoji skupinu (04), nastavovanou z ESPHome
* - Každá skupina má svůj "virtuální" PWM duty registr
* - Zápis do GROUPx_PWM se propíše do všech PWM, které mají danou skupinu
*
* -----------------------------------------------------------
* MODBUS MAPA REGISTRŮ (Holding Registers)
* -----------------------------------------------------------
* Index Adresa Název R/W Popis
* ----- ------ ---------------- --- ---------------------------------------
* 0 0 PWM_PERIOD R/W Společná perioda PWM (v sekundách), min. 1 s
* 1 1 TEST_MODE R/W 0 = normální režim, !=0 = test sekvence PWM
* 2 2 STATUS_FLAGS R Stavové bity:
* bit0 = config načten (platná konfigurace v RAM)
* bit1 = CRC OK (1) / CRC chyba (0)
* bit2 = verze configu nesouhlasila (použity defaulty)
* 3 3 LAST_ERROR R Rezervováno (zatím 0)
* 4 4 LAST_MODBUS_AGE R Čas od poslední korektní Modbus komunikace (ms / 65k wrap)
* 5 5 TEMPERATURE1 R Teplotní čidlo DS18B20 1
* 6 6 TEMPERATURE2 R Teplotní čidlo DS18B20 2
* 7 7 TEMPERATURE3 R Teplotní čidlo DS18B20 3
* 8 8 TEMPERATURE4 R Teplotní čidlo DS18B20 4
* 9 9 LED_SWITCH R/W 0 = LED D13 vyp, !=0 = LED D13 zap
* 10 10 REZERVA1 R Rezerva 1
*
* -- "virtuální" skupinové PWM (NEUKLÁDAJÍ se do EEPROM) --
*
* 11 11 GROUP1_PWM R/W Duty (%) pro skupinu 1:
* zápis se propíše do všech PWM,
* které mají PWMx_GROUP == 1
* 12 12 GROUP2_PWM R/W Stejně jako GROUP1_PWM, pro skupinu 2
* 13 13 GROUP3_PWM R/W Stejně jako GROUP1_PWM, pro skupinu 3
* 14 14 GROUP4_PWM R/W Stejně jako GROUP1_PWM, pro skupinu 4
* 15 15 REZERVA2 R Rezerva2
*
* -- Per-kanál konfigurace PWM (duty, typ, skupina) --
*
* 16 16 PWM1_DUTY R/W PWM1 - výkon v % (0100)
* 17 17 PWM1_TYPE R/W Typ výstupu PWM1: 0 = NC (normálně vyp.), 1 = NO (invert)
* 18 18 PWM1_GROUP R/W Skupina PWM1: 0 = žádná, 14 = GROUPx_PWM
* 19 19 PWM2_DUTY R/W
* 20 20 PWM2_TYPE R/W
* 21 21 PWM2_GROUP R/W
* 22 22 PWM3_DUTY R/W
* 23 23 PWM3_TYPE R/W
* 24 24 PWM3_GROUP R/W
* 25 25 PWM4_DUTY R/W
* 26 26 PWM4_TYPE R/W
* 27 27 PWM4_GROUP R/W
* 28 28 PWM5_DUTY R/W
* 29 29 PWM5_TYPE R/W
* 30 30 PWM5_GROUP R/W
* 31 31 PWM6_DUTY R/W
* 32 32 PWM6_TYPE R/W
* 33 33 PWM6_GROUP R/W
*
* Celkový počet registrů: TOTAL_REGS_SIZE (34)
*
* EEPROM:
* - Ukládají se registry:
* PWM_PERIOD (0)
* PWM1PWM6_DUTY (1631, po 3 skoky)
* PWM1PWM6_TYPE
* PWM1PWM6_GROUP
* - Neukládají se:
* GROUPx_PWM, TEST_MODE, LED_SWITCH, teploty, status, atd.
*
* EEPROM layout (uint16_t):
* adresa 01 : CONFIG_VERSION
* adresa 23 : CRC konfiguračních registrů
* adresa 4... : hodnoty registrů z pole CONFIG_REGS[] (každý 2 bajty)
*************************************************************/
#include <SoftwareSerial.h>
#include <EEPROM.h>
#include <SimpleModbusSlaveSoftwareSerial.h>
#include <GyverDS18.h>
// ================= PINY ======================
#define PIN_RX 3
#define PIN_TX 2
#define LED_PIN 13
#define ALIVE_PIN 10
#define PWM1_PIN 4
#define PWM2_PIN 5
#define PWM3_PIN 6
#define PWM4_PIN 7
#define PWM5_PIN 8
#define PWM6_PIN 9
#define DALLAS1 A2
#define DALLAS2 A3
#define DALLAS3 A4
#define DALLAS4 A5
// =============== MODBUS ======================
#define DEVICE_ID 3
#define BAUD_RATE 9600
#define RS485_EN -1 // -1 = nepoužíváme DE/RE řízení (přímo RX/TX RS485 převodníku)
// =============== DS18B20 ======================
#define DALLAS_RESOLUTION 9
#define DALLAS_PERIOD 5000
GyverDS18Single ds1(DALLAS1);
GyverDS18Single ds2(DALLAS2);
GyverDS18Single ds3(DALLAS3);
GyverDS18Single ds4(DALLAS4);
// =============== EEPROM CONFIG ===============
// Pozn.: změna CONFIG_VERSION zajistí ignorování staré EEPROM
#define CONFIG_VERSION 1 // verze layoutu konfigurace v EEPROM
#define EEPROM_WRITE_DELAY 200 // ms po změně configu počkáme a pak uložíme celý blok
// Příznak, že se nějaký konfigurační registr změnil (pro odložený zápis)
bool configDirty = false;
unsigned long lastConfigChange = 0;
// =============== REGISTR MAPA ===============
#define TOTAL_REGISTERS 34
enum {
PWM_PERIOD = 0, // 0
TEST_MODE, // 1
STATUS_FLAGS, // 2
LAST_ERROR, // 3
LAST_MODBUS_AGE, // 4
TEMPERATURE1, // 5
TEMPERATURE2, // 6
TEMPERATURE3, // 7
TEMPERATURE4, // 8
LED_SWITCH, // 9
REZERVA1, // 10
GROUP1_PWM, // 11
GROUP2_PWM, // 12
GROUP3_PWM, // 13
GROUP4_PWM, // 14
REZERVA2, // 15
// PWM 1
PWM1_DUTY, // 16
PWM1_TYPE, // 17
PWM1_GROUP, // 18
// PWM 2
PWM2_DUTY, // 19
PWM2_TYPE, // 20
PWM2_GROUP, // 21
// PWM 3
PWM3_DUTY, // 22
PWM3_TYPE, // 23
PWM3_GROUP, // 24
// PWM 4
PWM4_DUTY, // 25
PWM4_TYPE, // 26
PWM4_GROUP, // 27
// PWM 5
PWM5_DUTY, // 28
PWM5_TYPE, // 29
PWM5_GROUP, // 30
// PWM 6
PWM6_DUTY, // 31
PWM6_TYPE, // 32
PWM6_GROUP, // 33
TOTAL_REGS_SIZE // 34
};
unsigned int holdingRegs[TOTAL_REGS_SIZE];
// Seznam registrů, které patří do konfigurace (EEPROM)
const uint8_t CONFIG_REGS[] = {
// PWM duty
PWM1_DUTY,
PWM2_DUTY,
PWM3_DUTY,
PWM4_DUTY,
PWM5_DUTY,
PWM6_DUTY,
// společná perioda
PWM_PERIOD,
// typy
PWM1_TYPE,
PWM2_TYPE,
PWM3_TYPE,
PWM4_TYPE,
PWM5_TYPE,
PWM6_TYPE,
// skupiny
PWM1_GROUP,
PWM2_GROUP,
PWM3_GROUP,
PWM4_GROUP,
PWM5_GROUP,
PWM6_GROUP
};
const uint8_t CONFIG_REGS_COUNT = sizeof(CONFIG_REGS) / sizeof(CONFIG_REGS[0]);
// =============== MODBUS SERIAL ===============
SoftwareSerial modbusSerial(PIN_RX, PIN_TX);
unsigned long cycleStart = 0;
unsigned long lastModbusTime = 0;
// =============== EEPROM MAPA ==================
int eeAddrVersion = 0; // uint16_t verze configu
int eeAddrCRC = 2; // uint16_t CRC
int eeAddrRegs = 4; // začátek oblasti s registry (2 bajty na každý reg z CONFIG_REGS[])
// =============== CRC16 PRO KONFIG =============
// Standardní Modbus CRC16 (polynom 0xA001)
uint16_t crc16_update(uint16_t crc, uint8_t a) {
crc ^= a;
for (int i = 0; i < 8; ++i) {
if (crc & 1)
crc = (crc >> 1) ^ 0xA001;
else
crc = (crc >> 1);
}
return crc;
}
// Výpočet CRC přes všechny konfigurační registry z CONFIG_REGS[]
uint16_t calcConfigCRC() {
uint16_t crc = 0xFFFF;
for (uint8_t i = 0; i < CONFIG_REGS_COUNT; i++) {
uint16_t v = holdingRegs[CONFIG_REGS[i]];
crc = crc16_update(crc, v & 0xFF);
crc = crc16_update(crc, (v >> 8) & 0xFF);
}
return crc;
}
// Uložení konfigurace do EEPROM (jen pokud se změní / voláno z kódu)
void saveConfigToEEPROM() {
uint16_t version = CONFIG_VERSION;
EEPROM.put(eeAddrVersion, version);
// uložit všechny config registry postupně za sebou
for (uint8_t i = 0; i < CONFIG_REGS_COUNT; i++) {
uint16_t v = holdingRegs[CONFIG_REGS[i]];
EEPROM.put(eeAddrRegs + i * 2, v);
}
uint16_t crc = calcConfigCRC();
EEPROM.put(eeAddrCRC, crc);
// nastavit příznaky konfigurace je platná a CRC sedí
holdingRegs[STATUS_FLAGS] |= (1 << 0); // bit0 = config načten/platný
holdingRegs[STATUS_FLAGS] |= (1 << 1); // bit1 = CRC OK
// bit2 (verze nesouhlasila) případně ponecháme beze změny jako "historii"
}
// Načtení konfigurace z EEPROM
// Vrací true = konfigurace z EEPROM byla úspěšně načtena a CRC sedí
// Vrací false = verze nesedí NEBO CRC nesedí -> v setup() se použijí defaulty a přepíšou EEPROM
bool loadConfigFromEEPROM() {
uint16_t storedVersion;
uint16_t storedCRC;
EEPROM.get(eeAddrVersion, storedVersion);
EEPROM.get(eeAddrCRC, storedCRC);
// Kontrola verze layoutu
if (storedVersion != CONFIG_VERSION) {
// Verze nesouhlasí uložená data ignorujeme, v setupu se použijí defaulty
holdingRegs[STATUS_FLAGS] |= (1 << 2); // bit2 = verze configu nesouhlasila
return false;
}
// Načíst registry z EEPROM do RAM
for (uint8_t i = 0; i < CONFIG_REGS_COUNT; i++) {
uint16_t v;
EEPROM.get(eeAddrRegs + i * 2, v);
holdingRegs[CONFIG_REGS[i]] = v;
}
// Kontrola CRC
uint16_t calc = calcConfigCRC();
if (calc != storedCRC) {
// CRC chyba konfiguraci považujeme za neplatnou
holdingRegs[STATUS_FLAGS] &= ~(1 << 1); // bit1 = 0 (CRC error)
return false;
}
// Úspěšně načteno
holdingRegs[STATUS_FLAGS] |= (1 << 0); // bit0 = config načten
holdingRegs[STATUS_FLAGS] |= (1 << 1); // bit1 = CRC OK
holdingRegs[STATUS_FLAGS] &= ~(1 << 2); // bit2 = verze v pořádku
return true;
}
// =============== DEFAULTY =====================
void loadDefaults() {
// Perioda v sekundách
holdingRegs[PWM_PERIOD] = 30; // 30 s
// Typy výstupů: výchozí NC (0)
holdingRegs[PWM1_TYPE] = 0;
holdingRegs[PWM2_TYPE] = 0;
holdingRegs[PWM3_TYPE] = 0;
holdingRegs[PWM4_TYPE] = 0;
holdingRegs[PWM5_TYPE] = 0;
holdingRegs[PWM6_TYPE] = 0;
// Výchozí PWM duty = 0 %
holdingRegs[PWM1_DUTY] = 0;
holdingRegs[PWM2_DUTY] = 0;
holdingRegs[PWM3_DUTY] = 0;
holdingRegs[PWM4_DUTY] = 0;
holdingRegs[PWM5_DUTY] = 0;
holdingRegs[PWM6_DUTY] = 0;
// Skupiny: výchozí = 0 (žádná skupina)
holdingRegs[PWM1_GROUP] = 0;
holdingRegs[PWM2_GROUP] = 0;
holdingRegs[PWM3_GROUP] = 0;
holdingRegs[PWM4_GROUP] = 0;
holdingRegs[PWM5_GROUP] = 0;
holdingRegs[PWM6_GROUP] = 0;
// Test mode, LED, group PWM, status atd.
holdingRegs[TEST_MODE] = 0;
holdingRegs[LED_SWITCH] = 0;
holdingRegs[STATUS_FLAGS] = 0;
holdingRegs[LAST_ERROR] = 0;
holdingRegs[LAST_MODBUS_AGE] = 0;
holdingRegs[GROUP1_PWM] = 0;
holdingRegs[GROUP2_PWM] = 0;
holdingRegs[GROUP3_PWM] = 0;
holdingRegs[GROUP4_PWM] = 0;
}
// =============== SLOW PWM ======================
void slowPwm(int pin, int duty, int typeNO, unsigned long periodMs, unsigned long offset) {
bool on;
if (duty <= 0) {
on = false;
} else if (duty >= 100) {
on = true;
} else {
on = offset < (periodMs * (unsigned long)duty) / 100UL;
}
// typeNO = 1 → NO (invertované chování)
if (typeNO) on = !on;
digitalWrite(pin, on);
}
// =============== TEST MODE ======================
// Sekvenční test 6 PWM výstupů každý má 1/6 periody ON
void testModeRun(unsigned long offset, unsigned long periodMs) {
const uint8_t pwmCount = 6;
unsigned long segment = periodMs / pwmCount;
uint8_t active = offset / segment;
if (active >= pwmCount) active = pwmCount - 1;
digitalWrite(PWM1_PIN, active == 0);
digitalWrite(PWM2_PIN, active == 1);
digitalWrite(PWM3_PIN, active == 2);
digitalWrite(PWM4_PIN, active == 3);
digitalWrite(PWM5_PIN, active == 4);
digitalWrite(PWM6_PIN, active == 5);
}
// =============== SETUP =========================
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(ALIVE_PIN, OUTPUT);
pinMode(PWM1_PIN, OUTPUT);
pinMode(PWM2_PIN, OUTPUT);
pinMode(PWM3_PIN, OUTPUT);
pinMode(PWM4_PIN, OUTPUT);
pinMode(PWM5_PIN, OUTPUT);
pinMode(PWM6_PIN, OUTPUT);
// Nejprve nastavíme defaulty do RAM
loadDefaults();
// Pokus o načtení konfigurace z EEPROM
bool ok = loadConfigFromEEPROM();
if (!ok) {
// Pokud verze neseděla (bit2 = 1), máme už v RAM defaulty a jen je uložíme.
// Pokud verze seděla, ale CRC nesedělo, znovu nahrajeme defaulty.
if (!(holdingRegs[STATUS_FLAGS] & (1 << 2))) {
// žádná verze-mismatch vlajka -> CRC chyba nebo prázdná EEPROM
loadDefaults();
}
// V obou případech teď v RAM jsou defaulty -> zapíšeme je do EEPROM
saveConfigToEEPROM();
}
ds1.setResolution(DALLAS_RESOLUTION);
ds2.setResolution(DALLAS_RESOLUTION);
ds3.setResolution(DALLAS_RESOLUTION);
ds4.setResolution(DALLAS_RESOLUTION);
ds1.setPeriod(DALLAS_PERIOD);
ds2.setPeriod(DALLAS_PERIOD);
ds3.setPeriod(DALLAS_PERIOD);
ds4.setPeriod(DALLAS_PERIOD);
modbusSerial.begin(BAUD_RATE);
modbus_configure(&modbusSerial, BAUD_RATE, DEVICE_ID, RS485_EN, TOTAL_REGS_SIZE);
}
// =============== LOOP ==========================
void loop() {
// Čtení teplot (GyverDS18Single tick() spouští měření, po doběhu vrací false)
if (!ds1.tick()) {
holdingRegs[TEMPERATURE1] = ds1.getTemp() * 100;
}
if (!ds2.tick()) {
holdingRegs[TEMPERATURE2] = ds2.getTemp() * 100;
}
if (!ds3.tick()) {
holdingRegs[TEMPERATURE3] = ds3.getTemp() * 100;
}
if (!ds4.tick()) {
holdingRegs[TEMPERATURE4] = ds4.getTemp() * 100;
}
unsigned long now = millis();
holdingRegs[LAST_MODBUS_AGE] = (uint16_t)(now - lastModbusTime);
// Alive LED cca 1 Hz
digitalWrite(ALIVE_PIN, (now / 500) % 2);
// Kopie registrů před Modbusem pro zjištění změn
uint16_t before[TOTAL_REGS_SIZE];
memcpy(before, holdingRegs, sizeof(holdingRegs));
int errors = modbus_update(holdingRegs);
if (errors == 0) {
lastModbusTime = now;
}
// ==========================================================
// SKUPINOVÉ PWM obsluha GROUP1..4
// Pokud se změní GROUPx_PWM, přepíše se PWMx_DUTY
// všech kanálů se stejnou hodnotou PWMx_GROUP
// ==========================================================
for (uint8_t group = 1; group <= 4; group++) {
uint8_t groupReg = GROUP1_PWM + (group - 1);
// Skupinový registr se změnil?
if (holdingRegs[groupReg] != before[groupReg]) {
uint16_t duty = holdingRegs[groupReg];
if (duty > 100) duty = 100;
holdingRegs[groupReg] = duty; // ořez na 0100 %
// Projdi všech 6 PWM kanálů
for (uint8_t i = 0; i < 6; i++) {
uint8_t chGroupReg = PWM1_GROUP + i * 3;
uint8_t chDutyReg = PWM1_DUTY + i * 3;
// Kanál patří do této skupiny?
if (holdingRegs[chGroupReg] == group) {
holdingRegs[chDutyReg] = duty;
}
}
}
}
// ----------------------------------------------------------
// DETEKCE ZMĚNY KONFIGURAČNÍCH REGISTRŮ PRO EEPROM
// (porovnání aktuálních hodnot s "before" po obsluze skupin)
// ----------------------------------------------------------
for (uint8_t i = 0; i < CONFIG_REGS_COUNT; i++) {
uint8_t r = CONFIG_REGS[i];
if (holdingRegs[r] != before[r]) {
configDirty = true;
lastConfigChange = now;
}
}
// Odložený zápis konfigurace do EEPROM
if (configDirty && (now - lastConfigChange > EEPROM_WRITE_DELAY)) {
saveConfigToEEPROM();
configDirty = false;
}
// LED D13 řízená přes Modbus
digitalWrite(LED_PIN, holdingRegs[LED_SWITCH] ? HIGH : LOW);
// Perioda v ms registr je v sekundách
unsigned long periodMs = (unsigned long)holdingRegs[PWM_PERIOD] * 1000UL;
if (periodMs < 1000UL) periodMs = 1000UL; // aspoň 1 s
unsigned long offset = now - cycleStart;
if (offset >= periodMs) {
cycleStart = now;
offset = 0;
}
// Test mode
if (holdingRegs[TEST_MODE]) {
testModeRun(offset, periodMs);
return;
}
// Normální režim slow PWM
slowPwm(PWM1_PIN, holdingRegs[PWM1_DUTY], holdingRegs[PWM1_TYPE], periodMs, offset);
slowPwm(PWM2_PIN, holdingRegs[PWM2_DUTY], holdingRegs[PWM2_TYPE], periodMs, offset);
slowPwm(PWM3_PIN, holdingRegs[PWM3_DUTY], holdingRegs[PWM3_TYPE], periodMs, offset);
slowPwm(PWM4_PIN, holdingRegs[PWM4_DUTY], holdingRegs[PWM4_TYPE], periodMs, offset);
slowPwm(PWM5_PIN, holdingRegs[PWM5_DUTY], holdingRegs[PWM5_TYPE], periodMs, offset);
slowPwm(PWM6_PIN, holdingRegs[PWM6_DUTY], holdingRegs[PWM6_TYPE], periodMs, offset);
}