FS_S5PC100平臺上Linux Camera驅動開發詳解(一)
說明:
理解攝像頭驅動需要四個前提:
1)攝像頭基本的工作原理和S5PC100整合的 Camera控制器 的工作原理
2)platform_device和platform_driver工作原理
3)Linux核心V4L2驅動架構
4)Linux核心I2C驅動架構
1. 攝像頭工作原理
OV9650/9655是CMOS介面的影象感測器晶片,可以感知外部的視覺訊號並將其轉換為數字訊號並輸出。通過下面的框圖可以清晰的看到它的工作原理:
我們需要通過XVCLK1給攝像頭提供時鐘,RESET是復位線,PWDN在攝像頭工作時 應該始終為低
OV9650向外傳輸的影象格式是YUV的格式,YUV是一種壓縮後的影象資料格式,它裡面還包含很多具體的格式型別,我們的攝像頭對應的是YCbCr(8 bits, 4:2:2, Interpolated color).一定要搞清楚格式,後面的驅動裡面設定的格式一定要和這個格式一致。
OV9650裡面有很多暫存器需要配置,配置這些暫存器就需要通過晶片裡面的SCCB匯流排去配置。SCCB其實是一種弱化的I2C匯流排。我們可以直接把攝像頭接在S5PC100的I2C控制器上,利用I2C匯流排去讀寫暫存器,當然直接使用GPIO模擬I2C也可以實現讀寫。我們的驅動程式碼裡兩種操作模式都實現了。
從OV9650採集過來的資料沒法直接交給CPU處理。S5PC100晶片裡面集成了Camera控制器,叫FIMC(Fully Interactive Mobile Camera)。攝像頭需要先把影象資料傳給控制器,經過控制器處理(裁剪拉昇後直接預覽或者編碼)之後交給CPU處理。
實際上攝像頭工作需要的時鐘 也是FIMC給它提供的。
2. 驅動開發思路
因為驅動程式是承接硬體和軟體的橋樑,因此開發攝像頭驅動我們要搞清楚兩方面的內容:第一是攝像頭的硬體介面,也就是它是怎麼和晶片連線的,如何控制它,如何給攝像頭復位以及傳送資料的格式等等;第二是攝像頭的軟體介面,Linux核心裡面攝像頭屬於標準的V4L2裝置,但是這個攝像頭只是一個感測器,具體的操作都需要通過FIMC來控制
相比較而言,硬體介面容易搞懂,通過讀晶片手冊和原理圖基本上就沒有問題了,軟體介面比較複雜,主要中間有一個Camera控制器。下面主要集中分析軟體介面。
3. 硬體介面
攝像頭的硬體原理圖如下:
拿到原理圖,我們需要關注的是1、2兩個管腳分別連線到I2C_SDA1和I2C_SCL1,這說明可以通過I2C控制器1來配置攝像頭。另外除錯攝像頭的時候,可以根據這個原理圖使用示波器來測量波形以驗證程式碼是否正確。
這裡還需要注意的是開發驅動之前最好用萬用表測量攝像頭的各個管腳是否和晶片連線正確,否則即使程式碼沒有問題也看不到影象。
另外,還需要仔細閱讀晶片手冊裡Camera控制器一章的描述。主要是明確以下資訊:
FIMC支援以上三種視訊工業標準,OV9650支援ITU-R 601 YcbCr 8-bit mode,這對後面的驅動編寫非常重要。
MPLL和APLL都可以作為攝像頭的時鐘源,不過推薦使用MPLL。這對後面的驅動開發也有幫助。
4. 軟體介面(如何和FIMC驅動對接)
硬體的問題搞清楚之後就可以集中精力關注軟體的介面了。驅動可以有兩種實現方法:第一種是把攝像頭驅動做成普通的V4L2裝置,直接呼叫FIMC裡的暫存器實現視訊資料的捕捉和處理;第二種 利用核心已經實現好的FIMC的驅動,通過某種介面形式,把我們的 攝像頭驅動 掛接在FIMC驅動之下。
這兩種方法第一種實現起來程式碼量比較大,因為需要直接操作FIMC的暫存器,難度也大一些;第二種方法是利用核心已經做好的FIMC驅動,難點在於如何把攝像頭驅動和FIMC驅動整合起來。
在Android下面,第一種方法並不可行,因為FIMC這個模組不僅僅是一個攝像頭的控制介面,它還承擔著V4L2的output功能 和 overlay(顯示疊層)的功能,這兩個功能對Android的顯示系統非常重要。因此最好的方案還是第二種,找到攝像頭驅動和FIMC驅動對接的介面,只要明確了這個介面,後面的事情就好辦了,工作量也不大。
4-1: FIMC驅動的總體結構分析
FIMC的驅動在核心中的位置:
drivers/media/video/samsung/fimc
fimc40_regs.c
fimc43_regs.c
fimc_capture.c
fimc_dev.c
fimc_output.c
fimc_overlay.c
fimc_v4l2.c
這些原始碼裡面最基礎的是fimc_dev.c,這裡面註冊了一個platform_driver,在相應的平臺程式碼裡面有對應的platform_device的描述。這種SOC上的控制器一般都會掛接在platform_bus上以實現在系統初始化時的device和driver的匹配。
在driver的probe函式裡面,主要完成了資源獲取以及v4l2裝置的註冊。因為FIMC一共有三套一樣的控制器(fimc0, fimc1, fimc2),所以驅動裡使用了一個數組來描述:
struct video_device fimc_video_device[FIMC_DEVICES] = {
[0] = {
.fops = &fimc_fops,
.ioctl_ops = &fimc_v4l2_ops,
.release = fimc_vdev_release,
},
[1] = {
.fops = &fimc_fops,
.ioctl_ops = &fimc_v4l2_ops,
.release = fimc_vdev_release,
},
[2] = {
.fops = &fimc_fops,
.ioctl_ops = &fimc_v4l2_ops,
.release = fimc_vdev_release,
},
};
在probe函式裡,呼叫video_register_device()來註冊這三個video_device,在使用者空間裡就會在/dev下看到三個video裝置節點,video0,video1,video2. 每個video_device的成員fops對應的是針對v4l2裝置的基本操作,定義如下:
static const struct v4l2_file_operations fimc_fops = {
.owner = THIS_MODULE,
.open = fimc_open,
.release = fimc_release,
.ioctl = video_ioctl2,
.read = fimc_read,
.write = fimc_write,
.mmap = fimc_mmap,
.poll = fimc_poll,
};
另一個成員ioctl_ops非常重要,因為它是對v4l2的所有ioctl操作集合的描述。fimc_v4l2_ops定義在fimc_v4l2.c裡面:
const struct v4l2_ioctl_ops fimc_v4l2_ops = {
.vidioc_querycap = fimc_querycap,
.vidioc_reqbufs = fimc_reqbufs,
.vidioc_querybuf = fimc_querybuf,
.vidioc_g_ctrl = fimc_g_ctrl,
.vidioc_s_ctrl = fimc_s_ctrl,
.vidioc_cropcap = fimc_cropcap,
.vidioc_g_crop = fimc_g_crop,
.vidioc_s_crop = fimc_s_crop,
.vidioc_streamon = fimc_streamon,
.vidioc_streamoff = fimc_streamoff,
.vidioc_qbuf = fimc_qbuf,
.vidioc_dqbuf = fimc_dqbuf,
.vidioc_enum_fmt_vid_cap = fimc_enum_fmt_vid_capture,
.vidioc_g_fmt_vid_cap = fimc_g_fmt_vid_capture,
.vidioc_s_fmt_vid_cap = fimc_s_fmt_vid_capture,
.vidioc_try_fmt_vid_cap = fimc_try_fmt_vid_capture,
.vidioc_enum_input = fimc_enum_input,
.vidioc_g_input = fimc_g_input,
.vidioc_s_input = fimc_s_input,
.vidioc_g_parm = fimc_g_parm,
.vidioc_s_parm = fimc_s_parm,
.vidioc_g_fmt_vid_out = fimc_g_fmt_vid_out,
.vidioc_s_fmt_vid_out = fimc_s_fmt_vid_out,
.vidioc_try_fmt_vid_out = fimc_try_fmt_vid_out,
.vidioc_g_fbuf = fimc_g_fbuf,
.vidioc_s_fbuf = fimc_s_fbuf,
.vidioc_try_fmt_vid_overlay = fimc_try_fmt_overlay,
.vidioc_g_fmt_vid_overlay = fimc_g_fmt_vid_overlay,
.vidioc_s_fmt_vid_overlay = fimc_s_fmt_vid_overlay,
};
可以看到,FIMC的驅動實現了v4l2所有的介面,可以分為v4l2-input裝置介面,v4l2-output裝置介面以及v4l2-overlay裝置介面。這裡我們主要關注v4l2-input裝置介面,因為攝像頭屬於視訊輸入裝置。
fimc_v4l2.c裡面註冊了很多的回撥函式,都是用於實現v4l2的標準介面的,但是這些回撥函式基本上都不是在fimc_v4l2.c裡面實現的,而是有相應的.c分別去實現。比如:
v4l2-input裝置的操作實現: fimc_capture.c
v4l2-output裝置的操作實現: fimc_output.c
v4l2-overlay裝置的操作實現: fimc_overlay.c
這些程式碼其實都是和具體硬體操作無關的,這個驅動把所有操作硬體暫存器的程式碼都寫到一個檔案裡面了,就是fimc40_regs.c。這樣把硬體相關的程式碼和硬體無關的程式碼分開來實現是非常好的方式,可以最大限度的實現程式碼複用。
這些驅動原始碼的組織關係如下:
4-2: FIMC驅動的Camera介面分析
介面的關鍵還是在於fimc_dev.c裡的probe函式。probe裡面會呼叫一個函式叫fimc_init_global(),這裡面會完成攝像頭的分配以及時鐘的獲取。這個函式的原型如下:
static int fimc_init_global( struct platform_device *pdev )
這個platform_device是核心從平臺程式碼那裡傳遞過來的,裡面包含的就是和具體平臺相關的資訊,其中就應該包含攝像頭資訊。
函式的實現:
static int fimc_init_global(struct platform_device *pdev)
{
struct fimc_control *ctrl;
struct s3c_platform_fimc *pdata;
//這個結構體就是用來描述一個攝像頭的,先不管它裡面的內容
//等會兒在分析平臺程式碼的時候可以看到它是如何被填充的
struct s3c_platform_camera *cam;
struct clk *srclk;
int id, i;
//獲得平臺資訊
pdata = to_fimc_plat(&pdev->dev);
id = pdev->id; //id號可能是0,1,2
ctrl = get_fimc_ctrl(id); //獲得id號對應的fimc_control結構體指標
/* Registering external camera modules. re-arrange order to be sure */
for (i = 0; i < FIMC_MAXCAMS; i++) {
cam = pdata->camera[i]; //從平臺數據取得camera的資訊
if (!cam)
continue; // change break to continue by ys
/* WriteBack doesn't need clock setting */
if(cam->id == CAMERA_WB) {
fimc_dev->camera[cam->id] = cam;
break;
}
// 獲得時鐘源資訊
srclk = clk_get(&pdev->dev, cam->srclk_name);
if (IS_ERR(srclk)) {
fimc_err("%s: failed to get mclk source\n", __func__);
return -EINVAL;
}
// 獲得camera的時鐘資訊
/* mclk */
cam->clk = clk_get(&pdev->dev, cam->clk_name);
if (IS_ERR(cam->clk)) {
fimc_err("%s: failed to get mclk source\n", __func__);
return -EINVAL;
}
if (cam->clk->set_parent) {
cam->clk->parent = srclk;
cam->clk->set_parent(cam->clk, srclk);
}
/* Assign camera device to fimc */
fimc_dev->camera[cam->id] = cam; // 將從平臺獲得的camera分配給全域性資料結構
// fimc_dev
}
fimc_dev->initialized = 1;
return 0;
}
可以看到這個函式實際上就是把camera的資訊從平臺數據那裡取過來,然後分配給fimc_dev. fimc_dev定義在fimc.h裡面。型別為struct fimc_global,原型如下:
/* global */
struct fimc_global {
struct fimc_control ctrl[FIMC_DEVICES];
struct s3c_platform_camera *camera[FIMC_MAXCAMS];
int initialized;
};
現在我們需要看一下平臺程式碼那裡如何描述一個攝像頭以及如何把抽象資料結構傳遞到平臺數據裡面。
S5PC100 SOC對應的平臺程式碼位於:
arch/arm/mach-s5pc100/mach-smdkc100.c
我們是這樣來描述一個camera的:
#ifdef CONFIG_VIDEO_OV9650
/* add by ys for ov9650 */
static struct s3c_platform_camera camera_c = {
.id = CAMERA_PAR_A, /* FIXME */
.type = CAM_TYPE_ITU, /* 2.0M ITU */
.fmt = ITU_601_YCBCR422_8BIT,
.order422 = CAM_ORDER422_8BIT_YCBYCR,
.i2c_busnum = 1,
.info = &camera_info[2],
.pixelformat = V4L2_PIX_FMT_YUYV,
.srclk_name = "dout_mpll",
.clk_name = "sclk_cam",
.clk_rate = 16000000, /* 16MHz */
.line_length = 640, /* 640*480 */
/* default resol for preview kind of thing */
.width = 640,
.height = 480,
.window = {
.left = 0,
.top = 0,
.width = 640,
.height = 480,
},
/* Polarity */
.inv_pclk = 1,
.inv_vsync = 0,
.inv_href = 0,
.inv_hsync = 0,
.initialized = 0,
};
#endif
這裡面的資訊描述了OV9650相關的所有資訊。type代表攝像頭是ITU的介面,fmt代表攝像頭輸出的格式是ITU_601_YCBCR422_8BIT,order422代表YUV三個分量的順序是YcbCr。這些都和前面的描述相符。另外裡面還有時鐘源的資訊,時鐘的大小以及捕捉影象的解析度,這裡設定的是640x480(VGA模式),因為經過除錯發現OV9650工作在VGA的模式下比較流暢清晰。Polarity代表訊號的極性,具體的設定要和攝像頭本身的設定一致。
i2c_busnum是I2C匯流排的匯流排編號,因為S5PC100一共有兩條I2C匯流排(0和1),我們連在SDA1上,所以i2c_busnum是1。
camera_c是fimc_plat結構體的一個成員:
/* Interface setting */
static struct s3c_platform_fimc fimc_plat = {
.default_cam = CAMERA_PAR_A,
.camera[ 2 ] = &camera_c,
.hw_ver = 0x40,
};
這裡會把camera_c賦值給fimc_plat裡的camera陣列的第三個元素,之所以是第三個是因為Android的原因。這在分析Android的攝像頭硬體抽象層時會有解釋。
struct s3c_platform_fimc這個結構體其實就是fimc對應的平臺數據結構。在平臺程式碼裡,會由以下三個函式負責註冊:
s3c_fimc0_set_platdata(&fimc_plat);
s3c_fimc1_set_platdata(&fimc_plat);
s3c_fimc2_set_platdata(&fimc_plat);
至於這幾個函式如何實現,這裡就不分析了,有興趣可以自己看程式碼。
也就是說只要平臺程式碼這邊我們填充了一個struct s3c_platform_camera型別的結構體,然後把它新增到fimc_plat裡面,fimc的驅動就能獲得對應的Camera的資訊。