转自:https://shiminblog.github.io/dual-dsi-msm8937/
在 MSM8937 上有支持了 dual dsi 功能,本人在 msm8937-android6.0 上将其调通,以下简记其实现的思路以及关键代码片段。
高通支持的双屏有两种方式:其一为将一副图片左右均分,然后通过两个 DSI 硬件接口刷到屏幕上去,似乎在 MSM8952 上就是这种方式;其二为将一副图片不做分割操作,直接通过两个 DSI 硬件接口刷到屏幕上去,在 MSM8937 上则是支持的这种方式。
对于双屏显示功能,市面上最吸引人的其实是双屏异显。如果将一副图片左右均分,给人感觉上是双屏异显了,但它其实还是双屏同显。而 MSM8937 已经支持了一幅图片完整的刷到了两个屏幕上,证明从硬件资源上来看,其双屏异显是可以实现的—APP通过多媒体路由技术即可实现该功能。
高通的思路是,MSM8937 上两个 DSI 接口,其中主 DSI 接口接 MIPI 屏,副 DSI 接口挂一个 DSI-2-HDMI 的转换芯片,从上到下整个软件结构也是这样实现的。如果两个 DSI 接口都要挂 MIPI 接口的屏,则在驱动层需要做一些工作。
在原生的 MSM8937 dual dsi 思路中,主屏是必须要有的,起码在软件配置上要存在的;然后 HDMI 驱动注册的时候,会有一个 uevent 事件上报到用户空间,当 HAL 层的监听事件监听到该事件以后,就会做一些操作,包括设置 HDMI 的输出分辨率等等。
HAL 层和 Kernel 层 uevent 关联的字符串为: “change@/devices/virtual/switch/hdmi”。同时,HAL 层和 Kernel 层 show/store 关联的字符串为: “sys/devices/virtual/graphics/fb1/res_info”。当然, uevent 和 show()/store() 都属于 sysfs 系统的一部分。
在 Android 6.0 和 Android 7.0 上,SurfaceFlinger 通过对 loadFbHalModule()
和 loadHwcModule()
的调用,会在 HAL 层创建出一个监听 HDMI 热插拔事件的线程出来。以下为代码逻辑。
int HWCSession::Open(const hw_module_t *module, const char *name, hw_device_t **device)
被注册为 HAL 层 struct hw_module_methods_t
的 open()
函数:
static sdm::HWCSession::HWCModuleMethods g_hwc_module_methods;
hwc_module_t HAL_MODULE_INFO_SYM = {
.common = {
.tag = HARDWARE_MODULE_TAG,
.version_major = 2,
.version_minor = 0,
.id = HWC_HARDWARE_MODULE_ID,
.name = "QTI Hardware Composer Module",
.author = "CodeAurora Forum",
.methods = &g_hwc_module_methods,
.dso = 0,
.reserved = {0},
}
};
在 open 的时候,调用 Init:
int HWCSession::Open(const hw_module_t *module, const char *name, hw_device_t **device)
{
int status = hwc_session->Init();
}
在 Init 中会创建一个 uevent 监听线程:
int HWCSession::Init() {
if (pthread_create(&uevent_thread_, NULL, &HWCUeventThread, this) < 0) { }
}
void* HWCSession::HWCUeventThread(void *context) {
return reinterpret_cast(context)->HWCUeventThreadHandler();
}
void* HWCSession::HWCUeventThreadHandler() {
static char uevent_data[PAGE_SIZE];
int length = 0;
prctl(PR_SET_NAME, uevent_thread_name_, 0, 0, 0);
setpriority(PRIO_PROCESS, 0, HAL_PRIORITY_URGENT_DISPLAY);
if (!uevent_init()) {
DLOGE("Failed to init uevent");
pthread_exit(0);
return NULL;
}
while (!uevent_thread_exit_) {
// keep last 2 zeroes to ensure double 0 termination
length = uevent_next_event(uevent_data, INT32(sizeof(uevent_data)) - 2);
if (strcasestr(HWC_UEVENT_SWITCH_HDMI, uevent_data)) {
DLOGI("Uevent HDMI = %s", uevent_data);
int connected = GetEventValue(uevent_data, length, "SWITCH_STATE=");
if (connected >= 0) {
DLOGI("HDMI = %s", connected ? "connected" : "disconnected");
if (HotPlugHandler(connected) == -1) {
DLOGE("Failed handling Hotplug = %s", connected ? "connected" : "disconnected");
}
}
} else if (strcasestr(HWC_UEVENT_GRAPHICS_FB0, uevent_data)) {
DLOGI("Uevent FB0 = %s", uevent_data);
int panel_reset = GetEventValue(uevent_data, length, "PANEL_ALIVE=");
if (panel_reset == 0) {
if (hwc_procs_) {
reset_panel_ = true;
hwc_procs_->invalidate(hwc_procs_);
} else {
DLOGW("Ignore resetpanel - hwc_proc not registered");
}
}
}
}
pthread_exit(0);
return NULL;
}
在 HWCSession::HWCUeventThreadHandler()
中,首先调用 uevent_init()
创建 uevent 监听套接字,然后再调用 uevent_next_event()
循环读取 uevent 数据。一旦 kernel 发送了 “change@/devices/virtual/switch/hdmi” 或者 “change@/devices/virtual/graphics/fb0” 相关的字符串,这里就可以进入 HotPlugHandler()
进行相关的工作了。
当这里检测到了 HDMI uevent 插入事件以后,系统就会跑到 HDMI 设置相关的部分去了。其中最需要注意的是,HDMI 驱动在注册的时候,会读取到 HDMI 的 EDID 数据,该数据中包含了当前 HDMI 芯片支持的分辨率大小和一些其他数据。在 HAL 层,程序会通过 sysfs 去文件节点“sys/devices/virtual/graphics/fb1/res_info” 读取该数据,并通过该数据,来设置 MSM8937 输出的分辨率。需要关注的两个函数是:
DisplayError HWHDMI::GetDisplayAttributes(uint32_t index, HWDisplayAttributes *display_attributes){};
DisplayError HWHDMI::SetDisplayAttributes(uint32_t index){};
需要注意的是,MSM8937 会将读取到的分辨率长和宽进行交换,然后进行设置。如果是 MIPI 屏的话,这里的长宽就不能交换。或者说,想办法让其把长宽交换回来。
以上内容,在 msm8937-Android 6.0 和 msm8937-Android 7.0 上可以参考 hardware/ 目录下的 hw_hdmi.cpp 和 hwc_session.cpp 两个文件。
在 Kernel 中,MSM8937 有一个 DBA 的层,其全拼应该是 Display Bridge Abstract,显示桥抽象层。这样做的目的是因为,在 kernel 中,一部分 HDMI 驱动为 DSI-2-HDMI 转换芯片驱动,而有的芯片上,是直接集成了 HDMI 芯片,不需要转换这个过程了。
在 Kernel 中,在 DSI 控制器 probe 完毕的时候,会创建 dsi-dba 的 workqueue,在该 work 中,会去做一些 DBA 相关的初始化,最重要的就是,给当前的 DBA 设置一个默认的分辨率,如果后续桥接芯片(这里指的就是 HDMI 芯片吧)中返回的 EDID 数据中没有分辨率数据或者分辨率数据不符合要求,则 HAL 将根据这里设置的默认分辨率进行平台输出设置。
以下为上述过程的代码片段:
static int mdss_dsi_ctrl_probe(struct platform_device *pdev)
{
ctrl_pdata->workq = create_workqueue("mdss_dsi_dba");
INIT_DELAYED_WORK(&ctrl_pdata->dba_work, mdss_dsi_dba_work);
}
上面注册了延时工作。
static void mdss_dsi_dba_work(struct work_struct *work)
{
pinfo->dba_data = mdss_dba_utils_init(&utils_init_data);
}
上面的 work 函数中会对 DBA 进行一系列的初始化。
void *mdss_dba_utils_init(struct mdss_dba_utils_init_data *uid)
{
/* initialize DBA registration data */
info.instance_id = uid->instance_id;
info.cb = mdss_dba_utils_dba_cb;
info.cb_data = udata;
/* register client with DBA and get device's ops*/
udata->dba_data = msm_dba_register_client(&info, &udata->ops);
/* create sysfs nodes for other modules to intract with utils */
ret = mdss_dba_utils_sysfs_create(uid->kobj);
/* register with edid module for parsing edid buffer */
udata->edid_data = hdmi_edid_init(&edid_init_data);
/* Initialize to default resolution */
hdmi_edid_set_video_resolution(uid->pinfo->edid_data,
DEFAULT_VIDEO_RESOLUTION, true);
}
上面的 msm_dba_register_client()
会根据配置的 chip_name
和 instance_id
进行注册。如果其发现在配置文件中的这两个字段的值,同 HDMI 驱动注册的值不相同,这里就不能继续下去了,整个 DBA 的初始化也就终止了。
在 hdmi_edid_set_video_resolution()
函数中则是设置一个默认的分辨率。为了读写当前 HDMI 的分辨率,kernel 中提供了以下两个函数:
static ssize_t hdmi_edid_sysfs_rda_res_info(struct device *dev,struct device_attribute *attr, char *buf);
static ssize_t hdmi_edid_sysfs_wta_res_info(struct device *dev,struct device_attribute *attr, const char *buf, size_t count);
static DEVICE_ATTR(res_info, S_IRUGO | S_IWUSR, hdmi_edid_sysfs_rda_res_info,hdmi_edid_sysfs_wta_res_info); |
而在 DBA 设备连接到 MSM8937 上的时候,DBA 芯片驱动会发出一个 MSM_DBA_CB_HPD_CONNECT
的事件,告知 DBA 层有桥接芯片连接成功,接着,DBA 抽象层就发送 uevent 事件到 HAL 层进行处理。
static void mdss_dba_utils_dba_cb(void *data, enum msm_dba_callback_event event)
{
switch (event) {
case MSM_DBA_CB_HPD_CONNECT:
if (pluggable) {
mdss_dba_utils_notify_display(udata, 1);
}
}
}
mdss_dba_utils_dba_cb()
是在 mdss_dba_utils_init()
里面注册的回调函数。它会根据桥接芯片上报的各种事件,进行处理。
static void mdss_dba_utils_notify_display(struct mdss_dba_utils_data *udata, int val)
{
switch_set_state(&udata->sdev_display, val);
}
void switch_set_state(struct switch_dev *sdev, int state)
{
kobject_uevent_env(&sdev->dev->kobj, KOBJ_CHANGE, envp);
}
这里最终就到了 uevent 处理的地方了。
在 MSM8937 的 MIPI + HDMI 的框架上实现 MIPI + MIPI,其实已经非常简单了。
首先如上所分析,MSM8937 本来就是双 DSI,这在硬件上已经满足了双 MIPI;
再次,只要给一个正确的 uevent 事件,HAL 层的监听线程就能工作了。
则我们可以按照 HDMI 的要求,在 Kernel 进行配置各项参数—可以根据代码流程,需要什么参数,就配置什么参数。 然后在开机启动后的一定时间,通过 Kernel 向 HAL 报一个假的 uevent 事件,让 HAL 层误以为有桥接芯片连接上了。
需要配置的参数如下:
qcom,dba-panel;
qcom,bridge-name;
qcom,pluggable;
qcom,bridge-index
需要注意的是,DSI-2-HDMI芯片,可能 reset 的方式同 MIPI 屏不同,则处理的地方也就不同。在 MSM8937 默认的驱动框架中,是没有给 DSI1接口做 GPIO reset操作的,需要给 DSI1 的接口加上 MIPI 屏 reset 的代码,否则屏是不会亮的。
关于这个功能,google-android 有给出文档说明和 demo,其 APP 有两种方法进行选屏操作:一是 media router;二是 display manager。详细内容为如下连接:
Android 双屏异显选屏操作
(注意,可能需要)