The L298N and Arduino combination remains the default architecture for high-voltage (12V–24V) hobby robotics and heavy-duty actuator control. Despite the proliferation of modern MOSFET-based drivers, the L298N survives because of its rugged fault tolerance, massive thermal mass, and $3 price point. However, its bipolar junction transistor (BJT) internals introduce a notorious 2V voltage drop and specific failure modes that trap beginners.
This guide provides the exact wiring, bounds-checked compilable code, and a bench-tested debugging framework to get your L298N and Arduino project running without burning out your logic board.
Decision Path: L298N vs. Modern Alternatives
Before soldering, verify that the L298N is actually the right tool for your power train. The L298N uses BJT Darlington pairs, meaning it drops roughly 2V across the H-bridge at saturation ($V_{CE(sat)}$). At a 2A continuous draw, that is 4W of heat dissipated directly into the IC.
| Criteria | L298N (BJT) | TB6612FNG (MOSFET) | DRV8871 (Single H-Bridge) |
|---|---|---|---|
| Max Voltage | 35V | 15V (VM) | 45V |
| Continuous Current | 2A per channel | 1.2A per channel | 3.6A single channel |
| Voltage Drop | ~2.0V (High) | ~0.5V (Low) | ~0.4V (Low) |
| Logic Level | 5V tolerant | 3.3V / 5V | 3.3V / 5V |
| Typical Module Cost | $2.50 - $4.00 | $5.00 - $8.00 | $4.00 - $6.00 |
Exact Parts List and Module Variants
The code and wiring below target specific hardware variants. Substituting modules with different pinouts or logic regulators will require modifications.
- Microcontroller: Arduino Uno R3 (ATmega328P, 5V logic, 16MHz).
- Motor Driver: L298N Dual H-Bridge Module (Red PCB variant, STMicroelectronics L298N IC, equipped with the onboard 7805 5V regulator and the '5V EN' jumper).
- Actuators: 12V TT Gear Motors (stall current ~1.2A) or 12V 775 DC Motors (requires external heatsink on L298N).
- Power Supply: 3S 18650 Lithium-ion battery pack (Nominal 11.1V, fully charged 12.6V). Never use a standard 9V alkaline battery; it cannot supply the 2A peak inrush current and will cause immediate brownouts.
- Wiring: 18 AWG silicone wire for power rails, 22 AWG solid core for logic pins.
Pin Mapping and Wiring Procedure
Wiring the L298N requires strict attention to the logic voltage jumper. The red PCB module features an onboard 7805 linear regulator that steps down the motor supply voltage to 5V to power the L298N's internal logic AND the Arduino via the 5V output pin.
| L298N Pin | Arduino Uno R3 Pin | Function |
|---|---|---|
| 12V / VCC | N/A (Battery +) | Motor Power Input (up to 35V) |
| GND | GND | Common Ground (Must share with Arduino) |
| 5V | 5V (or Vin) | Logic Power Output (if jumpered) or Input |
| ENA | Pin 5 (PWM) | Motor A Speed Control |
| IN1 | Pin 4 | Motor A Direction 1 |
| IN2 | Pin 7 | Motor A Direction 2 |
| ENB | Pin 6 (PWM) | Motor B Speed Control |
| IN3 | Pin 8 | Motor B Direction 1 |
| IN4 | Pin 9 | Motor B Direction 2 |
Complete Compilable Code with Bounds Checking
This sketch implements a serial-controlled dual motor drive. It includes strict bounds checking on PWM values and explicit error handling for malformed serial commands, preventing the motors from receiving garbage data that could cause erratic behavior.
// Target Board: Arduino Uno R3 (ATmega328P)
// L298N Dual H-Bridge Serial Control with Error Handling
// --- Pin Definitions ---
#define ENA 5 // PWM pin for Motor A
#define IN1 4 // Direction pin Motor A
#define IN2 7 // Direction pin Motor A
#define ENB 6 // PWM pin for Motor B
#define IN3 8 // Direction pin Motor B
#define IN4 9 // Direction pin Motor B
// --- Protocol Constants ---
const int MIN_PWM = 40; // L298N needs ~40 PWM to overcome static friction
const int MAX_PWM = 255;
void setup() {
// Configure motor pins as outputs
pinMode(ENA, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(ENB, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
// Ensure motors are stopped on boot
stopMotors();
// Initialize Serial for debugging and control
Serial.begin(9600);
Serial.println(F("L298N Ready. Cmds: F[speed] B[speed] L[speed] R[speed] S"));
Serial.println(F("Example: F150 (Forward at PWM 150), S (Stop)"));
}
void loop() {
if (Serial.available() > 0) {
String cmd = Serial.readStringUntil('\n');
cmd.trim(); // Remove whitespace and carriage returns
if (cmd.length() == 0) return;
char direction = cmd.charAt(0);
int speedVal = 0;
bool parseError = false;
// Parse speed if command is longer than 1 character
if (cmd.length() > 1) {
String speedStr = cmd.substring(1);
// Check if the substring is a valid integer
for (unsigned int i = 0; i < speedStr.length(); i++) {
if (!isDigit(speedStr.charAt(i))) {
parseError = true;
break;
}
}
if (!parseError) {
speedVal = speedStr.toInt();
}
}
// Error Handling & Execution
if (parseError || (speedVal < 0 && direction != 'S')) {
Serial.print(F("ERR: BAD_CMD -> "));
Serial.println(cmd);
return;
}
// Constrain speed to safe operational bounds
speedVal = constrain(speedVal, MIN_PWM, MAX_PWM);
switch (direction) {
case 'F': // Forward
driveMotor(IN1, IN2, ENA, HIGH, LOW, speedVal);
driveMotor(IN3, IN4, ENB, HIGH, LOW, speedVal);
Serial.print(F("OK: FWD ")); Serial.println(speedVal);
break;
case 'B': // Backward
driveMotor(IN1, IN2, ENA, LOW, HIGH, speedVal);
driveMotor(IN3, IN4, ENB, LOW, HIGH, speedVal);
Serial.print(F("OK: REV ")); Serial.println(speedVal);
break;
case 'L': // Pivot Left (Motor A back, Motor B forward)
driveMotor(IN1, IN2, ENA, LOW, HIGH, speedVal);
driveMotor(IN3, IN4, ENB, HIGH, LOW, speedVal);
Serial.print(F("OK: LFT ")); Serial.println(speedVal);
break;
case 'R': // Pivot Right (Motor A forward, Motor B back)
driveMotor(IN1, IN2, ENA, HIGH, LOW, speedVal);
driveMotor(IN3, IN4, ENB, LOW, HIGH, speedVal);
Serial.print(F("OK: RGT ")); Serial.println(speedVal);
break;
case 'S': // Stop
stopMotors();
Serial.println(F("OK: STOP"));
break;
default:
Serial.print(F("ERR: UNKNOWN_DIR -> "));
Serial.println(direction);
break;
}
}
}
// --- Helper Functions ---
void driveMotor(int pinDir1, int pinDir2, int pinPWM, bool state1, bool state2, int pwm) {
digitalWrite(pinDir1, state1);
digitalWrite(pinDir2, state2);
analogWrite(pinPWM, pwm);
}
void stopMotors() {
analogWrite(ENA, 0);
analogWrite(ENB, 0);
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
}
Debugging: First Three Things to Check When It Fails
When the L298N and Arduino setup fails, it rarely fails silently. Use this ranked decision tree to diagnose the exact symptom.
1. Symptom: Serial Monitor Outputs Garbage (e.g., ⸮⸮⸮ or ?)
- Ranked Cause 1: Baud rate mismatch. The code initializes at
9600, but the IDE Serial Monitor dropdown is set to115200. - Ranked Cause 2: Missing common ground. The USB ground and the L298N battery ground are floating relative to each other, corrupting the UART TX line.
- Fix: Verify the monitor baud rate. Measure resistance between Arduino GND and L298N GND with the power off; it must read < 1 ohm. See the Arduino Serial Reference for timing details.
2. Symptom: Motor Hums or Whines, but the Shaft Does Not Spin
- Ranked Cause 1: PWM value is below the motor's static friction threshold. The L298N drops 2V; a PWM of 20 on a 12V system only delivers ~0.5V to the motor coils.
- Ranked Cause 2: Stall current exceeded. The motor is mechanically jammed, and the L298N's internal PTC thermal shutdown is rapidly cycling (you will hear a 1-2Hz clicking or humming).
- Fix: Send a command of
F255via serial. If it spins, yourMIN_PWMin the code needs to be raised to at least 60. If it still hums, disconnect the motor and measure the supply voltage under load. If VCC sags below 9V, your battery C-rating is too low.
3. Symptom: Arduino Resets or LCD Screen Flickers When Motors Start
- Ranked Cause 1: Ground loop noise. High current returning through a thin ground wire creates a voltage spike that resets the ATmega328P brownout detector (BOD).
- Ranked Cause 2: Back-EMF spike. Missing flyback diodes (though the L298N module includes built-in 1N4007 diodes, they are often too slow for high-inductance motors).
- Fix: Upgrade the GND wire between the battery, L298N, and Arduino to 16 AWG. Ensure the ground star-point is at the L298N power terminal, not the Arduino header. Refer to the STMicroelectronics L298N Datasheet for recommended external flyback diode placements for highly inductive loads.
How to Extend or Simplify the Build
Once the baseline L298N and Arduino circuit is stable, you can adapt the hardware to fit tighter constraints or higher performance requirements.
Simplifying the Build (Cost & Space Reduction)
If you only need to drive a single motor (e.g., a linear actuator or a conveyor belt), abandon the dual-channel red module. Switch to a single DRV8871 breakout board. It requires only 2 logic pins (IN1, IN2) instead of 3, eliminates the 2V BJT voltage drop, and costs roughly the same. You will need to update the pin definitions in the code, but the serial parsing logic remains identical.
Extending the Build (Adding Telemetry)
To prevent motor burnout and implement closed-loop speed control, add current sensing. The L298N module does not have built-in current sense outputs (unlike the TI DRV88xx series).
- Hardware Addition: Insert an ACS712-20A Hall-effect current sensor module in series with the motor's ground return path.
- Code Extension: Read the ACS712 analog output on
A0. The sensor outputs 2.5V at 0A, with a sensitivity of 100mV/A. Add a safety interrupt in theloop()that callsstopMotors()if the calculated current exceeds 2.5A for more than 500ms. - Alternative Driver: If telemetry is a hard requirement, migrate to the Pololu TB6612FNG or a VNH5019 module, which feature dedicated current-sense analog pins natively on the IC, saving you the extra ACS712 module and wiring complexity.






