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