/*
* Pushbutton Control (+ / -) & Zero-Angle Boot Synchronization
* Hardware: Arduino Uno, A4988, NEMA17, KY-040 Encoder, 16x2 I2C LCD, 2 Pushbuttons
* Optimized for Wokwi Simulation (Fixes Pin Conflicts & LCD I2C Stuttering)
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// --- Pin Definitions ---
const int DIR_PIN = 2; // A4988 Direction control pin
const int STEP_PIN = 3; // A4988 Pulse step pin
const int ENABLE_PIN = 4; // A4988 Active-LOW driver enable pin
// UPDATED FOR ARDUINO UNO: Pins 18/19 shifted to valid digital pins 8/9
const int ENCODER_CLK = 8; // KY-040 Quadrature CLK channel (Arduino Uno Pin 8)
const int ENCODER_DT = 9; // KY-040 Quadrature DT channel (Arduino Uno Pin 9)
const int ENCODER_SW = 5; // KY-040 Switch pin (optional/unused)
const int BTN_PLUS = 6; // Pushbutton for CW direction
const int BTN_MINUS = 7; // Pushbutton for CCW direction
// --- Peripherals & Motor Parameters ---
LiquidCrystal_I2C lcd(0x27, 16, 2);
const unsigned long STEP_DELAY_MICROS = 1000; // Step pulse interval in microseconds (1 ms)
const int STEPS_PER_REV = 200; // NEMA17 full steps per revolution (1.8 deg/step)
const int ENCODER_PPR = 20; // KY-040 resolution (18.0 deg/pulse)
// --- State Variables ---
unsigned long lastStepTime = 0;
unsigned long lastLcdTime = 0;
long totalSteps = 0; // Tracks absolute cumulative steps
bool motorRunning = false;
bool motorDirection = HIGH; // HIGH = Clockwise (CW), LOW = Counter-Clockwise (CCW)
int lastClkState;
// --- Smart LCD Refresh Tracking Variables ---
long lastDisplayedSteps = -999999; // Stores last displayed step count to prevent unnecessary redraws
bool lastDisplayedRunning = false; // Stores last displayed motor running state
bool lastDisplayedDirection = HIGH; // Stores last displayed motor direction state
void setup() {
Serial.begin(115200); // Initialize hardware serial communication at 115200 bps for telemetry/debugging
// Boost I2C bus speed to Fast Mode (400 kHz) to minimize CPU blocking overhead during LCD updates
Wire.begin();
Wire.setClock(400000);
// Pin Modes Configuration
pinMode(DIR_PIN, OUTPUT); // Configure direction pin as output (CW/CCW control)
pinMode(STEP_PIN, OUTPUT); // Configure step pin as output for step pulse generation
pinMode(ENABLE_PIN, OUTPUT); // Configure enable pin as output (Active-LOW driver power control)
digitalWrite(ENABLE_PIN, HIGH); // Set HIGH to keep driver power stage initially disabled at boot
pinMode(ENCODER_CLK, INPUT_PULLUP); // Set encoder CLK pin as input with internal pull-up
pinMode(ENCODER_DT, INPUT_PULLUP); // Set encoder DT pin as input with internal pull-up
pinMode(ENCODER_SW, INPUT_PULLUP); // Set encoder switch pin as input with internal pull-up
pinMode(BTN_PLUS, INPUT_PULLUP); // Configure (+/CW) pushbutton as active-low input
pinMode(BTN_MINUS, INPUT_PULLUP); // Configure (-/CCW) pushbutton as active-low input
// Display Initialization
lcd.init(); // Initialize I2C LCD controller
lcd.backlight(); // Turn on screen backlight
lcd.clear(); // Clear display memory buffer
lcd.setCursor(0, 0); lcd.print("Angle:"); // Render static label "Angle:"
lcd.setCursor(0, 1); lcd.print("Dir :"); // Render static label "Dir :"
lcd.setCursor(7, 0); lcd.print("0.0"); lcd.print((char)223); // Display initial angle "0.0" and degree symbol
lcd.setCursor(7, 1); lcd.print("STOP"); // Display initial motor state "STOP"
// Read baseline state after pullup stabilization
lastClkState = digitalRead(ENCODER_CLK); // Capture initial encoder CLK state for edge detection reference
}
void loop() { // Main execution loop acting as non-blocking scheduler
// 1. Read Inputs & Determine Direction (XOR Mutual Exclusion Logic)
bool plusActive = (digitalRead(BTN_PLUS) == LOW); // Active-LOW check for CW button
bool minusActive = (digitalRead(BTN_MINUS) == LOW); // Active-LOW check for CCW button
if (plusActive != minusActive) { // XOR condition: true strictly when one button is pressed
motorRunning = true; // Enable motion flag
motorDirection = plusActive ? HIGH : LOW; // Assign direction: HIGH for CW, LOW for CCW
} else {
motorRunning = false; // Stop motor if no button or both buttons are pressed
}
// 2. Non-Blocking Stepper Motor Control Sequence
if (motorRunning) {
digitalWrite(ENABLE_PIN, LOW); // Assert active-LOW enable line to power H-bridge MOSFETs
digitalWrite(DIR_PIN, motorDirection); // Apply directional logic level to DIR pin
if (micros() - lastStepTime >= STEP_DELAY_MICROS) { // Check step interval timestamp
lastStepTime = micros(); // Reset step baseline timestamp
// Generate 5us active STEP pulse
digitalWrite(STEP_PIN, HIGH); // Set pulse HIGH
delayMicroseconds(5); // Minimum pulse width threshold for A4988 driver
digitalWrite(STEP_PIN, LOW); // Set pulse LOW
// Accumulate total steps for software-estimated position tracking
totalSteps += (motorDirection == HIGH) ? 1 : -1;
}
} else {
digitalWrite(ENABLE_PIN, HIGH); // De-assert ENABLE line (HIGH) to prevent continuous coil heating when idle
}
// 3. Manual Encoder Reading (Polling Edge Detection)
int currentClkState = digitalRead(ENCODER_CLK);
if (currentClkState != lastClkState && currentClkState == LOW) { // Detect falling edge on CLK
// 1 encoder pulse (18.0 deg) maps directly to 10 motor steps (1.8 deg x 10 = 18.0 deg)
int stepChange = (digitalRead(ENCODER_DT) != currentClkState) ? 10 : -10;
totalSteps += stepChange; // Synchronize manual encoder turns into global step accumulator
}
lastClkState = currentClkState;
// 4. Smart Non-Blocking Display Refresh Engine (State-Driven Update)
if (millis() - lastLcdTime >= 100) {
// Smart Check: Only trigger I2C transfer IF total steps OR motor running state OR direction actually changed
if (totalSteps != lastDisplayedSteps ||
motorRunning != lastDisplayedRunning ||
motorDirection != lastDisplayedDirection) {
lastLcdTime = millis();
lastDisplayedSteps = totalSteps;
lastDisplayedRunning = motorRunning;
lastDisplayedDirection = motorDirection;
// Calculate continuous real-time angle (0.0° - 359.9°)
float currentAngle = (totalSteps % STEPS_PER_REV) * (360.0 / STEPS_PER_REV);
if (currentAngle < 0) currentAngle += 360.0; // Normalize negative angles for CCW rotations
// Update Angle Line
lcd.setCursor(7, 0);
lcd.print(currentAngle, 1);
lcd.print((char)223);
lcd.print(" "); // Trailing spaces clear leftover characters without calling lcd.clear()
// Update Direction Line
lcd.setCursor(7, 1);
if (!motorRunning) {
lcd.print("STOP ");
} else {
lcd.print(motorDirection == HIGH ? "CW " : "CCW ");
}
}
}
}