TEJVON
Back to Engineering Impact
BLE Engineering 8 min readJune 2026

CRC Integrity in BLE Data Pipelines: Catching Silent Corruption Before It Reaches Your App

BLE delivers packets. It does not guarantee your application layer received what was sent. CRC validation is the difference between safe firmware and a bricked device.

Summary

The BLE link layer has its own CRC — but it protects individual radio packets, not application payloads. Across fragmentation, reassembly, and application handling, silent data corruption is possible. Here's the validation strategy that prevented bricked devices in a production automotive OTA system.

T

Surendra — TEJVON

Senior Android & BLE Engineer

When you're transmitting a firmware image to an embedded device over BLE, the worst possible outcome is not a failed transfer. It's a successful transfer of corrupted data. A device that receives and applies a corrupted firmware image may become permanently inoperable — bricked, in the field, unreachable. That's a warranty claim, a truck roll, and a product recall risk.

We implemented a two-level CRC validation strategy in the automotive OTA system: per-chunk CRC for immediate detection, and full-image CRC before the device applies the firmware. This article covers both levels, the implementation, and the specific failure modes each level catches.

Why BLE's Built-in CRC Is Not Enough

The Bluetooth link layer computes a 24-bit CRC over every transmitted PDU. If the CRC fails, the packet is retransmitted. So why do you need application-level CRC? Because the link layer CRC only protects the radio transmission. Corruption can happen at other stages: memory allocation errors in the receiver, byte order issues in your fragmentation protocol, incorrect buffer offsets, integer overflow in length calculations. The link layer knows nothing about any of this.

CRC-16/CCITT-FALSE: The Right Algorithm for BLE Payloads

CRC-32 has better collision resistance but doubles your per-chunk overhead. For BLE where every byte counts against your MTU budget, CRC-16 is the right trade-off. We used CRC-16/CCITT-FALSE (polynomial 0x1021, initial value 0xFFFF), which is the same algorithm used in X.25, HDLC, and SD card protocols — well-tested on embedded targets.

CRC-16/CCITT-FALSE implementation for Android and matching C for embedded targetkotlin
// Kotlin — Android side
object Crc16Ccitt {
    private const val POLYNOMIAL = 0x1021
    private const val INITIAL_VALUE = 0xFFFF

    fun calculate(data: ByteArray, offset: Int = 0, length: Int = data.size): Int {
        var crc = INITIAL_VALUE
        for (i in offset until offset + length) {
            crc = crc xor ((data[i].toInt() and 0xFF) shl 8)
            repeat(8) {
                crc = if (crc and 0x8000 != 0) {
                    (crc shl 1) xor POLYNOMIAL
                } else {
                    crc shl 1
                }
                crc = crc and 0xFFFF // keep 16 bits
            }
        }
        return crc
    }

    fun verify(data: ByteArray, expectedCrc: Int): Boolean =
        calculate(data) == expectedCrc
}

// Equivalent C — embedded firmware side
uint16_t crc16_ccitt(const uint8_t *data, uint16_t length) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < length; i++) {
        crc ^= (uint16_t)data[i] << 8;
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x8000) crc = (crc << 1) ^ 0x1021;
            else              crc = crc << 1;
        }
    }
    return crc;
}

Two-Level Validation Strategy

Level 1: Per-Chunk CRC

Every fragment carries a 2-byte CRC of its payload. The receiver validates each chunk immediately on receipt. On mismatch, the receiver requests retransmission of the specific sequence number rather than the entire transfer. This catches transmission errors early and localises retransmission cost.

Level 2: Full-Image CRC

After all chunks are reassembled, we compute a CRC-16 of the complete payload and compare it against a reference value sent in the transfer initiation packet. This catches reassembly-order errors, buffer overflows, and any corruption that per-chunk CRC missed. The device firmware will not apply the image unless this check passes.

Two-level validation in the OTA transfer managerkotlin
class OtaTransferManager(
    private val bleManager: BleConnectionManager,
    private val fragmenter: BleFragmenter
) {
    suspend fun transfer(firmware: ByteArray): TransferResult {
        val fullImageCrc = Crc16Ccitt.calculate(firmware)
        val fragments = fragmenter.fragment(firmware)

        // Send transfer initiation: total chunks + full-image CRC
        bleManager.writeCharacteristic(
            OTA_CONTROL_CHAR_UUID,
            OtaCommand.Begin(fragments.size, fullImageCrc).toBytes()
        )

        for (fragment in fragments) {
            val packet = fragment.toByteArray()
            val ack = bleManager.writeAndAwaitAck(OTA_DATA_CHAR_UUID, packet)

            when (ack) {
                is OtaAck.ChunkCrcError -> {
                    // Resend this chunk only
                    bleManager.writeAndAwaitAck(OTA_DATA_CHAR_UUID, packet)
                }
                is OtaAck.Accepted -> continue
                is OtaAck.Rejected -> return TransferResult.Failed("Device rejected chunk ${fragment.seqNumber}")
            }
        }

        // Device now validates full-image CRC before applying
        val applyResult = bleManager.writeAndAwaitAck(
            OTA_CONTROL_CHAR_UUID,
            OtaCommand.Apply.toBytes()
        )

        return when (applyResult) {
            is OtaAck.ImageCrcValid -> TransferResult.Success
            is OtaAck.ImageCrcInvalid -> TransferResult.Failed("Full image CRC mismatch — transfer corrupted")
            else -> TransferResult.Failed("Unexpected response: $applyResult")
        }
    }
}

Production Result

In 18 months of production OTA deployments across automotive diagnostic devices, the two-level CRC strategy detected and recovered from 3 corruption events (all from RF interference during transfer). Zero devices were bricked. All events were handled transparently via chunk retransmission.

Topics

BLECRCData IntegrityOTAAndroidKotlin

Working on a similar challenge?

Let's discuss the architecture before you build.

BLE system design, OTA reliability, and connected product engineering — this is what TEJVON does every day.

Book a Technical Consultation