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定义的类:
整个dbc文件会被解析为一个Database类型的对象,每个BU_对应一个Node,每个BO_对应一个Message类型的对象,每个SG_对应Signal,特别的,对于dbc文件中每个信号的scale、offset和VAL_,会被解析成一个Conversion类型的对象。
对于BA_DEF_、BA_DEF_DEF_以及BA_标签的行,BA_DEF和BA_DEF_DEF会被转化为AttributeDefinition,其中kind对应BU_、BO_、SG_或者整个database,typename对应声明中的类型,如果类型为ENUM,那么choices中给出所有枚举值,如果类型为INT或HEX,则minimum和maximum指定最大值和最小值。 每个BA_标签的行都会转化为一个Attribute的实例
对于每个Database、Node、Message、Signal实例,都有一个dbc_specifics成员,是Dbc_specifics实例,指定与其关联的AttributeDefinition,Attribute和value tables
cantools生成c代码
Message/Signal到CodeGenMessage/CodeGenSignal的转换
cantools生成c代码首先会将Message和Signal转化为CodeGenMessage和CodeGenSignal:
在CodeGenMessage中,使用snake_name作为生成的代码中的message的名称。在CodeGenSignal中,使用snake_name作为生成的代码中signal的名称,生成type_name和type_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_float和is_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类型长度进行对齐
CodeGenSignal的segments方法对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上。
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); } ``` | |
| 函数定义 | 消息打包 | |
| 消息解包 | ||
| 信号值解码 | ||
| 信号值编码 | ||
| 信号值非法判断 | ||
| 消息结构体初始化 |