19е. DBC files: how to decode someone else's CAN bus
Section 19 taught the STM32 to receive CAN frames, and sections 19г and 19д to query the car by the standard: OBD-II and UDS diagnostics. But most of what travels on a car's bus does not answer any requests: the units simply send their frames round and round — speed, steering angle, RPM, pedals. What lies in which bit is described by a DBC file. Here you will see how to read it, where its traps are, how to decode a bus for which there is no file, and how to get C code for the STM32 from a DBC.
Sources: real files from the opendbc project (opendbc/dbc/vw_mqb.dbc and
opendbc/dbc/generator/toyota/_toyota_2017.dbc), frames and checksum
algorithms from the same place, motohawk.dbc and C code generated by cantools, from
the cantools tests. Everything was verified by a run on a PC: our own parser in Python,
the generated C code built by gcc in the stm-sys:1 container and by
arm-none-eabi-gcc for Cortex-M3 (STM32F103) in stm-arm:1. License: opendbc —
MIT (Comma.ai), cantools — MIT (Erik Moqvist); from can-utils and python-obd
(both GPL-2.0) only the log format and formulas were taken, no code.
Terms used in the documentation
Term Meaning DBC (CAN database) a text file describing the frames and signals of a bus BO_ (message) a message: identifier, length, sender SG_ (signal) a signal: where it lies in the frame and how to convert it to a physical value byte order: Intel / Motorola byte order: least significant first (@1) / most significant first (@0) factor, offset multiplier and offset: phys = raw × factor + offset multiplexor (M), multiplexed (mN) the multiplexer and the signals of its branch N VAL_, CM_, BA_ value table, comment, attribute rolling counter, checksum / CRC a frame counter and a checksum inside the frame itself
19е.1 What DBC is
DBC is an ordinary text file. It is understood, for example, by cantools and opendbc, which this section relies on. There are two main kinds of line.
BO_ declares a message. A line from vw_mqb.dbc:
BO_ 253 ESP_21: 8 Gateway_MQB
253 is the identifier (0xFD) in decimal, ESP_21 is the name, 8 is the data
length, Gateway_MQB is the sender. For an extended 29-bit identifier
the file sets the top bit of the number: 0x80000000 + identifier.
SG_ below it describes a signal:
SG_ ESP_v_Signal : 32|16@1+ (0.01,0) [0.00|655.32] "Unit_KiloMeterPerHour" XXX
In order: the name; start bit 32 and length 16 bits; @1 — Intel, least significant byte
first (@0 — Motorola, most significant first); + — unsigned (- — signed,
two's complement); (0.01,0) — factor and offset; [0.00|655.32] —
limits; the unit; the receivers. Physical value = raw × 0.01 + 0.
The other lines are for reference. VAL_ gives names to values
(VAL_ 173 GE_Fahrstufe 5 "P" 6 "R" 7 "N" 8 "D" … — the VW gear selector),
CM_ is a comment, BA_DEF_/BA_ are attributes. The most useful attribute is
GenMsgCycleTime, the send period in milliseconds; from it the receiver knows
that a unit has gone missing. In ford_lincoln_base_pt.dbc it is set for 156 messages, while
the VW and Toyota files from opendbc do not have it at all.
What is in the chosen files was counted by our own parser:
| File | Messages | Signals | Intel | Motorola | Signed | Value tables |
|---|---|---|---|---|---|---|
vw_mqb.dbc |
113 | 1348 | 1345 | 3 | 0 | 133 |
_toyota_2017.dbc |
50 | 310 | 5 | 305 | 23 | 51 |
Two manufacturers — two byte orders: VW is almost entirely Intel, Toyota is Motorola. This is the first thing people trip over.
19е.2 Intel and Motorola: the "sawtooth" of bit numbers
The bit number in DBC is the same everywhere: bit b is bit b % 8 of byte b / 8. Bit 0 is the least significant bit of byte 0, bit 15 is the most significant bit of byte 1. The difference between Intel and Motorola is in what "start" means and where to go next.
Intel (@1). The start is the least significant bit of the signal, then the numbers increase: 32, 33 …
39, 40 … 47. The low byte of the value comes first. A real ESP_21 frame from
the opendbc tests: 66 D0 1F 80 45 05 00 00. Byte 4 = 0x45, byte 5 = 0x05,
raw 0x0545 = 1349, speed 13.49 km/h.
Motorola (@0). The start is the most significant bit of the signal. Then it goes down through the bits
of the byte: 3, 2, 1, 0. When the byte ends, it jumps to the most significant bit of the next
byte: 15, 14 … 8. The numbers go in a sawtooth: 3, 2, 1, 0, 15, 14, 13, 12, 11, 10,
9, 8. This is how the Toyota steering angle STEER_ANGLE : 3|12@0- (1.5,0) lies in
message 0x25 STEER_ANGLE_SENSOR.
Decoding by hand. You need to send −45°. Raw = −45 / 1.5 = −30. In 12-bit
two's complement this is 0xFE2. The upper 4 bits (0xF) go into bits 3…0
of byte 0, the lower 8 (0xE2) into byte 1. Frame: 0F E2 00 00 00 00 00 00.
A byte-order error does not cause a failure; it gives a plausible wrong number: the
same bytes read as Intel 3|12 give raw −959 and −1438.5°. A second
example of the trap is the signal Temperature : 0|12@0- from motohawk.dbc: it starts at
bit 0 and occupies bits 0, 15, 14 … 8, 23, 22, 21 — three bytes for 12 bits.
Generated code (more on the generator in 19е.5) does the same with shifts, piece by piece:
void toy_steer_angle_sensor_unpack(toy_steer_angle_sensor_t *m, const uint8_t *d)
{
uint32_t v;
v = (uint32_t)(d[0] & 0x0Fu) << 8 | (uint32_t)d[1];
if (v & 0x800u) v |= 0xFFFFF000u; /* знак: дополнение до двух */
m->steer_angle = (int16_t)v;
19е.3 The multiplexer: several frames under one identifier
When there is more than eight bytes of data and a new identifier is too precious, part of the frame
is declared a multiplexer (M), and the signals are marked with a branch number (m0,
m1 …). A branch signal is present in the frame only if the multiplexer equals its
number. In vw_mqb.dbc the vehicle identification number (VIN) is sent this way in message 0x6B4
VIN_01:
SG_ VIN_01_MUX M : 0|2@1+ (1,0) [0|3] "" Airbag_MQB
SG_ KS_Geheimnis_1 m0 : 8|8@1+ (1,0) [0|255] "" Vector__XXX
SG_ VIN_4 m1 : 8|8@1+ (1,0) [0|255] "" Airbag_MQB
SG_ VIN_11 m2 : 8|8@1+ (1,0) [0|255] "" Airbag_MQB
Byte 1 is KS_Geheimnis_1, VIN_4 or VIN_11 depending on the two
low bits of byte 0. The seventeen characters of the VIN are spread over three branches: branch 0 holds
three characters and four key bytes, branches 1 and 2 hold seven characters each. The check
packed the sample VIN TESTVIN1234567890 into three frames 0000000000544553,
015456494E313233, 0234353637383930 and decoded it back. In C code the branches
become a switch on the multiplexer; when packing, only the branch
selected by the multiplexer is written.
vw_mqb.dbc has two multiplexers and 24 branch signals. Extended
multiplexing (SG_MUL_VAL_, a branch for a range of values) is
rarer; our parser does not understand it.
19е.4 Counters and checksums
Many frames — in the examples below these are the steering angle sensor and the brake system —
carry two service fields. The counter increments by 1 in every frame, wrapping around: the receiver
sees a gap or a "frozen" transmitter. The checksum is computed over the
frame data: the receiver sees corruption, and a forgery without knowledge of the algorithm
does not pass. In DBC these are ordinary signals COUNTER and CHECKSUM; how
to compute the sum, DBC does not say — the algorithm lives in code. In opendbc it is
chosen by the file name (opendbc/can/dbc.py, function
get_checksum_state): for toyota_ — toyota_checksum, for vw_mqb —
volkswagen_mqb_meb_checksum.
Toyota (opendbc/car/toyota/toyotacan.py) — a simple sum: the frame
length + the identifier bytes + all data bytes except the last one, modulo
256. The sum lies in the last byte, for example CHECKSUM : 63|8@0+
in message 0x260 STEER_TORQUE_SENSOR.
VW MQB (opendbc/car/volkswagen/mqbcan.py) — CRC-8 with polynomial 0x2F
(AUTOSAR E2E Profile 2): byte 0 is the CRC, the low nibble of byte 1 is the counter
0…15. The CRC runs over bytes 1…7, then over a "secret" byte chosen by the
counter; each message has its own table of 16 bytes. Without it you cannot get a correct
CRC, even if you know the polynomial. Our version in C (ported from
opendbc, without the 256-byte table):
uint8_t vw_mqb_crc(uint32_t addr, const uint8_t *d, uint8_t len)
{
const uint8_t *magic = addr == 0x86u ? magic_86 : addr == 0xFDu ? magic_fd : 0;
uint8_t crc = 0xFF;
for (uint8_t i = 1; i < len; i++) crc = crc8_2f(crc, d[i]);
if (magic) crc = crc8_2f(crc, magic[d[1] & 0x0Fu]);
return crc ^ 0xFFu;
}
The check uses real frames from opendbc/can/tests/test_checksums.py:
16 LWI_01 frames (0x86, steering angle) and 16 ESP_21 frames (0xFD) with
counters 0…15. The CRC matched in all 16 + 16, both in Python and in C code.
There is one more trap in the same place. The LWI_01 frame 03 03 BE A2 12 00 00 00:
LWI_Lenkradwinkel : 16|13@1+ (0.1,0) gives raw 702 and 70.2°, but
LWI_VZ_Lenkradwinkel (bit 29) is 1 — this is the sign. The angle is −70.2°. VW stores
the sign in a separate bit, and DBC cannot express that: - in DBC always means two's
complement. Code that takes only the angle signal confuses left and right.
19е.5 C code from DBC: cantools and our own generator
cantools can turn a DBC into C code: python -m cantools generate_c_source
file.dbc produces a .c/.h pair with a structure per message and the functions
_pack, _unpack, _encode, _decode. But in the stm-sys:1 container it does not
run: ModuleNotFoundError: No module named 'bitstruct' — the bitstruct and textparser
packages are missing, and this section does without third-party packages.
So the generator is our own: dbcmini.py in the verification directory, 301 lines.
It understands BO_, SG_ (Intel and Motorola, sign, factor and offset,
multiplexer M/mN), VAL_, CM_, message BA_; it does not understand
SG_MUL_VAL_, floating-point signals (SIG_VALTYPE_) or signals
longer than 32 bits. For each message it writes:
#define …_ID,…_DLC, and ifGenMsgCycleTimeis present,…_CYCLE_MS;- a structure with raw values in the narrowest integers (
int16_tfor a signed 12-bit value); …_unpack()and…_pack()— shifts and masks piece by piece, as in the listing in 19е.2;packclears the frame first;static inlinefunctions for conversion to the physical value; the type is set byDBC_REAL(doubleby default; on the F103floator even integers are more sensible).
Comparison with a reference. The cantools tests contain code generated by
cantools itself for motohawk.dbc (tests/files/c_source/motohawk.c). It and our code
for the same file were built together by gcc and run on 100000 random frames:
unpacking matched in 100000, packing back also in 100000.
Sizes for the STM32F103 (arm-none-eabi-gcc 12.2.1, -mcpu=cortex-m3 -Os):
| Code | Flash (text), bytes |
|---|---|
| Toyota: 2 messages, pack + unpack | 272 |
| VW: LWI_01, ESP_21, VIN_01 | 916 |
| motohawk, our code | 98 |
| motohawk, cantools (with encode/decode and limit checks) | 440 |
| Toyota sums and VW CRC | 198 |
By function: vw_mqb_crc — 108 bytes, toyota_checksum — 38; motohawk unpacking
is 48 bytes in ours and 58 in cantools. Not a single byte of RAM is needed: data and
bss are zero.
19е.6 A bus without a DBC: reconnaissance
There may be no file for your car. Then you write one yourself. The workflow:
- Record the bus.
candump -l can0writes a log with lines like(0000000004.540000) can0 0FD#AB0300009D060400— time, interface, identifier, bytes (the format is fromcandump.cin can-utils). The adapter can even be your own sniffer on STM32 (section 198е). - Split by identifier and see which bits change.
cansnifferfrom can-utils highlights changing bytes live. - Find the service fields. The counter is visible at once: its lowest bit changes in every frame, the next one in half of them, then 1/4, 1/8. A CRC byte looks like noise: each bit changes in roughly half of the frames.
- Move one thing and watch. Pressed the pedal, turned the wheel — in which field did changes appear? Better than eyeballing: in parallel, query a known value over OBD (section 19г) and look for the field that correlates with it.
- Fit the factor with a straight line through the "raw — OBD" points and write the signal into your own DBC.
Steps 3–5 were checked on a model. The model is 60 s of driving: speed 0 → 72 → 0 km/h,
the steering wheel back and forth; ESP_21 and LWI_01 frames every 20 ms built from
vw_mqb.dbc with a counter and a real CRC; an OBD response for PID 0x0D (speed,
byte A = km/h, as in python-obd) every 100 ms. 6600 frames in total, of which
600 are OBD responses. The result:
- bits 8…11 of frame 0xFD change in fractions 1.0, 0.5, 0.25 and 0.12 of the frames — the counter;
- the bits of byte 0 change in fractions from 0.48 to 0.50 — this is the CRC;
- 39 of 64 bits never change;
- a search over 237 fields (start, length 8/12/16, both orders) against the OBD speed found the field 32|16@1 with a correlation of 0.999913 and a factor of 0.00999 with an offset of −0.48 (OBD returns whole km/h with the fractional part truncated — hence almost −0.5).
But the fields 31|16@1 and 30|16@1 have the same correlation, 0.999913, with factors 0.005 and 0.0025: bits 30 and 31 do not change in the model, and the shifted field is the same number multiplied by 2 or 4. Statistics cannot find the boundary. The neighbouring bits decide: the field starts at the lowest changing bit, and engineers like a round factor — 0.01.
19е.7 Full code of the generator and the analysis
All the code in this section is our own: the dbcmini.py generator, the C files it produced,
the checksums dbc19e_e2e.c and the bus reconnaissance from the dbc19e.py run.
Below it is in order. From dbc19e_vw.c one function of six is shown —
unpacking VIN_01 with the multiplexer: the others are of the same kind as for
Toyota, and the generator writes them itself. The test bench dbc19e_host.c, which checks
our code against Python and against motohawk.c from cantools, is not shown: it only
reads run commands and prints answers. There is no firmware receiving frames over
bxCAN in this section — the code was checked on a PC and built into object files.
The C files are written by dbc19e.py --gen <directory>. It takes the DBC files from clones of
opendbc (github.com/commaai/opendbc) and cantools
(github.com/cantools/cantools), which lie side by side in the directory from
the OSS_DIR variable. The build for Cortex-M3 uses the same flags as in the run:
python dbc19e.py --gen .
for f in dbc19e_toy dbc19e_vw dbc19e_mh dbc19e_e2e; do
arm-none-eabi-gcc -std=c99 -mcpu=cortex-m3 -mthumb -Os -ffunction-sections -Wall -Wextra -I. -c $f.c -o $f.o
done
arm-none-eabi-size dbc19e_toy.o dbc19e_vw.o dbc19e_mh.o dbc19e_e2e.o
Parsing the DBC. Regular expressions for the BO_, SG_, VAL_, CM_
and BA_ lines, the signal and message classes, the parse() function. Signals belong
to the last BO_ above them; the first line of another kind ends their list.
# -*- coding: utf-8 -*-
"""Раздел 19е. Маленький разборщик DBC (подмножество) и генератор кода C для STM32.
Понимает: BO_, SG_ (Intel @1 и Motorola @0, знак +/-, множитель и смещение,
пределы, единица, получатели, мультиплексор M и ветви mN), VAL_, CM_ (BO_ и SG_),
BA_DEF_ / BA_DEF_DEF_ / BA_ для сообщений (GenMsgCycleTime и т. п.).
Не понимает: SG_MUL_VAL_ (расширенное мультиплексирование), SIG_VALTYPE_ (float),
сигналы длиннее 32 бит. cantools умеет больше, но в stm-sys:1 он не запускается:
нет пакетов bitstruct и textparser, а ставить их нельзя.
"""
import re
RE_BO = re.compile(r"^BO_ (\d+) (\w+) *: *(\d+) (\w+)", re.M)
RE_SG = re.compile(r'^ SG_ (\w+) *(M|m\d+)? *: *(\d+)\|(\d+)@([01])([+-]) *\(([^,]+),([^)]+)\) *'
r'\[([^|]*)\|([^\]]*)\] *"([^"]*)" *(.*)$')
RE_VAL = re.compile(r'^VAL_ (\d+) (\w+) (.*?);', re.M)
RE_CM = re.compile(r'^CM_ (BO_|SG_) (\d+) (?:(\w+) )?"(.*?)";', re.M | re.S)
RE_BADEF = re.compile(r'^BA_DEF_ BO_ +"(\w+)" (\w+)', re.M)
RE_BADEFDEF = re.compile(r'^BA_DEF_DEF_ +"(\w+)" +([^;]+);', re.M)
RE_BA = re.compile(r'^BA_ "(\w+)" BO_ (\d+) ([^;]+);', re.M)
class Signal:
def __init__(self, m):
(self.name, mux, sb, ln, bo, sg, fa, of, mn, mx, self.unit, rx) = m.groups()
self.start, self.length = int(sb), int(ln)
self.intel = bo == "1" # @1 — Intel (младший байт первым), @0 — Motorola
self.signed = sg == "-"
self.factor, self.offset = float(fa), float(of)
self.min, self.max = float(mn or 0), float(mx or 0)
self.receivers = [r for r in re.split(r"[ ,]+", rx.strip()) if r]
self.is_mux = mux == "M"
self.mux_id = int(mux[1:]) if mux and mux != "M" else None
self.choices, self.comment = {}, None
class Message:
def __init__(self, fid, name, dlc, sender):
self.frame_id, self.name, self.dlc, self.sender = fid, name, dlc, sender
self.extended = bool(fid & 0x80000000)
self.id = fid & 0x1FFFFFFF
self.signals, self.comment, self.attrs = [], None, {}
def sig(self, name):
return next(s for s in self.signals if s.name == name)
@property
def mux(self):
return next((s for s in self.signals if s.is_mux), None)
def parse(text):
msgs, cur = {}, None
for line in text.splitlines():
m = RE_BO.match(line)
if m:
fid = int(m.group(1))
cur = msgs[fid] = Message(fid, m.group(2), int(m.group(3)), m.group(4))
continue
m = RE_SG.match(line)
if m and cur is not None:
s = Signal(m)
assert s.length <= 32, s.name
cur.signals.append(s)
else:
cur = None
for fid, name, body in RE_VAL.findall(text):
if int(fid) in msgs:
s = next((x for x in msgs[int(fid)].signals if x.name == name), None)
if s is None:
continue
s.choices = {int(k): v for k, v in re.findall(r'(-?\d+) +"([^"]*)"', body)}
for kind, fid, name, txt in RE_CM.findall(text):
if int(fid) not in msgs:
continue
if kind == "BO_":
msgs[int(fid)].comment = txt
else:
s = next((x for x in msgs[int(fid)].signals if x.name == name), None)
if s is not None:
s.comment = txt
defaults = {n: v.strip().strip('"') for n, v in RE_BADEFDEF.findall(text)}
for n, _ in RE_BADEF.findall(text):
for mm in msgs.values():
if n in defaults:
mm.attrs[n] = defaults[n]
for n, fid, v in RE_BA.findall(text):
if int(fid) in msgs:
msgs[int(fid)].attrs[n] = v.strip().strip('"')
return msgs
Bit numbers. bit_positions() is the very sawtooth from 19е.2: for Motorola,
after bit 0 the number jumps up by 15. decode_raw() deliberately computes it
differently, through one big number: checking C against Python catches generator
errors rather than repeating them.
# ── Нумерация битов ─────────────────────────────────────────────────────
def bit_positions(s):
"""Номера битов сигнала от старшего к младшему (нумерация DBC: бит b = байт b//8, разряд b%8)."""
if s.intel:
return [s.start + i for i in range(s.length)][::-1]
out, b = [], s.start # Motorola: старт — старший бит, «пила»
for _ in range(s.length):
out.append(b)
b = b + 15 if b % 8 == 0 else b - 1
return out
def decode_raw(s, data):
"""Сырое значение другим способом, чем генератор C: через одно большое число."""
n = len(data)
if s.intel:
raw = (int.from_bytes(data, "little") >> s.start) & ((1 << s.length) - 1)
else:
msb = (s.start // 8) * 8 + (7 - s.start % 8) # номер старшего бита при счёте «слева направо»
raw = (int.from_bytes(data, "big") >> (n * 8 - msb - s.length)) & ((1 << s.length) - 1)
if s.signed and raw >> (s.length - 1):
raw -= 1 << s.length
return raw
def encode_raw(s, raw, data):
raw &= (1 << s.length) - 1
for k, b in enumerate(bit_positions(s)):
bit = (raw >> (s.length - 1 - k)) & 1
data[b // 8] = (data[b // 8] & ~(1 << (b % 8))) | (bit << (b % 8))
return data
def phys(s, raw):
return raw * s.factor + s.offset
def active(msg, data):
"""Сигналы, которые есть в кадре: мультиплексор выбирает свою ветвь."""
mx = msg.mux
sel = decode_raw(mx, data) if mx else None
return [s for s in msg.signals if s.mux_id is None or s.mux_id == sel]
def decode(msg, data):
return {s.name: decode_raw(s, data) for s in active(msg, data)}
def used_mask(msg, data):
mask = bytearray(msg.dlc)
for s in active(msg, data):
for b in bit_positions(s):
mask[b // 8] |= 1 << (b % 8)
return bytes(mask)
The C generator. segments() cuts a signal into pieces by bytes, and from the pieces
gen_c() writes the unpacking and packing lines. gen_dump() and py_dump()
are needed only by the test bench on the PC: they print the same thing via two independent
paths.
# ── Генератор C ─────────────────────────────────────────────────────────
def segments(s):
"""Куски сигнала по байтам: (байт, сдвиг в байте, ширина, позиция в значении)."""
pos = bit_positions(s) # от старшего к младшему
L, segs, k = s.length, [], 0
while k < L:
byte = pos[k] // 8
j = k
while j + 1 < L and pos[j + 1] // 8 == byte and pos[j + 1] == pos[j] - 1:
j += 1
lo = pos[j] % 8 # младший разряд куска в байте
segs.append((byte, lo, j - k + 1, L - 1 - j))
k = j + 1
return segs
def ctype(s):
for n in (8, 16, 32):
if s.length <= n:
return ("int%d_t" if s.signed else "uint%d_t") % n
def utype(s):
return "u" + ctype(s).lstrip("u")
def cname(x):
return re.sub(r"[^0-9a-z_]", "_", x.lower())
def fmt(x):
r = repr(float(x))
return r if ("." in r or "e" in r) else r + ".0"
def gen_c(prefix, msgs, names, hname):
"""Возвращает (текст .h, текст .c) для сообщений names."""
H = ["/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */",
"#ifndef %s_H" % prefix.upper(), "#define %s_H" % prefix.upper(),
"#include <stdint.h>", "", "#ifndef DBC_REAL", "#define DBC_REAL double", "#endif", ""]
C = ["/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */",
'#include <string.h>', '#include "%s"' % hname, ""]
for nm in names:
m = next(x for x in msgs.values() if x.name == nm)
t = "%s_%s_t" % (prefix, cname(m.name))
f = "%s_%s" % (prefix, cname(m.name))
cyc = m.attrs.get("GenMsgCycleTime")
H.append("#define %s_ID 0x%Xu%s" % (f.upper(), m.id, " /* расширенный */" if m.extended else ""))
H.append("#define %s_DLC %du" % (f.upper(), m.dlc))
if cyc is not None:
H.append("#define %s_CYCLE_MS %su" % (f.upper(), cyc))
H.append("typedef struct {")
for s in m.signals:
H.append(" %s %s;%s" % (ctype(s), cname(s.name),
" /* %s%s */" % ("мультиплексор" if s.is_mux else "ветвь %d" % s.mux_id,
"") if (s.is_mux or s.mux_id is not None) else ""))
H.append("} %s;" % t)
H.append("void %s_unpack(%s *m, const uint8_t *d);" % (f, t))
H.append("void %s_pack(uint8_t *d, const %s *m);" % (f, t))
for s in m.signals:
H.append("static inline DBC_REAL %s_%s_phys(%s raw) { return (DBC_REAL)raw * (DBC_REAL)%s + (DBC_REAL)%s; }"
% (f, cname(s.name), ctype(s), fmt(s.factor), fmt(s.offset)))
H.append("")
mx = m.mux
plain = [x for x in m.signals if x.mux_id is None]
plain.sort(key=lambda x: not x.is_mux)
branches = sorted({x.mux_id for x in m.signals if x.mux_id is not None})
def body(emit, ind):
out = []
for x in plain:
out += emit(x, ind)
if branches:
out.append(ind + "switch (m->%s) {" % cname(mx.name))
for k in branches:
out.append(ind + "case %d:" % k)
for x in m.signals:
if x.mux_id == k:
out += emit(x, ind + " ")
out.append(ind + " break;")
out.append(ind + "default:")
out.append(ind + " break;")
out.append(ind + "}")
return out
def emit_unpack(s, ind):
parts = []
for byte, lo, w, p in segments(s):
e = "(d[%d] >> %d)" % (byte, lo) if lo else "d[%d]" % byte
if lo + w < 8:
e = "(%s & 0x%02Xu)" % (e, (1 << w) - 1)
parts.append("(uint32_t)%s << %d" % (e, p) if p else "(uint32_t)%s" % e)
out = [ind + "v = " + " | ".join(parts) + ";"]
if s.signed:
out.append(ind + "if (v & 0x%Xu) v |= 0x%Xu; /* знак: дополнение до двух */"
% (1 << (s.length - 1), (0xFFFFFFFF << s.length) & 0xFFFFFFFF))
out.append(ind + "m->%s = (%s)v;" % (cname(s.name), ctype(s)))
return out
def emit_pack(s, ind):
out = [ind + "v = (uint32_t)(%s)m->%s;" % (utype(s), cname(s.name))]
for byte, lo, w, p in segments(s):
e = "(v >> %d)" % p if p else "v"
if w < 8:
e = "(%s & 0x%02Xu)" % (e, (1 << w) - 1)
out.append(ind + "d[%d] |= (uint8_t)%s;" % (byte, "(%s << %d)" % (e, lo) if lo else e))
return out
C.append("void %s_unpack(%s *m, const uint8_t *d)" % (f, t))
C += ["{", " uint32_t v;"] + body(emit_unpack, " ") + ["}", ""]
C.append("void %s_pack(uint8_t *d, const %s *m)" % (f, t))
C += ["{", " uint32_t v;", " memset(d, 0, %d);" % m.dlc] + body(emit_pack, " ") + ["}", ""]
H.append("#endif")
return "\n".join(H) + "\n", "\n".join(C)
def gen_dump(prefix, msgs, names):
"""Только для проверки на ПК: распаковать, напечатать сигналы, упаковать обратно."""
C = []
for nm in names:
m = next(x for x in msgs.values() if x.name == nm)
f = "%s_%s" % (prefix, cname(m.name))
C.append("static void %s_dump(const uint8_t *d)" % f)
C.append("{")
C.append(" %s_t m; uint8_t o[8]; int i;" % f)
C.append(" memset(&m, 0, sizeof m);")
C.append(" %s_unpack(&m, d);" % f)
C.append(' printf("%s");' % m.name)
mx = m.mux
for s in m.signals:
cond = "1" if s.mux_id is None else "m.%s == %d" % (cname(mx.name), s.mux_id)
C.append(' if (%s) printf(" %s=%%ld:%%.10g", (long)m.%s, %s_%s_phys(m.%s));'
% (cond, s.name, cname(s.name), f, cname(s.name), cname(s.name)))
C.append(" %s_pack(o, &m);" % f)
C.append(' printf(" |");')
C.append(' for (i = 0; i < %d; i++) printf("%%02X", o[i]);' % m.dlc)
C.append(' printf("\\n");')
C.append("}")
C.append("")
return "\n".join(C) + "\n"
def py_dump(msg, data):
"""То же, что печатает gen_dump, но независимым путём decode_raw/encode_raw."""
vals = decode(msg, data)
out = msg.name
for s in msg.signals:
if s.name in vals:
out += " %s=%d:%s" % (s.name, vals[s.name], "%.10g" % phys(s, vals[s.name]))
o = bytearray(msg.dlc)
for s in active(msg, data):
encode_raw(s, vals[s.name], o)
return out + " |" + o.hex().upper()
Which messages to generate — the beginning of dbc19e.py. DBC_TOY, DBC_VW
and DBC_MH are the paths to opendbc/dbc/generator/toyota/_toyota_2017.dbc,
opendbc/dbc/vw_mqb.dbc and to tests/files/dbc/motohawk.dbc from cantools:
TOY = dm.parse(read(DBC_TOY))
VW = dm.parse(read(DBC_VW))
MH = dm.parse(read(DBC_MH))
SETS = [("toy", TOY, ["STEER_ANGLE_SENSOR", "STEER_TORQUE_SENSOR"]),
("vw", VW, ["LWI_01", "ESP_21", "VIN_01"]),
("mh", MH, ["ExampleMessage"])]
BYNAME = {n: next(m for m in db.values() if m.name == n) for _, db, ns in SETS for n in ns}
def generate(outdir):
for pre, db, names in SETS:
h, c = dm.gen_c(pre, db, names, "dbc19e_%s.h" % pre)
for ext, txt in (("h", h), ("c", c)):
with open(os.path.join(outdir, "dbc19e_%s.%s" % (pre, ext)), "w", encoding="utf-8", newline="\n") as fh:
fh.write(txt)
What came out for Toyota — the header and the code in full. Conversion to the physical
value is done by static inline functions in the header; their type is set by DBC_REAL
(19е.5):
/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */
#ifndef TOY_H
#define TOY_H
#include <stdint.h>
#ifndef DBC_REAL
#define DBC_REAL double
#endif
#define TOY_STEER_ANGLE_SENSOR_ID 0x25u
#define TOY_STEER_ANGLE_SENSOR_DLC 8u
typedef struct {
int16_t steer_angle;
int8_t steer_fraction;
int16_t steer_rate;
} toy_steer_angle_sensor_t;
void toy_steer_angle_sensor_unpack(toy_steer_angle_sensor_t *m, const uint8_t *d);
void toy_steer_angle_sensor_pack(uint8_t *d, const toy_steer_angle_sensor_t *m);
static inline DBC_REAL toy_steer_angle_sensor_steer_angle_phys(int16_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.5 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_angle_sensor_steer_fraction_phys(int8_t raw) { return (DBC_REAL)raw * (DBC_REAL)0.1 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_angle_sensor_steer_rate_phys(int16_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
#define TOY_STEER_TORQUE_SENSOR_ID 0x260u
#define TOY_STEER_TORQUE_SENSOR_DLC 8u
typedef struct {
int16_t steer_torque_eps;
int16_t steer_torque_driver;
int16_t steer_angle;
uint8_t steer_angle_initializing;
uint8_t steer_override;
uint8_t checksum;
} toy_steer_torque_sensor_t;
void toy_steer_torque_sensor_unpack(toy_steer_torque_sensor_t *m, const uint8_t *d);
void toy_steer_torque_sensor_pack(uint8_t *d, const toy_steer_torque_sensor_t *m);
static inline DBC_REAL toy_steer_torque_sensor_steer_torque_eps_phys(int16_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_torque_sensor_steer_torque_driver_phys(int16_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_torque_sensor_steer_angle_phys(int16_t raw) { return (DBC_REAL)raw * (DBC_REAL)0.0573 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_torque_sensor_steer_angle_initializing_phys(uint8_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_torque_sensor_steer_override_phys(uint8_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
static inline DBC_REAL toy_steer_torque_sensor_checksum_phys(uint8_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
#endif
/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */
#include <string.h>
#include "dbc19e_toy.h"
void toy_steer_angle_sensor_unpack(toy_steer_angle_sensor_t *m, const uint8_t *d)
{
uint32_t v;
v = (uint32_t)(d[0] & 0x0Fu) << 8 | (uint32_t)d[1];
if (v & 0x800u) v |= 0xFFFFF000u; /* знак: дополнение до двух */
m->steer_angle = (int16_t)v;
v = (uint32_t)(d[4] >> 4);
if (v & 0x8u) v |= 0xFFFFFFF0u; /* знак: дополнение до двух */
m->steer_fraction = (int8_t)v;
v = (uint32_t)(d[4] & 0x0Fu) << 8 | (uint32_t)d[5];
if (v & 0x800u) v |= 0xFFFFF000u; /* знак: дополнение до двух */
m->steer_rate = (int16_t)v;
}
void toy_steer_angle_sensor_pack(uint8_t *d, const toy_steer_angle_sensor_t *m)
{
uint32_t v;
memset(d, 0, 8);
v = (uint32_t)(uint16_t)m->steer_angle;
d[0] |= (uint8_t)((v >> 8) & 0x0Fu);
d[1] |= (uint8_t)v;
v = (uint32_t)(uint8_t)m->steer_fraction;
d[4] |= (uint8_t)((v & 0x0Fu) << 4);
v = (uint32_t)(uint16_t)m->steer_rate;
d[4] |= (uint8_t)((v >> 8) & 0x0Fu);
d[5] |= (uint8_t)v;
}
void toy_steer_torque_sensor_unpack(toy_steer_torque_sensor_t *m, const uint8_t *d)
{
uint32_t v;
v = (uint32_t)d[5] << 8 | (uint32_t)d[6];
if (v & 0x8000u) v |= 0xFFFF0000u; /* знак: дополнение до двух */
m->steer_torque_eps = (int16_t)v;
v = (uint32_t)d[1] << 8 | (uint32_t)d[2];
if (v & 0x8000u) v |= 0xFFFF0000u; /* знак: дополнение до двух */
m->steer_torque_driver = (int16_t)v;
v = (uint32_t)d[3] << 8 | (uint32_t)d[4];
if (v & 0x8000u) v |= 0xFFFF0000u; /* знак: дополнение до двух */
m->steer_angle = (int16_t)v;
v = (uint32_t)((d[0] >> 3) & 0x01u);
m->steer_angle_initializing = (uint8_t)v;
v = (uint32_t)(d[0] & 0x01u);
m->steer_override = (uint8_t)v;
v = (uint32_t)d[7];
m->checksum = (uint8_t)v;
}
void toy_steer_torque_sensor_pack(uint8_t *d, const toy_steer_torque_sensor_t *m)
{
uint32_t v;
memset(d, 0, 8);
v = (uint32_t)(uint16_t)m->steer_torque_eps;
d[5] |= (uint8_t)(v >> 8);
d[6] |= (uint8_t)v;
v = (uint32_t)(uint16_t)m->steer_torque_driver;
d[1] |= (uint8_t)(v >> 8);
d[2] |= (uint8_t)v;
v = (uint32_t)(uint16_t)m->steer_angle;
d[3] |= (uint8_t)(v >> 8);
d[4] |= (uint8_t)v;
v = (uint32_t)(uint8_t)m->steer_angle_initializing;
d[0] |= (uint8_t)((v & 0x01u) << 3);
v = (uint32_t)(uint8_t)m->steer_override;
d[0] |= (uint8_t)(v & 0x01u);
v = (uint32_t)(uint8_t)m->checksum;
d[7] |= (uint8_t)v;
}
motohawk. The Temperature signal, which spans three bytes (19е.2),
became three pieces with different shifts:
/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */
#include <string.h>
#include "dbc19e_mh.h"
void mh_examplemessage_unpack(mh_examplemessage_t *m, const uint8_t *d)
{
uint32_t v;
v = (uint32_t)(d[0] & 0x01u) << 11 | (uint32_t)d[1] << 3 | (uint32_t)(d[2] >> 5);
if (v & 0x800u) v |= 0xFFFFF000u; /* знак: дополнение до двух */
m->temperature = (int16_t)v;
v = (uint32_t)((d[0] >> 1) & 0x3Fu);
m->averageradius = (uint8_t)v;
v = (uint32_t)(d[0] >> 7);
m->enable = (uint8_t)v;
}
void mh_examplemessage_pack(uint8_t *d, const mh_examplemessage_t *m)
{
uint32_t v;
memset(d, 0, 8);
v = (uint32_t)(uint16_t)m->temperature;
d[0] |= (uint8_t)((v >> 11) & 0x01u);
d[1] |= (uint8_t)(v >> 3);
d[2] |= (uint8_t)((v & 0x07u) << 5);
v = (uint32_t)(uint8_t)m->averageradius;
d[0] |= (uint8_t)((v & 0x3Fu) << 1);
v = (uint32_t)(uint8_t)m->enable;
d[0] |= (uint8_t)((v & 0x01u) << 7);
}
The VIN_01 multiplexer — a switch on the two low bits of byte 0
(19е.3):
void vw_vin_01_unpack(vw_vin_01_t *m, const uint8_t *d)
{
uint32_t v;
v = (uint32_t)(d[0] & 0x03u);
m->vin_01_mux = (uint8_t)v;
switch (m->vin_01_mux) {
case 0:
v = (uint32_t)d[1];
m->ks_geheimnis_1 = (uint8_t)v;
v = (uint32_t)d[2];
m->ks_geheimnis_2 = (uint8_t)v;
v = (uint32_t)d[3];
m->ks_geheimnis_3 = (uint8_t)v;
v = (uint32_t)d[4];
m->ks_geheimnis_4 = (uint8_t)v;
v = (uint32_t)d[5];
m->vin_1 = (uint8_t)v;
v = (uint32_t)d[6];
m->vin_2 = (uint8_t)v;
v = (uint32_t)d[7];
m->vin_3 = (uint8_t)v;
break;
case 1:
v = (uint32_t)d[1];
m->vin_4 = (uint8_t)v;
v = (uint32_t)d[2];
m->vin_5 = (uint8_t)v;
v = (uint32_t)d[3];
m->vin_6 = (uint8_t)v;
v = (uint32_t)d[4];
m->vin_7 = (uint8_t)v;
v = (uint32_t)d[5];
m->vin_8 = (uint8_t)v;
v = (uint32_t)d[6];
m->vin_9 = (uint8_t)v;
v = (uint32_t)d[7];
m->vin_10 = (uint8_t)v;
break;
case 2:
v = (uint32_t)d[1];
m->vin_11 = (uint8_t)v;
v = (uint32_t)d[2];
m->vin_12 = (uint8_t)v;
v = (uint32_t)d[3];
m->vin_13 = (uint8_t)v;
v = (uint32_t)d[4];
m->vin_14 = (uint8_t)v;
v = (uint32_t)d[5];
m->vin_15 = (uint8_t)v;
v = (uint32_t)d[6];
m->vin_16 = (uint8_t)v;
v = (uint32_t)d[7];
m->vin_17 = (uint8_t)v;
break;
default:
break;
}
}
Sums and the counter (19е.4): the Toyota sum, the VW MQB CRC-8 0x2F with "secret" bytes, and the counter check:
/* Раздел 19е: контрольные суммы и счётчики автомобильных кадров. */
#ifndef DBC19E_E2E_H
#define DBC19E_E2E_H
#include <stdint.h>
uint8_t toyota_checksum(uint32_t addr, const uint8_t *d, uint8_t len);
uint8_t vw_mqb_crc(uint32_t addr, const uint8_t *d, uint8_t len);
int counter_next_ok(uint8_t prev, uint8_t now, uint8_t bits);
#endif
/* Раздел 19е: контрольные суммы и счётчики автомобильных кадров.
Алгоритмы пересказаны по opendbc (MIT, Comma.ai): toyota_checksum —
opendbc/car/toyota/toyotacan.py, vw_mqb_crc — opendbc/car/volkswagen/mqbcan.py
(CRC-8 с многочленом 0x2F, как AUTOSAR E2E Profile 2). */
#include "dbc19e_e2e.h"
/* Toyota: длина + байты адреса + все байты, кроме последнего (там сама сумма) */
uint8_t toyota_checksum(uint32_t addr, const uint8_t *d, uint8_t len)
{
uint32_t s = len;
while (addr) { s += addr & 0xFFu; addr >>= 8; }
for (uint8_t i = 0; i + 1u < len; i++) s += d[i];
return (uint8_t)s;
}
static uint8_t crc8_2f(uint8_t crc, uint8_t b)
{
crc ^= b;
for (int k = 0; k < 8; k++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x2Fu) : (uint8_t)(crc << 1);
return crc;
}
/* «Секретный» байт на каждое значение счётчика: свой у каждого кадра */
static const uint8_t magic_86[16] = { 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86 };
static const uint8_t magic_fd[16] = { 0xB4, 0xEF, 0xF8, 0x49, 0x1E, 0xE5, 0xC2, 0xC0,
0x97, 0x19, 0x3C, 0xC9, 0xF1, 0x98, 0xD6, 0x61 };
/* VW MQB: CRC по байтам 1..len-1, затем по байту magic[счётчик]; счётчик — d[1] & 0x0F */
uint8_t vw_mqb_crc(uint32_t addr, const uint8_t *d, uint8_t len)
{
const uint8_t *magic = addr == 0x86u ? magic_86 : addr == 0xFDu ? magic_fd : 0;
uint8_t crc = 0xFF;
for (uint8_t i = 1; i < len; i++) crc = crc8_2f(crc, d[i]);
if (magic) crc = crc8_2f(crc, magic[d[1] & 0x0Fu]);
return crc ^ 0xFFu;
}
/* Счётчик должен вырасти ровно на 1 по модулю 2^bits */
int counter_next_ok(uint8_t prev, uint8_t now, uint8_t bits)
{
return now == (uint8_t)((prev + 1u) & ((1u << bits) - 1u));
}
Bus reconnaissance (19е.6). The same VW CRC in Python — via a 256-byte table; the model signs its frames with it:
CRC8H2F = []
for i in range(256):
c = i
for _ in range(8):
c = ((c << 1) ^ 0x2F) & 0xFF if c & 0x80 else (c << 1) & 0xFF
CRC8H2F.append(c)
MAGIC = {0x86: [0x86] * 16, 0xFD: [0xB4, 0xEF, 0xF8, 0x49, 0x1E, 0xE5, 0xC2, 0xC0, 0x97, 0x19, 0x3C, 0xC9, 0xF1, 0x98, 0xD6, 0x61]}
def vw_crc(addr, dd):
c = 0xFF
for x in dd[1:]:
c = CRC8H2F[c ^ x]
c = CRC8H2F[c ^ MAGIC[addr][dd[1] & 0x0F]]
return c ^ 0xFF
The drive model, the per-bit change fractions and the field search against the OBD speed are
the end of dbc19e.py in full. esp and lwi are the ESP_21 and LWI_01 messages
from the parsed vw_mqb.dbc, dm is the dbcmini module:
# ══ 6. Разведка чужой шины: модель ══════════════════════════════════════
# 60 с езды: скорость 0 → 72 → 0 км/ч, руль туда-сюда. Кадры ESP_21 и LWI_01 собираются
# настоящими правилами из vw_mqb.dbc (счётчик + CRC), OBD-ответы 0x7E8 на PID 0x0D — раз в 100 мс.
rnd = random.Random(1999)
log, obd = [], []
cnt = {0xFD: 0, 0x86: 0}
for i in range(3000): # шаг 20 мс
t = i * 0.02
v = 72.0 * math.sin(math.pi * t / 60.0) + rnd.uniform(-0.05, 0.05)
ang = 90.0 * math.sin(2 * math.pi * t / 7.0)
for addr, m, vals in ((0xFD, esp, {"ESP_v_Signal": round(max(v, 0) / 0.01), "ESP_Systemstatus": 1}),
(0x86, lwi, {"LWI_Lenkradwinkel": round(abs(ang) / 0.1), "LWI_VZ_Lenkradwinkel": int(ang < 0),
"LWI_Lenkradw_Geschw": rnd.randrange(0, 40)})):
dd = bytearray(8)
vals["COUNTER"] = cnt[addr]
cnt[addr] = (cnt[addr] + 1) & 15
for n, r in vals.items():
dm.encode_raw(m.sig(n), r, dd)
dd[0] = vw_crc(addr, dd)
log.append((t, addr, bytes(dd)))
if i % 5 == 0:
a = int(max(v, 0)) # PID 0x0D: байт A = км/ч
log.append((t + 0.001, 0x7E8, bytes([0x03, 0x41, 0x0D, a, 0x55, 0x55, 0x55, 0x55])))
obd.append((t, a))
print("модель: кадров в журнале %d, из них OBD-ответов %d; строка журнала: (%010d.%06d) can0 %03X#%s" % (
len(log), len(obd), int(log[500][0]), round(log[500][0] % 1 * 1e6), log[500][1], log[500][2].hex().upper()))
for addr in (0xFD, 0x86):
fr = [x[2] for x in log if x[1] == addr]
flips = [sum(((fr[k][b // 8] ^ fr[k + 1][b // 8]) >> (b % 8)) & 1 for k in range(len(fr) - 1)) / (len(fr) - 1) for b in range(64)]
print("0x%X: доля смен по битам 8..11 (счётчик) %s; байт 0 (CRC) от %.2f до %.2f; никогда не меняются %d бит из 64" % (
addr, [round(flips[b], 2) for b in range(8, 12)], min(flips[:8]), max(flips[:8]), sum(f == 0 for f in flips)))
fr = [(x[0], x[2]) for x in log if x[1] == 0xFD]
best = []
for start in range(0, 64):
for ln in (8, 12, 16):
for intel in (True, False):
if intel and start + ln > 64:
continue
s = dm.Signal(dm.RE_SG.match(' SG_ X : %d|%d@%s+ (1,0) [0|0] "" X' % (start, ln, "1" if intel else "0")))
if not intel and max(dm.bit_positions(s)) > 63 or not intel and any(b < 0 for b in dm.bit_positions(s)):
continue
xs, ys = [], []
j = 0
for t, a in obd:
while j + 1 < len(fr) and fr[j + 1][0] <= t:
j += 1
xs.append(dm.decode_raw(s, fr[j][1]))
ys.append(a)
mx, my = sum(xs) / len(xs), sum(ys) / len(ys)
sxx = sum((x - mx) ** 2 for x in xs)
syy = sum((y - my) ** 2 for y in ys)
if sxx == 0:
continue
sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
best.append((sxy / math.sqrt(sxx * syy), start, ln, intel, sxy / sxx, my - sxy / sxx * mx))
best.sort(reverse=True)
r, start, ln, intel, k, b = best[0]
print("разведка 0xFD: лучший кандидат %d|%d@%s, корреляция с OBD %.6f, множитель %.5f, смещение %.2f" % (
start, ln, "1" if intel else "0", r, k, b))
for c in best[1:3]:
print(" следующий: %d|%d@%s, корреляция %.6f, множитель %.5f" % (c[1], c[2], "1" if c[3] else "0", c[0], c[4]))
print("кандидатов перебрано:", len(best))
19е.8 Pitfalls
Steering angle −1438° instead of −45°. The byte order is mixed up: Motorola
was read as Intel. Toyota is @0, VW is @1; look at the character after @
in each line, not at the file as a whole — vw_mqb.dbc has 3 Motorola signals
among 1345 Intel ones.
A Motorola signal "slid" onto neighbouring bytes. The Motorola start is the most significant bit, not the least significant one, and the numbers go in a sawtooth. Decoding one frame by hand (as in 19е.2) catches the error at once.
Left and right are swapped. The sign is stored in a separate bit (LWI_VZ_… on
VW), while the angle signal is declared +. DBC cannot express sign-magnitude — you have to
do it in code.
The unit ignores your frame. No counter, a wrong CRC or the wrong "secret" byte. In VW MQB the CRC depends on a table of 16 counter values, different for each message. The algorithm is not in the DBC — it is in the opendbc code.
Branch signals read as garbage. The multiplexer was not checked: in a
VIN_01 frame with multiplexer 1, byte 1 is a VIN character, not a key byte.
Reconnaissance found a field with perfect correlation, but the scale is strange. Neighbouring unchanging bits give the same correlation with a shift of ×2, ×4. Take the lowest changing bit and a round factor.
cantools does not install. Generation needs Python with bitstruct and textparser.
No network — either your own generator for the subset you need, or ready-made
.c/.h files generated on another machine.
19е.9 Summary
- DBC is text:
BO_is a message (identifier, length, sender),SG_is a signalstart|length@order sign (factor,offset) [min|max] "unit". @1is Intel, the start is the least significant bit;@0is Motorola, the start is the most significant bit, the numbers go in a sawtooth: 3, 2, 1, 0, 15 … 8. −45° inSTEER_ANGLE 3|12@0-is0F E2; read as Intel, the same bytes give −1438.5°.vw_mqb.dbchas 113 messages and 1348 signals, almost all Intel;_toyota_2017.dbchas 50 and 310, almost all Motorola.- The multiplexer
Mand branchesmN: the VIN inVIN_01is sent in three frames. - The counter and the CRC are ordinary signals, but the algorithm is not in the DBC. The VW MQB CRC-8 0x2F with "secret" bytes matched on 16 + 16 real frames.
- Our own C generator was checked against cantools code: 100000 of 100000 frames; on the F103, pack + unpack for two Toyota messages takes 272 bytes of flash, RAM — 0.
- Reconnaissance of someone else's bus: the counter has change fractions 1, 1/2, 1/4…, the CRC about 0.5; the speed field was found by correlation with OBD (0.999913), the factor 0.01 — taking the neighbouring bits into account.
Full sources
dbcmini.py — a small DBC parser and C code generator.
# -*- coding: utf-8 -*-
"""Раздел 19е. Маленький разборщик DBC (подмножество) и генератор кода C для STM32.
Понимает: BO_, SG_ (Intel @1 и Motorola @0, знак +/-, множитель и смещение,
пределы, единица, получатели, мультиплексор M и ветви mN), VAL_, CM_ (BO_ и SG_),
BA_DEF_ / BA_DEF_DEF_ / BA_ для сообщений (GenMsgCycleTime и т. п.).
Не понимает: SG_MUL_VAL_ (расширенное мультиплексирование), SIG_VALTYPE_ (float),
сигналы длиннее 32 бит. cantools умеет больше, но в stm-sys:1 он не запускается:
нет пакетов bitstruct и textparser, а ставить их нельзя.
"""
import re
RE_BO = re.compile(r"^BO_ (\d+) (\w+) *: *(\d+) (\w+)", re.M)
RE_SG = re.compile(r'^ SG_ (\w+) *(M|m\d+)? *: *(\d+)\|(\d+)@([01])([+-]) *\(([^,]+),([^)]+)\) *'
r'\[([^|]*)\|([^\]]*)\] *"([^"]*)" *(.*)$')
RE_VAL = re.compile(r'^VAL_ (\d+) (\w+) (.*?);', re.M)
RE_CM = re.compile(r'^CM_ (BO_|SG_) (\d+) (?:(\w+) )?"(.*?)";', re.M | re.S)
RE_BADEF = re.compile(r'^BA_DEF_ BO_ +"(\w+)" (\w+)', re.M)
RE_BADEFDEF = re.compile(r'^BA_DEF_DEF_ +"(\w+)" +([^;]+);', re.M)
RE_BA = re.compile(r'^BA_ "(\w+)" BO_ (\d+) ([^;]+);', re.M)
class Signal:
def __init__(self, m):
(self.name, mux, sb, ln, bo, sg, fa, of, mn, mx, self.unit, rx) = m.groups()
self.start, self.length = int(sb), int(ln)
self.intel = bo == "1" # @1 — Intel (младший байт первым), @0 — Motorola
self.signed = sg == "-"
self.factor, self.offset = float(fa), float(of)
self.min, self.max = float(mn or 0), float(mx or 0)
self.receivers = [r for r in re.split(r"[ ,]+", rx.strip()) if r]
self.is_mux = mux == "M"
self.mux_id = int(mux[1:]) if mux and mux != "M" else None
self.choices, self.comment = {}, None
class Message:
def __init__(self, fid, name, dlc, sender):
self.frame_id, self.name, self.dlc, self.sender = fid, name, dlc, sender
self.extended = bool(fid & 0x80000000)
self.id = fid & 0x1FFFFFFF
self.signals, self.comment, self.attrs = [], None, {}
def sig(self, name):
return next(s for s in self.signals if s.name == name)
@property
def mux(self):
return next((s for s in self.signals if s.is_mux), None)
def parse(text):
msgs, cur = {}, None
for line in text.splitlines():
m = RE_BO.match(line)
if m:
fid = int(m.group(1))
cur = msgs[fid] = Message(fid, m.group(2), int(m.group(3)), m.group(4))
continue
m = RE_SG.match(line)
if m and cur is not None:
s = Signal(m)
assert s.length <= 32, s.name
cur.signals.append(s)
else:
cur = None
for fid, name, body in RE_VAL.findall(text):
if int(fid) in msgs:
s = next((x for x in msgs[int(fid)].signals if x.name == name), None)
if s is None:
continue
s.choices = {int(k): v for k, v in re.findall(r'(-?\d+) +"([^"]*)"', body)}
for kind, fid, name, txt in RE_CM.findall(text):
if int(fid) not in msgs:
continue
if kind == "BO_":
msgs[int(fid)].comment = txt
else:
s = next((x for x in msgs[int(fid)].signals if x.name == name), None)
if s is not None:
s.comment = txt
defaults = {n: v.strip().strip('"') for n, v in RE_BADEFDEF.findall(text)}
for n, _ in RE_BADEF.findall(text):
for mm in msgs.values():
if n in defaults:
mm.attrs[n] = defaults[n]
for n, fid, v in RE_BA.findall(text):
if int(fid) in msgs:
msgs[int(fid)].attrs[n] = v.strip().strip('"')
return msgs
# ── Нумерация битов ─────────────────────────────────────────────────────
def bit_positions(s):
"""Номера битов сигнала от старшего к младшему (нумерация DBC: бит b = байт b//8, разряд b%8)."""
if s.intel:
return [s.start + i for i in range(s.length)][::-1]
out, b = [], s.start # Motorola: старт — старший бит, «пила»
for _ in range(s.length):
out.append(b)
b = b + 15 if b % 8 == 0 else b - 1
return out
def decode_raw(s, data):
"""Сырое значение другим способом, чем генератор C: через одно большое число."""
n = len(data)
if s.intel:
raw = (int.from_bytes(data, "little") >> s.start) & ((1 << s.length) - 1)
else:
msb = (s.start // 8) * 8 + (7 - s.start % 8) # номер старшего бита при счёте «слева направо»
raw = (int.from_bytes(data, "big") >> (n * 8 - msb - s.length)) & ((1 << s.length) - 1)
if s.signed and raw >> (s.length - 1):
raw -= 1 << s.length
return raw
def encode_raw(s, raw, data):
raw &= (1 << s.length) - 1
for k, b in enumerate(bit_positions(s)):
bit = (raw >> (s.length - 1 - k)) & 1
data[b // 8] = (data[b // 8] & ~(1 << (b % 8))) | (bit << (b % 8))
return data
def phys(s, raw):
return raw * s.factor + s.offset
def active(msg, data):
"""Сигналы, которые есть в кадре: мультиплексор выбирает свою ветвь."""
mx = msg.mux
sel = decode_raw(mx, data) if mx else None
return [s for s in msg.signals if s.mux_id is None or s.mux_id == sel]
def decode(msg, data):
return {s.name: decode_raw(s, data) for s in active(msg, data)}
def used_mask(msg, data):
mask = bytearray(msg.dlc)
for s in active(msg, data):
for b in bit_positions(s):
mask[b // 8] |= 1 << (b % 8)
return bytes(mask)
# ── Генератор C ─────────────────────────────────────────────────────────
def segments(s):
"""Куски сигнала по байтам: (байт, сдвиг в байте, ширина, позиция в значении)."""
pos = bit_positions(s) # от старшего к младшему
L, segs, k = s.length, [], 0
while k < L:
byte = pos[k] // 8
j = k
while j + 1 < L and pos[j + 1] // 8 == byte and pos[j + 1] == pos[j] - 1:
j += 1
lo = pos[j] % 8 # младший разряд куска в байте
segs.append((byte, lo, j - k + 1, L - 1 - j))
k = j + 1
return segs
def ctype(s):
for n in (8, 16, 32):
if s.length <= n:
return ("int%d_t" if s.signed else "uint%d_t") % n
def utype(s):
return "u" + ctype(s).lstrip("u")
def cname(x):
return re.sub(r"[^0-9a-z_]", "_", x.lower())
def fmt(x):
r = repr(float(x))
return r if ("." in r or "e" in r) else r + ".0"
def gen_c(prefix, msgs, names, hname):
"""Возвращает (текст .h, текст .c) для сообщений names."""
H = ["/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */",
"#ifndef %s_H" % prefix.upper(), "#define %s_H" % prefix.upper(),
"#include <stdint.h>", "", "#ifndef DBC_REAL", "#define DBC_REAL double", "#endif", ""]
C = ["/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */",
'#include <string.h>', '#include "%s"' % hname, ""]
for nm in names:
m = next(x for x in msgs.values() if x.name == nm)
t = "%s_%s_t" % (prefix, cname(m.name))
f = "%s_%s" % (prefix, cname(m.name))
cyc = m.attrs.get("GenMsgCycleTime")
H.append("#define %s_ID 0x%Xu%s" % (f.upper(), m.id, " /* расширенный */" if m.extended else ""))
H.append("#define %s_DLC %du" % (f.upper(), m.dlc))
if cyc is not None:
H.append("#define %s_CYCLE_MS %su" % (f.upper(), cyc))
H.append("typedef struct {")
for s in m.signals:
H.append(" %s %s;%s" % (ctype(s), cname(s.name),
" /* %s%s */" % ("мультиплексор" if s.is_mux else "ветвь %d" % s.mux_id,
"") if (s.is_mux or s.mux_id is not None) else ""))
H.append("} %s;" % t)
H.append("void %s_unpack(%s *m, const uint8_t *d);" % (f, t))
H.append("void %s_pack(uint8_t *d, const %s *m);" % (f, t))
for s in m.signals:
H.append("static inline DBC_REAL %s_%s_phys(%s raw) { return (DBC_REAL)raw * (DBC_REAL)%s + (DBC_REAL)%s; }"
% (f, cname(s.name), ctype(s), fmt(s.factor), fmt(s.offset)))
H.append("")
mx = m.mux
plain = [x for x in m.signals if x.mux_id is None]
plain.sort(key=lambda x: not x.is_mux)
branches = sorted({x.mux_id for x in m.signals if x.mux_id is not None})
def body(emit, ind):
out = []
for x in plain:
out += emit(x, ind)
if branches:
out.append(ind + "switch (m->%s) {" % cname(mx.name))
for k in branches:
out.append(ind + "case %d:" % k)
for x in m.signals:
if x.mux_id == k:
out += emit(x, ind + " ")
out.append(ind + " break;")
out.append(ind + "default:")
out.append(ind + " break;")
out.append(ind + "}")
return out
def emit_unpack(s, ind):
parts = []
for byte, lo, w, p in segments(s):
e = "(d[%d] >> %d)" % (byte, lo) if lo else "d[%d]" % byte
if lo + w < 8:
e = "(%s & 0x%02Xu)" % (e, (1 << w) - 1)
parts.append("(uint32_t)%s << %d" % (e, p) if p else "(uint32_t)%s" % e)
out = [ind + "v = " + " | ".join(parts) + ";"]
if s.signed:
out.append(ind + "if (v & 0x%Xu) v |= 0x%Xu; /* знак: дополнение до двух */"
% (1 << (s.length - 1), (0xFFFFFFFF << s.length) & 0xFFFFFFFF))
out.append(ind + "m->%s = (%s)v;" % (cname(s.name), ctype(s)))
return out
def emit_pack(s, ind):
out = [ind + "v = (uint32_t)(%s)m->%s;" % (utype(s), cname(s.name))]
for byte, lo, w, p in segments(s):
e = "(v >> %d)" % p if p else "v"
if w < 8:
e = "(%s & 0x%02Xu)" % (e, (1 << w) - 1)
out.append(ind + "d[%d] |= (uint8_t)%s;" % (byte, "(%s << %d)" % (e, lo) if lo else e))
return out
C.append("void %s_unpack(%s *m, const uint8_t *d)" % (f, t))
C += ["{", " uint32_t v;"] + body(emit_unpack, " ") + ["}", ""]
C.append("void %s_pack(uint8_t *d, const %s *m)" % (f, t))
C += ["{", " uint32_t v;", " memset(d, 0, %d);" % m.dlc] + body(emit_pack, " ") + ["}", ""]
H.append("#endif")
return "\n".join(H) + "\n", "\n".join(C)
def gen_dump(prefix, msgs, names):
"""Только для проверки на ПК: распаковать, напечатать сигналы, упаковать обратно."""
C = []
for nm in names:
m = next(x for x in msgs.values() if x.name == nm)
f = "%s_%s" % (prefix, cname(m.name))
C.append("static void %s_dump(const uint8_t *d)" % f)
C.append("{")
C.append(" %s_t m; uint8_t o[8]; int i;" % f)
C.append(" memset(&m, 0, sizeof m);")
C.append(" %s_unpack(&m, d);" % f)
C.append(' printf("%s");' % m.name)
mx = m.mux
for s in m.signals:
cond = "1" if s.mux_id is None else "m.%s == %d" % (cname(mx.name), s.mux_id)
C.append(' if (%s) printf(" %s=%%ld:%%.10g", (long)m.%s, %s_%s_phys(m.%s));'
% (cond, s.name, cname(s.name), f, cname(s.name), cname(s.name)))
C.append(" %s_pack(o, &m);" % f)
C.append(' printf(" |");')
C.append(' for (i = 0; i < %d; i++) printf("%%02X", o[i]);' % m.dlc)
C.append(' printf("\\n");')
C.append("}")
C.append("")
return "\n".join(C) + "\n"
def py_dump(msg, data):
"""То же, что печатает gen_dump, но независимым путём decode_raw/encode_raw."""
vals = decode(msg, data)
out = msg.name
for s in msg.signals:
if s.name in vals:
out += " %s=%d:%s" % (s.name, vals[s.name], "%.10g" % phys(s, vals[s.name]))
o = bytearray(msg.dlc)
for s in active(msg, data):
encode_raw(s, vals[s.name], o)
return out + " |" + o.hex().upper()
dbc19e_mh.h — an example of a generated header.
/* Сгенерировано dbcmini.py (раздел 19е) — не править руками. */
#ifndef MH_H
#define MH_H
#include <stdint.h>
#ifndef DBC_REAL
#define DBC_REAL double
#endif
#define MH_EXAMPLEMESSAGE_ID 0x1F0u
#define MH_EXAMPLEMESSAGE_DLC 8u
#define MH_EXAMPLEMESSAGE_CYCLE_MS 0u
typedef struct {
int16_t temperature;
uint8_t averageradius;
uint8_t enable;
} mh_examplemessage_t;
void mh_examplemessage_unpack(mh_examplemessage_t *m, const uint8_t *d);
void mh_examplemessage_pack(uint8_t *d, const mh_examplemessage_t *m);
static inline DBC_REAL mh_examplemessage_temperature_phys(int16_t raw) { return (DBC_REAL)raw * (DBC_REAL)0.01 + (DBC_REAL)250.0; }
static inline DBC_REAL mh_examplemessage_averageradius_phys(uint8_t raw) { return (DBC_REAL)raw * (DBC_REAL)0.1 + (DBC_REAL)0.0; }
static inline DBC_REAL mh_examplemessage_enable_phys(uint8_t raw) { return (DBC_REAL)raw * (DBC_REAL)1.0 + (DBC_REAL)0.0; }
#endif
Next: section 19ж "USB-CAN adapter and SocketCAN: candleLight, can-utils and a car diagnostics test bench" — how a computer reads the same bus through an STM32-based adapter and can-utils.