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.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// 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.