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.

IntermediateRuns in your browser. Free, and no account needed.

Bluetooth lamp · Arduino Uno R3live0.000 s 0.00x
Hold the button
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

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.");
  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;
}

Parts list

5 parts, plus the jumper wires. Every one is in the editor's parts bin.

How it is wired

7 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Uno R3 pin 9; Resistor, 220 Ω pin 1
  • Arduino Uno R3 pin 4; Bluetooth serial module, HC-05 pin STATE
  • Arduino Uno R3 pin 3; Bluetooth serial module, HC-05 pin RXD
  • Arduino Uno R3 pin 2; Bluetooth serial module, HC-05 pin TXD
  • Arduino Uno R3 pin 5V; Bluetooth serial module, HC-05 pin VCC
  • Ground: Arduino Uno R3 pin GND; Bluetooth serial module, HC-05 pin GND; LED, yellow pin C
  • Resistor, 220 Ω pin 2; LED, yellow pin A

Change it and keep it

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