/************************************************************* * 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); }