Bluetooth Controlled Robot Car with HC-05 and Arduino Uno
Remote control is the quickest way to make a robot feel alive, and it is also a useful tool: you can drive a robot to test its motors, collect sensor data while steering it by hand, or use manual control as a fallback in a competition. The HC-05 Bluetooth module makes this easy because it appears to the Arduino as a plain serial port.
In this tutorial you will wire an HC-05 to the 2WD chassis, pair it with an Android phone, and write a sketch that turns single-character commands into motion with speed control. You will also add a safety timeout so the car stops when the connection drops, which is the feature most beginner projects forget.
About the HC-05 Module
The HC-05 is a Bluetooth Classic module using the Serial Port Profile (SPP). Once a phone pairs and connects, every byte the phone sends comes out of the module's TXD pin, and every byte you send to its RXD pin goes to the phone. Most HC-05 boards sold on a breakout have a regulator and accept 5 V on VCC, but their RXD input is a 3.3 V logic pin.
| Setting | Common default | Notes |
|---|---|---|
| Data mode baud rate | 9600 | Used for normal communication |
| AT command mode baud rate | 38400 | Entered by holding the EN or KEY button while powering up |
| Pairing PIN | 1234 (some modules 0000) | Change with an AT command if needed |
| Status LED, not connected | Fast blinking | Slow double blink when connected on many boards |
Wiring
The Uno's hardware serial port on pins 0 and 1 is also used for USB uploads, and a module connected there blocks uploading. So this project uses SoftwareSerial on pins 2 and 3. The Arduino TX signal is 5 V, so it goes through a voltage divider before reaching the HC-05 RXD pin.
HC-05 VCC --> Arduino 5V
HC-05 GND --> Arduino GND
HC-05 TXD --> Arduino D2 (SoftwareSerial RX)
Arduino D3 --> 1k --+--> HC-05 RXD
|
2k
|
GND (5 V x 2k / 3k = 3.3 V)
Motor driver: TB6612FNG on pins 4, 5, 6, 7, 8, 9, 10 as in the 2WD chassis tutorialCommand Protocol
Many free Android Bluetooth controller apps let you assign a character to each button. Using single characters keeps parsing trivial and robust. Configure your app to send these characters, or type them in a Bluetooth serial terminal app to test.
| Character | Action |
|---|---|
| F | Forward |
| B | Backward |
| L | Spin left |
| R | Spin right |
| G / I | Forward-left / forward-right curve |
| S | Stop |
| 0 to 9 | Speed level, from slow (0) to full (9) |
Arduino Code
#include <SoftwareSerial.h>
SoftwareSerial bt(2, 3); // RX = D2 (from HC-05 TXD), TX = D3 (to HC-05 RXD via divider)
const int PWMA = 5, AIN1 = 7, AIN2 = 8; // left motor
const int PWMB = 6, BIN1 = 9, BIN2 = 10; // right motor
const int STBY = 4;
int speedPwm = 180; // current speed, changed by '0'..'9'
unsigned long lastCmdMs = 0;
const unsigned long TIMEOUT_MS = 600; // stop if no command for this long
// Set to false if your app sends a command only once per button press
const bool USE_TIMEOUT = true;
void motor(int pwmPin, int in1, int in2, int s) {
s = constrain(s, -255, 255);
digitalWrite(in1, s > 0 ? HIGH : LOW);
digitalWrite(in2, s < 0 ? HIGH : LOW);
analogWrite(pwmPin, abs(s));
}
void drive(int l, int r) { motor(PWMA, AIN1, AIN2, l); motor(PWMB, BIN1, BIN2, r); }
void handleCommand(char c) {
int s = speedPwm;
switch (c) {
case 'F': drive(s, s); break;
case 'B': drive(-s, -s); break;
case 'L': drive(-s, s); break;
case 'R': drive(s, -s); break;
case 'G': drive(s / 2, s); break; // curve left
case 'I': drive(s, s / 2); break; // curve right
case 'S': drive(0, 0); break;
default:
if (c >= '0' && c <= '9') {
// map level 0..9 to PWM 100..255 (below ~100 many gear motors stall)
speedPwm = map(c - '0', 0, 9, 100, 255);
}
return; // ignore other bytes such as newlines
}
lastCmdMs = millis();
}
void setup() {
int pins[] = {PWMA, AIN1, AIN2, PWMB, BIN1, BIN2, STBY};
for (int p : pins) pinMode(p, OUTPUT);
digitalWrite(STBY, HIGH);
Serial.begin(9600); // USB debug
bt.begin(9600); // HC-05 default data-mode baud rate
Serial.println("Bluetooth car ready");
}
void loop() {
while (bt.available()) {
char c = bt.read();
Serial.write(c); // echo to Serial Monitor for debugging
handleCommand(c);
}
// Failsafe: stop if the phone stops sending (out of range, app closed)
if (USE_TIMEOUT && millis() - lastCmdMs > TIMEOUT_MS) {
drive(0, 0);
}
}
How It Works
SoftwareSerial creates a serial port in software on two ordinary pins. It is reliable at 9600 baud, which is plenty for single-character commands. The main loop reads every waiting byte and passes it to handleCommand, which either changes the motor outputs or stores a new speed level. Speed digits do not count as motion commands, so they do not reset the timeout.
The timeout assumes your controller app repeats the command while a button is held, which many apps do. If yours sends a character only when a button is pressed and an S when it is released, set USE_TIMEOUT to false, but then a lost connection leaves the car running until the HC-05 drops the link. Test this by walking away from the car with a button held.
Changing the name or PIN
To rename the module, enter AT mode by holding its button while applying power, connect at 38400 baud and send AT+NAME=MyRobot followed by carriage return and newline. AT+PSWD changes the PIN on many firmware versions, though the exact syntax varies between firmware releases, so check AT+VERSION and the documentation for your module.
Troubleshooting
- Phone pairs but nothing arrives: TXD and RXD are crossed wrongly. HC-05 TXD must go to the Arduino RX pin.
- Garbage characters in the Serial Monitor: the baud rate does not match. Try 9600, and if the module was changed earlier, 38400.
- Upload fails: the module is connected to pins 0 and 1. Move it to SoftwareSerial pins.
- Car keeps stopping and starting: the app sends commands too slowly for the timeout. Increase
TIMEOUT_MS. - Module resets when motors start: motor noise on the 5 V rail; add capacitors and keep the module wires away from motor wires.
Summary
The HC-05 behaves like a wireless serial cable. With SoftwareSerial on pins 2 and 3, a divider on its RXD input and a single-character command set, an Arduino becomes a phone-controlled car in a few dozen lines. Always add a failsafe stop, and remember that iPhones need a BLE module instead.