XCP 标定执行桥
一句话定位
前两个标定 Skill 回答「该不该标、值对不对」,本 Skill 回答「怎么把值安全写进去,并证明写进去了」。它是执行层,不是评审层。
| Skill | 回答的问题 | 是否碰 ECU 写入 |
|---|---|---|
vendor-ai-calibration-review |
厂商的 AI 标定能力是否真实、是否适用 | 否 |
two-wheeler-calibration-review |
标定文件的算术、量纲、联动是否正确 | 否(只静态审) |
| 本 Skill | 如何安全写入、如何证明生效 | 是 |
三者串起来才闭环:先评审(能不能标)→ 再执行(怎么标)→ 执行结果回灌评审(标得对不对)。
四段因果链
1 symbol A2L(MCD-2 MC) → 参数在哪、什么类型、怎么换算、边界多大
2 codec XCP(MCD-1) → 按传输层容量分块(经典 CAN 单响应仅 7 字节数据)
3 plan 白名单+边界+页 → 生成可审计划(不连 ECU 就能先看)
4 run 手动 / 自动 → 写前回读 → 写 → 写后回读 → 判定
命令
# 0 环境:需要搭建的环境 / 要装的 wheel / 要起的服务,全清单见 references/environment-setup.md
# (Windows + Python 3.13 + venv;离线仓库 D:\GCC编译环境;REST 服务 scripts/xcp_api.py)
# 原实测环境(Linux 沙箱)写法:PYTHONPATH=/home/gem/.aily/.cli/python python3 scripts/xcp_cal.py <子命令>
python scripts/xcp_cal.py <子命令> # venv 激活后直接跑
codec # 打印各传输层容量与帧样例
symbol <file.a2l> [-o json] # A2L → 符号表
elf-align <fw.elf> <fw.a2l> [--symbols X Y] # ELF/DWARF ↔ A2L 地址对齐门禁
plan <symbols.json> <req.json> # 生成并审核计划(不连 ECU)
run <plan.json> --transport sim|can|pyxcp|eth [--auto --evaluator x.py]
trial <symbols.json> --symbol X --value V # 单值试写:验链路,写完自动还原
daq <symbols.json> --measure X [--measure Y] [--plan-only] # DAQ 采集曲线
from-review <评审产物> <symbols.json> --spec s.json --symbol-map m.json # 闭环接入
a2l-crosscheck <file.a2l> [--symbols s.json] # 用 pyA2L 独立读一遍,逐符号比对
selftest # 虚拟总线端到端自测(145 项)
run / trial / daq 三个命令都支持 XCP over Ethernet:
--transport eth --eth-host 127.0.0.1 --eth-port 5555 --eth-protocol UDP|TCP
端口默认 5555 是 pyXCP 的(pyxcp/transport/eth.py DEFAULT_XCP_PORT = 5555)。别抄 OpenXilEnv 的 1802——两边是两套默认值,配错的表现是「连接建不起来」且不报端口错。
退出码三分,不得混用:0 通过可上车 / 1 硬失败不可上车 / 2 无硬失败但有未验证项,仍属未通过。
REST 服务层(v1.4.0 新增,FastAPI 架构)
scripts/xcp_api.py:把上面 10 个 CLI 子命令镜像成 HTTP 端点,handler 直接函数级复用 xcp_cal.py 的门禁实现(不走 subprocess,不另立规矩)。
python scripts/xcp_api.py --engine auto --host 127.0.0.1 --port 8000 # 起服务(fastapi 优先,缺 wheel 退 stdlib 兜底引擎)
python scripts/xcp_api.py --routes # 打印路由表(不起服务)
python scripts/xcp_api.py --check # 依赖矩阵自检(如实报 MISS,不美化)
python scripts/xcp_api.py --smoke # handler 级离线自测(不监听端口、不碰 socket)
| 端点 | 作用 |
|---|---|
GET / /health /codec |
服务信息 / 依赖矩阵 / 容量与帧样例 |
POST /symbol /plan /elf-align /a2l-crosscheck /from-review |
对应同名 CLI 子命令 |
POST /trial /run /daq /selftest |
对应同名 CLI 子命令(daq 支持 plan_only 离线排版) |
REST 层三条自己的红线(XCB-019/020 + 写锁):
- 状态码映射固定:exit
0→200 / 1→409 / 2→422,响应头恒带X-XCP-Exit;400请求格式错、404文件/路由不存在、409写锁忙、501缺依赖。 - 写入闸门 = CLI 非交互闸门:
/run/trial默认不写入(落 exit 2),body 显式"no_confirm": true才放行——不得为「方便联调」默认放行。 - 全局写锁:同一时刻只允许一个写 ECU 任务,并发第二个落
409 WRITE_BUSY。
需要搭建的环境、需要补装的缺失 wheel(fastapi/uvicorn/pydantic/construct/greenlet 等,清单随补装动态更新,当前剩 17 个)、需要启动的服务与端口(REST 8000 / pyXCP eth 5555 / xcpray 5591 / OpenXilEnv 1802)、REST 请求字段与 curl 样例——全清单见 references/environment-setup.md,缺什么、哪些实测过、哪些未验证,按 [A]/[B]/[C] 三档如实列明。
手动标定
xcp_cal.py run plan.json --transport can --interface vector --channel 0 --cmd-id 0x7E0 --res-id 0x7E1
逐项执行,每项写入前在交互终端询问 [y/N]。非交互环境下未显式 --no-confirm 一律不确认、不写入——默认拒绝,不默认放行。
自动 AI 标定
xcp_cal.py run plan.json --transport can --auto --evaluator my_eval.py --max-iter 5
评估器只需实现 evaluate(current, iteration) -> {"done":bool,"status":str,"detail":str,"suggest":{symbol:value}}。--auto 缺少 --evaluator 直接退出 1——没有评估器就没有闭环决策,不能靠一个开关冒充自动标定。
闭环每轮:读当前值 → 评估器给建议 → revise_items() 重过白名单/边界/溢出校验 → 写 → 回读 → 判定。
安全门禁(写死,绕过就是错)
- 无白名单即拒写:
whitelist为空且未显式allow_all→ FAIL。 - 非标量一律拒单值写:CURVE/MAP/CUBOID/VAL_BLK/ASCII 即便没声明长度也拒写。写首元素、回读首元素、判定通过——这是最容易蒙过验证的错法(XCB-014)。
- AI 建议不豁免:模型给的值和人工填的值走同一套校验,越界/溢出/非计划内符号全部 FAIL,闭环中断。
- 写后必须回读:不一致判硬失败,不重试掩盖——重试会让「写不进去」看起来像「偶尔抖动」。
- 未声明标定页只能判未验证:不声明 RAM/Flash 页就断言「已生效」是错的。
- A2L 不含标定数值:A2L 只有地址、类型、换算、边界;数值在 ECU 内存/hex 里。
- MEASUREMENT 的地址在
ECU_ADDRESS行,不在记录头:实测真实库 253/253 都如此。缺这步会把所有测量量指到地址 0,DAQ 采回一条平的零线且不报错。 - 库声明了什么就按什么发帧:A2L 的
OPTIONAL_CMD与SEGMENT/PAGE是契约不是建议。未声明 DOWNLOAD → 一条都不发;未声明 UPLOAD/SHORT_UPLOAD → 写得了但证不了,同样拒写;库把所有页都声明成XCP_WRITE_ACCESS_NOT_ALLOWED→ 拒写。页权限只对措辞无歧义的NOT_ALLOWED做硬判定,其余档位照录原文、不替 ASAM 推断语义。
协议事实(实测,非记忆)
| 项 | 值 | 来源 |
|---|---|---|
| 正响应首字节 | 0xFF |
虚拟总线实测 |
| 错误响应首字节 | 0xFE |
同上 |
| SET_MTA 请求 | F6 00 00 ext addr(4B) 定长 8 字节 |
实测 + 字节数断言 |
| UPLOAD 响应 | FF + data,单次数据 ≤ 7 字节 |
实测,8 字节溢出 |
| DOWNLOAD 请求 | F0 len + data,单次数据 ≤ 6 字节 |
实测 |
| 命令码全集 | 84 条 | pyXCP 0.29.14 Command 枚举 |
⚠ 保留位必须正好 2 个零字节。多一个或少一个会让地址在帧里整体错位一格,现象是「写返回成功但读回不变」——最难查的一类错。本 Skill 曾踩过(自测当场抓出)。
ELF/DWARF ↔ A2L 地址对齐(v1.3.0 新增)
xcp_cal.py elf-align fw.elf fw.a2l # 核 A2L 全部符号
xcp_cal.py elf-align fw.elf fw.a2l --symbols CAL_A CAL_M # 只核这几个
回答一个问题:A2L 里写的地址,到底是不是这份 ELF 里那个变量的地址。
| 规则 | 条件 | 判级 |
|---|---|---|
EWD-001 |
ELF 里找不到 A2L 声明的同名对象 | FAIL |
EWD-002 |
地址不一致(报精确字节差) | FAIL |
EWD-003 |
总宽度不一致(A2L size×维度 vs ELF st_size) |
FAIL |
EWD-004 |
ELF 无 DWARF(没带 -g) |
WARN → exit 2 |
EWD-005 |
A2L 地址为 0 / 缺失 | FAIL |
EWD-006 |
维度不一致 | FAIL |
EWD-007 |
ELF 侧无尺寸 | WARN |
EWD-010 |
全部一致 | PASS |
判级原则:能算出来的不一致就是 FAIL,算不出来只能 WARN。没有 DWARF 时「类型和维度核不了」是「未验证」,不能默认通过。
三个必须知道的坑(全部实测):
DW_FORM_exprloc的值在 pyelftools 里是list[int]不是bytes,首字节0x03 = DW_OP_addr。按 bytes 处理得到None且不报错——静默失败。has_dwarf_info()默认宽松,.eh_frame也算 DWARF。不带-g编出来的二进制一定有.eh_frame。必须strict=True且has_section(".debug_info")。- 重排符号地址的是优化级别,不是
-g。实测同一fw.c:-g -O0与-O0(无-g)地址完全相同(0x2008/0x200a/0x2010),-O1才整体位移(0x2016/0x2014/0x2008)。拿「-g版本的 A2L」去核「-O1版本二进制」必然EWD-002——这是真实场景(固件重编但 A2L 没重生成)。别把责任推给-g,否则会误以为「只要都带-g地址就稳」。
> A2L 的 size 是单元素宽度,ELF 的 st_size 是总字节。数组上两者必然不等——所以 EWD-003 必须先按维度乘出来再比,直接比会把所有数组误报成宽度错。
OpenXilEnv 地址对齐链路(本机未装宿主,属核实档)
C 侧真实入口(官方手册 §10.11.1 原文):
int XilEnv_SetupLinkToExternProcess(const char *A2LFileName,
const char *ProcessName, int UpdateFlag);
UpdateFlag 关键位:A2L_LINK_UPDATE_FLAG 0x1(要地址刷新就靠这一位)、A2L_LINK_UPDATE_ZERO_FLAG 0x4、A2L_LINK_ADDRESS_TRANSLATION_DLL_FLAG 0x8、A2L_LINK_REMEMBER_REFERENCED_LABELS_FLAG 0x20。返回 0 = 成功。
取刷新后地址:XilEnv_GetLinkToExternProcess → XilEnv_GetIndexFromLink → XilEnv_GetDataFromLink → XilEnv_GetLinkSingleValueAddress。
⚠ Python 侧有真实缺口,务必照实说明:openxilenv 包(167 个公开方法)的 A2lLink 章节只公开 2 个方法(GetA2lLinkNo、FetchA2lData)。SetupLink / GetLinkSingleValueAddress / GetLinkArrayValueAddress / ExportA2lFile 均无公开包装,只在私有 _XilEnv__lib 上有 ctypes 绑定。能公开做到「带 A2L 起进程」的是 StartProcessEx2(..., A2LFile, A2LFlags=0x1, ...)。
六项技术分工与验证档位
- XCPlite(Vector 开源 Slave,XCP v1.4,Ethernet UDP):做从站/模拟器参考与 A2L 生成,不当 CAN 主站。不支持 Seed&Key、Flash、Motorola 大端、DAQ 预分频。A2L 优先用它配套的
xcpclient生成——相对寻址模型自洽,换一套工具生成常对不上且错得隐蔽。 - OpenXilEnv(Eclipse 开源,Apache-2.0):DWARF 解析 + A2L 解析/导出 + XCP over Ethernet + 多核/在环。本 Skill 只把它的接口契约写进
references/api-inventory.md;本机没有宿主二进制,其真调用均为「源码/手册核实」档,不是「已跑通」。 - pyXCP(
0.29.14,Master):真实 ECU 主站执行引擎,支持 CAN/Ethernet/USB/Serial,108 个公开方法。pya2ldb未装,故 A2L 解析由本 Skill 自带。 - x1xcp / xcpray:MCU/ESP 嵌入式端(C++ 库),Python 自动化侧用
xcpray(JSON-RPC over TCP 5591)。用 Arduino/ESP 做在环台架时走这条。 - python-can(
4.6.1):总线抽象、过滤、错误帧;pyXCP 的 CAN 后端。VirtualBus用于免硬件验证。 - Scapy(
2.6.1):帧级构造/解码/故障注入。不做高层标定引擎。
pyXCP 两个必踩的坑
- 时区:C++ 扩展用
tzname缩写喂pytz,遇CST等非 IANA 名抛错。已在_safe_datetime_stub()里替换为 UTC 实现,用户不用管。 - 连接生命周期:
Master.__init__不会自动连传输层,手工使用必须先m.transport.connect()再m.connect()。PyXcpMasterAdapter已封装。
A2L 与协议解析的已知边界
- 只实现经典 CAN / CAN FD 容量模型;Ethernet 按 255 字节 CTO 上限。
- 不实现 Seed & Key 解锁(本轮明确不做)——需要解锁的 ECU 先由外部流程解锁,本 Skill 不碰安全访问。
- DAQ 已实现(动态分配 / ODT 布局 / 同步启停 / DTO 解码),但仅限原始帧路径;pyXCP 适配层取不到 DTO,抛
NotImplementedError并落 exit 2。 - DAQ 只做单事件通道、PID = ODT 序号的简单情形;多列表、预分频、时间戳字段未解。
- A2L 解析覆盖
VALUE/VAL_BLK/CURVE/MAP/CUBOID/ASCII与MEASUREMENT、COMPU_METHOD线性换算;复杂COMPU_VTAB/ 多项式换算未覆盖。 - 维度有两个来源,缺一不可:RECORD_LAYOUT 的
NO_AXIS_PTS_X/Y,以及记录块内自带的MATRIX_DIM与AXIS_DESCR的maxAxisPoints(ASAP2Demo 一个 RECORD_LAYOUT 都没有,19 个非标量全靠后者)。两套都取不到长度时按记录类型 fail-closed 判非标量:长度未知是「不知道多长」,不是「长度 1」。 - pyA2L 是可选第二读取器:装了(
pya2ldb,import 名pya2l)就逐符号比对名字/种类/地址/字节宽度;没装则C00 WARN+ exit 2,不当作通过。注意 PyPI 上pya2l与pya2ldb是两个不同项目,本技能用的是后者。 - A2L 类型以 RECORD_LAYOUT 为准,不按名字猜。CHARACTERISTIC 第 4 个词元是记录布局名:ETAS/Vector 生成的库里它是
STANDARD_VALUE_S32这种与类型毫无关系的名字,FNC_VALUES 1 <dtype>才是唯一权威类型源。按名字后缀认类型只在「厂商把布局名起得跟类型名一样」时成立——上一版正是被这一点骗过,换到 TC1797 库直接丢掉全部 21 个可标定参数且不报错。 - 没有
-g就没有类型与维度。elf-align此时只核地址,落EWD-004 WARN+ exit 2(未通过),不给你「通过」。要判类型/维度必须带-g重编,没有例外。 - 三个完整文档在
references/下:api-inventory.md(全部外部 API 的签名/调用方式/前置条件/验证三档)、chip-to-solution-7steps.md(从芯片到方案的七段流程、路线 A/B、验收关卡)、environment-setup.md(环境搭建 / wheel 清单 / 服务与端口 / REST 接口全表 / 本机验证记录)。要回答「这套到底能不能用」先看 api-inventory,要回答「一步步怎么做」看 7steps,要在新机器上把环境搭起来看 environment-setup。 - STATIC / DYNAMIC 由库自己声明。STATIC 库的列表与 ODT 由固件预定义,
OPTIONAL_CMD里没有 FREE_DAQ/ALLOC_*;硬发这几条会得 ERR_CMD_UNKNOWN,整段 DAQ 配置全盘失败。本 Skill 按DAQ_CONFIG_TYPE分流,STATIC 只发 CLEAR_DAQ_LIST + 写入口。 - PID 段首地址须减 FIRST_PID。STATIC 库按列表分段(0/10/20),不减会把 DAQ1 的第 0 个 ODT 取成 DAQ0 的第 10 个,采回错位的量、曲线形状却完全正常。
单值试写(验链路,不是标定)
xcp_cal.py trial mabs_symbols.json --symbol AWhl_Osci_Present_DP --value 1000 --no-confirm
run_plan 证明「这个值写对了」;trial 证明「这条链路通」。三关缺一不可:
写入 → 回读必须等于目标 → 还原原值并回读确认。
- 不还原就是给车上留了个没人认领的改动,所以 T06 是硬要求,不是可选项。
- 回读不符立即中止且不还原(T05 FAIL):此时 ECU 状态已未知,再写一次只会把「写不进去」叠成「状态不明」。
- 只接受 1 个参数(T01):多项就是批量刷写,不属于链路验证。
- 安全符号默认拒试(T02):名字命中
abs/asr/brake/torque/slip/decel/lock/esc/...一律拒绝。链路验证不该拿安全参数当试验品;确需验证用--allow-safety显式放行。 - 非交互终端默认不确认、不写入(与手动模式同一条规则)。
实测(真实 MABS 库 + 虚拟从站):T00 PASS / T04 PASS / T05 PASS / T06 PASS,写后内存回到原值 0。
DAQ 采集(要曲线才用)
xcp_cal.py daq mabs_symbols.json --measure ABSOffSwitchState --measure ABSOffSwitchVoltage
xcp_cal.py daq mabs_symbols.json --measure X --plan-only # 只排 ODT,不连 ECU
ODT 装填是算术不是感觉:单条 DTO 数据量 = 帧长 − 1 字节 PID(经典 CAN 8→7、CAN FD 64→63)。
装不下就顺延下一个 ODT,所以「挂了几个信号」不重要,「每个 ODT 装几个字节」才决定能不能采到。
实测 5 个 2 字节量 → 2 个 ODT(6 字节 + 2 字节);真实库 ABSOffSwitchState 采 49 帧/49 点,曲线随从站递增。
⚠ pyXCP 适配层的 daq_acquire 抛 NotImplementedError,这是有意为之:pyXCP 在 transport 内部
就把 DAQ 帧消化掉了,适配层拿不到原始 DTO。此时 CLI 落 exit 2(未验证),绝不编造样本。
要走 DAQ 请用 --transport can(原始帧路径)或 pyXCP 自带的 DaqRecorder。
闭环接入(算→标→验)
xcp_cal.py from-review /tmp/rv.txt mabs_symbols.json --spec spec.json --symbol-map map.json -o plan.json
xcp_cal.py run plan.json --auto --evaluator my_eval.py # 承接上一步生成的计划
from-review 读 two-wheeler-calibration-review 的结论,只有 exit 0 才生成计划:
| 评审档位 | 含义 | 本层动作 |
|---|---|---|
| 0 | 通过 | 允许生成计划,进入自动链路 |
| 1 | 硬失败 | G01 FAIL,一条计划都不生成(写进去等于把错值固化进 ECU) |
| 2 | 未验证 | 落 exit 2,证据不足,自动流程没资格替人补判断 |
评审产物两种形态都收:JSON(必须含 exit 字段,缺了就报错不猜)、或评审脚本的终端输出(标 exit_source=text)。
档位按规则状态判,不按中文措辞判——实测三种结论行全都含「硬失败」字样(exit 2 的原文是
「0 条硬失败,但有 1 项未能验证」),靠字面匹配必然错档。
另外三条硬约束:缺 symbol_map → G02 FAIL(不许猜字段对应哪个符号);基线项映射不到 → G03 WARN 逐条点名;
同一符号被两个基线指向不同值 → G05 FAIL(不拦会让后写覆盖前写,且全程无告警)。
实测:真实评审输出(exit 1 / C02+C03+C10 FAIL)→ G01 FAIL、计划 0 条、exit 1。
退役条件
> 蓝皮书第5章:67K 条 skill 里 80% 会在 2 年内消失。
触发以下任一条件即应启动退役评估:
- C1 上游内置化:pyXCP 或 python-can 官方直接提供 A2L 定位 + 门禁 + 回读校验的完整标定执行链,本技能的编排层不再有独立价值
- C2 协议退场:XCP 被 ASAM MCD-1 后继协议(如 SOVD / DDS 标定通道)全面替代,目标 ECU 不再提供 XCP 服务
- C3 能力被覆盖:
two-wheeler-calibration-review或其它标定 skill 已具备真实 ECU 写入与回读闭环能力 - C4 维护人变更:迪宝不再负责标定相关领域
- C5 质量问题:连续 3 个 MISTAKES 条目标记为设计错误(尤其出现「门禁被绕过仍判通过」类事故)
退役流程: 先在 SKILLS_INDEX.md 标记 🟡 冻结(停止新功能,只修 bug)→ 观察 90 天无触发 → 把四段链的判据沉淀到 _meta/MISTAKES.md → 删除本 skill 目录并在 CHANGELOG.md 留一行归档记录。
> 作者背书 · 迪宝
>
> - 所属:研究院
> - 领域:ABS/ASR 控制器标定 / XCP 协议执行 / CAN 总线
> - 权威度:★★★★(协议编解码与门禁链已实测 37/37;真实 ECU 侧仅跑过虚拟总线,未上车验证)
> - 边界:本技能负责写进去并证明写进去,不负责「这组值该不该这么标」——那是 two-wheeler-calibration-review 的事