简介:这是一套基于Sicar标准程序框架的培训演示程序包,面向工业自动化现场工程师、PLC调试人员及学习PROFINET/PROFIsafe组态的技术人员。包内以Sicar_Training_Demo项目为核心,配合MVK-MPNIO阀岛模块、Siemens G120驱动器及PSSu安全控制器等典型设备文件,可作理解标准程序架构与设备集成方式的参考。压缩包共106个文件,大小约38.19MB,涵盖27个xml配置文件、19个ini参数文件、以及bmp/png图片、cfs配置文件、GSDML设备描述文件等类型,便于按功能查找资料并进行组态比对。目前已有653人学习下载,适合需要快速掌握Sicar标准程序部署思路、梳理设备间数据交互逻辑,或准备相关实训项目的读者参考。通过研读其中的GSDML与示例图片,能够加深对从站设备导入、模块化配置及培训演示环境搭建的理解。
1. Sicar-Training-show-V16:它不刷 ECU,但能把诊断流程从头到尾教明白
如果你最近在找“sicar标准程序”相关的资源,大概率会看到 Sicar-Training-show-V16 这个压缩包。很多人把它当成一个能直接连车读故障码的刷写工具,下完装上才发现界面里全是报文列表和配置项,根本不知道点哪里。这里先说一个反直觉的结论:这个 V16 是一套训练演示框架,它本身不写任何 ECU,而是把 KWP2000/CAN 下的会话切换、安全访问、DID 读写这些诊断操作,拆成了可跟练的步骤和可回放的脚本。换句话说,它的价值不是替你干活,是让你看明白诊断设备底层是怎么跟 ECU 对话的。适合两类人:刚接触诊断协议开发的工程师,以及要给售后团队做上岗前演练的培训负责人。前提是你愿意先花半小时把协议配置这件事搞清楚,否则它就是个看不懂的黑匣子。
2. 先搞懂 Sicar 在“训”什么:KWP2000 与 CAN 寻址下的诊断服务链路
2.1 它是训练台架,不是刷写工具
Sicar-Training-show-V16 这个命名里,Training 和 Show 是两条独立入口。Training 面向学习者,把“建立连接 → 切换会话 → 安全访问 → 读数据”按步骤拆开,每一步都有状态指示和报文打印,适合跟着点。Show 面向演示者和测试者,可以加载一批预设脚本,自动按顺序发送诊断请求并记录每一条响应,也能把整个交互过程回放出来。两条入口共用同一套底层协议配置,所以你在 Training 模式里调通的参数,切到 Show 模式就能直接跑。这个设计思路在实际诊断开发里很实用:诊断仪与 ECU 之间的交互问题大多出在报文时序上,时序错误又经常被误判成参数配置问题。V16 把每一步的请求帧和响应帧都摊开打印,本质上就是给你一个看得见报文交换过程的透视镜。
2.2 通信层参数:波特率、物理寻址与功能寻址
开始跑之前,先把通信层的三个参数想清楚。波特率在 K 线时代常见 10400 和 38400,CAN 总线环境下则以 125k、250k、500k 为主。V16 程序里默认给的是 500k,这个值在高速 CAN 上最常见,但不代表你手头的 ECU 就是这个速率,所以后面第四章会专门写一个扫描流程,这里先记住结论:不要拿默认值去猜,要让程序自己测。
寻址方式则分物理寻址和功能寻址两类。物理寻址是点对点,比如测试仪发送 ID 0x7E0,只有 ID 为 0x7E8 的那个 ECU 会响应,读 DID、写 DID、安全访问这类一对一操作必须用它。功能寻址走广播 ID 0x7DF,总线上所有节点都会收到并响应,清除故障码、例程控制这类“希望全体执行”的操作才应该用它。这里有个容易忽略的坑:功能寻址发出后,如果挂了多个 ECU,响应帧会同时涌上来,轻则报文错乱,重则把总线上后续的正常请求都冲掉。所以程序里功能寻址之后一般会跟一个等待延时,这是正常现象。
2.3 服务层映射:10/22/27/2E 这些 SID 怎么驱动
Sicar 这类训练程序把诊断服务做成了内部映射表,每个操作最终都对应一个服务 ID。常用服务整理如下:
| 服务 ID | 功能 | 子功能说明 |
|---|---|---|
| 0x10 | 会话切换 | 0x01 默认会话,0x02 编程会话,0x03 扩展会话 |
| 0x22 | 读 DID | 无子功能,DID 为两字节标识符 |
| 0x2E | 写 DID | 无子功能,用于写入配置类数据 |
| 0x27 | 安全访问 | 0x01 请求种子,0x02 发送密钥,部分 ECU 用 0x05/0x06 作为扩展等级 |
| 0x14 | 清除 DTC | 0xFF 表示清除全部故障码 |
| 0x31 | 例程控制 | 0x01 启动例程,0x02 停止例程,0x03 查询例程结果 |
正响应的首字节是请求 SID 加 0x40,例如发送 0x10 切换会话,正响应首字节就是 0x50。如果收到 0x7F 开头的响应,后面跟的是否定响应码,比如 0x7F 0x22 0x31 表示“读 DID 请求超出范围”。V16 的报文窗口里会把这些十六进制原样显示出来,初看很劝退,但只要你记住“首字节加 0x40”这条规则,一眼就能判断操作是否成功。
2.4 会话切换的顺序优先级
服务层里最容易翻车的是会话切换顺序。多数 ECU 上电后处于默认会话(0x01),此时只开放最基本的读操作。要读完整的 DID 或做写入操作,需要先切到扩展会话(0x03);要做刷写,则需要进入编程会话(0x02)。常见的安全访问前置条件就是扩展会话或编程会话,顺序上必须先完成 0x10 切换,再发 0x27 请求种子。一个值得记住的细节是:进入编程会话后,部分 ECU 会暂时关闭普通 DID 读取服务,如果你在编程会话里发 0x22 读 DID,得到的可能是否定响应。所以训练程序里通常会在刷写流程最后显式退出编程会话并执行一次复位,让 ECU 回到默认会话。这个时序优先级是 V16 整个流程脚本的地基,后面所有步骤都在这个顺序上展开。
3. Training 与 Show 双模式实践:V16 界面里最值得先改的五个参数
3.1 Training 模式:像带教一样分步执行
Training 模式的界面一般分成左右两块:左侧是报文发送记录,右侧是配置面板和操作按钮。操作顺序被固定成了“连接适配器 → 扫描 ECU → 切换会话 → 安全访问 → 读取数据”,每一步都要等上一步返回正响应才能继续。这种设计对新手非常友好,因为你不可能跳步,每跳一步程序就会停在当前状态并打印错误码。我第一次用的时候就是在安全访问这一步卡住的,看了报文才发现自己跳过了扩展会话切换,直接发种子请求被 ECU 拒绝。如果你也刚拿到这个包,我建议先不开任何脚本,纯粹手动把 Training 模式整个流程点一遍,重点看每步请求报文的组包方式:SID 放第几个字节、子功能放哪一位、数据长度怎么算。这比直接跑 Show 脚本理解深得多。
3.2 Show 模式:脚本回放与批量演示
Show 模式则是另一种用途。它允许你加载一个脚本文件,里面预置了若干条诊断请求,程序按顺序发送并把每条响应记录成可读日志。这个模式我一般用来做两件事:一是验证通信参数改动之后,原本调通的诊断流程是否还能完整跑下来;二是给同事演示某个操作对应的报文交互过程,不用手点按钮,脚本按固定节奏发送,效果很直观。Show 模式还支持把一次演示生成回放日志,日志里每一行按“时间戳,请求报文,响应报文”的格式记录,后期可以拿去做自动化回归,这个用法在第六章展开。
3.3 配置文件五个关键参数与日志等级
V16 的配置文件通常是一个 JSON 格式的文本,界面上的配置项和它是同步的。这里给出一个最小可运行的配置示例:
{ "baud_rate": 500000, "can_channel": "can0", "tx_id": "0x7E0", "rx_id": "0x7E8", "default_session": 3, "security_algo": "xor01", "log_level": "debug" }参数说明:
- baud_rate:总线波特率,常用值 125000、250000、500000,部分老平台保留 10400 和 38400。
- can_channel:适配器通道名,Linux 下通常是 can0/can1,Windows 下是适配器厂商自定义的通道名。
- tx_id / rx_id:物理寻址的发送和接收 ID,读 DID 这类一对一操作依赖这两个值。
- default_session:程序启动后自动切换的会话等级,3 表示扩展会话,1 表示默认会话。
- security_algo:种子密钥算法标识,程序内置了几种常见算法,xor01 是最简单的异或变种。
- log_level:日志等级,调成 debug 后每一帧的收发时间和原始数据都会被打印出来。
我最常用的就是 log_level。出问题先开到 debug,看报文的收发时间是否合理、响应帧内容是否符合预期,比盯着界面上的状态灯有效得多。调完问题再把它改回 info,避免日志文件膨胀过快。
4. 三步跑通一次诊断任务:识别 ECU、读 DID、安全访问写入
4.1 先搭一个模拟 ECU,省得拿真实台架冒险
刚拿到 V16 的时候,别直接把实车或台架接上去试。训练程序本身带模拟 ECU 功能,但那个模拟器集成在界面内部,不好观察细节。我习惯先在 Python 里写一个极简的模拟 ECU,把流程原样走一遍,这样既能验证程序配置,又能熟悉报文的每一个字节。下面是一个只响应会话切换、读 DID、安全访问三个服务的模拟器:
import time class SimEcu: def __init__(self, did_table=None, seed=0x5AA5): self.did_table = did_table or {0xF190: b'\x01\x02\x03\x04'} self.seed = seed self.security_unlocked = False def handle(self, req: bytes) -> bytes: sid = req[0] if sid == 0x10: # 会话切换 # 正响应: 0x50 + 子功能 + 时间参数 return b'\x50\x03\x00\x32\x01\xF4' if sid == 0x22: # 读 DID did = (req[1] << 8) | req[2] if did in self.did_table: data = self.did_table[did] # 正响应: 0x62 + 数据长度 + 数据 return b'\x62' + bytes([len(data)]) + data return b'\x7f\x22\x31' # NRC 0x31 超出范围 if sid == 0x27: # 安全访问 if req[1] == 0x01: # 请求种子 return b'\x67\x01' + self.seed.to_bytes(4, 'little') if req[1] == 0x02: # 发送密钥 key = int.from_bytes(req[2:6], 'little') if key == (self.seed ^ 0xFFFF): self.security_unlocked = True return b'\x67\x02' return b'\x7f\x27\x35' # NRC 0x35 密钥错误 return b'\x7f\x11\x11' # 服务不支持逻辑说明:模拟器解析请求的第一个字节作为服务 ID,0x10 会话切换返回固定的扩展会话响应;0x22 读 DID 按两字节标识符查表,返回格式是“0x62 + 数据长度 + 数据”;0x27 安全访问做了最简单的异或校验,种子是 4 字节小端整数,密钥必须等于种子异或 0xFFFF。注意正响应的首字节都是请求 SID 加 0x40,这个规则和真车一致。参数上,seed 选 0x5AA5 只是为了让十六进制看起来有规律,实际 ECU 的种子长度有 2 字节也有 4 字节,算法更是五花八门,训练程序里一般会标注你正在模拟的是哪一种。
4.2 第一步:波特率扫描识别 ECU
拿到一个没接触过的 ECU,第一件事不是按默认配置去连,而是扫描波特率。Sicar 程序里常见的做法是把候选波特率逐个试一遍,直到收到有效响应为止。这个逻辑可以写成一个小函数:
BAUD_RATES = [10400, 38400, 125000, 250000, 500000] def scan_ecu(channel): for baud in BAUD_RATES: channel.set_baud(baud) try: resp = channel.request(b'\x10\x01', timeout=200) except TimeoutError: continue if resp: return baud raise RuntimeError("no ecu found")逻辑说明:遍历常见波特率,每次先切换适配器速率,再发送 0x10 默认会话请求,超时时间设为 200 毫秒。收到任何响应就说明当前波特率是对的。参数上,timeout 太短会漏掉慢速 K 线节点,太长又会让扫描过程变得很拖沓,200ms 是个兼顾两者的经验值。有一点要注意:扫描顺序把低速率的 10400 和 38400 放在前面,是因为 K 线设备切换速率后需要更长的稳定时间,先扫低速可以规避一些适配器的切换问题。
4.3 第二步:读 DID,验证会话状态
波特率确定后,读 DID 是最直接的验证方式。V16 的 Training 模式里,你只需要在配置面板填上目标 DID,程序会自己组包发送。这里把底层逻辑展开看:
def read_did(channel, did: int) -> bytes: req = bytes([0x22, (did >> 8) & 0xFF, did & 0xFF]) resp = channel.request(req) if resp[0] != 0x62: raise RuntimeError(f"read fail, NRC=0x{resp[-1]:02X}") length = resp[1] return resp[2:2 + length]逻辑说明:0x22 服务要求 DID 拆成高字节和低字节,放在请求的第 2 和第 3 个字节。正响应首字节是 0x62,第二个字节是数据长度,后面才是实际数据。参数上,DID 的取值参考诊断规范或 ECU 数据手册,比如 0xF190 在很多平台上代表车辆识别码的一部分,具体含义取决于你手头的 ECU 定义。如果响应是 0x7F 开头,说明当前会话等级不支持这个 DID,先检查是否还在默认会话里,切到扩展会话再试。
4.4 第三步:安全访问与密钥写入
读 DID 成功只能说明通信链路是通的,很多写操作和标定类 DID 需要安全访问通过后才能执行。下面这段代码演示标准的三次握手:
def security_access(channel, algo="xor01", sub_func=0x01): # 第一步:请求种子 resp = channel.request(bytes([0x27, sub_func])) if resp[0] == 0x7F: raise RuntimeError("seed request blocked") seed = resp[2:6] # 第二步:按算法从种子计算密钥 key = seed_to_key(seed, algo) # 第三步:发送密钥,sub_func + 1 表示发送密钥 resp = channel.request(bytes([0x27, sub_func + 0x01]) + key) if resp[0] != 0x67: raise RuntimeError("key verify failed") return True逻辑说明:0x27 服务的子功能分成两段,0x01 请求种子,0x02 发送密钥,第二段的子功能号是第一段加 1。种子和密钥都是变长数据,V16 程序里的算法模块支持切换 seed_to_key 的实现,xor01 只是最简单的一种,真实场景里常见移位、查表、加盐等变体。参数上,seed 的长度不是固定的,有的 ECU 给 2 字节,有的给 4 字节,resp[2:6] 这种写法要求程序知道当前 ECU 的种子长度,否则你会把校验位也当种子算进去,密钥永远算不对。这一步是训练程序里最容易出玄学问题的地方,第五章会专门说这个坑。
5. Sicar 训练程序避坑记录:五条踩坑记录与处理办法
| 避坑条目 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 波特率不匹配 | 连接持续超时 | 默认值与 ECU 实际速率不一致 | 先跑波特率扫描再固定参数 |
| 寻址方式误用 | 响应报文错乱或 ID 异常 | 读操作用了功能寻址 | 严格区分物理寻址与功能寻址 |
| 种子字节序颠倒 | 密钥校验反复失败 | 算法对但字节序不对 | 换大小端配置再试 |
| 编程会话未安全退出 | 刷写后 ECU 掉线 | 复位顺序错误 | 固定退出会话→复位→重建连接顺序 |
| 帧间隔不稳定 | 回放时校验和错误 | 跨平台定时精度差异 | 按时间戳回放,不要连发 |
5.1 波特率不匹配导致的“连不上”
现象:适配器连接正常,但发送 0x10 请求后程序一直显示超时,没有任何响应帧出现在报文窗口里。原因:V16 默认波特率是 500k,而你接的 ECU 实际工作在 250k,两边速率不一致,总线上的帧对 ECU 来说就是乱码,它根本不会回复。解决:不要手动猜,直接用 4.2 节的扫描逻辑把所有常用波特率扫一遍,扫到有响应的那个再固定进配置。血泪经验是:有些老平台 ECU 支持 500k 但不稳定,冷启动后要 10 秒才进入可通信状态,扫描时的超时时间别设太短。
5.2 功能寻址与物理寻址混淆引发响应错乱
现象:单发一条读 DID 请求,报文窗口里突然出现多个不同 ID 的响应帧。原因:读 DID 属于一对一操作,必须用物理寻址,结果你误用了功能寻址 0x7DF,总线上多个 ECU 都收到并同时回复,响应帧互相覆盖。解决:明确分工——读 DID、写 DID、安全访问都走物理寻址;清除故障码、例程控制这类全局操作才走功能寻址。另外,功能寻址之后程序通常会自动加等待延时,这个延时不是多余的,它是给总线时间把广播响应消化掉。
5.3 安全访问种子字节序不对,密钥永远校验失败
现象:种子读出来了,算法模块也没选错,但每次发送密钥都返回否定响应码 0x35,折腾一晚上找不出原因。原因:很多 ECU 的种子是多字节整数,传输时默认小端序,少数平台用大端序。程序按小端拼出种子,算出来的密钥对不上 ECU 的期望,ECU 自然拒绝。解决:把种子原始 hex 打出来,人工按两种字节序各算一次密钥,看哪种能通过。V16 的算法配置里通常有一个字节序开关,改成 swap 模式再换一次密钥即可。从那以后我每次遇到 0x35,第一反应永远是先煎种子字节序,而不是怀疑算法本身。
5.4 编程会话未结束就复位,刷写一半变砖
现象:刷写流程收尾时先断了连接,再重新上电,发现 DID 数据还是旧的,更严重的是 ECU 停留在编程会话状态,正常的会话切换请求都进不去。原因:编程会话是 ECU 的敏感状态,写入流程结束后必须先显式退出编程会话,再发送复位请求,最后重建默认会话。跳步会导致 ECU 认为刷写未完成,部分实现还会把闪存标记为异常。解决:把收尾流程固化成固定顺序——退出编程会话 → 发送复位请求 → 等待 100ms → 重新建立默认会话。顺序反了或者复位前没退出会话,都会翻车。
5.5 跨平台时序差异导致校验和不同
现象:Windows 上跑通过的 Show 脚本,换到 Linux 工控机再回放,个别响应帧的校验和总是报错。原因:脚本循环里发送间隔依赖系统计时,Windows 上 sleep 精度和 Linux 不一样,CAN 适配器的驱动缓冲大小也不同,导致帧间隔抖动,部分对时间敏感的 ECU 就开始丢帧或算错校验。解决:回放时把日志里的时间戳一起解析出来,按实际时间间隔去 sleep,而不是把所有请求一股脑连续发送。这个细节做对了,回放结果就和现场表现基本一致。
6. 把 Show 回放日志变成自动化回归用例:省去一周手工排错
6.1 解析回放日志
Show 模式生成的日志每一行都是固定的三段式格式:时间戳、请求报文、响应报文,用逗号分隔。这个格式天然适合做回归测试。我把日志解析封装成一个小函数,每次改动配置或换一台模拟 ECU 后,先把历史日志回放一遍,看响应是否和之前完全一致:
def parse_show_log(log_path): cases = [] with open(log_path) as fp: for line in fp: parts = line.strip().split(",") if len(parts) != 3: continue ts, tx_hex, rx_hex = parts cases.append({ "ts": float(ts), "tx": bytes.fromhex(tx_hex), "rx": None if rx_hex == "" else bytes.fromhex(rx_hex), }) return cases逻辑说明:日志里可能混入空响应行,比如请求发出后超时没有响应,这种情况 rx_hex 是空字符串。解析时不能直接跳过,要保留下来作为失败的基准案例。ts 是浮点数,单位毫秒,用于后续控制发送间隔。bytes.fromhex 负责把十六进制字符串还原成字节数组,这也是报文操作里最常用的解析手段。
6.2 按时间戳回放并断言
拿到解析结果后,回放的重点不在于“能不能收到响应”,而在于“响应和当初记录的是否一致”。我做了一个简单的断言循环:
def replay_cases(channel, cases): failures = [] prev_ts = None for case in cases: if prev_ts is not None: gap = case["ts"] - prev_ts if gap > 0: time.sleep(gap / 1000.0) prev_ts = case["ts"] got = channel.request(case["tx"]) if case["rx"] is None: if got is not None: failures.append((case["ts"], "expected timeout", got.hex())) elif got != case["rx"]: failures.append((case["ts"], case["rx"].hex(), got.hex())) return failures逻辑说明:sleep 用的是毫秒差值换算成秒,目的是让请求发送节奏尽量贴近当初现场记录的真实节奏。断言分两种情况:当初日志里是超时,那现在也应该超时;当初有响应,那内容必须逐字节一致。如果只比对“有没有响应”而不比对内容,回归就发现不了子功能变了、数据长度错位这类隐蔽问题。这套逻辑跑通之后,每次更新 V16 的配置文件或者换适配器,我只需要跑一遍历史日志,十分钟就能确认流程没被改坏。比起拿实车逐条手点测试,节省的时间非常可观。
从那以后,我每次接手新的训练程序,都会强制先跑一遍 Show 模式的回放脚本,确认通信参数和响应时间戳没有被悄悄改掉,再开始手动操作。这个习惯帮我避开了好几次配置被误改而引发的“疑难杂症”。希望帮到你。
本文还有配套的精品资源,点击获取