在平时工作中,camera模块是经常进行调试修改的模块,所以熟悉camera的工作流程以及工作原理将会大大的提供工作效率,但对于整个android系统camera是个十分复杂的模块,下面对camera的驱动加载进行分析。

1、 Camera成像简介

景物通过镜头(LENS)生成的光学图像投射到图像传感器(Sensor)表面上,然后转为模拟的电信号,经过 A/D(模数转换)转换后变为数字图像信号,再送到数字信号处理芯片(DSP)中加工处理,再通过 IO 接口传输到 CPU 中处理,通过 LCD 就可以看到图像了。
       图像传感器(SENSOR)是一种半导体芯片,其表面包含有几十万到几百万的光电二极管。光电二极管受到光照射时,就会产生电荷。目前的 SENSOR 类型有两种:CCD(ChargeCouple Device)和CMOS(ComplementaryMetal Oxide Semiconductor)。CCD电荷耦合器件,它是目前高像素类 sensor 中比较成熟的成像器件,是以一行为单位的电流信号。CMOS互补金属氧化物半导体。CMOS的信号是以点为单位的电荷信号,更为敏感,速度也更快,更为省电。ISP 的性能是决定影像流畅的关键,JPEGencoder 的性能也是关键指标之一。而 JPEGencoder 又分为硬件 JPEG 压缩方式和软件 RGB压缩方式。DSP 控制芯片的作用是:将感光芯片获取的数据及时快速地传到 baseband 中并刷新感光芯片,因此控制芯片的好坏,直接决定画面品质(比如色彩饱和度、清晰度)与流畅度。

2、Camera的硬件原理图及引脚

从上面可看出,连接 Camera 的pin引脚可大致分为以下几类:

2.1、 电源部分:

           a):VCAMD 就是DVDD数字供电,主要给ISP供电,由于 RAWDATA格式的sensor其ISP是在BB端,所以将其引脚将其NC。从上面的规格书上可以看出 DVDD是内部BB端供电。模组已将其 NC掉了,在MTK代码中的上电部分,DVDD电压用CAMERA_POWER_VCAM_D表示;
b):VCAM_IO 就是 VDDIO 数字 IO 电源主要给 I2C 部分供电,在MTK代码中的上电部分,DVDD电压用CAMERA_POWER_VCAM_D2表示;
c):VCAMA 就是 AVDD 模拟供电,主要给感光区和 ADC 部分供电,在MTK代码中的上电部分,DVDD电压用CAMERA_POWER_VCAM_A表示;
d):VCAM_AF 是对 Camera 自动对焦马达的供电,在MTK代码中的上电部分,DVDD电压用CAMERA_POWER_VCAM_A2表示。

2.2、 Sensor Input 部分:

a) :Reset 信号,用于复位、初始化。

b) :Standby/PowerDown信号,用于进入待机模式,降低功耗。

c) :Mclk,即MasterClock 信号,是由 BB 端提供。

2.3、Sensor OutPut 部分:

a) :Pclk,即 PixelClock 信号,由 MCLK 分频得到,作为外部时钟控制图像传输帧率;

b) :HSYNC,行同步信号,其上升沿表示新一列行图像数据的开始;

c) :VSYNC,帧同步信号,其下降沿表示新的一帧图片的开始;

d) :D0-D9 一共 10 根数据线(8/10 根等)。

2.4、I2C 部分

Camera也是挂载在I2C总线上的设备,主要利用I2C总线对寄存器进行一些读写,与其他该做在I2C总线上的设备一样,这部分就两根线SCL和SDA,SCL,I2C 时钟信号线和 SDA,I2C 数据信号线。

3、Camera架构

上图的架构相信大家都有了一定的了解,android 将系统大致分为应用层、库文件和硬件抽象层、Linux 内核三层。在底层的内核空间,Camera 的 driver 将其驱动起来以后,将硬件驱动的接口交给硬件抽象层,android 上层的 Camera 应用程序在 android 实时系统中的虚拟机中,加载 android 留给 Camera 公用的一些库文件,调用硬件抽象层的接口来控制 Camera硬件来实现功能。当然,如果是 Raw 模式的 Camera,还需要在硬件抽象层调用一些参数来控制 Camera 的效果。

Kernel 部分主要有两块:一块是image sensor 驱动,负责具体型号的sensor 的id 检测,上电,以及在preview,capture,初始化,3A 等等功能设定时的寄存器配置。另一块是isp driver,通过DMA 将sensor数据流上传。

HAL层这边主要分3 块,一块是imageio,主要是数据buffer上传的pipe。一块是drv,包含imgsensor 和isp 的hal 层控制。最后是feature io,包含各种3A 等性能配置。

4、Camera image sensor驱动模块驱动加载

camera模块驱动是一个字符驱动,驱动是挂载在总线上,一般在 Linux 总线驱动模型中,我们只需要关心总线、设备、驱动这三个实体。总线会充当红娘对加载于其上的设备与驱动进行配对,对于 Camera 模块也不例外,下面从总线、设备、驱动的角度来分析 Camera 模块驱动的注册、匹配与加载过程。
MTK的image_sensor注册的是一个platform类型总线驱动,首先要进行板极设备的初始化的工作,代码在:mediatek/platform/mt6572/kernel/core/mt_devs.c,里面会对platform总线注册的device进行注册:
 #if 1 ///defined(CONFIG_VIDEO_CAPTURE_DRIVERS)retval = platform_device_register(&sensor_dev);if (retval != 0){return retval;}
#endif
static struct platform_device sensor_dev = {.name            = "image_sensor",.id              = -1,};

image_sensor的platform类型驱动的device的name为 "image_sensor",而在linux中,所有的总线的driver与device都是通过name来与进行匹配的,platform总线也不例外,所以可以通过grep命令来查找camera注册的总线中driver的注册路径为:mediatek/custom/common/kernel/imgsensor/src/kd_sensorlist.c,代码为:

static struct platform_driver g_stCAMERA_HW_Driver = {.probe              = CAMERA_HW_probe,.remove         = CAMERA_HW_remove,.suspend    = CAMERA_HW_suspend,.resume         = CAMERA_HW_resume,.driver             = {.name   = "image_sensor",.owner  = THIS_MODULE,}
};

下面就来看看image_sensorplatform的driver整个注册流程是怎样实现的。先来看看kd_sensorlist.c驱动文件的init人口函数:

/*=======================================================================* CAMERA_HW_i2C_init()*=======================================================================*/
static int __init CAMERA_HW_i2C_init(void)
{struct proc_dir_entry *prEntry;//i2c_register_board_info(CAMERA_I2C_BUSNUM, &kd_camera_dev, 1);i2c_register_board_info(SUPPORT_I2C_BUS_NUM1, &i2c_devs1, 1); // 填充i2c的板极文件//i2c_register_board_info(SUPPORT_I2C_BUS_NUM2, &i2c_devs2, 1);if(platform_driver_register(&g_stCAMERA_HW_Driver)){ // 注册platform总线的driverPK_ERR("failed to register CAMERA_HW driver\n");return -ENODEV;}//if(platform_driver_register(&g_stCAMERA_HW_Driver2)){//    PK_ERR("failed to register CAMERA_HW driver\n");//    return -ENODEV;//}//Register proc file for main sensor register debugprEntry = create_proc_entry("driver/camsensor", 0, NULL); //在proc下创建driver/camsensor这个节点,用于前置摄像头进行adb效果调试if (prEntry) {prEntry->read_proc = CAMERA_HW_DumpReg_To_Proc;prEntry->write_proc = CAMERA_HW_Reg_Debug;}else {PK_ERR("add /proc/driver/camsensor entry fail \n");}//Register proc file for sub sensor register debugprEntry = create_proc_entry("driver/camsensor2", 0, NULL); //在proc下创建driver/camsensor2这个节点,用于后置摄像头进行adb效果调试if (prEntry) {prEntry->read_proc = CAMERA_HW_DumpReg_To_Proc;prEntry->write_proc = CAMERA_HW_Reg_Debug2;}else {PK_ERR("add /proc/driver/camsensor2 entry fail \n");}atomic_set(&g_CamHWOpend, 0);//atomic_set(&g_CamHWOpend2, 0);atomic_set(&g_CamDrvOpenCnt, 0);//atomic_set(&g_CamDrvOpenCnt2, 0);atomic_set(&g_CamHWOpening, 0);return 0;
}

CAMERA_HW_i2C_init函数主要做的是对I2C总线的版级文件进行了填充,然后注册platform总线的driver,通过g_stCAMERA_HW_Driver结构体里面的name来与device进行匹配。同时,在函数里面还在proc目录下创建了driver/camsensor和driver/camsensor2两个节点,这样做主要是方便sensor的IC原厂FAE利用adb进行效果调试的。

static struct platform_driver g_stCAMERA_HW_Driver = {.probe          = CAMERA_HW_probe,.remove         = CAMERA_HW_remove,.suspend     = CAMERA_HW_suspend,.resume         = CAMERA_HW_resume,.driver          = {.name     = "image_sensor",.owner     = THIS_MODULE,}
};

g_stCAMERA_HW_Driver结构体中主要有probe、remove、suspend等接口的实现,probe接口为设备注册的匹配函数,所以在在注册了driver就会调用 .probe          = CAMERA_HW_probe进入CAMERA_HW_probe函数:

static int CAMERA_HW_probe(struct platform_device *pdev)
{return i2c_add_driver(&CAMERA_HW_i2c_driver);
}
struct i2c_driver CAMERA_HW_i2c_driver = {.probe = CAMERA_HW_i2c_probe,.remove = CAMERA_HW_i2c_remove,.driver.name = CAMERA_HW_DRVNAME1,.id_table = CAMERA_HW_i2c_id,
};

CAMERA_HW_probe做的就是注册一个i2c的driver,camera会挂载在I2C总线上,利用I2C进入寄存器的读写,所以,sensor驱动最终还是会注册I2C设备,I2C总线的注册其实跟platform总线的注册大致相同,注册完I2C driver后系统就会调用CAMERA_HW_i2c_driver 结构体里面的 .probe = CAMERA_HW_i2c_probe:

static int CAMERA_HW_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{int i4RetValue = 0;PK_DBG("[CAMERA_HW] Attach I2C \n");//get sensor i2c clientspin_lock(&kdsensor_drv_lock);g_pstI2Cclient = client;   //这里是获得我们的clientdevice,并且以platform方式进行注册//set I2C clock rateg_pstI2Cclient->timing = 300;//200kspin_unlock(&kdsensor_drv_lock);//Register char driveri4RetValue = RegisterCAMERA_HWCharDrv(); // 注册字符驱动if(i4RetValue){PK_ERR("[CAMERA_HW] register char device failed!\n");return i4RetValue;}//spin_lock_init(&g_CamHWLock);PK_DBG("[CAMERA_HW] Attached!! \n");return 0;
}

在CAMERA_HW_i2c_probe函数里面主要就是调用了RegisterCAMERA_HWCharDrv函数来注册一个字符驱动。

inline static int RegisterCAMERA_HWCharDrv(void)
{struct device* sensor_device = NULL;#if CAMERA_HW_DYNAMIC_ALLOCATE_DEVNOif( alloc_chrdev_region(&g_CAMERA_HWdevno, 0, 1,CAMERA_HW_DRVNAME1) ) // 动态分配一个字符设备{PK_DBG("[CAMERA SENSOR] Allocate device no failed\n");return -EAGAIN;}
#elseif( register_chrdev_region(  g_CAMERA_HWdevno , 1 , CAMERA_HW_DRVNAME1) ) // 静态分配一个字符设备{PK_DBG("[CAMERA SENSOR] Register device no failed\n");return -EAGAIN;}
#endif//Allocate driverg_pCAMERA_HW_CharDrv = cdev_alloc();  // 申请一个cdev结构体if(NULL == g_pCAMERA_HW_CharDrv){unregister_chrdev_region(g_CAMERA_HWdevno, 1);PK_DBG("[CAMERA SENSOR] Allocate mem for kobject failed\n");return -ENOMEM;}//Attatch file operation.cdev_init(g_pCAMERA_HW_CharDrv, &g_stCAMERA_HW_fops); //关联到file_operation进入字符设备g_pCAMERA_HW_CharDrv->owner = THIS_MODULE;//Add to systemif(cdev_add(g_pCAMERA_HW_CharDrv, g_CAMERA_HWdevno, 1)) //将我们分配的字符设备,attach上file_operation添加到system{PK_DBG("[mt6516_IDP] Attatch file operation failed\n");unregister_chrdev_region(g_CAMERA_HWdevno, 1);return -EAGAIN;}sensor_class = class_create(THIS_MODULE, "sensordrv");  //创建一个sensordrv类if (IS_ERR(sensor_class)) {int ret = PTR_ERR(sensor_class);PK_DBG("Unable to create class, err = %d\n", ret);return ret;}sensor_device = device_create(sensor_class, NULL, g_CAMERA_HWdevno, NULL, CAMERA_HW_DRVNAME1);return 0;
}

linux驱动分为三大部分驱动,分别为字符驱动,块驱动以及网络驱动,而camera的模块驱动为字符驱动,所以RegisterCAMERA_HWCharDrv函数主要是对camera_image进行字符驱动注册,代码开始先判断是否定义了CAMERA_HW_DYNAMIC_ALLOCATE_DEVNO变量来判断动态还是静态分配一个字符设备,然后通过cdev_alloc申请了一个cdev结构体后,通过cdev_init将g_stCAMERA_HW_fops关联到字符设备,这是这个函数往下走下去的关键。然后就是将我们分配的字符设备,attach上file_operation添加到sys,最后在sys/class目录下创建一个sensordrv类,如下图所示:

下面来看看g_stCAMERA_HW_fops结构体里面的内容:
static const struct file_operations g_stCAMERA_HW_fops =
{.owner = THIS_MODULE,.open = CAMERA_HW_Open,.release = CAMERA_HW_Release,.unlocked_ioctl = CAMERA_HW_Ioctl};

file_operations 是为上层调用底层提供的接口,往往打开接口就是先打开open,那么先来看看struct file_operations g_stCAMERA_HW_fops结构体中的open函数;

static int CAMERA_HW_Open(struct inode * a_pstInode, struct file * a_pstFile)
{//reset once in multi-openif ( atomic_read(&g_CamDrvOpenCnt) == 0) {//default OFF state//MUST have//kdCISModulePowerOn(DUAL_CAMERA_MAIN_SENSOR,"",true,CAMERA_HW_DRVNAME1);//kdCISModulePowerOn(DUAL_CAMERA_SUB_SENSOR,"",true,CAMERA_HW_DRVNAME1);//kdCISModulePowerOn(DUAL_CAMERA_MAIN_2_SENSOR,"",true,CAMERA_HW_DRVNAME1);//kdCISModulePowerOn(DUAL_CAMERA_MAIN_SENSOR,"",false,CAMERA_HW_DRVNAME1);//kdCISModulePowerOn(DUAL_CAMERA_SUB_SENSOR,"",false,CAMERA_HW_DRVNAME1);//kdCISModulePowerOn(DUAL_CAMERA_MAIN_2_SENSOR,"",false,CAMERA_HW_DRVNAME1);}//atomic_inc(&g_CamDrvOpenCnt);return 0;
}

整个函数就是对g_CamDrvOpenCnt变量进行了一个原子读的过程,没有进行别的操作。而上层跟驱动进行通讯主要是通过ioctl发送命令,然后进行数据传输,然后在看看.unlocked_ioctl= CAMERA_HW_Ioctl中的CAMERA_HW_Ioctl操作;

static long CAMERA_HW_Ioctl(struct file * a_pstFile,unsigned int a_u4Command,unsigned long a_u4Param
)
{int i4RetValue = 0;void * pBuff = NULL;u32 *pIdx = NULL;mutex_lock(&kdCam_Mutex);if(_IOC_NONE == _IOC_DIR(a_u4Command)) {}else {pBuff = kmalloc(_IOC_SIZE(a_u4Command),GFP_KERNEL); //申请分配一个bufferif(NULL == pBuff) {PK_DBG("[CAMERA SENSOR] ioctl allocate mem failed\n");i4RetValue = -ENOMEM;goto CAMERA_HW_Ioctl_EXIT;}if(_IOC_WRITE & _IOC_DIR(a_u4Command)){ //判断是否可写//将用户传递过来的命令参数复制到内核空间,接下来我们会根据这个数据进行选择if(copy_from_user(pBuff , (void *) a_u4Param, _IOC_SIZE(a_u4Command))) {kfree(pBuff);PK_DBG("[CAMERA SENSOR] ioctl copy from user failed\n");i4RetValue =  -EFAULT;goto CAMERA_HW_Ioctl_EXIT;}}}pIdx = (u32*)pBuff;switch(a_u4Command) {
#if 0case KDIMGSENSORIOC_X_POWER_ON:i4RetValue = kdModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM) *pIdx, true, CAMERA_HW_DRVNAME);break;case KDIMGSENSORIOC_X_POWER_OFF:i4RetValue = kdModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM) *pIdx, false, CAMERA_HW_DRVNAME);break;
#endifcase KDIMGSENSORIOC_X_SET_DRIVER:i4RetValue = kdSetDriver((unsigned int*)pBuff);break;case KDIMGSENSORIOC_T_OPEN:i4RetValue = adopt_CAMERA_HW_Open();break;case KDIMGSENSORIOC_X_GETINFO:i4RetValue = adopt_CAMERA_HW_GetInfo(pBuff);break;case KDIMGSENSORIOC_X_GETRESOLUTION:i4RetValue = adopt_CAMERA_HW_GetResolution(pBuff);break;case KDIMGSENSORIOC_X_FEATURECONCTROL:i4RetValue = adopt_CAMERA_HW_FeatureControl(pBuff);break;case KDIMGSENSORIOC_X_CONTROL:i4RetValue = adopt_CAMERA_HW_Control(pBuff);break;case KDIMGSENSORIOC_T_CLOSE:i4RetValue = adopt_CAMERA_HW_Close();break;case KDIMGSENSORIOC_T_CHECK_IS_ALIVE:i4RetValue = adopt_CAMERA_HW_CheckIsAlive();break;case KDIMGSENSORIOC_X_GET_SOCKET_POS:i4RetValue = kdGetSocketPostion((unsigned int*)pBuff);break;case KDIMGSENSORIOC_X_SET_I2CBUS://i4RetValue = kdSetI2CBusNum(*pIdx);break;case KDIMGSENSORIOC_X_RELEASE_I2C_TRIGGER_LOCK://i4RetValue = kdReleaseI2CTriggerLock();break;default :PK_DBG("No such command \n");i4RetValue = -EPERM;break;}if(_IOC_READ & _IOC_DIR(a_u4Command)) {if(copy_to_user((void __user *) a_u4Param , pBuff , _IOC_SIZE(a_u4Command))) {kfree(pBuff);PK_DBG("[CAMERA SENSOR] ioctl copy to user failed\n");i4RetValue =  -EFAULT;goto CAMERA_HW_Ioctl_EXIT;}}kfree(pBuff);
CAMERA_HW_Ioctl_EXIT:mutex_unlock(&kdCam_Mutex);return i4RetValue;
}

ioctl主要就是上层通过cmd命令来与底层进行通信,下面就看看这个比较重要的cmd命令:

a):KDIMGSENSORIOC_T_OPEN命令:
 case KDIMGSENSORIOC_T_OPEN:i4RetValue = adopt_CAMERA_HW_Open();break;inline static int adopt_CAMERA_HW_Open(void)
{UINT32 err = 0;KD_IMGSENSOR_PROFILE_INIT();if (atomic_read(&g_CamHWOpend) == 0  ) {if (g_pSensorFunc) { //判断我们imagesensor 操作函数指针是否为NULL,如果为NULL,报错,因为我们就是靠这个操作函数集合去操作imagesensor 的err = g_pSensorFunc->SensorOpen(); // 会调用到kd_MultiSensorFunc里面的kd_MultiSensorOpen函数if(ERROR_NONE != err) {PK_DBG(" ERROR:SensorOpen(), turn off power \n");kdModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM*) g_invokeSocketIdx, g_invokeSensorNameStr, false, CAMERA_HW_DRVNAME1);}}else {PK_DBG(" ERROR:NULL g_pSensorFunc\n");}KD_IMGSENSOR_PROFILE("SensorOpen");}else {//PK_ERR("adopt_CAMERA_HW_Open Fail, g_CamHWOpend = %d,g_CamHWOpend2 = %d\n ",atomic_read(&g_CamHWOpend),atomic_read(&g_CamHWOpend2) );PK_ERR("adopt_CAMERA_HW_Open Fail, g_CamHWOpend = %d\n ",atomic_read(&g_CamHWOpend) );}if (err == 0 ) {atomic_set(&g_CamHWOpend, 1);//atomic_set(&g_CamHWOpend2, 1);}return err?-EIO:err;
}     /* adopt_CAMERA_HW_Open() */

g_pSensorFunc结构体的定义为staticMULTI_SENSOR_FUNCTION_STRUCT *g_pSensorFunc = &kd_MultiSensorFunc,它跟kd_MultiSensorFunc指向相同的地址,if (g_pSensorFunc) 由这里可以看出,上层调用ioctl命令时不可能先走KDIMGSENSORIOC_T_OPEN这个命令,因为此时g_pSensorFunc 为NULL,还没有赋值,由后面的代码可以看到,ioctl先走的cmd命令是KDIMGSENSORIOC_X_SET_DRIVER。当g_pSensorFunc不为NULL时,就是执行这个err =g_pSensorFunc->SensorOpen(); ,这里的SensorOpen函数指向的是kd_MultiSensorOpen函数。

MUINT32 kd_MultiSensorOpen ( void )
{
MUINT32 ret = ERROR_NONE;
MINT32 i = 0;KD_MULTI_FUNCTION_ENTRY(); // 得到当前时间for ( i = (KDIMGSENSOR_MAX_INVOKE_DRIVERS-1) ; i >= KDIMGSENSOR_INVOKE_DRIVER_0 ; i-- ) {if ( g_bEnableDriver[i] && g_pInvokeSensorFunc[i] ) {// turn on powerret = kdCISModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM)g_invokeSocketIdx[i],(char*)g_invokeSensorNameStr[i],true,CAMERA_HW_DRVNAME1);if ( ERROR_NONE != ret ) {  // 上电PK_ERR("[%s]",__FUNCTION__);return ret;}//wait for power stablemDELAY(10);KD_IMGSENSOR_PROFILE("kdModulePowerOn");ret = g_pInvokeSensorFunc[i]->SensorOpen(); //调用到模块驱动中的open函数,g_pInvokeSensorFunc保存的值为模块驱动中的SENSOR_FUNCTION_STRUCT     SensorFuncGC2035这个结构体if ( ERROR_NONE != ret ) {kdCISModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM)g_invokeSocketIdx[i],(char*)g_invokeSensorNameStr[i],false,CAMERA_HW_DRVNAME1); // 掉电PK_ERR("SensorOpen");return ret;}}}KD_MULTI_FUNCTION_EXIT();return ERROR_NONE;
}

g_bEnableDriver定义为一个bool型的变量,定义如下static BOOL g_bEnableDriver[KDIMGSENSOR_MAX_INVOKE_DRIVERS] ={FALSE,FALSE},而g_pInvokeSensorFunc的定义为static SENSOR_FUNCTION_STRUCT*g_pInvokeSensorFunc[KDIMGSENSOR_MAX_INVOKE_DRIVERS] = {NULL,NULL};而g_pInvokeSensorFunc的地址跟模块驱动中的SENSOR_FUNCTION_STRUCT     SensorFuncGC2035这个地址指向是相同的,他是作为一个参数传递来被调用的,这两个变量的实现都会在后面KDIMGSENSORIOC_X_SET_DRIVER命令调用时介绍的。当这两个变量都为true时,就对sensor进行上电,然后就通过ret =g_pInvokeSensorFunc[i]->SensorOpen()进入到了模块sensor驱动中的open函数。至于上电跟掉电函数kdCISModulePowerOn,在mediatek/custom/ztenj72_we_72_m536_kk/kernel/camera/camera/kd_camera_hw.c文件中,其实上电跟掉电时序就是配置一些GPIO口,然后在把camera的三路电压按dateshell配置一下上掉电的时间。

b):KDIMGSENSORIOC_X_SET_DRIVER命令:
case KDIMGSENSORIOC_X_SET_DRIVER:i4RetValue = kdSetDriver((unsigned int*)pBuff);break;

int kdSetDriver(unsigned int* pDrvIndex)
{ACDK_KD_SENSOR_INIT_FUNCTION_STRUCT *pSensorList = NULL; // 这是一个保存cameraId跟cameraName的结构体u32 drvIdx[KDIMGSENSOR_MAX_INVOKE_DRIVERS] = {0,0};u32 i;PK_XLOG_INFO("pDrvIndex:0x%08x/0x%08x \n",pDrvIndex[KDIMGSENSOR_INVOKE_DRIVER_0],pDrvIndex[KDIMGSENSOR_INVOKE_DRIVER_1]);//set driver for MAIN or SUB sensorif (0 != kdGetSensorInitFuncList(&pSensorList)) //调用这个函数,取得所有添加的sensor的结构的首地址{PK_ERR("ERROR:kdGetSensorInitFuncList()\n");return -EIO;}for ( i = KDIMGSENSOR_INVOKE_DRIVER_0; i < KDIMGSENSOR_MAX_INVOKE_DRIVERS ; i++ ) {//spin_lock(&kdsensor_drv_lock);g_bEnableDriver[i] = FALSE;g_invokeSocketIdx[i] = (CAMERA_DUAL_CAMERA_SENSOR_ENUM)((pDrvIndex[i] & KDIMGSENSOR_DUAL_MASK_MSB)>>KDIMGSENSOR_DUAL_SHIFT); // 保存camera的sensorIdspin_unlock(&kdsensor_drv_lock);drvIdx[i] = (pDrvIndex[i] & KDIMGSENSOR_DUAL_MASK_LSB);//if ( DUAL_CAMERA_NONE_SENSOR == g_invokeSocketIdx[i] ) { continue; }//ToDo: remove print informationPK_XLOG_INFO("[kdSetDriver] i,g_invokeSocketIdx[%d] = %d :\n",i,i,drvIdx[i]);PK_XLOG_INFO("[kdSetDriver] i,drvIdx[%d] = %d :\n",i,i,drvIdx[i]);//if ( MAX_NUM_OF_SUPPORT_SENSOR > drvIdx[i] ) {if (NULL == pSensorList[drvIdx[i]].SensorInit) {PK_ERR("ERROR:kdSetDriver()\n");return -EIO;}pSensorList[drvIdx[i]].SensorInit(&g_pInvokeSensorFunc[i]);// 调用模块驱动中的init函数,将模块驱动里面的SENSOR_FUNCTION_STRUCT  SensorFuncOV2685值传递给g_pInvokeSensorFunc结构体if (NULL == g_pInvokeSensorFunc[i]) {PK_ERR("ERROR:NULL g_pSensorFunc[%d]\n",i);return -EIO;}//spin_lock(&kdsensor_drv_lock);g_bEnableDriver[i] = TRUE;g_CurrentInvokeCam = g_invokeSocketIdx[i];              spin_unlock(&kdsensor_drv_lock);//get sensor namememcpy((char*)g_invokeSensorNameStr[i],(char*)pSensorList[drvIdx[i]].drvname,sizeof(pSensorList[drvIdx[i]].drvname));//return sensor ID//pDrvIndex[0] = (unsigned int)pSensorList[drvIdx].SensorId;PK_XLOG_INFO("[kdSetDriver] :[%d][%d][%d][%s][%d]\n",i,g_bEnableDriver[i],g_invokeSocketIdx[i],g_invokeSensorNameStr[i],sizeof(pSensorList[drvIdx[i]].drvname));}}return 0;
}

kdSetDriver这个函数比较重要,上层调用底层的命令应该第一步就是调用这个命令的,函数开头定义了一个ACDK_KD_SENSOR_INIT_FUNCTION_STRUCT *pSensorList = NULL;这样的结构体,ACDK_KD_SENSOR_INIT_FUNCTION_STRUCT 变量是为src/kd_sensorlist.h文件里面保存sensor的ID,NAME以及init的结构体,if (0 !=kdGetSensorInitFuncList(&pSensorList))通过调用kdGetSensorInitFuncList函数,将pSensorList的首地址指向src/kd_sensorlist.h里面ACDK_KD_SENSOR_INIT_FUNCTION_STRUCT变量的首地址。

UINT32 kdGetSensorInitFuncList(ACDK_KD_SENSOR_INIT_FUNCTION_STRUCT **ppSensorList)
{if (NULL == ppSensorList){PK_DBG("[kdGetSensorInitFuncList]ERROR: NULL ppSensorList\n");return 1;}*ppSensorList = &kdSensorList[0]; // kdSensorList在kd_sensorlist.h文件里面,就是保存cameraId,cameraNmae,模块驱动人口函数的结构体return 0;
}

调用了kdGetSensorInitFuncList后,代码先将g_bEnableDriver置为FALSE,而这个变量就是在上面的open函数里面出现过的。然后通过if (NULL ==pSensorList[drvIdx[i]].SensorInit)判断模块驱动的init函数是否为NULL,pSensorList[drvIdx[i]].SensorInit(&g_pInvokeSensorFunc[i]);传递&g_pInvokeSensorFunc[i]为参数,其中还是传递的地址,这就将g_pInvokeSensorFunc的首地址指向了模块驱动函数中的UINT32 GC2035_YUV_SensorInit(PSENSOR_FUNCTION_STRUCT *pfFunc)这个函数,这就是上面kd_MultiSensorOpen 中为什么g_pInvokeSensorFunc可以直接调用模块驱动中的open函数,其实就是在这里实现的。当调用完成后,然后就将camera的ID,NAME等信息保存并将g_bEnableDriver[i]变量置为TRUE。
     而ioctl里面的其他几条cmd命令基本都是在获取到了g_pInvokeSensorFunc后然后调用模块驱动中的各个接口函数,例如:

case KDIMGSENSORIOC_T_OPEN:i4RetValue = adopt_CAMERA_HW_Open();break;
inline static int adopt_CAMERA_HW_Open(void)
{UINT32 err = 0;KD_IMGSENSOR_PROFILE_INIT();//power on sensor//if ((atomic_read(&g_CamHWOpend) == 0) && (atomic_read(&g_CamHWOpend2) == 0)) { if (atomic_read(&g_CamHWOpend) == 0  ) {//move into SensorOpen() for 2on1 driver// turn on power//kdModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM*) g_invokeSocketIdx, g_invokeSensorNameStr,true, CAMERA_HW_DRVNAME);//wait for power stable//mDELAY(10);//KD_IMGSENSOR_PROFILE("kdModulePowerOn");//if (g_pSensorFunc) {err = g_pSensorFunc->SensorOpen();if(ERROR_NONE != err) {PK_DBG(" ERROR:SensorOpen(), turn off power \n");kdModulePowerOn((CAMERA_DUAL_CAMERA_SENSOR_ENUM*) g_invokeSocketIdx, g_invokeSensorNameStr, false, CAMERA_HW_DRVNAME1);}}else {PK_DBG(" ERROR:NULL g_pSensorFunc\n");}KD_IMGSENSOR_PROFILE("SensorOpen");}else {//PK_ERR("adopt_CAMERA_HW_Open Fail, g_CamHWOpend = %d,g_CamHWOpend2 = %d\n ",atomic_read(&g_CamHWOpend),atomic_read(&g_CamHWOpend2) );PK_ERR("adopt_CAMERA_HW_Open Fail, g_CamHWOpend = %d\n ",atomic_read(&g_CamHWOpend) );}if (err == 0 ) {atomic_set(&g_CamHWOpend, 1);//atomic_set(&g_CamHWOpend2, 1);}return err?-EIO:err;
}   /* adopt_CAMERA_HW_Open() */

KDIMGSENSORIOC_T_OPEN命令就是通过调用adopt_CAMERA_HW_Open函数,而adopt_CAMERA_HW_Open函数中会通过err =g_pSensorFunc->SensorOpen();调用到模块驱动中的open函数。而在这里就不一一介绍了,下面来看看你看驱动中的各个接口函数有什么含义,以gc2035驱动为模板分析。

UINT32 GC2035_YUV_SensorInit(PSENSOR_FUNCTION_STRUCT *pfFunc)
{/* To Do : Check Sensor status here */if (pfFunc!=NULL)*pfFunc=&SensorFuncGC2035;return ERROR_NONE;
}     /* SensorSENSOR_FUNCTION_STRUCT     SensorFuncGC2035=
{GC2035Open,GC2035GetInfo,GC2035GetResolution,GC2035FeatureControl,GC2035Control,GC2035Close
};
GC2035Open:
UINT32 GC2035Open(void)
{volatile signed char i;kal_uint16 sensor_id=0;zoom_factor = 0;Sleep(10);SENSORDB("GC2035Open\r\n");//  Read sensor ID to adjust I2C is OK?for(i=0;i<3;i++){sensor_id = (GC2035_read_cmos_sensor(0xf0) << 8) | GC2035_read_cmos_sensor(0xf1);if(sensor_id != GC2035_SENSOR_ID)  // GC2035_SENSOR_ID = 0x2035{return ERROR_SENSOR_CONNECT_FAIL;}}SENSORDB("GC2035 Sensor Read ID OK \r\n");GC2035_Sensor_Init();GC2035_Write_More();Preview_Shutter =GC2035_read_shutter();return ERROR_NONE;
}

通过函数实现就可以看出来,这里是在通过i2c控制imagesensor 的register,读取deivceid ,看是否链接上对应的imagesensor;

GC2035GetInfo:
UINT32 GC2035GetInfo(MSDK_SCENARIO_ID_ENUM ScenarioId,MSDK_SENSOR_INFO_STRUCT *pSensorInfo,MSDK_SENSOR_CONFIG_STRUCT *pSensorConfigData)
{pSensorInfo->SensorPreviewResolutionX=GC2035_IMAGE_SENSOR_PV_WIDTH;pSensorInfo->SensorPreviewResolutionY=GC2035_IMAGE_SENSOR_PV_HEIGHT;pSensorInfo->SensorFullResolutionX=GC2035_IMAGE_SENSOR_FULL_WIDTH;pSensorInfo->SensorFullResolutionY=GC2035_IMAGE_SENSOR_FULL_HEIGHT;pSensorInfo->SensorCameraPreviewFrameRate=30;pSensorInfo->SensorVideoFrameRate=30;pSensorInfo->SensorStillCaptureFrameRate=10;pSensorInfo->SensorWebCamCaptureFrameRate=15;pSensorInfo->SensorResetActiveHigh=FALSE;pSensorInfo->SensorResetDelayCount=1;pSensorInfo->SensorOutputDataFormat=SENSOR_OUTPUT_FORMAT_YUYV;pSensorInfo->SensorClockPolarity=SENSOR_CLOCK_POLARITY_LOW;     /*??? */pSensorInfo->SensorClockFallingPolarity=SENSOR_CLOCK_POLARITY_LOW;pSensorInfo->SensorHsyncPolarity = SENSOR_CLOCK_POLARITY_LOW;pSensorInfo->SensorVsyncPolarity = SENSOR_CLOCK_POLARITY_LOW;pSensorInfo->SensorInterruptDelayLines = 1;pSensorInfo->CaptureDelayFrame = 4;pSensorInfo->PreviewDelayFrame = 1; // 2 bettypSensorInfo->VideoDelayFrame = 0;pSensorInfo->SensorMasterClockSwitch = 0;pSensorInfo->SensorDrivingCurrent = ISP_DRIVING_6MA;pSensorInfo->SensroInterfaceType=SENSOR_INTERFACE_TYPE_PARALLEL;SENSORDB("GC2035GetInfo\r\n");switch (ScenarioId){case MSDK_SCENARIO_ID_CAMERA_PREVIEW:case MSDK_SCENARIO_ID_VIDEO_PREVIEW:pSensorInfo->SensorClockFreq=22;pSensorInfo->SensorClockDividCount=3;pSensorInfo->SensorClockRisingCount= 0;pSensorInfo->SensorClockFallingCount= 2;pSensorInfo->SensorPixelClockCount= 3;pSensorInfo->SensorDataLatchCount= 2;pSensorInfo->SensorGrabStartX = 2;pSensorInfo->SensorGrabStartY = 2;break;case MSDK_SCENARIO_ID_CAMERA_CAPTURE_JPEG:pSensorInfo->SensorClockFreq=22;pSensorInfo->SensorClockDividCount=3;pSensorInfo->SensorClockRisingCount= 0;pSensorInfo->SensorClockFallingCount= 2;pSensorInfo->SensorPixelClockCount= 3;pSensorInfo->SensorDataLatchCount= 2;pSensorInfo->SensorGrabStartX = 2;pSensorInfo->SensorGrabStartY = 2;              break;default:pSensorInfo->SensorClockFreq=22;pSensorInfo->SensorClockDividCount=3;pSensorInfo->SensorClockRisingCount=0;pSensorInfo->SensorClockFallingCount=2;pSensorInfo->SensorPixelClockCount=3;pSensorInfo->SensorDataLatchCount=2;pSensorInfo->SensorGrabStartX = 2;  pSensorInfo->SensorGrabStartY = 2;            break;}memcpy(pSensorConfigData, &GC2035SensorConfigData, sizeof(MSDK_SENSOR_CONFIG_STRUCT));return ERROR_NONE;
}

上面的函数一共传递进来了3个变量,第一个变量:是控制camera的工作模式,(拍照、摄像等等)第2个参数:主要设置imagesensor 的频率的(时钟频率、预览频率、以及同步频率);第3个参数同样也是camera的设置,其实要看到底是在干嘛,只要看看这个参数是如何定义的就可以了。

GC2035GetResolution:
UINT32 GC2035GetResolution(MSDK_SENSOR_RESOLUTION_INFO_STRUCT *pSensorResolution)
{SENSORDB("GC2035GetResolution\r\n");pSensorResolution->SensorFullWidth=GC2035_IMAGE_SENSOR_FULL_WIDTH - 2 * IMAGE_SENSOR_START_GRAB_X;pSensorResolution->SensorFullHeight=GC2035_IMAGE_SENSOR_FULL_HEIGHT - 2 * IMAGE_SENSOR_START_GRAB_Y;pSensorResolution->SensorPreviewWidth=GC2035_IMAGE_SENSOR_PV_WIDTH - 2 * IMAGE_SENSOR_START_GRAB_X;pSensorResolution->SensorPreviewHeight=GC2035_IMAGE_SENSOR_PV_HEIGHT - 2 * IMAGE_SENSOR_START_GRAB_Y;pSensorResolution->SensorVideoWidth=GC2035_IMAGE_SENSOR_PV_WIDTH - 2 * IMAGE_SENSOR_START_GRAB_X;pSensorResolution->SensorVideoHeight=GC2035_IMAGE_SENSOR_PV_HEIGHT - 2 * IMAGE_SENSOR_START_GRAB_Y;return ERROR_NONE;
}

设置camera在预览模式下的高度、宽度等;

GC2035FeatureControl:
UINT32 GC2035FeatureControl(MSDK_SENSOR_FEATURE_ENUM FeatureId,UINT8 *pFeaturePara,UINT32 *pFeatureParaLen)
{UINT16 *pFeatureReturnPara16=(UINT16 *) pFeaturePara;UINT16 *pFeatureData16=(UINT16 *) pFeaturePara;UINT32 *pFeatureReturnPara32=(UINT32 *) pFeaturePara;UINT32 *pFeatureData32=(UINT32 *) pFeaturePara;MSDK_SENSOR_CONFIG_STRUCT *pSensorConfigData=(MSDK_SENSOR_CONFIG_STRUCT *) pFeaturePara;MSDK_SENSOR_REG_INFO_STRUCT *pSensorRegData=(MSDK_SENSOR_REG_INFO_STRUCT *) pFeaturePara;switch (FeatureId){case SENSOR_FEATURE_GET_RESOLUTION:*pFeatureReturnPara16++=GC2035_IMAGE_SENSOR_FULL_WIDTH;*pFeatureReturnPara16=GC2035_IMAGE_SENSOR_FULL_HEIGHT;*pFeatureParaLen=4;break;case SENSOR_FEATURE_GET_PERIOD:*pFeatureReturnPara16++=GC2035_IMAGE_SENSOR_PV_WIDTH;*pFeatureReturnPara16=GC2035_IMAGE_SENSOR_PV_HEIGHT;*pFeatureParaLen=4;break;case SENSOR_FEATURE_GET_PIXEL_CLOCK_FREQ://*pFeatureReturnPara32 = GC2035_sensor_pclk/10;*pFeatureParaLen=4;break;case SENSOR_FEATURE_SET_ESHUTTER:break;case SENSOR_FEATURE_SET_NIGHTMODE:GC2035_night_mode((BOOL) *pFeatureData16);break;case SENSOR_FEATURE_SET_GAIN:case SENSOR_FEATURE_SET_FLASHLIGHT:break;case SENSOR_FEATURE_SET_ISP_MASTER_CLOCK_FREQ:GC2035_isp_master_clock=*pFeatureData32;break;case SENSOR_FEATURE_SET_REGISTER:GC2035_write_cmos_sensor(pSensorRegData->RegAddr, pSensorRegData->RegData);break;case SENSOR_FEATURE_GET_REGISTER:pSensorRegData->RegData = GC2035_read_cmos_sensor(pSensorRegData->RegAddr);break;case SENSOR_FEATURE_GET_CONFIG_PARA:memcpy(pSensorConfigData, &GC2035SensorConfigData, sizeof(MSDK_SENSOR_CONFIG_STRUCT));*pFeatureParaLen=sizeof(MSDK_SENSOR_CONFIG_STRUCT);break;case SENSOR_FEATURE_SET_CCT_REGISTER:case SENSOR_FEATURE_GET_CCT_REGISTER:case SENSOR_FEATURE_SET_ENG_REGISTER:case SENSOR_FEATURE_GET_ENG_REGISTER:case SENSOR_FEATURE_GET_REGISTER_DEFAULT:case SENSOR_FEATURE_CAMERA_PARA_TO_SENSOR:case SENSOR_FEATURE_SENSOR_TO_CAMERA_PARA:case SENSOR_FEATURE_GET_GROUP_INFO:case SENSOR_FEATURE_GET_ITEM_INFO:case SENSOR_FEATURE_SET_ITEM_INFO:case SENSOR_FEATURE_GET_ENG_INFO:break;case SENSOR_FEATURE_GET_GROUP_COUNT:*pFeatureReturnPara32++=0;*pFeatureParaLen=4;        break;case SENSOR_FEATURE_GET_LENS_DRIVER_ID:// get the lens driver ID from EEPROM or just return LENS_DRIVER_ID_DO_NOT_CARE// if EEPROM does not exist in camera module.*pFeatureReturnPara32=LENS_DRIVER_ID_DO_NOT_CARE;*pFeatureParaLen=4;break;case SENSOR_FEATURE_CHECK_SENSOR_ID:GC2035_GetSensorID(pFeatureData32);break;case SENSOR_FEATURE_SET_YUV_CMD://printk("GC2035 YUV sensor Setting:%d, %d \n", *pFeatureData32,  *(pFeatureData32+1));GC2035YUVSensorSetting((FEATURE_ID)*pFeatureData32, *(pFeatureData32+1));break;case SENSOR_FEATURE_SET_VIDEO_MODE:GC2035YUVSetVideoMode(*pFeatureData16);break;default:break;              }return ERROR_NONE;
}

这个是上层会提供featureid,底层通过这个id进行不同case的执行为para和paralen赋值:

GC2035Control:
UINT32 GC2035Control(MSDK_SCENARIO_ID_ENUM ScenarioId, MSDK_SENSOR_EXPOSURE_WINDOW_STRUCT *pImageWindow,MSDK_SENSOR_CONFIG_STRUCT *pSensorConfigData)
{switch (ScenarioId){case MSDK_SCENARIO_ID_CAMERA_PREVIEW:case MSDK_SCENARIO_ID_VIDEO_PREVIEW:GC2035_sensor_cap_zsd = KAL_FALSE;GC2035Preview(pImageWindow, pSensorConfigData);break;case MSDK_SCENARIO_ID_CAMERA_CAPTURE_JPEG:GC2035_sensor_cap_zsd = KAL_FALSE;GC2035Capture(pImageWindow, pSensorConfigData);break;case MSDK_SCENARIO_ID_CAMERA_ZSD:GC2035_sensor_cap_zsd = KAL_TRUE;GC2035Capture(pImageWindow, pSensorConfigData);break;default:break;}return TRUE;
}

这个函数和上面一样,也是提供控制的一个Interface。

GC2035Close:

UINT32 GC2035Close(void)
{
//     CISModulePowerOn(FALSE);SENSORDB("GC2035Close\r\n");return ERROR_NONE;
}

这里close没有实现任何事情。
     到这来MTK的camera驱动的加载就结束了,但这只是在整个android中对camera架构中最基础的一些分析,android系统中camera还有很多十分复杂的东西,例如ISP,上层图像的形成,camera数据的传输等等。

camer驱动模块加载分析相关推荐

  1. Linux驱动模块加载和卸载以及设备注册与注销

    一.驱动模块的加载和卸载 Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序.第二种就是将驱动编译成模块(Linux下模块扩展名为 ...

  2. 【Android 安全】DEX 加密 ( 不同 Android 版本的 DEX 加载 | Android 8.0 版本 DEX 加载分析 | Android 5.0 版本 DEX 加载分析 )

    文章目录 一.不同版本的 DEX 加载 1.Android 8.0 版本 DEX 加载分析 2.Android 6.0 版本 DEX 加载分析 3.Android 5.0 版本 DEX 加载分析 一. ...

  3. ios应用程序加载分析(一)

    app启动分析+猜测 首先通过入口函数main断点查看 nothing ... 通过load入口断点查看 得到大致的堆栈关键信息 (反向调用信息如下) dyld - _dyld_start dyld ...

  4. ios应用程序加载分析(二)

    为了不至于分析链条发生断层,请参阅ios应用程序加载分析(一) _dyld_objc_notify_register ---- sNotifyObjcInit 是如何关联上的 sNotifyObjcI ...

  5. 【VirtualAPP 双开系列07】第三方 APP Service、Provider 加载分析

    目录: 1. Service 加载分析 2. Provider 加载分析 1. Service 加载分析 2. Provider 加载分析

  6. CANoe软件使用(二)——数据加载分析

    CANoe软件使用(二)--数据加载分析 目录 新建CANoe工程 DBC和通道加载 数据分析 离线设置 数据查看 数据保存 目录 本节主要讲述下离线的CAN数据分析.通常情况下,工程师通过CANoe ...

  7. Linux驱动3:驱动模块加载与卸载

    目录 一.环境配置 1.开发板环境 2.uboot环境 ①设置bootargs ②设置bootcmd 二.加载驱动与卸载驱动 1.加载命令选择 2.创建目录环境以及驱动文件复制 3.加载驱动 提示①& ...

  8. nodejs模块加载分析(1).md

    前言 上篇 nodejs 启动流程分析中,遗留了几个问题.这一篇,主要讲讲模块加载流程.大家都应该熟悉 timer 模块的相关功能.我们就以 timer 为引子,一步步看下去吧. C++ init 方 ...

  9. Chrome DevTools 之 Network,网络加载分析利器

    虽然一直在用Chrome DevTools,但大多停留在常用的功能和调试上,比如Elements/Network/Sources/Console等主要功能,而对于性能分析/优化相关的Timeline/ ...

最新文章

  1. 请输入课程编号C语言,C语言 学生选课系统 程序
  2. 职场减压妙计:主动降职
  3. Python 条件语句 学习转载
  4. getter/setter_Getters / Setters。 邪恶。 期。
  5. Flash网站流量统计的方法
  6. self 实例对象-代码详细解释
  7. 800多名各国院士热忱参与 第三届“科学探索奖”名单公布
  8. python读取大文件的坑_Python读取大文件的坑“与内存占用检测
  9. 三步法助你快速定位网站性能问题
  10. lol服务器维修2020,lol2020年5月29日停机维护到几点 英雄联盟维护公告是什么
  11. kafka partition分配_【kafka】消费者对应的分配partition分区策略
  12. Debian上如何打开关闭端口
  13. 1212_MISRA_C规范学习笔记_控制表达式规则要求
  14. 【备忘】尚学堂白贺翔java互联网架构师视频教程下载
  15. 如何把网吧计算机放到桌面,网吧电脑怎么切换游戏桌面
  16. 计算机变量与变量地址,数据缓冲区与变量的地址(更新1)
  17. 网络工程师十月份免费讲座
  18. Ansys Meshing
  19. php全局变量与局部变量
  20. 新形黑猩猩脸部辨识系统 帮助保护朕亨公益组织及时发现网络非法交易

热门文章

  1. Linux搭建Hyperledger Fabric区块链框架 - Hyperledger Fabric模型概念
  2. 华为nova8se和vivoS7e的区别哪个好
  3. VBOX虚拟硬件修改
  4. 苹果cmsv10仿西瓜影院电脑手机影视自适应免费模板
  5. 自我介绍及注册github和上传文件
  6. 计算机学后感作文400,毕业感想作文400字(4篇)
  7. 晚上看了一个知乎评论区,我失眠了
  8. 高端android手机,2021年最值得买的几款高端手机,颜值还超高!
  9. 无法像唐骏一样地成功
  10. 【Web前端开发】——HTML练习一:标记信件