#include <EEPROM.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
// Embaladora semi-automatica sem display.
// Um toque no pedal executa exatamente um percurso entre os dois fins de curso.
const uint8_t PIN_UNASSIGNED = 255;
const uint8_t PIN_POT_SPEED = A0;
const uint8_t PIN_POT_POWER = A1;
const uint8_t PIN_PEDAL = 4;
const uint8_t PIN_LED_RED = 6;
const uint8_t PIN_LED_BLUE = 8;
const uint8_t PIN_LED_STATUS = PIN_LED_BLUE;
const uint8_t PIN_LED_ACTIVITY = PIN_LED_RED;
const uint8_t PIN_HEATER_PWM = 9; // D4184 da fita de niquel-cromo.
const uint8_t PIN_MOTOR_IN_B = A2; // Motor 1B; PWM por software.
const uint8_t PIN_MOTOR_IN_A = A3; // Motor 1A; PWM por software.
const uint8_t PIN_LIMIT_START = 5; // Fim de curso A.
const uint8_t PIN_LIMIT_END = 7; // Fim de curso B.
// Fins de curso NC fail-safe:
// LOW = contato fechado e curso livre; HIGH = fim atingido ou cabo aberto.
// Pedal NO: pressionado fecha D4 para GND.
const uint8_t MOTOR_MIN_PWM = 80;
const uint8_t MOTOR_MAX_PWM = 255;
const uint8_t MOTOR_RAMP_STEP_PWM = 8;
const uint8_t HEATER_MAX_PWM = 255;
const uint8_t MOTOR_SOFT_PWM_STEPS = 64;
const uint16_t POT_ZERO_DEADBAND = 20;
const unsigned long INPUT_DEBOUNCE_MS = 35UL;
const unsigned long INPUT_SETTLE_MS = 150UL;
const unsigned long POT_SAMPLE_MS = 20UL;
const unsigned long MOTOR_RAMP_STEP_MS = 25UL;
const unsigned long ORIGIN_RELEASE_TIMEOUT_MS = 1200UL;
const unsigned long STROKE_TIMEOUT_MS = 15000UL;
const unsigned long HEATER_MAX_ON_MS = 10000UL;
const unsigned long FAULT_RESET_HOLD_MS = 2000UL;
const unsigned long FAULT_BLINK_MS = 180UL;
const unsigned long SERIAL_STATUS_MS = 1000UL;
const uint16_t COUNTER_MAGIC = 0xC318;
const uint16_t LEGACY_COUNTER_MAGIC = 0xC07A;
const uint8_t COUNTER_SLOT_COUNT = 80;
const uint8_t LEGACY_COUNTER_SLOT_COUNT = 48;
const int LEGACY_COUNTER_ADDRESS = 64;
enum MachineState {
BOOT,
IDLE,
RUN_TO_END,
RUN_TO_START,
FAULT
};
enum FaultCode {
FAULT_NONE,
FAULT_PIN_CONFIGURATION,
FAULT_NO_REFERENCE,
FAULT_BOTH_LIMITS,
FAULT_ORIGIN_NOT_RELEASED,
FAULT_STROKE_TIMEOUT,
FAULT_HEATER_TIMEOUT,
FAULT_SPEED_ZERO
};
struct DebouncedInput {
uint8_t pin;
bool activeHigh;
bool stableActive;
bool lastRawActive;
bool activatedEvent;
bool deactivatedEvent;
unsigned long lastRawChangeMs;
};
struct CounterRecord {
uint16_t magic;
uint32_t sequence;
uint32_t lifetimeCount;
uint16_t checksum;
} __attribute__((packed));
struct LegacyCounterRecord {
uint16_t magic;
uint16_t sequence;
uint32_t sessionCount;
uint32_t lifetimeCount;
uint16_t checksum;
} __attribute__((packed));
static_assert(sizeof(CounterRecord) == 12, "CounterRecord deve ocupar 12 bytes");
DebouncedInput pedal = {PIN_PEDAL, false, false, false, false, false, 0};
DebouncedInput limitStart = {PIN_LIMIT_START, true, false, false, false, false, 0};
DebouncedInput limitEnd = {PIN_LIMIT_END, true, false, false, false, false, 0};
MachineState state = BOOT;
FaultCode faultCode = FAULT_NONE;
unsigned long stateStartedMs = 0;
unsigned long lastPotSampleMs = 0;
unsigned long lastMotorRampMs = 0;
unsigned long heaterStartedMs = 0;
unsigned long pedalPressedAtMs = 0;
unsigned long lastFaultBlinkMs = 0;
unsigned long lastSerialStatusMs = 0;
uint16_t speedPotFiltered = 0;
uint16_t powerPotFiltered = 0;
uint8_t requestedMotorPwm = 0;
uint8_t requestedHeaterPwm = 0;
uint8_t motorCurrentPwm = 0;
uint8_t motorTargetPwm = 0;
uint32_t sessionCount = 0;
uint32_t lifetimeCount = 0;
uint32_t counterSequence = 0;
uint8_t nextCounterSlot = 0;
bool pedalLongReported = false;
bool pedalLongPressEvent = false;
bool faultNeedsPedalRelease = false;
bool originReleased = false;
bool heaterActive = false;
bool heaterHasEnergized = false;
bool motorGoingToEnd = true;
bool faultLedState = false;
volatile uint8_t motorSoftwareDuty = 0;
volatile bool motorSoftwareToEnd = true;
ISR(TIMER2_COMPA_vect) {
static uint8_t phase = 0;
phase = (phase + 1U) & (MOTOR_SOFT_PWM_STEPS - 1U);
const uint8_t maskA = _BV(PC3); // A3 / Motor 1A.
const uint8_t maskB = _BV(PC2); // A2 / Motor 1B.
uint8_t portValue = PORTC & (uint8_t)~(maskA | maskB);
if (phase < motorSoftwareDuty) {
portValue |= motorSoftwareToEnd ? maskA : maskB;
}
PORTC = portValue;
}
bool isAssigned(uint8_t pin) {
return pin != PIN_UNASSIGNED;
}
bool requiredPinsConfigured() {
const uint8_t pins[] = {
PIN_POT_SPEED,
PIN_POT_POWER,
PIN_PEDAL,
PIN_LED_STATUS,
PIN_LED_ACTIVITY,
PIN_HEATER_PWM,
PIN_MOTOR_IN_A,
PIN_MOTOR_IN_B,
PIN_LIMIT_END,
PIN_LIMIT_START
};
for (uint8_t i = 0; i < sizeof(pins) / sizeof(pins[0]); i++) {
if (!isAssigned(pins[i])) {
return false;
}
for (uint8_t j = i + 1; j < sizeof(pins) / sizeof(pins[0]); j++) {
if (pins[i] == pins[j]) {
return false;
}
}
}
return true;
}
bool rawInputActive(const DebouncedInput &input) {
if (!isAssigned(input.pin)) {
return false;
}
bool high = digitalRead(input.pin) == HIGH;
return input.activeHigh ? high : !high;
}
void initializeInput(DebouncedInput &input) {
bool active = rawInputActive(input);
input.stableActive = active;
input.lastRawActive = active;
input.activatedEvent = false;
input.deactivatedEvent = false;
input.lastRawChangeMs = millis();
}
void updateInput(DebouncedInput &input) {
input.activatedEvent = false;
input.deactivatedEvent = false;
if (!isAssigned(input.pin)) {
input.stableActive = false;
input.lastRawActive = false;
return;
}
bool rawActive = rawInputActive(input);
unsigned long now = millis();
if (rawActive != input.lastRawActive) {
input.lastRawActive = rawActive;
input.lastRawChangeMs = now;
}
if ((now - input.lastRawChangeMs) >= INPUT_DEBOUNCE_MS &&
input.stableActive != rawActive) {
input.stableActive = rawActive;
input.activatedEvent = rawActive;
input.deactivatedEvent = !rawActive;
}
}
void updatePedalLongPress() {
pedalLongPressEvent = false;
if (pedal.activatedEvent) {
pedalPressedAtMs = millis();
pedalLongReported = false;
}
if (pedal.deactivatedEvent) {
pedalLongReported = false;
}
if (pedal.stableActive && !pedalLongReported &&
(millis() - pedalPressedAtMs) >= FAULT_RESET_HOLD_MS) {
pedalLongPressEvent = true;
pedalLongReported = true;
}
}
uint16_t readAnalogAverage(uint8_t pin) {
uint32_t sum = 0;
for (uint8_t i = 0; i < 8; i++) {
sum += analogRead(pin);
}
return (uint16_t)(sum / 8U);
}
uint8_t mapSpeedPwm(uint16_t adc) {
if (adc <= POT_ZERO_DEADBAND) {
return 0;
}
return (uint8_t)map(adc, POT_ZERO_DEADBAND + 1, 1023,
MOTOR_MIN_PWM, MOTOR_MAX_PWM);
}
uint8_t mapHeaterPwm(uint16_t adc) {
if (adc <= POT_ZERO_DEADBAND) {
return 0;
}
return (uint8_t)map(adc, POT_ZERO_DEADBAND + 1, 1023,
1, HEATER_MAX_PWM);
}
void updatePotentiometers() {
unsigned long now = millis();
if ((now - lastPotSampleMs) < POT_SAMPLE_MS) {
return;
}
lastPotSampleMs = now;
uint16_t speedRaw = analogRead(PIN_POT_SPEED);
uint16_t powerRaw = analogRead(PIN_POT_POWER);
speedPotFiltered = (uint16_t)(((uint32_t)speedPotFiltered * 7U + speedRaw) / 8U);
powerPotFiltered = (uint16_t)(((uint32_t)powerPotFiltered * 7U + powerRaw) / 8U);
requestedMotorPwm = mapSpeedPwm(speedPotFiltered);
requestedHeaterPwm = mapHeaterPwm(powerPotFiltered);
}
void applyMotorPwm(uint8_t pwm) {
motorSoftwareToEnd = motorGoingToEnd;
motorSoftwareDuty = (uint8_t)(((uint16_t)pwm * MOTOR_SOFT_PWM_STEPS + 127U) / 255U);
}
void setMotorStopImmediate() {
motorTargetPwm = 0;
motorCurrentPwm = 0;
motorSoftwareDuty = 0;
digitalWrite(PIN_MOTOR_IN_A, LOW);
digitalWrite(PIN_MOTOR_IN_B, LOW);
}
void updateMotorRamp() {
unsigned long now = millis();
if ((now - lastMotorRampMs) < MOTOR_RAMP_STEP_MS) {
return;
}
lastMotorRampMs = now;
if (motorCurrentPwm < motorTargetPwm) {
uint16_t nextPwm = (uint16_t)motorCurrentPwm + MOTOR_RAMP_STEP_PWM;
motorCurrentPwm = nextPwm > motorTargetPwm ? motorTargetPwm : (uint8_t)nextPwm;
} else if (motorCurrentPwm > motorTargetPwm) {
if (motorCurrentPwm > MOTOR_RAMP_STEP_PWM) {
motorCurrentPwm -= MOTOR_RAMP_STEP_PWM;
} else {
motorCurrentPwm = 0;
}
if (motorCurrentPwm < motorTargetPwm) {
motorCurrentPwm = motorTargetPwm;
}
}
applyMotorPwm(motorCurrentPwm);
}
void setHeaterOff() {
heaterActive = false;
heaterHasEnergized = false;
heaterStartedMs = 0;
analogWrite(PIN_HEATER_PWM, 0);
}
void updateHeater() {
if (!heaterActive || requestedHeaterPwm == 0) {
analogWrite(PIN_HEATER_PWM, 0);
return;
}
if (!heaterHasEnergized) {
heaterStartedMs = millis();
heaterHasEnergized = true;
}
analogWrite(PIN_HEATER_PWM, requestedHeaterPwm);
}
uint16_t counterChecksum(const CounterRecord &record) {
uint16_t sum = record.magic;
sum ^= (uint16_t)(record.sequence & 0xFFFFUL);
sum ^= (uint16_t)(record.sequence >> 16);
sum ^= (uint16_t)(record.lifetimeCount & 0xFFFFUL);
sum ^= (uint16_t)(record.lifetimeCount >> 16);
return sum;
}
uint16_t legacyCounterChecksum(const LegacyCounterRecord &record) {
uint16_t sum = record.magic ^ record.sequence;
sum ^= (uint16_t)(record.sessionCount & 0xFFFFUL);
sum ^= (uint16_t)(record.sessionCount >> 16);
sum ^= (uint16_t)(record.lifetimeCount & 0xFFFFUL);
sum ^= (uint16_t)(record.lifetimeCount >> 16);
return sum;
}
int counterSlotAddress(uint8_t slot) {
return (int)slot * (int)sizeof(CounterRecord);
}
bool newerSequence(uint32_t candidate, uint32_t current) {
return (int32_t)(candidate - current) > 0;
}
void saveLifetimeCounter() {
CounterRecord record;
record.magic = COUNTER_MAGIC;
record.sequence = counterSequence + 1UL;
record.lifetimeCount = lifetimeCount;
record.checksum = counterChecksum(record);
EEPROM.put(counterSlotAddress(nextCounterSlot), record);
counterSequence = record.sequence;
nextCounterSlot++;
if (nextCounterSlot >= COUNTER_SLOT_COUNT) {
nextCounterSlot = 0;
}
}
bool migrateLegacyCounter() {
LegacyCounterRecord record;
LegacyCounterRecord best;
bool found = false;
for (uint8_t slot = 0; slot < LEGACY_COUNTER_SLOT_COUNT; slot++) {
int address = LEGACY_COUNTER_ADDRESS +
((int)slot * (int)sizeof(LegacyCounterRecord));
EEPROM.get(address, record);
if (record.magic == LEGACY_COUNTER_MAGIC &&
record.checksum == legacyCounterChecksum(record)) {
if (!found || (uint16_t)(record.sequence - best.sequence) < 32768U) {
best = record;
found = true;
}
}
}
if (found) {
lifetimeCount = best.lifetimeCount;
}
return found;
}
void loadLifetimeCounter() {
CounterRecord record;
CounterRecord best;
bool found = false;
for (uint8_t slot = 0; slot < COUNTER_SLOT_COUNT; slot++) {
EEPROM.get(counterSlotAddress(slot), record);
if (record.magic == COUNTER_MAGIC && record.checksum == counterChecksum(record)) {
if (!found || newerSequence(record.sequence, best.sequence)) {
best = record;
nextCounterSlot = slot + 1;
if (nextCounterSlot >= COUNTER_SLOT_COUNT) {
nextCounterSlot = 0;
}
found = true;
}
}
}
if (found) {
lifetimeCount = best.lifetimeCount;
counterSequence = best.sequence;
return;
}
lifetimeCount = 0;
counterSequence = 0;
nextCounterSlot = 0;
migrateLegacyCounter();
saveLifetimeCounter();
}
void incrementCounters() {
sessionCount++;
lifetimeCount++;
saveLifetimeCounter();
}
const __FlashStringHelper *stateName() {
switch (state) {
case BOOT: return F("BOOT");
case IDLE: return F("IDLE");
case RUN_TO_END: return F("RUN_TO_END");
case RUN_TO_START: return F("RUN_TO_START");
case FAULT: return F("FAULT");
}
return F("UNKNOWN");
}
const __FlashStringHelper *faultName() {
switch (faultCode) {
case FAULT_NONE: return F("NONE");
case FAULT_PIN_CONFIGURATION: return F("PIN_CONFIGURATION");
case FAULT_NO_REFERENCE: return F("NO_REFERENCE");
case FAULT_BOTH_LIMITS: return F("BOTH_LIMITS");
case FAULT_ORIGIN_NOT_RELEASED: return F("ORIGIN_NOT_RELEASED");
case FAULT_STROKE_TIMEOUT: return F("STROKE_TIMEOUT");
case FAULT_HEATER_TIMEOUT: return F("HEATER_TIMEOUT");
case FAULT_SPEED_ZERO: return F("SPEED_ZERO");
}
return F("UNKNOWN");
}
void printStatus() {
Serial.print(F("state="));
Serial.print(stateName());
Serial.print(F(" fault="));
Serial.print(faultName());
Serial.print(F(" limitA="));
Serial.print(limitStart.stableActive ? 1 : 0);
Serial.print(F(" limitB="));
Serial.print(limitEnd.stableActive ? 1 : 0);
Serial.print(F(" pedal="));
Serial.print(pedal.stableActive ? 1 : 0);
Serial.print(F(" motor="));
Serial.print(requestedMotorPwm);
Serial.print(F(" heater="));
Serial.print(requestedHeaterPwm);
Serial.print(F(" session="));
Serial.print(sessionCount);
Serial.print(F(" lifetime="));
Serial.println(lifetimeCount);
}
void handleSerialCommands() {
while (Serial.available() > 0) {
char command = (char)Serial.read();
if (command == 's' || command == 'S') {
printStatus();
} else if (command == 'r' || command == 'R') {
sessionCount = 0;
Serial.println(F("session=0"));
}
}
}
void setOutputsForState() {
switch (state) {
case BOOT:
digitalWrite(PIN_LED_STATUS, LOW);
digitalWrite(PIN_LED_ACTIVITY, LOW);
break;
case IDLE:
digitalWrite(PIN_LED_STATUS, HIGH);
digitalWrite(PIN_LED_ACTIVITY, LOW);
break;
case RUN_TO_END:
case RUN_TO_START:
digitalWrite(PIN_LED_STATUS, HIGH);
digitalWrite(PIN_LED_ACTIVITY, HIGH);
break;
case FAULT:
digitalWrite(PIN_LED_STATUS, LOW);
if ((millis() - lastFaultBlinkMs) >= FAULT_BLINK_MS) {
lastFaultBlinkMs = millis();
faultLedState = !faultLedState;
digitalWrite(PIN_LED_ACTIVITY, faultLedState ? HIGH : LOW);
}
break;
}
}
void enterState(MachineState nextState) {
state = nextState;
stateStartedMs = millis();
}
void setFault(FaultCode code) {
setHeaterOff();
setMotorStopImmediate();
faultCode = code;
faultNeedsPedalRelease = true;
faultLedState = false;
enterState(FAULT);
Serial.print(F("FAULT: "));
Serial.println(faultName());
}
bool limitsAreCoherentAtRest() {
return limitStart.stableActive != limitEnd.stableActive;
}
void clearFaultIfSafe() {
if (!limitsAreCoherentAtRest()) {
Serial.println(F("Falha nao limpa: posicao sem referencia segura"));
return;
}
faultCode = FAULT_NONE;
faultNeedsPedalRelease = true;
enterState(IDLE);
Serial.println(F("Falha limpa; solte o pedal"));
}
void startStroke() {
if (requestedMotorPwm == 0) {
setFault(FAULT_SPEED_ZERO);
return;
}
if (!limitsAreCoherentAtRest()) {
setFault(limitStart.stableActive && limitEnd.stableActive
? FAULT_BOTH_LIMITS
: FAULT_NO_REFERENCE);
return;
}
bool goToEnd = limitStart.stableActive;
motorGoingToEnd = goToEnd;
motorCurrentPwm = 0;
motorTargetPwm = requestedMotorPwm;
applyMotorPwm(0);
originReleased = false;
heaterStartedMs = 0;
heaterHasEnergized = false;
heaterActive = true;
updateHeater();
enterState(goToEnd ? RUN_TO_END : RUN_TO_START);
Serial.println(goToEnd ? F("Curso: A -> B") : F("Curso: B -> A"));
}
void finishStroke() {
setHeaterOff();
setMotorStopImmediate();
incrementCounters();
enterState(IDLE);
Serial.println(F("Curso concluido"));
}
void updateRunningState(bool goingToEnd) {
bool originActive = goingToEnd ? limitStart.stableActive : limitEnd.stableActive;
bool targetActive = goingToEnd ? limitEnd.stableActive : limitStart.stableActive;
if (limitStart.stableActive && limitEnd.stableActive) {
setFault(FAULT_BOTH_LIMITS);
return;
}
if (targetActive) {
finishStroke();
return;
}
if (!originActive) {
originReleased = true;
} else if (!originReleased &&
(millis() - stateStartedMs) >= ORIGIN_RELEASE_TIMEOUT_MS) {
setFault(FAULT_ORIGIN_NOT_RELEASED);
return;
}
if ((millis() - stateStartedMs) >= STROKE_TIMEOUT_MS) {
setFault(FAULT_STROKE_TIMEOUT);
return;
}
if (heaterHasEnergized && (millis() - heaterStartedMs) >= HEATER_MAX_ON_MS) {
setFault(FAULT_HEATER_TIMEOUT);
return;
}
if (requestedMotorPwm == 0) {
setFault(FAULT_SPEED_ZERO);
return;
}
motorTargetPwm = requestedMotorPwm;
updateMotorRamp();
updateHeater();
}
void setupPins() {
pinMode(PIN_LED_STATUS, OUTPUT);
pinMode(PIN_LED_ACTIVITY, OUTPUT);
pinMode(PIN_HEATER_PWM, OUTPUT);
pinMode(PIN_MOTOR_IN_A, OUTPUT);
pinMode(PIN_MOTOR_IN_B, OUTPUT);
digitalWrite(PIN_LED_STATUS, LOW);
digitalWrite(PIN_LED_ACTIVITY, LOW);
analogWrite(PIN_HEATER_PWM, 0);
motorSoftwareDuty = 0;
digitalWrite(PIN_MOTOR_IN_A, LOW);
digitalWrite(PIN_MOTOR_IN_B, LOW);
pinMode(PIN_POT_SPEED, INPUT);
pinMode(PIN_POT_POWER, INPUT);
if (isAssigned(PIN_PEDAL)) {
pinMode(PIN_PEDAL, INPUT_PULLUP);
}
pinMode(PIN_LIMIT_START, INPUT_PULLUP);
pinMode(PIN_LIMIT_END, INPUT_PULLUP);
// Timer2: interrupcao a 31,25 kHz e PWM de 6 bits a aproximadamente 488 Hz.
noInterrupts();
TCCR2A = 0;
TCCR2B = 0;
TCNT2 = 0;
OCR2A = 63;
TCCR2A = _BV(WGM21);
TCCR2B = _BV(CS21);
TIMSK2 = _BV(OCIE2A);
interrupts();
}
void setup() {
setupPins();
Serial.begin(115200);
speedPotFiltered = readAnalogAverage(PIN_POT_SPEED);
powerPotFiltered = readAnalogAverage(PIN_POT_POWER);
requestedMotorPwm = mapSpeedPwm(speedPotFiltered);
requestedHeaterPwm = mapHeaterPwm(powerPotFiltered);
initializeInput(pedal);
initializeInput(limitStart);
initializeInput(limitEnd);
loadLifetimeCounter();
enterState(BOOT);
wdt_enable(WDTO_2S);
Serial.println(F("Embaladora sem display - firmware v4"));
}
void loop() {
wdt_reset();
updateInput(pedal);
updateInput(limitStart);
updateInput(limitEnd);
updatePedalLongPress();
updatePotentiometers();
handleSerialCommands();
if ((millis() - lastSerialStatusMs) >= SERIAL_STATUS_MS) {
lastSerialStatusMs = millis();
printStatus();
}
switch (state) {
case BOOT:
setMotorStopImmediate();
setHeaterOff();
if ((millis() - stateStartedMs) >= INPUT_SETTLE_MS) {
if (!requiredPinsConfigured()) {
setFault(FAULT_PIN_CONFIGURATION);
} else if (!limitsAreCoherentAtRest()) {
setFault(limitStart.stableActive && limitEnd.stableActive
? FAULT_BOTH_LIMITS
: FAULT_NO_REFERENCE);
} else {
enterState(IDLE);
Serial.println(F("Pronta; solte e pressione o pedal para um curso"));
}
}
break;
case IDLE:
setMotorStopImmediate();
setHeaterOff();
if (limitStart.stableActive && limitEnd.stableActive) {
setFault(FAULT_BOTH_LIMITS);
} else if (!limitStart.stableActive && !limitEnd.stableActive) {
setFault(FAULT_NO_REFERENCE);
} else if (pedal.activatedEvent) {
startStroke();
}
break;
case RUN_TO_END:
updateRunningState(true);
break;
case RUN_TO_START:
updateRunningState(false);
break;
case FAULT:
setMotorStopImmediate();
setHeaterOff();
if (!pedal.stableActive) {
faultNeedsPedalRelease = false;
}
if (!faultNeedsPedalRelease && pedalLongPressEvent) {
clearFaultIfSafe();
}
break;
}
setOutputsForState();
}