这篇文章给介绍一下HDMI-IN的通路,实现的热插拔、切换分辨率是怎么实现的。HDMI-IN使用HDMI转MIPI的通路,将转接芯片当做类似camera的设备进行开发,但是通常的MIPI camera都是固定的分辨率,就算有分辨率切换也是主控端主动发起,逻辑比较容易实现,但是HDMI-IN的场景,由于外介入的信号源不可控制,所以需要实现分辨率切换与热拔插的逻辑,下面一起看下这部分的实现,我们按照安卓14,RK3576使用RK628F实现HDMI-IN,采用camera的应用框架。
1.驱动实现部分
目前的框架,驱动是按照上报事件的形式,通过V4l2框架上报拔插或者切换分辨率的事件通知上层,这里我们看下驱动的实现,主要有三个事件
需要订阅三个对应的事件,通过回调v4l2_subdev_core_ops的成员.subscribe_event来订阅需要的V4L2事件:
static int rk628_csi_subscribe_event(struct v4l2_subdev *sd, struct v4l2_fh *fh,
struct v4l2_event_subscription *sub)
{
switch (sub->type) {
case V4L2_EVENT_SOURCE_CHANGE:
return v4l2_src_change_event_subdev_subscribe(sd, fh, sub);
case V4L2_EVENT_CTRL:
return v4l2_ctrl_subdev_subscribe_event(sd, fh, sub);
case RK_HDMIRX_V4L2_EVENT_SIGNAL_LOST:
return v4l2_event_subscribe(fh, sub, 0, NULL);
default:
return -EINVAL;
}
}
更新上报分辨率变化的事件:需要在信号分辨率发生变换之后,上报事件通知应用。
static int rk628_csi_format_change(struct v4l2_subdev *sd)
{
struct rk628_csi *csi = to_csi(sd);
struct v4l2_dv_timings timings;
const struct v4l2_event rk628_csi_ev_fmt = {
.type = V4L2_EVENT_SOURCE_CHANGE,
.u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION,
};
int ret;
ret = rk628_csi_get_detected_timings(sd, &timings);
if (ret) {
v4l2_dbg(1, debug, sd, "%s: get timing fail\n", __func__);
return LOCK_FAIL;
}
if (!v4l2_match_dv_timings(&csi->timings, &timings, 0, false)) {
/* automatically set timing rather than set by userspace */
rk628_csi_s_dv_timings(sd, &timings);
v4l2_print_dv_timings(sd->name,
"rk628_csi_format_change: New format: ",
&timings, false);
}
if (sd->devnode)
v4l2_subdev_notify_event(sd, &rk628_csi_ev_fmt);
return LOCK_OK;
}
更新热拔插的事件:热插拔的出了需要更新事件之外,还需要更新当前的状态,指示目前是拔出的事件还是插入的事件,这里我们使用V4L2的控制变量来设置,每次发生热拔插的中断的时候去更新这个变量值,如果这个值发生变化的话,框架会自动上报事件,并且也可以读取到对应的状态值。
static void rk628_csi_delayed_work_enable_hotplug(struct work_struct *work)
{
....
plugin = tx_5v_power_present(sd);
v4l2_ctrl_s_ctrl(csi->detect_tx_5v_ctrl, plugin);
....
}
更新信号丢失的事件:出了上述两个事件,驱动框架还自定义了一个signal lost的事件,这个事件用于应用来判断信号是否丢失,一般我们在信号变化的中断或者拔出的时候上报这个事件,用于apk断第一时间刷新黑屏:
static int rk628_hdmirx_general_isr(struct v4l2_subdev *sd, u32 status, bool *handled)
{
......
const struct v4l2_event evt_signal_lost = {
.type = RK_HDMIRX_V4L2_EVENT_SIGNAL_LOST,
};
......
csi->nosignal = true;
v4l2_event_queue(sd->devnode, &evt_signal_lost);
......
}
2.HDMI服务监听线程
应用需要启动一个HDMI服务来监听驱动的事件上报,camera框架对应的代码路径为:
vendor/rockchip/hardware/interfaces/hdmi
关键的接口实现:
findMipiHdmi:查找MIPI通路对应的设备节点,这里使用了私有的ioctl来区分与其他的v4l-subdev节点的区别:RKMODULE_GET_HDMI_MODE,获取到对应的值之后,就认为找到相应的节点设备。
getMipiStatus:获取设备节点的状态,主要用与apk端获取设备的分辨率等信息
重点关注监听事件的线程处理:获取驱动不同的事件,并通过回调的形式通知apk
bool V4L2DeviceEvent::V4L2EventThread::threadLoop() {
ALOGV("@%s", __FUNCTION__);
struct pollfd fds[2];
//int retry = 3;
//fds.events = POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM | POLLRDBAND | POLLPRI;
fds[0].fd = pipefd[0];
fds[0].events = POLLIN;
fds[1].fd = mVideoFd;
fds[1].events = POLLPRI;
struct v4l2_event ev;
CLEAR(ev);
if (poll(fds, 2, 5000) < 0) {
ALOGD("%d: poll failed: %s\n", mVideoFd, strerror(errno));
return false;
}
if (fds[0].revents & POLLIN) {
ALOGD("%d: quit message received\n", mVideoFd);
return false;
}
if (fds[1].revents & POLLPRI) {
if (ioctl(fds[1].fd, VIDIOC_DQEVENT, &ev) == 0) {
switch (ev.type) {
case V4L2_EVENT_SOURCE_CHANGE:
{
ALOGD("%d: V4L2_EVENT_SOURCE_CHANGE event\n", mVideoFd);
struct v4l2_subdev_format aFormat;
int ret = ioctl(mVideoFd, VIDIOC_SUBDEV_G_FMT, &aFormat);
if (ret < 0) {
ALOGE("VIDIOC_SUBDEV_G_FMT failed: %s", strerror(errno));
return true;
}
ALOGD("VIDIOC_SUBDEV_G_FMT: pad: %d, which: %d, width: %d, "
"height: %d, format: 0x%x, field: %d, color space: %d",
aFormat.pad,
aFormat.which,
aFormat.format.width,
aFormat.format.height,
aFormat.format.code,
aFormat.format.field,
aFormat.format.colorspace);
mCurformat = new V4L2DeviceEvent::FormartSize(aFormat.format.width,aFormat.format.height,1);
}
break;
case V4L2_EVENT_CTRL:{
struct v4l2_event_ctrl* ctrl =(struct v4l2_event_ctrl*) &(ev.u);
ALOGD("%d: V4L2_EVENT_CTRL event %d\n", mVideoFd ,ctrl->value);
}
break;
default:
ALOGD("%d: unknown event\n", mVideoFd);
break;
}
if(mCallback_ != NULL)
mCallback_((void*)this,ev.type,&ev);
} else {
ALOGD("%d: VIDIOC_DQEVENT failed: %s\n",mVideoFd, strerror(errno));
}
}
return true;
}
对不同的事件,按照如下方式进行处理:这里有onDisconnect、onConnect、以及onFormatChange三个回调来与apk方面进行交互。
V4L2EventCallBack Hdmi::eventCallback(void* sender,int event_type,struct v4l2_event *event){
ALOGD("@%s,event_type:%d",__FUNCTION__,event_type);
std::unique_lock<std::mutex> lk(mLock);
if (event_type == V4L2_EVENT_CTRL)
{
struct v4l2_event_ctrl* ctrl =(struct v4l2_event_ctrl*) &(event->u);
if (mCb != nullptr)
{
if (!ctrl->value)
{
mCb->onDisconnect(getMipiID());
}else{
mCb->onConnect(getMipiID());
}
}
ALOGD("V4L2_EVENT_CTRL event %d\n", ctrl->value);
}else if (event_type == V4L2_EVENT_SOURCE_CHANGE)
{
if (sender!=nullptr)
{
V4L2DeviceEvent::V4L2EventThread* eventThread = (V4L2DeviceEvent::V4L2EventThread*)sender;
sp<V4L2DeviceEvent::FormartSize> format = eventThread->getFormat();
if (format!=nullptr)
{
ALOGD("getFormatWeight:%d,getFormatHeight:%d",format->getFormatWeight(),format->getFormatHeight());
if (mCb != nullptr)
{
mCb->onFormatChange(getMipiID(),format->getFormatWeight(),format->getFormatHeight());
mCb->onConnect(getMipiID());
}
}
}
} else if (event_type == RK_HDMIRX_V4L2_EVENT_AUDIOINFO) {
ALOGD("%s do RK_HDMIRX_V4L2_EVENT_AUDIOINFO", __FUNCTION__);
mKernelAudioStatus = !!get_HdmiAudioPresent();
AudioChange(mAppConnected, mKernelAudioStatus);
}
lk.unlock();
return 0;
}
3.APK与HDMI服务交互
看下apk是如何实现的,apk需要接收到HDMI服务的回调之后,进行重新开关预览的设置
onFormatChange:事件变化,或者最新的分辨率重新开流
onConnect:设备连接的事件,可以开流
onDisconnect:设备断连的事件,apk需要关闭
apk的实现参考:packages\apps\rkCamera2\src\com\android\rockchip\camera2\RockchipCamera2.java
主要实现如下:
V4L2EventCallBack Hdmi::eventCallback(void* sender,int event_type,struct v4l2_event *event){
ALOGD("@%s,event_type:%d",__FUNCTION__,event_type);
std::unique_lock<std::mutex> lk(mLock);
if (event_type == V4L2_EVENT_CTRL)
{
struct v4l2_event_ctrl* ctrl =(struct v4l2_event_ctrl*) &(event->u);
if (mCb != nullptr)
{
if (!ctrl->value)
{
mCb->onDisconnect(getMipiID());
}else{
mCb->onConnect(getMipiID());
}
}
ALOGD("V4L2_EVENT_CTRL event %d\n", ctrl->value);
}else if (event_type == V4L2_EVENT_SOURCE_CHANGE)
{
if (sender!=nullptr)
{
V4L2DeviceEvent::V4L2EventThread* eventThread = (V4L2DeviceEvent::V4L2EventThread*)sender;
sp<V4L2DeviceEvent::FormartSize> format = eventThread->getFormat();
if (format!=nullptr)
{
ALOGD("getFormatWeight:%d,getFormatHeight:%d",format->getFormatWeight(),format->getFormatHeight());
if (mCb != nullptr)
{
mCb->onFormatChange(getMipiID(),format->getFormatWeight(),format->getFormatHeight());
mCb->onConnect(getMipiID());
}
}
}
} else if (event_type == RK_HDMIRX_V4L2_EVENT_AUDIOINFO) {
ALOGD("%s do RK_HDMIRX_V4L2_EVENT_AUDIOINFO", __FUNCTION__);
mKernelAudioStatus = !!get_HdmiAudioPresent();
AudioChange(mAppConnected, mKernelAudioStatus);
}
lk.unlock();
return 0;
}
4.cameraHAL 延伸动态注册camera
通过上述热插拔的监听,HDMI服务完全可以监听到HDMI-IN设备的拔插、切分辨的事件,在某些场景就可以延伸事件动态注册camera,在插入信号源的时候,注册cameraID ,拔出的时候,移除cameraID,这样在CTS测试的时候,就不会因为HDMI信号源没插入而导致测试到其camera设备,从而导致很多测试项目无法通过。
目前RK的代码已经包含这个功能,hardware/rockchip/camera_aidl,如下提交修改:
Author: Jian Qiu <qiujian@rock-chips.com>
Date: Tue Sep 10 15:36:58 2024 +0800
Camera: mipi hdmi hotplug support
Type: Function
Redmine ID: N/A
Associated modifications: N/A
Test: N/A
Change-Id: I337eb0d70bd102b46c88b253245c5c7c06a247a4
Signed-off-by: Jian Qiu <qiujian@rock-chips.com>