嵌入式Linux开发---Socket CAN通信驱动硬件编程

提醒:使用Linux CAN开发的需要具备网络编程的部分基础,Socket CAN的使用类似于TCP/IP

Linux开发板通过Socket can驱动设备的参考源码demo见文末。

0、CAN基础准备

        CAN,全称为“Controller Area Network”,即控制器局域网,是国际上应用最广泛的现场总线之一。 最初,CAN 被设计作为汽车环境中的微控制器通讯,在车载各电子控制装置 ECU 之间交换信息,形成汽车电子控制网络。比如:发动机管理系统、变速箱控制器、仪表装备、电子主干系统中,均嵌入 CAN 控制装置。

        一个由 CAN 总线构成的单一网络中,理论上可以挂接无数个节点。实际应用中,节点数目受网络硬件的电气特性所限制。例如,当使用 Philips P82C250 作为 CAN 收发器时,同一网络中允许挂接 110 个节点。CAN 可提供高达 1Mbit/s 的数据传输速率,这使实时控制变得非常容易。另外,硬件的错误检定特性也增强了 CAN 的抗电磁干扰能力。CAN 已经在汽车工业、航空工业、工业控制、安全防护等领 域中得到了广泛应用。

        CAN 通讯协议主要描述设备之间的信息传递方式。CAN 层的定义与开放系统互连模型(OSI)一致。每 一层与另一设备上相同的那一层通讯。虽然CAN传输协议参考了OSI 七层模型,但是实际上CAN协议只定义了两层“物理层”和“数据链路层”,因此出现了各种不同的“应用层”协议,比如用在自动化技术的现场总线标准DeviceNet,用于工业控制的CanOpen,用于乘用车的诊断协议OBD、UDS(统一诊断服务,ISO14229),用于商用车的CAN总线协议SAEJ1939。

层次

描述

应用层

主要定义CAN应用层。

数据链路层

数据链路层分为逻辑链接控制子层LLC和介质访问控制子层MAC。

MAC 子层是 CAN 协议的核心。它把接收到的报文提供给 LLC 子层,并接收来自 LLC 子层的报文。 MAC 子层负责报文分帧、仲裁、应答、错误检测和标定。MAC 子层也被称作故障界定的管理实体监管

LLC 子层涉及报文滤波、过载通知、以及恢复管理。

LLC = Logical Link Control

MAC = Medium Access Control

物理层

物理层,为物理编码子层PCS.

该层定义信号是如何实际地传输的,因此涉及到位时间、位编码、同步。

CAN总线是一种分布式的控制总线,传输差分信号

CAN总线作为一种控制器局域网,和普通以太网一样,它的网络很多CAN节点构成。

CAN网络的每个节点非常简单,均由一个MCU(微控制器)、一个CAN控制器和一个CAN收发器构成,然后使用双绞线连接到CAN网络中。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第1张图片

模分信号使用电平的绝对值来表示逻辑的差别,差分信号使用两个电平的差值来表示逻辑值。CAN传输使用两根数据线 ------------- CANH和CANL。

如果CANH(3.5V)和CANL(1.5V)的电压差 = 2V此时表示逻辑0,叫做显性电平

如果CANH(2.5V)和CANL(2.5V)的电压差 = 0V此时表示逻辑1,叫做隐性电平

//如果多个节点同时控制总线电平,最终显示的是显性电平(0)

1、CAN通信报文帧

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第2张图片

        CAN使用5种通信帧,其中数据帧是使用最多的帧类型,以数据帧为例来介绍CAN的帧结构。

数据帧的分析:

数据帧分为11位标准数据帧29位扩展数据帧

D --- 显性电平 R --- 隐性电平

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第3张图片

CAN的数据帧由7部分组成:

(1)帧起始。表示数据帧开始的段。

        1位显性电平

(2)仲裁段。表示该帧优先级的段。

用来实现帧的优先级和帧的过滤

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第4张图片

1.标准帧

        标准数据帧的仲裁段由11位ID1位RTR位组成,RTR用来区分数据帧(显性电平)和遥控帧

2.扩展帧

        扩展数据帧由29位ID1位RTR1位SRR1位IDE组成,RTR用来区分数据帧(显性电平)和遥控帧

        SRR用来代替标准帧中的RTR位,由于SRR是隐性电平,相同ID的标准帧优先级高于扩展帧

        IDE用来区分标准帧(显性电平)和扩展帧,显性电平表示标准帧,隐性电平表示扩展帧

        报文的优先级由总线通过ID仲裁来判断,当总线上同时出现显性电平和隐形电平时,最终显示为显性电平

        当多个节点同时竞争总线占有权时,谁先出现隐形电平,将失去总线占有权,转为接收状态

(3)控制段。表示数据的字节数及保留位的段。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第5张图片

r0,r1为保留位,默认是显性电平

4DLC表示数据段的长度(0~8)

(4)数据段。数据的内容,一帧可发送0~8个字节的数据。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第6张图片

长度0~8字节,高位先出

(5)CRC段。检查帧的传输错误的段。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第7张图片

CRC错误校验,由15位CRC校验码和1位CRC界定符组成

校验出了错误信息,可利用错误帧请求重发,重发次数可设定

(6)ACK段。表示确认正常接收的段。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第8张图片

由1位ACK槽和1位ACK界定符组成,发送方的ACK槽是隐性电平

接收方确认收到正确的数据后以显性电平应答

(7)帧结束。表示数据帧结束的段。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第9张图片

7位隐形电平

3、Linux Socket CAN编程

        Socket CAN是在Linux下CAN协议(Controller Area Network)实现的一种实现方法。Linux下最早使用CAN的方法是基于字符设备来实现的,与之不同的是Socket CAN使用伯克利的socket接口和linux网络协议栈,这种方法使得can设备驱动可以通过网络接口来调用。Socket CAN的接口被设计的尽量接近TCP/IP的协议。

        使用Socket CAN的主要目的就是为用户空间的应用程序提供基于Linux网络层的套接字接口。与广为人知的TCP/IP协议以及以太网不同,CAN总线没有类似以太网的MAC层地址只能用于广播。CAN ID仅仅用来进行总线的仲裁。因此CAN ID在总线上必须是唯一的。当设计一个CAN-ECU(Electronic Control Unit 电子控制单元)网络的时候,CAN报文ID可以映射到具体的ECU。因此CAN报文ID可以当作发送源的地址来使用。

socket can通信常用函数

(1)、socket()函数
(2)、bind()函数
(3)、ioctl()函数
(4)、setsockopt()函数
(5)、write()函数
(6)、read()函数
(7)、close()函数
(8)、close()函数

4、Linux开发板CAN发送数据

        嵌入式Linux开发---Socket CAN通信驱动硬件编程_第10张图片

        Linux开发板循环发送数据到CAN工具嵌入式Linux开发---Socket CAN通信驱动硬件编程_第11张图片

        CAN工具接收到Linux开发板发送过来的数据        嵌入式Linux开发---Socket CAN通信驱动硬件编程_第12张图片

5、Linux开发板CAN接收数据

        嵌入式Linux开发---Socket CAN通信驱动硬件编程_第13张图片

        USB转CAN工具上位机循环发送数据给开发板

        嵌入式Linux开发---Socket CAN通信驱动硬件编程_第14张图片

        Linux开发板运行接收程序,接收到来自CAN工具发送过来的数据

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第15张图片

6、CAN驱动故障问题

        CAN驱动需要预先适配,才能使用,部分设备由于开发者自身此前在驱动开发时未进行CAN的驱动适配,或厂家并未提供CAN驱动适配,会导致无法进行CAN开发。

请提前检查Linux开发板的CAN是否正常。可使用如下指令测试CAN是否正常:

1、关闭CAN口

ip link set can0 down

2、设置 can0 传输速率为 500kbps

ip link set can0 type can bitrate 500000

3、打印can0 的信息

ip -details link show can0

4、打开 can0 接口

ip link set can0 up

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第16张图片

如上图所示为CAN驱动存在故障,在重新烧录适配CAN驱动的Linux系统后,可恢复正常使用。

嵌入式Linux开发---Socket CAN通信驱动硬件编程_第17张图片

7、Linux CAN通信测试程序源码

#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 

typedef struct
{
  unsigned int StdId;    /* CAN标准帧ID,占11bit,范围:0~0x7FF */ 
  unsigned int ExtId;    /* CAN扩展帧ID,占29bit,范围:0~0x1FFFFFFF */ 
  unsigned char IDE;     /* CAN报文ID类型,CAN_ID_STD 或 CAN_ID_EXT */
  unsigned char RTR;     /* CAN报文类型,CAN_RTR_DATA 或 CAN_RTR_REMOTE */ 
  unsigned char DLC;     /* CAN报文数据长度, 范围:0~8  */ 
  unsigned char Data[8]; /* CAN报文数据内容,每个字节范围:0~0xFF*/
} CanTxMsg;

typedef struct
{
  unsigned int StdId;    /* CAN标准帧ID,范围:0~0x7FF */ 
  unsigned int ExtId;    /* CAN扩展帧ID,范围:0~0x1FFFFFFF */ 
  unsigned char IDE;     /* CAN报文ID类型,CAN_ID_STD 或 CAN_ID_EXT */
  unsigned char RTR;     /* CAN报文类型,CAN_RTR_DATA 或 CAN_RTR_REMOTE */ 
  unsigned char DLC;     /* CAN报文数据长度, 范围:0~8  */ 
  unsigned char Data[8]; /* CAN报文数据内容,每个字节范围:0~0xFF*/
  unsigned char FMI;     /* 过滤模式,总共有14中,定义有宏,其值依次为0x1,0x2,0x4,0x8,0x10,0x20,0x40……,此处可以不考虑,忽略*/
} CanRxMsg;

/**
  * @brief  Linux socket can通信初始化
  * @param  fd:CAN通信文件描述符
  * @retval 成功返回0,失败返回-1
  */
int can_init_for_linux(int *fd)
{
    //0、设置can通信波特率
    system("ip link set can0 down");            
    //波特率设置为500Kbps
    system("ip link set can0 type can bitrate 500000");     
    system("ip link set can0 up"); 

    int ret = 0;
    //1、创建socket can套接字
    *fd = socket(AF_CAN, SOCK_RAW, CAN_RAW);
    if(*fd == -1)
    {
        perror("create socket can failed");
        return -1;
    }

    //2、套接子绑定到can0端口
    struct sockaddr_can addr;
    struct ifreq ifr;
    strcpy(ifr.ifr_name, "can0");
    ioctl(*fd, SIOCGIFINDEX, &ifr);
    memset(&addr, 0, sizeof(addr));
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    ret = bind(*fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret < 0)
    {
        perror("bind can0 error");
        return -1;
    }

    //4、设置CAN过滤规则
    #if 0
    struct can_filter recv_filter;
    recv_filter.can_id = 0x201;
    recv_filter.can_mask = CAN_SFF_MASK;
    setsockopt(*can_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &recv_filter, sizeof(recv_filter));
    #endif

    //5、设置read、write为非堵塞方式
    int flags;
    flags = fcntl(*fd, F_GETFL);
    flags |= O_NONBLOCK;
    fcntl(*fd, F_SETFL, flags);

    return 0;
}

/**
  * @brief  关闭CAN通信文件描述符
  * @param  fd:CAN通信文件描述符
  * @retval void
  */
void can_close(int fd)
{
    close(fd);
}

/**
  * @brief  CAN发送数据
  * @param  fd:CAN通信文件描述符
  * @param  can_id:can设备id
  * @param  data:需要发送的数据(一次最大8字节)
  * @param  data_len:需要发送数据长度(Byte)
  * @retval 成功返回0,失败返回-1
  */
int can_write_data(int fd, int can_id,char data[8],int data_len)
{
    int ret = 0;
    CanRxMsg msg;
    struct can_frame send_data;
    if(fd == -1)
    {
        perror("can write data failed!");
        return -1;
    }
    send_data.can_dlc = data_len;
    send_data.can_id = can_id;
    memcpy(&send_data.data[0], data, data_len);
    ret = write(fd, &send_data, sizeof(struct can_frame));
    if(ret == -1)
    {
        perror("can write data failed!");
        return -1;
    }else{
        printf("ID=%03X, DLC=%d, data=%02X %02X %02X %02X %02X %02X %02X %02X \n",  \
        send_data.can_id, send_data.can_dlc,  \
        send_data.data[0],\
        send_data.data[1],\
        send_data.data[2],\
        send_data.data[3],\
        send_data.data[4],\
        send_data.data[5],\
        send_data.data[6],\
        send_data.data[7] );
    }
    
    return 0;
}

/**
  * @brief  CAN发送数据
  * @param  fd:CAN通信文件描述符
  * @param  can_id:can设备id
  * @param  data:存储CAN接收的8字节数据
  * @retval 成功返回0,失败返回-1
  */
int can_read_data(int fd, int can_id, char data[8])
{
    if(fd == -1)
    {
        perror("can read data failed!");
        return -1;
    }
    int ret = 0;
    struct can_frame recv_data;
    recv_data.can_id = can_id;
    recv_data.can_dlc = 8;

    ret = read(fd, &recv_data,sizeof(struct can_frame));
    if(ret == -1)
    {
        perror("can read data failed!");
        return -1;
    }else{
        memcpy(data, &recv_data.data[0], recv_data.can_dlc);
        if(recv_data.can_id != can_id)
        {
            printf("recv other can id data\n");

            return -1;
        }
		printf("ID=%03X, DLC=%d, data=%02X %02X %02X %02X %02X %02X %02X %02X \n",  \
			recv_data.can_id, recv_data.can_dlc,  \
			recv_data.data[0],\
			recv_data.data[1],\
			recv_data.data[2],\
			recv_data.data[3],\
			recv_data.data[4],\
			recv_data.data[5],\
			recv_data.data[6],\
			recv_data.data[7] );
    }
    
    return 0;
}

/**
  * @brief  主函数
  * @param  NONE
  * @retval 成功返回0,失败返回-1
  */
int main(int argc, char **argv)
{
    int can_fd;
    int ret = 0;
    ret = can_init_for_linux(&can_fd);
    if(ret == -1)
    {
        perror("can init failed!\n");
        return -1;
    }
    char data[8] = {0};
    while (1)
    {
        sleep(2);
        can_read_data(can_fd, 0x01, data);
    }

    printf("hello world!\n");
    return 0;
}

嵌入式Linux C串口硬件驱动编程_linux c串口 csdn-CSDN博客

你可能感兴趣的:(嵌入式Linux经验教程,嵌入式硬件,linux,网络协议,arm开发,mcu,iot,tcp/ip)