之前的文章讲了rk的hdmi-in功能在安卓11/10/9上面的实现,切换分辨率与热拔插都是通过APK主动轮询的方式实现的。这篇文章我们主要介绍一下在安卓12之后的平台上面是如何实现hdmi-in功能的。并且在安卓12的平台上,rk还增加了TV框架的支持。
目前RK的主控芯片中只有RK3588带有独立的HDMI-RX模块,因此本文还是介绍HDMI转MIPI-CSI的实现方式。安卓12/13/14等平台主要对应主控RK356X、RK3588、RK3576等主控。
目录
(1)HDMI-IN 新框架对比旧框架差异
安卓12之后的版本HDMI-IN的差异主要在热拔插、切换分辨率等的实现上面,安卓12的版本,采用了事件上报的方式,应用订阅事件获取拔插的状态与分辨率的信息。而旧的版本则是采用apk轮询的方式,相较于轮训的方式,事件的方式没有selinux的权限问题,更适合安卓版本。
另外安卓12之后的版本新增了TV框架的支持,相较于camera,可以实现更低延时的显示。
差异 | 安卓12/13/14 | 安卓9/10/11 |
驱动 | 基于V4l2,新增V4l2事件上报 | 基于V4l2实现 |
应用框架 | 支持camera、TV低延时送显 | camera框架 |
热拔插 | 驱动上报事件,应用订阅获取事件 | APK轮询获取状态 |
切换分辨率 | 驱动上报事件,应用订阅获取事件 | APK轮询获取状态 |
(2)热拔插、切换分辨率驱动部分实现
1.热拔插
热拔插功能,驱动使用了中断检测HDMI 5V的状态来判断拔插状态,并注册5V的v4l2 ctrl变量
csi->detect_tx_5v_ctrl = v4l2_ctrl_new_std(&csi->hdl,
NULL, V4L2_CID_DV_RX_POWER_PRESENT,
0, 1, 0, 0);
同时需要注册对应的v4l2事件,对应的事件为V4L2_EVENT_CTRL
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_CTRL:
return v4l2_ctrl_subdev_subscribe_event(sd, fh, sub);
.......
default:
return -EINVAL;
}
}
在驱动检测到拔插的gpio中断时,中断函数中设置对应的detect_tx_5v_ctrl变量。我们只需要每次发生拔插的中断的时候对应的设置该值即可,如果没有预留拔插的检测GPIO,也可以采用驱动轮训的方式,但是转接芯片必须有对应的寄存器可以指示拔插的状态,该变量设置之后,若前后两次有发生变化,则会自动上报一个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);
......
}
2.分辨率变化
切换分辨率,在驱动中我们定义了一个source change的事件进行控制,在发生切换分辨率的动作之后,驱动会上报一个change的事件,通知应用层分辨率发生变化,应用端在通过其他接口获取分辨率,从而重新对hdmi信源下发新的预览。
定义分辨率变化source change事件:
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);
......
}
}
在分辨率发生变化的位置,上报事件:
static int rk628_csi_format_change(struct v4l2_subdev *sd)
{
......
const struct v4l2_event rk628_csi_ev_fmt = {
.type = V4L2_EVENT_SOURCE_CHANGE,
.u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION,
};
.......
if (sd->devnode)
v4l2_subdev_notify_event(sd, &rk628_csi_ev_fmt);
......
}
3.驱动增加ioctl
驱动需要增加RKMODULE_GET_HDMI_MODE支持,使应用可以识别到相对应的转接芯片的v4l2设备。
static long rk628_csi_ioctl(struct v4l2_subdev *sd, unsigned int cmd, void *arg)
{
.......
switch (cmd) {
case RKMODULE_GET_HDMI_MODE:
*(int *)arg = RKMODULE_HDMIIN_MODE;
break;
(3)camera框架
camera框架需要配置camera3_profiles.xml文件进行注册cameraID,在上篇文章已经介绍过,此处不再赘述。需要注意的是为了实现热拔插与切换分辨率的功能,我们新增了一个HDMI服务来上报对应的事件。
源代码:
hardware/rockchip/camera:cameraHAL层代码,camera框架取流、拍照等功能实现。
vendor/rockchip/hardware/interfaces/hdmi:走camera框架使用,负责监听分辨率变化与热拔插事件,与驱动以及APK交互。
1.打开宏配置
走camera框架需要打开:
vim device/rockchip/rk3588/BoardConfig.mk CAMERA_SUPPORT_HDMI := true
只配置CAMERA_SUPPORT_HDMI,而不配置BOARD_HDMI_IN_SUPPORT的情况下。如想要用rkCamera2进行camera预览,需要将rkCamera2加入到编译并配置属性persist.sys.hdmiinmode值为2。
2.HDMI 服务实现
源代码路径:vendor/rockchip/hardware/interfaces/hdmi
以下对其流程做简要的介绍:
①查找转接芯片的v4l2设备,使用上述的RKMODULE_GET_HDMI_MODE来实现,主要是为了区分MIPI摄像头与HDMI转接芯片。
int findMipiHdmi()
{
DIR* devdir = opendir(kDevicePath);
if(devdir == 0) {
ALOGE("%s: cannot open %s! ", __FUNCTION__, kDevicePath);
return -1;
}
struct dirent* de;
int videofd,ret;
while ((de = readdir(devdir)) != 0) {
// Find external v4l devices that's existing before we start watching and add them
if (!strncmp(kPrefix, de->d_name, kPrefixLen)) {
std::string deviceId(de->d_name + kPrefixLen);
ALOGD("found %s", de->d_name);
char v4l2DeviceDriver[16];
snprintf(kV4l2DevicePath, kMaxDevicePathLen,"%s%s", kDevicePath, de->d_name);
videofd = open(kV4l2DevicePath, O_RDWR);
if (videofd < 0){
ALOGE("[%s %d] open device failed:%x [%s]", __FUNCTION__, __LINE__, videofd,strerror(errno));
continue;
} else {
uint32_t ishdmi;
ret = ::ioctl(videofd, RKMODULE_GET_HDMI_MODE, (void*)&ishdmi);
if (ret < 0) {
ALOGE("RKMODULE_GET_HDMI_MODE Failed, error: %s", strerror(errno));
close(videofd);
continue;
}
ALOGD("%s RKMODULE_GET_HDMI_MODE:%d",kV4l2DevicePath,ishdmi);
if (ishdmi)
{
mMipiHdmi = videofd;
ALOGD("MipiHdmi fd:%d",mMipiHdmi);
if (mMipiHdmi < 0)
{
return ret;
}
mV4l2Event->initialize(mMipiHdmi);
}
}
}
}
closedir(devdir);
return ret;
}
②订阅v4l2事件
需要订阅拔插与切换分辨率的事件。
int V4L2DeviceEvent::subscribeEvent(int event)
{
ALOGI("@%s", __FUNCTION__);
int ret(0);
struct v4l2_event_subscription sub;
if (mFd == -1) {
ALOGW("Device %d already closed. cannot subscribe.",mFd);
return -1;
}
CLEAR(sub);
sub.type = event;
if(event == V4L2_EVENT_CTRL)
sub.id = V4L2_CID_DV_RX_POWER_PRESENT;
ret = ioctl(mFd, VIDIOC_SUBSCRIBE_EVENT, &sub);
if (ret < 0) {
ALOGE("error subscribing event %x: %s", event, strerror(errno));
return ret;
}
return ret;
}
③获取事件
以下为获取事件的流程,需要循环查询事件,若有事件,则将事件DQEVENT取出。
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;
}
④获取状态信息
需要提供获取分辨率等信息给apk接口查询。这里我们直接采用v4l2的标准接口实现。
Return<void> Hdmi::getMipiStatus(Hdmi::getMipiStatus_cb _hidl_cb){
ALOGD("@%s",__FUNCTION__);
V1_0::HdmiStatus status;
struct v4l2_subdev_format aFormat;
int err = ioctl(mMipiHdmi, VIDIOC_SUBDEV_G_FMT, &aFormat);
if (err < 0) {
ALOGE("VIDIOC_SUBDEV_G_FMT failed: %s", strerror(errno));
_hidl_cb(status);
return Void();
}
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);
status.width = aFormat.format.width;
status.height = aFormat.format.height;
struct v4l2_dv_timings timings;
err = ioctl(mMipiHdmi, VIDIOC_SUBDEV_QUERY_DV_TIMINGS, &timings);
if (err < 0) {
ALOGD("get VIDIOC_SUBDEV_QUERY_DV_TIMINGS failed ,%d(%s)", errno, strerror(errno));
_hidl_cb(status);
return Void();
}
const struct v4l2_bt_timings *bt =&timings.bt;
double tot_width, tot_height;
tot_height = bt->height +
bt->vfrontporch + bt->vsync + bt->vbackporch +
bt->il_vfrontporch + bt->il_vsync + bt->il_vbackporch;
tot_width = bt->width +
bt->hfrontporch + bt->hsync + bt->hbackporch;
ALOGD("%s:%dx%d, pixelclock:%lld Hz, %.2f fps", __func__,
timings.bt.width, timings.bt.height,
timings.bt.pixelclock,static_cast<double>(bt->pixelclock) /(tot_width * tot_height));
status.fps = round(static_cast<double>(bt->pixelclock) /(tot_width * tot_height));
struct v4l2_control control;
memset(&control, 0, sizeof(struct v4l2_control));
control.id = V4L2_CID_DV_RX_POWER_PRESENT;
err = ioctl(mMipiHdmi, VIDIOC_G_CTRL, &control);
if (err < 0) {
ALOGE("V4L2_CID_DV_RX_POWER_PRESENT failed ,%d(%s)", errno, strerror(errno));
}
ALOGD("VIDIOC_G_CTRL:%d",control.value);
status.status = control.value;
_hidl_cb(status);
return Void();
}
3.切换预览
camera预览方式,需要打开RockchipCamera2界面,注意需要使能CAMERA_SUPPORT_HDMI
setprop persist.sys.hdmiinmode 2
(4)TV框架
1.调试配置
SDK默认代码HDMI IN功能是关闭的,使能HDMI IN功能,需配置如下属性,开启后会编译含上述 APK在内的相关模块:
vim device/rockchip/rk3588/BoardConfig.mk
BOARD_HDMI_IN_SUPPORT := true
APK支持RK3588 HDMI RX通路数据预览和HDMI转MIPI-CSI通路数据预览,使用时需要切换。
TIF预览方式,需要设置MIPI-CSI2通路:
setprop tvinput.hdmiin.type 1
2.源码路径
TV的源码实现,关于热拔插、切换分辨率事件变化的部分可以参考上述HDMI服务。
路径如下:
packages/apps/TV/partner_support/samples :提供TV源数据服务,通过framework与HAL 层、预览APK进行交互,由于是开机运行的隐藏服务,该APK在桌面上是隐藏图标的。
hardware/rockchip/tv_input :TVHAL层代码,开关流、热拔插和分辨率切换事件等与驱动进行命 令交互。
(5)调试方法
1.查询热拔插事件
使用如下命令可以查询热拔插事件是否正常,以此可以判断驱动检测拔插的事件以及上报事件的机制是否正常工作。其中subdev对应的是转接芯片的节点,可以使用media-ctl -p查看。
v4l2-ctl -d /dev/v4l-subdev2 --poll-for-event=ctrl=power_present
2.查询分辨率变化事件
如下命令可以查询分辨率变化事件:
v4l2-ctl -d /dev/v4l-subdev2 --poll-for-event=source_change=0
(6)总结
本文简单介绍了安卓12之后的版本,HDMI-IN实现的一些变化,以及如何调试HDMI功能,后续我们再讨论关于低延时等方面的内容。