| /* |
| * Copyright 2022, NXP |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT nxp_imx_mipi_dsi |
| |
| #include <zephyr/drivers/mipi_dsi.h> |
| #include <fsl_mipi_dsi.h> |
| #include <fsl_clock.h> |
| #include <zephyr/logging/log.h> |
| |
| #include <soc.h> |
| |
| LOG_MODULE_REGISTER(dsi_mcux, CONFIG_MIPI_DSI_LOG_LEVEL); |
| |
| #define MIPI_DPHY_REF_CLK DT_INST_PROP(0, dphy_ref_frequency) |
| |
| /* Max output frequency of DPHY bit clock */ |
| #define MIPI_DPHY_MAX_FREQ MHZ(800) |
| |
| /* PLL CN should be in the range of 1 to 32. */ |
| #define DSI_DPHY_PLL_CN_MIN 1U |
| #define DSI_DPHY_PLL_CN_MAX 32U |
| |
| /* PLL refClk / CN should be in the range of 24M to 30M. */ |
| #define DSI_DPHY_PLL_REFCLK_CN_MIN MHZ(24) |
| #define DSI_DPHY_PLL_REFCLK_CN_MAX MHZ(30) |
| |
| /* PLL CM should be in the range of 16 to 255. */ |
| #define DSI_DPHY_PLL_CM_MIN 16U |
| #define DSI_DPHY_PLL_CM_MAX 255U |
| |
| #define DSI_DPHY_PLL_CO_MIN 0 |
| #define DSI_DPHY_PLL_CO_MAX 3 |
| |
| /* MAX DSI TX payload */ |
| #define DSI_TX_MAX_PAYLOAD_BYTE (64U * 4U) |
| |
| struct display_mcux_mipi_dsi_config { |
| MIPI_DSI_Type base; |
| dsi_dpi_config_t dpi_config; |
| bool auto_insert_eotp; |
| uint32_t phy_clock; |
| }; |
| |
| struct display_mcux_mipi_dsi_data { |
| const struct device *dev; |
| }; |
| |
| static uint32_t dsi_mcux_best_clock(uint32_t ref_clk, uint32_t target_freq) |
| { |
| /* |
| * This function is intended to find the closest realizable DPHY |
| * bit clock for a given target frequency, such that the DPHY clock |
| * is faster than the target frequency. MCUX SDK implements a similar |
| * function with DSI_DphyGetPllDivider, but this function will |
| * configure the DPHY to output the closest realizable clock frequency |
| * to the requested value. This can cause dropped pixels if |
| * the output frequency is less than the requested one. |
| */ |
| uint32_t co_shift, cn, cm; |
| uint32_t cand_freq, vco_freq, refclk_cn_freq; |
| uint32_t best_pll_freq = 0U; |
| uint32_t best_diff = UINT32_MAX; |
| |
| /* |
| * The formula for the DPHY output frequency is: |
| * ref_clk * (CM / (CN * (1 << CO))) |
| */ |
| |
| /* Test all available CO shifts (1x, 2x, 4x, 8x) */ |
| for (co_shift = DSI_DPHY_PLL_CO_MIN; co_shift <= DSI_DPHY_PLL_CO_MAX; co_shift++) { |
| /* Determine VCO output frequency before CO divider */ |
| vco_freq = target_freq << co_shift; |
| |
| /* If desired VCO output frequency is too low, try next CO shift */ |
| if (vco_freq < DSI_DPHY_PLL_VCO_MIN) { |
| continue; |
| } |
| |
| /* If desired VCO output frequency is too high, no point in |
| * searching further |
| */ |
| if (vco_freq > DSI_DPHY_PLL_VCO_MAX) { |
| break; |
| } |
| |
| /* Search the best CN and CM values for desired VCO frequency */ |
| for (cn = DSI_DPHY_PLL_CN_MIN; cn <= DSI_DPHY_PLL_CN_MAX; cn++) { |
| refclk_cn_freq = ref_clk / cn; |
| |
| /* If the frequency after input divider is too high, |
| * try next CN value |
| */ |
| if (refclk_cn_freq > DSI_DPHY_PLL_REFCLK_CN_MAX) { |
| continue; |
| } |
| |
| /* If the frequency after input divider is too low, |
| * no point in trying higher dividers. |
| */ |
| if (refclk_cn_freq < DSI_DPHY_PLL_REFCLK_CN_MIN) { |
| break; |
| } |
| |
| /* Get the closest CM value for this vco frequency |
| * and input divider. Round up, to bias towards higher |
| * frequencies |
| * NOTE: we differ from the SDK algorithm here, which |
| * would round cm to the closest integer |
| */ |
| cm = (vco_freq + (refclk_cn_freq - 1)) / refclk_cn_freq; |
| |
| /* If CM was rounded up to one over valid range, |
| * round down |
| */ |
| if (cm == (DSI_DPHY_PLL_CM_MAX + 1)) { |
| cm = DSI_DPHY_PLL_CM_MAX; |
| } |
| |
| /* If CM value is still out of range, CN/CO setting won't work */ |
| if ((cm < DSI_DPHY_PLL_CM_MIN) || (cm > DSI_DPHY_PLL_CM_MAX)) { |
| continue; |
| } |
| |
| /* Calculate candidate frequency */ |
| cand_freq = (refclk_cn_freq * cm) >> co_shift; |
| |
| if (cand_freq < target_freq) { |
| /* SKIP frequencies less than target frequency. |
| * this is where the algorithm differs from the |
| * SDK. |
| */ |
| continue; |
| } else { |
| if ((cand_freq - target_freq) < best_diff) { |
| /* New best CN, CM, and CO found */ |
| best_diff = (cand_freq - target_freq); |
| best_pll_freq = cand_freq; |
| } |
| } |
| |
| if (best_diff == 0U) { |
| /* We have found exact match for CN, CM, CO. |
| * return now. |
| */ |
| return best_pll_freq; |
| } |
| } |
| } |
| return best_pll_freq; |
| } |
| |
| |
| static int dsi_mcux_attach(const struct device *dev, |
| uint8_t channel, |
| const struct mipi_dsi_device *mdev) |
| { |
| const struct display_mcux_mipi_dsi_config *config = dev->config; |
| dsi_dphy_config_t dphy_config; |
| dsi_config_t dsi_config; |
| uint32_t mipi_dsi_esc_clk_hz; |
| uint32_t mipi_dsi_tx_esc_clk_hz; |
| uint32_t mipi_dsi_dphy_ref_clk_hz = MIPI_DPHY_REF_CLK; |
| |
| DSI_GetDefaultConfig(&dsi_config); |
| dsi_config.numLanes = mdev->data_lanes; |
| dsi_config.autoInsertEoTp = config->auto_insert_eotp; |
| |
| /* Init the DSI module. */ |
| DSI_Init((MIPI_DSI_Type *)&config->base, &dsi_config); |
| |
| /* Init DPHY. |
| * |
| * The DPHY bit clock must be fast enough to send out the pixels, it should be |
| * larger than: |
| * |
| * (Pixel clock * bit per output pixel) / number of MIPI data lane |
| */ |
| uint32_t mipi_dsi_dpi_clk_hz = CLOCK_GetRootClockFreq(kCLOCK_Root_Lcdif); |
| /* Find the best realizable clock value for the MIPI DSI */ |
| uint32_t mipi_dsi_dphy_bit_clk_hz = |
| dsi_mcux_best_clock(mipi_dsi_dphy_ref_clk_hz, config->phy_clock); |
| if (mipi_dsi_dphy_bit_clk_hz == 0) { |
| LOG_ERR("DPHY cannot support requested PHY clock"); |
| return -ENOTSUP; |
| } |
| /* Cap clock value to max frequency */ |
| mipi_dsi_dphy_bit_clk_hz = MIN(mipi_dsi_dphy_bit_clk_hz, MIPI_DPHY_MAX_FREQ); |
| |
| mipi_dsi_esc_clk_hz = CLOCK_GetRootClockFreq(kCLOCK_Root_Mipi_Esc); |
| mipi_dsi_tx_esc_clk_hz = mipi_dsi_esc_clk_hz / 3; |
| |
| DSI_GetDphyDefaultConfig(&dphy_config, mipi_dsi_dphy_bit_clk_hz, mipi_dsi_tx_esc_clk_hz); |
| |
| mipi_dsi_dphy_bit_clk_hz = DSI_InitDphy((MIPI_DSI_Type *)&config->base, |
| &dphy_config, mipi_dsi_dphy_ref_clk_hz); |
| |
| LOG_DBG("DPHY clock set to %u", mipi_dsi_dphy_bit_clk_hz); |
| /* |
| * If nxp,lcdif node is present, then the MIPI DSI driver will |
| * accept input on the DPI port from the LCDIF, and convert the output |
| * to DSI data. This is useful for video mode, where the LCDIF can |
| * constantly refresh the MIPI panel. |
| */ |
| if (mdev->mode_flags & MIPI_DSI_MODE_VIDEO) { |
| /* Init DPI interface. */ |
| DSI_SetDpiConfig((MIPI_DSI_Type *)&config->base, |
| &config->dpi_config, mdev->data_lanes, |
| mipi_dsi_dpi_clk_hz, mipi_dsi_dphy_bit_clk_hz); |
| } |
| |
| imxrt_post_init_display_interface(); |
| |
| return 0; |
| } |
| |
| static ssize_t dsi_mcux_transfer(const struct device *dev, uint8_t channel, |
| struct mipi_dsi_msg *msg) |
| { |
| const struct display_mcux_mipi_dsi_config *config = dev->config; |
| dsi_transfer_t dsi_xfer = {0}; |
| status_t status; |
| |
| dsi_xfer.virtualChannel = channel; |
| dsi_xfer.txDataSize = msg->tx_len; |
| dsi_xfer.txData = msg->tx_buf; |
| dsi_xfer.rxDataSize = msg->rx_len; |
| dsi_xfer.rxData = msg->rx_buf; |
| |
| switch (msg->type) { |
| |
| case MIPI_DSI_DCS_READ: |
| LOG_ERR("DCS Read not yet implemented or used"); |
| return -ENOTSUP; |
| case MIPI_DSI_DCS_SHORT_WRITE: |
| dsi_xfer.sendDscCmd = true; |
| dsi_xfer.dscCmd = msg->cmd; |
| dsi_xfer.txDataType = kDSI_TxDataDcsShortWrNoParam; |
| break; |
| case MIPI_DSI_DCS_SHORT_WRITE_PARAM: |
| dsi_xfer.sendDscCmd = true; |
| dsi_xfer.dscCmd = msg->cmd; |
| dsi_xfer.txDataType = kDSI_TxDataDcsShortWrOneParam; |
| break; |
| case MIPI_DSI_DCS_LONG_WRITE: |
| dsi_xfer.sendDscCmd = true; |
| dsi_xfer.dscCmd = msg->cmd; |
| dsi_xfer.flags = kDSI_TransferUseHighSpeed; |
| dsi_xfer.txDataType = kDSI_TxDataDcsLongWr; |
| /* |
| * Cap transfer size. Note that we subtract six bytes here, |
| * one for the DSC command and one to insure that |
| * transfers are still aligned on a pixel boundary |
| * (two or three byte pixel sizes are supported). |
| */ |
| dsi_xfer.txDataSize = MIN(dsi_xfer.txDataSize, |
| (DSI_TX_MAX_PAYLOAD_BYTE - 6)); |
| break; |
| case MIPI_DSI_GENERIC_SHORT_WRITE_0_PARAM: |
| dsi_xfer.txDataType = kDSI_TxDataGenShortWrNoParam; |
| break; |
| case MIPI_DSI_GENERIC_SHORT_WRITE_1_PARAM: |
| dsi_xfer.txDataType = kDSI_TxDataGenShortWrOneParam; |
| break; |
| case MIPI_DSI_GENERIC_SHORT_WRITE_2_PARAM: |
| dsi_xfer.txDataType = kDSI_TxDataGenShortWrTwoParam; |
| break; |
| case MIPI_DSI_GENERIC_LONG_WRITE: |
| dsi_xfer.txDataType = kDSI_TxDataGenLongWr; |
| break; |
| case MIPI_DSI_GENERIC_READ_REQUEST_0_PARAM: |
| __fallthrough; |
| case MIPI_DSI_GENERIC_READ_REQUEST_1_PARAM: |
| __fallthrough; |
| case MIPI_DSI_GENERIC_READ_REQUEST_2_PARAM: |
| LOG_ERR("Generic Read not yet implemented or used"); |
| return -ENOTSUP; |
| default: |
| LOG_ERR("Unsupported message type (%d)", msg->type); |
| return -ENOTSUP; |
| } |
| |
| status = DSI_TransferBlocking(&config->base, &dsi_xfer); |
| |
| if (status != kStatus_Success) { |
| LOG_ERR("Transmission failed"); |
| return -EIO; |
| } |
| |
| if (msg->rx_len != 0) { |
| /* Return rx_len on a read */ |
| return dsi_xfer.rxDataSize; |
| } |
| |
| /* Return tx_len on a write */ |
| return dsi_xfer.txDataSize; |
| |
| } |
| |
| static struct mipi_dsi_driver_api dsi_mcux_api = { |
| .attach = dsi_mcux_attach, |
| .transfer = dsi_mcux_transfer, |
| }; |
| |
| static int display_mcux_mipi_dsi_init(const struct device *dev) |
| { |
| imxrt_pre_init_display_interface(); |
| |
| return 0; |
| } |
| |
| #define MCUX_DSI_DPI_CONFIG(id) \ |
| IF_ENABLED(DT_NODE_HAS_PROP(DT_DRV_INST(id), nxp_lcdif), \ |
| (.dpi_config = { \ |
| .dpiColorCoding = DT_INST_ENUM_IDX(id, dpi_color_coding), \ |
| .pixelPacket = DT_INST_ENUM_IDX(id, dpi_pixel_packet), \ |
| .videoMode = DT_INST_ENUM_IDX(id, dpi_video_mode), \ |
| .bllpMode = DT_INST_ENUM_IDX(id, dpi_bllp_mode), \ |
| .pixelPayloadSize = DT_INST_PROP_BY_PHANDLE(id, nxp_lcdif, width), \ |
| .panelHeight = DT_INST_PROP_BY_PHANDLE(id, nxp_lcdif, height), \ |
| .polarityFlags = (DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), hsync_active) ? \ |
| kDSI_DpiHsyncActiveHigh : kDSI_DpiHsyncActiveLow) | \ |
| (DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), vsync_active) ? \ |
| kDSI_DpiVsyncActiveHigh : kDSI_DpiVsyncActiveLow), \ |
| .hfp = DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), hfront_porch), \ |
| .hbp = DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), hback_porch), \ |
| .hsw = DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), hsync_len), \ |
| .vfp = DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), vfront_porch), \ |
| .vbp = DT_PROP(DT_CHILD(DT_INST_PHANDLE(id, nxp_lcdif), \ |
| display_timings), vback_porch), \ |
| },)) |
| |
| #define MCUX_MIPI_DSI_DEVICE(id) \ |
| static const struct display_mcux_mipi_dsi_config display_mcux_mipi_dsi_config_##id = { \ |
| .base = { \ |
| .host = (DSI_HOST_Type *)DT_INST_REG_ADDR_BY_IDX(id, 0), \ |
| .dpi = (DSI_HOST_DPI_INTFC_Type *)DT_INST_REG_ADDR_BY_IDX(id, 1), \ |
| .apb = (DSI_HOST_APB_PKT_IF_Type *)DT_INST_REG_ADDR_BY_IDX(id, 2), \ |
| .dphy = (DSI_HOST_NXP_FDSOI28_DPHY_INTFC_Type *) \ |
| DT_INST_REG_ADDR_BY_IDX(id, 3), \ |
| }, \ |
| MCUX_DSI_DPI_CONFIG(id) \ |
| .auto_insert_eotp = DT_INST_PROP(id, autoinsert_eotp), \ |
| .phy_clock = DT_INST_PROP(id, phy_clock), \ |
| }; \ |
| static struct display_mcux_mipi_dsi_data display_mcux_mipi_dsi_data_##id; \ |
| DEVICE_DT_INST_DEFINE(id, \ |
| &display_mcux_mipi_dsi_init, \ |
| NULL, \ |
| &display_mcux_mipi_dsi_data_##id, \ |
| &display_mcux_mipi_dsi_config_##id, \ |
| POST_KERNEL, \ |
| CONFIG_MIPI_DSI_INIT_PRIORITY, \ |
| &dsi_mcux_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(MCUX_MIPI_DSI_DEVICE) |