Learning Goals 5 min
Yesterday the motor was either full speed or stopped — nothing in between. Today we throttle it. By the end of this lesson you will be able to:
- Drive a DC motor through an L298N at any speed from 0 to full by applying
analogWrite()to the ENA (or ENB) enable pin — keeping direction control on IN1 / IN2. - Identify and work around the "dead band": the minimum duty cycle below which the motor doesn't spin at all (typically 30–60 of 255 on a hobby motor).
- Build a non-blocking "throttle + reverse" rig where a single potentiometer sets both speed and direction (centre = stop, full clockwise = full forward, full counter-clockwise = full reverse).
Warm-Up 10 min
Rebuild yesterday's L298N wiring (Motor A on OUT1/OUT2, IN1 → D9, IN2 → D8, ENA jumper installed, battery pack on VS/GND, common ground).
The one change for today
Remove the ENA jumper from the L298N board. Run a wire from ENA on the header to a PWM-capable Arduino pin — we'll use D5 (a PWM pin that we haven't taken for Servo or for IN1/IN2).
| Signal | From Arduino pin | To L298N pin |
|---|---|---|
| IN1 | D9 | IN1 |
| IN2 | D8 | IN2 |
| ENA (PWM) | D5 | ENA |
Recall: PWM is "average voltage"
From L02-02 / L02-03 you know analogWrite(pin, duty) sends a square wave that's HIGH for duty/255 of each period and LOW for the rest. The motor sees that as an average voltage = supply × duty/255. So duty 128 ≈ half the supply voltage, duty 64 ≈ a quarter, and so on.
Prediction
If the L298N drops 1.5 V across its transistors and the battery pack supplies 6 V, what's the average voltage at the motor at duty cycle 128?
Reveal
The L298N output rail is about 6 − 1.5 = 4.5 V when the transistors are on. Average across the motor at duty 128 ≈ 4.5 × (128/255) ≈ 2.3 V. For a motor rated 6 V that's well below its starting voltage — at duty 128 the motor may still spin happily if it's already moving, but might not start from rest.
New Concept · PWM, the dead band, and signed speed 25 min
Speed control = PWM on the enable pin
This is the headline. IN1 / IN2 still set direction; the enable pin is PWM'd to set speed.
const int IN1 = 9, IN2 = 8, ENA = 5;
void setMotor(int duty) {
// duty: -255..+255. Positive = forward, negative = reverse, zero = brake.
if (duty > 0) {
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
analogWrite(ENA, duty);
} else if (duty < 0) {
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
analogWrite(ENA, -duty);
} else {
digitalWrite(IN1, LOW); digitalWrite(IN2, LOW);
analogWrite(ENA, 0);
}
}setMotor(150) → forward at 150/255 of full speed. setMotor(-200) → reverse at 200/255. setMotor(0) → brake. This signed-speed convention will reappear everywhere we control motors.
The dead band
Below some minimum duty cycle (often 30–60 out of 255 on a hobby motor) the motor will not move at all. Friction in the brushes, gearbox, and bearings stops the shaft from starting. Once it's moving, you can drop the duty further and it'll keep spinning — but it can't self-start at low duties.
The practical fix is to map your "0% → 100%" control range onto "dead-band → 255" so 0% gives just-enough duty to actually start.
const int DEAD_BAND = 55; // measure for your motor
int withDeadBand(int requestedPct /* 0..100 */) {
if (requestedPct == 0) return 0;
return map(requestedPct, 1, 100, DEAD_BAND, 255);
}Your user / sensor input becomes "motor effort 0–100", and the motor either gets 0 (off) or at least DEAD_BAND (just enough to start).
PWM frequency & motor whine
The UNO's default PWM frequency on pins 5, 6 is ~980 Hz, and on pins 3, 9, 10, 11 it's ~490 Hz. Both are in the human hearing range, and they make a clearly audible whine in the motor — "eeeeeeee". Higher frequencies (20 kHz +) are inaudible but require fiddling with timer registers; we'll meet that in Level 4.
Why the whine matters
For a robot toy it's charming. For a quiet desktop fan or a robotic camera mount, the whine is unprofessional. Three workarounds:
- Bigger smoothing capacitor across the motor: reduces the PWM ripple seen by the motor → quieter.
- Move to pin 5 or 6 (980 Hz is twice as quiet-sounding as 490 Hz).
- Bit-bang a 20 kHz PWM with Timer1 directly (advanced — Level 4).
Brake vs coast at duty 0
When you write duty 0 to ENA, the L298N disconnects the motor — that's coast. With ENA jumpered HIGH (yesterday's default) you couldn't coast. Now you can, by writing 0 to ENA. For an active brake, set IN1 = IN2 = LOW and ENA = 255 — both motor leads tied to GND through the H-bridge.
Worked Example · Potentiometer-controlled throttle + direction 25 min
Step 1 — wiring
| Component | UNO pin |
|---|---|
| L298N IN1 | D9 |
| L298N IN2 | D8 |
| L298N ENA | D5 (PWM) |
| L298N VS / GND | 4 × AA pack +/− |
| L298N GND | Arduino GND (common ground) |
| Pot wiper | A0 |
| Pot outer pins | 5V and GND |
| Motor | OUT1 / OUT2 |
Step 2 — measure your dead band
Upload this throwaway sketch:
const int IN1 = 9, IN2 = 8, ENA = 5;
void setup() {
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); // forward
Serial.begin(9600);
}
void loop() {
for (int duty = 0; duty <= 255; duty += 5) {
analogWrite(ENA, duty);
Serial.print("duty="); Serial.println(duty);
delay(500);
}
analogWrite(ENA, 0);
delay(2000);
}Watch the motor. Write down the duty at which it just starts spinning from rest. That's your DEAD_BAND — likely 30–60 depending on the motor and battery.
Step 3 — the centre-stick throttle sketch
// L03-09 · Pot-controlled throttle with reverse
// Centre pot = stop. Clockwise = forward. Counter-clockwise = reverse.
const int IN1 = 9, IN2 = 8, ENA = 5;
const int POT_PIN = A0;
const int DEAD_ZONE = 30; // pot raw counts either side of centre that mean "stop"
const int DEAD_BAND = 55; // duty below which the motor doesn't start
void setMotor(int duty) {
if (duty > 0) {
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
analogWrite(ENA, duty);
} else if (duty < 0) {
digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
analogWrite(ENA, -duty);
} else {
digitalWrite(IN1, LOW); digitalWrite(IN2, LOW);
analogWrite(ENA, 0);
}
}
void setup() {
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
Serial.begin(9600);
setMotor(0);
}
void loop() {
int raw = analogRead(POT_PIN); // 0..1023
int offset = raw - 512; // -512..+511
int duty;
if (abs(offset) < DEAD_ZONE) {
duty = 0;
} else if (offset > 0) {
duty = map(offset, DEAD_ZONE, 511, DEAD_BAND, 255);
} else {
duty = map(offset, -DEAD_ZONE, -511, -DEAD_BAND, -255);
}
setMotor(duty);
static unsigned long lastLog = 0;
if (millis() - lastLog >= 200) {
lastLog = millis();
Serial.print("raw="); Serial.print(raw);
Serial.print(" off="); Serial.print(offset);
Serial.print(" duty="); Serial.println(duty);
}
}Step 4 — drive it
Turn the pot slowly from one end to the other. You should see:
- Centre (raw ≈ 512): motor stopped.
- Slightly off centre (raw ≈ 530): motor stopped (you're still in the dead zone).
- Past the dead zone: motor jumps to
DEAD_BANDduty and starts spinning slowly. - Towards full clockwise: duty climbs to 255.
- Centre again: instant stop. Past centre to the other side: motor spins the opposite direction.
The two dead-zones are doing important work: a centre dead-zone (raw ±DEAD_ZONE) so jittery pot readings don't cause the motor to twitch around zero; a per-direction dead-band (DEAD_BAND) so the moment you leave the zone, the motor actually moves.
Step 5 — feel the difference with vs without dead-band
Set DEAD_BAND to 0 and re-upload. Turn the pot just past the dead zone. The motor does nothing for a while (low duty isn't enough to start it from rest), and finally jumps to life at some unpredictable duty. Re-set DEAD_BAND to your measured value — now the motor responds smoothly to any pot motion past the dead zone.
Try It Yourself 15 min
Goal: Add an LED on D6 that's ON whenever the motor is running (in either direction) and OFF when it's at duty 0. Useful as a "motor live" warning.
Hint
digitalWrite(6, duty != 0 ? HIGH : LOW);Don't forget pinMode(6, OUTPUT) in setup().
Goal: Add slew-rate limiting to the duty so the motor ramps to its requested speed instead of jumping. Cap the change in duty at 10 per 50 ms tick.
Hint
int currentDuty = 0;
const int MAX_DUTY_STEP = 10;
const unsigned long TICK_MS = 50;
unsigned long lastTick = 0;
// in loop(), once requested duty is computed:
if (millis() - lastTick >= TICK_MS) {
lastTick = millis();
int diff = duty - currentDuty;
if (diff > MAX_DUTY_STEP) diff = MAX_DUTY_STEP;
if (diff < -MAX_DUTY_STEP) diff = -MAX_DUTY_STEP;
currentDuty += diff;
setMotor(currentDuty);
}This makes the throttle feel smoother and protects the gears from sudden full-reverse-from-full-forward shocks. Same trick we used on the servo needle in L03-04.
Goal: Replace the pot with two buttons (forward, reverse) that ramp the speed: hold forward = duty climbs at 5 per 50 ms; release = duty falls at 10 per 50 ms (faster decel for safety). Adding a brake button forces an instant stop.
Hint
State variables: int targetDuty (the value we're heading towards) and int currentDuty (what we've actually written). On each tick, button states update targetDuty by ±5; slew limiter moves currentDuty towards targetDuty at the appropriate rate.
bool fwdHeld = (digitalRead(FWD_BTN) == LOW);
bool revHeld = (digitalRead(REV_BTN) == LOW);
bool braking = (digitalRead(BRAKE_BTN) == LOW);
if (braking) {
targetDuty = 0;
currentDuty = 0;
setMotor(0);
return;
}
int acc = 5;
int dec = 10;
if (fwdHeld) targetDuty = min(targetDuty + acc, 255);
if (revHeld) targetDuty = max(targetDuty - acc, -255);
if (!fwdHeld && !revHeld) {
if (targetDuty > 0) targetDuty = max(targetDuty - dec, 0);
if (targetDuty < 0) targetDuty = min(targetDuty + dec, 0);
}
// then a slew limiter from currentDuty -> targetDuty as in the easy task
This is essentially how a real electric throttle in a small EV is structured: input + ramp + slew-limit → motor controller.
Mini-Challenge · Extend the motor helper 10 min
Add a motorSpeed(m, duty) helper to the motor.h you built in L03-08. Signature:
struct Motor {
int in1;
int in2;
int en; // ENA / ENB — must be a PWM pin
};
inline void motorInit(const Motor& m) {
pinMode(m.in1, OUTPUT);
pinMode(m.in2, OUTPUT);
pinMode(m.en, OUTPUT);
}
inline void motorSpeed(const Motor& m, int duty) {
if (duty > 0) {
digitalWrite(m.in1, HIGH);
digitalWrite(m.in2, LOW);
analogWrite(m.en, duty);
} else if (duty < 0) {
digitalWrite(m.in1, LOW);
digitalWrite(m.in2, HIGH);
analogWrite(m.en, -duty);
} else {
digitalWrite(m.in1, LOW);
digitalWrite(m.in2, LOW);
analogWrite(m.en, 0);
}
}Update your sketch to use it:
#include "motor.h"
const Motor motorA = {9, 8, 5}; // IN1, IN2, ENA
void setup() {
motorInit(motorA);
}
void loop() {
motorSpeed(motorA, 150); // forward, ~60% duty
}Now every sketch from here on can declare const Motor motorA = {...}; and use motorSpeed(). We'll declare both motors of a robot chassis this way in L03-10.
Recap 5 min
Direction comes from IN1 / IN2; speed comes from analogWrite on ENA. Together they give you a signed −255 to +255 speed control, with 0 = stop. Two real-world details: the dead band (the motor can't self-start below ~50/255 duty) and the centre dead-zone (jittery inputs near zero shouldn't cause twitch). The L298N's default PWM at 490–980 Hz is in the audible range — your motor will whine; that's a feature today, an annoyance later. Tomorrow we wire two motors as a chassis and drive it around with buttons — the first robot of L3.
- Enable pin (ENA / ENB)
- The L298N's "gate" for each motor. Held HIGH (or PWM'd) to enable; held LOW to coast.
- Duty cycle
- The fraction of each PWM period that the signal is HIGH.
analogWrite(pin, x)sets duty = x/255. - Dead band
- The minimum duty cycle required for the motor to start from rest. Below this, the motor doesn't move at all.
- Dead zone (input)
- A range of input values near "zero" that you deliberately treat as zero, to avoid jittery sensors causing the output to twitch.
- Signed speed
- A single integer where the sign means direction and the magnitude means speed. Cleaner than two separate variables for "direction" and "duty".
- Active brake
- Holding both motor leads at the same potential (both GND or both VS). Back-EMF dumps into a short → fast deceleration.
- Coast
- Floating both motor leads. The motor spins down under friction alone — slower but gentler on the mechanism.
- PWM whine
- The audible buzz of a motor being driven by audio-frequency PWM (490 / 980 Hz default on UNO). Higher PWM frequencies move it out of human hearing range.
Homework 5 min
- Measure and record your motor's dead band. Bring the value to next class — every motor is different, even "identical" ones from the same kit.
- Build the centre-stick throttle from §4 and shoot a short video of the motor responding to pot motion: stop in the centre, forward one way, reverse the other.
- Save the "both motors" version of
motor.hwith the PWM-awaremotorSpeedhelper. You'll need it tomorrow for the two-wheel test bed. - Bring tomorrow: a 2WD robot chassis (or any cardboard / 3D-printed thing with two wheels and a motor on each), plus your L298N already wired up, plus 4 × AA pack.
Bring back next class:
- Your measured dead-band number.
- Your throttle video.
- The chassis assembly and dual-motor wiring for L03-10.