基于C#实现的汽车CAN总线诊断工具开发实践
ProgramTools是一款基于C# .NET 4.7.2和WPF开发的汽车CAN总线诊断工具,主要用于ECU诊断、CAN数据监控和UDS协议通信。该工具支持多种CAN设备,采用分层架构设计,包括底层驱动封装层、中间通信层和应用层。通过NLua引擎支持Lua脚本配置,可灵活编辑CAN消息流程和界面交互。最新优化包括27服务接口修改为GenerateKeyEx、增加同星驱动支持等。该工具具有强大的
1. 介绍
ProgramTools是一款基于C# .NET 4.7.2和WPF开发的汽车CAN总线诊断工具,主要应用于汽车ECU诊断、CAN总线数据监控与分析、UDS协议通信等场景。该工具支持多种CAN设备(如CANALYST、周立功ZLG、FreeCan等),提供了灵活的通信接口和强大的UDS协议支持,可以进行ECU诊断、刷写等高级功能。
原作者泥泥虎https://pan.baidu.com/s/1BDN84JYaIOB7wPqQ4Vj1bw 提取码: bhxh
优化项:
1、使用lua脚本编辑流程以及界面信息
2、修改27服务接口为GenerateKeyEx
3、增加同星驱动
2. 技术栈与项目架构
2.1 技术栈
- 开发语言 :C#
- 框架版本 :.NET Framework 4.7.2
- 界面框架 :WPF
- CAN通信 :PInvoke调用第三方CAN驱动库
- 脚本支持 :NLua(Lua脚本引擎)
- UDS协议 :UdsTp.dll(CAN传输层)
- 其他依赖 :CommonUse.dll、ICSharpCode.SharpZipLib.dll等
-
2.2 Lua脚本示例
1、请求地址的编辑
2、按钮的内容以及每个按钮的UDS流程编辑
-- CAN消息配置脚本示例
-- 此脚本用于动态配置CAN通信消息
FUNC_ADDR_ID = 0x7DF -- 功能地址
PHYS_ADDR_ID = 0x705 -- 物理地址
RESP_ADDR_ID = 0x785 -- 响应地址
CRC_DID = {02,02} -- CRC校验DID[31 01 02 02 xx xx xx xx]
MEM_ER_DID = {0xff,0x00} -- 内存擦除did[31 01 FF 00 xx xx xx xx xx xx xx xx]
MEM_ER_START = 5
-- 按钮文本配置
Button1Text = "软件版本"
Button2Text = "测试"
Button3Text = "按钮3"
Button4Text = "按钮4"
Button5Text = "按钮5"
--数据可配置参数:
--参数1:ID
--参数2:数据
--参数3:等待下位机回复时间。可为空,默认参数为200
--参数4:上位机延迟时间。可为空,默认参数为0【调度时间,1ms】
--参数5:0为CAN、1为canFD。可为空,默认参数为0
-- 刷写流程
BtMessage(PHYS_ADDR_ID, {0x10, 0x03})
BtMessage(PHYS_ADDR_ID, {0x31, 0x01,02,03})
BtMessage(PHYS_ADDR_ID, {0x85, 0x02})
BtMessage(PHYS_ADDR_ID, {0x22, 0xF1,0x5B})
BtMessage(PHYS_ADDR_ID, {0x10, 0x02})
BtMessage(PHYS_ADDR_ID, {0x27, 0x09})
BtMessage(PHYS_ADDR_ID, {0x27, 0x0A, 0x00, 0x00, 0x00, 0x00})
BtMessage(PHYS_ADDR_ID, {0x34, 0x36, 0x37})
BtMessage(PHYS_ADDR_ID, {0x31, 0x01, 0x02,0x00,0x00,0x60,0x00,0x00})
BtMessage(PHYS_ADDR_ID, {0x31, 0x01, 0xFF,0x00,0x44,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00})
BtMessage(PHYS_ADDR_ID, {0x34, 0x36, 0x37})
BtMessage(PHYS_ADDR_ID, {0x31, 0x01, 0x02,0x02,0x00,0x00,0x00,0x00})
BtMessage(PHYS_ADDR_ID, {0x31, 0x01, 0xff,0x01})
BtMessage(PHYS_ADDR_ID, {0x11, 0x01})
-- 按钮1流程
button1Message(PHYS_ADDR_ID, {0x10, 0x02})
-- 按钮2流程
button2Message(PHYS_ADDR_ID, {0x10, 0x03})
button2Message(PHYS_ADDR_ID, {0x27, 0x01})
button2Message(PHYS_ADDR_ID, {0x27, 0x02, 0x00, 0x00, 0x00, 0x00})
-- 按钮3流程
button3Message(PHYS_ADDR_ID, {0x10, 0x03})
-- 按钮4流程
button4Message(PHYS_ADDR_ID, {0x10, 0x03})
-- 按钮5流程
button5Message(PHYS_ADDR_ID, {0x10, 0x03})
2.2 分层架构设计
ProgramTools采用清晰的分层架构,确保代码的可维护性和扩展性:
- 底层驱动封装层 :封装各种CAN设备驱动,提供统一接口
- 中间通信层 :处理CAN消息收发、UDS协议转换
- 应用层 :实现业务逻辑和用户界面
应用层 (ExecuteDownload.cs, View层)
↑
中间通信层 (CanDriver.cs, UDS协议处理)
↑
底层驱动封装层 (CanAlyst.cs, ZlgCan.cs等)
↑
物理CAN设备
3. 底层CAN驱动封装层实现
3.1 设备驱动接口设计
底层驱动封装层为每种CAN设备实现了统一的接口,主要包括初始化、关闭、发送消息、接收消息等功能。以周立功ZLG CAN设备为例,其实现如下
// ZlgCan.cs 核心代码片段
public static class ZlgCan
{
// 设备类型常量
private const int ZCAN_USBCANFD_200U = 40; // USB-CANFD-200U
private const int ZCAN_USBCANFD_MINI = 41; // USB-CANFD-MINI
private const int ZCAN_USBCANFD_100U = 42; // USB-CANFD-100U
// 初始化CAN设备
public static bool Init(int channel)
{
// 设备类型遍历尝试
int[] devTypeArray = new int[] { ZCAN_USBCANFD_MINI, ZCAN_USBCANFD_200U, ZCAN_USBCANFD_100U };
foreach (int devType in devTypeArray)
{
// 打开设备
int ret = ZCAN_OpenDevice(devType, 0, 0);
if (ret == 1)
{
// 配置CAN通道参数
ZCAN_CHANNEL_INIT_CONFIG config = new ZCAN_CHANNEL_INIT_CONFIG();
config.mode = 0; // 正常模式
config.can_type = 1; // CAN FD模式
// 设置波特率 500Kbps
config.timing0 = 0x00;
config.timing1 = 0x1c;
// 初始化CAN通道
if (ZCAN_InitCAN(devType, 0, channel, ref config) == 1)
{
// 启动CAN通道
if (ZCAN_StartCAN(devType, 0, channel) == 1)
{
return true;
}
}
ZCAN_CloseDevice(devType, 0);
}
}
return false;
}
// 接收消息处理
public static bool RxMsgHandler(int channel)
{
// 检查接收缓冲区消息数量
int receiveNum = ZCAN_GetReceiveNum(deviceType, 0, channel);
if (receiveNum <= 0) return false;
// 根据CAN类型选择不同的接收方法
if (deviceType == ZCAN_USBCANFD_MINI || deviceType == ZCAN_USBCANFD_200U || deviceType == ZCAN_USBCANFD_100U)
{
// CAN FD接收处理
ZCAN_ReceiveFD_Data[] recvData = new ZCAN_ReceiveFD_Data[receiveNum];
int recvLen = ZCAN_ReceiveFD(deviceType, 0, channel, recvData, receiveNum, 10);
if (recvLen > 0)
{
for (int i = 0; i < recvLen; i++)
{
// 处理接收到的CAN FD消息
if (RxMsg != null)
{
byte[] buff = new byte[recvData[i].data.len];
Marshal.Copy(recvData[i].data.data, buff, 0, recvData[i].data.len);
RxMsg(CanTypeEnum.CANFD, (uint)recvData[i].id.id, buff, recvData[i].data.len);
}
}
}
}
return true;
}
// 发送消息处理
public static bool TxMsgHandler(int channel, CanTypeEnum canType, uint id, byte[] buff, int len)
{
if (canType == CanTypeEnum.CANFD)
{
// CAN FD消息发送
ZCAN_TransmitFD_Data sendData = new ZCAN_TransmitFD_Data();
sendData.id.id = id;
sendData.id.format = 0; // 标准帧
sendData.id.type = 0; // 数据帧
sendData.data.len = (byte)len;
sendData.data.flags = 0; // 标准CAN FD数据
// 复制数据
Marshal.Copy(buff, 0, sendData.data.data, len);
// 发送消息
return ZCAN_TransmitFD(deviceType, 0, channel, ref sendData, 1) == 1;
}
else
{
// 标准CAN消息发送
ZCAN_Transmit_Data sendData = new ZCAN_Transmit_Data();
sendData.id = MAKE_CAN_ID(id, false, false);
sendData.len = (byte)len;
// 复制数据
Marshal.Copy(buff, 0, sendData.data, len);
// 发送消息
return ZCAN_Transmit(deviceType, 0, channel, ref sendData, 1) == 1;
}
}
}
3.2 设备驱动PInvoke实现
底层驱动通过PInvoke技术调用C/C++编写的CAN驱动动态库,以下是部分关键函数的PInvoke声明:
// ZlgCan.cs 中的PInvoke声明
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr ZCAN_OpenDevice(uint device_type, uint device_index, uint reserved);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_SetValue(IntPtr device_handle, string path, byte[] value);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr ZCAN_InitCAN(IntPtr device_handle, uint can_index, IntPtr pInitConfig);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_StartCAN(IntPtr channel_handle);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_CloseDevice(IntPtr device_handle);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_Transmit(IntPtr channel_handle, IntPtr pTransmit, uint len);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_TransmitFD(IntPtr channel_handle, IntPtr pTransmit, uint len);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_GetReceiveNum(IntPtr channel_handle, byte type);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_Receive(IntPtr channel_handle, IntPtr data, uint len, int wait_time = -1);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
private static extern uint ZCAN_ReceiveFD(IntPtr channel_handle, IntPtr data, uint len, int wait_time = -1);
4. 中间层通信模块实现
4.1 CanDriver.cs 核心实现
CanDriver.cs作为中间通信层的核心,负责统一管理不同CAN设备的操作,并提供UDS协议支持。主要功能包括:
- 设备初始化和自动识别
- CAN消息收发统一接口
- UDS协议消息处理
- 安全访问与密钥计算
// CanDriver.cs 核心功能实现
public static class CanDriver
{
// 设备类型枚举
public enum DeviceEnum { NoDevice, CanAlyst, ZlgCan, FreeCan, Toomoss, PcanPro, Tsmaster };
// 接收消息委托
public delegate void RxMsgFun(CanTypeEnum canType, uint id, byte[] buff, int len);
// 设备初始化(自动检测设备类型)
public static bool DeviceInit()
{
DeviceType = DeviceEnum.NoDevice;
// 尝试初始化不同类型的CAN设备
if (CanAlyst.Init(DeviceChannel))
{
CanAlyst.RxMsg = RxMsgInterrupt;
DeviceType = DeviceEnum.CanAlyst;
return true;
}
if (ZlgCan.Init(DeviceChannel))
{
ZlgCan.RxMsg = RxMsgInterrupt;
DeviceType = DeviceEnum.ZlgCan;
return true;
}
// 其他设备类型的初始化...
return false;
}
// UDS消息接收处理
public static bool CanAwaitRxUdsMsg(out CanTypeEnum canType, out uint id, byte[] p_buff, out int len, int await = 1000)
{
canType = CanTypeEnum.CAN;
id = 0; len = 0;
WaitTimeTick = 0;
byte[] p_frame = new byte[1];
uint[] p_id = new uint[1];
int[] p_len = new int[1];
if (DeviceType == DeviceEnum.NoDevice) { return false; }
// 等待接收UDS消息,带超时机制
while (WaitTimeTick < await)
{
MainThread();
if (CanTp_UdsRxMsg(p_frame, p_id, p_buff, p_len) == 1)
{
// 处理等待响应的情况(0x78 NRC码)
if ((p_buff[0] == 0x7F) && (p_buff[2] == 0x78))
{
WaitTimeTick = 0;
await = 5000; // 延长等待时间
continue;
}
canType = p_frame[0] == 0 ? CanTypeEnum.CAN : CanTypeEnum.CANFD;
id = p_id[0];
len = p_len[0];
return true;
}
}
return false;
}
// UDS消息发送处理
public static bool CanAwaitTxUdsMsg(CanTypeEnum canType, uint id, byte[] p_buff, int len, int await = 0)
{
WaitTimeTick = 0;
if (DeviceType == DeviceEnum.NoDevice) { return false; }
// 等待指定时间
while (WaitTimeTick < await) { MainThread(); }
// 发送UDS消息
if (CanTp_UdsTxMsg((byte)canType, id, p_buff, len) == 0) { return false; }
// 等待发送完成
while (CanTp_CheckIfDataIsTxOk() == 0) { MainThread(); }
return true;
}
// 主循环处理
public static void MainThread()
{
// 调用UDS传输层主函数
CanTp_Main();
// 处理接收消息
RxMsgHandler();
// 处理发送消息
TxMsgHandler();
}
}
5. UDS协议支持实现
5.1 UDS协议服务实现
ProgramTools通过UdsTp.dll提供的CAN传输层功能,实现了完整的UDS诊断协议支持。ExecuteDownload.cs中实现了各种UDS诊断服务:
// ExecuteDownload.cs 中UDS服务实现
private static bool? DiagnosticSessionControl_10()
{
// 检查是否需要等待响应
bool resp = ((TxMsg.buff[1] & 0x80) >> 7) == 0;
// 发送UDS诊断会话控制请求
if (!CanAwaitTxUdsMsg(TxMsg.canType, TxMsg.id, TxMsg.buff, TxMsg.len, TxMsg.await)) { return false; }
if (!resp) { return true; }
// 等待并处理响应
if (CanAwaitRxUdsMsg(out RxMsg.canType, out RxMsg.id, RxMsg.buff, out RxMsg.len, TxMsg.wait))
{
// 记录日志
WriteLog($"0x{TxMsg.id:X} {TxMsg.len} TX: {CommonLib.ToStr(TxMsg.buff, 0, TxMsg.len)}");
WriteLog($"0x{RxMsg.id:X} {RxMsg.len} RX: {CommonLib.ToStr(RxMsg.buff, 0, RxMsg.len)}");
// 验证响应消息ID和服务ID
if ((RxMsg.id == RESP_ADDR_ID)
&& (RxMsg.buff[0] == (TxMsg.buff[0] + 0x40))
&& (RxMsg.buff[1] == TxMsg.buff[1]))
{
return true; // 成功响应
}
}
return false; // 失败
}
// ECU复位服务实现
private static bool? ECUReset_11()
{
// 类似的实现逻辑...
}
5.2 UDS传输层PInvoke调用
通过PInvoke调用UdsTp.dll提供的CAN传输层功能:
// UDS传输层PInvoke声明
[DllImport("UdsTp.dll", EntryPoint = "CanTp_Main", CallingConvention = CallingConvention.Cdecl)]
private static extern void CanTp_Main();
[DllImport("UdsTp.dll", EntryPoint = "CanTp_TimerCtrl", CallingConvention = CallingConvention.Cdecl)]
private static extern void CanTp_TimerCtrl();
[DllImport("UdsTp.dll", EntryPoint = "CanTp_SetRxTxAddrId", CallingConvention = CallingConvention.Cdecl)]
private static extern void CanTp_SetRxTxAddrId(uint funcAddrId, uint physAddrId, uint respAddrId);
[DllImport("UdsTp.dll", EntryPoint = "CanTp_UdsRxMsg", CallingConvention = CallingConvention.Cdecl)]
private static extern byte CanTp_UdsRxMsg(byte[] p_frame, uint[] p_id, byte[] p_buff, int[] p_len);
[DllImport("UdsTp.dll", EntryPoint = "CanTp_UdsTxMsg", CallingConvention = CallingConvention.Cdecl)]
private static extern byte CanTp_UdsTxMsg(byte canType, uint id, byte[] p_buff, int len);
6. 安全访问与密钥计算
ProgramTools实现了UDS安全访问(Security Access)功能,支持动态加载密钥计算DLL:
// 密钥计算委托定义
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate int seed_key(byte* ipSeedArray, uint iSeedArraySize, uint iSecurityLevel, string ipVariant, byte* iopKeyArray, uint iMaxKeyArraySize, ref int oActualKeyArraySize);
// 加载密钥计算DLL
public static void IputSecurityKeyDllFile()
{
if (!File.Exists("SecurityKey.dll")) { return; }
DllImport.IputPath("SecurityKey.dll");
SecurityKeyCalculation = (seed_key)DllImport.InvokeFunction("GenerateKeyEx", typeof(seed_key));
}
// 密钥计算实现
public static bool EncryptionKeyCalculation(byte[] seed, out byte[] key)
{
key = null;
if (SecurityKeyCalculation == null) { return false; }
unsafe
{
key = new byte[16];
int keySize = 0;
fixed (byte* p_seed = seed)
fixed (byte* p_key = key)
{
// 调用密钥计算函数
int ret = SecurityKeyCalculation(p_seed, (uint)seed.Length, 0x01, null, p_key, (uint)key.Length, ref keySize);
if (ret == 0 && keySize > 0)
{
// 调整密钥长度
byte[] temp = new byte[keySize];
Array.Copy(key, 0, temp, 0, keySize);
key = temp;
return true;
}
}
}
return false;
}
7. Lua脚本配置支持
ProgramTools支持通过Lua脚本配置CAN消息和诊断流程,提高了工具的灵活性和可配置性:
// Lua脚本执行
private static bool ExecuteLuaScript()
{
try
{
string luaScriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PATH_VALUE);
if (!File.Exists(luaScriptPath))
return false;
// 创建Lua实例
using (Lua lua = new Lua())
{
// 创建消息处理列表
ProcessList = new List<Msg_Type>();
// 注册C#方法到Lua环境
lua.RegisterFunction("BtMessage", null, typeof(ExecuteDownload).GetMethod("AddMessageToProcessList"));
lua.RegisterFunction("button1Message", null, typeof(ExecuteDownload).GetMethod("TEST"));
// 其他方法注册...
// 读取并执行Lua脚本
string scriptContent = File.ReadAllText(luaScriptPath, Encoding.UTF8);
lua.DoString(scriptContent);
return ProcessList.Count > 0;
}
}
catch (Exception ex)
{
Console.WriteLine($"执行Lua脚本时发生错误: {ex.Message}");
return false;
}
}
8. 总结
ProgramTools项目通过清晰的分层架构和灵活的设计,实现了一个功能强大的汽车CAN总线诊断工具。底层驱动封装层提供了统一的设备操作接口,中间通信层处理消息收发和UDS协议转换,应用层实现业务逻辑。该工具支持多种CAN设备和完整的UDS协议,具有良好的可扩展性和可维护性。
通过PInvoke技术调用第三方CAN驱动库和UDS传输层,结合C#强大的开发能力和Lua脚本的灵活性,ProgramTools为汽车电子开发和诊断提供了一个高效、可靠的解决方案。
更多推荐
所有评论(0)