Chinaunix首页 | 论坛 | 博客
  • 博客访问: 57283
  • 博文数量: 8
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 105
  • 用 户 组: 普通用户
  • 注册时间: 2015-06-19 10:45
个人简介

分享笔者从事嵌入式工作几年来在Linux, SylixOS等操作系统中关于内核、驱动框架以及各种中间件开发和设计的技术。

文章分类
文章存档

2015年(8)

我的朋友

分类: 嵌入式

2015-08-14 16:48:52

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

目前,CAN应用层协议有DeviceNetCANopenCAL等,它们针对不同的应用场合有自己的协议标准。限于篇幅,本篇不介绍CAN底层协议以及应用协议的具体知识,有兴趣的读者可通过其他途径了解更多相关内容。

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


  1. typedef struct {
  2.     UINT    CAN_uiId;                         /* 标识码 */
  3.     UINT    CAN_uiChannel;                   /* 通道号 */
  4.     BOOL    CAN_bExtId;                       /* 是否是扩展帧 */
  5.     BOOL    CAN_bRtr;                         /* 是否是远程帧 */
  6.     UCHAR   CAN_ucLen;                        /* 数据长度 */
  7.     UCHAR   CAN_ucData[CAN_MAX_DATA];       /* 帧数据 */
  8. } CAN_FRAME;
  9. typedef CAN_FRAME *PCAN_FRAME;               /* CAN帧指针类型 */



成员CAN_uiIdCAN节点标示符,在一个CAN总线系统中,每个节点的标示符都是唯一的。如果成员CAN_bExtIdFALSE,这表示一个标准帧,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里。

 CAN自定义应用协议appLib.h:


  1. #ifndef __APP_LIB_H
  2. #define __APP_LIB_H

  3. #define APP_TYPE_MASTER 0
  4. #define APP_TYPE_INT32 1
  5. #define APP_TYPE_STRING 2

  6. #define APP_ADDR_MASTER 0

  7. #define APP_TYPE(id)       ((id >> 7) & 0x0f)
  8. #define APP_ADDR(id)       (id & 0x3f)

  9. #define APP_NET_ID(t, a)  ((((UINT)t & 0x0f) << 7) | ((UINT)a & 0x3f))

  10. static inline INT32 __appByteToInt32 (const UCHAR *pucByte)
  11. {
  12.     INT32 iData;

  13.     iData = ((INT32)pucByte[0])
  14.           | ((INT32)pucByte[1] << 8)
  15.           | ((INT32)pucByte[2] << 16)
  16.           | ((INT32)pucByte[3] << 24);

  17.     return (iData);
  18. }

  19. static inline VOID __appInt32ToByte (UCHAR *pucByte, INT32 iData)
  20. {
  21.     pucByte[0] = iData & 0xff;
  22.     pucByte[1] = (iData >> 8) & 0xff;
  23.     pucByte[2] = (iData >> 16) & 0xff;
  24.     pucByte[3] = (iData >> 24) & 0xff;
  25. }

  26. extern UINT __appSalveAddrGet(VOID);

  27. #endif /* __APP_LIB_H */


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

为主节点预留了一个设备地址,这说明,整个系统中,只能存在一个主节点。

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

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

 CAN自定义协议之主节点示例:

  1. #include <SylixOS.h>
  2. #include "appLib.h"

  3. #define CAN_DEV_NAME "/dev/can0"

  4. int main(int argc, char *argv[])
  5. {
  6.     INT          iCanFd;
  7.     CAN_FRAME    canframe;
  8.     UINT         uiType;
  9.     UINT         uiAddr;
  10.     ssize_t      sstReadLen;
  11.     UINT         uiNetId;

  12.     iCanFd = open(CAN_DEV_NAME, O_RDONLY);
  13.     if (iCanFd < 0) {
  14.         fprintf(stderr, "open %s failed.\n", CAN_DEV_NAME);
  15.         return (-1);
  16.     }

  17.     while (1) {
  18.         sstReadLen = read(iCanFd, &canframe, sizeof(CAN_FRAME));
  19.         if (sstReadLen < 0) {
  20.             fprintf(stderr, "read error.\n");
  21.             break;
  22.         }
  23.         if (sstReadLen < sizeof(CAN_FRAME)) {
  24.             continue;
  25.         }

  26.         uiNetId = canframe.CAN_uiId;
  27.         uiAddr  = APP_ADDR(uiNetId);
  28.         uiType  = APP_TYPE(uiNetId);

  29.         switch (uiType) {
  30.         case APP_TYPE_INT32:
  31.         {
  32.             INT32 iData;
  33.             iData = __appByteToInt32(canframe.CAN_ucData);
  34.             printf("node addr = %d, type = int32, value = %d.\n", uiAddr, iData);
  35.         }
  36.             break;
  37.         case APP_TYPE_STRING:
  38.         {
  39.             CHAR *pcData = (CHAR *)canframe.CAN_ucData;
  40.             pcData[canframe.CAN_ucLen] = '\0';
  41.             printf("node addr = %d, type = string, value = %s.\n", uiAddr, pcData);
  42.         }
  43.             break;

  44.         default:
  45.             break;
  46.         }
  47.     }

  48.     close(iCanFd);
  49.     return (0);
  50. }

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

CAN定义协议之从节点示例:

  1. #include <SylixOS.h>
  2. #include "appLib.h"

  3. #define CAN_DEV_NAME "/dev/can0"

  4. int main(int argc, char *argv[])
  5. {
  6.     INT        iCanFd;
  7.     CAN_FRAME  canframe;
  8.     UINT       uiAddr;
  9.     ssize_t    sstWriteLen;
  10.     INT32      iData = 0;

  11.     iCanFd = open(CAN_DEV_NAME, O_WRONLY);
  12.     if (iCanFd < 0) {
  13.         fprintf(stderr, "open %s failed.\n", CAN_DEV_NAME);
  14.         return (-1);
  15.     }

  16.     uiAddr = __appSalveAddrGet();
  17.     canframe.CAN_uiId      = APP_NET_ID(APP_TYPE_INT32, uiAddr);
  18.     canframe.CAN_ucLen     = sizeof(INT32);
  19.     canframe.CAN_bExtId    = LW_FALSE;
  20.     canframe.CAN_bRtr      = LW_FALSE;
  21.     canframe.CAN_uiChannel = 0;

  22.     while (1) {
  23.         __appInt32ToByte(canframe.CAN_ucData, iData);

  24.         sstWriteLen = write(iCanFd, &canframe, sizeof(CAN_FRAME));
  25.         if (sstWriteLen < 0) {
  26.             fprintf(stderr, "write error.\n");
  27.             break;
  28.         }

  29.         iData++;
  30.         sleep(5);
  31.     }

  32.     close(iCanFd);
  33.     return (0);
  34. }

上面的从节点程序每隔5秒钟向系统报告一次类型为整数的数据,需要做的工作仅仅是将ID里面的数据类型域设置为APP_TYPE_INT32。我们的示例程序中,并没有用到CAN帧里面的扩展帧和远程帧标识,也没有处理更多通信的细节。比如某一个节点通信出现异常时有效地恢复、通信的发起和应答、不同节点之间大小端数据的处理等。前面提到已有的CAN应用层协议均会处理这些保证通信稳定性和有效性的问题,并为应用提供方便的操作接口。

阅读(2337) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~