Built-in project · Arduino Uno R3

Bluetooth lamp on an Arduino Uno

An HC-05 modeled as what it is to the board: a serial bridge whose far end is the serial monitor. Type on or off and the lamp answers. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $35. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • How a UART frames a byte with start and stop bits
  • Why both ends must use the same baud rate
  • Reading a connection status pin before sending
  • Parsing a short text command into an action
  • Blinking without delay by checking the clock
  • Difficulty: Intermediate
  • Time: About 20 minutes
  • Board: Arduino Uno R3
  • Parts: 5

How it works

The HC-05 module is wired to the Uno as a serial link. Pin 2 listens to the module's TXD, pin 3 talks to its RXD, and pin 4 reads STATE, which is high while someone is connected. In Mokxi the far end of the link is the serial monitor, not a real radio.

The sketch waits for the line on pin 2 to fall, which is the start bit of a byte. It reads the byte, builds a line of text, and when a newline arrives it checks for on, off, blink or status.

The yellow LED on pin 9 (through a 220 ohm resistor) follows the command. The sketch also sends a reply back through pin 3, such as lamp on.

Parts list

5 parts, plus the jumper wires. They retail for about $35; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Resistor, 220 Ω pin 1Arduino Uno R3 pin 9
Bluetooth serial module, HC-05 pin STATEArduino Uno R3 pin 4
Bluetooth serial module, HC-05 pin RXDArduino Uno R3 pin 3
Bluetooth serial module, HC-05 pin TXDArduino Uno R3 pin 2
Bluetooth serial module, HC-05 pin VCCArduino Uno R3 pin 5V
Bluetooth serial module, HC-05 pin GNDArduino Uno R3 pin GND
LED, yellow pin CArduino Uno R3 pin GND
LED, yellow pin AResistor, 220 Ω pin 2

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Bluetooth lamp: an HC-05 on two pins, a lamp and a text command.
//
//   pin 2   <- the module's TXD   (the board's receive)
//   pin 3   -> the module's RXD   (the board's transmit)
//   pin 9   the lamp, through 220 ohm to GND
//   pin 4   <- the module's STATE, high while the link is up
//
// Type `on`, `off`, `blink` or `status` into the *module's* box in the serial
// monitor (it appears there under its own name, beside the board), and the
// lamp answers. Click the module on the canvas to hang up and redial.
//
// # There is no radio, and that is the point
//
// Mokxi does not model Bluetooth: no pairing, no 2.4 GHz, no other device. What
// it models is the only half of an HC-05 a circuit can see, which is a UART.
// So the far end of the link is the serial monitor, and from the board's side
// that is indistinguishable from a paired phone, which is exactly why this
// module is worth using in a lesson. What you cannot learn here is anything
// about pairing, range or interference.
//
// # The UART is real, and so are its failures
//
// TXD and RXD carry 8N1 at the module's baud, bit by bit. Change the module's
// `baud` property without changing BIT_US below and you get the same rubbish
// you get on the bench, because the stop bit lands in the middle of a byte and
// the frame is thrown away. That is the commonest HC-05 problem in the world
// and it is worth being able to reproduce on purpose.
//
// # Bit-banged, and why it runs at 1200 baud
//
// This board has one hardware UART and it is the serial monitor's, so the
// module gets a software one: the loop below is a start bit, eight data bits
// and a stop bit counted out with delayMicroseconds.
//
// That is where 1200 baud comes from, and it is worth the paragraph.
// `digitalRead` on an ATmega328P is not one instruction: it looks the pin up
// in two tables and disconnects it from its timer first, and here it measures
// about forty microseconds. At 9600 baud a bit is 104 us, so each sample lands
// forty microseconds later than the last and by the third one it has walked
// out of its bit: every frame arrives as rubbish, which is exactly what
// happens on a real Uno with a loop like this one.
//
// A real `SoftwareSerial` gets 9600 out of the same chip by catching the start
// bit on a pin-change interrupt and counting the rest out in hand-written
// assembly. This sketch is the honest version of that, so it runs at a rate it
// can actually hold: 1200 baud, 833 us a bit, where forty microseconds of
// overhead is five percent of a bit instead of forty. `AT+UART=1200,0,0` is
// how you would set a real module to match.
//
// Change the module's `baud` property to 9600 without changing BIT_US below
// and watch the framing errors arrive. That is the same fault, and it is the
// commonest one there is with these modules.
//
// # STATE
//
// High while the module has somebody on the other end. A sketch that sends into
// a link that is not up is talking to itself, and STATE is how it knows. Hang
// up on the canvas and watch the lamp's own status line say so.

const int RX_PIN = 2;
const int TX_PIN = 3;
const int STATE_PIN = 4;
const int LAMP = 9;

// 1200 baud: 833 microseconds a bit. See the note above for why not 9600.
const unsigned int BIT_US = 833;

char line[24];
unsigned char at = 0;
bool blinking = false;
unsigned long blinkAt = 0;
bool lampOn = false;

void send(const char *text);
void handle(const char *text);
int readByte();
void writeByte(unsigned char value);
bool equals(const char *a, const char *b);

void setup() {
  pinMode(LAMP, OUTPUT);
  pinMode(STATE_PIN, INPUT);
  pinMode(RX_PIN, INPUT);
  // The idle level of a UART's transmit line is high.
  digitalWrite(TX_PIN, HIGH);
  pinMode(TX_PIN, OUTPUT);
  digitalWrite(LAMP, LOW);
  Serial.begin(115200);
  Serial.println("Mokxi Uno: HC-05 lamp. Type on / off / blink / status at the module.");
  // Leave the line idle for a couple of frames first: a receiver only knows a
  // start bit as the line falling from idle. (This used to come for free while
  // the println above waited for every byte; now it goes out behind the
  // sketch from Serial's transmit buffer.)
  delay(20);
  send("lamp ready");
}

void loop() {
  // A start bit is the line falling.
  if (digitalRead(RX_PIN) == LOW) {
    int byte = readByte();
    if (byte >= 0) {
      if (byte == '\n' || byte == '\r') {
        if (at > 0) {
          line[at] = 0;
          handle(line);
          at = 0;
        }
      } else if (at < sizeof(line) - 1) {
        line[at++] = (char)byte;
      }
    }
  }

  if (blinking && millis() - blinkAt >= 300) {
    blinkAt = millis();
    lampOn = !lampOn;
    digitalWrite(LAMP, lampOn ? HIGH : LOW);
  }
}

// One 8N1 byte, sampled in the middle of each bit. -1 for a framing error,
// which is what a rate that does not match looks like.
int readByte() {
  // Half a bit from the falling edge puts the next sample in the middle of
  // bit 0.
  delayMicroseconds(BIT_US + BIT_US / 2);
  int value = 0;
  for (int bit = 0; bit < 8; bit++) {
    if (digitalRead(RX_PIN) == HIGH) {
      value |= (1 << bit);
    }
    delayMicroseconds(BIT_US);
  }
  // The stop bit must be high. If it is not, the byte was never a byte.
  if (digitalRead(RX_PIN) != HIGH) {
    Serial.println("framing error: is the module's baud the same as ours?");
    return -1;
  }
  return value;
}

void writeByte(unsigned char value) {
  digitalWrite(TX_PIN, LOW);
  delayMicroseconds(BIT_US);
  for (int bit = 0; bit < 8; bit++) {
    digitalWrite(TX_PIN, (value & (1 << bit)) ? HIGH : LOW);
    delayMicroseconds(BIT_US);
  }
  digitalWrite(TX_PIN, HIGH);
  delayMicroseconds(BIT_US);
}

void send(const char *text) {
  if (digitalRead(STATE_PIN) == LOW) {
    Serial.println("nobody is connected; not sending");
    return;
  }
  while (*text) {
    writeByte((unsigned char)*text++);
  }
  writeByte('\n');
}

void handle(const char *text) {
  Serial.print("from the link: ");
  Serial.println(text);

  if (equals(text, "on")) {
    blinking = false;
    lampOn = true;
    digitalWrite(LAMP, HIGH);
    send("lamp on");
  } else if (equals(text, "off")) {
    blinking = false;
    lampOn = false;
    digitalWrite(LAMP, LOW);
    send("lamp off");
  } else if (equals(text, "blink")) {
    blinking = true;
    blinkAt = millis();
    send("lamp blinking");
  } else if (equals(text, "status")) {
    send(blinking ? "blinking" : (lampOn ? "on" : "off"));
  } else {
    send("try on, off, blink or status");
  }
}

bool equals(const char *a, const char *b) {
  while (*a && *b) {
    if (*a++ != *b++) {
      return false;
    }
  }
  return *a == 0 && *b == 0;
}

Code walkthrough

The key functions of the sketch, and what each one does.

readByte sketch.ino

Called once the line falls. It waits one and a half bit times so it samples in the middle of bit 0, then reads eight bits, lowest first, one BIT_US apart. Last it checks the stop bit is high. If not, it prints a framing error and returns -1, which is what a baud mismatch looks like.

// One 8N1 byte, sampled in the middle of each bit. -1 for a framing error,
// which is what a rate that does not match looks like.
int readByte() {
  // Half a bit from the falling edge puts the next sample in the middle of
  // bit 0.
  delayMicroseconds(BIT_US + BIT_US / 2);
  int value = 0;
  for (int bit = 0; bit < 8; bit++) {
    if (digitalRead(RX_PIN) == HIGH) {
      value |= (1 << bit);
    }
    delayMicroseconds(BIT_US);
  }
  // The stop bit must be high. If it is not, the byte was never a byte.
  if (digitalRead(RX_PIN) != HIGH) {
    Serial.println("framing error: is the module's baud the same as ours?");
    return -1;
  }
  return value;
}

send sketch.ino

Sends a string to the module, one byte at a time with writeByte, then a newline. First it reads the STATE pin. If it is low, nobody is connected, so it prints a message and sends nothing.

void send(const char *text) {
  if (digitalRead(STATE_PIN) == LOW) {
    Serial.println("nobody is connected; not sending");
    return;
  }
  while (*text) {
    writeByte((unsigned char)*text++);
  }
  writeByte('\n');
}

handle sketch.ino

Runs when a full line has arrived. It compares the text with each command. on and off stop any blinking and set the lamp. blink turns on a flag that loop uses to toggle the LED every 300 ms. status replies with the current state. Anything else gets a hint.

void handle(const char *text) {
  Serial.print("from the link: ");
  Serial.println(text);

  if (equals(text, "on")) {
    blinking = false;
    lampOn = true;
    digitalWrite(LAMP, HIGH);
    send("lamp on");
  } else if (equals(text, "off")) {
    blinking = false;
    lampOn = false;
    digitalWrite(LAMP, LOW);
    send("lamp off");
  } else if (equals(text, "blink")) {
    blinking = true;
    blinkAt = millis();
    send("lamp blinking");
  } else if (equals(text, "status")) {
    send(blinking ? "blinking" : (lampOn ? "on" : "off"));
  } else {
    send("try on, off, blink or status");
  }
}

loop sketch.ino

Watches pin 2 for a start bit. When one arrives it reads a byte and adds it to the line buffer, or runs handle at a newline. It also does the blinking: every 300 ms, if blinking is set, it flips the lamp.

void loop() {
  // A start bit is the line falling.
  if (digitalRead(RX_PIN) == LOW) {
    int byte = readByte();
    if (byte >= 0) {
      if (byte == '\n' || byte == '\r') {
        if (at > 0) {
          line[at] = 0;
          handle(line);
          at = 0;
        }
      } else if (at < sizeof(line) - 1) {
        line[at++] = (char)byte;
      }
    }
  }

  if (blinking && millis() - blinkAt >= 300) {
    blinkAt = millis();
    lampOn = !lampOn;
    digitalWrite(LAMP, lampOn ? HIGH : LOW);
  }
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The serial monitor prints framing errors and the lamp ignores commands This project
    The module's baud property must match the sketch. BIT_US is 833, which is 1200 baud. Set the module to 1200.
  • The sketch says nobody is connected and replies never appear This project
    STATE on pin 4 is low because the link is hung up. Click the module on the canvas to redial.
  • The LED never lights This project
    Check the LED's anode goes to the resistor from pin 9 and its cathode goes to GND.
  • Garbled text or nothing arrives. Bluetooth serial module
    Cross the wires: module TX to Arduino RX, module RX to Arduino TX. Match the baud rate. The default data rate is 9600 and the pairing code is usually 1234.
  • The module's RX pin is wired straight to a 5 V pin. Bluetooth serial module
    HC-05 logic is 3.3 V. Use a voltage divider (for example 1 k and 2 k ohms) on the Arduino TX line going into module RX.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.