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采用清晰的分层架构,确保代码的可维护性和扩展性:

  1. 底层驱动封装层 :封装各种CAN设备驱动,提供统一接口
  2. 中间通信层 :处理CAN消息收发、UDS协议转换
  3. 应用层 :实现业务逻辑和用户界面

应用层 (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为汽车电子开发和诊断提供了一个高效、可靠的解决方案。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐