GitHub: CAN

Email: BuddyZhang1 buddy.zhang@aliyun.com

目录

  1. CAN 原理

  2. Kernel 内核源码中使用 CAN

  3. 用户空间源码使用 CAN

  4. 用户空间工具使用 CAN

  5. CAN 测试

  6. 附录


CAN 原理

  1. CAN 总线协议

  2. CAN 数据格式

CAN总线协议

CAN 是 Controller Area Network(控制器局域网)的缩写。CAN 通信协议在 1986 年由 德国电气商博世公司所开发,主要面向汽车的通信系统。现已是 ISO 国际标准化的串行 通信协议。根据不同的距离、不同的网络,可配置不同的速度,最高速度为1MBit/s。CAN 被细分为三个层次:

  1. CAN对象层(the object layer)

  2. CAN传输层(the transfer layer)

  3. CAN物理层(the phyical layer)

对象层和传输层包括所有由 ISO/OSI 模型定义的数据链路层的服务和功能。对象层的作 用范围包括:

  1. 查找被发送的报文。

  2. 确定由实际要使用的传输层接收哪一个报文。

  3. 为应用层相关硬件提供接口。

传输层的作用主要:

  1. 传送规则,也就是控制帧结构、执行仲裁、错误检测、出错标定、故障界定。

  2. 总线上什么时候开始发送新报文及什么时候开始接收报文,均在传输层里确定。

  3. 位定时的一些普通功能也可以看作是传输层的一部分。

  4. 传输层的修改是受到限制的。

物理层的作用:

在不同节点之间根据所有的电气属性进行位信息的实际传输。当然,同一网络内,物理层 对于所有的节点必须是相同的。尽管如此,在选择物理层方面还是很自由的。

CAN具有以下的属性:

  1. 报文(Messages):简单来说就是具有固定格式的数据包。

  2. 信息路由(Information Routing):即,报文寻找结点的方式。

  3. 位速率(Bit rate):数据位的传输速度。

  4. 优先权(Priorities):即报文发送的优先权。

  5. 远程数据请求(Remote Data Request):通过发送远程帧,需要数据的节点可以请 求另一节点发送相应的数据帧。

  6. 多主机(Multimaster):总线空闲时,任何结点都可以开始传送报文。

  7. 仲裁(Arbitration):当2个及以上的单元同时开始传送报文,那么就会有总线访 问冲突。仲裁是确定哪个单元的具有发送优先权。

  8. 安全性(Safety):CAN的每一个节点均采取了强有力的措施以进行错误检测、错误 标定及错误自检。
  9. 错误检测(Error Detection):包括监视、循环冗余检查、位填充、报文格式检查。

  10. 错误检测的执行(Performance of Error Detection)

  11. 错误标定和恢复时间(Error Sinalling and Recovery Time):任何检测到错误 的结点会标志出已损坏的报文。此报文会失效并将自动地开始重新传送。如果不再 出现新的错误,从检测到错误到下一报文的传送开始为止,恢复时间最多为29个位 的时间。

  12. 故障界定(Fault Confinement):CAN结点能够把永久故障和短暂扰动区分开来。 永久故障的结点会被关闭。
  13. 连接(Connections):CAN串行通讯链路是可以连接许多结点的总线。理论上,可 连接无数多的结点。但由于实际上受延迟时间或者总线线路上电气负载的影响,连 接结点的数量是有限的。

  14. 单通道(Single Channel):总线是由单一进行双向位信号传送的通道组成。

  15. 总线值(Bus value):总线可以具有两种互补的逻辑值之一:“显性”(可表示为 逻辑0)或“隐性”(可表示为逻辑1)。
  16. 应答(Acknowledgment):所有的接收器检查报文的连贯性。对于连贯的报文,接 收器应答;对于不连贯的报文,接收器作出标志。

  17. 睡眠模式/唤醒(Sleep Mode / Wake-up):为了减少系统电源的功率消耗,可以 将CAN器件设为睡眠模式以便停止内部活动及断开与总线驱动器的连接。CAN器件可 由总线激活,或系统内部状态而被唤醒。

CAN总线的报文格式

CAN传输的报文,可分为五种类型:

  1. 数据帧:用于发送结点向接收结点传送数据的帧。

  2. 远程帧:总线结点发出远程帧,请求发送具有同一识别符的数据帧。

  3. 错误帧:任何结点检测到一总线错误就发出错误帧。

  4. 过载帧:过载帧用以在先行的和后续的数据帧(或远程帧)之间提供一附加的延时。

  5. 帧间隔:用于将数据帧及远程帧与前面的帧分离开来的帧。

数据帧由 7 个不同的位场组成:

CAN

错误帧用于在接收和发送消息时检测出错误,通知错误的帧。错误帧由错误标志和错误界 定符构成。错误标志包括主动错误标志和被动错误标志两种。

  1. 主动错误标志:6个位的显性位,处于主动错误状态的单元检测出错误时输出的错 误标志。

  2. 被动错误标志:6个位的隐性位,处于被动错误状态的单元检测出错误时输出的错 误标志。

错误界定符由8个位的隐性位构成。错误帧格式如下表示:

Menuconfig1

过载帧是用于接收单元通知其尚未完成接收准备的帧。过载帧由过载标志和过载界定符构 成。过载帧格式如下表示:

Menuconfig1

帧间隔是用于分隔数据帧和远程帧的帧。数据帧和远程帧可通过插入帧间隔将本帧与前面 的任何帧(数据帧、远程帧、错误帧、过载帧)分开。过载帧和错误帧前不能插入帧间隔。 帧间隔如下图所示:

Menuconfig1

CAN总线的仲裁方式

在总线空闲态,最先开始发送消息的单元获得发送权。多个单元同时开始发送时,各发送 单元从仲裁段的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送。即逐位 地对比各个结点发出的报文ID。由于线与的关系,显示位“0”可以覆盖隐性位“1”,因此ID 最小的节点赢得仲裁,总线上表现为该结点的报文,其他结点失去仲裁,退出发送,转为 接收状态。标准格式ID与具有相同ID的远程帧或者扩展格式的数据帧在总线上竞争时,标 准格式的RTR位为显性位的具有优先权,可继续发送。

Menuconfig1

位填充(BitStuffing)

位填充是为了防止突发错误而设定的功能。位填充的规则如下:

  1. 5位连续相同电平之后,必须填充一位反向位,即不允许有6个连续相同位

  2. SOF之前为总线空闲状态,不需要同步,因此不需要位填充

  3. CRC之后为固定格式,不允许填充

  4. 由CAN控制器自动实现

CAN的错误处理

CAN控制器检测错误共有以下5种:

  1. 位填充错误 在使用位填充的帧场内,结点如果检测到6个连续相同的位值,则产生位填充错误, 在下一位开始时,该结点将发送一个错误帧。

  2. 位错误 在发送期间,结点检测到总线的位值与自身发送的位值不一致时,则产生位错误, 在下一位开始时,该结点将发送一个错误帧。

  3. CRC错误 接收结点计算的CRC码与数据帧本身自带的CRC码不一致,接收结点将丢弃该帧,并 在ACK界定符之后发送一个错误帧。

  4. 应答错误 发送结点在ACK Slot位会发送隐性位,同时监听总线是否为显性位,如果是显性 位,则表明至少一个节点正确收到该帧;如果是隐性位,将产生ACK错误,发送结点 发送一个错误帧。

  5. 格式错误 发送结节在(CRC界定符、ACK界定符、帧结束EOF)固定格式的位置检测到显性位 时,将发生格式错误,并发送一个错误帧。

CAN总线同步

CAN 总线的通信方式为 NRZ 方式。各个位的开关或者结尾都没有附加同步信号。发送结 点以与位时序同步的方式开始发送数据。另外,接收结点根据总线上电平的变化进行同步 并进行接收工作。但是,发送结点和接收结点存在的时钟频率误差及传输路径上的(电缆、 驱动器等)相位延迟会引进同步偏差。因此接收结点需要通过同步的方式调整时序进行接 收。同步的作用是尽量使本地位时序与总结信号的位时序一致(本地同步段与总结信号边 沿同步)。只有接收结点需要同步;同步只会发生在隐性到显性电平的跳沿。同步的方式 为硬件同步和再同步。

CAN 采样周期

标准数据帧

Menuconfig1

SOF: can 帧以一个 StartOf-Frame(SOF) 位开始,该位是一个显性的状态和允许硬件同 步所以的节点。

  1. Arbitration Field: 开始位之后是 12 位的仲裁域(arbitration field),其由 11 位的 ID 识别和远程传输帧请求位 RTR (Remote Transmission Request)。RTR 位用于区分帧是远程帧与否。

  2. Control Field: 接下来的仲裁域是控制域,包含 6 个位,其中的第一位是拓展识 别位 (Identifier Extension IDE), 该位显性指明是否是标准帧还是拓展帧。

  3. Control Field: RB0 位预留给 CAN 协议,保持为 0

  4. Control Field: 控制位中剩余的 4 位是 DLC 域,该域用于指定 CAN 数据的长度, 支持 0-8 位

  5. Data Field: 接下来的是数据域

  6. CRC: 接下来的 16 位是冗长检测,其中包括 15 位的 CRC 序列,第 16 位为 CRC 分隔符

  7. ACK field: 最后一个域包含了 2 位的应答域。在 ACK Slot bit 期间,传输节点 发送一个接收位。任何接收到一个 error-frame Frame 来应答是否正确的收到数据

拓展数据帧

Menuconfig1

远程帧

Menuconfig1

错误帧

Menuconfig1

Overload 帧

Menuconfig1

BIT TIMING 调试

CAN BUS 进行采样时,每采一次所花的时间如下:

Menuconfig1

因此,采样周期 = SyncSeg + PropSeg + PhaseSeg1 + PhaseSeg2 = 同步段 + 传播时间 段 + 相位缓存段1 + 相位缓存段2。 根据 MCP2515 的介绍,名义上的 BIT rate (NBR) 的计算公式为

Menuconfig1

从上面的 tbit 计算周期我们知道:

Menuconfig1

CAN 总线的波特率由不重叠的段组成,如上面分析。每个这样的段由完整的单元组成,每 个单元称为 Time Quanta(TQ)。对于这些段,我们一一分析

  1. 同步段 SyncSeg:

    同步段是 NBT 中的第一个段,其用于同步总线上的节点。其期望 bit 的触发发生 在 SyncSeg 内, SyncSeg 固定在一个 TQ 内

  2. 广播段 PropSeg:

    广播段的存在是为了抵消两个节点之间的物理延时。广播延时定义为广播段的两倍。> 一般其值为 1-8 TQ

  3. 象限缓存段1,2

    两个象限缓存 PS1 和 PS2 用于抵消采样时触发错误,PS1 可改变为 1-8TQ,PS2 可以改变为 2-8TQ

  4. 采样点 Sample Point

    采样点就是逻辑电平的读取和翻译,采样点位于 PS1 的末尾

  5. IPT

    The Information Processing Time。就是读取到一个逻辑电平的时间,IPT 开始于 采样点,固定长度为 2TQ

  6. SJW

    The Synchronization Jump Width.

  7. TQ

    Time Quatum, 每个段都是由一个个完整的 TQ 组成,其长度基于 tOSC( oscillator period).基础 TQ 为 2 倍外部晶振周期。TQ 长度等于一个 TQ 时钟 周期(tBRPCL), 其与一个参数 BRP 有关,其计算方法如下:

时钟周期图如下:

Menuconfig1


Kernel 中源码使用 CAN

源码 github:https://github.com/BiscuitOS/HardStack/tree/master/bus/CAN/kernel

为了能在 linux 内核中通过源码访问 CAN,需要执行以下两个步骤

  1. 配置内核,打开 CAN 支持

  2. 添加并编译源码

配置内核,打开 CAN 支持

为了在 Linux 中使用 CAN,开发者应该在编译内核的时候,打开 CAN 子系统,具体步骤 可以参考如下:

在源码树下,使用命令进行内核编译

make menuconfig ARCH=arm64

Menuconfig1

选择 Networking support –> 选项

Menuconfig1

选择 CAN bus subsystem support —>

Menuconfig1

选择一下选项,并进入 CAN Device Drivers —>

<*> Raw CAN protocol (raw access with CAN-ID filtering)
<*> Broadcast Manager CAN Protocol (with content filtering)
<*> CAN Gateway/Route (with netlink configuration)

Menuconfig1

最后选择 CAN bit-timing calculation. 保存退出之后,查看 .config 中是否包含 如下宏:

CONFIG_CAN=y
CONFIG_CAN_RAW=y
CONFIG_CAN_BCM=y
CONFIG_CAN_GW=y
CONFIG_CAN_DEV=y
CONFIG_CAN_CALC_BITTIMING=y

添加并编译源码

总共有两个驱动: can.c 和 can_adv.c。 两个驱动都是在内核中源码如何使用 CAN 的, 其中 can.c 只涉及简单的 CAN 设备注册,而 can_adv.c 则包含了 CAN 设备的数据收发 方法等。开发者可以根据需求自行参考。

can.c

/*
* Can demo driver
*
* (C) 2018.12.18  BuddyZhang1 <buddy.zhang@aliyun.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/can/core.h>
#include <linux/can/dev.h>

#define TX_ECHO_SKB_MAX  1

static struct net_device *can_net;

/* 'ifconfig canx up' will invoke this function firstly. */
static int demo_open(struct net_device *net)
{
    /* Open can device and do some specify operation. */
    return 0;
}

/* 'ifconfig canx down' will invoke this function firstly. */
static int demo_stop(struct net_device *net)
{
    /* Close can device and do some release operation. */
    return 0;
}

static netdev_tx_t demo_hard_start_xmit(struct sk_buff *skb,
                                              struct net_device *dev)
{
    /* To do Can transfer data */
    return NETDEV_TX_OK;
}

static const struct net_device_ops demo_netdev_ops = {
    .ndo_open   = demo_open,
    .ndo_stop   = demo_stop,
    .ndo_start_xmit = demo_hard_start_xmit,
};

static int __init demo_can_device_init(void)
{
    int ret;

    /* Allocate can/net device */
    can_net = alloc_candev(sizeof(*can_net), TX_ECHO_SKB_MAX);
    if (!can_net) {
        ret = -ENOMEM;
        goto error_alloc;
     }

    can_net->netdev_ops = &demo_netdev_ops; /* necessary */

    /* register can device */
    ret = register_candev(can_net);
    if (ret) {
        ret = -EINVAL;
        goto error_register_dev;
    }
    
    return 0;

error_register_dev:
    free_candev(can_net);
error_alloc:
    return ret;
}

static void __exit demo_can_device_exit(void)
{
    /* Unregister can device */
    unregister_candev(can_dev);

    /* release can device */
    free_candev(can_net);
}

module_init(demo_can_device_init);
module_exit(demo_can_device_exit);
MODULE_LICENSE("GPL v2");

can_adv.c

/*
* Perfect Can driver
*
* (C) 2018.12.18 BuddyZhang1 <buddy.zhang@aliyun.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/can/core.h>
#include <linux/can/dev.h>

#define CAN_FRAME_MAX_DATA_LEN     8
#define DEMO_TRANSFER_BUF_LEN      (6 + CAN_FRAME_MAX_DATA_LEN)
#define TX_ECHO_SKB_MAX            1
#define DEVICE_NAME                "demo_can"

/* Can bus frequency */
#define CAN_BUS_FREQUENCY    (1*1024*1024)

static struct net_device *can_net;

/* can device private data */
struct demo_can_private_data
{
    struct can_priv can;    /* can device private data, must be first member */
    struct net_device *net; /* net device */

    /* work queue */
    struct workqueue_struct *wq;
    struct work_struct tx_work;
    struct work_struct restart_work;

    /* trans */
    struct sk_buff *tx_skb;
    int tx_len;

    /* flags */
    int force_quit;
    int after_suspend;
    int restart_tx;
};

/* Can hardware-depends bit-timing */
static struct can_bittiming_const demo_bittiming_const = {
    .name        = DEVICE_NAME,
    .tseg1_min   = 3,
    .tseg1_max   = 16,
    .tseg2_min   = 2,
    .tseg2_max   = 8,
    .sjw_max     = 4,
    .brp_min     = 1,
    .brp_max     = 64,
    .brp_inc     = 1,
};

/* parse can frame and trans */
static void demo_hw_tx(struct demo_can_private_data *priv,
       struct can_frame *frame, int tx_buf_idx)
{
    u32 sid, eid, exide, rtr;

    /* parse can frame */
    exide = (frame->can_id & CAN_EFF_FLAG) ? 1 : 0; /* Extended ID Enable */
    if (exide)
        sid = (frame->can_id & CAN_EFF_MASK) >> 18;
    else
        sid = frame->can_id & CAN_EFF_MASK; /* Standard ID */
    eid = frame->can_id & CAN_EFF_MASK;     /* Extended ID */
    rtr = (frame->can_id & CAN_RTR_FLAG) ? 1 : 0; /* Remote transmission */

    /* prase can frame done. */
}

/* tx work queue handler */
static void demo_tx_work_handler(struct work_struct *ws)
{
    struct demo_can_private_data *priv = container_of(ws,
                 struct demo_can_private_data, tx_work);
    struct net_device *net = priv->net;
    struct can_frame *frame;

    if (priv->tx_skb) {
        if (priv->can.state == CAN_STATE_BUS_OFF) {
        } else {
            frame = (struct can_frame *)priv->tx_skb->data;

            if (frame->can_dlc > CAN_FRAME_MAX_DATA_LEN)
                frame->can_dlc = CAN_FRAME_MAX_DATA_LEN;
            /* process can frame */
            demo_hw_tx(priv, frame, 0);
            priv->tx_len = 1 + frame->can_dlc;
            can_put_echo_skb(priv->tx_skb, net, 0);
            priv->tx_skb = NULL;
        }
    }
}

/* restart work queue */
static void demo_restart_work_handler(struct work_struct *ws)
{
    struct demo_can_private_data *priv =
           container_of(ws, struct demo_can_private_data, restart_work);
    struct net_device *net = priv->net;

    if (priv->after_suspend) {
        priv->after_suspend = 0;
        priv->force_quit    = 0;
    }

    if (priv->restart_tx) {
        priv->restart_tx = 0;
        netif_wake_queue(net);
    }
}

/* 'ifconfig canx up' will invoke this function firstly. */
static int demo_open(struct net_device *net)
{
    /* Open can device and do some specify operation. */
    struct demo_can_private_data *priv = netdev_priv(net);
    int ret;

    /* open can device*/
    ret = open_candev(net);
    if (ret)
        return ret;

    /* clear flags */
    priv->force_quit = 0;
    priv->tx_skb = NULL;
    priv->tx_len = 0;
    
    /* request work queue to process can frame */
    priv->wq = create_freezable_workqueue("demo_can_wq");
    INIT_WORK(&priv->tx_work, demo_tx_work_handler);
    INIT_WORK(&priv->restart_work, demo_restart_work_handler);

    /* hardware initialization */

    /* refresh can state */
    priv->can.state = CAN_STATE_ERROR_ACTIVE;
    /* wakeup net queue */
    netif_wake_queue(net);

    return ret;
}

/* 'ifconfig canx down' will invoke this function firstly. */
static int demo_stop(struct net_device *net)
{
    /* Close can device and do some release operation. */
    return 0;
}

/* To do can transfer data */
static netdev_tx_t demo_hard_start_xmit(struct sk_buff *skb,
                   struct net_device *net)
{
    struct demo_can_private_data *priv = netdev_priv(net);

    if (priv->tx_skb || priv->tx_len)
        return NETDEV_TX_BUSY;

    if (can_dropped_invalid_skb(net, skb))
        return NETDEV_TX_OK;

    netif_stop_queue(net);
    priv->tx_skb = skb;
    queue_work(priv->wq, &priv->tx_work);
    return NETDEV_TX_OK;
}

static const struct net_device_ops demo_netdev_ops = {
    .ndo_open   = demo_open,
    .ndo_stop   = demo_stop,
    .ndo_start_xmit = demo_hard_start_xmit,
};

/*
* Set can mode.
*  Can device support CAN_MODE_START, CAN_MODE_STOP and CAN_MODE_SLEEP.
* @net: net device
* @mode: can mode that be setup.
*/
static int demo_do_set_mode(struct net_device *net, enum can_mode mode)
{
    switch (mode) {
    case CAN_MODE_START:
        break;
    case CAN_MODE_STOP:
        break;
    case CAN_MODE_SLEEP:
        break;
    default:
        return -EOPNOTSUPP;
    }
    return 0;
}

static __init int demo_can_device_init(void)
{
    struct net_device *net;
    struct demo_can_private_data *priv;
    int ret;

    /* Allocate can/net device */
    net = alloc_candev(sizeof(struct demo_can_private_data), TX_ECHO_SKB_MAX);
    if (!net) {
        ret = -ENOMEM;
        goto error_alloc;
    }

    net->netdev_ops = &demo_netdev_ops; /* necessary */
    net->flags |= IFF_ECHO;

    /* get private data for can device */
    priv = netdev_priv(net);
    can_net = net;
    /* Setup can bit-timing parameters */

    /* Setup can hardware-dependent bit-timing */
    priv->can.bittiming_const = &demo_bittiming_const;
    /* Setup interface for can mode */
    priv->can.do_set_mode = demo_do_set_mode;
    /* Setup can bus frequence. */
    priv->can.clock.freq = CAN_BUS_FREQUENCY;
    /* Setup can device control mode */
    priv->can.ctrlmode_supported = CAN_CTRLMODE_3_SAMPLES |
          CAN_CTRLMODE_LOOPBACK |  CAN_CTRLMODE_LISTENONLY;
    /* callback */
    priv->net = net;

    /* register can device */
    ret = register_candev(net);
    if (ret) {
        ret = -EINVAL;
        goto error_register_dev;
    }
    
    return 0;

error_register_dev:
    free_candev(net);
error_alloc:
    return ret;
}

static void __exit demo_can_device_exit(void)
{
    /* Unregister can device */
    unregister_candev(can_net);

    /* release can device */
    free_candev(can_net);
}

module_init(demo_can_device_init);
module_exit(demo_can_device_exit);
MODULE_LICENSE("GPL");

Makefile

obj-m += can.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

PWD       := $(shell pwd)

ROOT := $(dir $(M))
DEMOINCLUDE := -I$(ROOT)../include -I$(ROOT)/include

GCCVERSION = $(shell gcc -dumpversion | sed -e 's/\.\([0-9][0-9]\)/\1/g' -e 's/\.\([0-9]\)/0\1/g' -e 's/^[0-9]\{3,4\}$$/&00/')

GCC49 := $(shell expr $(GCCVERSION) \>= 40900)

all:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

install: all
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
        depmod -a

clean:
        rm -rf *.o *.o.d *~ core .depend .*.cmd *.ko *.ko.unsigned *.mod.c .tmp_versions *.symvers \
        .cache.mk *.save *.bak Modules.* modules.order Module.markers *.bin

CFLAGS_can.o := -Wall $(DEMOINCLUDE)

ifeq ($(GCC49),1)
        CFLAGS_can.o += -Wno-error=date-time
endif

CFLAGS_can.o := $(DEMOINCLUDE)

CAN 驱动使用

开发者可以将上述驱动添加到内核源码树中进行编译,或者使用上述的 Makefile 进行外 部编译。当编译成模块之后,可以使用如下命令加如到运行的内核中:

sudo insmod can.ko

当添加完模块或 CAN 驱动已经被编译到内核之后,可以使用 net-tools 工具查看 CAN 设备是否添加成功,如下:

root@BiscuitOS:~# ifconfig -a

Menuconfig1


用户空间访问 CAN

用户空间要访问 CAN 设备,必须经过两个步骤,分别如下:

  1. 配置 CAN 设备

  2. 通过工具或源码访问 CAN 设备

配置 CAN 设备

在使用 CAN 设备之前,需要对 CAN 设备进行配置,主要配置 CAN 的波特率和 bus-off 恢复时间等参数。开发者可以使用 ip 命令或 can-tools 进行配置。ip 工具需要增强版 的 ip-route2 支持,如果内核中不包含了这个工具,可以在 busybox 中打开这个工具。 开发者可以使用 can-tools 工具集进行 CAN 配置,以上两个工具的移植和使用在下文中 将详细介绍。

ip-route2 的配置

开发者如何源码树中包含了 Busyox 的源码,可以参照如下步骤进行 ip-route2 工具的 配置。首先,在 Busybox 源码中,使用命令打开图形化配置窗口,使用如下命令:

cd Busybox
make menuconfig

Menuconfig1

进入 Network —>

Menuconfig1

进入 Routing and Redirection —>

Menuconfig1

勾选 ip-bridge 工具

can-tools 工具配置

如果开发者的编译源码树中不包含 Busybox 源码,可以使用 can-tools。can-tools 的 移植需要两个源码包,github 位置如下:

https://github.com/BiscuitOS/HardStack/tree/master/bus/CAN/user/tools

开发者可以通过下面命令下载源码也可以自行下载:

cd /tmp/
mkdir CAN
cd CAN
git init
git remote add -f  origin https://github.com/BiscuitOS/HardStack.git
git config core.sparsecheckout true
echo "CAN" >> .git/info/sparse-checkout
git pull origin master
cd bus/CAN/user/tools/

通过上面的命令将获得两个源码压缩包: canutils.tar.gzlibsocketcan.tar.gz

由于 canutils 依赖于 libsocketcan 库,所以首先编译 libsocketcan,请开发者参考 如下命令进行编译,

tar -xvjf libsocketcan.tar.gz
cd libsocketcan/
./configure --host=arm-openwrt-linux --prefix=/tmp/CAN/libsocketcan/output
make
make install

接下来编译 canutils 工具集合,请参考如下命令

tar -xvjf canutils.tar.gz
cd canutils/
./configure --host=arm-openwrt-linux --prefix=/tmp/CAN/output libsocketcan_LDFLAGS=-L/tmp/CAN/libsocketcan/output/lib libsocketcan_LIBS=-lsocketcan libsocketcan_CFLAGS=-I/tmp/CAN/libsocketcan/output/include LDFLAGS=-L/tmp/CAN/libsocketcan/output/lib CPPFLAGS=-I/tmp/CAN/libsocketcan/output/include
make
make install

编译完之后,可以看到生成的可执行文件和库文件,如下图:

Menuconfig1

canutils 提供了五个工具:candump, canecho, cansend, canconfig, 和 cansequence。 将这些工具拷贝到目标版的 /usr/bin/ 目录下。至此,CAN 工具准备完成。

CAN 工具使用

工具准备之后,接下来开始配置 CAN 设备。目前开发者可以使用 ip 命令和 cantuils 工具集。下面内容将按工具进行讲解。

ip 工具配置 CAN 设备

在 Linux 系统中,CAN 总线接口设备作为网络设备被系统进行统一管理。在控制台下, CAN 总线的配置和以太网的配置使用相同的命令。在控制台上输入命令:

ifconfig -a

Menuconfig1

使用命令之后,可以看到 can0 设备相关信息,但此时的状态是 DOWN,所以在使用之前 需要将 can0 设置为 UP。为了使 CAN 设备设置为 UP 状态,需要设置 CAN 设备的波特 率,可以参考如下命令,将 can0 的波特兰设置为 125K:

ip link set can0 up type can bitrate 1000000

当设置完成后,可以通过下面命令查询 can0 设备的设置:

ip -details link show can0

Menuconfig1

通过上面的命令,我们可以查看到 can0 已经 UP,接下来我们可以操作 can0 了。也可 以使用下面命令取消 can0 设备使能

ifconfig can0 down

在设备工作中,可以使用下面的命令来查询工作状态

ip -details -statistics link show can0

canutils 工具配置 CAN 设备

canutils 工具集提供了五个有用的工具,分别是 canconfig, candump, canecho, candump 和 cansequence。每个工具的说明如下:

canconfig

canconfig 用于配置 CAN 设备的参数,其中包括 bitrate 的设置,也可以设置 BR 和 SP 的值,其中我们比较关心的就是 CAN 频率设置和控制模式。如果波形的 TQ 有问题的 话,可以设置 tq 的值等。

Menuconfig1

开发者在使用 can 设备之前,要先设置 can 总线的频率,如果不设置,那么在 up can 设备的时候就会报错,原因是 can 设备在 open 的时候,会检测是否已经设置了 bittiming, 如果没有设置就报错。所以我们先配置 can 设备的 bittiming,使用如下 命令

canconfig canX bitrate 125000
candump

使用 candump 接收 can 包

Menuconfig1

canecho

使用 canecho 查看 canX 状态

Menuconfig1

cansend

使用 cansend 发送 can 包

Menuconfig1

canutils 工具使用

canconfig 配置 CAN 设备

canconfig can0 restart-ms 1000 bitrate 10000000 ctrmode triple-sampling on
canconfig can1 restart-ms 1000 bitrate 10000000 ctrmode triple-sampling on

canconfig 启动命令

canconfig can0 start
canconfig can1 start

cansend 发送命令

cansend can0 7ff@898787
cansend can1 7ff#234567

candump 监控接受命令

candump can0
candump can1

canconfig 停止命令

canconfig can0 stop
canconfig can1 stop

源码访问 CAN 设备

当配置好一个 CAN 设备之后,开发者可以参考如下代码进行 CAN 设备的数据首发,源码 分为两个,一个用于描述如何通过 CAN 设备发送数据,另一个用于描述如何通过 CAN 设备接受数据。

can_tx.c: 用于描述如何通过 CAN 设备发送数据

/*
* CAN send message
*
* (C) 2018.12.18 BuddyZhang1 <buddy.zhang@aliyun.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

int main(void)
{
    int s, nbytes;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame[2] = 0;
    
    /* Create can socket */
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    strcpy(ifr.ifr_name, "can0");
    /* set can device */
    ioctl(s, SIOCGIFINDEX, &ifr);
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    /* bind to can0 */
    bind(s, (struct sockaddr *)&addr, sizeof(addr));
    /* forbidden filter rule, only send message. */
    setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
    /* Create two message */
    frame[0].can_id  = 0x011;
    frame[0].can_dlc = 1;
    frame[0].data[0] = 0x23;
    frame[1].can_id  = 0x01;
    frame[1].can_dlc = 1;
    frame[1].data[0] = 'N';

    /* send meesage nop */
    while (1) {
        /* Send first message */
        nbytes = write(s, &frame[0], sizeof(frame[0]));
        if (nbytes != sizeof(frame[0])) {
            printf("Send Error frame[0] bytes %d\n", nbytes);
            break;
        } else
            printf("Write1 %d\n", nbytes);
        sleep(1);
        /* Send second message */
        nbytes = write(s, &frame[1], sizeof(frame[1]));
        if (nbytes != sizeof(frame[1])) {
            printf("Send Error frame[1] %d\n", nbytes);
            break;
        } else
            printf("Write2 %d\n", nbytes);
        sleep(1);
    }
    close(s);
    return 0;
}

can_rx.c: 通过 CAN 设备接受数据

/*
* CAN receive message
*
* (C) 2018.12.18 BuddyZhang1 <buddy.zhang@aliyun.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

int main(void)
{
    int s, nbytes;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;
    struct can_filter rfilter[1];

    /* Create socket */
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    strcpy(ifr.ifr_name, "can0");
    ioctl(s, SIOCGIFINDEX, &ifr);
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    /* bind socket to can0 */
    bind(s, (struct sockaddr *)&addr, sizeof(addr));
    /* define rule to filter can frame */
    rfilter[0].can_id = 0x11;
    rfilter[0].can_mask = CAN_SFF_MASK;
    /* setup filter rule */
    setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
    /* monitoring can0 */
    printf("|    ID    |     DLC     |  DATA[0]\n");
    while (1) {
        /* Receive message */
        nbytes = read(s, &frame, sizeof(frame));
        /* dump message */
        if (nbytes > 0)
            printf("%#x %#x %#x\n", frame.can_id,
                   frame.can_dlc, frame.data[0]);
    }
    close(s);
    return 0;
}

上面的程序主要是对一个已经处于 UP 状态的 can 接口进行数据的写,其代码逻辑如 下:

初始化

首先在用户空间对 socketcan 进行初始化动作,在用户空间,使用 struct sockaddr_can 来表示 can,其定义在 /usr/include/linux/can.h 文件中, 如下:

/**
 * struct sockaddr_can - the sockaddr structure for CAN sockets
 * @can_family:  address family number AF_CAN.
 * @can_ifindex: CAN network interface index.
 * @can_addr:    protocol specific address information
 */
struct sockaddr_can {
        __kernel_sa_family_t can_family;
        int         can_ifindex;
        union {
                /* transport protocol class address information (e.g. ISOTP) */
                struct { canid_t rx_id, tx_id; } tp;
                /* reserved for future CAN protocols address information */
        } can_addr;
};

接着使用 struct ifreq 结构体用来配置 ip 地址,激活接口和配置 MTU 等接口信息。 其中包含一个接口的名字和具体内容(是个共用体,有可能是 IP 地址,广播地址,子网 掩码,MAC 号,MTU 或其他内容)。因此,CAN 应用程序的初始化代码如下:

int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame[2] = 0;

/* Create can socket */
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
strcpy(ifr.ifr_name, "can0");

/* set can device */
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));

调用 socket() 函数创建 socket,CAN 传入 PF_CAN, socket 是原始 socket 和原始 CAN。网络接口的名字设置为 can0. 调用 ioctl 函数传入 SIOCGIFINDEX, 指定 can0 。并设置 sockaddr_can 的 can_family 为 AF_CAN ,最后调用 bind() 函数,将 can0 与 socket 设备挂钩。支持用户空间 can0 初始化完成。

数据发送

在数据收发内容方面,CAN 总线与标准套接字通信稍微不同,每一次通信都采用 can_frame 结构体将数据封装成帧。struct can_frame 的定义如下:

/**
 * struct can_frame - basic CAN frame structure
 * @can_id:  CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
 * @can_dlc: frame payload length in byte (0 .. 8) aka data length code
 *           N.B. the DLC field from ISO 11898-1 Chapter 8.4.2.3 has a 1:1
 *           mapping of the 'data length code' to the real payload length
 * @data:    CAN frame payload (up to 8 byte)
 */
struct can_frame {
        canid_t can_id;  /* 32 bit CAN_ID + EFF/RTR/ERR flags */
        __u8    can_dlc; /* frame payload length in byte (0 .. CAN_MAX_DLEN) */
        __u8    data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};

can_id 为帧的标识符,如果发出去的是标准帧,就使用 can_id 的低 11 位。如果是拓 展帧,就使用 0~28 位。can_id 的第 29 ,30, 31 位是帧的标志位,用来定义帧的类 型。定义如下:

#define CAN_EFF_FLAG 0x80000000U    //拓展的标识
#define CAN_RTR_FLAG 0x40000000U    //远程帧的标识
#define CAN_ERR_FLAG 0x20000000U    //错误帧的标识,用于错误检查

数据发送使用 write 函数来实现,如果发送的数据帧(标识符为 0x123) 包含单个字 节 (0xAB) 的数据,可采用如下方法进行发送:

struct can_frame frame;
int nbytes;
frame.can_id = 0x123; // 如果为拓展帧,那么 frame.can_id = CAN_EFF_FLAG | 0x123
frame.can_dlc = 1; // 数据长度为 1
frame.data[0] = 0xAB; // 数据内容为 0xAB
nbytes = write(s, &frame, sizeof(frame)); // 发送数据
if (nbytes != sizeof(frame))
    printf("ERROR.\n");

Can 在用户空间使用 struct can_frame 来表示一个 CAN 帧,其定义如下:

/**
 * struct can_frame - basic CAN frame structure
 * @can_id:  CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
 * @can_dlc: frame payload length in byte (0 .. 8) aka data length code
 *           N.B. the DLC field from ISO 11898-1 Chapter 8.4.2.3 has a 1:1
 *           mapping of the 'data length code' to the real payload length
 * @data:    CAN frame payload (up to 8 byte)
 */
struct can_frame {
        canid_t can_id;  /* 32 bit CAN_ID + EFF/RTR/ERR flags */
        __u8    can_dlc; /* frame payload length in byte (0 .. CAN_MAX_DLEN) */
        __u8    data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};

canid_t 的定义如下:

/*
 * Controller Area Network Identifier structure
 *
 * bit 0-28 : CAN identifier (11/29 bit)
 * bit 29 : error message frame flag (0 = data frame, 1 = error message)
 * bit 30 : remote transmission request flag (1 = rtr frame)
 * bit 31 : frame format flag (0 = standard 11 bit, 1 = extended 29 bit)
 */
typedef __u32 canid_t;

0-28 位为标识符,如果是扩展帧,则高 11 位为标准 ID
29 位标识是数据帧还是错误消息
30 位说明是否是远程帧
31 位说明是标准帧还是扩展帧

以下是 can_frame 使用的标识和掩码

/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */
#define CAN_RTR_FLAG 0x40000000U /* remote transmission request */
#define CAN_ERR_FLAG 0x20000000U /* error message frame */
/* valid bits in CAN ID for frame formats */
#define CAN_SFF_MASK 0x000007FFU /* standard frame format (SFF) */
#define CAN_EFF_MASK 0x1FFFFFFFU /* extended frame format (EFF) */
#define CAN_ERR_MASK 0x1FFFFFFFU /* omit EFF, RTR, ERR flags */

CAN 信号测试

CAN 使用差分信号,通过 H 和 L 两个信号线进行数据传输。当工程开发过程中,需要抓 取 CAN 信号进行分析。抓取信号的方式很多,这里介绍通过 CAN 逻辑分析仪进行信号抓 取。

Menuconfig1

配套的 Windows 分析工具,使用这个工具可以简单的对 CAN 信号进行分析。

Menuconfig1


附录

CAN Information

BiscuitOS Home

BiscuitOS Driver

BiscuitOS Kernel Build

赞赏一下吧 🙂

MMU