我司最近有款轮式巡检机器人用到了海康机器人的工业相机MV-CA060-10GC
,我们的开发平台是树莓派(运行Ubuntu Server 1804),开发语言是Java,但该相机没有Java SDK,于是我决定自己开发一个。
好消息是海康机器人提供了C语言的SDK,这样我就能通过JNA直接调用,而不必写一行C代码。
开发过程中发现,有2个API在Windows下正常运行,在树莓派下却总是报错误的参数
,错误码80000004
第一个API负责获取原始图像,第二个负责将原始图像压缩编码成jpg或bmp等文件格式,都是必须要用的API
int MV_CC_GetOneFrameTimeout( void *handle,
unsigned char *pData, unsigned int nDataSize,
MV_FRAME_OUT_INFO_EX *pFrameInfo, int nMsec);
int MV_CC_SaveImageEx2( IN void* handle,
MV_SAVE_IMAGE_PARAM_EX *pSaveParam);
向厂商的技术支持求助,技术支持问样例代码在树莓派上运行正常吗?我试了试,发现正常,技术支持说那就是你们代码写得有问题,仔细查查吧。
我的Java代码应该不会有问题,否则Windows上就报错了,平台差异JNA已经帮我屏蔽了呀,除非JNA屏蔽得有问题。
注意到JNA在不通平台有相应的jnidispatch
动态库,而树莓派是armhf
架构,会不会是因为JNA没有linux-armhf
版jnidispatch
所致?
将Ubuntu server换成64位版,对应架构是linux-aarch64
,所以是有专属的jnidispatch
的,但错误依旧。
怀疑是Ubuntu上的JDK有问题,那换树莓派官方发行版Raspbian应该没问题了吧?可惜,错误依旧。
另外Raspbian没有64位版,所以也没法进一步试
项目进度逼迫之下,我用JNI方式实现了轮询式拍照,这是最简单的拍照方式,顺利实现功能。
但是测试中发现在Linux-arm下有一个BUG,如果开启抓取后几秒钟内没有调用GetOneFrameTimeout,则后续再调用就永远返回无数据
(错误码80000007
)且无法恢复,现在看来应该是缓冲区没管理好。
这个问技术支持,也没下文。
最后自己想出规避办法,每次在拍照前开启抓取,开启后立即读取帧缓存,读完立即停止抓取。
但是在测试中又发现新问题,因为巡检机器人面对的环境光照强度差别较大,所以将相机配置成自动曝光后,每次开启抓取后等待曝光稳定就要好几秒,效率很低下。
总之,就用这么慢的相机,我们的机器人通过了中期验收
验收通过后,总结主要问题,拍照慢明显排第一。琢磨解决方法,既然第一个BUG无法解决,那么试试回调式拍照。
先在回调拍照的样例代码上做了几个实验,发现该方式有以下特点:
无数据
问题,因为SDK会启动一个线程不停地从相机帧缓存读取帧数据到用户缓存,用户回调函数对帧缓存处理完毕就会回收考虑重新用JNA做,因为JNI依赖的C做线程同步是很麻烦的,最好能用Java来做(synchronized关键字不要太简单)。
参照回调样例代码翻译成Java版,期间熟悉了JNA访问Union、注册回调,总算编译通过了,但一运行MV_CC_SaveImageEx2
函数就报错误的参数
这个老问题
既然报错误的参数,那就说明参数格式有误,而不是内存不足或功能不支持等其他错误,所以检查MV_CC_SaveImageEx2
的2个参数,第一个handle
嫌疑很小,如果有问题前面的开启抓取就会失败,另一个参数pSaveParam
是我自己创建的,按理说不会有问题,难道是回调函数的入参有问题?
void(__stdcall* cbOutput)( unsigned char *pData,
MV_FRAME_OUT_INFO_EX *pFrameInfo, void *pUser);
在IDEA下检查3个入参,pData
确实是图像数据,能看到相邻2个像素的值很接近;pUser
也确实是我传的handle;就剩下pFrameInfo
嫌疑最大。
导出x64下和arm下pFrameInfo
内存内容,比对发现问题!
注意红线标注的数字,是enPixelType
字段(C下是枚举类型,JNA推断其存储类型
为int
)的一种取值PixelType_Gvsp_RGB8_Packed
,x64下正常,arm下却被赋值给nFrameNum
字段,很诡异。
因为pFrameInfo
的nFrameLen
和enPixelType
都会分别赋值给pSaveParam
的nDataLen
和enPixelType
字段,所以拦截掉这2处赋值,替换为x64下的正常值,错误依旧。
最后没办法,想着只能是pSaveParam
本身哪里有问题,于是比对该结构体在x64下和arm下的内存内容,结果发现一处异常!
发现x64比arm多了12字节,而不是我以为的8字节(2个pointer,x64下每个pointer 8字节),原来额外多出来的4字节是因为pImageBuffer
前面为保证8字节对齐
而填充了4字节,白高兴一场。
想看一下样例c代码中pSaveParam
的内存内容,跟JNA申请的native内存里,是否一样。用gdb调试样例C代码,在回调执行编码函数处打断点,断住后
(gdb) p sizeof(MV_FRAME_OUT_INFO_EX)
$2 = 56
pSaveParam
的size竟然是56,而同样跑在arm下,JNA版却是52,为何会不一样?逐个检查结构体的每个字段,最终找到异常字段enPixelType
,其size是8!
(gdb) p sizeof(MvGvspPixelType)
$16 = 8
至于为什么是8?我做了一系列实验寻找答案,这应该是一种未定义的行为,要尽量避免
就是厂商开发人员将PixelType_Gvsp_Undefined
的值定义成-1导致的
enum MvGvspPixelType
{
// Undefined pixel type
#ifdef WIN32
PixelType_Gvsp_Undefined = 0xFFFFFFFF,
#else
PixelType_Gvsp_Undefined = -1,
#endif
}
猜测厂商开发人员一开始所有平台都是-1
,后来发现win32平台编译不过,必须改成0xffffffff
才能过,为了变动最小,于是加个条件编译宏,殊不知微软的编译报错才是用心良苦。
知道是因为enum
size变8字节导致JNA解析方法跟内存内容对不上,进而出现错误,所以解决办法就是更改JNA的解析方法,同时不影响x64
一开始尝试自定义type mapper,但随着了解的深入,发现不用大动干戈,只要为这个特殊的enum单独定义converter
就行,但是这还不够简单,更简单的方法是让该enum实现NativeMapped
接口,这样JNA就能将该enum当作int
、long
之类的已知对应native类型的类来看待了。
完整的解决方案,见我的这个repo
如果相同的JNA代码,在一个平台OK,另一个不OK,那么一定是native部分出了问题。
native部分又分两种情况,一种是我们的JNA wrapper类没有为特定平台做适配,另一种是特定平台本身C代码有问题。怎么判定到底是那一种?
方法就是:比对JNA分配的内存
跟C分配的内存
,看哪一个跟头文件中结构体的定义
有出入,问题就在哪边。
为何枚举值定义成-1
会触发这种异常行为?猜测是编译器实现enum时,会先检查所有枚举值中的最大值,如果是32位的,就至少用32位来存
再检查是否有负数,有的话说明是有符号的,那枚举值的字面量就只能在-231到231-1之间了
再检查有没有字面量最高位为1的,如果有,编译器就认为用户想要这个字面量为正值,这样就超出了int32
的表示范围,那就只能扩成int64
了
在Linux-amd64平台上测试,MvGvspPixelType
的size也是8字节
test.c: In function ‘main’:
test.c:8:38: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long unsigned int’ [-Wformat=]
printf("sizeof enum PixelType = %d\n", sizeof(enum PixelType));
~^
%ld
test.c:10:24: warning: format ‘%llx’ expects argument of type ‘long long unsigned int’, but argument 2 has type ‘long int’ [-Wformat=]
printf("RGB = 0x%llx\n", RGB);
~~~^
%lx
whp@whp-STCK1A32WFC:~$ ./a.out
sizeof enum PixelType = 8
MONO = 0xffffffff
RGB = 0x80000000
所以用芯片类型来区分enum szie是不合适的,有空改下代码