Skip to main content
The BlueBus bootloader provides a UART-based protocol for firmware upgrades and device management. It runs on startup with a 100ms timeout window, during which it listens for upgrade commands.

Protocol specification

Packet structure

All communication follows this packet format:
<Command Byte> <Length> <Data...> <XOR Checksum>
FieldSizeDescription
Command1 byteProtocol command identifier
Length1 byteTotal packet length (including command, length, and checksum)
Data0-252 bytesCommand payload (minimum 1 byte)
Checksum1 byteXOR of all preceding bytes
At least one data byte is required per the protocol, though it can be 0x00 for commands without parameters.

Checksum calculation

From protocol.c:ProtocolValidatePacket():
uint8_t chk = packet->command;
chk = chk ^ packet->dataSize;
for (idx = 0; idx < msgSize; idx++) {
    chk = chk ^ packet->data[idx];
}
chk = chk ^ validation;  // Should equal 0 for valid packet

UART configuration

  • Baud rate: 115200
  • Parity: Odd
  • Data bits: 8
  • Stop bits: 1
  • Flow control: None
The application firmware may use different UART settings (typically no parity). Tools must reconfigure the serial port when switching between application and bootloader modes.

Command set

Platform identification

Request platform (0x00) Query the bootloader platform identifier.
# Request
command = 0x00
data = [0x00]  # Dummy byte
packet = generate_packet(command, data)
Platform response (0x01) Returns the platform string (e.g., BLUEBUS_BOOTLOADER_2_4).
# Response contains ASCII string
platform = 'BLUEBUS_BOOTLOADER_2_4'

Flash operations

Erase flash request (0x02) Erases all application memory from 0x1800 to 0xAB000.
command = 0x02
data = [0x00]
The bootloader iterates through the application region:
uint32_t address = 0x1800;  // BOOTLOADER_APPLICATION_START
while (address <= 0xAB000) {
    FlashErasePage(address);
    address += 1024;  // Page size
}
Erase flash response (0x03) Acknowledges erase completion.
Flash erase can take several seconds. The bootloader does not respond until the entire erase operation completes.
Write data request (0x04) Writes a block of firmware data to flash. Packet structure:
[0x04] [Length] [Addr2] [Addr1] [Addr0] [Data0] [Data1] ... [DataN] [XOR]
  • Address: 3 bytes (24-bit big-endian)
  • Data: 246 bytes (82 instructions × 3 bytes each)
From the Python tools:
row_data = []
addr_bytes = [b for b in pack('>I', address)]
addr_bytes.pop(0)  # Remove MSB (always 0)
for b in addr_bytes:
    row_data.append(b)

# Add 82 instructions (246 bytes)
for addr in range(82 * 2):  # 164 program memory addresses
    if addr % 2 == 0:
        op_bytes = pack('>I', get_opcode(addr))
        # Append 3 bytes per instruction
Write data response OK (0x05) Write succeeded. Write data response error (0x06) Write failed. From protocol.c:ProtocolFlashWrite(), this occurs when:
  • Flash write operation returns an error
  • Data validation fails

Application control

Start application request (0x09) Exits the bootloader and jumps to the application. From main.c:
// Set IVT to application mode
IVT_MODE = IVT_MODE_APP;

// Jump to application vector
void (*appptr)(void);
appptr = (void (*)(void))BOOTLOADER_APPLICATION_VECTOR;  // 0x2000
appptr();
Start application response (0x0A) Sent before jumping to application (50ms delay to ensure transmission).

Device information

Firmware version request (0x0B) Reads firmware version from EEPROM. Firmware version response (0x0C) Returns 3 bytes: [Major] [Minor] [Patch]
uint8_t response[] = {
    EEPROMReadByte(0x02),  // Major
    EEPROMReadByte(0x03),  // Minor
    EEPROMReadByte(0x04)   // Patch
};
Read serial number request (0x0D) Read serial number response (0x0E) Returns 2 bytes: [MSB] [LSB] (16-bit serial number) Write serial number request (0x0F) Writes serial number to EEPROM (only if currently 0xFFFF). Packet: [0x0F] [Length] [MSB] [LSB] [XOR] Write serial number response OK (0x10) Write serial number response error (0x11) Serial number already set (can only be written once). Read build date request (0x12) Read build date response (0x13) Returns 2 bytes: [Week] [Year] (ISO week date) Write build date request (0x14) Write build date response OK (0x15) Write build date response error (0x16) Build date already set.

Bluetooth management

BT mode request (0x07) Switches UART connection to Bluetooth module (for direct firmware updates). From protocol.c:ProtocolBTMode():
// Board v1: Switch TMUX154E to BT UART mode
UART_SEL = UART_SEL_BT;

// Board v2: Bridge system UART to BT UART
while (1) {
    // Bidirectional UART forwarding loop
}
BT mode response (0x08) BT DFU mode request (0x17) Puts Bluetooth module into Device Firmware Update mode and bridges UART.
BT_MFB = 1;
BT_BOOT_TYPE_MODE = 0;  // Pull pin low for "Test" mode
BT_BOOT_TYPE = 0;
// Wait 150ms
BT_RST = 1;  // Release reset
BT DFU mode response (0x18)

Error responses

Bad packet response (0xFF) Checksum validation failed. The bootloader:
  • Resets the RX queue
  • Requests retransmission
Packet timeout (0xFE) No complete packet received within 100ms. From protocol.c:ProtocolProcessQueue():
if ((now - lastRx) >= 100) {  // PROTOCOL_PACKET_TIMEOUT
    UARTRXQueueReset(uart);
    ProtocolSendPacket(uart, 0xFE, 0, 0);
}

Bootloader operation

Startup sequence

From main.c:main():
1

Initialize hardware

  • Set all ports to digital mode
  • Configure pin directions (inputs/outputs)
  • Initialize timer and EEPROM (SPI)
  • Set up UART2 at 115200 baud, odd parity
2

Check boot mode

uint8_t BOOT_MODE = BOOT_MODE_APPLICATION;
unsigned char bootMode = EEPROMReadByte(CONFIG_BOOTLOADER_MODE);

if (bootMode != 0x00 || RECOVERY_STATUS == 0) {
    BOOT_MODE = BOOT_MODE_BOOTLOADER;
    TimerEnableLED();  // Flash LED to indicate bootloader mode
}
3

Wait for commands

while ((BOOT_MODE == BOOT_MODE_BOOTLOADER) ||
       TimerGetMillis() <= 100) {  // BOOTLOADER_TIMEOUT
    // Process incoming packets
    if (queueSize >= PROTOCOL_PACKET_MIN_SIZE) {
        ProtocolProcessMessage(&systemUart, &BOOT_MODE);
    }
}
The bootloader remains active if:
  • Bootloader mode flag is set in EEPROM
  • Recovery pin (RD5) is pulled low
  • Valid packet received within 100ms timeout
4

Jump to application

// Mark bootloader mode as "application" for next boot
EEPROMWriteByte(CONFIG_BOOTLOADER_MODE, 0xAB);

// Clean up peripherals
UARTDestroy(SYSTEM_UART_MODULE);
EEPROMDestroy();
TimerDestroy();

// Switch interrupt vectors
IVT_MODE = IVT_MODE_APP;

// Jump to application
void (*appptr)(void) = (void (*)(void))0x2000;
appptr();

Mode locking

Once the bootloader receives a valid packet, it locks into bootloader mode:
if (packet.status == PROTOCOL_PACKET_STATUS_OK) {
    if (*BOOT_MODE == 0) {
        *BOOT_MODE = BOOT_MODE_BOOTLOADER;  // Lock in bootloader
    }
}
This prevents the 100ms timeout from expiring during firmware upgrades.

Memory protection

The bootloader protects itself from being overwritten:
if (address < BOOTLOADER_APPLICATION_START) {  // 0x1800
    // Skip this DWORD - protected bootloader region
    address += 0x2;
    index += 3;
} else {
    // Write to application region
    flashRes = FlashWriteDWORDAddress(address, data, data2);
}
Attempts to write to addresses 0x0000 - 0x17FF are silently skipped.

Example firmware upgrade sequence

1

Request platform

send([0x00, 0x03, 0x00, 0x03])  # Command, Length, Data, XOR
response = receive()  # 0x01, Length, "BLUEBUS_BOOTLOADER_2_4", XOR
2

Erase flash

send([0x02, 0x03, 0x00, 0x01])
response = receive()  # 0x03, 0x03, 0x00, 0x00
# Wait for erase completion (can take 2-3 seconds)
3

Write firmware blocks

for block in firmware_blocks:
    # block = [Addr2, Addr1, Addr0, Data0...Data245]
    send([0x04, len(block) + 3, *block, calculate_xor()])
    response = receive()  # 0x05 (OK) or 0x06 (Error)
4

Start application

send([0x09, 0x03, 0x00, 0x0A])
response = receive()  # 0x0A, 0x03, 0x00, 0x09
# Bootloader jumps to application after 50ms

Interrupt handling

The bootloader implements trap handlers for fault conditions:
void __attribute__ ((__interrupt__, auto_psv)) _OscillatorFail(void) {
    INTCON1bits.OSCFAIL = 0;
    ON_LED = 0;
    while (1);  // Halt on error
}

void __attribute__ ((__interrupt__, auto_psv)) _AddressError(void)
void __attribute__ ((__interrupt__, auto_psv)) _StackError(void)
void __attribute__ ((__interrupt__, auto_psv)) _MathError(void)
void __attribute__ ((__interrupt__, auto_psv)) _NVMError(void)
void __attribute__ ((__interrupt__, auto_psv)) _GeneralError(void)
All traps turn off the LED and halt the processor.

EEPROM storage

The bootloader uses an external SPI EEPROM for persistent configuration:
AddressSizeContent
0x002 bytesSerial number (MSB, LSB)
0x023 bytesFirmware version (Major, Minor, Patch)
0x052 bytesBuild date (Week, Year)
0x071 byteBootloader mode flag
Serial number and build date can only be written once (when EEPROM values are 0xFF). This prevents accidental modification during production.

Build docs developers (and LLMs) love