rotator_main
/*
EW8ZO Rotator Controller v1.76
- displayAz гарантирует шаг 5° при повороте энкодера (300,305,310...)
- Геркон (reed) на D2
- EncButton eb(5,6,7)
- Реле: RELAY_1 = 8, RELAY_2 = 9 (2 реле)
- START/STOP кнопка на D3 (второй контакт -> GND), INPUT_PULLUP
- Режимы реле:
Ожидание: оба OFF
Вправо: RELAY_2 ON
Влево: RELAY_1 + RELAY_2 ON
*/
#include <Arduino.h>
#include <EncButton.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
// LCD и энкодер (CLK->5, DT->6, SW->7)
LiquidCrystal_I2C lcd(0x27, 16, 2);
EncButton eb(5, 6, 7);
// ===== Реле (2 реле) =====
#define RELAY_1 8
#define RELAY_2 9
const bool RELAY_ACTIVE_HIGH = false; // true если модуль реле активен HIGH
inline void relayWriteRaw(uint8_t pin, bool level) { digitalWrite(pin, level ? HIGH : LOW); }
inline void relayWrite(uint8_t pin, bool active) { if (RELAY_ACTIVE_HIGH) relayWriteRaw(pin, active); else relayWriteRaw(pin, !active); }
// ===== Константы =====
const long COUNTS_PER_REV = 1200;
const unsigned long SAVE_DELAY_MS = 1000;
const unsigned long LCD_UPDATE_MS = 100;
const unsigned long SAVED_DISPLAY_MS = 1000;
const unsigned long SPLASH_MS = 2000;
const unsigned long SAVE_DEDUP_MS = 1500;
// EEPROM
const int EEPROM_ADDR_MAGIC = 0;
const int EEPROM_ADDR_VALUE = 2;
const uint16_t EEPROM_MAGIC = 0xA5A5;
// ===== START/STOP кнопка =====
const int STARTSTOP_PIN = 3; // D3
const unsigned long BTN_DEBOUNCE_MS = 50;
bool startStopLastState = HIGH;
unsigned long startStopLastChange = 0;
bool startStopPressedHandled = false;
// ===== Глобальные переменные =====
volatile long sensor = 0;
volatile long temp_sensor = 0;
unsigned long lastMoveMillis = 0;
bool movedSinceLastSave = false;
unsigned long lastLcdMillis = 0;
const int COL_NEW_VAL = 5;
const int W_NEW_VAL = 5;
const int COL_CUR_AZ = 9;
const int W_CUR_AZ = 3;
enum Mode { MODE_NORMAL, MODE_ADD };
Mode mode = MODE_NORMAL;
int addAz = 1;
bool suppressNextClick = false;
bool movementActive = false;
int movementTargetAz = 1;
unsigned long savedDisplayMillis = 0;
unsigned long lastSavedMillis = 0;
// ===== Геркон (reed) =====
const int REED_PIN = 2;
const unsigned long REED_DEBOUNCE_US = 8000UL;
volatile unsigned long lastReedMicros = 0;
// ===== Флаги направления для ISR геркона =====
volatile bool dirRightFlag = false;
volatile bool dirLeftFlag = false;
// ===== Новая переменная: отображаемый азимут, всегда кратный 5 =====
int displayAz = 0; // будет инициализирован в setup()
// ===== Функции управления мотором (по вашей логике) =====
void motorStop() {
relayWrite(RELAY_1, false);
relayWrite(RELAY_2, false);
dirRightFlag = false;
dirLeftFlag = false;
Serial.println("Motor: STOP (both relays OFF)");
}
void motorForward() { // вправо: только RELAY_2 ON
relayWrite(RELAY_1, false);
relayWrite(RELAY_2, true);
dirRightFlag = true;
dirLeftFlag = false;
Serial.println("Motor: FORWARD (RELAY_2 ON)");
}
void motorBackward() { // влево: оба реле ON
relayWrite(RELAY_1, true);
relayWrite(RELAY_2, true);
dirRightFlag = false;
dirLeftFlag = true;
Serial.println("Motor: BACKWARD (both RELAY_1 & RELAY_2 ON)");
}
// ===== EEPROM helpers =====
void savePositionToEEPROM(long val) {
uint16_t magic;
EEPROM.get(EEPROM_ADDR_MAGIC, magic);
if (magic != EEPROM_MAGIC) EEPROM.put(EEPROM_ADDR_MAGIC, EEPROM_MAGIC);
long stored; EEPROM.get(EEPROM_ADDR_VALUE, stored);
if (stored != val) EEPROM.put(EEPROM_ADDR_VALUE, val);
lastSavedMillis = millis();
}
bool loadPositionFromEEPROM(long &outVal) {
uint16_t magic; EEPROM.get(EEPROM_ADDR_MAGIC, magic);
if (magic != EEPROM_MAGIC) return false;
EEPROM.get(EEPROM_ADDR_VALUE, outVal);
return true;
}
// ===== Вспомогательные конвертации =====
int countsToAzimuth1(long counts) {
long c = counts % COUNTS_PER_REV; if (c < 0) c += COUNTS_PER_REV;
long deg = (c * 360L) / COUNTS_PER_REV; int az = (int)deg; return (az == 0) ? 360 : az;
}
long azimuthToCounts(int az) {
int a = az % 360; long c = ((long)a * COUNTS_PER_REV + 180) / 360;
if (az == 360) c = 0; c = c % COUNTS_PER_REV; if (c < 0) c += COUNTS_PER_REV; return c;
}
int shortestTurnDirection(int current, int target) {
if (current == target) return 0;
int diff = target - current;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
return (diff > 0) ? 1 : -1;
}
// ===== Управление движением =====
void stopMovement() {
motorStop();
movementActive = false;
noInterrupts(); long cur = sensor; interrupts();
if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(cur); Serial.println("Movement stopped, saved (immediate)"); }
else Serial.println("Movement stopped (save skipped - recent)");
movedSinceLastSave = false; lastMoveMillis = millis(); savedDisplayMillis = millis();
eb.counter = countsToAzimuth1(cur);
// Лог остановки
Serial.print("STOP event: curCounts="); Serial.print(cur);
Serial.print(" curAz="); Serial.print(countsToAzimuth1(cur));
Serial.print(" targetAz="); Serial.print(movementTargetAz);
Serial.print(" dirR="); Serial.print(dirRightFlag);
Serial.print(" dirL="); Serial.println(dirLeftFlag);
}
void startMovementTo(int targetAz) {
if (movementActive) return;
noInterrupts(); long curSensor = sensor; interrupts();
int curAz = countsToAzimuth1(curSensor);
int dir = shortestTurnDirection(curAz, targetAz);
if (dir == 0) {
Serial.println("Already at target azimuth, no movement");
if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(curSensor); savedDisplayMillis = millis(); }
return;
}
movementTargetAz = targetAz; movementActive = true;
if (dir > 0) motorForward(); else motorBackward();
// Лог старта
Serial.print("START event: curCounts="); Serial.print(curSensor);
Serial.print(" curAz="); Serial.print(curAz);
Serial.print(" targetAz="); Serial.print(targetAz);
Serial.print(" dir="); Serial.println((dir>0) ? "CW" : "CCW");
}
// ===== LCD helpers =====
void lcdPrintFieldStr(int col, int row, const char* s, int width) {
lcd.setCursor(col, row); for (int i = 0; i < width; ++i) lcd.print(' '); lcd.setCursor(col, row); lcd.print(s);
}
void lcdPrintFieldLong(int col, int row, long value, int width) { char buf[20]; snprintf(buf, sizeof(buf), "%ld", value); lcdPrintFieldStr(col, row, buf, width); }
void lcdPrintFieldIntFmt(int col, int row, int value, const char* fmt, int width) { char buf[20]; snprintf(buf, sizeof(buf), fmt, value); lcdPrintFieldStr(col, row, buf, width); }
// ===== ISR геркона (reed) =====
void reedISR() {
unsigned long t = micros();
if (t - lastReedMicros < REED_DEBOUNCE_US) return;
lastReedMicros = t;
if (dirRightFlag) sensor++;
else if (dirLeftFlag) sensor--;
}
// ===== Setup =====
void setup() {
Serial.begin(115200);
// Геркон
pinMode(REED_PIN, INPUT_PULLUP);
// Реле: безопасный уровень до OUTPUT
if (RELAY_ACTIVE_HIGH) { relayWriteRaw(RELAY_1, LOW); relayWriteRaw(RELAY_2, LOW); }
else { relayWriteRaw(RELAY_1, HIGH); relayWriteRaw(RELAY_2, HIGH); }
pinMode(RELAY_1, OUTPUT); pinMode(RELAY_2, OUTPUT);
motorStop();
// START/STOP кнопка (D3)
pinMode(STARTSTOP_PIN, INPUT_PULLUP);
startStopLastState = digitalRead(STARTSTOP_PIN);
startStopLastChange = millis();
// Прерывание для геркона
attachInterrupt(digitalPinToInterrupt(REED_PIN), reedISR, FALLING);
// LCD splash
lcd.begin(); lcd.backlight(); lcd.clear();
lcd.setCursor(0,0); lcd.print("EW8ZO Controller");
lcd.setCursor(0,1); lcd.print("VER 1.76");
delay(SPLASH_MS); lcd.clear();
// EncButton настройки
eb.setBtnLevel(LOW); eb.setClickTimeout(800); eb.setDebTimeout(8); eb.setHoldTimeout(3000);
eb.setEncType(EB_STEP4_LOW); eb.setEncReverse(0); eb.setFastTimeout(10); eb.counter = 0;
// Загрузка сохранённой позиции
long saved = 0;
if (loadPositionFromEEPROM(saved)) { noInterrupts(); sensor = saved; interrupts(); Serial.print("Loaded pos counts: "); Serial.println(saved); lastSavedMillis = millis(); }
else Serial.println("No saved pos in EEPROM");
// Инициализация eb.counter и displayAz
noInterrupts();
int curAz = countsToAzimuth1(sensor);
interrupts();
// Инициализируем displayAz как нижнее кратное 5 (304 -> 300)
displayAz = (curAz / 5) * 5;
if (displayAz <= 0) displayAz = 360;
eb.counter = displayAz;
lcdPrintFieldStr(0,0,"New:",5); lcdPrintFieldStr(0,1,"Azimuth:",8);
lastLcdMillis = millis();
// Информационный лог при старте
Serial.println("Controller v1.76 started");
Serial.print("Initial curAz="); Serial.print(curAz); Serial.print(" displayAz="); Serial.println(displayAz);
}
// ===== Main loop =====
void loop() {
eb.tick();
// Обработка START/STOP кнопки (debounce) с логом в Serial
bool curState = digitalRead(STARTSTOP_PIN);
if (curState != startStopLastState) {
startStopLastChange = millis();
startStopLastState = curState;
startStopPressedHandled = false;
} else {
if (!startStopPressedHandled && (millis() - startStopLastChange) > BTN_DEBOUNCE_MS) {
if (curState == LOW) { // нажата
noInterrupts(); long curSensor = sensor; interrupts();
int curAz = countsToAzimuth1(curSensor);
Serial.print("BUTTON START/STOP pressed -> New="); Serial.print(eb.counter);
Serial.print(" CurAz="); Serial.print(curAz);
Serial.print(" movementActive="); Serial.println(movementActive);
if (movementActive) {
stopMovement();
} else {
int desiredAz = eb.counter;
if (desiredAz <= 0) desiredAz = 1;
if (desiredAz > 360) desiredAz = ((desiredAz - 1) % 360) + 1;
if (desiredAz != curAz) startMovementTo(desiredAz);
else Serial.println("Start button: desired == current, nothing to do");
}
}
startStopPressedHandled = true;
}
}
// Вход в ADD режим при удержании энкодера
if (eb.hold() && mode == MODE_NORMAL && !movementActive) {
mode = MODE_ADD;
noInterrupts(); int curAz = countsToAzimuth1(sensor); interrupts();
addAz = curAz; Serial.println("Entered ADD mode"); suppressNextClick = false; lastLcdMillis = 0;
}
// Отслеживание изменений sensor
if (sensor != temp_sensor) { temp_sensor = sensor; lastMoveMillis = millis(); movedSinceLastSave = true; }
// Если движение активно — проверяем достижение цели
if (movementActive) {
noInterrupts(); long curSensor = sensor; interrupts();
int curAz = countsToAzimuth1(curSensor);
if (curAz == movementTargetAz) { stopMovement(); lastLcdMillis = 0; }
}
// Сохранение в EEPROM после задержки
if (movedSinceLastSave && (millis() - lastMoveMillis >= SAVE_DELAY_MS)) {
if (millis() - lastSavedMillis > SAVE_DEDUP_MS) {
noInterrupts(); long toSave = sensor; interrupts();
int azForLog = countsToAzimuth1(toSave);
Serial.print("Saving counts: "); Serial.print(toSave); Serial.print(" azimuth: "); Serial.println(azForLog);
savePositionToEEPROM(toSave); movedSinceLastSave = false; savedDisplayMillis = millis();
int newAz = countsToAzimuth1(toSave); eb.counter = newAz; lastLcdMillis = 0;
} else movedSinceLastSave = false;
}
// Обработка поворотов энкодера
static unsigned long lastTurnMs = 0;
if (eb.turn()) {
unsigned long now = millis(); unsigned long dt = now - lastTurnMs; lastTurnMs = now;
int dir = eb.dir(); // 1 или -1
if (mode == MODE_NORMAL) {
if (!movementActive) {
// Обновляем displayAz ровно на 5 градусов за один шаг энкодера
displayAz += dir * 5;
// Нормализация 1..360
while (displayAz <= 0) displayAz += 360;
displayAz = ((displayAz - 1) % 360) + 1;
// Синхронизируем New с displayAz
eb.counter = displayAz;
lastLcdMillis = 0;
}
} else if (mode == MODE_ADD) {
// ADD режим с ускорением (как раньше)
int baseStep = 5;
int mult;
if (dt < 30) mult = 20;
else if (dt < 80) mult = 5;
else mult = 1;
int delta = dir * baseStep * mult;
addAz += delta;
while (addAz <= 0) addAz += 360;
addAz = ((addAz - 1) % 360) + 1;
lastLcdMillis = 0;
}
}
// Подтверждение ADD кликом
if (mode == MODE_ADD && eb.click()) {
long newCounts = azimuthToCounts(addAz); noInterrupts(); sensor = newCounts; interrupts();
if (millis() - lastSavedMillis > SAVE_DEDUP_MS) { savePositionToEEPROM(newCounts); Serial.print("ADD confirmed: az="); Serial.print(addAz); Serial.print(" counts="); Serial.println(newCounts); }
else { Serial.print("ADD confirmed: az="); Serial.print(addAz); Serial.println(" (save skipped - recent)"); }
eb.counter = addAz; mode = MODE_NORMAL; suppressNextClick = true; savedDisplayMillis = millis(); lastLcdMillis = 0;
// Синхронизируем displayAz с новым значением
displayAz = addAz;
}
// Нормальный клик энкодера: старт движения
if (mode == MODE_NORMAL && eb.click()) {
if (suppressNextClick) suppressNextClick = false;
else {
noInterrupts(); long curSensor = sensor; interrupts();
int curAz = countsToAzimuth1(curSensor);
int desiredAz = eb.counter;
if (desiredAz <= 0) desiredAz = 1;
if (desiredAz > 360) desiredAz = ((desiredAz - 1) % 360) + 1;
if (desiredAz != curAz) startMovementTo(desiredAz);
else Serial.println("Click: desired == current, nothing to do");
lastLcdMillis = 0;
}
}
// Обновление LCD
if (millis() - lastLcdMillis >= LCD_UPDATE_MS) {
lastLcdMillis = millis(); noInterrupts(); long curSensor = sensor; interrupts();
int curAz = countsToAzimuth1(curSensor);
if (mode == MODE_NORMAL) {
if (millis() - savedDisplayMillis < SAVED_DISPLAY_MS) { lcdPrintFieldStr(0,0,"SAVED",5); lcdPrintFieldLong(COL_NEW_VAL,0,eb.counter,W_NEW_VAL); }
else { lcdPrintFieldStr(0,0,"New:",5); lcdPrintFieldLong(COL_NEW_VAL,0,eb.counter,W_NEW_VAL); }
lcdPrintFieldIntFmt(COL_CUR_AZ,1,curAz,"%03d",W_CUR_AZ);
lcd.setCursor(13,1);
if (movementActive) { int dir = shortestTurnDirection(curAz, movementTargetAz); if (dir > 0) lcd.print('>'); else if (dir < 0) lcd.print('<'); else lcd.print('='); }
else lcd.print(' ');
} else if (mode == MODE_ADD) {
lcdPrintFieldStr(0,0,"ADD:",5); lcdPrintFieldIntFmt(COL_NEW_VAL,0,addAz,"%03d",3); lcdPrintFieldIntFmt(COL_CUR_AZ,1,curAz,"%03d",W_CUR_AZ); lcd.setCursor(13,1); lcd.print(' ');
}
}
delay(2);
}
rotator_main.txt · Последнее изменение: — eu8t
