OpenHarmony4.0适配LVDS屏幕驱动
1.概述
手头有一块RK3568的开发板OK3568-C,但是还没有适配OpenHarmony,用的还是LVDS屏幕,但是官方和网上好像还没有OpenHarmony4.0的LVDS屏幕驱动的通用实现,所以决定尝试了一下适配该开发板,完成LVDS屏幕驱动的适配,点亮屏幕。
源代码:oh4.0-lvds-ok3568-c
2.具体实现
2.1 添加ok3568产品(非必须,可以跳过,直接修改原有的rk3568产品)
因为OK3568-C开发板使用的是RK3568芯片,和OpenHarmony的主线分支一样,所以添加ok3568产品的基础流程比较简单,就是复制device和vendor下面rk3568的文件夹,改为ok3568,并加入ok3568对应的设备树文件,修改build文件下面的编译配置文件(不修改编译会报错),具体流程可以参考官方和网上的其他教程。
本次使用的LVDS屏幕对应的设备树节点如下:
OK3568-common.dtsi
2.2 分析原有的MIPI屏幕驱动
参考鸿蒙系统原有的MIPI屏幕驱动目录(drivers/framework/model/display/driver)下面的ili9881_st_5p5.c和hdf_drm_panel.c:
可以看出鸿蒙原有的MIPI屏幕驱动实际上参考了Linux内核的drivers/gpu/drm/panel/panel-simple.c的“simple-panel-dsi”部分,不知道为什么官方没有把panel-simple.c中剩下的“simple-panel”部分加进去。而“simple-panel”剩余的部分就是LVDS和EDP这类屏幕通用的驱动。
2.3 实现LVDS屏幕驱动
这里我们也参考Linux内核的drivers/gpu/drm/panel/panel-simple.c
2.3.1 读取设备树里面的"simple-panel"节点,获取Panel的参数
添加如下两个文件:
panel_simple_common.h
/*** HDF is dual licensed: you can use it either under the terms of* the GPL, or the BSD license, at your option.* See the LICENSE file in the root of this repository for complete details.*/#ifndef PANEL_SIMPLE_COMMON_H
#define PANEL_SIMPLE_COMMON_H#include <linux/backlight.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/regulator/consumer.h>
#include <linux/gpio/consumer.h>#include <video/display_timing.h>
#include <video/mipi_display.h>
#include <video/of_display_timing.h>
#include <video/videomode.h>#include <drm/drm_modes.h>
#include <drm/drm_panel.h>#include <uapi/drm/drm_mode.h>#include "securec.h"#include "hdf_disp.h"
#include "hdf_drm_panel_simple.h"#endif /* PANEL_SIMPLE_COMMON_H */
panel_simple_common.c
/*** HDF is dual licensed: you can use it either under the terms of* the GPL, or the BSD license, at your option.* See the LICENSE file in the root of this repository for complete details.*/#include "panel_simple_common.h"
#include "gpio_if.h"
#include "hdf_bl.h"
#include "hdf_disp.h"
#include "osal.h"static inline struct panel_simple *to_panel_simple(const struct drm_panel *panel)
{return container_of(panel, struct panel_simple, panel);
}static inline struct panel_simple *to_panel_simple_by_data(const struct PanelData *panel_data)
{return container_of(panel_data, struct panel_simple, panel_data);
}static int panel_simple_parse_cmd_seq(struct device *dev,const u8 *data, int length,struct panel_cmd_seq *seq)
{struct panel_cmd_header *header;struct panel_cmd_desc *desc;char *buf, *d;unsigned int i, cnt, len;if (!seq)return -EINVAL;buf = (u8 *)OsalMemCalloc(length);if (!buf)return -ENOMEM;memcpy_s(buf, length, data, length);d = buf;len = length;cnt = 0;while (len > sizeof(*header)) {header = (struct panel_cmd_header *)d;d += sizeof(*header);len -= sizeof(*header);if (header->payload_length > len)return -EINVAL;d += header->payload_length;len -= header->payload_length;cnt++;}if (len)return -EINVAL;seq->cmd_cnt = cnt;seq->cmds = (struct panel_cmd_desc *)OsalMemCalloc(sizeof(*desc));if (!seq->cmds)return -ENOMEM;d = buf;len = length;for (i = 0; i < cnt; i++) {header = (struct panel_cmd_header *)d;len -= sizeof(*header);d += sizeof(*header);desc = &seq->cmds[i];desc->header = *header;desc->payload = d;d += header->payload_length;len -= header->payload_length;}return 0;
}static int32_t panel_simple_regulator_enable(struct panel_simple *p)
{int32_t err;if (p->power_invert) {if (regulator_is_enabled(p->supply) > 0)regulator_disable(p->supply);} else {err = regulator_enable(p->supply);if (err < 0)return err;}return 0;
}static int32_t panel_simple_regulator_disable(struct panel_simple *p)
{int err;if (p->power_invert) {if (!regulator_is_enabled(p->supply)) {err = regulator_enable(p->supply);if (err < 0)return err;}} else {regulator_disable(p->supply);}return 0;
}int panel_simple_loader_protect(struct drm_panel *panel)
{struct panel_simple *p = to_panel_simple(panel);int err;err = panel_simple_regulator_enable(p);if (err < 0) {HDF_LOGE("failed to enable supply: %d\n", err);return err;}p->prepared = true;p->enabled = true;return 0;
}
EXPORT_SYMBOL(panel_simple_loader_protect);static int panel_simple_get_hpd_gpio(struct device *dev,struct panel_simple *p, bool from_probe)
{int err;p->hpd_gpio = devm_gpiod_get_optional(dev, "hpd", GPIOD_IN);if (IS_ERR(p->hpd_gpio)) {err = PTR_ERR(p->hpd_gpio);/** If we're called from probe we won't consider '-EPROBE_DEFER'* to be an error--we'll leave the error code in "hpd_gpio".* When we try to use it we'll try again. This allows for* circular dependencies where the component providing the* hpd gpio needs the panel to init before probing.*/if (err != -EPROBE_DEFER || !from_probe) {HDF_LOGE("failed to get 'hpd' GPIO: %d\n", err);return err;}}return 0;
}#define PANEL_SIMPLE_BOUNDS_CHECK(to_check, bounds, field) \(to_check->field.typ >= bounds->field.min && \to_check->field.typ <= bounds->field.max)
static void panel_simple_parse_panel_timing_node(struct device *dev,struct panel_simple *panel,const struct display_timing *ot)
{const struct panel_desc *desc = panel->desc;struct videomode vm;unsigned int i;if (WARN_ON(desc->num_modes)) {HDF_LOGE("Reject override mode: panel has a fixed mode\n");return;}if (WARN_ON(!desc->num_timings)) {HDF_LOGE("Reject override mode: no timings specified\n");return;}for (i = 0; i < panel->desc->num_timings; i++) {const struct display_timing *dt = &panel->desc->timings[i];if (!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, hactive) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, hfront_porch) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, hback_porch) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, hsync_len) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, vactive) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, vfront_porch) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, vback_porch) ||!PANEL_SIMPLE_BOUNDS_CHECK(ot, dt, vsync_len))continue;if (ot->flags != dt->flags)continue;videomode_from_timing(ot, &vm);drm_display_mode_from_videomode(&vm, &panel->override_mode);panel->override_mode.type |= DRM_MODE_TYPE_DRIVER |DRM_MODE_TYPE_PREFERRED;break;}if (WARN_ON(!panel->override_mode.type))HDF_LOGE("Reject override mode: No display_timing found\n");
}static int32_t PanelOn(struct PanelData *data)
{struct panel_simple *p = to_panel_simple_by_data(data);if (p->enabled)return HDF_SUCCESS;if (p->desc->delay.enable)OsalMSleep(p->desc->delay.enable);p->enabled = true;return HDF_SUCCESS;
}static int32_t PanelOff(struct PanelData *data)
{struct panel_simple *simplePanel = NULL;simplePanel = to_panel_simple_by_data(data);if (simplePanel->desc->delay.disable) {OsalMSleep(simplePanel->desc->delay.disable);}return HDF_SUCCESS;
}static int32_t PanelPrepare(struct PanelData *data)
{struct panel_simple *p = to_panel_simple_by_data(data);unsigned int delay;int err;int hpd_asserted;if (p->prepared)return HDF_SUCCESS;err = panel_simple_regulator_enable(p);if (err < 0) {HDF_LOGE("failed to enable supply: %d\n", err);return err;}gpiod_direction_output(p->enable_gpio, 1);if (p->desc->delay.reset)OsalMSleep(p->desc->delay.reset);gpiod_direction_output(p->reset_gpio, 1);if (p->desc->delay.reset)OsalMSleep(p->desc->delay.reset);gpiod_direction_output(p->reset_gpio, 0);delay = p->desc->delay.prepare;if (p->no_hpd)delay += p->desc->delay.hpd_absent_delay;if (delay)OsalMSleep(delay);// if (p->hpd_gpio) {// if (IS_ERR(p->hpd_gpio)) {// err = panel_simple_get_hpd_gpio(panel->dev, p, false);// if (err)// return err;// }// err = readx_poll_timeout(gpiod_get_value_cansleep, p->hpd_gpio,// hpd_asserted, hpd_asserted,// 1000, 2000000);// if (hpd_asserted < 0)// err = hpd_asserted;// if (err) {// HDF_LOGE("error waiting for hpd GPIO: %d\n", err);// return err;// }// }// if (p->desc->init_seq)// if (p->dsi)// panel_simple_xfer_dsi_cmd_seq(p, p->desc->init_seq);if (p->desc->delay.init)OsalMSleep(p->desc->delay.init);p->prepared = true;return HDF_SUCCESS;
}static int32_t PanelUnprepare(struct PanelData *data)
{int32_t ret;struct panel_simple *p = NULL;p = to_panel_simple_by_data(data);if (!p->prepared)return HDF_SUCCESS;gpiod_direction_output(p->reset_gpio, 1);gpiod_direction_output(p->enable_gpio, 0);panel_simple_regulator_disable(p);if (p->desc->delay.unprepare) {OsalMSleep(p->desc->delay.unprepare);}return HDF_SUCCESS;
}static int32_t PanelInit(struct PanelData *panel)
{return 0;
}static int32_t panel_simple_probe(struct device *dev, struct panel_simple *panel)
{const struct panel_desc *desc = panel->desc;struct display_timing dt;int connector_type;u32 bus_flags;int32_t err;panel->no_hpd = of_property_read_bool(dev->of_node, "no-hpd");if (!panel->no_hpd) {err = panel_simple_get_hpd_gpio(dev, panel, true);if (err){HDF_LOGE("%s Get hpd gpio fail %d", __func__, err);goto FAIL;}}panel->supply = devm_regulator_get(dev, "power");if (IS_ERR(panel->supply)) {HDF_LOGE("%s Get regulator fail %d", __func__, PTR_ERR(panel->supply));goto FAIL;}panel->enable_gpio = devm_gpiod_get_optional(dev, "enable", GPIOD_ASIS);if (IS_ERR(panel->enable_gpio)) {HDF_LOGE("%s get enable_gpio fail %d", __func__, PTR_ERR(panel->enable_gpio));goto FAIL;}panel->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_ASIS);if (IS_ERR(panel->reset_gpio)) {HDF_LOGE("get reset_gpio fail %d", __func__, PTR_ERR(panel->reset_gpio));goto FAIL;}if (!of_get_display_timing(dev->of_node, "panel-timing", &dt))panel_simple_parse_panel_timing_node(dev, panel, &dt);connector_type = desc->connector_type;/* Catch common mistakes for panels. */switch (connector_type) {case 0:HDF_LOGD("Specify missing connector_type\n");connector_type = DRM_MODE_CONNECTOR_DPI;break;case DRM_MODE_CONNECTOR_LVDS:WARN_ON(desc->bus_flags &~(DRM_BUS_FLAG_DE_LOW |DRM_BUS_FLAG_DE_HIGH |DRM_BUS_FLAG_DATA_MSB_TO_LSB |DRM_BUS_FLAG_DATA_LSB_TO_MSB));WARN_ON(desc->bus_format != MEDIA_BUS_FMT_RGB666_1X7X3_SPWG &&desc->bus_format != MEDIA_BUS_FMT_RGB888_1X7X4_SPWG &&desc->bus_format != MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA);WARN_ON(desc->bus_format == MEDIA_BUS_FMT_RGB666_1X7X3_SPWG &&desc->bpc != 6);WARN_ON((desc->bus_format == MEDIA_BUS_FMT_RGB888_1X7X4_SPWG ||desc->bus_format == MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA) &&desc->bpc != 8);break;case DRM_MODE_CONNECTOR_eDP:if (desc->bus_format == 0)HDF_LOGW("Specify missing bus_format\n");if (desc->bpc != 6 && desc->bpc != 8)HDF_LOGW("Expected bpc in {6,8} but got: %u\n", desc->bpc);break;case DRM_MODE_CONNECTOR_DSI:if (desc->bpc != 6 && desc->bpc != 8)HDF_LOGW("Expected bpc in {6,8} but got: %u\n", desc->bpc);break;case DRM_MODE_CONNECTOR_DPI:bus_flags = DRM_BUS_FLAG_DE_LOW |DRM_BUS_FLAG_DE_HIGH |DRM_BUS_FLAG_PIXDATA_SAMPLE_POSEDGE |DRM_BUS_FLAG_PIXDATA_SAMPLE_NEGEDGE |DRM_BUS_FLAG_DATA_MSB_TO_LSB |DRM_BUS_FLAG_DATA_LSB_TO_MSB |DRM_BUS_FLAG_SYNC_SAMPLE_POSEDGE |DRM_BUS_FLAG_SYNC_SAMPLE_NEGEDGE;if (desc->bus_flags & ~bus_flags)HDF_LOGW("Unexpected bus_flags(%d)\n", desc->bus_flags & ~bus_flags);if (!(desc->bus_flags & bus_flags))HDF_LOGW("Specify missing bus_flags\n");if (desc->bus_format == 0)HDF_LOGW("Specify missing bus_format\n");if (desc->bpc != 6 && desc->bpc != 8)HDF_LOGW("Expected bpc in {6,8} but got: %u\n", desc->bpc);break;default:HDF_LOGW("Specify a valid connector_type: %d\n", desc->connector_type);connector_type = DRM_MODE_CONNECTOR_DPI;break;}HDF_LOGI("%s success", __func__);return HDF_SUCCESS;
FAIL:return HDF_FAILURE;
}static bool of_child_node_is_present(const struct device_node *node,const char *name)
{struct device_node *child;child = of_get_child_by_name(node, name);of_node_put(child);return !!child;
}static int panel_simple_of_get_desc_data(struct device *dev,struct panel_desc *desc)
{struct device_node *np = dev->of_node;u32 bus_flags;const void *data;int len;int err;if (of_child_node_is_present(np, "display-timings")) {struct drm_display_mode *mode;mode = (struct drm_display_mode *)OsalMemCalloc(sizeof(*mode));if (!mode)return -ENOMEM;err = of_get_drm_display_mode(np, mode, &bus_flags,OF_USE_NATIVE_MODE);if (!err) {desc->modes = mode;desc->num_modes = 1;desc->bus_flags = bus_flags;}} else if (of_child_node_is_present(np, "panel-timing")) {struct display_timing *timing;struct videomode vm;timing = (struct display_timing *)OsalMemCalloc(sizeof(*timing));if (!timing)return -ENOMEM;if (!of_get_display_timing(np, "panel-timing", timing)) {desc->timings = timing;desc->num_timings = 1;bus_flags = 0;vm.flags = timing->flags;drm_bus_flags_from_videomode(&vm, &bus_flags);desc->bus_flags = bus_flags;}}if (desc->num_modes || desc->num_timings) {of_property_read_u32(np, "bpc", &desc->bpc);of_property_read_u32(np, "bus-format", &desc->bus_format);of_property_read_u32(np, "width-mm", &desc->size.width);of_property_read_u32(np, "height-mm", &desc->size.height);}of_property_read_u32(np, "prepare-delay-ms", &desc->delay.prepare);of_property_read_u32(np, "enable-delay-ms", &desc->delay.enable);of_property_read_u32(np, "disable-delay-ms", &desc->delay.disable);of_property_read_u32(np, "unprepare-delay-ms", &desc->delay.unprepare);of_property_read_u32(np, "reset-delay-ms", &desc->delay.reset);of_property_read_u32(np, "init-delay-ms", &desc->delay.init);data = of_get_property(np, "panel-init-sequence", &len);if (data) {desc->init_seq = (struct panel_cmd_seq *)OsalMemCalloc(sizeof(*desc->init_seq));if (!desc->init_seq)return -ENOMEM;err = panel_simple_parse_cmd_seq(dev, data, len,desc->init_seq);if (err) {HDF_LOGE("failed to parse init sequence\n");return err;}}data = of_get_property(np, "panel-exit-sequence", &len);if (data) {desc->exit_seq = (struct panel_cmd_seq *)OsalMemCalloc(sizeof(*desc->exit_seq));if (!desc->exit_seq)return -ENOMEM;err = panel_simple_parse_cmd_seq(dev, data, len,desc->exit_seq);if (err) {HDF_LOGE("failed to parse exit sequence\n");return err;}}return 0;
}#define BLK_PWM_INDEX 2
#define PWM_MAX_PERIOD 40000
/* backlight setting */
#define MIN_LEVEL 0
#define MAX_LEVEL 255
#define DEFAULT_LEVEL 127static struct PanelInfo g_panelInfo = {.width = 720, /* width */.height = 1280, /* height */.hbp = 40, /* horizontal back porch */.hfp = 40, /* horizontal front porch */.hsw = 10, /* horizontal sync width */.vbp = 15, /* vertical back porch */.vfp = 10, /* vertical front porch */.vsw = 36, /* vertical sync width */.clockFreq = 75000000, /* clock */.pWidth = 68, /* physical width */.pHeight = 121, /* physical height */.connectorType = DRM_MODE_CONNECTOR_DPI, /* DRM_MODE_CONNECTOR_DPI=17 */.blk = { BLK_PWM, MIN_LEVEL, MAX_LEVEL, DEFAULT_LEVEL },
};static void PanelDataInit(struct panel_simple *panel, struct HdfDeviceObject *object)
{struct PanelData *panel_data = &panel->panel_data;panel_data->object = object;panel_data->init = PanelInit;panel_data->on = PanelOn;panel_data->off = PanelOff;panel_data->prepare = PanelPrepare;panel_data->unprepare = PanelUnprepare;panel_data->info = &g_panelInfo;panel_data->priv = panel;
}static int32_t PanelEntryInit(struct HdfDeviceObject *object)
{struct device_node *panelNode = NULL;struct platform_device *pdev = NULL;struct panel_simple *simplePanel = NULL;struct panel_desc *desc = NULL;int err;bool hasPanel = false;HDF_LOGI("PanelEntryInit");while((panelNode = of_find_compatible_node(panelNode, NULL, "simple-panel")) != NULL){hasPanel = true;pdev = of_find_device_by_node(panelNode);if (pdev == NULL) {HDF_LOGE("%s of_find_device_by_node fail", __func__);goto FAIL;}desc = (struct panel_desc *)OsalMemCalloc(sizeof(*desc));if (desc == NULL) {HDF_LOGE("%s panel_desc malloc fail", __func__);goto FAIL;}err = panel_simple_of_get_desc_data(&pdev->dev, desc);if (err) {HDF_LOGE("%s failed to get desc data: %d\n", __func__, err);goto FAIL;}simplePanel = (struct panel_simple *)OsalMemCalloc(sizeof(struct panel_simple));if (simplePanel == NULL) {HDF_LOGE("%s simplePanel malloc fail", __func__);goto FAIL;}simplePanel->desc = desc;simplePanel->panel.dev = &pdev->dev;err = panel_simple_probe(&pdev->dev, simplePanel);if (err) {HDF_LOGE("%s failed to panel_simple_probe: %d\n", __func__, err);goto FAIL;}PanelDataInit(simplePanel, object);if (RegisterPanel(&simplePanel->panel_data) != HDF_SUCCESS) {HDF_LOGE("RegisterPanel fail");goto FAIL;}of_node_put(panelNode);}if (!hasPanel) {HDF_LOGE("%s panel simple not found!!!", __func__);goto FAIL;}HDF_LOGI("%s success", __func__);return HDF_SUCCESS;
FAIL:OsalMemFree(desc);OsalMemFree(simplePanel);of_node_put(panelNode);return HDF_FAILURE;
}struct HdfDriverEntry g_commonPanelSimpleDevEntry = {.moduleVersion = 1,.moduleName = "PANEL_SIMPLE_COMMON",.Init = PanelEntryInit,
};HDF_INIT(g_commonPanelSimpleDevEntry);
2.3.2 在DRM显示框架中注册上一步获取到的Panel对象
添加如下两个文件:
hdf_drm_panel_simple.h
/** Copyright (c) 2020-2021 Huawei Device Co., Ltd.** HDF is dual licensed: you can use it either under the terms of* the GPL, or the BSD license, at your option.* See the LICENSE file in the root of this repository for complete details.*/#ifndef HDF_DRM_PANEL_SIMPLE_H
#define HDF_DRM_PANEL_SIMPLE_H
#include <linux/of_platform.h>
#include <linux/platform_device.h>#include <video/display_timing.h>
#include <video/mipi_display.h>
#include <video/of_display_timing.h>
#include <video/videomode.h>#include <drm/drm_crtc.h>
#include <drm/drm_device.h>
#include <drm/drm_mipi_dsi.h>
#include <drm/drm_panel.h>
#include <drm/drm_dsc.h>#include "hdf_disp.h"
#include "hdf_bl.h"struct panel_cmd_header {u8 data_type;u8 delay;u8 payload_length;
} __packed;struct panel_cmd_desc {struct panel_cmd_header header;u8 *payload;
};struct panel_cmd_seq {struct panel_cmd_desc *cmds;unsigned int cmd_cnt;
};/*** @modes: Pointer to array of fixed modes appropriate for this panel. If* only one mode then this can just be the address of this the mode.* NOTE: cannot be used with "timings" and also if this is specified* then you cannot override the mode in the device tree.* @num_modes: Number of elements in modes array.* @timings: Pointer to array of display timings. NOTE: cannot be used with* "modes" and also these will be used to validate a device tree* override if one is present.* @num_timings: Number of elements in timings array.* @bpc: Bits per color.* @size: Structure containing the physical size of this panel.* @delay: Structure containing various delay values for this panel.* @bus_format: See MEDIA_BUS_FMT_... defines.* @bus_flags: See DRM_BUS_FLAG_... defines.*/
struct panel_desc {const struct drm_display_mode *modes;unsigned int num_modes;const struct display_timing *timings;unsigned int num_timings;unsigned int bpc;/*** @width: width (in millimeters) of the panel's active display area* @height: height (in millimeters) of the panel's active display area*/struct {unsigned int width;unsigned int height;} size;/*** @prepare: the time (in milliseconds) that it takes for the panel to* become ready and start receiving video data* @hpd_absent_delay: Add this to the prepare delay if we know Hot* Plug Detect isn't used.* @enable: the time (in milliseconds) that it takes for the panel to* display the first valid frame after starting to receive* video data* @disable: the time (in milliseconds) that it takes for the panel to* turn the display off (no content is visible)* @unprepare: the time (in milliseconds) that it takes for the panel* to power itself down completely* @reset: the time (in milliseconds) that it takes for the panel* to reset itself completely* @init: the time (in milliseconds) that it takes for the panel to* send init command sequence after reset deassert*/struct {unsigned int prepare;unsigned int hpd_absent_delay;unsigned int enable;unsigned int disable;unsigned int unprepare;unsigned int reset;unsigned int init;} delay;u32 bus_format;u32 bus_flags;int connector_type;struct panel_cmd_seq *init_seq;struct panel_cmd_seq *exit_seq;
};struct panel_simple {struct drm_panel panel;struct mipi_dsi_device *dsi;bool prepared;bool enabled;bool power_invert;bool no_hpd;const struct panel_desc *desc;struct regulator *supply;struct gpio_desc *enable_gpio;struct gpio_desc *reset_gpio;struct gpio_desc *hpd_gpio;struct drm_display_mode override_mode;enum drm_panel_orientation orientation;struct PanelData panel_data;uint32_t index;struct DispManager *manager;
};#endif /* HDF_DRM_PANEL_SIMPLE_H */
hdf_drm_panel_simple.c
/*** HDF is dual licensed: you can use it either under the terms of* the GPL, or the BSD license, at your option.* See the LICENSE file in the root of this repository for complete details.*/#include "hdf_drm_panel_simple.h"
#include <drm/drm_device.h>
#include <drm/drm_atomic_helper.h>
#include <linux/backlight.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/pm_runtime.h>
#include <linux/regulator/consumer.h>
#include <video/mipi_display.h>
#include <video/of_display_timing.h>
#include <video/videomode.h>
#include <uapi/drm/drm_mode.h>
#include "osal_mem.h"
#include "osal.h"static inline struct panel_simple *to_panel_simple(const struct drm_panel *panel)
{return container_of(panel, struct panel_simple, panel);
}static int HdfDrmPanelSimpleUnprepare(struct drm_panel *panel)
{struct panel_simple *simpePanel = to_panel_simple(panel);struct PanelData *panelData;struct DispManager *manager = GetDispManager();HDF_LOGD("HdfDrmPanelSimpleUnprepare");if (simpePanel->index >= PANEL_MAX) {HDF_LOGE("panel num out of PANEL_MAX");return HDF_FAILURE;}OsalMutexLock(&manager->dispMutex);panelData = manager->panelManager->panel[simpePanel->index];panelData->unprepare(panelData);OsalMutexUnlock(&manager->dispMutex);return HDF_SUCCESS;
}static int HdfDrmPanelSimplePrepare(struct drm_panel *panel)
{struct panel_simple *simpePanel = to_panel_simple(panel);struct PanelData *panelData;struct DispManager *manager = GetDispManager();HDF_LOGD("HdfDrmPanelSimplePrepare");if (simpePanel->index >= PANEL_MAX) {HDF_LOGE("panel num out of PANEL_MAX");return HDF_FAILURE;}OsalMutexLock(&manager->dispMutex);panelData = manager->panelManager->panel[simpePanel->index];panelData->prepare(panelData);OsalMutexUnlock(&manager->dispMutex);return HDF_SUCCESS;
}static int HdfDrmPanelSimpleDisable(struct drm_panel *panel)
{struct panel_simple *simpePanel = to_panel_simple(panel);struct PanelData *panelData;struct DispManager *manager = GetDispManager();HDF_LOGD("HdfDrmPanelSimpleDisable");if (simpePanel->index >= PANEL_MAX) {HDF_LOGE("panel num out of PANEL_MAX");return HDF_FAILURE;}OsalMutexLock(&manager->dispMutex);panelData = manager->panelManager->panel[simpePanel->index];panelData->off(panelData);OsalMutexUnlock(&manager->dispMutex);return HDF_SUCCESS;
}static int HdfDrmPanelSimpleEnable(struct drm_panel *panel)
{struct panel_simple *simpePanel = to_panel_simple(panel);struct PanelData *panelData;struct DispManager *manager = GetDispManager();HDF_LOGD("HdfDrmPanelSimpleEnable");if (simpePanel->index >= PANEL_MAX) {HDF_LOGE("panel num out of PANEL_MAX");return HDF_FAILURE;}panelData = manager->panelManager->panel[simpePanel->index];OsalMutexLock(&manager->dispMutex);panelData->on(panelData);OsalMutexUnlock(&manager->dispMutex);return HDF_SUCCESS;
}#define MIN_LEVEL 0
#define MAX_LEVEL 255
#define DEFAULT_LEVEL 100static int32_t SetPanelInfo(struct panel_simple *simplePanel, struct drm_display_mode *mode)
{HDF_LOGI("SetPanelInfo enter");struct PanelData *panel = &simplePanel->panel_data;panel->info->clockFreq = mode->clock * 1000;panel->info->width = mode->hdisplay;panel->info->height = mode->vdisplay;panel->info->hbp = mode->htotal - mode->hsync_end;panel->info->hfp = mode->hsync_start - mode->hdisplay;panel->info->hsw = mode->hsync_end - mode->hsync_start;panel->info->vbp = mode->vtotal - mode->vsync_end;panel->info->vfp = mode->vsync_start - mode->vdisplay;panel->info->vsw = mode->vsync_end - mode->vsync_start;// panel->info->intfType = LCD_24BIT;// panel->info->intfSync = 0;// panel->info->frameRate = 0;panel->info->blk.type = BLK_PWM;panel->info->blk.minLevel = MIN_LEVEL;panel->info->blk.maxLevel = MAX_LEVEL;panel->info->blk.defLevel = DEFAULT_LEVEL;return HDF_SUCCESS;
}static unsigned int panel_simple_get_timings_modes(struct panel_simple *panel,struct drm_connector *connector)
{struct drm_display_mode *mode;unsigned int i, num = 0;for (i = 0; i < panel->desc->num_timings; i++) {const struct display_timing *dt = &panel->desc->timings[i];struct videomode vm;videomode_from_timing(dt, &vm);mode = drm_mode_create(connector->dev);if (!mode) {HDF_LOGE("failed to add mode %ux%u\n", dt->hactive.typ, dt->vactive.typ);continue;}drm_display_mode_from_videomode(&vm, mode);mode->type |= DRM_MODE_TYPE_DRIVER;if (panel->desc->num_timings == 1)mode->type |= DRM_MODE_TYPE_PREFERRED;drm_mode_probed_add(connector, mode);num++;HDF_LOGI("panel_simple_get_timings_modes SetPanelInfo:%d", i);SetPanelInfo(panel, mode);}return num;
}static unsigned int panel_simple_get_display_modes(struct panel_simple *panel,struct drm_connector *connector)
{struct drm_display_mode *mode;unsigned int i, num = 0;HDF_LOGI("panel_simple_get_display_modes enter");for (i = 0; i < panel->desc->num_modes; i++) {const struct drm_display_mode *m = &panel->desc->modes[i];mode = drm_mode_duplicate(connector->dev, m);if (!mode) {HDF_LOGE("failed to add mode %ux%u@%u\n",m->hdisplay, m->vdisplay,drm_mode_vrefresh(m));continue;}mode->type |= DRM_MODE_TYPE_DRIVER;if (panel->desc->num_modes == 1)mode->type |= DRM_MODE_TYPE_PREFERRED;drm_mode_set_name(mode);drm_mode_probed_add(connector, mode);num++;HDF_LOGI("panel_simple_get_display_modes SetPanelInfo:%d num:%d", i, num);SetPanelInfo(panel, mode);}return num;
}static int panel_simple_get_non_edid_modes(struct panel_simple *panel,struct drm_connector *connector)
{struct drm_display_mode *mode;bool has_override = panel->override_mode.type;unsigned int num = 0;if (!panel->desc)return 0;if (has_override) {mode = drm_mode_duplicate(connector->dev,&panel->override_mode);if (mode) {drm_mode_probed_add(connector, mode);num = 1;HDF_LOGI("panel_simple_get_non_edid_modes SetPanelInfo");SetPanelInfo(panel, mode);} else {HDF_LOGE("failed to add override mode\n");}}/* Only add timings if override was not there or failed to validate */if (num == 0 && panel->desc->num_timings)num = panel_simple_get_timings_modes(panel, connector);/** Only add fixed modes if timings/override added no mode.** We should only ever have either the display timings specified* or a fixed mode. Anything else is rather bogus.*/WARN_ON(panel->desc->num_timings && panel->desc->num_modes);if (num == 0)num = panel_simple_get_display_modes(panel, connector);if (panel->desc->bpc)connector->display_info.bpc = panel->desc->bpc;if (panel->desc->size.width)connector->display_info.width_mm = panel->desc->size.width;if (panel->desc->size.height)connector->display_info.height_mm = panel->desc->size.height;if (panel->desc->bus_format)drm_display_info_set_bus_formats(&connector->display_info,&panel->desc->bus_format, 1);if (panel->desc->bus_flags)connector->display_info.bus_flags = panel->desc->bus_flags;return num;
}static int HdfDrmPanelSimpleGetModes(struct drm_panel *panel, struct drm_connector *connector)
{struct panel_simple *p = to_panel_simple(panel);int num = 0;HDF_LOGI("HdfDrmPanelSimpleGetModes");if (panel == NULL) {HDF_LOGE("panel is NULL");return 0;}if (connector == NULL) {HDF_LOGE("connector is NULL");return 0;}// /* probe EDID if a DDC bus is available */// if (p->ddc) {// struct edid *edid = drm_get_edid(connector, p->ddc);// drm_connector_update_edid_property(connector, edid);// if (edid) {// num += drm_add_edid_modes(connector, edid);// kfree(edid);// }// }/* add hard-coded panel modes */num += panel_simple_get_non_edid_modes(p, connector);// /* set up connector's "panel orientation" property */// drm_connector_set_panel_orientation(connector, p->orientation);return num;
}static int HdfDrmPanelSimpleGetTimings(struct drm_panel *panel,unsigned int num_timings,struct display_timing *timings)
{struct panel_simple *p = to_panel_simple(panel);unsigned int i;if (p->desc->num_timings < num_timings)num_timings = p->desc->num_timings;if (timings)for (i = 0; i < num_timings; i++)timings[i] = p->desc->timings[i];return p->desc->num_timings;
}static struct drm_panel_funcs g_hdfDrmPanelFuncs = {.disable = HdfDrmPanelSimpleDisable,.unprepare = HdfDrmPanelSimpleUnprepare,.prepare = HdfDrmPanelSimplePrepare,.enable = HdfDrmPanelSimpleEnable,.get_modes = HdfDrmPanelSimpleGetModes,.get_timings = HdfDrmPanelSimpleGetTimings,
};static ssize_t SuspendStore(struct device *dev,struct device_attribute *attr, const char *buf, size_t count)
{int32_t ret;struct panel_simple *simpePanel = dev_get_drvdata(dev);ret = HdfDrmPanelSimpleDisable(&simpePanel->panel);if (ret != HDF_SUCCESS) {HDF_LOGE("%s HdfDrmPanelSimpleDisable fail", __func__);return count;}ret = HdfDrmPanelSimpleUnprepare(&simpePanel->panel);if (ret != HDF_SUCCESS) {HDF_LOGE("%s HdfDrmPanelSimpleUnprepare fail", __func__);return count;}return count;
}
static DEVICE_ATTR(suspend, S_IWUSR, NULL, SuspendStore);static ssize_t ResumeStore(struct device *dev,struct device_attribute *attr, const char *buf, size_t count)
{int32_t ret;struct panel_simple *simpePanel = dev_get_drvdata(dev);ret = HdfDrmPanelSimplePrepare(&simpePanel->panel);if (ret != HDF_SUCCESS) {HDF_LOGE("%s HdfDrmPanelSimplePrepare fail", __func__);return count;}ret = HdfDrmPanelSimpleEnable(&simpePanel->panel);if (ret != HDF_SUCCESS) {HDF_LOGE("%s HdfDrmPanelSimpleEnable fail", __func__);return count;}return count;
}
static DEVICE_ATTR(resume, S_IWUSR, NULL, ResumeStore);static ssize_t BacklightStore(struct device *dev,struct device_attribute *attr, const char *buf, size_t count)
{int32_t ret;unsigned long level;struct PanelData *panelData = NULL;struct panel_simple *simpePanel = dev_get_drvdata(dev);struct DispManager *manager = GetDispManager();ret = kstrtoul(buf, 0, &level);if (ret != 0) {return ret;}HDF_LOGI("%s enter", __func__);OsalMutexLock(&manager->dispMutex);panelData = manager->panelManager->panel[simpePanel->index];OsalMutexUnlock(&manager->dispMutex);ret = UpdateBrightness(panelData->blDev, level);if (ret != HDF_SUCCESS) {HDF_LOGE("%s UpdateBrightness fail", __func__);}return count;
}
static DEVICE_ATTR(backlight, S_IWUSR, NULL, BacklightStore);#define ATTR_NUM 3
static struct device_attribute *g_panelAttrs[] = {&dev_attr_suspend,&dev_attr_resume,&dev_attr_backlight,NULL,
};static int32_t HdfDrmPanelSimpleEntryInit(struct HdfDeviceObject *object)
{(void)object;uint32_t ret;uint32_t i;uint32_t j;uint32_t panelNum;struct panel_simple *simplePanel = NULL;struct DispManager *manager = NULL;struct drm_panel *panel = NULL;struct device *dev = NULL;manager = GetDispManager();if (manager == NULL) {HDF_LOGE("%s manager is null", __func__);return HDF_FAILURE;}panelNum = manager->panelManager->panelNum;for (i = 0; i < panelNum; i++) {simplePanel = (struct panel_simple *)manager->panelManager->panel[i]->priv;simplePanel->index = i;panel = &simplePanel->panel;dev = panel->dev;drm_panel_init(panel, dev, &g_hdfDrmPanelFuncs, simplePanel->desc->connector_type);ret = drm_panel_of_backlight(panel);if (ret){HDF_LOGE("%s drm_panel_of_backlight failed %d", __func__, ret);drm_panel_remove(panel);return ret;}drm_panel_add(panel);dev_set_drvdata(dev, simplePanel);for (j = 0; j < ATTR_NUM; j++) {if (device_create_file(dev, g_panelAttrs[j]) != 0) {HDF_LOGE("%s line = %d device_create_file fail", __func__, __LINE__);}}HDF_LOGI("%s panel[%d] registered success", __func__, i);}HDF_LOGI("%s success", __func__);return HDF_SUCCESS;
}struct HdfDriverEntry g_hdfDrmPanelSimpleEntry = {.moduleVersion = 1,.moduleName = "HDF_DRM_PANEL_SIMPLE",.Init = HdfDrmPanelSimpleEntryInit,
};HDF_INIT(g_hdfDrmPanelSimpleEntry);
2.3.3 修改panel驱动的编译配置文件
前两步添加的文件需要加到编译配置文件里面才可以进行编译,如下:
Makefile
2.3.4 修改device_info驱动配置文件
device_info驱动配置文件原来加载的是MIPI屏幕驱动,这里我们改成加载我们编写的LVDS屏幕驱动,如下:
device_info.hcs
3. 编译运行
3.1 编译我们修改过的源码
./build.sh --product-name ok3568 --target-cpu arm64
3.2 烧录镜像文件并运行查看效果
4. 可能遇到的问题
4.1 pwm背光调节没有反应
原因: RK3568背光功能默认使用原生pwm驱动,并且pwm路径已经写死了,如下:
解决办法: 找到设备树里面lvds屏幕所使用的 “pwm-backlight” 节点,修改节点名字为backlight,如下
或者修改drm_connector.cpp文件中的背光调节功能的文件路径。
5. 参考
标准系统方案之瑞芯微RK3568移植案例
相关文章:

OpenHarmony4.0适配LVDS屏幕驱动
1.概述 手头有一块RK3568的开发板OK3568-C,但是还没有适配OpenHarmony,用的还是LVDS屏幕,但是官方和网上好像还没有OpenHarmony4.0的LVDS屏幕驱动的通用实现,所以决定尝试了一下适配该开发板,完成LVDS屏幕驱动的适配&…...

【playwright】新一代自动化测试神器playwright+python系列课程01-playwright驱动浏览器
Playwright驱动浏览器 安装 Playwright 时,Playwright默认自动安装了三种浏览器(Chromium、Firefox 和 WebKit)。我们可以驱动这三种浏览器中的任意一种。 使用with上下文管理器 启动chromium浏览器 python # # author: 测试-老姜 交流…...

POSIX API与网络协议栈
本文介绍linux中与tcp网络通信相关的POSIX API,在每次调用的时候,网络协议栈会进行的操作与记录。 POSIX API Posix API,提供了统一的接口,使程序能得以在不同的系统上运行。简单来说不同的操作系统进行同一个活动,比…...

互联网加竞赛 基于卷积神经网络的乳腺癌分类 深度学习 医学图像
文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度,召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…...

腾讯云 IPv6 解决方案
产品矩阵全覆盖 腾讯云全线产品 All in IPv6;云服务器、私有网络、负载均衡、内容分发、域名解析、DDoS 高防等都已支持 IPv6。 全球 IPv6 基础设施 腾讯云在全球开放25个地理区域,运营53个可用区;目前已有多个地域提供 IPv6 接入能力。 …...

Appium 自动化测试
1.Appium介绍 1,appium是开源的移动端自动化测试框架; 2,appium可以测试原生的、混合的、以及移动端的web项目; 3,appium可以测试ios,android应用(当然了,还有firefoxos)…...

深入浅出Android dmabuf_dump工具
目录 dmabuf是什么? dmabuf_dump工具介绍(基于Android 14) Android.bp dmabuf_dump.cpp 整体架构结构如下 dmabuf_dump主要包含以下功能 前置背景知识 fdinfo 思考 bufinfo Dump整个手机系统的dmabuf Dump某个进程的dmabuf 以Table[buff…...

Guava RateLimiter预热模型
本文已收录至我的个人网站:程序员波特,主要记录Java相关技术系列教程,共享电子书、Java学习路线、视频教程、简历模板和面试题等学习资源,让想要学习的你,不再迷茫。 什么是流量预热 我们都知道在做运动之前先得来几组…...

【搭建个人知识库-3】
搭建个人知识库-3 1 大模型开发范式1.1 RAG原理1.2 LangChain框架1.3 构建向量数据库1.4 构建知识库助手1.5 Web Demo部署 2 动手实践2.1 环境配置2.2 知识库搭建2.2.1 数据收集2.2.2 加载数据2.2.3 构建向量数据库 2.3 InternLM接入LangChain2.4 构建检索问答链1 加载向量数据…...

如何看待 Linux 内核邮件列表重启将内核中的 C 代码转换为 C++
如何看待 Linux 内核邮件列表重启将内核中的 C 代码转换为 C 的讨论? 在开始前我有一些资料,是我根据网友给的问题精心整理了一份「Linux的资料从专业入门到高级教程」, 点个关注在评论区回复“888”之后私信回复“888”,全部无偿…...

springboot网关添加swagger
添加依赖 <dependency><groupId>com.spring4all</groupId><artifactId>swagger-spring-boot-starter</artifactId><version>2.0.2</version></dependency>添加配置类,与服务启动类同一个层级 地址:http…...

代码随想录 Leetcode383. 赎金信
题目: 代码(首刷自解 2024年1月15日): class Solution { public:bool canConstruct(string ransomNote, string magazine) {vector<int> v(26);for(auto letter : magazine) {v[letter - a];}for(auto letter : ransomNote…...

上下左右视频转场模板PR项目工程文件 Vol. 05
pr转场模板,视频画面上下左右转场后带有一点点回弹效果的PR项目工程模板 Vol. 05 项目特点: 回弹效果视频转场; Premiere Pro 2020及以上; 适用于照片和视频转场; 适用于任何FPS和分辨率; 视频教程。 PR转场…...

【正点原子STM32连载】第三十三章 单通道ADC采集实验 摘自【正点原子】APM32E103最小系统板使用指南
1)实验平台:正点原子APM32E103最小系统板 2)平台购买地址:https://detail.tmall.com/item.htm?id609294757420 3)全套实验源码手册视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban 第三…...

Linux系统使用docker部署Geoserver(简单粗暴,复制即用)
1、拉取镜像 docker pull kartoza/geoserver:2.20.32、创建数据挂载目录 # 统一管理Docker容器的数据文件,geoserver mkdir -p /mydata/geoserver# 创建geoserver的挂载数据目录 mkdir -p /mydata/geoserver/data_dir# 创建geoserver的挂载数据目录,存放shp数据 m…...

libcurl使用默认编译的winssl进行https的双向认证
双向认证: 1.服务器回验证客户端上报的证书 2.客户端回验证服务器的证书 而证书一般分为:1.受信任的根证书,2不受信任的根证书。 但是由于各种限制不想在libcurl中增加openssl,那么使用默认的winssl也可以完成以上两种证书的双…...

MySQL运维实战(3.3) 管理数据库(database)
作者:俊达 引言 数据库的创建和管理是构建可靠数据的关键,关系到所存储数据的安全与稳定。在 MySQL 这个强大的关系型数据库系统中,数据库的创建与管理需要精准的步骤和妥善的配置。下面,将深入探讨如何使用MySQL 来管理数据库&…...

Web3去中心化存储:重新定义云服务
随着Web3技术的崭露头角,去中心化存储正在成为数字时代云服务的全新范式。传统的云服务依赖于中心化的数据存储架构,而Web3的去中心化存储则为用户带来了更安全、更隐私、更可靠的数据管理方式,重新定义了云服务的未来。 1.摒弃中心化的弊端 …...

纸尿裤行业调研:预计到2024年提高至68.1%
母婴大消费是指围绕孕产妇和0-14岁婴幼童人群,贯穿孕产妇孕产及产后护理周期、婴幼童成长周期的满足其衣、食、住、行、用、玩、教等需求的消费品的总和。 不同产品消费频次各异,纸尿裤是母婴大消费中的最为高频且刚需的易耗品。当前,消费升…...

目标检测数据集 - 行人检测数据集下载「包含VOC、COCO、YOLO三种格式」
数据集介绍:行人检测数据集,真实场景高质量图片数据,涉及场景丰富,比如校园行人、街景行人、道路行人、遮挡行人、严重遮挡行人数据;适用实际项目应用:公共场所监控场景下行人检测项目,以及作为…...

重磅!巨匠纺品鉴正式签约“体坛冠军程晨”为品牌形象代言人
2024年,巨匠纺品鉴打响品牌营销开年第一战,携手全国啦啦操冠军程晨,强势开启“冠军品牌、冠军优选、冠军品质”中国年,实现品牌战略全面升级,全力传递"冠军品质"的品牌精神,拓展品牌影响力的深度和广度,为品…...

亚信安慧AntDB超融合框架——数智化时代数据库管理的新里程碑
在信息科技飞速发展的时代,亚信科技AntDB团队提出了一项颠覆性的“超融合”理念,旨在满足企业日益增长的复杂混合负载和多样化数据类型的业务需求。这一创新性框架的核心思想在于融合多引擎和多能力,充分发挥分布式数据库引擎的架构优势&…...

设计模式之命令模式【行为型模式】
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档> 学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某…...

肯尼斯·里科《C和指针》第6章 指针(4)实例
肯尼斯里科《C和指针》第6章 指针(1)-CSDN博客 肯尼斯里科《C和指针》第6章 指针(2)-CSDN博客 肯尼斯里科《C和指针》第6章 指针(3)-CSDN博客 6.12 实例 /* ** 计算一个字符串的长度。 */ #include <…...

diffusers flask streamlit 简洁可视化文生图页面
参考: https://python-bloggers.com/2022/12/stable-diffusion-application-with-streamlit/ https://github.com/LowinLi/stable-diffusion-streamlit 项目结构 本项目很简洁,暂时每次只能返回一张图片;gpu资源T4 16g;测试下来基本也只能支持同时一个人使用 flask:作为…...

ubuntu 使用VNC链接树莓派
ubuntu PC端安装remina sudo apt-add-repository ppa:remmina-ppa-team/remmina-next 然后,运行以下命令来安装 Remmina 软件包: sudo apt update sudo apt install remmina remmina-plugin-rdp remmina-plugin-secret flatpak run -- pkill remmina p…...

水利部:加大北斗、无人机等安全监测新技术的应用推广
水利部:加大北斗、无人机等安全监测新技术的应用推广 近日,水利部召开2023年水库安全管理情况新闻发布会。会上,副部长刘伟平介绍有关情况,并与水利工程建设司、运行管理司、水旱灾害防御司负责人回答记者提问。 为了高质量…...

如何定位和优化程序CPU、内存等性能之巅
摘要 性能优化指在不影响系统运行正确性的前提下,使之运行得更快,完成特定功能所需的时间更短,或拥有更强大的服务能力。本文将介绍性能优化的基本概念以及如何定位和优化程序中的CPU、内存和IO瓶颈。 引言 随着计算机系统的日益复杂和应用…...

一体机旅游景区污水处理设备工艺说明
一体机旅游景区污水处理设备工艺说明 原水浓度:COD≤500mg/L,BOD≤300mg/L,NH3-N≤40mg/L,超过以上浓度需另行设计。 出水标准:COD≤60mg/L,BOD≤20mg/L,NH3-N≤15mg/L,出水要求如更…...

java返回文件时为图片或pdf等设置在线预览或下载
设置Content-Disposition响应头类型 "inline"查看预览 ; "attachment"下载; inline:表示回复中的消息体会以页面的一部分或者整个页面的形式展示 attchment:以附件形式被下载到本地;/*** 文件或图…...