06-DBC到can_ids.yaml工具链

📅 2026/7/22 15:10:25
06-DBC到can_ids.yaml工具链
06 · DBC → can_ids.yaml:车规契约怎么变成解码表上一篇走完了warn id → QML banner。这一篇往回退一层,看信号从哪来:candash.dbc(车规源)怎么经tools/dbc_to_yaml.py变成can_ids.yaml,再被DecodeTable吃掉。核心三件事:不要手写 can_ids.yaml、cantools 41 啃不动 29-bit 帧要程序化注入、validate_dbc_yaml双向校验护住 CI。为什么 can_ids.yaml 不能手写DBC 是整车厂 / 车规团队给的契约:CAN ID、周期、字节布局、缩放因子、单位全在里面。仪表这边要做的是消费,不是再抄一份。手写can_ids.yaml会发生什么:DBC 改了bat_volt的 scale(0.1 → 0.01),yaml 忘改 → 仪表上电压差 10 倍,报警阈值全错新人按engine.py的帧布局对照着写yaml,跟 DBC 注释对不上 → 调试时两边互相骂加一帧报文要在 yaml 里手填byte/bits/endian/formula——DBC 里已经有了,抄一遍必错所以仓库约定写死在文件头注释里:# 由 tools/dbc_to_yaml.py 从 candash.dbc 生成 (不要手写)src/can_frame.h也写了同一条:不要手写 can_ids.yaml。解码表只认生成物。整条链路一览config/candash.dbc ← 车规契约 (Vector CANdb 标准) │ ▼ python3 tools/dbc_to_yaml.py config/can_ids.yaml ← 项目内部格式 (byte/bits/endian/formula) │ ▼ DecodeTable::load() 启动时一次 内存里的 MessageSpec 表 │ ▼ 每帧 decode(frame, ctx) ctx[bat_volt] 425.0 ← LogicEngine 只看见物理量LogicEngine不关心字节布局——它只吃ctx里的double。字节解析是DecodeTable的活,布局来源是 DBC 工具链。换车型 换 DBC 重跑脚本,0 行 c。一次真实转换长什么样命令:python3 tools/dbc_to_yaml.py config/candash.dbc config/can_ids.yaml# → ✓ Generated 19 can_sources# → ✓ DBC ↔ YAML 一致 (19 messages verified)DBC 里 VCPU 长这样(config/candash.dbc):BO_ 515 VCPU_Frame: 8 Vector__XXX SG_ reserved_vcpu : 0|81 (1,0) [0|255] Vector__XXX SG_ brake : 8|81 (0.4,0) [0|100] % Vector__XXX SG_ vehicle_speed : 24|161 (0.1,0) [0|6553.5] km/h Vector__XXX BA_ GenMsgCycleTime BO_ 515 50;生成到can_ids.yaml:-name:VCPUcan_id:0x203# 515 0x203period_ms:50# 来自 GenMsgCycleTimefields:-name:brakebyte:1bits:8endian:littletype:uint8formula:x * 0.4unit:%-name:vehicle_speedbyte:[3,4]bits:16endian:littletype:uint16formula:x * 0.1unit:km/h注意reserved_vcpu消失了——message_to_source里显式跳过reserved_*:fields:[signal_to_field(sig)forsiginmsg.signalsifnotsig.name.startswith(reserved_)],占位比特不是业务信号,塞进 ctx 只会污染 LogicEngine 变量表。转换器干的三件事1. DBC bit position → byte 范围DBC 用的是LSB-first 比特编号(start|lengthendian±),项目内部用bytebits更直观。bits_to_byte_range:defbits_to_byte_range(start_bit:int,length:int,byte_order:str)-dict:ifbyte_orderlittle_endian:# Intel, 1first_bytestart_bit//8last_byte(start_bitlength-1)//8endianlittleelse:# Motorola 0first_bytestart_bit//8last_byte(start_bitlength-1)//8endianbigreturn{byte:[first_byte,last_byte]iffirst_byte!last_byteelsefirst_byte,bits:length,endian:endian,}vehicle_speedstart24、length16 → byte[3, 4]。单字节信号(如brakestart8)塌成标量byte: 1,yaml 更干净。2. scale/offset → formula 字符串DBC:physical raw * scale offset。can_ids.yaml:把这对数字还原成字符串,给 C 端DecodeTable::applyFormula吃:scaleoffsetformula1.00.0(省略)0.10.0x * 0.11.0-40x -400.1-1000x * 0.1 - 1000signal_to_field里这段就是在做还原——不引入 ExprTk,因为 DBC 物理公式不会比axb更复杂。C 端applyFormula也只认这几种形式(见can_frame_impl.h),复杂了再换引擎。3. 类型映射ifsig.is_float:field[type]floatelifsig.is_signed:field[type]fint{sig.length}else:field[type]fuint{sig.length}DecodeTable用type前缀判 signed(int8/int16→ 补码扩展),跟 cantools 行为对齐。29-bit 扩展帧:cantools 41 的硬坑项目里最关键的一帧是 BMS:0x186040F3,29-bit extended,周期 100ms。engine.py真发这帧,欠压/过压报警全靠它。但candash.dbc故意没有BO_行写这帧——只有一条注释:CM_ BMS_Frame 0x186040F3 (29-bit extended, 100ms) - BMS 数据: bat_volt ... CM_ 注: 原 can_ids.yaml 中 0x186040F3 是 29-bit 扩展 ID ... DBC 标准格式不直接支持原因写在_inject_extended_bms的 docstring 里:cantools 41 不支持从 DBC 文本解析 29-bit 帧(parser 在frame_id 11 bits时硬报错),但 can-dash 真实发送0x186040F3。这里以程序化构造替代,等上游支持时可以删掉这个 helper。转换入口:defdbc_to_yaml(dbc_path:str,yaml_path:str,pretty:boolFalse)-dict:db:Databasecantools.database.load_file(dbc_path)_inject_extended_bms(db)# ← load 之后、遍历之前cfg{can_sources:[message_to_source(msg)formsgindb.messages],}...注入本身是用 cantools 的Signal/MessageAPI 手搓一帧:bms_msgMessage(frame_id0x186040F3,nameBMS_Frame,length8,signals[bat_volt,bat_curr,bat_soc,battery_temp],cycle_time100,is_extended_frameTrue,)db._add_message(bms_msg)# _add_message 只更新字典, db.messages 基于 _messages list —— 必须再 appendifBMS_Framenotin[m.nameformindb._messages]:db._messages.append(bms_msg)字节布局跟engine.py/ 最终 yaml 对齐:字节信号公式单位0-1bat_voltx * 0.1V2-3bat_currx * 0.1 - 1000A4bat_soc(直通)%5battery_tempx -40degC这是工具链里唯一一处硬编码业务帧。它存在是因为依赖库的限制,不是因为平台想知道 BMS。上游 cantools 支持 29-bit DBC 文本那天,这段 helper 整段删掉,改回标准BO_行。生成结果就是 yaml 末尾那条:-name:BMScan_id:0x186040F3period_ms:100fields:-name:bat_voltbyte:[0,1]bits:16endian:littletype:uint16formula:x * 0.1unit:V# ... bat_curr / bat_soc / battery_tempDecodeTable 怎么吃这份 yaml启动时(main.cpp/main_ui.cpp):if(!loadDecodeTable(config/can_ids.yaml)){// FATAL}DecodeTable::load把每个can_source塞进m_messages[can_id]。之后每帧:boolDecodeTable::decode(constCanFramef,std::unordered_mapstd::string,doublectx)const{autoitm_messages.find(f.can_id);if(itm_messages.end())returnfalse;for(constautofs:it-second.fields){uint64_traw_uextractRaw(f.data,fs);doubleraw_dfs.is_signed?static_castdouble(toSigned(raw_u,fs.bits)):static_castdouble(raw_u);ctx[fs.name]fs.formula.empty()?raw_d:applyFormula(fs.formula,raw_d);}returntrue;}端到端验一下过压场景(跟tests/test_decode_table.cpp一致):帧 0x186040F3, data [0x9A, 0x10, ...] bat_volt raw 0x109A 4250 formula x * 0.1 → 425.0 V → ctx[bat_volt] 425.0 → logic.yaml: bat_volt 420 → setwarnon(bat_overvolt)DBC 改 scale、重跑脚本、重启进程——c 一行没动,阈值判断仍然读bat_volt这个字符串。validate_dbc_yaml:CI 里的双向护栏转换完自动跑一次;也可以单独:python3 tools/dbc_to_yaml.py--validateconfig/candash.dbc config/can_ids.yaml检查项(tools/dbc_to_yaml.py:191):yaml 每个can_source.can_id在 DBC(注入)里能找到对应 messageyaml 每个field.name在对应 message 的 signal 集合里反向:DBC 有、yaml 没有的 signal → 打INFO(可能是可选信号,不 abort)warnings目前不 abort CI(返回 0),但会打出来——防止有人手改 yaml 漂了还浑然不知。单测tests/test_dbc_to_yaml.py覆盖:DBC 能 load、关键信号属性对转换产物含can_sources,formula 含0.1生成 ID 集合 ⊇ DBC(BMS) 全部 frame_idvalidate_dbc_yaml返回 0cantools 端到端解码vehicle_speed88.5工程上这条线为什么值钱回头看系列开篇那张表:谁改什么重编仪表?车规团队candash.dbc否(跑脚本即可)平台 cDecodeTable/ 工具链是(极少)标定/业务logic.yaml/warn.yaml否DBC 工具链把字节布局变更从 c diff 里剔出去了。没有它,换一版 DBC 改decodeFrameToCtx里一堆data[2] 8,跟业务if (volt 420)搅在一起——正是开篇那段反例。29-bit 注入看起来像脏补丁,但它把脏限制在一个 Python helper里,没有泄漏进 LogicEngine,也没有泄漏进 QML。这就是平台化该有的边界:脏事关在工具链,运行时只吃干净的 yaml。一句话总结DBC 是唯一真相源;dbc_to_yaml.py负责展开 补上 cantools 啃不动的 29-bit BMS;DecodeTable只认生成物;validate_dbc_yaml防止 yaml 手漂。手写can_ids.yaml 跟车规契约分叉。分叉一旦发生,报警阈值、量纲、周期全会静默错。接下来07 · Qt 仪表实机 vs PC sim:同一套 QML 在桌面 sim 和实机上的差异,以及QmlLoggerBridge怎么把 C 日志桥到 QML LogPanel