CAN 总线设备

更新时间:
2024-05-13
下载文档

CAN 总线设备

CAN(Controller Area Network),即控制局域网,是一种串行通讯协议,在汽车电子、自动控制、安防监控等领域都有广泛的应用。

CAN 总线协议仅仅定义了 OSI 模型中的物理层和数据链路层,在实际应用中,通常会在一个基于 CAN 基本协议的高层协议进行通信。

CAN 的基本协议中,使用帧为基本传输单元,类似于以太网中的 MAC 帧,CAN 控制器负责对 CAN 帧进行电平转换、报文校验、错误处理、总线仲裁等处理。但是 CAN 帧里没有源地址和目的地址,这说明在一个 CAN 总线系统中,所有节点的 CAN 控制器不能像以太网那样进行硬件地址过滤从而实现定向通信,只能由应用层根据 CAN 帧的内容决定是否接收该帧。CAN 高层协议即是处理类似的工作,将 CAN 帧的各个字段认真地进行定义,赋予其特殊的含义,并且处理一些底层协议没有处理的工作,例如总线上的设备类型定义、设备状态监控和管理等,我们把 CAN 高层协议统称为 CAN 应用层协议。

目前,CAN 应用层协议有 DeviceNet、CANopen、CAL 等,它们针对不同的应用场合有自己的协议标准。限于篇幅,本篇不介绍 CAN 底层协议以及应用协议的具体知识。

SylixOS 中的 CAN 总线设备仅支持底层协议,该设备为一个字符型设备,但是对它的读写操作都必须以 CAN 帧为基本单元。SylixOS 中对 CAN 帧的定义位于 <SylixOS/system/device/can.h>,结构如下:

#define   CAN_MAX_DATA      8                        /*  CAN 帧数据最大长度               */
typedef struct {
    UINT            CAN_uiId;                        /*  标识码                          */
    UINT            CAN_uiChannel;                   /*  通道号                          */
    BOOL            CAN_bExtId;                      /*  是否是扩展帧                     */
    BOOL            CAN_bRtr;                        /*  是否是远程帧                     */
    UCHAR           CAN_ucLen;                       /*  数据长度                        */
    UCHAR           CAN_ucData[CAN_MAX_DATA];        /*  帧数据                          */
} CAN_FRAME;
typedef CAN_FRAME  *PCAN_FRAME;    

成员 CAN_uiId 为 CAN 节点标示符,在一个 CAN 总线系统中,每个节点的标示符都是唯一的。如果成员 CAN_bExtId 为 FALSE,这表示一个标准帧,CAN_uiId 的低 11 位有效,反之则表示为一个扩展帧,则 CAN_uiId 的低 29 位有效。大多数应用协议都会将 CAN_uiId 进行再定义,比如将一部分位用来表示设备的数据类型,一部分位表示设备的地址等。因此,扩展帧的目的是为了在已有的基础上,满足更多的应用数据需求和统一网络内支持更多的设备。

CAN_uiChannel 不是 CAN 协议规定的,SylixOS 中用该数据表示系统中 CAN 设备的硬件通道编号,实际应用中通常不用处理。CAN_bRtr 表示是否为一个远程帧,远程帧的作用是让希望获取帧的节点主动向 CAN 系统中的节点请求与该远程帧标示符相同的帧。一个 CAN 帧的最大帧数据长度为 8 字节,成员 CAN_ucLen 表示当前帧中数据的实际长度,CAN_ucData 表示实际的数据。

现在我们考虑这样一个应用场景:在一个 CAN 总线系统中,存在许多专门负责数据采集的节点,它们采集的数据类型不同,相应的数据格式也不相同,当然也可能存在多个采集同一种数据类型的节点。系统中还有一个负责收集并处理这些数据的节点,它的基本要求是能够正确地识别不同的数据格式并作相应的解析处理。

为了有效地区分不同的数据类型,我们可以将 CAN 帧里面的数据进行人为的定义,例如可以将 CAN_uiId 的一部分数据位用来表示数据类型,剩下的表示节点 ID,但这样会让整个 CAN 系统所能支持的总的 CAN 节点变少;另一种方法是将 CAN_ucData 的一部分数据位(比如第一个字节)表示数据类型,但这样会让单次可传输的数据量减少。

为了描述这些行为,我们有必要定义一个通用的操作标准,相当于我们自己定义了一个 CAN 应用层协议,这里我们将自定义的协议简单的称为 APP,且定义在 appLib.h 里。

#ifndef __APP_LIB_H
#define __APP_LIB_H
#define APP_TYPE_MASTER        0
#define APP_TYPE_INT32         1
#define APP_TYPE_STRING        2
#define APP_ADDR_MASTER        0
#define APP_TYPE(id)           ((id >> 7) & 0x0f)
#define APP_ADDR(id)           (id & 0x3f)
#define APP_NET_ID(t, a)       ((((UINT)t & 0x0f) << 7) | ((UINT)a & 0x3f))
static  inline INT32  __appByteToInt32 (const UCHAR *pucByte)
{
    INT32 iData;
    iData =    ((INT32)pucByte[0])
           |     ((INT32)pucByte[1] << 8)
           |     ((INT32)pucByte[2] << 16)
           |     ((INT32)pucByte[3] << 24);
    return  (iData);
}
static inline  VOID  __appInt32ToByte (UCHAR *pucByte, INT32 iData)
{
    pucByte[0] = iData & 0xff;
    pucByte[1] = (iData >> 8) & 0xff;
    pucByte[2] = (iData >> 16) & 0xff;
    pucByte[3] = (iData >> 24) & 0xff;
}
extern UINT  __appSlaveAddrGet(VOID);
#endif                                                            /* __APP_LIB_H        */

如下程序所示,我们将 CAN 底层协议定义的 ID 进行了重新定义,首先该系统中只存在标准帧,这意味着 ID 的有效数据位为 11 位,我们将高 4 位用来表示数据的类型,低 7 位表示 CAN 设备地址。为了简单,我们定义了两个数据类型,一个表示数据是 32 位的有符号整数,另一个表示数据是字符串。此外,我们将系统中负责收集数据的节点称作主节点(master),将负责信息采集的节点称作从节点(slave)。宏定义 APP_ADDR_MASTER 为主节点预留了一个设备地址,这说明,整个系统中,只能存在一个主节点。

内联函数 __appByteToInt32 和 __appInt32ToByte 处理类型为整数的数据,前者用于主节点在接收到字节形式的数据后解析为实际使用的整数,后者用于从节点在发送整数数据之前将其处理为字节形式的数据,随后发送到网络。

__appSlaveAddrGet 用于获得一个唯一的从节点地址。正如以太网中的 IP 地址和 MAC 地址一样,一个网络中节点的唯一标识一定是由第三方机构来管理的(IP 地址由 IANA 管理,MAC 地址由 IEEE 管理)。我们的 CAN 总线系统中,每一个节点虽然可以人为保证地址的唯一性,但地址的来源或者说存储方式可能不尽相同,它可以来自于 EEPROM、NANDFLASH 或 SD 卡等非易失存储器,甚至我们可以在整个系统中提供一个类似 DHCP 的服务节点,让其他节点动态获得地址信息。正因为有如此多的可能性,我们将地址获取的方法交给具体的应用来处理,因此上面的函数并未实现,而是仅仅声明为一个外部函数。

#include <SylixOS.h>
#include "appLib.h"
#define CAN_DEV_NAME        "/dev/can0"
int main(int argc, char *argv[])
{
    INT             iCanFd;
    CAN_FRAME       canframe;
    UINT            uiType;
    UINT            uiAddr;
    ssize_t         sstReadLen;
    UINT            uiNetId;
    iCanFd = open(CAN_DEV_NAME, O_RDONLY);
    if (iCanFd < 0) {
        fprintf(stderr, "open %s failed.\n", CAN_DEV_NAME);
        return  (-1);
    }
    while (1) {
        sstReadLen = read(iCanFd, &canframe, sizeof(CAN_FRAME));
        if (sstReadLen < 0) {
            fprintf(stderr, "read error.\n");
            break;
        }
        if (sstReadLen < sizeof(CAN_FRAME)) {
            continue;
        }
        uiNetId = canframe.CAN_uiId;
        uiAddr  = APP_ADDR(uiNetId);
        uiType  = APP_TYPE(uiNetId);
        switch (uiType) {
        case APP_TYPE_INT32:
        {
            INT32   iData;
            iData = __appByteToInt32(canframe.CAN_ucData);
            printf("node addr = %d, type = int32, value = %d.\n",
                     uiAddr, iData);
        }
            break;
        case APP_TYPE_STRING:
        {
            CHAR *pcData = (CHAR *)canframe.CAN_ucData;
            pcData[canframe.CAN_ucLen] = '\0';
            printf("node addr = %d, type = string, value = %s.\n",
 uiAddr, pcData);
        }
            break;
        default:
            break;
        }
    }
    close(iCanFd);
    return  (0);
}

上面为主节点程序,其功能非常简单,即不断地获取网络中来自从节点的数据,并根据数据类型作相应处理后打印出来。注意一定要以一个 CAN 帧为基本大小读取数据。

#include <SylixOS.h>
#include "appLib.h"
#define CAN_DEV_NAME        "/dev/can0"
int main(int argc, char *argv[])
{
    INT             iCanFd;
    CAN_FRAME       canframe;
    UINT            uiAddr;
    ssize_t         sstWriteLen;
    INT32           iData = 0;
    iCanFd = open(CAN_DEV_NAME, O_WRONLY);
    if (iCanFd < 0) {
        fprintf(stderr, "open %s failed.\n", CAN_DEV_NAME);
        return  (-1);
    }
    uiAddr  = __appSlaveAddrGet();
    canframe.CAN_uiId          = APP_NET_ID(APP_TYPE_INT32, uiAddr);
    canframe.CAN_ucLen         = sizeof(INT32);
    canframe.CAN_bExtId          = LW_FALSE;
    canframe.CAN_bRtr              = LW_FALSE;
    canframe.CAN_uiChannel    = 0;
    while (1) {
        __appInt32ToByte(canframe.CAN_ucData, iData);
        sstWriteLen = write(iCanFd, &canframe, sizeof(CAN_FRAME));
        if (sstWriteLen < 0) {
            fprintf(stderr, "write error.\n");
            break;
        }
        iData++;
        sleep(5);
    }
    close(iCanFd);
    return  (0);
}

上面的从节点程序每隔 5 秒钟向系统报告一次类型为整数的数据,需要做的工作仅仅是将 ID 里面的数据类型域设置为 APP_TYPE_INT32。

我们的示例程序中,并没有用到 CAN 帧里面的扩展帧和远程帧标识,也没有处理更多通信的细节。比如某一个节点通信出现异常时有效地恢复、通信的发起和应答、不同节点之间大小端数据的处理等。前面提到已有的 CAN 应用层协议均会处理这些保证通信稳定性和有效性的问题,并为应用提供方便的操作接口。

文档内容是否对您有所帮助?
有帮助
没帮助