Zephyr OS CRC-16 Streaming: Carry the Unfinalized State Across Buffer Boundaries
A CRC that matches for a short message but fails after the message is split into receive buffers often points to the streaming wrapper rather than the polynomial. Check where the initial seed is applied and where finalization occurs. A transport buffer boundary is not necessarily the end of the protected message.
This article follows the Zephyr v4.4.0 CRC-16 helper contracts. It does not introduce a new wire protocol or claim that an existing product passes a conformance test. The proposed checks concern software integration of an already specified CRC algorithm.

Name the complete variant
The tagged CRC header distinguishes crc16_ccitt, which reflects input and output, from crc16_itu_t, which does not. Both describe polynomial 0x1021, accept a seed and return a value without a final XOR. Their documented streaming method carries the previous block's result into the next block. A polynomial number alone therefore cannot select the correct helper or complete the protocol calculation.
Write down the protocol's reflection requirements, initial state, final XOR, protected byte range and transmitted byte order. Keep those choices together in a named wrapper. Otherwise a call site may borrow the seed from one familiar CRC variant and the finalization from another while still using a plausible function name.
The official CRC API reference is useful for navigating the available interfaces. It is a live reference and can describe a later development version. Use the tagged header above when evaluating the v4.4.0 behavior discussed here.
Keep intermediate state separate from the wire value
Suppose a hypothetical protocol requires a final XOR after processing its complete protected message. The receiver obtains the header and payload in three buffers. Start with the protocol's seed, update the state with the first buffer, and pass that returned state into the second and third updates. Apply the final XOR after the final protected byte, when producing the comparison or transmitted value.
Do not XOR each intermediate result merely because that value could be printed as a completed checksum. Feeding a finalized value back as the next seed changes the calculation unless the wrapper explicitly reverses that transformation. Similarly, reinitializing the seed for each received buffer computes separate block CRCs rather than one message CRC.
Use different names for the running state and the finalized wire checksum. This small distinction makes code review easier: a logging statement should not mutate the running state, and a packet-finalization helper should not be called from every buffer-completion callback.
Test partition invariance before changing parameters
Construct one canonical protected byte sequence. Calculate it in one call, then through the streaming wrapper using several partitions, including a split inside a multibyte protocol field. The finalized result should be independent of those partitions. This is a proposed software property test, not a reported execution result.
Also test consecutive messages. After completing one message, a new message must begin with its prescribed seed rather than the prior message's state. Decide how an aborted message resets the wrapper. A transport reconnect or an incomplete frame should not accidentally leave the next frame continuing an unrelated calculation.
Partition agreement alone is insufficient for protocol correctness. Two consistently wrong implementations can agree. Add an independently specified known-answer vector for the exact variant and a complete example frame with the documented protected range. Check the serialized checksum bytes separately from the numeric checksum value.
Keep framing decisions outside the CRC helper
The helper consumes bytes supplied by its caller. It cannot determine whether a length field, escaped representation, padding or the checksum field itself belongs in the protected range. Resolve those choices from the protocol specification before comparing algorithms.
When the complete-buffer result passes but a split-buffer result fails, inspect state handoff and finalization first. When both fail identically, inspect the variant and byte range. That division gives a useful diagnosis without randomly cycling through polynomial names until one test packet happens to match.