当前位置: 首页 > article >正文

Android操作CAN总线(基于linux-canutils库,发送、接收、设置过滤器)

文章目录

    • 一、前言
    • 二、前言2
    • 三、前置知识
      • 3.1 epoll
      • 3.2 jni
    • 四、具体实现
      • 4.1 aar设计
      • 4.2 部分代码解读
    • 五、应用层调用示例
        • 1. 打开CAN口和接收功能
        • 2. 发送CAN帧
        • 3. 关闭CAN口
    • 六、参考文档
    • 七、附录

一、前言

CAN总线(Controller Area Network Bus)控制器局域网总线 CAN总线是构建的一种局域网网络。

最近工作涉及到在Android端发送和接收Can帧的内容,写一篇博客记录下。
主要是移植了linux-canutils库中的candump.c和cansend.c代码到aar中。
后续会给出aar和应用层代码的具体调用。
aar源码见七、附录

二、前言2

最开始的can_aar是基于SOCKETCAN实现的,这里给出SOCKET_CAN的官方文档(https://www.kernel.org/doc/html/v4.17/networking/can.html#socketcan-raw-sockets),SOCKETCAN是使用一个socket绑定所有CAN口,但是如果是高速发送:比如每帧间隔1ms 连续发送几百帧以上会有丢帧的问题,表征为概率性无法收到can帧
于是参照github上linux-canutils的代码对can_aar进行修改,设备有多个can口,
效果:使用两个CAN口和电脑端连接,电脑端间隔1ms发送一帧,电脑端发送的同时,设备端也发送CAN帧,共发送3000000+帧未发生丢帧的情况。

这里先给出linux-canutils candump.c和cansend.c的链接:
https://github.com/linux-can/can-utils/blob/master/candump.c
https://github.com/linux-can/can-utils/blob/master/cansend.c

candump对应通过can口读数据,cansend对应通过can口写数据,也分别对应cantest.c中的doRealCanReadBytes和canWriteBytesDebug两个函数。

三、前置知识

3.1 epoll

定位:Linux内核提供的一种高效I/O事件通知机制,专为处理大量并发连接设计(如Web服务器、实时通信系统)。

核心功能:监控多个文件描述符(如Socket)的I/O状态变化(可读/可写/异常),避免轮询消耗CPU资源。

设计目标:解决传统select/poll的性能瓶颈(线性扫描、内存拷贝、连接数限制)。
在这里插入图片描述

3.2 jni

作为java代码调用c代码的媒介,详见《Android开发艺术探索》

四、具体实现

4.1 aar设计

在这里插入图片描述
can_test.c: 各类函数具体实现的代码
com_example_x6_mc_cantest_CanUtils.h: jni函数

CanFilter: Can帧过滤器实体类
CanFrame: Can帧实体类
CanUtils: 封装给上层调用的工具类
DataListener:接口类,用于向上层回调数据

4.2 部分代码解读

CanUtils.java:

// 打开can口并使能
public void canOpen(String can, String baudRate) {
    execRootCmdSilent("ifconfig " + can + " down");
    execRootCmdSilent("ip link set " + can + " up type can bitrate " + baudRate);
}

// 提供给上层调用,也是jni函数,通过can口发送数据,具体实现见can_test.c
public native void canWriteBytesDebug(CanFrame canFrame, String canPort);

// 提供给上层调用,通过can口接收数据,具体实现见can_test.c
public void canReadBytesDebug(DataListener listener, ArrayList<String> can){
    createEpoll();
    for(int i = 0; i < can.size(); i++){
        doSocketBind(can.get(i));
    }
    doRealCanReadBytes(listener);
}

// 对上层不可见,jni函数,创建系统调用epoll,具体实现见can_test.c
private native void createEpoll();

// 对上层不可见,jni函数,真正实现读取can口数据的逻辑,监听并读取然后通过listener返回给上层
private native void doRealCanReadBytes(DataListener listener);

// 对上层可见,设置can帧过滤器
public native int canSetFilters(List<CanFilter> canFilters, String can);
// 对上层可见,清空过滤器
public native int canClearFilters();

// 对上层可见,关闭can口
public void canClose(String can) {
    execRootCmdSilent("ifconfig " + can + " down");
    Log.e(TAG,"hello from before doRealCanClose");
    doRealCanClose(can);
}

//对上层不可见,真正实现关闭can口,清空一些变量
private native int doRealCanClose(String can);

// 绑定socket到can口,对上层不可见,属于监听can口前的准备工作
private native int doSocketBind(String can);

static {
    System.loadLibrary("mc_can");
}

can_test.c:

//关注CanUtils.java中的canReadBytesDebug
// 1. canReadBytesDebug 先createEpoll()创建系统调用,再绑定socket到can接口,最后开启监听

JNIEXPORT void JNICALL
Java_com_example_x6_mc_1cantest_CanUtils_createEpoll(JNIEnv *env, jobject thiz) {
	LOGE("createEpoll begin");
	fd_epoll = epoll_create(1);
	if (fd_epoll < 0) {
		perror("epoll_create");
		return ;
	}
	LOGE("createEpoll end");
}

//该函数作用:1. 将Java字符串转为C字符串;
//2. 创建CAN原始套接字;
//3. 将套接字加入epoll监听队列;
//4. 配置网络接口参数;
//5. 绑定Socket到指定CAN接口,支持CAN FD模式。
JNIEXPORT jint JNICALL
Java_com_example_x6_mc_1cantest_CanUtils_doSocketBind(JNIEnv *env, jobject thiz, jstring can) {
	char *port = (*env)->GetStringUTFChars(env, can, NULL);
	LOGE("port = %s",port);
	struct if_info* obj = &sock_info[port[3] - '0'];
	obj->s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
	if (obj->s < 0) {
		LOGE("socket");
		return -1;
	}

	event_setup.data.ptr = obj; /* remember the instance as private data */
	if (epoll_ctl(fd_epoll, EPOLL_CTL_ADD, obj->s, &event_setup)) {
		LOGE("failed to add socket to epoll");
		return -1;
	}

	obj->cmdlinename = port; /* save pointer to cmdline name of this socket */
	nbytes = strlen(port); /* no ',' found => no filter definitions */

	if (nbytes > max_devname_len)
		max_devname_len = nbytes; /* for nice printing */

	addr.can_family = AF_CAN;

	memset(&ifr.ifr_name, 0, sizeof(ifr.ifr_name));
	strncpy(ifr.ifr_name, port, nbytes);

	if (strcmp(ANYDEV, ifr.ifr_name) != 0) {
		if (ioctl(obj->s, SIOCGIFINDEX, &ifr) < 0) {
			perror("SIOCGIFINDEX");
			exit(1);
		}
		addr.can_ifindex = ifr.ifr_ifindex;
	} else
		addr.can_ifindex = 0; /* any can interface */

	/* try to switch the socket into CAN FD mode */
	setsockopt(obj->s, SOL_CAN_RAW, CAN_RAW, &canfd_on, sizeof(canfd_on));

	if (bind(obj->s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
		LOGE("bind");
		return -1;
	}
//	(*env)->DeleteLocalRef(env,port);
}

// 该函数实现:当epoll监听到有数据时,就回调数据给上层
JNIEXPORT void JNICALL
Java_com_example_x6_mc_1cantest_CanUtils_doRealCanReadBytes(JNIEnv *env, jobject thiz, jobject listener) {
	running = 1;
	jclass callClass = (*env)->GetObjectClass(env,listener);
	jmethodID callMethod = (*env)->GetMethodID(env,callClass,"onData",
											   "(Ljava/lang/String;Ljava/lang/String;I)V");

	/* these settings are static and can be held out of the hot path */
	iov.iov_base = &frame;
	msg.msg_name = &addr;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = &ctrlmsg;

	while (running) {
		num_events = epoll_wait(fd_epoll, events_pending, currmax, timeout_ms);
		if (num_events == -1) {
			running = 0;
			continue;
		}
        LOGE("num_events = %d",num_events);
		for (int i = 0; i < num_events; i++) {  /* check waiting CAN RAW sockets */
			struct if_info* obj = events_pending[i].data.ptr;
			/* these settings may be modified by recvmsg() */
			iov.iov_len = sizeof(frame);
			msg.msg_namelen = sizeof(addr);
			msg.msg_controllen = sizeof(ctrlmsg);
			msg.msg_flags = 0;

			nbytes = recvmsg(obj->s, &msg, 0);

			if (nbytes < 0) {
				LOGE("nbytes < 0 is true\n");
				break;
			}
			if ((size_t)nbytes == CAN_MTU)
				maxdlen = CAN_MAX_DLEN;
			else if ((size_t)nbytes == CANFD_MTU)
				maxdlen = CANFD_MAX_DLEN;
			else {
				LOGE("read: incomplete CAN frame\n");
				break ;
			}
			int idx = idx2dindex(addr.can_ifindex,obj->s);
//			LOGE("CAN口 = %s",devname[idx]);
			frame_count ++;
			LOGE("recv frame count = %d\n",frame_count);
			fprint_long_canframe(stdout, &frame, NULL, view, maxdlen); //输出收到的can帧数据:canid can帧长度 can帧数据
			jstring data = (*env)->NewStringUTF(env,buf);
			jstring canPort = (*env)->NewStringUTF(env,devname[idx]);
			(*env)->CallVoidMethod(env,listener,callMethod,data,canPort,frame_count);
            (*env)->DeleteLocalRef(env,data);
            (*env)->DeleteLocalRef(env,canPort);
		}
	}
    LOGE("canReadBytesDebug end");
	(*env)->DeleteLocalRef(env,callClass);
}

五、应用层调用示例

前言:

TestCan是进行CAN测试的APK示例,基于can_aar (linux-can-utils)实现。

目前只支持3位canid(表示标准帧)和8位canid(表示扩展帧),可以设置Can帧过滤器

须在build.gradle中添加一行代码以使用can_aar

implementation files('libs/can-debug.aar')

can_aar包含以下功能:

1. 打开CAN口和接收功能
private CanUtils canUtil; //can工具类实例

public void openCan() {
    canUtil = CanUtils.getInstance();

    canUtil.canOpen("can0","1000000");
    canUtil.canOpen("can1","1000000");
    canUtil.canOpen("can2","1000000");

    ArrayList<String> canList = new ArrayList<>();
    canList.add("can0");
	canList.add("can1");
	canList.add("can2");

    Executors.newCachedThreadPool().execute(new Runnable() {
        @Override
        public void run() {
            canUtil.canReadBytesDebug(new DataListener() {
                @Override
                public void onData(String data, String canPort, int frameCount) {
                    Log.e(TAG, "canPort == " + canPort + " data == " + data + " frameCount == " + frameCount);
                }
            }, canList);
        }
    });
    //监听3个can口,就将can0,can1,can2都加入canList中
}
2. 发送CAN帧
//CanFrame实体类
public class CanFrame {
    public String canId; //can帧id
    public String data; //can帧数据

    public CanFrame() {
    }
}
// 发送代码示例
if (canUtil != null) {
    CanFrame canFrame = makeCanFrame();
    canUtil.canWriteBytesDebug(canFrame,"can0"); // 通过can0口发送
    canUtil.canWriteBytesDebug(canFrame,"can1"); // 通过can1口发送
    canUtil.canWriteBytesDebug(canFrame,"can2"); // 通过can2口发送
}

private CanFrame makeCanFrame() {
    CanFrame canFrame = new CanFrame();
    canFrame.canId = "123";
    canFrame.data = "1122334455667788"
    return canFrame;
}

3. 关闭CAN口
public void closeCan() {
    if (canUtil != null) {
        canUtil.canClose();
    }
    canUtil = null;
}

六、参考文档

https://www.xiaolincoding.com/os/8_network_system/selete_poll_epoll.html#epoll
https://github.com/linux-can/can-utils

七、附录

源码见:https://github.com/qilin02811/can_aar


http://www.kler.cn/a/580151.html

相关文章:

  • 【2025】基于PHP+Vue的电影购票系统(源码+文档+调试+图文修改+答疑)
  • 基于muduo+mysql+jsoncpp的简易HTTPWebServer
  • 【医院内部控制专题】7.医院内部控制环境要素剖析(三):人力资源政策
  • 发起请求的步骤
  • JVM 类加载原理之双亲委派机制(JDK8版本)
  • ThreadLocal源码剖析
  • 网络安全通信架构图
  • CPU 负载 和 CPU利用率 的区别
  • 数据结构——树,二叉树
  • 火语言RPA--PDF转Word
  • mysql的锁--一篇读懂所有锁机制
  • 微服务项目如何部署?
  • 截取一对中括号里面的内容
  • 深入解析K8s VolumeMounts中的subPath字段及其应用
  • Unity开发——CanvasGroup组件介绍和应用
  • Netty基础—1.网络编程基础一
  • Python助力区块链数据可视化:从数据到洞见
  • vue项目纯前端把PDF转成图片并下载
  • [250310] Mistral 发布世界领先的文档理解 API:Mistral OCR | 谷歌利用 AI 保护自然的三种新方式
  • 三星首款三折叠手机被曝外屏6.49英寸:折叠屏领域的新突破