Commit 06c0dd96 authored by Rob Clark's avatar Rob Clark

drm/msm: add mdp5/apq8x74

Add support for the new MDP5 display controller block.  The mapping
between parts of the display controller and KMS is:

  plane   -> PIPE{RGBn,VIGn}             \
  crtc    -> LM (layer mixer)            |-> MDP "device"
  encoder -> INTF                        /
  connector -> HDMI/DSI/eDP/etc          --> other device(s)

Unlike MDP4, it appears we can get by with a single encoder, rather
than needing a different implementation for DTV, DSI, etc.  (Ie. the
register interface is same, just different bases.)

Also unlike MDP4, all the IRQs for other blocks (HDMI, DSI, etc) are
routed through MDP.

And finally, MDP5 has this "Shared Memory Pool" (called "SMP"), from
which blocks need to be allocated to the active pipes based on fetch
stride.
Signed-off-by: default avatarRob Clark <robdclark@gmail.com>
parent dada25bd
...@@ -20,6 +20,12 @@ msm-y := \ ...@@ -20,6 +20,12 @@ msm-y := \
mdp/mdp4/mdp4_irq.o \ mdp/mdp4/mdp4_irq.o \
mdp/mdp4/mdp4_kms.o \ mdp/mdp4/mdp4_kms.o \
mdp/mdp4/mdp4_plane.o \ mdp/mdp4/mdp4_plane.o \
mdp/mdp5/mdp5_crtc.o \
mdp/mdp5/mdp5_encoder.o \
mdp/mdp5/mdp5_irq.o \
mdp/mdp5/mdp5_kms.o \
mdp/mdp5/mdp5_plane.o \
mdp/mdp5/mdp5_smp.o \
msm_drv.o \ msm_drv.o \
msm_fb.o \ msm_fb.o \
msm_gem.o \ msm_gem.o \
......
...@@ -4,7 +4,7 @@ In the current snapdragon SoC's, we have (at least) 3 different ...@@ -4,7 +4,7 @@ In the current snapdragon SoC's, we have (at least) 3 different
display controller blocks at play: display controller blocks at play:
+ MDP3 - ?? seems to be what is on geeksphone peak device + MDP3 - ?? seems to be what is on geeksphone peak device
+ MDP4 - S3 (APQ8060, touchpad), S4-pro (APQ8064, nexus4 & ifc6410) + MDP4 - S3 (APQ8060, touchpad), S4-pro (APQ8064, nexus4 & ifc6410)
+ MDSS - snapdragon 800 + MDP5 - snapdragon 800
(I don't have a completely clear picture on which display controller (I don't have a completely clear picture on which display controller
maps to which part #) maps to which part #)
...@@ -46,6 +46,24 @@ and treat the MDP4 block's irq as "the" irq. Even though the connectors ...@@ -46,6 +46,24 @@ and treat the MDP4 block's irq as "the" irq. Even though the connectors
may have their own irqs which they install themselves. For this reason may have their own irqs which they install themselves. For this reason
the display controller is the "master" device. the display controller is the "master" device.
For MDP5, the mapping is:
plane -> PIPE{RGBn,VIGn} \
crtc -> LM (layer mixer) |-> MDP "device"
encoder -> INTF /
connector -> HDMI/DSI/eDP/etc --> other device(s)
Unlike MDP4, it appears we can get by with a single encoder, rather
than needing a different implementation for DTV, DSI, etc. (Ie. the
register interface is same, just different bases.)
Also unlike MDP4, with MDP5 all the IRQs for other blocks (HDMI, DSI,
etc) are routed through MDP.
And finally, MDP5 has this "Shared Memory Pool" (called "SMP"), from
which blocks need to be allocated to the active pipes based on fetch
stride.
Each connector probably ends up being a separate device, just for the Each connector probably ends up being a separate device, just for the
logistics of finding/mapping io region, irq, etc. Idealy we would logistics of finding/mapping io region, irq, etc. Idealy we would
have a better way than just stashing the platform device in a global have a better way than just stashing the platform device in a global
......
This diff is collapsed.
/*
* Copyright (C) 2013 Red Hat
* Author: Rob Clark <robdclark@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "mdp5_kms.h"
#include "drm_crtc.h"
#include "drm_crtc_helper.h"
struct mdp5_encoder {
struct drm_encoder base;
int intf;
enum mdp5_intf intf_id;
bool enabled;
uint32_t bsc;
};
#define to_mdp5_encoder(x) container_of(x, struct mdp5_encoder, base)
static struct mdp5_kms *get_kms(struct drm_encoder *encoder)
{
struct msm_drm_private *priv = encoder->dev->dev_private;
return to_mdp5_kms(to_mdp_kms(priv->kms));
}
#ifdef CONFIG_MSM_BUS_SCALING
#include <mach/board.h>
#include <mach/msm_bus.h>
#include <mach/msm_bus_board.h>
#define MDP_BUS_VECTOR_ENTRY(ab_val, ib_val) \
{ \
.src = MSM_BUS_MASTER_MDP_PORT0, \
.dst = MSM_BUS_SLAVE_EBI_CH0, \
.ab = (ab_val), \
.ib = (ib_val), \
}
static struct msm_bus_vectors mdp_bus_vectors[] = {
MDP_BUS_VECTOR_ENTRY(0, 0),
MDP_BUS_VECTOR_ENTRY(2000000000, 2000000000),
};
static struct msm_bus_paths mdp_bus_usecases[] = { {
.num_paths = 1,
.vectors = &mdp_bus_vectors[0],
}, {
.num_paths = 1,
.vectors = &mdp_bus_vectors[1],
} };
static struct msm_bus_scale_pdata mdp_bus_scale_table = {
.usecase = mdp_bus_usecases,
.num_usecases = ARRAY_SIZE(mdp_bus_usecases),
.name = "mdss_mdp",
};
static void bs_init(struct mdp5_encoder *mdp5_encoder)
{
mdp5_encoder->bsc = msm_bus_scale_register_client(
&mdp_bus_scale_table);
DBG("bus scale client: %08x", mdp5_encoder->bsc);
}
static void bs_fini(struct mdp5_encoder *mdp5_encoder)
{
if (mdp5_encoder->bsc) {
msm_bus_scale_unregister_client(mdp5_encoder->bsc);
mdp5_encoder->bsc = 0;
}
}
static void bs_set(struct mdp5_encoder *mdp5_encoder, int idx)
{
if (mdp5_encoder->bsc) {
DBG("set bus scaling: %d", idx);
/* HACK: scaling down, and then immediately back up
* seems to leave things broken (underflow).. so
* never disable:
*/
idx = 1;
msm_bus_scale_client_update_request(mdp5_encoder->bsc, idx);
}
}
#else
static void bs_init(struct mdp5_encoder *mdp5_encoder) {}
static void bs_fini(struct mdp5_encoder *mdp5_encoder) {}
static void bs_set(struct mdp5_encoder *mdp5_encoder, int idx) {}
#endif
static void mdp5_encoder_destroy(struct drm_encoder *encoder)
{
struct mdp5_encoder *mdp5_encoder = to_mdp5_encoder(encoder);
bs_fini(mdp5_encoder);
drm_encoder_cleanup(encoder);
kfree(mdp5_encoder);
}
static const struct drm_encoder_funcs mdp5_encoder_funcs = {
.destroy = mdp5_encoder_destroy,
};
static void mdp5_encoder_dpms(struct drm_encoder *encoder, int mode)
{
struct mdp5_encoder *mdp5_encoder = to_mdp5_encoder(encoder);
struct mdp5_kms *mdp5_kms = get_kms(encoder);
int intf = mdp5_encoder->intf;
bool enabled = (mode == DRM_MODE_DPMS_ON);
DBG("mode=%d", mode);
if (enabled == mdp5_encoder->enabled)
return;
if (enabled) {
bs_set(mdp5_encoder, 1);
mdp5_write(mdp5_kms, REG_MDP5_INTF_TIMING_ENGINE_EN(intf), 1);
} else {
mdp5_write(mdp5_kms, REG_MDP5_INTF_TIMING_ENGINE_EN(intf), 0);
bs_set(mdp5_encoder, 0);
}
mdp5_encoder->enabled = enabled;
}
static bool mdp5_encoder_mode_fixup(struct drm_encoder *encoder,
const struct drm_display_mode *mode,
struct drm_display_mode *adjusted_mode)
{
return true;
}
static void mdp5_encoder_mode_set(struct drm_encoder *encoder,
struct drm_display_mode *mode,
struct drm_display_mode *adjusted_mode)
{
struct mdp5_encoder *mdp5_encoder = to_mdp5_encoder(encoder);
struct mdp5_kms *mdp5_kms = get_kms(encoder);
int intf = mdp5_encoder->intf;
uint32_t dtv_hsync_skew, vsync_period, vsync_len, ctrl_pol;
uint32_t display_v_start, display_v_end;
uint32_t hsync_start_x, hsync_end_x;
uint32_t format;
mode = adjusted_mode;
DBG("set mode: %d:\"%s\" %d %d %d %d %d %d %d %d %d %d 0x%x 0x%x",
mode->base.id, mode->name,
mode->vrefresh, mode->clock,
mode->hdisplay, mode->hsync_start,
mode->hsync_end, mode->htotal,
mode->vdisplay, mode->vsync_start,
mode->vsync_end, mode->vtotal,
mode->type, mode->flags);
ctrl_pol = 0;
if (mode->flags & DRM_MODE_FLAG_NHSYNC)
ctrl_pol |= MDP5_INTF_POLARITY_CTL_HSYNC_LOW;
if (mode->flags & DRM_MODE_FLAG_NVSYNC)
ctrl_pol |= MDP5_INTF_POLARITY_CTL_VSYNC_LOW;
/* probably need to get DATA_EN polarity from panel.. */
dtv_hsync_skew = 0; /* get this from panel? */
format = 0x213f; /* get this from panel? */
hsync_start_x = (mode->htotal - mode->hsync_start);
hsync_end_x = mode->htotal - (mode->hsync_start - mode->hdisplay) - 1;
vsync_period = mode->vtotal * mode->htotal;
vsync_len = (mode->vsync_end - mode->vsync_start) * mode->htotal;
display_v_start = (mode->vtotal - mode->vsync_start) * mode->htotal + dtv_hsync_skew;
display_v_end = vsync_period - ((mode->vsync_start - mode->vdisplay) * mode->htotal) + dtv_hsync_skew - 1;
mdp5_write(mdp5_kms, REG_MDP5_INTF_HSYNC_CTL(intf),
MDP5_INTF_HSYNC_CTL_PULSEW(mode->hsync_end - mode->hsync_start) |
MDP5_INTF_HSYNC_CTL_PERIOD(mode->htotal));
mdp5_write(mdp5_kms, REG_MDP5_INTF_VSYNC_PERIOD_F0(intf), vsync_period);
mdp5_write(mdp5_kms, REG_MDP5_INTF_VSYNC_LEN_F0(intf), vsync_len);
mdp5_write(mdp5_kms, REG_MDP5_INTF_DISPLAY_HCTL(intf),
MDP5_INTF_DISPLAY_HCTL_START(hsync_start_x) |
MDP5_INTF_DISPLAY_HCTL_END(hsync_end_x));
mdp5_write(mdp5_kms, REG_MDP5_INTF_DISPLAY_VSTART_F0(intf), display_v_start);
mdp5_write(mdp5_kms, REG_MDP5_INTF_DISPLAY_VEND_F0(intf), display_v_end);
mdp5_write(mdp5_kms, REG_MDP5_INTF_BORDER_COLOR(intf), 0);
mdp5_write(mdp5_kms, REG_MDP5_INTF_UNDERFLOW_COLOR(intf), 0xff);
mdp5_write(mdp5_kms, REG_MDP5_INTF_HSYNC_SKEW(intf), dtv_hsync_skew);
mdp5_write(mdp5_kms, REG_MDP5_INTF_POLARITY_CTL(intf), ctrl_pol);
mdp5_write(mdp5_kms, REG_MDP5_INTF_ACTIVE_HCTL(intf),
MDP5_INTF_ACTIVE_HCTL_START(0) |
MDP5_INTF_ACTIVE_HCTL_END(0));
mdp5_write(mdp5_kms, REG_MDP5_INTF_ACTIVE_VSTART_F0(intf), 0);
mdp5_write(mdp5_kms, REG_MDP5_INTF_ACTIVE_VEND_F0(intf), 0);
mdp5_write(mdp5_kms, REG_MDP5_INTF_PANEL_FORMAT(intf), format);
mdp5_write(mdp5_kms, REG_MDP5_INTF_FRAME_LINE_COUNT_EN(intf), 0x3); /* frame+line? */
}
static void mdp5_encoder_prepare(struct drm_encoder *encoder)
{
mdp5_encoder_dpms(encoder, DRM_MODE_DPMS_OFF);
}
static void mdp5_encoder_commit(struct drm_encoder *encoder)
{
struct mdp5_encoder *mdp5_encoder = to_mdp5_encoder(encoder);
mdp5_crtc_set_intf(encoder->crtc, mdp5_encoder->intf,
mdp5_encoder->intf_id);
mdp5_encoder_dpms(encoder, DRM_MODE_DPMS_ON);
}
static const struct drm_encoder_helper_funcs mdp5_encoder_helper_funcs = {
.dpms = mdp5_encoder_dpms,
.mode_fixup = mdp5_encoder_mode_fixup,
.mode_set = mdp5_encoder_mode_set,
.prepare = mdp5_encoder_prepare,
.commit = mdp5_encoder_commit,
};
/* initialize encoder */
struct drm_encoder *mdp5_encoder_init(struct drm_device *dev, int intf,
enum mdp5_intf intf_id)
{
struct drm_encoder *encoder = NULL;
struct mdp5_encoder *mdp5_encoder;
int ret;
mdp5_encoder = kzalloc(sizeof(*mdp5_encoder), GFP_KERNEL);
if (!mdp5_encoder) {
ret = -ENOMEM;
goto fail;
}
mdp5_encoder->intf = intf;
mdp5_encoder->intf_id = intf_id;
encoder = &mdp5_encoder->base;
drm_encoder_init(dev, encoder, &mdp5_encoder_funcs,
DRM_MODE_ENCODER_TMDS);
drm_encoder_helper_add(encoder, &mdp5_encoder_helper_funcs);
bs_init(mdp5_encoder);
return encoder;
fail:
if (encoder)
mdp5_encoder_destroy(encoder);
return ERR_PTR(ret);
}
/*
* Copyright (C) 2013 Red Hat
* Author: Rob Clark <robdclark@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "msm_drv.h"
#include "mdp5_kms.h"
void mdp5_set_irqmask(struct mdp_kms *mdp_kms, uint32_t irqmask)
{
mdp5_write(to_mdp5_kms(mdp_kms), REG_MDP5_INTR_EN, irqmask);
}
static void mdp5_irq_error_handler(struct mdp_irq *irq, uint32_t irqstatus)
{
DRM_ERROR("errors: %08x\n", irqstatus);
}
void mdp5_irq_preinstall(struct msm_kms *kms)
{
struct mdp5_kms *mdp5_kms = to_mdp5_kms(to_mdp_kms(kms));
mdp5_write(mdp5_kms, REG_MDP5_INTR_CLEAR, 0xffffffff);
}
int mdp5_irq_postinstall(struct msm_kms *kms)
{
struct mdp_kms *mdp_kms = to_mdp_kms(kms);
struct mdp5_kms *mdp5_kms = to_mdp5_kms(mdp_kms);
struct mdp_irq *error_handler = &mdp5_kms->error_handler;
error_handler->irq = mdp5_irq_error_handler;
error_handler->irqmask = MDP5_IRQ_INTF0_UNDER_RUN |
MDP5_IRQ_INTF1_UNDER_RUN |
MDP5_IRQ_INTF2_UNDER_RUN |
MDP5_IRQ_INTF3_UNDER_RUN;
mdp_irq_register(mdp_kms, error_handler);
return 0;
}
void mdp5_irq_uninstall(struct msm_kms *kms)
{
struct mdp5_kms *mdp5_kms = to_mdp5_kms(to_mdp_kms(kms));
mdp5_write(mdp5_kms, REG_MDP5_INTR_EN, 0x00000000);
}
static void mdp5_irq_mdp(struct mdp_kms *mdp_kms)
{
struct mdp5_kms *mdp5_kms = to_mdp5_kms(mdp_kms);
struct drm_device *dev = mdp5_kms->dev;
struct msm_drm_private *priv = dev->dev_private;
unsigned int id;
uint32_t status;
status = mdp5_read(mdp5_kms, REG_MDP5_INTR_STATUS);
mdp5_write(mdp5_kms, REG_MDP5_INTR_CLEAR, status);
VERB("status=%08x", status);
for (id = 0; id < priv->num_crtcs; id++)
if (status & mdp5_crtc_vblank(priv->crtcs[id]))
drm_handle_vblank(dev, id);
mdp_dispatch_irqs(mdp_kms, status);
}
irqreturn_t mdp5_irq(struct msm_kms *kms)
{
struct mdp_kms *mdp_kms = to_mdp_kms(kms);
struct mdp5_kms *mdp5_kms = to_mdp5_kms(mdp_kms);
uint32_t intr;
intr = mdp5_read(mdp5_kms, REG_MDP5_HW_INTR_STATUS);
VERB("intr=%08x", intr);
if (intr & MDP5_HW_INTR_STATUS_INTR_MDP)
mdp5_irq_mdp(mdp_kms);
if (intr & MDP5_HW_INTR_STATUS_INTR_HDMI)
hdmi_irq(0, mdp5_kms->hdmi);
return IRQ_HANDLED;
}
int mdp5_enable_vblank(struct msm_kms *kms, struct drm_crtc *crtc)
{
mdp_update_vblank_mask(to_mdp_kms(kms),
mdp5_crtc_vblank(crtc), true);
return 0;
}
void mdp5_disable_vblank(struct msm_kms *kms, struct drm_crtc *crtc)
{
mdp_update_vblank_mask(to_mdp_kms(kms),
mdp5_crtc_vblank(crtc), false);
}
/*
* Copyright (C) 2013 Red Hat
* Author: Rob Clark <robdclark@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "msm_drv.h"
#include "msm_mmu.h"
#include "mdp5_kms.h"
static struct mdp5_platform_config *mdp5_get_config(struct platform_device *dev);
static int mdp5_hw_init(struct msm_kms *kms)
{
struct mdp5_kms *mdp5_kms = to_mdp5_kms(to_mdp_kms(kms));
struct drm_device *dev = mdp5_kms->dev;
uint32_t version, major, minor;
int ret = 0;
pm_runtime_get_sync(dev->dev);
mdp5_enable(mdp5_kms);
version = mdp5_read(mdp5_kms, REG_MDP5_MDP_VERSION);
mdp5_disable(mdp5_kms);
major = FIELD(version, MDP5_MDP_VERSION_MAJOR);
minor = FIELD(version, MDP5_MDP_VERSION_MINOR);
DBG("found MDP5 version v%d.%d", major, minor);
if ((major != 1) || ((minor != 0) && (minor != 2))) {
dev_err(dev->dev, "unexpected MDP version: v%d.%d\n",
major, minor);
ret = -ENXIO;
goto out;
}
mdp5_kms->rev = minor;
/* Magic unknown register writes:
*
* W VBIF:0x004 00000001 (mdss_mdp.c:839)
* W MDP5:0x2e0 0xe9 (mdss_mdp.c:839)
* W MDP5:0x2e4 0x55 (mdss_mdp.c:839)
* W MDP5:0x3ac 0xc0000ccc (mdss_mdp.c:839)
* W MDP5:0x3b4 0xc0000ccc (mdss_mdp.c:839)
* W MDP5:0x3bc 0xcccccc (mdss_mdp.c:839)
* W MDP5:0x4a8 0xcccc0c0 (mdss_mdp.c:839)
* W MDP5:0x4b0 0xccccc0c0 (mdss_mdp.c:839)
* W MDP5:0x4b8 0xccccc000 (mdss_mdp.c:839)
*
* Downstream fbdev driver gets these register offsets/values
* from DT.. not really sure what these registers are or if
* different values for different boards/SoC's, etc. I guess
* they are the golden registers.
*
* Not setting these does not seem to cause any problem. But
* we may be getting lucky with the bootloader initializing
* them for us. OTOH, if we can always count on the bootloader
* setting the golden registers, then perhaps we don't need to
* care.
*/
mdp5_write(mdp5_kms, REG_MDP5_DISP_INTF_SEL, 0);
mdp5_write(mdp5_kms, REG_MDP5_CTL_OP(0), 0);
mdp5_write(mdp5_kms, REG_MDP5_CTL_OP(1), 0);
mdp5_write(mdp5_kms, REG_MDP5_CTL_OP(2), 0);
mdp5_write(mdp5_kms, REG_MDP5_CTL_OP(3), 0);
out:
pm_runtime_put_sync(dev->dev);
return ret;
}
static long mdp5_round_pixclk(struct msm_kms *kms, unsigned long rate,
struct drm_encoder *encoder)
{
return rate;
}
static void mdp5_preclose(struct msm_kms *kms, struct drm_file *file)
{
struct mdp5_kms *mdp5_kms = to_mdp5_kms(to_mdp_kms(kms));
struct msm_drm_private *priv = mdp5_kms->dev->dev_private;
unsigned i;
for (i = 0; i < priv->num_crtcs; i++)
mdp5_crtc_cancel_pending_flip(priv->crtcs[i], file);
}
static void mdp5_destroy(struct msm_kms *kms)
{
struct mdp5_kms *mdp5_kms = to_mdp5_kms(to_mdp_kms(kms));
kfree(mdp5_kms);
}
static const struct mdp_kms_funcs kms_funcs = {
.base = {
.hw_init = mdp5_hw_init,
.irq_preinstall = mdp5_irq_preinstall,
.irq_postinstall = mdp5_irq_postinstall,
.irq_uninstall = mdp5_irq_uninstall,
.irq = mdp5_irq,
.enable_vblank = mdp5_enable_vblank,
.disable_vblank = mdp5_disable_vblank,
.get_format = mdp_get_format,
.round_pixclk = mdp5_round_pixclk,
.preclose = mdp5_preclose,
.destroy = mdp5_destroy,
},
.set_irqmask = mdp5_set_irqmask,
};
int mdp5_disable(struct mdp5_kms *mdp5_kms)
{
DBG("");
clk_disable_unprepare(mdp5_kms->ahb_clk);
clk_disable_unprepare(mdp5_kms->axi_clk);
clk_disable_unprepare(mdp5_kms->core_clk);
clk_disable_unprepare(mdp5_kms->lut_clk);
return 0;
}
int mdp5_enable(struct mdp5_kms *mdp5_kms)
{
DBG("");
clk_prepare_enable(mdp5_kms->ahb_clk);
clk_prepare_enable(mdp5_kms->axi_clk);
clk_prepare_enable(mdp5_kms->core_clk);
clk_prepare_enable(mdp5_kms->lut_clk);
return 0;
}
static int modeset_init(struct mdp5_kms *mdp5_kms)
{
static const enum mdp5_pipe crtcs[] = {
SSPP_RGB0, SSPP_RGB1, SSPP_RGB2,
};
struct drm_device *dev = mdp5_kms->dev;
struct msm_drm_private *priv = dev->dev_private;
struct drm_encoder *encoder;
int i, ret;
/* construct CRTCs: */
for (i = 0; i < ARRAY_SIZE(crtcs); i++) {
struct drm_plane *plane;
struct drm_crtc *crtc;
plane = mdp5_plane_init(dev, crtcs[i], true);
if (IS_ERR(plane)) {
ret = PTR_ERR(plane);
dev_err(dev->dev, "failed to construct plane for %s (%d)\n",
pipe2name(crtcs[i]), ret);
goto fail;
}
crtc = mdp5_crtc_init(dev, plane, i);
if (IS_ERR(crtc)) {
ret = PTR_ERR(crtc);
dev_err(dev->dev, "failed to construct crtc for %s (%d)\n",
pipe2name(crtcs[i]), ret);
goto fail;
}
priv->crtcs[priv->num_crtcs++] = crtc;
}
/* Construct encoder for HDMI: */
encoder = mdp5_encoder_init(dev, 3, INTF_HDMI);
if (IS_ERR(encoder)) {
dev_err(dev->dev, "failed to construct encoder\n");
ret = PTR_ERR(encoder);
goto fail;
}
/* NOTE: the vsync and error irq's are actually associated with
* the INTF/encoder.. the easiest way to deal with this (ie. what
* we do now) is assume a fixed relationship between crtc's and
* encoders. I'm not sure if there is ever a need to more freely
* assign crtcs to encoders, but if there is then we need to take
* care of error and vblank irq's that the crtc has registered,
* and also update user-requested vblank_mask.
*/
encoder->possible_crtcs = BIT(0);
mdp5_crtc_set_intf(priv->crtcs[0], 3, INTF_HDMI);
priv->encoders[priv->num_encoders++] = encoder;
/* Construct bridge/connector for HDMI: */
mdp5_kms->hdmi = hdmi_init(dev, encoder);
if (IS_ERR(mdp5_kms->hdmi)) {
ret = PTR_ERR(mdp5_kms->hdmi);
dev_err(dev->dev, "failed to initialize HDMI: %d\n", ret);
goto fail;
}
return 0;
fail:
return ret;
}
static const char *iommu_ports[] = {
"mdp_0",
};
static int get_clk(struct platform_device *pdev, struct clk **clkp,
const char *name)
{
struct device *dev = &pdev->dev;
struct clk *clk = devm_clk_get(dev, name);
if (IS_ERR(clk)) {
dev_err(dev, "failed to get %s (%ld)\n", name, PTR_ERR(clk));
return PTR_ERR(clk);
}
*clkp = clk;
return 0;
}
struct msm_kms *mdp5_kms_init(struct drm_device *dev)
{
struct platform_device *pdev = dev->platformdev;
struct mdp5_platform_config *config = mdp5_get_config(pdev);
struct mdp5_kms *mdp5_kms;
struct msm_kms *kms = NULL;
struct msm_mmu *mmu;
int ret;
mdp5_kms = kzalloc(sizeof(*mdp5_kms), GFP_KERNEL);
if (!mdp5_kms) {
dev_err(dev->dev, "failed to allocate kms\n");
ret = -ENOMEM;
goto fail;
}
mdp_kms_init(&mdp5_kms->base, &kms_funcs);
kms = &mdp5_kms->base.base;
mdp5_kms->dev = dev;
mdp5_kms->smp_blk_cnt = config->smp_blk_cnt;
mdp5_kms->mmio = msm_ioremap(pdev, "mdp_phys", "MDP5");
if (IS_ERR(mdp5_kms->mmio)) {
ret = PTR_ERR(mdp5_kms->mmio);
goto fail;
}
mdp5_kms->vbif = msm_ioremap(pdev, "vbif_phys", "VBIF");
if (IS_ERR(mdp5_kms->vbif)) {
ret = PTR_ERR(mdp5_kms->vbif);
goto fail;
}
mdp5_kms->vdd = devm_regulator_get(&pdev->dev, "vdd");
if (IS_ERR(mdp5_kms->vdd)) {
ret = PTR_ERR(mdp5_kms->vdd);
goto fail;
}
ret = regulator_enable(mdp5_kms->vdd);
if (ret) {
dev_err(dev->dev, "failed to enable regulator vdd: %d\n", ret);
goto fail;
}
ret = get_clk(pdev, &mdp5_kms->axi_clk, "bus_clk") ||
get_clk(pdev, &mdp5_kms->ahb_clk, "iface_clk") ||
get_clk(pdev, &mdp5_kms->src_clk, "core_clk_src") ||
get_clk(pdev, &mdp5_kms->core_clk, "core_clk") ||
get_clk(pdev, &mdp5_kms->lut_clk, "lut_clk") ||
get_clk(pdev, &mdp5_kms->vsync_clk, "vsync_clk");
if (ret)
goto fail;
ret = clk_set_rate(mdp5_kms->src_clk, config->max_clk);
/* make sure things are off before attaching iommu (bootloader could
* have left things on, in which case we'll start getting faults if
* we don't disable):
*/
mdp5_enable(mdp5_kms);
mdp5_write(mdp5_kms, REG_MDP5_INTF_TIMING_ENGINE_EN(0), 0);
mdp5_write(mdp5_kms, REG_MDP5_INTF_TIMING_ENGINE_EN(1), 0);
mdp5_write(mdp5_kms, REG_MDP5_INTF_TIMING_ENGINE_EN(2), 0);
mdp5_write(mdp5_kms, REG_MDP5_INTF_TIMING_ENGINE_EN(3), 0);
mdp5_disable(mdp5_kms);
mdelay(16);
if (config->iommu) {
mmu = msm_iommu_new(dev, config->iommu);
if (IS_ERR(mmu)) {
ret = PTR_ERR(mmu);
goto fail;
}
ret = mmu->funcs->attach(mmu, iommu_ports,
ARRAY_SIZE(iommu_ports));
if (ret)
goto fail;
} else {
dev_info(dev->dev, "no iommu, fallback to phys "
"contig buffers for scanout\n");
mmu = NULL;
}
mdp5_kms->id = msm_register_mmu(dev, mmu);
if (mdp5_kms->id < 0) {
ret = mdp5_kms->id;
dev_err(dev->dev, "failed to register mdp5 iommu: %d\n", ret);
goto fail;
}
ret = modeset_init(mdp5_kms);
if (ret) {
dev_err(dev->dev, "modeset_init failed: %d\n", ret);
goto fail;
}
return kms;
fail:
if (kms)
mdp5_destroy(kms);
return ERR_PTR(ret);
}
static struct mdp5_platform_config *mdp5_get_config(struct platform_device *dev)
{
static struct mdp5_platform_config config = {};
#ifdef CONFIG_OF
/* TODO */
#endif
return &config;
}
/*
* Copyright (C) 2013 Red Hat
* Author: Rob Clark <robdclark@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __MDP5_KMS_H__
#define __MDP5_KMS_H__
#include "msm_drv.h"
#include "msm_kms.h"
#include "mdp/mdp_kms.h"
#include "mdp5.xml.h"
#include "mdp5_smp.h"
struct mdp5_kms {
struct mdp_kms base;
struct drm_device *dev;
int rev;
/* mapper-id used to request GEM buffer mapped for scanout: */
int id;
/* for tracking smp allocation amongst pipes: */
mdp5_smp_state_t smp_state;
struct mdp5_client_smp_state smp_client_state[CID_MAX];
int smp_blk_cnt;
/* io/register spaces: */
void __iomem *mmio, *vbif;
struct regulator *vdd;
struct clk *axi_clk;
struct clk *ahb_clk;
struct clk *src_clk;
struct clk *core_clk;
struct clk *lut_clk;
struct clk *vsync_clk;
struct hdmi *hdmi;
struct mdp_irq error_handler;
};
#define to_mdp5_kms(x) container_of(x, struct mdp5_kms, base)
/* platform config data (ie. from DT, or pdata) */
struct mdp5_platform_config {
struct iommu_domain *iommu;
uint32_t max_clk;
int smp_blk_cnt;
};
static inline void mdp5_write(struct mdp5_kms *mdp5_kms, u32 reg, u32 data)
{
msm_writel(data, mdp5_kms->mmio + reg);
}
static inline u32 mdp5_read(struct mdp5_kms *mdp5_kms, u32 reg)
{
return msm_readl(mdp5_kms->mmio + reg);
}
static inline const char *pipe2name(enum mdp5_pipe pipe)
{
static const char *names[] = {
#define NAME(n) [SSPP_ ## n] = #n
NAME(VIG0), NAME(VIG1), NAME(VIG2),
NAME(RGB0), NAME(RGB1), NAME(RGB2),
NAME(DMA0), NAME(DMA1),
#undef NAME
};
return names[pipe];
}
static inline uint32_t pipe2flush(enum mdp5_pipe pipe)
{
switch (pipe) {
case SSPP_VIG0: return MDP5_CTL_FLUSH_VIG0;
case SSPP_VIG1: return MDP5_CTL_FLUSH_VIG1;
case SSPP_VIG2: return MDP5_CTL_FLUSH_VIG2;
case SSPP_RGB0: return MDP5_CTL_FLUSH_RGB0;
case SSPP_RGB1: return MDP5_CTL_FLUSH_RGB1;
case SSPP_RGB2: return MDP5_CTL_FLUSH_RGB2;
case SSPP_DMA0: return MDP5_CTL_FLUSH_DMA0;
case SSPP_DMA1: return MDP5_CTL_FLUSH_DMA1;
default: return 0;
}
}
static inline int pipe2nclients(enum mdp5_pipe pipe)
{
switch (pipe) {
case SSPP_RGB0:
case SSPP_RGB1:
case SSPP_RGB2:
return 1;
default:
return 3;
}
}
static inline enum mdp5_client_id pipe2client(enum mdp5_pipe pipe, int plane)
{
WARN_ON(plane >= pipe2nclients(pipe));
switch (pipe) {
case SSPP_VIG0: return CID_VIG0_Y + plane;
case SSPP_VIG1: return CID_VIG1_Y + plane;
case SSPP_VIG2: return CID_VIG2_Y + plane;
case SSPP_RGB0: return CID_RGB0;
case SSPP_RGB1: return CID_RGB1;
case SSPP_RGB2: return CID_RGB2;
case SSPP_DMA0: return CID_DMA0_Y + plane;
case SSPP_DMA1: return CID_DMA1_Y + plane;
default: return CID_UNUSED;
}
}
static inline uint32_t mixer2flush(int lm)
{
switch (lm) {
case 0: return MDP5_CTL_FLUSH_LM0;
case 1: return MDP5_CTL_FLUSH_LM1;
case 2: return MDP5_CTL_FLUSH_LM2;
default: return 0;
}
}
static inline uint32_t intf2err(int intf)
{
switch (intf) {
case 0: return MDP5_IRQ_INTF0_UNDER_RUN;
case 1: return MDP5_IRQ_INTF1_UNDER_RUN;
case 2: return MDP5_IRQ_INTF2_UNDER_RUN;
case 3: return MDP5_IRQ_INTF3_UNDER_RUN;
default: return 0;
}
}
static inline uint32_t intf2vblank(int intf)
{
switch (intf) {
case 0: return MDP5_IRQ_INTF0_VSYNC;
case 1: return MDP5_IRQ_INTF1_VSYNC;
case 2: return MDP5_IRQ_INTF2_VSYNC;
case 3: return MDP5_IRQ_INTF3_VSYNC;
default: return 0;
}
}
int mdp5_disable(struct mdp5_kms *mdp5_kms);
int mdp5_enable(struct mdp5_kms *mdp5_kms);
void mdp5_set_irqmask(struct mdp_kms *mdp_kms, uint32_t irqmask);
void mdp5_irq_preinstall(struct msm_kms *kms);
int mdp5_irq_postinstall(struct msm_kms *kms);
void mdp5_irq_uninstall(struct msm_kms *kms);
irqreturn_t mdp5_irq(struct msm_kms *kms);
int mdp5_enable_vblank(struct msm_kms *kms, struct drm_crtc *crtc);
void mdp5_disable_vblank(struct msm_kms *kms, struct drm_crtc *crtc);
static inline
uint32_t mdp5_get_formats(enum mdp5_pipe pipe, uint32_t *pixel_formats,
uint32_t max_formats)
{
/* TODO when we have YUV, we need to filter supported formats
* based on pipe id..
*/
return mdp_get_formats(pixel_formats, max_formats);
}
void mdp5_plane_install_properties(struct drm_plane *plane,
struct drm_mode_object *obj);
void mdp5_plane_set_scanout(struct drm_plane *plane,
struct drm_framebuffer *fb);
int mdp5_plane_mode_set(struct drm_plane *plane,
struct drm_crtc *crtc, struct drm_framebuffer *fb,
int crtc_x, int crtc_y,
unsigned int crtc_w, unsigned int crtc_h,
uint32_t src_x, uint32_t src_y,
uint32_t src_w, uint32_t src_h);
void mdp5_plane_complete_flip(struct drm_plane *plane);
enum mdp5_pipe mdp5_plane_pipe(struct drm_plane *plane);
struct drm_plane *mdp5_plane_init(struct drm_device *dev,
enum mdp5_pipe pipe, bool private_plane);
uint32_t mdp5_crtc_vblank(struct drm_crtc *crtc);
void mdp5_crtc_cancel_pending_flip(struct drm_crtc *crtc, struct drm_file *file);
void mdp5_crtc_set_intf(struct drm_crtc *crtc, int intf,
enum mdp5_intf intf_id);
void mdp5_crtc_attach(struct drm_crtc *crtc, struct drm_plane *plane);
void mdp5_crtc_detach(struct drm_crtc *crtc, struct drm_plane *plane);
struct drm_crtc *mdp5_crtc_init(struct drm_device *dev,
struct drm_plane *plane, int id);
struct drm_encoder *mdp5_encoder_init(struct drm_device *dev, int intf,
enum mdp5_intf intf_id);
#endif /* __MDP5_KMS_H__ */
This diff is collapsed.
/*
* Copyright (C) 2013 Red Hat
* Author: Rob Clark <robdclark@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "mdp5_kms.h"
#include "mdp5_smp.h"
/* SMP - Shared Memory Pool
*
* These are shared between all the clients, where each plane in a
* scanout buffer is a SMP client. Ie. scanout of 3 plane I420 on
* pipe VIG0 => 3 clients: VIG0_Y, VIG0_CB, VIG0_CR.
*
* Based on the size of the attached scanout buffer, a certain # of
* blocks must be allocated to that client out of the shared pool.
*
* For each block, it can be either free, or pending/in-use by a
* client. The updates happen in three steps:
*
* 1) mdp5_smp_request():
* When plane scanout is setup, calculate required number of
* blocks needed per client, and request. Blocks not inuse or
* pending by any other client are added to client's pending
* set.
*
* 2) mdp5_smp_configure():
* As hw is programmed, before FLUSH, MDP5_SMP_ALLOC registers
* are configured for the union(pending, inuse)
*
* 3) mdp5_smp_commit():
* After next vblank, copy pending -> inuse. Optionally update
* MDP5_SMP_ALLOC registers if there are newly unused blocks
*
* On the next vblank after changes have been committed to hw, the
* client's pending blocks become it's in-use blocks (and no-longer
* in-use blocks become available to other clients).
*
* btw, hurray for confusing overloaded acronyms! :-/
*
* NOTE: for atomic modeset/pageflip NONBLOCK operations, step #1
* should happen at (or before)? atomic->check(). And we'd need
* an API to discard previous requests if update is aborted or
* (test-only).
*
* TODO would perhaps be nice to have debugfs to dump out kernel
* inuse and pending state of all clients..
*/
static DEFINE_SPINLOCK(smp_lock);
/* step #1: update # of blocks pending for the client: */
int mdp5_smp_request(struct mdp5_kms *mdp5_kms,
enum mdp5_client_id cid, int nblks)
{
struct mdp5_client_smp_state *ps = &mdp5_kms->smp_client_state[cid];
int i, ret, avail, cur_nblks, cnt = mdp5_kms->smp_blk_cnt;
unsigned long flags;
spin_lock_irqsave(&smp_lock, flags);
avail = cnt - bitmap_weight(mdp5_kms->smp_state, cnt);
if (nblks > avail) {
ret = -ENOSPC;
goto fail;
}
cur_nblks = bitmap_weight(ps->pending, cnt);
if (nblks > cur_nblks) {
/* grow the existing pending reservation: */
for (i = cur_nblks; i < nblks; i++) {
int blk = find_first_zero_bit(mdp5_kms->smp_state, cnt);
set_bit(blk, ps->pending);
set_bit(blk, mdp5_kms->smp_state);
}
} else {
/* shrink the existing pending reservation: */
for (i = cur_nblks; i > nblks; i--) {
int blk = find_first_bit(ps->pending, cnt);
clear_bit(blk, ps->pending);
/* don't clear in global smp_state until _commit() */
}
}
fail:
spin_unlock_irqrestore(&smp_lock, flags);
return 0;
}
static void update_smp_state(struct mdp5_kms *mdp5_kms,
enum mdp5_client_id cid, mdp5_smp_state_t *assigned)
{
int cnt = mdp5_kms->smp_blk_cnt;
uint32_t blk, val;
for_each_set_bit(blk, *assigned, cnt) {
int idx = blk / 3;
int fld = blk % 3;
val = mdp5_read(mdp5_kms, REG_MDP5_SMP_ALLOC_W_REG(idx));
switch (fld) {
case 0:
val &= ~MDP5_SMP_ALLOC_W_REG_CLIENT0__MASK;
val |= MDP5_SMP_ALLOC_W_REG_CLIENT0(cid);
break;
case 1:
val &= ~MDP5_SMP_ALLOC_W_REG_CLIENT1__MASK;
val |= MDP5_SMP_ALLOC_W_REG_CLIENT1(cid);
break;
case 2:
val &= ~MDP5_SMP_ALLOC_W_REG_CLIENT2__MASK;
val |= MDP5_SMP_ALLOC_W_REG_CLIENT2(cid);
break;
}
mdp5_write(mdp5_kms, REG_MDP5_SMP_ALLOC_W_REG(idx), val);
mdp5_write(mdp5_kms, REG_MDP5_SMP_ALLOC_R_REG(idx), val);
}
}
/* step #2: configure hw for union(pending, inuse): */
void mdp5_smp_configure(struct mdp5_kms *mdp5_kms, enum mdp5_client_id cid)
{
struct mdp5_client_smp_state *ps = &mdp5_kms->smp_client_state[cid];
int cnt = mdp5_kms->smp_blk_cnt;
mdp5_smp_state_t assigned;
bitmap_or(assigned, ps->inuse, ps->pending, cnt);
update_smp_state(mdp5_kms, cid, &assigned);
}
/* step #3: after vblank, copy pending -> inuse: */
void mdp5_smp_commit(struct mdp5_kms *mdp5_kms, enum mdp5_client_id cid)
{
struct mdp5_client_smp_state *ps = &mdp5_kms->smp_client_state[cid];
int cnt = mdp5_kms->smp_blk_cnt;
mdp5_smp_state_t released;
/*
* Figure out if there are any blocks we where previously
* using, which can be released and made available to other
* clients:
*/
if (bitmap_andnot(released, ps->inuse, ps->pending, cnt)) {
unsigned long flags;
spin_lock_irqsave(&smp_lock, flags);
/* clear released blocks: */
bitmap_andnot(mdp5_kms->smp_state, mdp5_kms->smp_state,
released, cnt);
spin_unlock_irqrestore(&smp_lock, flags);
update_smp_state(mdp5_kms, CID_UNUSED, &released);
}
bitmap_copy(ps->inuse, ps->pending, cnt);
}
/*
* Copyright (C) 2013 Red Hat
* Author: Rob Clark <robdclark@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __MDP5_SMP_H__
#define __MDP5_SMP_H__
#include "msm_drv.h"
#define MAX_SMP_BLOCKS 22
#define SMP_BLK_SIZE 4096
#define SMP_ENTRIES_PER_BLK (SMP_BLK_SIZE / 16)
typedef DECLARE_BITMAP(mdp5_smp_state_t, MAX_SMP_BLOCKS);
struct mdp5_client_smp_state {
mdp5_smp_state_t inuse;
mdp5_smp_state_t pending;
};
struct mdp5_kms;
int mdp5_smp_request(struct mdp5_kms *mdp5_kms, enum mdp5_client_id cid, int nblks);
void mdp5_smp_configure(struct mdp5_kms *mdp5_kms, enum mdp5_client_id cid);
void mdp5_smp_commit(struct mdp5_kms *mdp5_kms, enum mdp5_client_id cid);
#endif /* __MDP5_SMP_H__ */
...@@ -150,6 +150,24 @@ static int msm_unload(struct drm_device *dev) ...@@ -150,6 +150,24 @@ static int msm_unload(struct drm_device *dev)
return 0; return 0;
} }
static int get_mdp_ver(struct platform_device *pdev)
{
#ifdef CONFIG_OF
const static struct of_device_id match_types[] = { {
.compatible = "qcom,mdss_mdp",
.data = (void *)5,
}, {
/* end node */
} };
struct device *dev = &pdev->dev;
const struct of_device_id *match;
match = of_match_node(match_types, dev->of_node);
if (match)
return (int)match->data;
#endif
return 4;
}
static int msm_load(struct drm_device *dev, unsigned long flags) static int msm_load(struct drm_device *dev, unsigned long flags)
{ {
struct platform_device *pdev = dev->platformdev; struct platform_device *pdev = dev->platformdev;
...@@ -208,7 +226,18 @@ static int msm_load(struct drm_device *dev, unsigned long flags) ...@@ -208,7 +226,18 @@ static int msm_load(struct drm_device *dev, unsigned long flags)
(uint32_t)(priv->vram.paddr + size)); (uint32_t)(priv->vram.paddr + size));
} }
kms = mdp4_kms_init(dev); switch (get_mdp_ver(pdev)) {
case 4:
kms = mdp4_kms_init(dev);
break;
case 5:
kms = mdp5_kms_init(dev);
break;
default:
kms = ERR_PTR(-ENODEV);
break;
}
if (IS_ERR(kms)) { if (IS_ERR(kms)) {
/* /*
* NOTE: once we have GPU support, having no kms should not * NOTE: once we have GPU support, having no kms should not
...@@ -811,12 +840,19 @@ static const struct platform_device_id msm_id[] = { ...@@ -811,12 +840,19 @@ static const struct platform_device_id msm_id[] = {
{ } { }
}; };
static const struct of_device_id dt_match[] = {
{ .compatible = "qcom,mdss_mdp" },
{}
};
MODULE_DEVICE_TABLE(of, dt_match);
static struct platform_driver msm_platform_driver = { static struct platform_driver msm_platform_driver = {
.probe = msm_pdev_probe, .probe = msm_pdev_probe,
.remove = msm_pdev_remove, .remove = msm_pdev_remove,
.driver = { .driver = {
.owner = THIS_MODULE, .owner = THIS_MODULE,
.name = "msm", .name = "msm",
.of_match_table = dt_match,
.pm = &msm_pm_ops, .pm = &msm_pm_ops,
}, },
.id_table = msm_id, .id_table = msm_id,
......
...@@ -63,5 +63,6 @@ static inline void msm_kms_init(struct msm_kms *kms, ...@@ -63,5 +63,6 @@ static inline void msm_kms_init(struct msm_kms *kms,
} }
struct msm_kms *mdp4_kms_init(struct drm_device *dev); struct msm_kms *mdp4_kms_init(struct drm_device *dev);
struct msm_kms *mdp5_kms_init(struct drm_device *dev);
#endif /* __MSM_KMS_H__ */ #endif /* __MSM_KMS_H__ */
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment