Nahrát soubory do „pwm-modul“

This commit is contained in:
2025-12-02 09:57:07 +01:00
commit 93ac705a74

View File

@@ -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 (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);
}