博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android照相功能驱动层中HAL的实现(基于OK6410开发板+OV9650摄像头)
阅读量:2152 次
发布时间:2019-04-30

本文共 8402 字,大约阅读时间需要 28 分钟。

Motivation 

前些日子买了块飞凌OK6410的开发板+OV9650摄像头模块准备做Android应用开发。自己手里虽有现成的Android手机,但考虑到日后裁减硬件,不得不从最原始的开发板着手。但不知飞凌出于什么原因,没有完善Android的照相驱动,每次拍照后,返回的照片都是一张绿色的小机器人。之前没有写过Android的驱动,在飞凌的技术支持论坛提问也没得到什么帮助,尝试刷前几个飞凌提供的Android版本,都没有解决这个问题...看来要等官方完善得有些时日了。与其指望他人,还不如自己动手,于是着手开发了这个模块。

本文涉及到以下几个方面的内容:

  1. Android 模块编译
  2. Android 模块的板上加载及调试
  3. Android Camera 模块的改写

Android 模块编译

 每次为了一个模块而编译整个Android系统是一个灾难(4个小时一次),这里会展示如何仅仅编译一个模块而节省大量的宝贵时间。网上多数的方法是通过执行envsetup.sh,接着运行mmm <directory>命令来编译一个文件夹下的模块,但在编译libcamera这个模块时一直没能成功,显示编译依赖于其他几个模块。这里介绍另一种方法,每个模块的文件夹下都必须有一个Android.mk文件,在其中有一项LOCAL_MODULE用于定义模块名称,以照相模块为例,即被定义为LOCAL_MODULE:=libcamera,记下这个模块名称,跳转到Android源码的根目录下,执行以下操作:

Step 1.  进入宿主机linux终端,输入以下命令:

 

<
name
>
@
<
machine
>
:
<
folder
>#source ./build/envsetup.sh

<name>@<machine>:<folder>#choosecombo

 

执行效果如图:

 

 

 

 

Step 2.  选择Device->Release->键入OK6410->eng

Step 3.  输入make <libname>编译特定模块,如摄像头模块: 

 

<
name
>
@
<
machine
>
:
<
folder
>#
make libcamera

 

执行效果如图: 

 

 

编译完成效果图:

 

 

Step 4. 经过以上几个步骤后,摄像头模块就开始编译了,生成后的动态连接库文件(*.so)会存放在out/target/product/OK6410/system/lib/下,本文我们仅需要libcamera.so

我把上述步骤做成了一个shell脚本,每次修改照相模块的HAL后会自动编译,并将更新后的libcamera.so拷贝到Android源码根目录下,如果愿意,也可以自行修改脚本将libcamera.so拷贝到SD中。 

附件下载:

 


Android 模块的板上加载及调试 

libcamera.so已经生成了,那怎么调试呢?一种办法是加载到模拟的Android系统中,但这种方法对于硬件调试往往行不通,那剩下的方法就是板上调试了。如果板子已经能够和PC进行adb连接,那就用adb push把libcamera.so推到目标机/system/lib/中。但可能是OK6410 USB接口设计的问题,与MacOSX总是无法建立起连接,于是每次我只能通过SD卡进行中转...手动从SD卡上把照相模块cp到lib目录下,然后reboot。

嵌入式开发比起应用开发,其开发环境往往要恶劣许多。就拿调试而言,往往要通过代码中插入类printf的语句来查看运行状态。android中提供了一个很好的工具logcat,在用户空间中,通过LOGV(Verbose),LOGE(Error),LOGD(Debug)等提供类似printf的功能。假定在程序中#define LOG_TAG "CameraHardware",那通过如LOGE("%s, Hello World!", LOG_TAG)就可以记录在系统日志中。系统日志杂乱繁多,要查看特定的日志就要限定范围,在目标机上定义ANDROID_LOG_TAGS环境变量就可以通过logcat -d来查看CameraHardware的“错误”日志了:

 

export ANDROID_LOG_TAGS
=
"
CameraHardware:E *:S
"
logcat -d

 

目标机和宿主机相连后,通过超级终端来执行以上命令后的结果:

 


Android Camera 模块的改写

这是本文的重点,展示如何在驱动层实现拍照功能。通过查看飞凌的Android源代码会发现,其OV9650和USB摄像头HAL的实现就是Android Fake Camera的改写,前者位于<android root folder>/hardware/forlinx/libcamera,后者位于<android root folder>/frameworks/base/services/camera/libcameraservice中。在Android中,OV9650已经有了基本的预览功能,这证明至少Preview函数已经完善,我们就从preview功能切入,来分析它的实现。打开libcamera下的S3C6410CameraHardware.cpp,在initDefaultParapeters方法中,可以看到preview的格式是RGB565,是一种常用于TFT显示的格式。原FakeCamera中是YUV420SP(从Ov965xCamera.cpp中看得出,飞凌尝试过用YUV420SP但可能失败了):

void
 CameraHardware::initDefaultParameters()
{
    CameraParameters p;
    p.
set
(CameraParameters::KEY_SUPPORTED_PREVIEW_SIZES, 
"
320x240
"
);
    p.setPreviewSize(
320
240
);
    p.setPreviewFrameRate(
15
);
//
    p.setPreviewFormat(CameraParameters::PIXEL_FORMAT_YUV420SP);
    p.setPreviewFormat(CameraParameters::PIXEL_FORMAT_RGB565);
    p.
set
(CameraParameters::KEY_ROTATION, 
0
);
//
90
//
 其余代码省略
    }
    
}

现在让我们看看Preview功能的实现,也许可以给我们启发,我们不难注意到previewThread方法,其中mPreviewHeap存储着n个帧的缓冲,这块区域被分割为n个mBuffers。buffer为当前帧的引用,通过mDataCb(CAMERA_MSG_PREVIEW_FRAME, buffer, mCallbackCookie)就可以将buffer输出到屏幕。那每一个帧是怎么存到mPreviewHeap上的呢?关键的一句就是Ov965xCamera->getNextFrameAsRgb565((uint16_t *)frame) ,通过看它的实现可以知道(在Ov96xCamera.app中),一个帧的数据以16位的格式写入frame中,这里的frame即是对mPreviewHeap上某个mBuffer的引用:

int CameraHardware::previewThread() 

{

    mLock.lock();
        
// the attributes below can change under our feet...
        
int previewFrameRate = mParameters.getPreviewFrameRate();
        
// Find the offset within the heap of the current buffer.
        ssize_t offset = mCurrentPreviewFrame * mPreviewFrameSize;
        sp<MemoryHeapBase> heap = mPreviewHeap;
        
// this assumes the internal state of fake camera doesn't change
        
// (or is thread safe)
        Ov965xCamera* Ov965xCamera = mOv965xCamera;
        USBCamera* USBCamera = mUSBCamera;
        sp<MemoryBase> buffer = mBuffers[mCurrentPreviewFrame];
    mLock.unlock();
    
// TODO: here check all the conditions that could go wrong
    if (buffer != 0) {
        
// Calculate how long to wait between frames.
        int delay = (int)(1000000.0f / float(previewFrameRate));
        
// This is always valid, even if the client died -- the memory
        
// is still mapped in our process.
        void *base = heap->base();
        
// Fill the current frame with the fake camera.
        uint8_t *frame = ((uint8_t *)base+ offset;
        
//Ov965xCamera->getNextFrameAsYuv420(frame);
    
if(mCamType == CAMTYPE_CMOS) 
        Ov965xCamera->getNextFrameAsRgb565((uint16_t *)frame);//获取一个帧的数据,放入frame
    else if (mCamType == CAMTYPE_USB)
        USBCamera->getNextFrameAsRgb565((uint16_t *)frame);
        
//LOGV("previewThread: generated frame to buffer %d", mCurrentPreviewFrame);
        
// Notify the client of a new frame.
        if (mMsgEnabled & CAMERA_MSG_PREVIEW_FRAME)
            mDataCb(CAMERA_MSG_PREVIEW_FRAME, buffer, mCallbackCookie);
        
// Advance the buffer pointer.
        mCurrentPreviewFrame = (mCurrentPreviewFrame + 1% kBufferCount;
        
// Wait for it...
        usleep(delay);
    }
    
return NO_ERROR;
}

这样分析过后,问题就变得很明了了,要将图片存储下来,只要获取其一帧数据(getNextFrameAsRGB565),在takePicture函数中将其存储下来即可。好,让我们看看takePicture的实现:它启动了一个线程来调用pictureThread方法,这里就是我们大显身手的地方了!

int
 CameraHardware::pictureThread()
{
    
if
 (mMsgEnabled 
&
 CAMERA_MSG_SHUTTER)
        mNotifyCb(CAMERA_MSG_SHUTTER, 
0
0
, mCallbackCookie); 
//
 对应ShutterCallback
    
if
 (mMsgEnabled 
&
 CAMERA_MSG_RAW_IMAGE) {
        mDataCb(CAMERA_MSG_RAW_IMAGE, mem, mCallbackCookie); 
//
 对应原始图片(RAW)的PictureCallback
    }
    
if
 (mMsgEnabled 
&
 CAMERA_MSG_COMPRESSED_IMAGE) {
        mDataCb(CAMERA_MSG_COMPRESSED_IMAGE, mem, mCallbackCookie); 
//
 对应JPEG图片的PictureCallback
    }
    
return
 NO_ERROR;

每个if都对应了一个takePicture函数的callback,第二第三个就是图片要输出的地方!Android上的照相应用程序并不管RAW图片的输出,我们直接聚焦到第三个if。这里我们就不难理解为什么老是输出“小机器人”了,原来在第三个if中,飞凌并没有改原FakeCamera的代码,FakeCamera在这里直接读入一个CannedJpeg.h中的数据,而这里面存的就是那个“可爱”的机器人....好,那我们就改这里!首先先不管RAW到JPEG的转换,我们把RAW的数据直接写成BMP格式输出,看看是否工作。BMP格式文件头有54个字节,16位的数据格式为RGB555,所以完成的流程就三步:

Step1. 在MemoryHeap上申请一块BMP数据暂存区,并写文件头

Step2. 将原始数据从RGB565转换到RGB555,并存储到BMP数据暂存区

Step3. 将BMP暂存区数据传递给mDataCb输出

 

具体代码如下: 

Step1. 首先申请BMP暂存区:

    
int
 w, h;
    unsigned 
int
 DATA_OFFSET 
=
 
54
;
    uint16_t WIDTH 
=
 w;
    uint16_t HEIGHT 
=
 h;
    mParameters.getPictureSize(
&
w, 
&
h);
    Ov965xCamera
*
 Ov965xCamera 
=
 mOv965xCamera;    
    sp
<
MemoryHeapBase
>
 heap 
=
 
new
 MemoryHeapBase(DATA_OFFSET
+
*
 h
*
 
2
);
    sp
<
MemoryBase
>
 mem 
=
 
new
 MemoryBase(heap, 
0
, DATA_OFFSET
+
*
 h
*
 
2
); 
//
 2个字节构成一个像素

 

写BMP文件头:

        uint8_t header[
54
=
 { 
0x42
//
 identity : B
        
0x4d
//
 identity : M
        
0
0
0
0
//
 file size
        
0
0
//
 reserved1
        
0
0
//
 reserved2
        
54
0
0
0
//
 RGB data offset
        
40
0
0
0
//
 struct BITMAPINFOHEADER size
        
0
0
0
0
//
 bmp height
        
0
0
0
0
//
 bmp width
        
1
0
//
 planes
        
16
0
//
 bit per pixel
        
0
0
0
0
//
 compression
        
0
0
0
0
//
 data size
        
0
0
0
0
//
 h resolution
        
0
0
0
0
//
 v resolution
        
0
0
0
0
//
 used colors
        
0
0
0
0
 
//
 important colors
        };
        
//
 file size offset 54
    uint16_t file_size 
=
 WIDTH 
*
 HEIGHT 
*
 
2
 
+
 DATA_OFFSET;
    header[
2
=
 (uint8_t)(file_size 
&
 
0x000000ff
);
    header[
3
=
 (file_size 
>>
 
8
&
 
0x000000ff
;
    header[
4
=
 (file_size 
>>
 
16
&
 
0x000000ff
;
    header[
5
=
 (file_size 
>>
 
24
&
 
0x000000ff
;
    
//
 height
    header[
18
=
 HEIGHT 
&
 
0x000000ff
;
    header[
19
=
 (HEIGHT 
>>
 
8
&
 
0x000000ff
;
    header[
20
=
 (HEIGHT 
>>
 
16
&
 
0x000000ff
;
    header[
21
=
 (HEIGHT 
>>
 
24
&
 
0x000000ff
;
    
//
 width
    header[
22
=
 WIDTH 
&
 
0x000000ff
;
    header[
23
=
 (WIDTH 
>>
 
8
&
 
0x000000ff
;
    header[
24
=
 (WIDTH 
>>
 
16
&
 
0x000000ff
;

    header[25= (WIDTH >> 24& 0x000000ff;  

 

Step2.  获取当前帧,进行RGB565到RGB555的转换,将转换后的数据放入MemoryHeap中

    unsigned 
int
 i;
    
for
(i
=
0
;i
<
DATA_OFFSET;i
++
){
      
*
((uint8_t
*
)heap
->
base
()
+
i)
=
header[i];
    }
    
    Ov965xCamera
->
getNextFrameAsRgb565((uint16_t
*
)heap
->
base
()
+
DATA_OFFSET
/
2
);
    
    uint16_t 
*
heap_base 
=
 (uint16_t
*
)heap
->
base
();
    uint16_t pixel_data;
    uint8_t tail_data;
    
for
(i
=
DATA_OFFSET
/
2
;i
<
DATA_OFFSET
/
2
+
WIDTH
*
HEIGHT;i
++
){
      pixel_data 
=
 
*
(heap_base
+
i);
      tail_data 
=
 (uint8_t)(pixel_data 
&
 
0x001f
);
      pixel_data 
=
 (pixel_data 
&
 
0xffc0
)
>>
1
 
|
 tail_data;
      
*
(heap_base
+
i)
=
pixel_data;
    }
  

Step3. 调用callback,将数据存储到设备,并释放MemoryHeap

    mDataCb(CAMERA_MSG_COMPRESSED_IMAGE, mem, mCallbackCookie);

    heap=NULL;  

 

到这里,OK6410的Android系统就真正可以拍照了,通过之前介绍编译的方法,将编译好的libcamera.so放入目标机的/system/lib中,重启就能看到效果了,这是我用OV9650拍的照片,在PC上查看建议把jpg后缀名改为bmp,在Android上查看没任何问题:

 

 

附件下载: 

 


待解决的问题:

1. 目前输出的文件虽然是jpg后缀,但实际是BMP格式的,想法是使用external/jpeg库中的函数来解决。

2. 目前输出为320*240,若要使用更大分辨率,势必要更大的MemoryHeap。比如需要1024*768*2字节的空间,虽然可以申请,但根本无法访问到后面的空间,目前还没想到解决方案。

希望大家能一起把以上这两个问题解决,这样我们的开发板就能在图像应用上有用武之地了!

另:转文请注明出处,谢谢!

 

References:

  1. Android Camera Hal 的初步实现1(http://blog.csdn.net/flyingpipi/article/details/5773666)
  2. Android硬件抽象层(HAL)概要介绍和学习计划 (http://blog.csdn.net/luoshengyang/article/details/6567257 )
  3. 真OO无双之真乱舞书 (http://www.cnblogs.com/oomusou/) 
你可能感兴趣的文章
【LEETCODE】27-Remove Element
查看>>
【LEETCODE】66-Plus One
查看>>
【LEETCODE】26-Remove Duplicates from Sorted Array
查看>>
【LEETCODE】118-Pascal's Triangle
查看>>
【LEETCODE】119-Pascal's Triangle II
查看>>
【LEETCODE】88-Merge Sorted Array
查看>>
【LEETCODE】19-Remove Nth Node From End of List
查看>>
【LEETCODE】125-Valid Palindrome
查看>>
【LEETCODE】28-Implement strStr()
查看>>
【LEETCODE】6-ZigZag Conversion
查看>>
【LEETCODE】8-String to Integer (atoi)
查看>>
【LEETCODE】14-Longest Common Prefix
查看>>
【LEETCODE】38-Count and Say
查看>>
【LEETCODE】278-First Bad Version
查看>>
【LEETCODE】303-Range Sum Query - Immutable
查看>>
【LEETCODE】21-Merge Two Sorted Lists
查看>>
【LEETCODE】231-Power of Two
查看>>
【LEETCODE】172-Factorial Trailing Zeroes
查看>>
【LEETCODE】112-Path Sum
查看>>
【LEETCODE】9-Palindrome Number
查看>>