🔌

Arduino Professional

Build a complete embedded motor controller: a potentiometer sets fan speed via PWM, a push-button toggles direction through an H-bridge, a status LED shows running state, and Serial streams live telemetry — all without a single blocking delay().

6 lessons 8 tasks
Lessons Arduino Lab Quiz Certificate

📚 Lessons

1 System design: thinking in blocks

Professional embedded code starts on paper, not in the IDE. Before writing a single line, draw a block diagram showing every input, output, and the logic that connects them.

Our smart fan has four blocks:

  • Speed input — a potentiometer on A0 produces a raw value 0–1023.
  • Direction input — a push-button on pin 2 (INPUT_PULLUP) toggles forward / reverse.
  • Actuator — an L298N H-bridge drives a DC motor; ENA → pin 6 (PWM), IN1 → pin 4, IN2 → pin 5.
  • Status output — a green LED on pin 13 glows while the motor is running; Serial streams speed and direction every 500 ms.

This separation matters: each block can be coded, tested, and debugged independently before you wire them together.

Why an H-bridge? A microcontroller pin can only source a few milliamps — far too little for a motor. The L298N is a power driver: it takes logic-level signals (IN1, IN2) and switches up to 2 A from a separate 12 V supply. Setting IN1 HIGH, IN2 LOW spins the motor one way; reversing them reverses it. ENA is the PWM speed pin — higher duty cycle = more average voltage = faster spin.

2 Non-blocking timing with millis()

delay(ms) is a blocking call: the processor sits idle, doing nothing, for the entire duration. That means you cannot read buttons, update PWM, or respond to any event while a delay is running.

millis() returns the number of milliseconds since the board started. The non-blocking pattern stores a timestamp and checks whether enough time has elapsed:

unsigned long prevMs = 0;\nconst long INTERVAL = 500;\n\nvoid loop() {\n  unsigned long now = millis();\n  if (now - prevMs >= INTERVAL) {\n    prevMs = now;   // save the new timestamp\n    // do the periodic work here\n  }\n  // other code runs every loop — nothing is blocked\n}

Key points:

  • Use unsigned long (not int) — millis() overflows after ~50 days; the subtraction now - prevMs still works correctly with unsigned wrap-around.
  • Update prevMs = now (not prevMs += INTERVAL) only if you want drift-tolerant timing. Use prevMs += INTERVAL for perfectly even intervals.
  • The virtual clock in this simulator advances ~1 ms per delay-free loop iteration, so millis()-based code works exactly as on real hardware.
// Non-blocking blink — LED on pin 13 toggles every 500 ms
// without freezing anything else
unsigned long prevMs = 0;
bool ledState = false;

void setup() {
  pinMode(13, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  unsigned long now = millis();
  if (now - prevMs >= 500) {
    prevMs = now;
    ledState = !ledState;
    digitalWrite(13, ledState ? HIGH : LOW);
    Serial.print("uptime ms: ");
    Serial.println(now);
  }
  // read sensors here — not blocked!
}

3 Reading pot + button together

Reading multiple inputs in a single loop() is straightforward — but you must handle each correctly:

Potentiometer (A0)analogRead(A0) returns 0–1023. We use map() to scale to the PWM range 0–255 for the motor speed.

Button (pin 2, INPUT_PULLUP) — idles HIGH; reads LOW when pressed. To detect a single press (not a continuous hold), we compare the current reading to the previous reading and act only on the falling edge (HIGH→LOW transition). This is called edge detection and it also provides basic software debouncing.

Debouncing in depth: mechanical buttons bounce — they flicker between open and closed several times in the first ~10 ms of a press. The edge-detection pattern with a small delay(20) or a millis()-based 20 ms lockout is enough for most buttons. More robust projects use a dedicated state-machine debouncer, but for this course the simple approach is fine.

bool dirForward = true;
bool lastBtn = HIGH;

void setup() {
  pinMode(2, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  // --- read speed ---
  int raw = analogRead(A0);               // 0–1023
  int speed = map(raw, 0, 1023, 0, 255); // scale to PWM

  // --- read direction button (edge detection) ---
  bool btn = digitalRead(2);              // LOW = pressed
  if (btn == LOW && lastBtn == HIGH) {    // falling edge only
    dirForward = !dirForward;            // toggle
  }
  lastBtn = btn;

  Serial.print("speed=");
  Serial.print(speed);
  Serial.print(" dir=");
  Serial.println(dirForward ? 1 : 0);
  delay(20);
}

4 H-bridge direction state machine

An H-bridge routes current through a motor in either direction using four transistor switches arranged like the letter H. The L298N exposes this via two logic pins:

IN1IN2Motor
HIGHLOWForward
LOWHIGHReverse
LOWLOWCoast (free)
HIGHHIGHBrake (short)

Speed is set on ENA (pin 6) with analogWrite(6, duty). Setting ENA to 0 stops the motor regardless of IN1/IN2. We model this as a two-state machine:

if (dirForward) {\n  digitalWrite(4, HIGH);  // IN1\n  digitalWrite(5, LOW);   // IN2\n} else {\n  digitalWrite(4, LOW);\n  digitalWrite(5, HIGH);\n}\nanalogWrite(6, speed);  // ENA — sets RPM

The status LED on pin 13 should reflect whether the motor is actually running: it is on whenever speed > 0.

bool dirForward = true;
bool lastBtn = HIGH;

void setup() {
  pinMode(4, OUTPUT);   // IN1
  pinMode(5, OUTPUT);   // IN2
  pinMode(6, OUTPUT);   // ENA (PWM speed)
  pinMode(13, OUTPUT);  // status LED
  pinMode(2, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  int raw   = analogRead(A0);
  int speed = map(raw, 0, 1023, 0, 255);

  bool btn = digitalRead(2);
  if (btn == LOW && lastBtn == HIGH) dirForward = !dirForward;
  lastBtn = btn;

  if (dirForward) { digitalWrite(4, HIGH); digitalWrite(5, LOW); }
  else            { digitalWrite(4, LOW);  digitalWrite(5, HIGH); }

  analogWrite(6, speed);
  digitalWrite(13, speed > 0 ? HIGH : LOW);

  delay(20);
}

5 Serial telemetry and the full system

Good embedded systems log what they're doing. Serial telemetry lets you watch the system in real time and diagnose problems without a debugger.

We output a compact, machine-readable line every 500 ms using the non-blocking millis() pattern from Lesson 2:

Serial.print(\"spd:\");\nSerial.print(speed);\nSerial.print(\" dir:\");\nSerial.println(dirForward ? \"FWD\" : \"REV\");

Putting it all together — the complete smart-fan sketch:

// Smart-fan: non-blocking, state-machine driven
bool dirForward = true;
bool lastBtn    = HIGH;
unsigned long prevTele = 0;

void setup() {
  pinMode(4, OUTPUT);        // IN1 — H-bridge direction A
  pinMode(5, OUTPUT);        // IN2 — H-bridge direction B
  pinMode(6, OUTPUT);        // ENA — PWM speed
  pinMode(13, OUTPUT);       // status LED
  pinMode(2, INPUT_PULLUP);  // direction button
  Serial.begin(9600);
}

void loop() {
  // 1. Read speed
  int raw   = analogRead(A0);
  int speed = map(raw, 0, 1023, 0, 255);

  // 2. Direction toggle (edge detection = basic debounce)
  bool btn = digitalRead(2);
  if (btn == LOW && lastBtn == HIGH) dirForward = !dirForward;
  lastBtn = btn;

  // 3. Drive H-bridge
  if (dirForward) { digitalWrite(4, HIGH); digitalWrite(5, LOW); }
  else            { digitalWrite(4, LOW);  digitalWrite(5, HIGH); }
  analogWrite(6, speed);

  // 4. Status LED — on while motor is spinning
  digitalWrite(13, speed > 0 ? HIGH : LOW);

  // 5. Non-blocking telemetry every 500 ms
  unsigned long now = millis();
  if (now - prevTele >= 500) {
    prevTele = now;
    Serial.print("spd:");
    Serial.print(speed);
    Serial.print(" dir:");
    Serial.println(dirForward ? 1 : 0);
  }
}

6 Clean structure and good practice

As projects grow, structure prevents bugs. Here are the professional habits demonstrated in the smart-fan sketch:

  • No blocking delay() in the main loop — millis() timing keeps the system responsive at all times.
  • State machines over nested flags — the direction is a single bool dirForward, not multiple fragile variables.
  • Edge detection for buttons — comparing current vs. previous state catches exactly one event per press, regardless of how long the button is held.
  • constrain() as a safety net — even if analogRead or map() returns out-of-range values, a constrain keeps the PWM within 0–255.
  • Descriptive pin #defines — using #define ENA 6 means you change the pin in one place, not scattered through the code.
  • Common ground — the Arduino GND, the H-bridge GND, and the motor power supply GND must all connect together. Floating grounds cause mysterious failures.

With these patterns you can extend the smart-fan: add a temperature sensor that auto-adjusts speed, an LCD display for status, or a second motor channel — all without rewriting the core loop.

🔌 Arduino Lab — simulated Uno R3

Build the circuit (connect each lead to the correct port), then write a sketch, press Upload & Run, and watch the simulated board react — LEDs glow, motors spin, the serial monitor prints. Pass every challenge to earn your certificate.

Wire up the smart-fan system: potentiometer wiper to A0, button to pin 2 (with internal pull-up), H-bridge control pins to 4 (IN1), 5 (IN2), and 6 (ENA, PWM). Status LED anode through a 220 Ω resistor to pin 13, cathode to GND. The H-bridge OUT1/OUT2 connect to the motor terminals.

📝 Tasks

8 tasks across 3 pages — multiple-choice and fill-in (type the answer). Score 70% or higher to earn your certificate.

🎓 Certificate of Completion

🔒 Pass the quiz above (70%+) to unlock your downloadable certificate.