4、CAN总线通信基础:CAN协议帧结构、CAN控制器驱动配置、使用SocketCAN进行收发

各位同学,咱们今天聊聊CAN总线。说实话,在座舱系统里,CAN总线就像人的神经系统。MTK8676这颗芯片要跟车身各个ECU(电子控制单元)打交道,靠的就是它。我当年第一次调CAN驱动时,被那个帧结构搞得晕头转向,后来才发现,搞懂了帧结构,后面的事情就顺了。

4.1 CAN协议帧结构:标准帧与扩展帧

CAN总线上的数据,是以“帧”为单位传输的。说白了,就像寄快递,你得有个包裹,包裹上写清楚地址、内容、校验信息。CAN帧也一样。

咱们最常用的有两种帧:标准帧扩展帧。它们的核心区别,就是ID(标识符)的长度不同。

特性 标准帧 (CAN 2.0A) 扩展帧 (CAN 2.0B)
ID长度 11位 29位
最大节点数 2048个 5亿多个
仲裁场结构 ID + RTR + IDE + r0 ID + SRR + IDE + 扩展ID + RTR + r1 + r0
应用场景 传统车身控制(车窗、灯光) 诊断、多节点复杂网络

标准帧的ID只有11位,范围是0x000到0x7FF。在早期的车上,这个数量够用了。但现在的车,一个座舱域控制器要跟几十个ECU通信,11位ID就显得捉襟见肘。所以就有了扩展帧,ID扩展到29位,范围是0x00000000到0x1FFFFFFF。

关键点:IDE位(Identifier Extension)决定了这是标准帧还是扩展帧。IDE=0是标准帧,IDE=1是扩展帧。这个位在仲裁场里,接收方通过它来区分帧类型。

我个人的习惯是,在座舱项目中,优先使用扩展帧。为什么?因为MTK8676作为中央网关,需要处理大量不同优先级的消息。29位ID可以灵活地分配优先级,比如高优先级的事件(碰撞预警)用低数值ID,低优先级的(空调温度)用高数值ID。这样在总线仲裁时,高优先级消息总能抢到总线。

4.2 CAN控制器驱动配置

搞清楚了帧结构,接下来就是让硬件干活了。MTK8676内部集成了CAN控制器,我们需要配置它。嗯,这里要注意,不同芯片的寄存器地址可能不一样,但配置思路是通用的。

配置CAN控制器,核心就三步:

  1. 模式设置:把控制器从初始化模式切换到正常模式。
  2. 波特率配置:设置通信速率,座舱系统常用500kbps或1Mbps。
  3. 中断使能:打开接收中断和错误中断。

我曾经在一个项目里,因为波特率没配准,导致CAN总线一直报错。排查了两天,最后发现是晶振频率和分频系数算错了。所以,我建议你配置波特率时,一定要用示波器量一下CAN_H和CAN_L的差分信号,确认位时间对不对。

下面是一个典型的CAN控制器初始化代码片段(以Linux内核驱动为例):

/* CAN控制器初始化 */
static int can_init(struct can_priv *priv)
{
    /* 1. 请求时钟并使能 */
    clk_prepare_enable(priv->clk);
    
    /* 2. 复位控制器,进入初始化模式 */
    writel(0x01, priv->base + CAN_CTRL_REG);
    udelay(10);
    
    /* 3. 配置波特率:假设系统时钟100MHz,目标500kbps */
    /* 位时间 = 1/500k = 2000ns */
    /* 假设采样点设置在75%,则TSEG1=15, TSEG2=4, SJW=1 */
    u32 btr = (15 << 16) | (4 << 20) | (1 << 24);
    writel(btr, priv->base + CAN_BTR_REG);
    
    /* 4. 配置中断掩码:使能接收中断和错误中断 */
    writel(INT_RX | INT_ERR, priv->base + CAN_INT_MASK_REG);
    
    /* 5. 退出初始化模式,进入正常模式 */
    writel(0x02, priv->base + CAN_CTRL_REG);
    
    return 0;
}

小技巧:配置完波特率后,可以用CAN分析仪发一个标准帧,看看控制器能不能正确接收。如果收不到,先检查CAN_H和CAN_L有没有接反,再检查终端电阻(120Ω)有没有焊上。这两个坑,我当年都踩过。

4.3 使用SocketCAN进行收发

驱动配好了,怎么在应用层收发数据呢?在Linux系统上,最优雅的方式就是SocketCAN。它把CAN接口抽象成了一个网络设备,你可以像操作TCP/IP socket一样操作CAN。

说白了,SocketCAN就是一套API,让你不用直接操作寄存器,而是通过socket来收发CAN帧。MTK8676跑的是Android/Linux系统,所以SocketCAN是标配。

4.3.1 初始化SocketCAN

第一步,把CAN接口启动起来。在命令行里,通常这样操作:

# 设置CAN接口为1Mbps
ip link set can0 type can bitrate 1000000
# 启动CAN接口
ip link set can0 up

在代码里,我们通过socket()和ioctl()来实现:

#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>

int can_socket_init(const char *ifname)
{
    int s;
    struct sockaddr_can addr;
    struct ifreq ifr;
    
    /* 1. 创建原始CAN socket */
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (s < 0) {
        perror("socket");
        return -1;
    }
    
    /* 2. 获取接口索引 */
    strcpy(ifr.ifr_name, ifname);
    ioctl(s, SIOCGIFINDEX, &ifr);
    
    /* 3. 绑定socket到CAN接口 */
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    bind(s, (struct sockaddr *)&addr, sizeof(addr));
    
    return s;
}

4.3.2 发送CAN帧

发送帧时,我们需要填充一个struct can_frame结构体。这个结构体里,can_id就是帧ID,can_dlc是数据长度(0-8字节)。

/* 发送一个扩展帧 */
int can_send_frame(int s, uint32_t id, uint8_t *data, uint8_t len)
{
    struct can_frame frame;
    int ret;
    
    /* 填充帧结构 */
    frame.can_id = id | CAN_EFF_FLAG;  /* 设置扩展帧标志 */
    frame.can_dlc = len;
    memcpy(frame.data, data, len);
    
    /* 发送 */
    ret = write(s, &frame, sizeof(frame));
    if (ret != sizeof(frame)) {
        perror("write");
        return -1;
    }
    
    return 0;
}

注意:发送标准帧时,不要加CAN_EFF_FLAG标志。如果加了,接收方会把它当成扩展帧来处理,导致ID解析错误。我曾经在调试时,因为忘记清除这个标志,导致两个ECU之间通信一直对不上,查了整整一个下午。

4.3.3 接收CAN帧

接收帧就更简单了,用read()阻塞读取就行。实际项目中,我一般会开一个独立的线程来循环读取:

/* 接收线程 */
void *can_receive_thread(void *arg)
{
    int s = *(int *)arg;
    struct can_frame frame;
    int ret;
    
    while (1) {
        /* 阻塞读取 */
        ret = read(s, &frame, sizeof(frame));
        if (ret < 0) {
            perror("read");
            break;
        }
        
        /* 判断帧类型 */
        if (frame.can_id & CAN_EFF_FLAG) {
            /* 扩展帧:去掉标志位,获取真实ID */
            uint32_t real_id = frame.can_id & CAN_EFF_MASK;
            printf("收到扩展帧: ID=0x%08X, 数据=", real_id);
        } else {
            /* 标准帧 */
            uint32_t real_id = frame.can_id & CAN_SFF_MASK;
            printf("收到标准帧: ID=0x%03X, 数据=", real_id);
        }
        
        /* 打印数据 */
        for (int i = 0; i < frame.can_dlc; i++) {
            printf("%02X ", frame.data[i]);
        }
        printf("\n");
    }
    
    return NULL;
}

4.4 避坑指南与实战经验

最后,我分享几个实战中容易踩的坑:

  • 终端电阻:CAN总线两端必须各有一个120Ω电阻。少一个,信号反射会导致通信不稳定。我见过有人忘记焊电阻,结果总线偶尔丢帧,排查起来特别痛苦。
  • ID冲突:同一个CAN网络上,不能有两个节点发送相同ID的帧。否则总线仲裁会乱套。建议在项目初期就规划好ID分配表。
  • 数据长度:CAN帧最多8字节数据。如果需要传输超过8字节,就得拆分成多帧发送,或者使用CAN FD(灵活数据速率)。MTK8676支持CAN FD,但那是后面章节的内容了。
  • 错误处理:SocketCAN的read()返回-1时,不一定是网络断了,也可能是总线错误。建议检查errno,如果是ENETDOWN,说明CAN接口被down掉了,需要重新初始化。

好了,这一章的内容就到这里。CAN总线看起来简单,但真正用好它,需要你对帧结构、驱动配置和SocketAPI都有深入理解。下一章,我们会讲CAN FD,那才是真正发挥MTK8676性能的地方。