因毫米波雷达通过CAN口进行数据传输,为便于在LInux系统下进行毫米波雷达数据的获取与解析,简单收集整理了一些CAN总线及CAN接口应用开发的资料。本文主要是记录学习利用SocketCAN接口进行CAN总线数据的收发。仍在学习过程中,难免存在理解误区,欢迎批评指正,共同进步。
1、基础:CAN总线简介
-
CAN 是控制器局域网络(Controller Area Network,CAN)的简称,由德国BOSCH公司开发,并最终成为国际标准(ISO 11898-1)。CAN总线主要应用于工业控制和汽车电子领域,是国际上应用最广泛的现场总线之一。
-
CAN 总线是一种串行通信协议,能有效地支持具有很高安全等级的分布实时控制。
-
CAN 总线规范从最初的CAN 1.2 规范(标准格式)发展为兼容CAN 1.2 规范的CAN 2.0 规范(CAN 2.0A为标准格式,CAN 2.0B为扩展格式),目前应用的CAN器件大多符合CAN 2.0规范。
-
当CAN 总线上的节点发送数据时,以报文形式广播给网络中的所有节点,总线上的所有节点都不使用节点地址等系统配置信息,只根据每组报文开头的11位标识符(CAN 2.0A规范)解释数据的含义来决定是否接收。这种数据收发方式称为面向内容的编址方案。
2、Linux系统中CAN总线接口配置
-
在Linux系统中,CAN总线接口设备作为网络设备被系统进行统一管理。在控制台下, CAN总线的配置和以太网的配置使用相同的命令。
-
可以在终端输入如下命令查看CAN接口信息
ifconfig -a
图片来源,其中,eth0设备是以太网接口,can0和can1设备是两个CAN总线接口。
- 使用 ip 命令来配置CAN总线的波特率:(下列命令均以can0为例)
ip link set can0 up type can bitrate 1000000 //设置can0的波特率为1Mbps
- 显示can0设备的详细信息:
ip -details link show can0
- 设置完成后,可以用下列命令来使用或取消can0设备:
ifconfig can0 up //使用can0设备
ifconfig can0 down //取消使用can0设备
- 其它一些Linux下调试can0设备常用命令
ip link set can0 down //关闭can0网络
ip link set can0 up //打开can0网络
candump can0 //接收can0数据
canconfig can0 bitrate + 波特率
canconfig can0 start //启动can0设备
canconfig can0 ctrlmode loopback on //回环测试
canconfig can0 restart //重启can0设备
canconfig can0 stop //停止can0设备
canecho can0 //查看can0设备总线状态
cansend can0 --identifer=ID+数据 //发送数据;
candump can0 --filter=ID:mask //使用滤波器接受ID匹配的数据
3、Linux系统中CAN接口应用开发
由于系统将CAN设备作为网络设备进行管理,因此在CAN总线应用开发方面,Linux提供了SocketCAN接口,使得CAN总线通信近似于和以太网的通信。
此外,通过https://gitorious.org/linux-can/can-utils 网站发布的基于SocketCAN的can-utils工具套件,也可以实现CAN总线通信。
- SocketCAN 中大部分的数据结构和函数在头文件 linux/can.h 中进行了定义。 CAN 总线套接字的创建采用标准的网络套接字操作来完成。网络套接字在头文件 sys/socket.h 中定义。套接字的初始化方法如下:
int s;
struct sockaddr_can addr;
struct ifreq ifr;
s = socket(PF_CAN, SOCK_RAW, CAN_RAW); //创建 SocketCAN 套接字()
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr); //指定 can0 设备
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr)); //将套接字与 can0 绑定
其中: ifreq结构定义在/usr/include/net/if.h,用来配置ip地址,激活接口,配置MTU等接口信息的。其中包含了一个接口的名字和具体内容——(是个共用体,有可能是IP地址,广播地址,子网掩码,MAC号,MTU或其他内容)。ifreq包含在ifconf结构中。而 ifconf结构通常是用来保存所有接口的信息的。
- 在数据收发的内容方面,CAN 总线与标准套接字通信稍有不同,每一次通信都采用 can_ frame 结构体将数据封装成帧。 结构体定义如下:
struct can_frame{
canid_t can_id; //CAN标识符
__u8 can_dlc; //数据场的长度
__u8 data[8]; //数据
}
- 数据发送使用 write 函数来实现。
struct can_frame frame;
frame.can_id = 0x123;
frame.can_dlc = 1; //数据长度为 1
frame.data[0] = 0xAB; //数据内容为 0xAB
int nbytes = write(s, &frame, sizeof(frame)); //发送数据
if(nbytes != sizeof(frame)) //如果 nbytes 不等于帧长度,就说明发送失败
- 数据接收使用 read 函数来完成:
struct can_frame frame;
int nbytes = read(s, &frame, sizeof(frame));
4、自定义用于CAN口数据收发的SocketCAN类接口
- 引入需要用到的头文件
#include <cstdint>
#include <errno.h> //错误号头文件,包含系统中各种出错号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //Linux 标准头文件,定义了各种符号常数和类型
#include <string.h>
#include <net/if.h> //用来配置和获取ip地址,掩码,MTU等接口信息的
#include <sys/types.h> //类型头文件,定义了基本的系统数据类型。
#include <sys/socket.h> //定义socket的头文件
#include <sys/ioctl.h> //包含设备I/O通道管理的函数
#include <linux/can.h> //包含了SocketCAN中大部分的数据结构和函数
#include <linux/can/raw.h>
- 声明命名空间及类的属性和成员函数
namespace socket_can{
class SocketCAN{
public:
SocketCAN(const char * ifname);
SocketCAN(const char * ifname, long timeout);
~SocketCAN();
bool is_connected(); //用于确定是否建立连接
bool write(uint32_t frame_id, uint8_t dlc, uint8_t * data); //写入数据
bool read(uint32_t * can_id, uint8_t * dlc, uint8_t * data); //获取数据
private:
void init();
const char * ifname_; //用于指定网络设备的名称
int socket_; //用于接收创建的socket返回的描述符
bool connected_;
long timeout_;
};
}
- 类的实现
#include <socket_can/socket_can.hpp>
namespace socket_can{
SocketCAN::SocketCAN(const char * ifname) :
ifname_(ifname), //指定网络设备的名称
connected_(false), //创建socket默认设置为未连接状态
timeout_(1000l) //设置默认的套接字超时时间
{
init(); //默认调用SocketCAN类的初始化函数
}
SocketCAN::SocketCAN(const char * ifname, long timeout) :
ifname_(ifname),
connected_(false),
timeout_(timeout) //从外部获取套接字超时时间
{
init();
}
SocketCAN::~SocketCAN(){
if (close(socket_) < 0) {
perror("Closing: ");
printf("Error: %d", errno); //如果未成功关闭socket描述符对应的操作空间,则输出错误信息
}
}
void SocketCAN::init(){
if ((socket_ = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) { //其中:PF_CAN为所用的协议族;SOCK_RAW为所用的套接字类,这里采用的是原始套接字;CAN_RAW是指原始CAN协议
perror("Error while opening socket"); //创建socket套接字,并对返回的描述符进行判断,失败(<0)则输出错误信息并返回
return;
}
struct ifreq ifr{}; //定义ifeq结构体,用于配置和获取接口信息
strcpy(ifr.ifr_name, ifname_); //将从外部获取网络设备名称拷贝给ifr.ifr_name
ioctl(socket_, SIOCGIFINDEX, &ifr); //获取网络设备接口地址
struct sockaddr_can addr{}; //通用地址结构
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex; //设置CAN协议
printf("%s at index %d\n", ifname_, ifr.ifr_ifindex);
if (bind(socket_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { //将刚生成的套接字与网络地址进行绑定,并对bind()的返回值进行判断,失败则输出错误信息并返回
perror("Error in socket bind");
return;
}
int error = 0; //用于保存错误代码
socklen_t len = sizeof (error);
int retval = getsockopt (socket_, SOL_SOCKET, SO_ERROR, &error, &len); //获取socket_的状态
if (retval != 0) {
/* there was a problem getting the error code */
printf("Error getting socket error code: %s\n", strerror(retval));
return;
}
if (error != 0) {
/* socket has a non zero error status */
printf("Socket error: %s\n", strerror(error)); //将error中保存的错误代码输出
return;
}
struct timeval timeout{}; //设置超时时间
timeout.tv_sec = (timeout_ / 1000);
timeout.tv_usec = (timeout_ % 1000) * 1000;
if (setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (char *) & timeout, sizeof(timeout)) < 0) { //SO_RCVTIMEO参数表示设置socket接收超时时间
perror("Setting timeout failed");
}
connected_ = true;
}
bool SocketCAN::is_connected(){ //获取套接字连接状态
return connected_;
}
bool SocketCAN::write(uint32_t can_id, uint8_t dlc, uint8_t * data){ //数据写入函数
struct can_frame frame{};
frame.can_id = can_id;
frame.can_dlc = dlc;
memcpy(frame.data, data, dlc * sizeof(uint8_t)); //将要写入的数据保存到can_frame结构体中
auto num_bytes = ::write(socket_, &frame, sizeof(struct can_frame)); //获取写入的字节数
return num_bytes > 0;
}
bool SocketCAN::read(uint32_t * can_id, uint8_t * dlc, uint8_t * data){
struct can_frame frame{};
auto num_bytes = ::read(socket_, &frame, sizeof(struct can_frame)); //获取读到的字节数
if (num_bytes != sizeof(struct can_frame)) {
return false; 如果返回的bytes不等于帧长度,则读取失败,并返回 false
}
(* can_id) = frame.can_id;
(* dlc) = frame.can_dlc;
memcpy(data, frame.data, sizeof(frame.data)); //将读取的数据保存到传出参数中
return true;
}
}
- SocketCAN类的使用
```cpp
//创建类对象及其初始化
socket_can::SocketCAN can_("can0");
//类的构造函数中已经完成了socket的创建以及与本地网络地址绑定的工作,若无异常则说明连接建立成功
/*数据获取
bool SocketCAN::read(uint32_t * can_id, uint8_t * dlc, uint8_t * data)
自定义数据读取函数的参数均为指针类型,因此在调用前需要创建对应类型的变量
为确保数据读取成功,还需要对read()函数的返回值进行判断
*/
uint32_t frame_id;
uint8_t dlc;
uint8_t data[8] = {0};
bool read_status = can_.read(&frame_id, &dlc, data);
if (!read_status) {
return false;
}
/*数据写入
bool SocketCAN::write(uint32_t can_id, uint8_t dlc, uint8_t * data)
*/
uint8_t raw_data[8]
can_.write(frame_id, 8, raw_data)