Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level intermediate · 46 min read
Open in the interactive handbook

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.

Two rows of eight-bit cells. Top, Motorola: the Toyota signal STEER_ANGLE 3|12@0- occupies the low nibble of byte 0 (bits 3, 2, 1, 0) and all of byte 1 (bits 15 … 8), read left to right from b11 to b0, bytes 0F E2, raw 0xFE2 = −30, times 1.5 gives minus 45 degrees. Bottom, Intel: the VW signal ESP_v_Signal 32|16@1+ occupies bytes 4 and 5, the start is the least significant bit 32, the most significant byte is byte 5, bytes 45 05, raw 0x0545 = 1349, times 0.01 gives 13.49 km/h. At the bottom — the error: bytes 0F E2 read as Intel give −1438.5 degrees

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:

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:

  1. Record the bus. candump -l can0 writes a log with lines like (0000000004.540000) can0 0FD#AB0300009D060400 — time, interface, identifier, bytes (the format is from candump.c in can-utils). The adapter can even be your own sniffer on STM32 (section 198е).
  2. Split by identifier and see which bits change. cansniffer from can-utils highlights changing bytes live.
  3. 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.
  4. 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.
  5. 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:

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


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.

Open in the interactive handbook