行业知识
AIS 模块总结
SA8155P QCOM 车载camera 模块
1. 前言
车载和手机的Camera系统是两套不一样的架构,手机Camera系统最终生成符合人眼感官的图像,需要考虑多样化的场景,如美颜、夜景提亮、降噪、虚化等;而车载Camera系统AIS(Automotive Imaging System)的图像大部分是给自动驾驶等机器识别使用,更多考虑的是远距离传输、多摄像头图像处理等场景。因此两套系统不管是从硬件结构还是从软件框架上差异都很大。
2. 简介
车载Camera的基本组成如下,黄色的箭头代表数据的传输,蓝色的箭头代表控制信号的传输。
ISP图像处理芯片有的会放置在摄像头这边,把处理后的信号传输给到主机,有的是不放置ISP芯片,由主机那边的内置ISP芯片进行图像处理,这样摄像头端的散热会好很多,辐射也小。
这里跟手机Camera最大的差别是多了一个串行器和解串器。我们知道相机传感器或ISP的图像数据输出总线是MIPI CSI标准,其特点是高速穿行,但是传输总线距离较短,否则容易受到干扰无法保证信号的完整性,所以在车辆上,我们需要将其转换成例如GMSL等适合在车上长距离传输的高速总线标准进行传输,所以相机模组内部通常会通过串行板进行总线的转换。另外同轴电缆既可以用来为模组提供电源,也可以传输图像数据。如一些项目,串行器使用MAX92745,解串器是MAX9296A。
总的来说,手机系统上,SOC对接camera硬件,可以直接操作摄像头;而车载系统上,SOC对接的是解串器,camera的配置、上下电需要摄像头模组内的串行器来控制,另外摄像头的供电和信号传输是通过同轴电缆完成的。
车载Camera最经典的使用场景是360°全景图像系统(AVM:Around View Monitor),其硬件拓扑图如下。
原理是利用最少4个广角摄像头的数据合成一幅360°图像的画面。
高清摄像头项目有2种配置方式,分为泊车摄像头(前、后、左、有)和倒车后视摄像头。泊车摄像头和倒车后视摄像头除了Lens其他硬件方案一样。
泊车摄像头系统由安装于车身前、后、左、右四路高清摄像头和智能域控制器组成,域控制器通过同轴线(POC)给摄像头供电,摄像头经过LVDS串行器将视频信号传输给域控制器。摄像头进行图像采集,域控制器对图像进行裁剪、畸变校正、图像拼接、2D/3D视角切换、轨迹线生成等处理,XPU处理完后送入中央控制域进行显示。如下图高清全景系统连接示意图。
倒车后视摄像头系统由安装于车身后部高清摄像头和中央域控制器组成,摄像头进行图像采集,中央域控制器对图像进行裁减、轨迹线生成、显示屏显示等处理。如下图倒车后视摄像头系统连接示意图。
3. AIS系统简介
qcom AIS软件框架图如下。
AIS Server作为一个守护进程运行在Native层,该服务通过封装好的平台相关库最终使用IOCTL和Kernel进行交互,使用中断产生事件,并以V4L2视频框架进行驱动;
同时AIS Server通过Socket和AIS Client交互,经过封装的Socket实现了向对端发送指令并取回对端返回的数据,其实就是实现了IPC。
最新的软件框架图如下。
其实变化不大,主要在Configurer管理的硬件模块,这个跟芯片平台是强相关的。
4. 车载Camera模块
车载Camera模块从上到下涉及Camera APP、Framework、CameraService、CameraProvider、AISService。因车载Camera使用场景比较单一简单,不需要复杂的逐帧控制等需求,当前使用的是Camera1 API。其中APP、Framework层是Android通用接口,这里不予介绍。
关键流程的软件架构图如下。
4.1 CameraService
CameraService注册在Native ServiceManager,使用binder和Framework进行通信。
4.2 CameraProvider
CameraProvider注册在hwServiceManager,使用hwbinder和CameraService进行通信(HwBinder机制可以跨System和Vendor分区使用,使用HIDL接口)。同时使用Socket和ais_service进行通信。
这里涉及到厂商对camera device的具体实现,脑图就不贴上来了。
4.3 ais_service
ais_service是高通针对车载系统的Camera HAL实现,作为后台服务程序运行在native层。
在手机系统里,这部分代码是运行在camera.provider进程的。车载系统把这部分代码独立成一个服务的原因,我猜测是为了适配快速倒车的场景。快速倒车是指Android系统还没有完全起来,用户切换到倒车模式时把倒车影像显示出来。Android启动完成(显示桌面)需要比较长的时间(10s以上),而倒车影像不必等framework启动完成,就可以通过native进程完成图像显示。实现原理是native层的client拉通display和ais_service,把图像数据显示到大屏。native层测试用例可参考qcarcam_test。
ais_server作为camera HAL层的核心,内部有跟平台和硬件强相关的配置信息。可参考高通文档(80-pg469-93_e_sa6155_sa8155_sa8195_automotive_camera_ais_customization_guide.pdf)。
首先是跟平台强相关的CameraBoardType结构体。
/** * This structure defines the board specific configurable items related to the * camera subsystem. */typedef struct{ CameraHwBoardType boardType; /** 该结构体描述了连接到camera子系统的硬件配置信息,以及连接方式等。编译到动态库libais_config.so,通过接口GetCameraConfigInterface()返回ICameraConfig结构体给外部使用。
i2c设备跟平台芯片强相关,一般不需要我们修改。
/** * This structure describes I2C Devices */typedef struct { //目前只支持 CAMERA_I2C_TYPE_CCI CameraI2CType i2ctype; /** 每个输入设备被描述为CameraSensorBoardType,包括解串器的信息,CSI、I2C、GPIO、中断的配置信息等。
/** * This structure defines the board specific configurable items for a given * camera sensor. */typedef struct{ /** CameraDeviceConfigType配置 input device 信息。
/** * This structure defines input device configuration */typedef struct { /** CameraCsiInfo配置了csi的信息,需要结合硬件电路图确定de-serializer的csi输出口。
CameraChannelInfoType定义了QCarCam IDs到streams的映射。
/** * This structure defines the mapping of unique qcarcam descriptor to * input sources as well as the default operation mode. */typedef struct{ /** * unique qcarcam inputId that maps to */ uint32 aisInputId; //映射到inputSrc的唯一ID,HAL层使用,如qcarcam_config_singleid.xml里配置的input_id /** * unique of input id and default operation mode */ struct { uint32 devId; //匹配CameraSensorBoardType里的devId uint32 srcId; //匹配sensor的subchannels信息里的src_id,最终匹配到modes }inputSrc[MAX_CHANNEL_INPUT_SRCS]; qcarcam_opmode_type opMode; //选择对应的Pproc进行post process} CameraChannelInfoType; qcarcam_opmode_type 可以选择具体的 Pproc。
typedef enum { QCARCAM_OPMODE_RAW_DUMP, QCARCAM_OPMODE_SHDR, QCARCAM_OPMODE_INJECT, QCARCAM_OPMODE_PAIRED_INPUT, QCARCAM_OPMODE_DEINTERLACE, QCARCAM_OPMODE_MAX} qcarcam_opmode_type;//对应的ProcChain/** * Proc Chain Definitions */static AisProcChainDefType* g_ProcChainDefs[QCARCAM_OPMODE_MAX] ={ [QCARCAM_OPMODE_RAW_DUMP] = &RawdumpProcChainDef, [QCARCAM_OPMODE_SHDR] = &SHDR_ProcChainDef, [QCARCAM_OPMODE_INJECT] = &InjectProcChainDef, [QCARCAM_OPMODE_PAIRED_INPUT] = &PairedInput_ProcChainDef, [QCARCAM_OPMODE_DEINTERLACE] = &DeinterlaceProcChainDef,};//默认是 QCARCAM_OPMODE_RAW_DUMP/** * RAW Dump usecase */static AisBuflistDefType RawdumpBuflist[] ={ { .id = AIS_BUFLIST_OUTPUT_USR, .GetBuf = GetBufferDefault, .PutBuf = BuflistEnqIfe, .AllocBuf = NULL, .allocParams = { .allocType = AIS_BUFLIST_ALLOC_NONE, .maxBuffers = QCARCAM_MAX_NUM_BUFFERS, .matchBuflistId = 0, .fmt = (qcarcam_color_fmt_t)0, .width = 0, .height = 0, .stride = 0 } },};// 只有一个 pProcChainstatic AisProcChainType RawdumpPProc[] ={ { AIS_PPROC_USR_DONE, NULL, 0, {AIS_BUFLIST_OUTPUT_USR}, {} },};static AisProcChainDefType RawdumpProcChainDef ={ .pProcChain = RawdumpPProc, .nProc = STD_ARRAY_SIZE(RawdumpPProc), .pBufferlistDef = RawdumpBuflist, .nBuflist = STD_ARRAY_SIZE(RawdumpBuflist)};CameraResult AisProcChainManagerPrivate::Init(void){ CameraResult rc = CAMERA_SUCCESS; uint32 i;//PProcId 对应到具体的实现类 m_pPProc[AIS_PPROC_USR_DONE] = AisPProcUsrDone::Create(); m_pPProc[AIS_PPROC_ISP] = AisPProcIsp::Create(); m_pPProc[AIS_PPROC_GPU] = AisPProcGpu::Create(); m_pPProc[AIS_PPROC_FRAMESYNC] = AisPprocFrameSync::Create(); for (i = 0; i 其中aisInputId是给native层使用的,和framework传下来的cameraId的对应关系在文件/vendor/bin/input_mapping_id.xml中。
然后是具体解串器的配置信息,如MAX9296A。最重要的是结构体sensor_lib_t。
typedef struct{ /* private context - must be first element */ void* priv_ctxt; /* close lib function ptr */ int (*sensor_close_lib)(void* ctxt); /* channels */ img_src_channel_t channels[MAX_IMAGE_SOURCES]; //channels的输出格式,如图像格式,长宽,帧率 unsigned int num_channels; /* subchannels */ img_src_subchannel_t subchannels[MAX_IMAGE_SOURCES]; unsigned int num_subchannels; /* sensor slave info */ struct camera_sensor_slave_info sensor_slave_info;/* 重要struct camera_sensor_slave_info { char sensor_name[32]; //sensor name,framework用来打印log enum sensor_camera_id camera_id; //对应到CameraSensorBoardType的索引 unsigned short slave_addr; //I2C从设备地址 enum camera_i2c_freq_mode i2c_freq_mode; enum camera_i2c_reg_addr_type addr_type; //I2C地址字节数 /* 具体由厂商给出的地址格式决定,如地址是0x0313,则为addr_type=CAMERA_I2C_WORD_ADDR,若是0x12,则为CAMERA_I2C_BYTE_ADDR enum camera_i2c_reg_addr_type { CAMERA_I2C_BYTE_ADDR = 1, CAMERA_I2C_WORD_ADDR, CAMERA_I2C_3B_ADDR, CAMERA_I2C_ADDR_TYPE_MAX }; */ enum camera_i2c_data_type data_type; //跟addr_type类似,I2C数据的字节数 struct sensor_id_info_t sensor_id_info; struct camera_power_setting_array power_setting_array; //上电和下电时序 unsigned char is_init_params_valid; };*/ /* sensor output settings */ sensor_output_t sensor_output; //unused /* sensor output register address */ struct sensor_output_reg_addr_t output_reg_addr; /* sensor exposure gain register address */ struct sensor_exp_gain_info_t exp_gain_info; /* sensor aec info */ sensor_aec_data_t aec_info; /* number of frames to skip after start stream info */ unsigned short sensor_num_frame_skip; //unused /* number of frames to skip after start HDR stream info */ unsigned short sensor_num_HDR_frame_skip; //unused /* sensor pipeline delay */ unsigned int sensor_max_pipeline_frame_delay; /* sensor lens info */ sensor_property_t sensor_property; /* imaging pixel array size info */ sensor_imaging_pixel_array_size pixel_array_size_info; /* Sensor color level information */ sensor_color_level_info color_level_info; /* sensor port info that consists of cid mask and fourcc mapaping */ sensor_stream_info_array_t sensor_stream_info_array; /* Sensor Settings */ struct camera_i2c_reg_setting start_settings; struct camera_i2c_reg_setting stop_settings; struct camera_i2c_reg_setting groupon_settings; struct camera_i2c_reg_setting groupoff_settings; struct camera_i2c_reg_setting embedded_data_enable_settings; struct camera_i2c_reg_setting embedded_data_disable_settings; struct camera_i2c_reg_setting aec_enable_settings; struct camera_i2c_reg_setting aec_disable_settings; /* sensor test pattern info */ sensor_test_info test_pattern_info; /* sensor effects info */ struct sensor_effect_info effect_info; /* Sensor Settings Array */ struct sensor_lib_reg_settings_array init_settings_array; struct sensor_lib_reg_settings_array res_settings_array; struct sensor_lib_out_info_array out_info_array; struct sensor_csi_params csi_params; struct sensor_csid_lut_params_array csid_lut_params_array; struct sensor_lib_crop_params_array crop_params_array; /* Exposure Info */ sensor_exposure_table_t exposure_func_table; /* video_hdr mode info*/ struct sensor_lib_meta_data_info_array meta_data_out_info_array; /* sensor_capability */ /* bit mask setting for sensor capability */ /* bit positions are provided in sensor_capability_t */ unsigned int sensor_capability; /* sensor_awb_table_t */ sensor_awb_table_t awb_func_table; /* Parse RDI stats callback function */ sensor_RDI_parser_stats_t parse_RDI_stats; /* full size info */ sensor_rolloff_config rolloff_config; /* analog-digital conversion time */ long long adc_readout_time; /* number of frames to skip for fast AEC use case */ unsigned short sensor_num_fast_aec_frame_skip; /* add soft delay for sensor settings like exposure, gain ...*/ unsigned char app_delay[SENSOR_DELAY_MAX]; /* for noise profile calculation Tuning team must update with proper values. */ struct sensor_noise_coefficient_t noise_coeff; /* Flag to be set if any external library are to be loaded */ unsigned char external_library; /* custom func table ptr */ sensor_custom_func_table_t sensor_custom_func; //客制化API boolean use_sensor_custom_func; //是否使用客制化接口 uint32 src_id_enable_mask; uint32 input_id;} sensor_lib_t; ais调用sensor接口的时序图如下。
AIS使用了大量IOCTL和V4L2结合的实例,来完成ais_server和kernel的通信。以解串器的中断诊断为例。
/* --------------------------------------------------------------------------- * FUNCTION - sensor_setup_gpio_interrupt - * * DESCRIPTION: Setup gpio id as interrupt * ------------------------------------------------------------------------ */CameraResult SensorPlatformLinux::SensorSetupGpioInterrupt(enum camera_gpio_type gpio_id, sensor_intr_callback_t cb, void *data){ //1.获取中断配置信息 //.... //2.以GPIO号为id订阅V4L2消息 struct v4l2_event_subscription sub = {}; sub.type = AIS_SENSOR_EVENT_TYPE; sub.id = m_probeCmd->gpio_intr_config[freeIdx].gpio_num; rc = ioctl(m_sensorFd, VIDIOC_SUBSCRIBE_EVENT, &sub); if(rc sensor_slave_info.sensor_name, AIS_SENSOR_EVENT_TYPE, rc); result = CAMERA_EFAILED; }//3.通过IOCTL初始化中断信息 if (CAMERA_SUCCESS == result) { cam_cmd.op_code = AIS_SENSOR_INTR_INIT; cam_cmd.size = sizeof(struct ais_sensor_gpio_intr_config); cam_cmd.handle_type = CAM_HANDLE_USER_POINTER; cam_cmd.reserved = 0; cam_cmd.handle = (uint64_t)m_probeCmd->gpio_intr_config; rc = ioctl(m_sensorFd, VIDIOC_CAM_CONTROL, &cam_cmd, "intr_init"); if (rc pCtxt = this; pPlatformIntr->isUsed = TRUE; pPlatformIntr->pCbFcn = cb; pPlatformIntr->pData = data; pPlatformIntr->gpio_id = gpio_id; pPlatformIntr->gpio_num = pinInfo.pin_id; snprintf(pPlatformIntr->tname, sizeof(pPlatformIntr->tname), "sensor_platform_intr_poll_thread_%d", m_eCameraInterface); result = CameraCreateThread(CAMERA_THREAD_PRIO_DEFAULT, 0, SensorPlatformIntrPollThread, (void*)(pPlatformIntr), 0, pPlatformIntr->tname, &pPlatformIntr->hThread); (void)CameraSetThreadPriority(pPlatformIntr->hThread, CAMERA_THREAD_PRIO_HIGH_REALTIME); if (result != CAMERA_SUCCESS) { pPlatformIntr->isUsed = FALSE; //@todo: any cleanup... } }}//kernel层对应的代码int32_t cam_sensor_driver_cmd(struct cam_sensor_ctrl_t *s_ctrl,void *arg){ switch (cmd->op_code): case AIS_SENSOR_INTR_INIT: { rc = cam_sensor_init_gpio_intr(gpio_intr_cfg,s_ctrl); } }}static int32_t cam_sensor_init_gpio_intr(struct ais_sensor_gpio_intr_config *gpio_intr_info,struct cam_sensor_ctrl_t *s_ctrl){int32_t rc = 0;int32_t gpio_num = 0;int32_t gpio_cfg0 = 0;int32_t idx = 0;//kernel层支持多个INTR的注册,但HAL层获取INTR的代码只允许注册一个for (idx = 0; idx s_intr[idx].work_inited &&gpio_intr_info->gpio_num != -1) {gpio_num = gpio_intr_info->gpio_num;gpio_cfg0 = gpio_intr_info->gpio_cfg0;s_ctrl->s_intr[idx].sctrl = s_ctrl;s_ctrl->s_intr[idx].gpio_array[0].gpio = gpio_num;//初始化工作队列INIT_WORK(&s_ctrl->s_intr[idx].irq_work,bridge_irq_work);//申请一个GPIO口rc = gpio_request_one(gpio_num,GPIOF_DIR_IN, "camera_intr");if (!rc) { //用该GPIO口申请硬件中断,中断响应函数为bridge_irqrc = request_irq(gpio_to_irq(gpio_num),bridge_irq,IRQF_ONESHOT | gpio_cfg0,"qcom,ais",&s_ctrl->s_intr[idx]);if (rc s_intr[idx].work_inited = 1;break;}}return rc;}//在任务队列里生成一个id为GPIO口的V4L2事件并入队,因为native层订阅过该id的信息,所以native能触发该消息的处理函数static void bridge_irq_work(struct work_struct *work){struct cam_sensor_ctrl_t *s_ctrl;struct cam_sensor_intr_t *s_intr;struct v4l2_event event;s_intr = container_of(work, struct cam_sensor_intr_t,irq_work);s_ctrl = s_intr->sctrl;mutex_lock(&s_ctrl->cam_sensor_mutex);/* Queue the event */memset(&event, 0, sizeof(struct v4l2_event));event.id = s_intr->gpio_array[0].gpio;event.type = AIS_SENSOR_EVENT_TYPE;v4l2_event_queue(s_ctrl->v4l2_dev_str.sd.devnode, &event);mutex_unlock(&s_ctrl->cam_sensor_mutex);}//在中断响应函数里触发工作任务static irqreturn_t bridge_irq(int irq_num, void *dev){struct cam_sensor_intr_t *s_intr = dev;//将任务提交到工作队列schedule_work(&s_intr->irq_work);return IRQ_HANDLED;} 4.4 MAX9296A
有些项目使用的是美信的MAX9296A解串器,该解串器有GMSL1和GMSL2两种工作模式。默认工作模式由硬件电路决定:
MAX9296A 有两个输入 LINKA/B 和两个输出 Port A/B,内部有4个 Pipeline。同个 Port 口可以支持多路输出,通过 VC 来区分。
以下是当前GMSL2模式下默认数据流,配置0x51 -> 0x00,表示 streamId 为0的数据流会 remap 到 Pipeline Y 上,在 GMSL2 模式下,Pipeline 和 streamId 绑定,streamId 由出入信号的串行器设置,此时跟输入流接 LINKA 或 B 没有关系,但是硬件上输入流具体接入的是 LINKA 还是 LINKB,需要配置寄存器 0x0010 来选择。具体拓扑图就不贴上来了,需要可以自行联系美信对接人。
5. 调试
这个章节描述如何进行debug。
5.1 ais debug
ais的log由log等级控制,默认输出等级是
/** * ais default configuration */#if defined(__INTEGRITY)#define AIS_LOG_DEFAULT_CONF AIS_LOG_CONF_MAKE(AIS_LOG_MODE_CONSOLE, AIS_LOG_LVL_WARN)#elif defined(CAMERA_UNITTEST)#define AIS_LOG_DEFAULT_CONF AIS_LOG_CONF_MAKE(AIS_LOG_MODE_CONSOLE, AIS_LOG_LVL_WARN)#else#define AIS_LOG_DEFAULT_CONF AIS_LOG_CONF_MAKE(AIS_LOG_MODE_OS, AIS_LOG_LVL_HIGH)#endif
可以通过文件来修改输出等级。
//ais_server的Log配置文件: vendor/etc/camera/ais_log_ais_server.conf//ais_client的Log配置文件: vendor/etc/camera/ais_log.conf## AIS Log Configurations## Copyright (c) 2018-2020 Qualcomm Technologies, Inc.# All Rights Reserved.# Confidential and Proprietary - Qualcomm Technologies, Inc.##memory log size#MEM_SIZE=1048576#os log size#OS_SIZE=1048576#log file#FILE_PATH=/tmp/ais_log.txt#FILE_SIZE=104857600## Module-Specified Log Configuration# Log id:##define AIS_MOD_ID_RSRV 0 //reserved#define AIS_MOD_ID_CAMERA_PLATFORM 1#define AIS_MOD_ID_CAMERAQ 2# ...## Log Level: 1 == AIS_LOG_LVL_FATAL# 2 == AIS_LOG_LVL_CRIT# 3 == AIS_LOG_LVL_ERR# 4 == AIS_LOG_LVL_WARN# 5 == AIS_LOG_LVL_HIGH# 6 == AIS_LOG_LVL_MED# 7 == AIS_LOG_LVL_LOW# 8 == AIS_LOG_LVL_DBG# 9 == AIS_LOG_LVL_DBG1## Log Mode: M == log to memory# S == log to OS logger# F == log to file# C == log to console## Example:# MODULE_xx=yMSFC# xx means module ID, 0 is reserved, the maximum number is 255# y means log level# MSFC means combination of log modes, which means one message can be output to more than one destinations### Conf file path in target# Android: /vendor/etc/cameraGLOBAL=8S #global log level set to DBG and log to OS loggerMODULE_2=8SF #module CAMERAQ set log level to DBG and log to both os logger and file
5.2 Linux
5.2.1 Kernel log
Camera Kernel drivers模块的Log可通过文件节点设置。
echo 0x20 > /sys/module/cam_debug_util/parameters/debug_mdl #enable CAM_SENSOR debug log#define CAM_CDM (1 // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "(gdb) 启动", "type": "cppdbg", "request": "launch", //带符号的目标调试程序 "program": "Z:\android\out\target\product\au8155\symbols\vendor\bin\ais_server", "args": [], "stopAtEntry": false, //其他的动态库路径 "additionalSOLibSearchPath": "Z:\android\out\target\product\au8155\symbols\vendor\lib64", //本地主机的socket地址,注意需要用adb forward 把设备的地址链接到本地主机上 "miDebuggerServerAddress": "localhost:5555", //源码路径 "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", //gdb路径 "miDebuggerPath": "D:\Program Files\android-ndk-r23b\prebuilt\windows-x86_64\bin\gdb.exe", //gdb参数 "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true }, { "text": "-environment-directory Z:\android", "description": "gdb command: dir", "ignoreFailures": false }, { "text": "-gdb-set solib-absolute-prefix Z:\android\out\target\product\au8155\symbols", "description": "gdb command: set solib-absolute-prefix", "ignoreFailures": false }, { "text": "-gdb-set solib-search-path Z:\android\out\target\product\au8155\symbols\vendor\lib64;Z:\android\out\target\product\au8155\symbols\vendor\lib;", "description": "gdb command: set solib-search-path", "ignoreFailures": false }, { "description": "将反汇编风格设置为 Intel", "text": "-gdb-set disassembly-flavor intel", "ignoreFailures": true}, ] } ]} "text": "handle SIGPIPE nostop noprint pass", "description": "ignore SIGPIPE", "ignoreFailures": true },
下一篇:域名需要怎么备案?