commit 93ac705a7460152c53f60bb689b67011c620858b Author: miki Date: Tue Dec 2 09:57:07 2025 +0100 Nahrát soubory do „pwm-modul“ diff --git a/pwm-modul/modbus-pwm-2.2.ino b/pwm-modul/modbus-pwm-2.2.ino new file mode 100644 index 0000000..65bd643 --- /dev/null +++ b/pwm-modul/modbus-pwm-2.2.ino @@ -0,0 +1,536 @@ +/************************************************************* + * 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 (0–4), 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 % (0–100) + * 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á, 1–4 = 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) + * PWM1–PWM6_DUTY (16–31, po 3 skoky) + * PWM1–PWM6_TYPE + * PWM1–PWM6_GROUP + * - Neukládají se: + * GROUPx_PWM, TEST_MODE, LED_SWITCH, teploty, status, atd. + * + * EEPROM layout (uint16_t): + * adresa 0–1 : CONFIG_VERSION + * adresa 2–3 : CRC konfiguračních registrů + * adresa 4–... : hodnoty registrů z pole CONFIG_REGS[] (každý 2 bajty) + *************************************************************/ + +#include +#include +#include +#include + +// ================= 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 0–100 % + + // 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); +}