cantools工具详解

2025-08-22

cantools使用

cantools提供的feature

dbc文件解析

cantools 支持将 dbc文件解析为cantools定义的类,执行下述代码:

import cantools
db = cantools.database.load_file('test.dbc')
db

,会输出如下结果:

version('')

node('<node_name>', None)
...

message('<message_name>', <frame_id>, <is_extended>, <dlc>, <comments>)
  signal('<signal_name>', <start_bit>, <length>, <byte_order>, <is_signed>, <raw_initial>, <scale>, <offset>, <minimum>, <maximum>, '<unit>', <is_multiplexer>, <multiplexer_ids>, <choices>, <spn>, <comments>)
    ...
...

执行下述代码:

db.messages

,可以查看所有dbc文件中定义的messages

[message('<message_name>', <frame_id>, <is_extended>, <dlc>, <comments>), ...]

执行下述代码:

db.message[].signals

,可以查看一个message下所有的signal

signal1('<signal_name>', <start_bit>, <length>, <byte_order>, <is_signed>, <raw_initial>, <scale>, <offset>, <minimum>, <maximum>, '<unit>', <is_multiplexer>, <multiplexer_ids>, <choices>, <spn>, <comments>)
signal2('<signal_name>', <start_bit>, <length>, <byte_order>, <is_signed>, <raw_initial>, <scale>, <offset>, <minimum>, <maximum>, '<unit>', <is_multiplexer>, <multiplexer_ids>, <choices>, <spn>, <comments>)
...

c文件生成

执行如下shell脚本:

python3 -m cantools test.dbc

,可以根据dbc文件生成.h.c文件,提供从CAN Matrix到message结构体的解包和message结构体到CAN Matrix的打包,以及signal的value与原始值之间的转换,下表是脚本提供的一些参数:

参数 作用
--database-name 指定生成的文件中database的名称
--no-floating-point-numbers 在生成的代码中不使用浮点数
--bit-fields 使用bit fields以最小化结构体大小
-e--encoding 指定dbc文件的编码
--prune 缩减输出的value table define中名称的长度
--no-strict 跳过dbc文件一致性检查
-o--output-directory 指定文件的输出位置
--use-float 使用float代替double
--node 指定c代码的运行的node name,用于去除无用的pack/unpack函数

cantools源码分析

dbc文件解析

dbc会通过textparser库对dbc文件进行语法分析并转化为cantools定义的类:

def map

整个dbc文件会被解析为一个Database类型的对象,每个BU_对应一个Node,每个BO_对应一个Message类型的对象,每个SG_对应Signal,特别的,对于dbc文件中每个信号的scale、offset和VAL_,会被解析成一个Conversion类型的对象。

对于BA_DEF_BA_DEF_DEF_以及BA_标签的行,BA_DEFBA_DEF_DEF会被转化为AttributeDefinition,其中kind对应BU_BO_SG_或者整个database,typename对应声明中的类型,如果类型为ENUM,那么choices中给出所有枚举值,如果类型为INTHEX,则minimummaximum指定最大值和最小值。 每个BA_标签的行都会转化为一个Attribute的实例

def map

对于每个DatabaseNodeMessageSignal实例,都有一个dbc_specifics成员,是Dbc_specifics实例,指定与其关联的AttributeDefinitionAttribute和value tables

cantools生成c代码

def c map

Message/Signal到CodeGenMessage/CodeGenSignal的转换

cantools生成c代码首先会将MessageSignal转化为CodeGenMessageCodeGenSignal

CodeGenMessage中,使用snake_name作为生成的代码中的message的名称。在CodeGenSignal中,使用snake_name作为生成的代码中signal的名称,生成type_nametype_length的代码如下:

@property
def type_name(self) -> str:
    if self.signal.conversion.is_float:
        if self.signal.length == 32:
            type_name = 'float'
        else:
            type_name = 'double'
    else:
        type_name = f'int{self.type_length}_t'

        if not self.signal.is_signed:
            type_name = 'u' + type_name

    return type_name

,根据signal的is_floatis_signed以及length确定signal的c数据类型;

@property
def type_length(self) -> int:
    if self.signal.length <= 8:
        return 8
    elif self.signal.length <= 16:
        return 16
    elif self.signal.length <= 32:
        return 32
    else:
        return 64

,根据signal的占用的bit数与c类型长度进行对齐

CodeGenSignalsegments方法对signal进行拆分,当signal存在跨字节时,segments会计算signal在每个字节内的shift, shift direction和mask:

def segments(self, invert_shift: bool) -> Iterator[tuple[int, int, str, int]]:
    index, pos = divmod(self.signal.start, 8)
    left = self.signal.length

    while left > 0:
        if self.signal.byte_order == 'big_endian':
            if left >= (pos + 1):
                length = (pos + 1)
                pos = 7 #始终指向当前最高位需要填充的位置
                shift = -(left - length)
                mask = ((1 << length) - 1)
            else:
                length = left
                shift = (pos - length + 1)
                mask = ((1 << length) - 1)
                mask <<= (pos - length + 1)
        else:
            shift = (left - self.signal.length) + pos

            if left >= (8 - pos):
                length = (8 - pos)
                mask = ((1 << length) - 1)
                mask <<= pos
                pos = 0 #始终指向当前最低位需要填充的位置
            else:
                length = left
                mask = ((1 << length) - 1)
                mask <<= pos

        ...

        yield index, shift, shift_direction, mask

        left -= length
        index += 1
  • 对于每一个字节,mask取决于需要在改行填充的字节,那么,对于大端序,要么从0到start bit%8填充,要么整个字节填充,对于小端序,要么从start bit%8到7填充,要么整个字节填充。
  • 对于每个字节,shift的长度与方向如下:除了最低位填充的字节内的位,其他字节中的位都是 > 0的,显然都要右移到字节中的最低位与0对齐;对于最低位所在字节内的位,由于其开始填充的位$\geq 0$,于是需要左移与最低位的开始位对齐。

于是,对于大端序,从最高位所在字节开始填充,当剩余位数大于等于可以填充的位数时(不是最低位所在字节,或者最低位刚好在0),则右移直到当前字节内的最低位与0对齐,否则左移直到最低位与相应位对齐;对于小端序,从最低位所在字节开始填充,左移的位数始终时当前剩余的位数-总位数+pos,即始终将当前最低位移动到pos上。

big edian

small edian

CodeGenMessage/ CodeGenSignal -> c代码

cantools生成的对象包括<database_name>.h<database_name.c>

<database_name>.h文件中,cantools主要生成了如下几个部分的代码:

宏部分消息id ```c #define TEST_MESSAGE_1_FRAME_ID (0x01u) ```
消息长度 ```c #define TEST_MESSAGE_1_LENGTH (8u) ```
是否是扩展类型 ```c #define TEST_MESSAGE_1_IS_EXTENDED (0) ```
cycle类型的消息的cycletimes ```c #define TEST_MESSAGE_1_CYCLE_TIME_MS (20u) ```
VAL_(cantools内的choice) ```c #define TEST_MESSAGE_1_SIGNAL_1_DESCRITPTION_1_CHOICE (0u) ```
消息名 ```c #define TEST_MESSAGE_1_NAME "Message_1" ```
信号名 ```c #define TEST_MESSAGE_1_SIGNAL_1_NAME "Signal_1" ```
结构体定义 ```c struct test_message_1_t { uint16_t signal_1; }; ```
函数声明消息打包 ```c int test_message_1_pack( uint8_t *dst_p, const struct test_message_1_t *src_p, size_t size); ```
消息解包 ```c int test_message_1_pack( struct test_message_1_t *dst_p, const uint8_t *src_p, size_t size); ```
信号值解码 ```c double test_message_1_signal_1_decode(uint16_t value); ```
信号值编码 ```c uint16_t test_message_1_signal_1_encode(double value); ```
信号值非法判断 ```c bool test_message_1_signal_1_is_in_range(uint16_t value); ```
消息结构体初始化 ```c int test_message_1_init(struct test_message_1_t *msg_p); ```

<database_name>.c文件中,cantools主要生成了如下几个部分的代码:

辅助函数内联辅助左移函数 ```c static inline uint8_t pack_left_shift_u16( uint16_t value, uint8_t shift, uint8_t mask) { return (uint8_t)((uint8_t)(value << shift) & mask); } ```
内联辅助右移函数 ```c static inline uint8_t pack_right_shift_u16( uint16_t value, uint8_t shift, uint8_t mask) { return (uint8_t)((uint8_t)(value >> shift) & mask); } ```
函数定义消息打包
消息解包
信号值解码
信号值编码
信号值非法判断
消息结构体初始化