1. 项目缘起:从“CANable”到“PCAN”的奇妙转换
最近在调试一个CAN总线项目,手头正好有一个闲置的CANable设备。这玩意儿小巧便宜,开源,用起来也方便,但配套的上位机软件要么功能简单,要么生态不够丰富。而另一边,像PCAN-View、PCAN-Explorer这类由PEAK-System公司出品的专业工具,在汽车电子、工业控制领域几乎是行业标准,功能强大,协议支持全面,生态成熟。一个念头就冒出来了:能不能让这个廉价的CANable,在电脑上被识别为一台“PCAN”设备,从而无缝接入那个强大的软件生态呢?
这听起来有点像“李鬼扮李逵”,但在实际开发测试中,这招非常实用。很多成熟的软件、库(比如Python的python-can库,其PCAN后端就非常稳定)对PCAN的接口支持是最好的。如果你手头只有CANable或者类似的开源USB-CAN适配器,但又想利用这些成熟工具链,那么实现这个“PCA”(这里指让CANable模拟PCAN设备接口)功能,就成了一个高性价比的解决方案。它本质上是通过修改CANable的固件,使其USB设备描述符和通信协议与PCAN设备兼容,从而“欺骗”操作系统和上层软件,让它们以为连接的就是一台正版的PCAN-USB适配器。
2. 核心原理:固件魔改与协议模拟
要让CANable变身PCAN,核心工作在于固件。CANable项目本身基于一颗常见的STM32F0系列微控制器,其USB-CAN桥接功能由芯片内运行的固件程序决定。原版固件通常使用自定义的USB协议或兼容SLAB的USB转串口协议,再通过串口发送特定的ASCII或二进制指令来控制CAN。而PCAN-USB设备使用的是PEAK公司自定义的一套USB协议。
因此,我们的目标就是重写或修改CANable的固件,使其在USB枚举阶段,向电脑报告与PCAN-USB相同的厂商ID(VID)和产品ID(PID),并且在后续的数据通信中,严格遵守PCAN-USB的数据帧格式和命令集。
2.1 关键识别点:VID/PID与设备字符串
PCAN-USB设备的VID/PID是固定的,例如经典的PCAN-USB接口卡,其VID通常是0x0C72,PID是0x000C。当设备插入电脑,系统会读取这些ID来加载对应的驱动程序。CANable原版固件使用的是ST官方的VID(0x0483)或开源项目自定义的PID。修改固件的第一步,就是在USB设备描述符中将这些标识符改成PCAN设备的。
注意:直接使用他人的VID/PID可能存在法律风险,且可能导致与系统中真正的PCAN设备冲突。此举仅用于学习、测试以及与已有软件生态兼容的目的。在生产或商业环境中,应使用自己合法注册的VID/PID。
除了VID/PID,设备名称、制造商字符串等也需要相应修改,以便PCAN的官方驱动(pcanusb.sys等)能正确识别并绑定到这个设备上。
2.2 通信协议解析:数据帧结构
这是改造中最具技术含量的部分。PCAN-USB协议并非公开文档,但其协议格式已被开源社区通过逆向工程基本掌握。一个典型的PCAN-USB数据消息帧包含一个固定的消息头(Message Header)和负载数据。
消息头中会包含关键信息,例如:
- 消息类型:是标准帧还是扩展帧?
- 数据长度码(DLC):指示后续数据段的字节数(0-8)。
- 时间戳:消息被接收或发送的时间戳(高精度)。
- CAN ID:报文的标准或扩展标识符。
固件需要精确地按照这个格式来打包从CAN总线上接收到的数据,并通过USB发送给电脑;同时,也要能准确解析从电脑发来的、符合此格式的指令和数据,并将其转换成CAN控制器可以理解的格式发送到总线上。
2.3 固件开发环境搭建
CANable的固件通常是使用ARM GCC工具链进行编译的。你需要准备以下环境:
- ARM GNU Toolchain:用于编译STM32的代码。
- OpenOCD或ST-LINK Utility:用于通过SWD接口给CANable的STM32芯片烧录程序。CANable板子上通常留有SWD调试接口。
- 源代码:你需要一份CANable的固件源代码作为基础。最理想的是找到已经有人为模拟PCAN而修改过的分支或项目(例如,开源社区中存在的
candleLight_fw项目的某些分支)。如果找不到,就需要基于原版固件(如使用libopencm3库的版本)自行修改。 - 文本编辑器/IDE:如VSCode、STM32CubeIDE等,用于代码编辑。
3. 实操步骤:从代码修改到固件烧录
假设我们已经找到了一个基础项目(例如一个旨在兼容PCAN的candleLight_fw分支),以下是详细的实现步骤。
3.1 获取并理解基础代码
首先,将项目代码克隆到本地。
git clone https://github.com/[username]/candleLight_fw.git cd candleLight_fw仔细阅读README.md和项目结构。关键文件通常包括:
src/usb.c/src/usbd_*.c:包含USB设备描述符(VID, PID, 字符串描述符)的定义。src/can.c:包含CAN控制器(如STM32的bxCAN)的初始化和收发函数。src/pcan_protocol.c或类似:这是实现PCAN协议解析和打包的核心文件。你需要重点关注这里的数据结构定义和帧处理函数。
3.2 修改设备描述符
在usb.c文件中,找到设备描述符结构体usb_device_descriptor或类似的定义。修改其中的idVendor和idProduct字段。
// 示例:修改为PCAN-USB的常见VID/PID #define USB_VID 0x0C72 #define USB_PID 0x000C const struct usb_device_descriptor dev_descr = { .bLength = USB_DT_DEVICE_SIZE, .bDescriptorType = USB_DT_DEVICE, .bcdUSB = 0x0200, .bDeviceClass = 0xFF, // 厂商自定义类 .bDeviceSubClass = 0xFF, .bDeviceProtocol = 0xFF, .bMaxPacketSize0 = 64, .idVendor = USB_VID, .idProduct = USB_PID, .bcdDevice = 0x0100, .iManufacturer = 1, .iProduct = 2, .iSerialNumber = 3, .bNumConfigurations = 1, };同时,修改字符串描述符数组,将制造商和产品名称改为“PEAK-System”和“PCAN-USB”之类的文本,以便驱动识别。
3.3 实现PCAN协议处理
这是核心。你需要在固件中实现两个方向的转换:
- CAN到USB(接收):当CAN控制器收到一帧报文时,中断服务程序会将其存入缓冲区。你需要编写一个函数,从缓冲区取出CAN帧,按照PCAN-USB的报文头格式(包括ID、DLC、时间戳、帧类型)进行封装,然后通过USB端点发送出去。
- USB到CAN(发送):当主机软件(如PCAN-View)通过USB下发指令时,固件需要解析这些数据。PCAN协议通常有特定的命令字,比如“发送CAN帧”、“设置波特率”、“打开/关闭通道”等。你需要解析这些命令,提取出CAN ID和数据,然后调用STM32的CAN控制器发送函数将其送到总线上。
一个简化的协议处理函数框架可能如下:
// 处理从USB接收到的数据 void pcan_process_usb_rx(uint8_t* data, uint32_t len) { if (len < PCAN_MSG_HEADER_SIZE) return; pcan_msg_header_t* header = (pcan_msg_header_t*)data; switch(header->command) { case PCAN_CMD_SET_BITRATE: // 解析波特率参数,调用 can_set_bitrate() break; case PCAN_CMD_MESSAGE: // 解析CAN消息,调用 can_transmit() break; case PCAN_CMD_OPEN: // 打开CAN通道,调用 can_init() break; // ... 其他命令 default: break; } } // 将接收到的CAN帧打包成PCAN格式并通过USB发送 void pcan_send_can_frame(can_frame_t* frame) { uint8_t usb_buffer[64]; pcan_msg_header_t* header = (pcan_msg_header_t*)usb_buffer; header->command = PCAN_CMD_MESSAGE; header->id = frame->id; header->dlc = frame->dlc; header->flags = (frame->ide) ? PCAN_FLAG_EXTENDED : 0; // 设置扩展帧标志 // ... 填充时间戳等 memcpy(usb_buffer + sizeof(pcan_msg_header_t), frame->data, frame->dlc); usb_send_data(usb_buffer, sizeof(pcan_msg_header_t) + frame->dlc); }你需要根据找到的开源协议定义,精确实现这些数据结构和对齐方式。一个字节的顺序错误都可能导致通信失败。
3.4 编译与烧录
代码修改完成后,进入项目根目录,使用Make工具进行编译。
make clean make如果一切顺利,会生成一个build/目录,里面包含最终的二进制文件(通常是.bin或.hex文件)。
接下来是烧录。将CANable通过USB连接到电脑,同时通过SWD调试器(如ST-LINK)连接其SWD接口(SWDIO, SWCLK, GND)。然后使用OpenOCD进行烧录:
openocd -f interface/stlink-v2.cfg -f target/stm32f0x.cfg -c "program build/candlelight_fw.bin verify reset exit 0x08000000"这条命令会擦除芯片、烧录固件、校验并复位。烧录完成后,拔掉SWD调试器,只保留USB线。
3.5 驱动安装与验证
首次插入修改后的CANable,Windows可能会提示找到新硬件“PCAN-USB”。此时你需要手动指定驱动程序路径,安装PEAK-System官方的PCAN-USB驱动。驱动安装成功后,在设备管理器中应该能看到“PEAK-System PCAN-USB”设备,而不是之前的“USB Serial Device”或“CANable”。
然后,打开PCAN-View软件。在连接设置中,选择“PCAN-USB”通道,设置与你目标CAN网络匹配的波特率,点击连接。如果一切正常,你应该能看到软件成功连接到设备,并且可以接收和发送CAN报文。
4. 深度踩坑与排错实录
这个过程绝不会一帆风顺。以下是我在实际操作中遇到的一些典型问题及其排查思路。
4.1 驱动安装失败或设备感叹号
现象:设备管理器中出现“PCAN-USB”但带有黄色感叹号,提示“设备无法启动(代码10)”或驱动未成功安装。
排查思路:
- 检查VID/PID:首先确认固件中设置的VID/PID是否与PCAN官方驱动支持的型号完全一致。可以使用USB查看工具(如
USBDeview或lsusbin Linux)查看设备枚举出的实际ID。 - 检查设备描述符:驱动可能不仅检查VID/PID,还会检查设备类(bDeviceClass)、接口描述符等。确保你的设备描述符与真实PCAN设备尽可能一致。有时需要将bDeviceClass/bDeviceSubClass/bDeviceProtocol都设置为
0xFF(厂商自定义)。 - 驱动签名:在64位Windows上,未正确签名的驱动可能被阻止加载。尝试在高级启动选项中禁用驱动程序强制签名,或者寻找经过正确签名的PCAN驱动版本。
- 固件USB栈问题:如果固件USB初始化不完整或端点配置错误,设备枚举可能不完整。检查固件中USB初始化的代码,确保所有必需的描述符都已正确配置并启用。
4.2 PCAN-View可以连接但收不到数据
现象:软件显示连接成功,波特率设置也正确,但总线上有报文时软件却看不到。
排查链路:
- 硬件连接与终端电阻:这是最容易被忽略的一步。确认CANable的CAN_H和CAN_L已正确连接到总线,并且总线上至少有两个节点(或一个节点加上一个120欧姆的终端电阻)。没有终端电阻,信号反射严重,可能导致无法通信。
- 固件波特率匹配:确认PCAN-View中设置的波特率与固件中CAN控制器初始化的波特率完全一致。常见的错误是把
500000设成了50000。 - 数据方向与端点:检查固件中USB数据发送的端点地址是否正确。接收CAN数据的函数是否被正确调用?添加调试信息,在
pcan_send_can_frame函数入口点一个LED闪烁或通过备用串口打印日志,确认CAN接收中断确实触发并进入了发送函数。 - PCAN协议封装错误:这是最可能的原因。使用USB协议分析软件(如Wireshark配合USBPcap)抓取真实PCAN-USB设备与电脑的通信数据。对比你的固件发出的数据包和真实设备的数据包。重点检查:
- 报文头长度和结构:每个字段的字节顺序(大小端)是否正确?
- 时间戳字段:PCAN协议中的时间戳是微秒还是毫秒?是否需要从系统时钟换算?
- 标志位(Flags):标准帧、扩展帧、远程帧的标志位设置是否正确?
- 数据对齐:整个消息结构体是否使用了
#pragma pack(1)或__attribute__((packed))来取消字节对齐?否则结构体中间可能会有编译器插入的填充字节,导致格式错乱。
4.3 可以接收但不能发送
现象:能收到总线报文,但通过PCAN-View发送的报文无法出现在总线上。
排查思路:
- USB接收解析:首先确认固件是否正确收到了来自主机的数据。同样用调试信息或日志,检查
pcan_process_usb_rx函数是否被调用。 - 命令解析:检查对PCAN发送命令(如
PCAN_CMD_MESSAGE)的解析逻辑。是否正确提取出了CAN ID、DLC和数据? - CAN控制器发送函数:确认调用的
can_transmit函数本身是工作正常的。可以写一个简单的测试函数,在固件启动后自动发送一帧固定的报文,来验证CAN发送功能本身是否完好。 - 发送邮箱状态:STM32的CAN控制器发送邮箱是有限的。如果连续快速发送,可能因为邮箱满而导致发送失败。检查发送函数的返回值,必要时实现简单的发送队列。
4.4 性能与稳定性优化
当基本功能调通后,你可能会遇到性能瓶颈或稳定性问题。
- USB传输延迟与丢帧:在高速CAN(如1Mbps)下,报文可能非常密集。原版固件的USB传输可能采用查询方式,效率低。优化方法是:
- 确保CAN接收中断服务程序(ISR)尽量短,只做将CAN帧存入环形缓冲区的操作。
- 在主循环中,检查缓冲区并有数据时,尽快打包并通过USB批量传输(Bulk Transfer)发送。批量传输具有更高的带宽和优先级。
- 适当增大USB发送缓冲区和CAN接收环形缓冲区的大小。
- 时间戳精度:PCAN协议的高精度时间戳是其特色之一。你需要一个高精度的时钟源(如STM32的SysTick或定时器)来提供微秒级的时间戳。注意处理定时器溢出问题。
- 错误帧与状态报告:真实的PCAN设备会报告总线错误、警告状态、负载率等信息。完善你的固件,使其能够响应PCAN-View查询状态和错误计数的命令,提升兼容性和专业性。
5. 进阶应用与生态整合
成功实现PCA后,你的CANable就拥有了更广阔的应用场景。
兼容更多软件:除了PCAN-View,现在你可以使用所有支持PCAN接口的软件,如:
- CANoe(通过PCAN-USB接口卡选项)
- Python-can库:使用
python-can的pcan后端,编写自动化测试脚本。 - CAN-utils工具链(Linux):安装
peak-linux-driver后,可以使用candump,cansend等命令。 - SavvyCAN:这款强大的开源CAN分析仪也支持PCAN接口。
集成到自定义上位机:你可以使用PCAN官方提供的PCAN-Basic API(Windows/Linux)来开发自己的上位机程序。这个API稳定且功能丰富,现在你的低成本硬件也能享受同等待遇。
多协议支持拓展:基于这个框架,你可以进一步修改固件,使其不仅模拟PCAN,还能通过USB命令切换为兼容其他流行适配器(如Kvaser, Vector)的模式,成为一个真正的“多协议USB-CAN”适配器。
整个项目从想法到实现,是一个对USB协议、CAN总线、嵌入式固件开发都有深入实践的绝佳学习过程。它教会你的不仅仅是让一个设备兼容另一个设备,更是如何通过软件定义硬件接口的灵活性。当你用自己改造的CANable成功在专业的软件环境中收发报文时,那种成就感远超直接购买一个成品。当然,整个过程需要耐心、细致的调试和对协议的深刻理解,但最终的回报——一个高性价比且完全受控的开发工具——绝对是值得的。