PROJECT #01 · UART

ESP32 ↔ BeagleBone Black
Serial Protocol

A step-by-step journey from sending a single raw byte over UART to building a checksum-verified, two-way command protocol between an ESP32 and a BeagleBone Black.

ESP32 BeagleBone Black Python 2.7 Arduino C++ 115200 baud Logic Analyzer

Hardware & Wiring

Transmitter
ESP32 DevKit
UART2 · GPIO17 TX · GPIO16 RX · GPIO4 Button
Receiver
BeagleBone Black
UART4 · P9_11 TX · P9_13 RX · P9_12 Button · P9_14 LED

Connection Table

ESP32BBBPurpose
GPIO17 (TX)P9_13 (RX)UART data
GPIO16 (RX)P9_11 (TX)UART data
GNDGNDCommon ground — mandatory
GPIO4Corrupt checksum button
P9_12Corrupt ACK button
P9_14LED output
Pull-up resistors: Both buttons use a 10kΩ pull-up resistor between the signal pin and 3.3V. When the button is released the pin reads HIGH; when pressed it pulls LOW.
1

Single Raw Byte

The starting point: send a single byte over UART with no structure at all. The goal is to see a UART frame on the logic analyzer and understand its anatomy.

Every UART frame follows the same pattern regardless of content: the line sits HIGH (idle), drops to LOW for exactly one bit period (start bit), then the 8 data bits follow LSB first, and finally the line returns HIGH for the stop bit.

IDLE
START
D0
D1
D2
D3
D4
D5
D6
D7
STOP
IDLE
Start bit — always LOW
Data bits — LSB first
Stop bit — always HIGH

0x55 is an ideal test byte (01010101) — bits alternate HIGH and LOW every period, making baud rate verification trivial on the logic analyzer.

src/main.cpp C++
#include <Arduino.h>

#define TX_PIN    17
#define RX_PIN    16
#define BAUD_RATE 9600

HardwareSerial MySerial(2);

void setup() {
  MySerial.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN);
}

void loop() {
  MySerial.write(0x55);   // 01010101 — perfect test byte
  MySerial.flush();
  delay(500);
}
Logic Analyzer setup: Sample rate ≥ 1 MHz · Trigger: Falling Edge on CH0 · Decoder: UART, TX = CH0, baud = 9600, 8N1
2

Multiple Bytes

Instead of a single byte, we send an array of three bytes back-to-back. On the logic analyzer this produces three consecutive UART frames with a small inter-frame gap between each.

src/main.cpp C++
#include <Arduino.h>

#define TX_PIN    17
#define RX_PIN    16
#define BAUD_RATE 115200

HardwareSerial MySerial(2);

uint8_t packet[] = { 0xAA, 0xCB, 0x55 };

void setup() {
  Serial.begin(115200);
  MySerial.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN);
}

void loop() {
  MySerial.write(packet, sizeof(packet));
  MySerial.flush();
  delay(1000);
}

The logic analyzer now shows three frames in sequence: 0xAA, 0xCB, 0x55. Each frame is independent — its own start bit, 8 data bits, and stop bit.

3

Meaningful Data — ESP32 ↔ BBB

We now assign meaning to each byte. A five-byte frame carries a command from the ESP32 to the BeagleBone Black, which responds with an ACK. The BBB toggles a GPIO LED based on the received command.

Frame Structure

ByteValueDescription
frame[0]0xAAStart byte — marks beginning of frame
frame[1]0x01 / 0x02Command — LED ON or LED OFF
frame[2]0x00Payload length (no payload in this step)
frame[3]XORChecksum — XOR of bytes 1 and 2
frame[4]0x55Stop byte — marks end of frame
src/main.cpp — ESP32 C++
#include <Arduino.h>

#define TX_PIN    17
#define RX_PIN    16
#define BAUD_RATE 115200

HardwareSerial MySerial(2);

const uint8_t START_BYTE  = 0xAA;
const uint8_t END_BYTE    = 0x55;
const uint8_t CMD_LED_ON  = 0x01;
const uint8_t CMD_LED_OFF = 0x02;
const uint8_t CMD_ACK     = 0x10;

uint8_t calcChecksum(uint8_t* data, int len) {
  uint8_t cs = 0;
  for (int i = 0; i < len; i++) cs ^= data[i];
  return cs;
}

void sendCommand(uint8_t cmd) {
  uint8_t frame[5];
  frame[0] = START_BYTE;
  frame[1] = cmd;
  frame[2] = 0x00;
  frame[3] = calcChecksum(&frame[1], 2);
  frame[4] = END_BYTE;
  MySerial.write(frame, 5);
  MySerial.flush();
}

bool readAck() {
  unsigned long t = millis();
  uint8_t buf[16]; int idx = 0;
  while (millis() - t < 500) {
    if (MySerial.available()) {
      uint8_t b = MySerial.read();
      if (b == START_BYTE) idx = 0;
      buf[idx++] = b;
      if (idx >= 5 && buf[idx-1] == END_BYTE)
        if (buf[1] == CMD_ACK) return true;
    }
  }
  return false;
}

void setup() {
  Serial.begin(115200);
  MySerial.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN);
}

void loop() {
  sendCommand(CMD_LED_ON);
  Serial.println(readAck() ? "ACK received!" : "Timeout!");
  delay(2000);
  sendCommand(CMD_LED_OFF);
  Serial.println(readAck() ? "ACK received!" : "Timeout!");
  delay(2000);
}
receiver.py — BeagleBone Black Python 2.7
import serial
import Adafruit_BBIO.GPIO as GPIO

LED_PIN = "P9_14"
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.output(LED_PIN, GPIO.LOW)

ser = serial.Serial('/dev/ttyO4', baudrate=115200, timeout=1)

START, END       = 0xAA, 0x55
CMD_LED_ON       = 0x01
CMD_LED_OFF      = 0x02
CMD_ACK          = 0x10

def checksum(data):
    cs = 0
    for b in bytearray(data): cs ^= b
    return cs

def send_ack():
    frame = bytearray([START, CMD_ACK, 0x00])
    frame += bytearray([checksum(frame[1:]), END])
    ser.write(bytes(frame))

def read_frame():
    buf = bytearray()
    while True:
        byte = ser.read(1)
        if not byte: return None
        b = ord(byte)
        if b == START: buf = bytearray([b])
        else: buf += bytearray([b])
        if len(buf) >= 5 and buf[-1] == END: return buf

print "BBB ready, listening..."
while True:
    frame = read_frame()
    if frame and len(frame) >= 5:
        if frame[0] == START and frame[-1] == END:
            cmd = frame[1]
            cs  = checksum(frame[1:3])
            if cs == frame[3]:
                if cmd == CMD_LED_ON:
                    GPIO.output(LED_PIN, GPIO.HIGH)
                    print "LED ON"
                elif cmd == CMD_LED_OFF:
                    GPIO.output(LED_PIN, GPIO.LOW)
                    print "LED OFF"
                send_ack()
            else:
                print "Checksum error! Packet rejected."
4

Error Detection — Checksum Testing

With the protocol working correctly, we verify that the checksum mechanism actually catches errors. Two physical buttons — one on each board — are used to deliberately corrupt outgoing frames.

ESP32 button (GPIO4): When held during a send cycle, the checksum byte is flipped with XOR 0xFF before transmission. The BeagleBone Black receives the frame, recalculates the expected checksum, finds a mismatch, and prints "Checksum error! Packet rejected." — the LED does not change state and no ACK is sent. The ESP32 times out waiting for the ACK and logs "Timeout!"

BBB button (P9_12): When held, the BBB processes the command normally and toggles the LED, but corrupts the ACK checksum before sending it back. The ESP32 receives the ACK frame, verifies its checksum, detects the corruption, and logs "ACK checksum error!"

This two-sided test confirms that both endpoints independently validate every frame they receive — neither blindly trusts incoming data.

Checksum algorithm: XOR of all bytes between (and including) the command byte and the length byte. A single flipped bit changes the result, making corruption detectable with minimal overhead.

Logic Analyzer & Hardware

Waveform captures from PulseView and hardware setup photos.

Step 1 — 0x55 single byte waveform
Step 1 — Single Byte
0x55 waveform decoded
The UART decoder identifies the start bit, eight alternating data bits of 0x55 (01010101), and the stop bit. The alternating pattern makes bit timing easy to verify by eye.
Step 2 — Three consecutive frames AA CB 55
Step 2 — Multiple Bytes
Three frames: AA → CB → 55
Three independent UART frames transmitted back-to-back. Each frame has its own start (S) and stop (T) bit. The small inter-frame gap between each byte is clearly visible.
Step 3 — Full protocol frame annotated
Step 3 — Full Protocol Frame
5-byte frame: AA 02 00 02 55 — LED OFF command
Each byte of the protocol frame is annotated: frame[0] = start byte (0xAA), frame[1] = command (LED OFF), frame[2] = payload length, frame[3] = XOR checksum, frame[4] = end byte (0x55). The hand-written labels show the mapping from code to wire.
Step 3 — BBB ACK response on RX channel
Step 3 — ACK Response
BBB send_ack() — RX channel
The BeagleBone Black responds with a 5-byte ACK frame (AA 10 00 10 55) visible on the RX channel (D1). Both TX and RX channels are active — two-way communication confirmed.
Step 4 — Checksum error test
Step 4 — Error Detection
Deliberate checksum corruption
With the ESP32 button held, the checksum byte is flipped — 0xFD instead of 0x02. The BBB rejects the frame and sends no ACK; the ESP32 times out. The terminal shows "ACK yok / timeout!" followed by a successful round-trip after the button is released. Inter-frame gap: ~4 ms.
Hardware setup — ESP32, BBB, logic analyzer on clipboard
Hardware Setup
ESP32 + BeagleBone Black + Logic Analyzer
The full test bench mounted on a clipboard — ESP32 (bottom left), BeagleBone Black (right), breadboard with two push-buttons and pull-up resistors (centre), 24 MHz 8-channel logic analyzer (top), and the blue LED on the BBB responding to a command.