第3章:CAN报文收发实战:使用PCAN-USB设备、配置SocketCAN接口、发送标准帧与扩展帧

好,咱们进入实战环节了。前面两章讲了CAN的基础理论和报文结构,说实话,那些东西光看是记不住的。你得亲手发一条报文出去,再看着它回来,才能真正理解CAN总线是怎么回事。

这一章,我带你走一遍完整的流程:从硬件连接,到驱动安装,再到用SocketCAN发标准帧和扩展帧。嗯,都是我在项目里反复折腾过的经验。

3.1 硬件准备:PCAN-USB设备

PCAN-USB是PEAK-System公司的一款经典工具。说白了,它就是一个USB转CAN的适配器。你把它插到电脑上,另一头接CAN总线,电脑就能收发CAN报文了。

我个人习惯用PCAN-USB FD版本,兼容经典CAN和CAN FD。但如果你手头只有普通版,也完全够用。标准CAN 2.0的帧,它都能搞定。

小提示: 买PCAN-USB时注意看接口类型。有的是DB9,有的是OpenCable。DB9的接线定义是:Pin2为CAN_L,Pin7为CAN_H,Pin3为GND。别接反了,我见过有人把CAN_H和CAN_L接反,折腾了半天才发现。

3.2 驱动安装与验证

驱动安装其实很简单。去PEAK官网下载对应系统的驱动包。Windows下直接运行安装程序,Linux下需要编译内核模块。

我记得第一次在Ubuntu上装驱动时,踩了个坑——没装内核头文件。编译报错说找不到linux/can.h。后来发现要先装linux-headers-$(uname -r)这个包。

装好驱动后,怎么验证?插上设备,看系统能不能识别。

Windows下: 打开设备管理器,看到「PCAN-USB」出现在端口或网络适配器下,就对了。

Linux下: 运行lsusb,能看到类似这样的输出:

Bus 001 Device 003: ID 0c72:000c PEAK System PCAN-USB

如果看不到,检查USB线或者换个USB口。我遇到过USB口供电不足导致设备无法识别的情况,换到机箱后面的USB口就好了。

3.3 配置SocketCAN接口

SocketCAN是Linux内核自带的CAN协议栈。它把CAN设备抽象成网络接口,你可以用ip命令来配置它。说实话,这比Windows下用第三方API方便多了。

配置步骤很简单,就三条命令:

# 加载驱动模块
sudo modprobe peak_usb

# 设置CAN接口(这里假设设备是can0)
sudo ip link set can0 type can bitrate 500000

# 启动接口
sudo ip link set can0 up

等等,为什么是500k?这是最常见的CAN总线波特率。整车厂和工业设备大多用这个。你想想看,如果两边的波特率对不上,那报文根本收不到。

启动成功后,用ip -details link show can0查看状态:

3: can0: <NOARP,UP,LOWER_UP> mtu 16 qdisc pfifo_fast state UP mode DEFAULT group default qlen 10
    link/can  promiscuity 0 minmtu 0 maxmtu 0
    can state ERROR-ACTIVE restart-ms 0
    bitrate 500000 sample-point 0.875
    tq 125 prop-seg 6 phase-seg1 7 phase-seg2 2 sjw 1

看到state ERROR-ACTIVE,说明接口正常。如果显示state BUS-OFF,那就有问题了——总线可能短路,或者波特率不匹配。

注意: 每次拔插PCAN-USB设备后,都需要重新执行ip link set can0 up。我曾经在调试时忘了这一步,一直收不到数据,还以为是硬件坏了。

3.4 发送标准帧

标准帧的ID范围是0x000到0x7FF,共11位。发送标准帧,我一般用cansend工具,或者自己写个小程序。

方法一:用cansend命令

cansend can0 123#11223344

这条命令的意思是:往can0接口发送一条ID为0x123的标准帧,数据是0x11、0x22、0x33、0x44四个字节。

格式说明:ID#数据,数据是十六进制,两个字符一个字节。如果数据长度不够8字节,后面自动补0。

方法二:用cangen生成随机报文

cangen can0 -g 100 -I 123 -L 8 -D 1122334455667788

这个命令每100毫秒发一条ID为0x123、数据为8字节的报文。适合做压力测试。

我在项目中遇到过一个问题:用cansend发标准帧,对方ECU死活不响应。后来发现是ID写错了——标准帧ID是11位,但我写了个0x800,这已经超出范围了。嗯,标准帧ID最大就是0x7FF,超过这个值就是扩展帧了。

3.5 发送扩展帧

扩展帧的ID范围是0x00000000到0x1FFFFFFF,共29位。发送扩展帧时,需要在ID后面加一个#R标记?不对,那是远程帧。扩展帧的发送格式其实很简单:

cansend can0 00000123#11223344

看到区别了吗?ID从3位十六进制变成了8位。只要ID大于0x7FF,cansend就自动识别为扩展帧。

但有时候你需要明确指定帧类型。比如ID是0x123,但你想发扩展帧(虽然不常见)。这时候可以用##标记:

cansend can0 00000123##011223344

##后面的第一个数字是帧类型:0表示标准帧,1表示扩展帧。后面跟数据。

说实话,我很少用这种写法。大多数情况下,ID本身就能区分帧类型。但如果你在做网关开发,需要转发报文时保持帧类型不变,那就得用这个语法了。

3.6 用C语言编程发送报文

命令行工具方便归方便,但实际项目中,你得写代码。下面是一个简单的C语言示例,演示如何通过SocketCAN发送标准帧和扩展帧。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/can.h>
#include <linux/can/raw.h>

int main() {
    int s;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;

    // 创建Socket
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (s < 0) {
        perror("socket");
        return 1;
    }

    // 指定CAN接口
    strcpy(ifr.ifr_name, "can0");
    ioctl(s, SIOCGIFINDEX, &ifr);

    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    bind(s, (struct sockaddr *)&addr, sizeof(addr));

    // 发送标准帧
    memset(&frame, 0, sizeof(frame));
    frame.can_id = 0x123;          // 标准帧ID
    frame.can_dlc = 4;             // 数据长度
    frame.data[0] = 0x11;
    frame.data[1] = 0x22;
    frame.data[2] = 0x33;
    frame.data[3] = 0x44;

    if (write(s, &frame, sizeof(frame)) != sizeof(frame)) {
        perror("write");
        return 1;
    }
    printf("发送标准帧成功: ID=0x%03X, 数据=0x%02X%02X%02X%02X\n",
           frame.can_id, frame.data[0], frame.data[1],
           frame.data[2], frame.data[3]);

    // 发送扩展帧
    memset(&frame, 0, sizeof(frame));
    frame.can_id = CAN_EFF_FLAG | 0x00000123;  // 扩展帧ID,必须加EFF_FLAG
    frame.can_dlc = 8;
    frame.data[0] = 0xAA;
    frame.data[1] = 0xBB;
    frame.data[2] = 0xCC;
    frame.data[3] = 0xDD;
    frame.data[4] = 0xEE;
    frame.data[5] = 0xFF;
    frame.data[6] = 0x00;
    frame.data[7] = 0x11;

    if (write(s, &frame, sizeof(frame)) != sizeof(frame)) {
        perror("write");
        return 1;
    }
    printf("发送扩展帧成功: ID=0x%08X, 数据长度=%d\n",
           frame.can_id & CAN_EFF_MASK, frame.can_dlc);

    close(s);
    return 0;
}

编译命令:

gcc -o can_send can_send.c
sudo ./can_send

这里有个关键点:发送扩展帧时,can_id必须与CAN_EFF_FLAG进行或运算。如果不加这个标志位,内核会把它当成标准帧处理。我刚开始写代码时就忘了这个,发出去的扩展帧ID全乱了。

3.7 接收报文验证

发出去的消息对不对?用candump抓一下看看:

candump can0

你会看到类似这样的输出:

  can0  123   [4]  11 22 33 44
  can0  00000123   [8]  AA BB CC DD EE FF 00 11

第一行是标准帧,第二行是扩展帧。注意看,扩展帧的ID显示为8位十六进制,前面补了0。

如果你用两个PCAN-USB设备,一个发一个收,那效果更直观。我曾经在实验室里搭过一套环境:一个设备模拟ECU发送报文,另一个设备模拟网关接收。这样能完整验证整个通信链路。

3.8 常见问题与避坑指南

实战中总会遇到各种问题。我整理了几个高频坑:

问题现象 可能原因 解决办法
cansend报错:Cannot send: Network is down CAN接口未启动 执行sudo ip link set can0 up
candump收不到任何报文 波特率不匹配或总线未连接终端电阻 检查波特率设置,在总线两端各加120Ω电阻
发送扩展帧后,收到的ID不对 代码中未设置CAN_EFF_FLAG 检查can_id是否与CAN_EFF_FLAG或运算
设备插入后lsusb看不到 USB驱动未加载或供电不足 执行sudo modprobe peak_usb,换USB口
避坑指南: 我曾经在项目现场调试时,发现CAN总线一直报错。折腾了两个小时,最后发现是终端电阻没接。CAN总线两端必须各接一个120Ω电阻,否则信号反射会导致通信不稳定。嗯,这个坑我踩过,你别再踩了。

3.9 本章小结

这一章的内容其实就三件事:把PCAN-USB插上、配好SocketCAN接口、发一条报文出去。但就是这三件事,能把理论和实践串起来。

你想想看,从硬件连接到驱动安装,从命令行到C代码,每一步都有坑。但只要你亲手走一遍,CAN报文收发对你来说就不再是黑盒了。

下一章,我们会深入CAN报文的过滤机制。为什么有时候你抓到的报文全是0x7FF?怎么只收你关心的ID?到时候我教你用硬件过滤器和软件过滤器两种方法。

好,今天就到这里。动手试试吧,有问题随时翻回这一章。