Dual DSI on msm8937

转自: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 系统的一部分。

HAL

在 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

在 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 处理的地方了。

Dual dsi 之 Dual mipi 屏

在 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 的代码,否则屏是不会亮的。

Dual dsi 之双屏异显

关于这个功能,google-android 有给出文档说明和 demo,其 APP 有两种方法进行选屏操作:一是 media router;二是 display manager。详细内容为如下连接:

Android 双屏异显选屏操作

(注意,可能需要)


你可能感兴趣的:(qcom)