文章目录
一、前言
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;} elseaddr.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包含以下功能:
CAN_236">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() {@Overridepublic void run() {canUtil.canReadBytesDebug(new DataListener() {@Overridepublic void onData(String data, String canPort, int frameCount) {Log.e(TAG, "canPort == " + canPort + " data == " + data + " frameCount == " + frameCount);}}, canList);}});//监听3个can口,就将can0,can1,can2都加入canList中
}
CAN_268">2. 发送CAN帧
//CanFrame实体类
public class CanFrame {public String canId; //can帧idpublic 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;
}
CAN_299">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