Raw CAN bytes are not engineering data
A CAN frame might look like this:
can0 321 [8] 68 13 FF FF 40 1F 00 00
That line tells you the bus, identifier, payload length, and data bytes. It does not tell you the engine speed, battery current, gear, torque request, or temperature. To get those values, you need a database that defines what each bit means.
That is the job of a DBC file.
A DBC signal definition typically answers:
- Which CAN ID carries the signal?
- Where does the signal start?
- How many bits does it use?
- Is it little-endian or big-endian?
- Is it signed or unsigned?
- What scale and offset convert raw value to physical value?
- What unit should be shown?
- Is the signal multiplexed?
If one of those fields is wrong, the decoded value can look plausible and still be wrong.
The core formula
For a normal numeric signal, decoding follows this path:
raw_bits -> raw_integer -> physical_value
The final conversion is usually:
physical_value = raw_value * scale + offset
Encoding is the reverse:
raw_value = (physical_value - offset) / scale
Then the raw integer is packed into the payload at the signal's bit position.
Example: a simple little-endian signal
Assume this signal definition:
Signal: EngineSpeed
Start bit: 24
Length: 16 bits
Byte order: little_endian
Signed: no
Scale: 0.125
Offset: 0
Unit: rpm
And this payload:
FF FF FF 68 13 FF FF FF
Bytes 3 and 4 are:
68 13
For an Intel/little-endian signal, that becomes:
0x1368 = 4968
Apply scale:
4968 * 0.125 = 621 rpm
That exact kind of conversion appears constantly in heavy-duty and vehicle CAN databases.
Why byte order causes so many bugs
Byte order is where many DBC mistakes hide.
In DBC language:
- Intel usually means little-endian.
- Motorola usually means big-endian.
The confusing part is not the names. The confusing part is bit numbering and how multi-byte signals cross byte boundaries. A one-byte signal is easy. A 13-bit or 16-bit signal crossing bytes is where engineers start seeing values that are off by a factor, mirrored, or jumping unexpectedly.
When a value looks wrong:
Expected: 621 rpm
Decoded: 26643 rpm
Check byte order before changing scale.
Signedness matters after bit extraction
Signedness tells the decoder how to interpret the extracted raw integer.
For an 8-bit unsigned signal:
0xFE = 254
For an 8-bit signed two's-complement signal:
0xFE = -2
That difference is huge for signals like current, torque, acceleration, steering angle, or temperature delta.
Scale and offset are not optional polish
Scale and offset convert raw bus values into physical values.
Example:
Signal: BatteryVoltage
Raw value: 4032
Scale: 0.01
Offset: 0
Unit: V
Physical: 40.32 V
Offset is common when the raw value is stored as an unsigned number but needs to represent a range around zero.
Signal: Temperature
Raw value: 90
Scale: 1
Offset: -40
Physical: 50 degC
If the offset is missing, every decoded value looks shifted.
Decode with cantools
Install:
python3 -m pip install cantools
Load a DBC and inspect a message:
import cantools
db = cantools.database.load_file("vehicle.dbc")
message = db.get_message_by_name("EngineData")
print(message.frame_id)
print(message.length)
for signal in message.signals:
print(
signal.name,
signal.start,
signal.length,
signal.byte_order,
signal.is_signed,
signal.scale,
signal.offset,
signal.unit,
)
Decode one frame:
import cantools
db = cantools.database.load_file("vehicle.dbc")
frame_id = 0x321
data = bytes.fromhex("6813FFFF401F0000")
decoded = db.decode_message(frame_id, data)
print(decoded)
The output might look like:
{
"EngineSpeed": 621.0,
"CoolantTemperature": 50,
"TorqueRequest": 12.5
}
Encode with cantools
Encoding is useful for simulation, bench testing, and gateway validation.
import cantools
db = cantools.database.load_file("vehicle.dbc")
message = db.get_message_by_name("EngineData")
payload = message.encode({
"EngineSpeed": 621.0,
"CoolantTemperature": 50,
"TorqueRequest": 12.5,
})
print(payload.hex(" ").upper())
Send it on a virtual CAN interface:
import can
import cantools
db = cantools.database.load_file("vehicle.dbc")
message_def = db.get_message_by_name("EngineData")
payload = message_def.encode({
"EngineSpeed": 621.0,
"CoolantTemperature": 50,
"TorqueRequest": 12.5,
})
bus = can.Bus(channel="vcan0", interface="socketcan")
frame = can.Message(
arbitration_id=message_def.frame_id,
data=payload,
is_extended_id=message_def.is_extended_frame,
)
bus.send(frame)
Then watch it:
candump vcan0
Decode a candump log
For command-line decoding:
cat capture.log | python3 -m cantools decode vehicle.dbc
For compact output:
cat capture.log | python3 -m cantools decode --single-line vehicle.dbc
For plotting:
python3 -m pip install "cantools[plot]"
cat capture.log | python3 -m cantools plot vehicle.dbc "*Speed*" "*Temperature*"
This is a practical way to move from raw CAN logs to signal-level investigation.
Multiplexed messages need one more step
A multiplexed message uses one signal as a selector. The selector decides which other signals are valid in the payload.
Example:
Message: StatusFrame
Mux signal: Mode
Mode = 0:
PackVoltage
PackCurrent
Mode = 1:
CellMinVoltage
CellMaxVoltage
If the mux selector is wrong in the DBC, the decoder may extract the wrong set of signals from a valid frame. This is why multiplexer-aware DBC editing matters. You need to see the selector and dependent signals together.
DBC review checklist
When reviewing a DBC update, inspect:
- frame ID and extended-frame flag
- message length
- signal start bit
- signal length
- byte order
- signedness
- scale
- offset
- unit
- minimum and maximum
- receivers and senders
- value tables
- multiplexer configuration
- comments that explain non-obvious behavior
DBC Utility is built around exactly this kind of review. It is not just a file viewer. The value is seeing the message structure, signal metadata, edit context, and compare workflow together.