树莓派CAN通讯教程 – MCP2515

树莓派CAN通讯教程 - MCP2515

1. 楔

在上篇文章树莓派GPIO和PWM控制教程中,笔者详细介绍了如何使用树莓进行普通IO控制模拟,以及PWM 波形发送等操作,同时还提到了汽车电子常见的CAN 通讯也能够使用树莓派完成,本文针对此进行详细说明。

使用硬件为树莓派3b+, MCP2515 spiCAN模块 ,总成本控制在300 元以内。使用树莓派4b也可以,不过近期的4b价格离谱,而且仅仅can和io的测试,3b+性能完全够用。

操作系统使用ubuntu server ,如使用Raspberry Pi OS 也可以,本教程也可适用。需要注意的是,不能魔法上网的同学需要首先进行操作系统换源,以及python3 的换源。而其中ubuntu server arm 的系统不能够按照常用的清华源教程替换,需要使用后缀为-ports的源,笔者使用的源如下:

  • /etc/apt/sources.list
    # 默认注释了源码仓库,如有需要可自行取消注释
    deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal main restricted universe multiverse
    # deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal main main restricted universe multiverse
    deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-updates main restricted universe multiverse
    # deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-updates main restricted universe multiverse
    deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-backports main restricted universe multiverse
    # deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-backports main restricted universe multiverse
    deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-security main restricted universe multiverse
    # deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-security main restricted universe multiverse
    

2. 硬件连接和环境准备

连接MCP2515和树莓派spi接口,并在操作系统中开启spi,整个的运行原理就是让MCP2515的CAN 通讯作为网络通讯接口,挂接到socketCAN 上,使用系统驱动spi,无需手工编写spi驱动以及can wrapper部分。针对socketCAN,网上有很多优秀的开源工具可以使用,笔者这里使用的是cantools ,可以使用dbc进行报文格式解析。

2.1. 硬件连接

RPi Pin    RPi Label     CAN Module
02---------5V------------VCC
06---------GND-----------GND
19---------GPIO10--------MOSI (SI)
21---------GPIO9---------MISO (SO)
22---------GPIO25--------INT
23---------GPIO11--------SCK
24---------GPIO8---------CS

2.2. 环境准备

  • 安装socket can工具以及cantools工具
    sudo apt install can-utils
    pip3 install cantools
    
  • 使能树莓派SPI并加载MCP2515内核驱动

    针对Ubuntu server 操作系统,在/boot/firmware/usercfg.txt文件后添加如下内容,若操作系统为Raspberry Pi OS,则在/boot/config.txt文件后添加如下内容:

    dtparam=spi=on
    dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
    dtoverlay=spi1-1cs
    
  • 重启 sudo reboot -h now

2.3. 检测MCP2515是否被正确挂载

输入sudo ifconfig -a指令可以看到已经挂载了网络通讯卡CAN0,如没有ifconfig,则使用sudo apt install net-tools进行安装

ubuntu@ubuntu:~$ sudo ifconfig -a
can0: flags=193<UP,RUNNING,NOARP>  mtu 16
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 10  (UNSPEC)
        RX packets 9343435  bytes 74747480 (74.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 64 (64.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether b8:27:eb:c2:61:84  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

输入 sudo ip -s -d link show can0 查看can0 通讯是否进入ready状态。

ubuntu@ubuntu:~$ sudo ip -s -d link show can0
3: can0: <NOARP,ECHO> mtu 16 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 10
    link/can  promiscuity 0 minmtu 0 maxmtu 0
    can state STOPPED restart-ms 0
          bitrate 1000000 sample-point 0.750
          tq 125 prop-seg 2 phase-seg1 3 phase-seg2 2 sjw 1
          mcp251x: tseg1 3..16 tseg2 2..8 sjw 1..4 brp 1..64 brp-inc 1
          clock 8000000
          re-started bus-errors arbit-lost error-warn error-pass bus-off
          0          0          0          0          0          0         numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    RX: bytes  packets  errors  dropped overrun mcast
    74955120   9369390  0       0       0       0
    TX: bytes  packets  errors  dropped carrier collsns
    64         8        0       0       0       0

3. CAN 通讯设置及程序开发

如上步骤如果已经完成,则可进行相关CAN 通讯开发,使用python和c都可以,因为系统支持命令行进行报文发送读取及设定,所以python可以简单的调用系统命令。c的话不建议调用系统命令,而是使用socket接口进行编程。

需注意,由于硬件限制,此方案can通讯波特率最高仅支持500Kbps

3.1. 系统命令

  • 关闭can0

    sudo ip link set can0 down

  • 设置波特率 500K ,需注意bitrate 需要除2才是常规的通讯波特率

    sudo ip link set can0 type can bitrate 1000000

  • 开启can0

    sudo ip link set can0 up

  • 查看状态

    sudo ip -s -d link show can0

  • 接收报文命令

    candump any,0:0,#FFFFFFFF

  • 联合 cantools 使用dbc文件进行报文解码

    candump can0 | cantools decode temp.dbc

  • 发送报文命令

    cansend can0 123#1122334455667788

  • 设置回环 波特率 250K ,用于测试can通路,在没有其它硬件连接测试的情况下,可以设定成回环,自发自收

    sudo ip link set can0 type can bitrate 500000 loopback on

3.2. c语言调用socket接口进行开发

/**
 *-----------------------------------------------------------------------------
 * @file can_control.c
 * @brief 
 * @author Tomato
 * @version 0.1
 * @date 2021-07-22
 * @note [change history] 
 * 
 * @copyright NAAAAA
 *-----------------------------------------------------------------------------
 */

#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>
#define command "ip link set can0 type can bitrate 1000000"//将CAN0波特率设置为500K
#define up "ifconfig can0 up"//打开CAN0
#define down "ifconfig can0 down"//关闭CAN0

int can_init()
{
    //关闭CAN设备,设置波特率后,重新打开CAN设备
    system(down);
    system(command);
    system(up);
    return 0;
}

int can_send(can_frame frame)
{
    int s, nbytes;
    struct sockaddr_can addr;
    struct ifreq ifr;
    //创建套接字
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    strcpy(ifr.ifr_name, "can0" );
    //指定 can0 设备
    ioctl(s, SIOCGIFINDEX, &ifr); 
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    //将套接字与 can0 绑定
    bind(s, (struct sockaddr *)&addr, sizeof(addr));
    //发送 frame[0]
    nbytes = write(s, &frame, sizeof(frame));
    if(nbytes != sizeof(frame))
    {
        printf("Send Error frame[0]\n!");
    }
    close(s);
    return 0;
}

int can_receive(struct can_frame * r_frame,unsigned int filter_id)
{
    int s, nbytes = 0;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;
    struct can_filter rfilter;

    // Initial fram
    memset(&frame,0,sizeof(can_frame));
    //创建套接字
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    strcpy(ifr.ifr_name, "can0" );
    //指定 can0 设备
    ioctl(s, SIOCGIFINDEX, &ifr); 
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    //将套接字与 can0 绑定
    bind(s, (struct sockaddr *)&addr, sizeof(addr));
    //设置过滤规则,取消当前注释为禁用过滤规则,即不接收所有报文,
    // 不设置此项(即如当前代码被注释)为接收所有ID的报文。
    if (filter_id != 0)
    {
        rfilter.can_id   = 0x123;
        // CAN_EFF_MASK | CAN_SFF_MASK
        rfilter.can_mask = CAN_SFF_MASK;
        setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
    }
    while (nbytes == 0)
    {
        //接收总线上的报文保存在frame中
        nbytes = read(s, &frame, sizeof(frame));
    }
    *r_frame = frame;
#ifdef MSG_DEBUG
    printf("the nbytes:%d\n", nbytes);
    printf("length:%d", sizeof(frame));
    printf("ID=0x%X DLC=%d\n", frame.can_id, frame.can_dlc);
    printf("data0=0x%02x\n",frame.data[0]);
    printf("data1=0x%02x\n",frame.data[1]);
    printf("data2=0x%02x\n",frame.data[2]);
    printf("data3=0x%02x\n",frame.data[3]);
    printf("data4=0x%02x\n",frame.data[4]);
    printf("data5=0x%02x\n",frame.data[5]);
    printf("data6=0x%02x\n",frame.data[6]);
    printf("data7=0x%02x\n",frame.data[7]);
#endif
    return 0;
}

int led_ctl_on(void)
{
    struct can_frame frame;
    memset(&frame, 0, sizeof(can_frame));
    frame.can_id = 0x101;
    frame.can_dlc = 8;
    frame.data[0] = 1;
    can_send(frame);
    return 0;
}

int led_ctl_off(void)
{
    struct can_frame frame;
    memset(&frame, 0, sizeof(can_frame));
    frame.can_id = 0x101;
    frame.can_dlc = 8;
    frame.data[0] = 2;
    can_send(frame);
    return 0;
}

float can_get_vol(void)
{
    float vol_vle = 0;
    struct can_frame frame;
    memset(&frame, 0, sizeof(can_frame));
    // wait until can frame 100 received
    can_receive(&frame,0);
    printf("###############################\n");
    printf("length:%d", sizeof(frame));
    printf("ID=0x%X DLC=%d\n", frame.can_id, frame.can_dlc);
    printf("data0=0x%02x\n",frame.data[0]);
    printf("data1=0x%02x\n",frame.data[1]);
    printf("data2=0x%02x\n",frame.data[2]);
    printf("data3=0x%02x\n",frame.data[3]);
    printf("data4=0x%02x\n",frame.data[4]);
    printf("data5=0x%02x\n",frame.data[5]);
    printf("data6=0x%02x\n",frame.data[6]);
    printf("data7=0x%02x\n",frame.data[7]);

    vol_vle = (float)frame.data[0]/50;
    return vol_vle;
}

int main(int argc, char* argv[])
{
    char control_str[15]; 
    float vol_val = 0;

    if (argc < 2) {
        printf("can_control service_type\n"
            "    example: ./can_control led_off/led_on/get_vol\n"
            );
        return 0;
    }
    strcpy(control_str,argv[1]);

    // debug
    printf("Argc : %d\n",argc);
    printf("Argv : %s\n , %s\n",argv[0], argv[1]);

    // can_init();
    if (strcmp(control_str,"led_off")==0)
    {
        led_ctl_off();
    }
    else if (strcmp(control_str,"led_on")==0)
    {
        led_ctl_on();
    }
    else if (strcmp(control_str,"get_vol")==0)
    {
        vol_val = can_get_vol();
        printf("Voltage is : %5.2f V\n", vol_val);
    }
    else
    {
        /* Do nothing */
    }
    return 0;
}

3.3. 实际效果