Module 4: Your First Arduino Sketch
Level: 🟢 Beginner Board: Arduino Uno Prerequisites: Module 03 Estimated time: 60–90 minutes Goal: Upload a working motor-control sketch that drives both motors forward and backward under code control.
What You'll Learn
By the end of this module you will understand the setup/loop execution model, know when to use digitalWrite versus analogWrite, and have written clean motor-control functions. The lab culminates in a robot that drives a recognizable square pattern using nothing but timing and direction logic.
4.1 The two functions every sketch must have
Every Arduino program (called a sketch) must contain exactly two functions: setup() and loop(). The board will not compile without both. This is not arbitrary. It reflects exactly how a microcontroller runs: it initializes once, then repeats a task forever until power is cut.
<svg viewBox="0 0 760 300" width="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="aB" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a1a18" stroke-width="1.5" stroke-linecap="round"/></marker>
<marker id="aG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a6b4a" stroke-width="1.5" stroke-linecap="round"/></marker>
<marker id="aO" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#c8420a" stroke-width="1.5" stroke-linecap="round"/></marker>
</defs>
<!-- POWER ON box -->
<rect x="310" y="20" width="140" height="40" rx="20" fill="#3d3c38" stroke="#6b6a65" stroke-width="1.5"/>
<text x="380" y="45" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="12" fill="#f5f2eb">POWER ON</text>
<line x1="380" y1="60" x2="380" y2="80" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aB)"/>
<!-- setup() box -->
<rect x="270" y="80" width="220" height="70" rx="6" fill="#daeaf5" stroke="#1f4d8c" stroke-width="1.5"/>
<text x="380" y="108" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="14" font-weight="500" fill="#1f4d8c">setup()</text>
<text x="380" y="128" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#2c4a6e">Runs ONCE at startup.</text>
<text x="380" y="142" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#2c4a6e">Set pin modes, start Serial.</text>
<line x1="380" y1="150" x2="380" y2="170" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aB)"/>
<!-- loop() box -->
<rect x="270" y="170" width="220" height="70" rx="6" fill="#d8f0e5" stroke="#1a6b4a" stroke-width="1.5"/>
<text x="380" y="198" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="14" font-weight="500" fill="#1a6b4a">loop()</text>
<text x="380" y="218" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#0e3d28">Runs FOREVER, top to bottom,</text>
<text x="380" y="232" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#0e3d28">then starts again immediately.</text>
<!-- feedback loop arrow -->
<path d="M490 205 Q560 205 560 255 Q560 280 490 280 L490 240" fill="none" stroke="#1a6b4a" stroke-width="1.5" stroke-dasharray="5 3" marker-end="url(#aG)"/>
<text x="570" y="248" font-family="IBM Plex Mono,monospace" font-size="11" fill="#1a6b4a">repeats</text>
<text x="570" y="264" font-family="IBM Plex Mono,monospace" font-size="11" fill="#1a6b4a">forever</text>
<!-- side annotation: timing -->
<rect x="30" y="80" width="220" height="160" rx="4" fill="#f5f2eb" stroke="#c4bfb0" stroke-width="1"/>
<text x="40" y="104" font-family="IBM Plex Mono,monospace" font-size="11" font-weight="500" fill="#1a1a18">Typical loop speed</text>
<line x1="40" y1="112" x2="238" y2="112" stroke="#c4bfb0" stroke-width="1"/>
<text x="40" y="132" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">Empty loop:</text>
<text x="40" y="148" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a"> ~100,000/sec</text>
<text x="40" y="168" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">With Serial.print:</text>
<text x="40" y="184" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a"> ~10,000/sec</text>
<text x="40" y="204" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">With delay(1000):</text>
<text x="40" y="220" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a"> ~1/sec</text>
<text x="40" y="236" font-family="Manrope,sans-serif" font-size="10" fill="#6b6a65">delay() BLOCKS everything</text>
</svg>
Fig 4.1: Arduino execution model. setup() runs once when power is applied. loop() runs continuously, restarting immediately each time it reaches the closing brace. A delay() call inside loop() pauses the entire program; the board cannot do anything else while waiting. This is why delay-based motion code breaks in Module 9 and must be replaced with timers.
The most important thing to understand about loop() is that it never stops on its own. Once your sketch is uploaded, the board runs loop() over and over until you cut power or upload a new sketch. Every decision your robot makes (read sensor, check condition, act) happens in one pass through loop(). Make that pass fast and your robot reacts quickly. Fill it with delay() calls and your robot goes deaf while waiting.
delay() is a learning tool. It is not how production robot code works. You will use it now, and you will replace it in Module 9.
NiMH users: Four NiMH cells produce 4.8 V, which is below the Arduino Uno's recommended Vin minimum of 5.5 V. If you are using NiMH batteries, power the Arduino via the L298N's 5 V output pin instead of Vin. The jumper/PWREN setup is described below.
Advanced path When operating the robot away from a laptop, the Arduino needs its own power source. The L298N breakout module has a small onboard 5V regulator. How you enable it varies by board variant:
Jumper variant (most common): A 2-pin jumper labeled 5V EN or +5V EN sits near the voltage regulator. With this jumper installed, the regulator is active and the module's 5V pin outputs regulated power. Connect L298N 5V → Arduino 5V pin.
PWREN variant: Some boards use a pin labeled PWREN instead of a jumper. Bridging PWREN to VCC (or leaving it unconnected with a solder bridge on the PCB) enables the regulator; check your module's silkscreen or datasheet.
No onboard regulator variant: A few bare L298N modules omit the regulator entirely. If your module has no jumper or PWREN pin and the 5V pad is absent, power the Arduino separately via its barrel jack from the battery (7–9V) or use a dedicated 5V step-down regulator. Without any of these connections, the robot only works when the USB cable is plugged in.
4.2 digitalWrite vs analogWrite
The Arduino controls the motor driver through its output pins. Two different functions do two different jobs, and confusing them is the most common first-sketch mistake.
<svg viewBox="0 0 760 260" width="100%" xmlns="http://www.w3.org/2000/svg">
<!-- LEFT: digitalWrite -->
<text x="190" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="12" letter-spacing="1" fill="#1f4d8c">digitalWrite()</text>
<text x="190" y="40" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">full ON or full OFF — no middle</text>
<line x1="40" y1="200" x2="340" y2="200" stroke="#c4bfb0" stroke-width="1"/>
<line x1="40" y1="70" x2="40" y2="205" stroke="#c4bfb0" stroke-width="1"/>
<text x="30" y="78" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">5V</text>
<text x="30" y="205" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">0V</text>
<polyline points="50,200 50,80 140,80 140,200 150,200 150,80 240,80 240,200 250,200 250,80 330,80" fill="none" stroke="#1f4d8c" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="95" y="75" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1f4d8c">HIGH</text>
<text x="145" y="215" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">LOW</text>
<text x="190" y="240" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">used for IN1–IN4 (direction)</text>
<!-- RIGHT: analogWrite / PWM -->
<text x="570" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="12" letter-spacing="1" fill="#c8420a">analogWrite()</text>
<text x="570" y="40" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">rapid on/off — duty cycle sets speed</text>
<line x1="420" y1="200" x2="720" y2="200" stroke="#c4bfb0" stroke-width="1"/>
<line x1="420" y1="70" x2="420" y2="205" stroke="#c4bfb0" stroke-width="1"/>
<text x="410" y="78" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">5V</text>
<text x="410" y="205" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">0V</text>
<polyline points="430,200 430,80 444,80 444,200 468,200 468,80 482,80 482,200 506,200" fill="none" stroke="#c8420a" stroke-width="2" opacity=".4"/>
<text x="468" y="68" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8420a" opacity=".6">25%</text>
<polyline points="510,200 510,80 530,80 530,200 550,200 550,80 570,80 570,200 590,200" fill="none" stroke="#c8420a" stroke-width="2" opacity=".7"/>
<text x="550" y="68" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8420a" opacity=".8">50%</text>
<polyline points="600,200 600,80 626,80 626,200 640,200 640,80 666,80 666,200 680,200" fill="none" stroke="#c8420a" stroke-width="2"/>
<text x="643" y="68" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8420a">75%</text>
<text x="570" y="228" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">analogWrite(pin, 0–255) → 0–100% duty</text>
<text x="570" y="244" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">used for ENA/ENB (speed)</text>
</svg>
Fig 4.2: digitalWrite() sets a pin fully HIGH (5V) or LOW (0V). analogWrite() rapidly switches the pin between HIGH and LOW; the ratio of ON time to total time is the duty cycle. The motor driver averages this into a proportional speed. analogWrite(pin, 255) is full speed. analogWrite(pin, 128) is half speed. analogWrite(pin, 0) is stopped.
The direction pins (IN1–IN4) use digitalWrite() because they are binary choices: forward or backward, no halfway. The enable pins (ENA/ENB) use analogWrite() because speed is continuous; you want the full range from stopped to flat out. The confusion to watch for: analogWrite() only works on pins marked with a tilde (~) on the Arduino board. D3, D5, D6, D9, D10, D11 are the PWM-capable pins. If you wire ENA to a non-PWM pin and call analogWrite(), the pin will not respond and the motor will not move.
4.3 Writing clean motor control code
Raw pin manipulation scattered through your main logic makes code hard to read and harder to debug. Wrap motor commands in named functions instead. Write them once, call them anywhere. When the pin assignments change (and they will), you change one number in one place.
// Pin assignments — change these if your wiring differs
const int ENA = 5; // PWM — left motor speed
const int IN1 = 7; // left motor direction A
const int IN2 = 8; // left motor direction B
const int ENB = 6; // PWM — right motor speed
const int IN3 = 9; // right motor direction A
const int IN4 = 10; // right motor direction B
void setup() {
pinMode(ENA, OUTPUT); pinMode(ENB, OUTPUT);
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
stopMotors(); // safe state on power-up — motors off before any command runs
Serial.begin(9600);
}
// speed: 0 (stopped) to 255 (full)
void driveForward(int speed) {
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
analogWrite(ENA, speed); analogWrite(ENB, speed);
}
void driveBackward(int speed) {
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH);
analogWrite(ENA, speed); analogWrite(ENB, speed);
}
void turnLeft(int speed) {
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); // left motor reverse
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW); // right motor forward
analogWrite(ENA, speed); analogWrite(ENB, speed);
}
void stopMotors() {
analogWrite(ENA, 0); analogWrite(ENB, 0);
}
void turnRight(int speed) {
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); // left motor forward
digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH); // right motor reverse
analogWrite(ENA, speed); analogWrite(ENB, speed);
}
<svg viewBox="0 0 760 220" width="100%" xmlns="http://www.w3.org/2000/svg">
<text x="380" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="12" letter-spacing="1" fill="#6b6a65">L298N DIRECTION TRUTH TABLE</text>
<!-- Table header -->
<rect x="40" y="34" width="680" height="28" rx="3" fill="#1a1a18"/>
<text x="100" y="53" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">IN1</text>
<text x="170" y="53" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">IN2</text>
<text x="270" y="53" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">IN3</text>
<text x="340" y="53" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">IN4</text>
<text x="470" y="53" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">LEFT MOTOR</text>
<text x="630" y="53" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">RIGHT MOTOR</text>
<line x1="40" y1="62" x2="720" y2="62" stroke="#333" stroke-width="1"/>
<line x1="220" y1="34" x2="220" y2="220" stroke="#333" stroke-width="1"/>
<line x1="400" y1="34" x2="400" y2="220" stroke="#333" stroke-width="1"/>
<!-- Row 1: Forward -->
<rect x="40" y="62" width="680" height="32" fill="#edeae0"/>
<text x="100" y="83" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#1a6b4a">HIGH</text>
<text x="170" y="83" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">LOW</text>
<text x="270" y="83" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#1a6b4a">HIGH</text>
<text x="340" y="83" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">LOW</text>
<text x="470" y="83" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#1a6b4a">▶ Forward</text>
<text x="630" y="83" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#1a6b4a">▶ Forward</text>
<!-- Row 2: Backward -->
<rect x="40" y="94" width="680" height="32" fill="#f5f2eb"/>
<text x="100" y="115" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">LOW</text>
<text x="170" y="115" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#c8420a">HIGH</text>
<text x="270" y="115" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">LOW</text>
<text x="340" y="115" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#c8420a">HIGH</text>
<text x="470" y="115" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#c8420a">◀ Backward</text>
<text x="630" y="115" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#c8420a">◀ Backward</text>
<!-- Row 3: Turn left -->
<rect x="40" y="126" width="680" height="32" fill="#edeae0"/>
<text x="100" y="147" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">LOW</text>
<text x="170" y="147" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#c8420a">HIGH</text>
<text x="270" y="147" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#1a6b4a">HIGH</text>
<text x="340" y="147" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">LOW</text>
<text x="470" y="147" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#c8420a">◀ Reverse</text>
<text x="630" y="147" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#1a6b4a">▶ Forward</text>
<!-- Row 4: Stop -->
<rect x="40" y="158" width="680" height="32" fill="#f5f2eb"/>
<text x="100" y="179" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">any</text>
<text x="170" y="179" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">any</text>
<text x="270" y="179" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">any</text>
<text x="340" y="179" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">any</text>
<text x="470" y="179" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#6b6a65">■ Stop (ENA=0)</text>
<text x="630" y="179" text-anchor="middle" font-family="Manrope,sans-serif" font-size="12" fill="#6b6a65">■ Stop (ENB=0)</text>
<line x1="40" y1="190" x2="720" y2="190" stroke="#c4bfb0" stroke-width="1"/>
<text x="380" y="210" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">Turn right: reverse IN1/IN2 and IN3/IN4 relative to turn left</text>
</svg>
Fig 4.3: L298N direction truth table. IN1/IN2 control left motor direction; IN3/IN4 control right. Setting both pins the same (both HIGH or both LOW) results in brake, which is not the same as coasting stop. Speed (ENA/ENB) at zero gives a clean stop regardless of IN states.
4.4 Driving a square using timing
Without encoders or GPS, the only way to make your robot drive a defined path is to calculate how long each segment takes at a given speed, and use delay() to time it. This is called open-loop control: you send a command and trust the robot to execute it, with no feedback about whether it actually did. It works well enough to pass the milestone. It falls apart on uneven floors, tired batteries, or motor wear. You will replace it with better methods later.
<svg viewBox="0 0 760 300" width="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="aSq" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1f4d8c" stroke-width="1.5" stroke-linecap="round"/></marker>
</defs>
<!-- square path -->
<path d="M160 240 L160 80 L320 80 L320 240 L160 240" fill="none" stroke="#1f4d8c" stroke-width="2" stroke-dasharray="8 4" marker-end="url(#aSq)"/>
<!-- start point -->
<circle cx="160" cy="240" r="8" fill="#c8420a"/>
<text x="148" y="262" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a">START</text>
<!-- corner labels -->
<circle cx="160" cy="80" r="5" fill="#1f4d8c"/>
<circle cx="320" cy="80" r="5" fill="#1f4d8c"/>
<circle cx="320" cy="240" r="5" fill="#1f4d8c"/>
<!-- turn indicators -->
<path d="M172 80 Q188 64 204 80" fill="none" stroke="#1a6b4a" stroke-width="1.5" marker-end="url(#aSq)"/>
<path d="M320 92 Q336 108 320 124" fill="none" stroke="#1a6b4a" stroke-width="1.5" marker-end="url(#aSq)"/>
<path d="M308 240 Q292 256 276 240" fill="none" stroke="#1a6b4a" stroke-width="1.5" marker-end="url(#aSq)"/>
<!-- dimension arrows -->
<line x1="130" y1="80" x2="130" y2="240" stroke="#6b6a65" stroke-width="1"/>
<line x1="125" y1="80" x2="135" y2="80" stroke="#6b6a65" stroke-width="1"/>
<line x1="125" y1="240" x2="135" y2="240" stroke="#6b6a65" stroke-width="1"/>
<text x="118" y="165" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">~1m</text>
<line x1="160" y1="56" x2="320" y2="56" stroke="#6b6a65" stroke-width="1"/>
<line x1="160" y1="51" x2="160" y2="61" stroke="#6b6a65" stroke-width="1"/>
<line x1="320" y1="51" x2="320" y2="61" stroke="#6b6a65" stroke-width="1"/>
<text x="240" y="50" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">~1m</text>
<!-- Step-by-step sequence panel -->
<rect x="420" y="30" width="310" height="240" rx="5" fill="#1a1a18"/>
<text x="575" y="56" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">loop() sequence</text>
<line x1="430" y1="64" x2="720" y2="64" stroke="#333" stroke-width="1"/>
<text x="434" y="84" font-family="IBM Plex Mono,monospace" font-size="10" fill="#a8e8c8">driveForward(180);</text>
<text x="434" y="100" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">delay(1400); // ~1m</text>
<text x="434" y="120" font-family="IBM Plex Mono,monospace" font-size="10" fill="#a8e8c8">stopMotors();</text>
<text x="434" y="136" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">delay(200);</text>
<text x="434" y="156" font-family="IBM Plex Mono,monospace" font-size="10" fill="#a8e8c8">turnLeft(180);</text>
<text x="434" y="172" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">delay(500); // ~90°</text>
<text x="434" y="192" font-family="IBM Plex Mono,monospace" font-size="10" fill="#a8e8c8">stopMotors();</text>
<text x="434" y="208" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">delay(200);</text>
<text x="434" y="228" font-family="Manrope,sans-serif" font-size="10" fill="#555">repeat × 4 sides</text>
<text x="434" y="248" font-family="Manrope,sans-serif" font-size="10" fill="#555">tune delay values for your robot</text>
</svg>
Fig 4.4: Driving a square by timing. Each side is a driveForward() call followed by a delay() for travel time, then a turnLeft() call followed by a delay() for the 90° rotation time. The exact delay values depend on your motor speed and floor; tune them empirically. A brief stopMotors() between segments prevents the momentum of one move carrying into the next.
Advanced path analogWrite() does not output a true analog voltage. The Arduino's output pins are digital; they can only be 0V or 5V. PWM (Pulse Width Modulation) simulates a middle value by switching between 0 and 5V at approximately 490Hz on pins D9/D10/D11 (Timer1/Timer2) and 976Hz on pins D5/D6 (Timer0). At 50% duty cycle, the pin spends equal time at 0V and 5V. The motor's electrical inductance smooths these pulses into an average voltage. The motor "sees" approximately 2.5V, or half speed. You can verify this with an oscilloscope: the signal looks like a square wave, not a slope.
The frequency matters: too slow and the motor twitches instead of running smoothly. Too fast and the switching losses in the driver increase. 490Hz (Arduino's default) works fine for DC motors at this scale.
4.5 Serial Monitor: your first debugging tool
The Serial Monitor is the channel between your running sketch and your laptop. Serial.print() calls let you see what the robot is thinking: what sensor values it's reading, what state it's in, what it's about to do. Without it, debugging is guesswork.
// In setup():
Serial.begin(9600); // baud rate must match Tools → Serial Monitor
// In loop():
Serial.print("Speed: ");
Serial.println(speed); // println adds newline — print does not
// Useful for state reporting:
Serial.println("Moving forward");
Two things bite beginners here. First: the baud rate in Serial.begin() must match the baud rate selected in the Serial Monitor window. 9600 is the conventional starting point; use it until you have a reason to change. Second: Serial.print() is slow. Calling it every loop pass when loop() runs 10,000 times per second will flood the serial buffer and make your sketch crawl. Print only when something changes, or add a counter to print every 100th pass.
Lab 04: Drive the robot in a square
Write a sketch that drives the robot in a square pattern: four sides, four 90° left turns, back to approximately where it started. The exact dimensions do not matter. Clean corners and consistent geometry do.
Safety first: Before uploading any motor sketch, hold the robot in the air or prop it on a cup so the wheels spin freely. The motors may start immediately after upload. Once you confirm the sketch works as expected, set the robot on the ground for a full test run.
Safety note (pinch points): Keep fingers clear of the drive wheels while the robot is powered. The gap between the wheel and chassis is a pinch point; a moving wheel can trap a fingertip, especially during pivot turns when both wheels spin simultaneously. Place the robot on the floor or a raised surface where it can move freely before running the sketch. Never hold the robot in your hand while the motors are running.
Steps
Open the Arduino IDE. Create a new sketch. Set up your pin constants at the top.
Write the pinMode() calls in setup() for all six motor control pins.
Route all wires above the chassis top surface. Group parallel wires with a small rubber band or zip tie. Keep a minimum 30mm clearance from all wheel positions; wires that hang below the chassis will catch in the drive wheels.
Write driveForward(), turnLeft(), and stopMotors() functions.
In loop(), call driveForward(180) then delay(). Start with delay(1000); you will tune this. Then stopMotors() and delay(200). Then turnLeft(180) and delay(500). Then stopMotors() and delay(200). Then repeat the block three more times for the remaining sides.
Upload the sketch. Watch the robot move. Note how far it travels and whether the turns are actually 90°.
Straight-line check (before tuning delays): Run
driveForward(CRUISE)for 2 seconds on a straight tape line. If the robot drifts more than 5 cm, fix it mechanically by checking motor bracket angles from Module 2. Bracket realignment is a 5-minute job: loosen the motor bracket bolts, sight both shafts along the 1 m tape line from Module 2, and retighten in diagonal pairs. Do not compensate with delay timing; timing offsets mask mechanical problems and produce larger errors in Module 5.Tune the delay values. If the robot overshoots the turn, reduce the turn delay. If sides are too short, increase the forward delay. Small increments, 50ms at a time.
Add Serial.println() calls before each movement to log state to the monitor while it runs.
After the robot completes the square, add a long stopMotors() + delay(5000) at the end of loop() so it pauses before repeating rather than driving continuously.
In Module 6 we will replace these instant speed writes with smooth acceleration ramps. This open-loop foundation is the starting point for that refinement.
Module milestone The robot drives a recognizable square on the floor, with four sides of roughly equal length and four clean turns, returning within about 20cm of its starting point. Ready for Module 5.
L298N heat. After running the sketch, the top of the L298N chip body will feel warm. This is normal. During sustained pivot turns, when both motors reverse simultaneously, the chip can reach 60–70°C — hot enough to cause a burn. Do not press your finger against the heatsink fin after extended operation. The module is rated for this temperature and will not be damaged; just keep your fingers away until it cools.
Self-Check: Module 4
- I can explain why setup() runs once and loop() runs forever.
- I know which Arduino pins support PWM and why non-PWM pins will not work with analogWrite().
- I have written separate named functions for driveForward(), driveBackward(), turnLeft(), turnRight(), and stopMotors().
- I called stopMotors() at the end of setup() so the robot starts in a safe state.
- I used Serial.begin() and Serial.println() to log state to the Serial Monitor.
- My robot drove a recognizable square and returned within roughly 20cm of the start.
- I tuned the delay values incrementally (50ms steps) rather than guessing large numbers.
Key Terms Glossary
| Term | Definition |
|---|---|
| sketch | Arduino's name for a program. Must contain setup() and loop(). Compiled to machine code and stored in flash memory on the microcontroller. |
| setup() | The initialization function that runs once when the Arduino powers on or resets. Use it for pinMode(), Serial.begin(), and any one-time configuration. |
| loop() | The main execution function. Runs repeatedly until power is cut. Every sensor read, decision, and motor command happens in here or in functions called from here. |
| digitalWrite() | Sets a digital pin to HIGH (5V) or LOW (0V). Binary, no middle value. Used for direction control signals to the motor driver. |
| analogWrite() | Outputs a PWM signal on a PWM-capable pin (~). Takes a value 0–255. Used for motor speed control. Only works on pins marked with tilde on the board. |
| delay() | Pauses the entire program for the specified number of milliseconds. While waiting, nothing else runs: no sensor reads, no serial output. Useful for learning, problematic in production code. |
| PWM | Pulse Width Modulation. A technique for simulating variable voltage by rapidly switching a digital pin on and off. The duty cycle (% of time HIGH) determines the effective voltage. |
| open-loop control | Commanding a robot without feedback. You tell it to run for 1.4 seconds and trust it went straight. No measurement of what actually happened. Works on clean floors with fresh batteries. |
Previous: ← Module 03: Power & Wiring Basics · Next: Module 05: Differential Steering & Turning →