Blog

CAN Frame Bit Decoder

 

Introduction

One of my ongoing projects is improving end to end test automation for my automotive OEM client. So far, I have developed several tools for them which we are slowly combining into a single framework. In 2026, we are planning on properly productizing the framework into a deployable solution which can be used by the entire team. My Zephyr Testing and Websockets tools are also part of the same initiative, though the websockets was more experiemental and will most lilely not be used in the final version.

Today's article will cover a CAN frame bit decoder tool where hex CAN frames are separated into individual bytes and converted into binary so we can look at specific bit positions for specific values. In the world of CAN data, automotive OEMs will assign specific bits within the frame and specify their bit lengths to represent a single data point. So, for example, if we get an 8 byte hex frame, the 2nd byte may represent door functions and the 3rd+4th bits within the 2nd byte will represent some decimal value for that door's lock status. This particular OEM has 1028 of such data values corresponding to some functionality or sensor data on the vehicle. This data is sent to the backend server and is used to initiate certain functions. For example, if the backend receives data for a certain CAN ID representing the latest vehicle status, the mobile app can be updated with the latest vehicle status for the customer. The same thing goes for sending specific commands to the vehicle and getting ACK responses with the updated CAN frame.

Up until now, we had to manually convert the values from the hex string, map the values, and look at the bits for it.

The full code can be found on my github page.

Step 1: Global CAN specs

Each OEM has a master CAN spec document which lists all CAN frame IDs, bit position, bit length, and field name. In global_can_specs.py you will find dictionary definitions for all data values for a CAN ID (denoted by FIELDS_). For each data value, in the parenthases, are the byte numbers, bit positions, and bit lengths respectively. So for example, when we look at

FIELDS_D06 = {
    "AYYVA_EUFT": (1, 0, 8),
    "BAOIG_SYF": (8, 2, 1),
    "DAQVL_UTTJJ": (3, 3, 1),
    "EEG_NSOE91": (8, 4, 1),
    "EQY_BZZ": (3, 1, 1),
    "GQDV38": (5, 0, 2),
    "KIU_IIUW": (3, 2, 1),
    "KUCHL39": (5, 4, 2),
    "KVEXA_PJINM4": (3, 0, 1),
    "LISJ_DPBU": (5, 2, 2),
    "LQJFV_FJDPE": (5, 6, 2),
    "MLOI_OJV": (3, 4, 1),
    "PKU": (8, 3, 1),
    "TNU": (8, 1, 1),
    "TZJW": (2, 0, 8),
    "XWHV": (4, 0, 1),
}

D06 is the CAN ID, PKU is one of the data values within the frame, and the (8, 3, 1) tells us this value can be found in byte 8, 3rd bit, and the value is a single bit (so 1 or 0 in this case). Some are longer and can continue into previous bytes as well. Such as with FIELDS_093, the XXEQR_GCQR43 field is located in the 3rd byte but the total length of the value requires us to read 16 bits which would go into the 2nd byte (more on this later).

The values from this file are a randomized copy of the actual CAN global spec for this particular OEM since the field and bit mappings are proprietary. The values and fields are all random strings to protect the intellectual property. The concept remains the same however.

Step 2: Decoder and examples

Let's look at an example. Let's say we have a hex string D0600000400AA000000. When converting it to raw bits, we see 0000 0000 0000 0000 0000 0100 0000 0000 1010 1010 0000 0000 0000 0000 0000 0000. So if we look at value GQDV38, we should find its value in the 5th byte (1010 1010), 0th position, and we read 2 bits (10). So from the decoder, we should expect the value 2 as the final decimal for that value.

After entering it into the decoder script prompt, we receive the output below and we find GQDV38 with a value of 2 as we expected.

CAN ID: D06
Data hex: '00000400AA000000' (length 16)
Data length: 8 bytes
AYYVA_EUFT: 0 (0x0)
BAOIG_SYF: 0 (0x0)
DAQVL_UTTJJ: 0 (0x0)
EEG_NSOE91: 0 (0x0)
EQY_BZZ: 0 (0x0)
GQDV38: 2 (0x2)
KIU_IIUW: 1 (0x1)
KUCHL39: 2 (0x2)
KVEXA_PJINM4: 0 (0x0)
LISJ_DPBU: 2 (0x2)
LQJFV_FJDPE: 2 (0x2)
MLOI_OJV: 0 (0x0)
PKU: 0 (0x0)
TNU: 0 (0x0)
TZJW: 0 (0x0)
XWHV: 0 (0x0)

The decoder iterates through each value mapped to this hex CAN ID (D06) and parses each value in our bit sequence to output the final values. In this example, GQDV38's 2 value may represent a charging state of active and where a value of 3 could be charging error.

Conclusion

This simple tool has dramatically reduced the amount of time it takes for our team to decode the sequences and look at the values being sent by the ECU to the server (and eventually to the customer's device through the official app). This comes in especially handy when troubleshooting countless CAN messages being sent from the on board ECU to validate values being sent. In the future, I plan on importing this tool into our larger automation platform to help further automate our ability to automatically pull and analyze logs from the server. For specific test cases, we can define what the values should be based on the spec and enacted action and verify the correct information is being sent/captured and the test case can be marked as pass/fail based on the received values.

Next steps

I have already begun a second version of this decoder tool and will post an update once it has been tested and validated. The goal for iteration 2 is to add more human readable description of each values in our printout. Using GQDV38 again as our example, when doing on-the-fly analysis of CAN data, the printout would read GQDV38: 2 (Charging active) so we, as testers, can more easily read the data without having to go back to our specs documents and find what value of 2 represents for GQDV38.