1. 前言
之前在网上淘了一个mipi屏幕型号是bv050fwm。正好研究一下linux对 mipi dsi屏幕驱动方式。
2. 硬件驱动板
立创开源广场有大佬已经做了 驱动板:https://oshwhub.com/5473675a/28-yuan-make-5inch-touch-display
这里详细说明一些电路硬件部分,方便后续设备树的编写,15pin引脚为标准的树莓派mipi dsi接口。其内部定义:
| 引脚号 |
功能 |
| 1 |
3V3 |
| 2 |
3V3 |
| 3 |
GND |
| 4 |
I2C_SDA |
| 5 |
I2C_SCL |
| 6 |
GND |
| 7 |
MIPI_DSI_D0P |
| 8 |
MIPI_DSI_D0N |
| 9 |
GND |
| 10 |
MIPI_DSI_CLKP |
| 11 |
MIPI_DSI_CLKN |
| 12 |
GND |
| 13 |
MIPI_DSI_D1P |
| 14 |
MIPI_DSI_D1N |
| 15 |
GND |
可以看到除了 mipi 2line 线以外,还有个I2C接口,然后他们中间用GND分割用于屏蔽串扰。
驱动板还可以驱动触控,不过这里我只关注屏幕显示部分,所以不介绍此部分。
屏幕驱动 需要RST引脚,但15pin mipi中没有这一部分,所以电路借助了一个 PCA9536 芯片,用于拓展IO口(I2C->4 GPIO), 其中 俩路BL_EN (背光引脚), VCI_EN(屏幕使能引脚), LCD_RST_L(屏幕rest引脚)。
关于屏幕背光部分:
引脚定义
其pwm引脚是一个 输出引脚,所以调光的原理,是主机通过mipi协议想屏幕驱动板发送数据然后此引脚会输出一个pwm波,用于调控屏幕背光, AP3019AKTR芯片接收pwm波,然后对VCC_LED_A, VCC_LED_K输出高电压。
驱动板预留了,BL_EN 与BL_PWM短接的的位置,只需要用跳线冒短接,在driver中将其设置为高电平,相当于PWM占空比100%即屏幕最亮。
3. 树莓派上驱动此屏幕
3.1 编译设备树
对于树莓派要知道此硬件设备,需要在设备树中配置。
树莓派的 dtoverlay(设备树覆盖)机制,是一种在不修改内核主设备树文件(DTB)的前提下,动态地向系统添加或修改硬件描述的“热插拔”方案 。
所以这里使用dtoverlay的配置方式,进行配置屏幕的设备树:
vi boe-bv050fwm.dts
// SPDX-License-Identifier: GPL-2.0-only
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2837";
fragment@0 {
target-path = "/soc/i2c0mux/i2c@1";
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
pca9536: gpio@41 {
compatible = "nxp,pca9536";
reg = <0x41>;
gpio-controller;
#gpio-cells = <2>;
status = "okay";
gpio-line-names = "BL_EN", "VCI_EN", "LCD_RST", "TP_RST";
};
};
};
fragment@1 {
target = <&dsi1>;
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
panel: panel@0 {
status = "okay";
compatible = "boe,bv050fwm";
reg = <0>;
vci-gpios = <&pca9536 1 0>;
reset-gpios = <&pca9536 2 1>;
backlight-gpios = <&pca9536 0 0>;
port {
panel_in: endpoint {
remote-endpoint = <&dsi1_out>;
};
};
};
port {
dsi1_out: endpoint {
remote-endpoint = <&panel_in>;
data-lanes = <0 1>;
};
};
};
};
fragment@2 {
target = <&spidev0>;
__overlay__ {
status = "disabled";
};
};
};
然后将其进行编译,并放到/boot/overlays/ 目录下:
dtc -@ -I dts -O dtb -o /boot/overlays/panel_boe_bv050fwm.dtbo boe-bv050fwm.dts
这里简单补充一下几个信息:
- 关于I2C通道,在树莓派3b使用mux(多路复用器), 如果直接指定I2C物理总线,会导致在调用pca9536模块进行i2c通信没有响应,直到timeout从而影响 屏幕驱动的reset vci backlight引脚,从而导致驱动屏幕失败,这个也是我在调试设备树遇到的最复杂的问题,所以这里指定的是"/soc/i2c0mux/i2c@1" 虚拟I2C通道,当mux检测到使用虚拟通道1时会自动进行切换通信,这样不需要每次进行唤醒切换。
- I2C总线下挂载pca9536 模块,其给屏幕提供 reset vci backlight引脚。
- panel面板中没有定义时序,时序有driver中进行定义,其会加载bv050fwm(驱动名字)。
在设备树编译完成后,在 /boot/firmware/config.txt 中使用 panel_boe_bv050fwm.dtbo:
dtoverlay=panel_boe_bv050fwm
3.2 编译屏幕驱动
在编译内核驱动前,需要安装内核头文件:
sudo apt install linux-headers-$(uname -r) -y
touch panel-boe-bv050fwm.c
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2019, Michael Srba
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/regulator/consumer.h>
#include <linux/backlight.h>
#include <video/mipi_display.h>
#include <drm/drm_mipi_dsi.h>
#include <drm/drm_modes.h>
#include <drm/drm_panel.h>
struct bv050fwm {
struct drm_panel panel;
struct mipi_dsi_device *dsi;
struct regulator_bulk_data supplies[1];
struct gpio_desc *vci_gpio;
struct gpio_desc *reset_gpio;
struct gpio_desc *backlight_gpio;
bool prepared;
};
static inline struct
bv050fwm *to_bv050fwm(struct drm_panel *panel)
{
return container_of(panel, struct bv050fwm, panel);
}
static void bv050fwm_reset(struct bv050fwm *ctx)
{
struct mipi_dsi_device *dsi = ctx->dsi;
struct device *dev = &dsi->dev;
gpiod_set_value_cansleep(ctx->backlight_gpio, 1);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
usleep_range(1000, 2000);
gpiod_set_value_cansleep(ctx->reset_gpio, 1);
usleep_range(5000, 6000);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
usleep_range(10000, 11000);
}
static int bv050fwm_on(struct bv050fwm *ctx)
{
struct mipi_dsi_device *dsi = ctx->dsi;
struct device *dev = &dsi->dev;
int ret;
dsi->mode_flags |= MIPI_DSI_MODE_LPM;
dev_info(dev, "bv050fwm_on\n");
mipi_dsi_dcs_write_seq(dsi, 0x11, 0x00); // SLPOUT
msleep(120);
mipi_dsi_dcs_write_seq(dsi, 0x29, 0x00); // DISPON
mipi_dsi_dcs_write_seq(dsi, 0xFF, 0x80, 0x00, 0x00);
msleep(10);
mipi_dsi_dcs_write_seq(dsi, 0xFF, 0x80, 0x19, 0x01);
mipi_dsi_dcs_write_seq(dsi, 0x00, 0x80);
mipi_dsi_dcs_write_seq(dsi, 0xFF, 0x80, 0x19);
mipi_dsi_dcs_write_seq(dsi, 0x00, 0xB0);
mipi_dsi_dcs_write_seq(dsi, 0xCA, 0xF0);
mipi_dsi_dcs_write_seq(dsi, 0x00, 0x00);
mipi_dsi_dcs_write_seq(dsi, 0xFB, 0x00);
msleep(50);
mipi_dsi_dcs_write_seq(dsi, 0x00, 0x00);
mipi_dsi_dcs_write_seq(dsi, 0xFF, 0xFF, 0xFF, 0xFF);
mipi_dsi_dcs_write_seq(dsi, 0x51, 0x80);
mipi_dsi_dcs_write_seq(dsi, 0x53, 0x24);
mipi_dsi_dcs_write_seq(dsi, 0x55, 0x01);
mipi_dsi_dcs_write_seq(dsi, 0x5E, 0x28);
ret = mipi_dsi_dcs_exit_sleep_mode(dsi);
if (ret < 0) {
dev_err(dev, "Failed to exit sleep mode: %d\n", ret);
return ret;
}
msleep(120);
ret = mipi_dsi_dcs_set_display_on(dsi);
if (ret < 0) {
dev_err(dev, "Failed to set display on: %d\n", ret);
return ret;
}
msleep(20);
dev_info(dev, "bv050fwm_on end\n");
return 0;
}
static int bv050fwm_off(struct bv050fwm *ctx)
{
struct mipi_dsi_device *dsi = ctx->dsi;
struct device *dev = &dsi->dev;
int ret;
dsi->mode_flags &= ~MIPI_DSI_MODE_LPM;
ret = mipi_dsi_dcs_set_display_off(dsi);
if (ret < 0) {
dev_err(dev, "Failed to set display off: %d\n", ret);
return ret;
}
msleep(20);
ret = mipi_dsi_dcs_enter_sleep_mode(dsi);
if (ret < 0) {
dev_err(dev, "Failed to enter sleep mode: %d\n", ret);
return ret;
}
msleep(120);
return 0;
}
static int bv050fwm_prepare(struct drm_panel *panel)
{
struct bv050fwm *ctx = to_bv050fwm(panel);
struct device *dev = &ctx->dsi->dev;
int ret;
if (ctx->prepared)
return 0;
// regulator_bulk_disable(ARRAY_SIZE(ctx->supplies),
// ctx->supplies);
// msleep(1000);
ret = regulator_bulk_enable(ARRAY_SIZE(ctx->supplies), ctx->supplies);
if (ret < 0) {
dev_err(dev, "Failed to enable regulators: %d\n", ret);
return ret;
}
bv050fwm_reset(ctx);
ret = bv050fwm_on(ctx);
if (ret < 0) {
dev_err(dev, "Failed to initialize panel: %d\n", ret);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
regulator_bulk_disable(ARRAY_SIZE(ctx->supplies),
ctx->supplies);
return ret;
}
ctx->prepared = true;
return 0;
}
static int bv050fwm_unprepare(struct drm_panel *panel)
{
struct bv050fwm *ctx = to_bv050fwm(panel);
struct device *dev = &ctx->dsi->dev;
int ret;
if (!ctx->prepared)
return 0;
ret = bv050fwm_off(ctx);
if (ret < 0)
dev_err(dev, "Failed to un-initialize panel: %d\n", ret);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
regulator_bulk_disable(ARRAY_SIZE(ctx->supplies), ctx->supplies);
ctx->prepared = false;
return 0;
}
/* Backlight control code */
static int bv050fwm_bl_update_status(struct backlight_device *bl)
{
struct mipi_dsi_device *dsi = bl_get_data(bl);
struct device *dev = &bl->dev;
u8 brightness = backlight_get_brightness(bl) & 0xff;
//mipi_dsi_dcs_write_seq(dsi, 0x15, 0x00, 0x02, 0x51, 0xEF);
//mipi_dsi_dcs_write_seq(dsi, 0x51, brightness);
//mipi_dsi_dcs_write(dsi, 0x51, &brightness, 1);
mipi_dsi_dcs_set_display_brightness(dsi, brightness);
dev_info(dev, "boe_bl_update_status: brightness = %u\n", brightness);
return 0;
}
static int bv050fwm_bl_get_brightness(struct backlight_device *bl)
{
struct mipi_dsi_device *dsi = bl_get_data(bl);
struct device *dev = &bl->dev;
u16 brightness = 0;
int ret;
ret = mipi_dsi_dcs_get_display_brightness(dsi, &brightness);
if (ret < 0) {
dev_err(&bl->dev, "Failed to get display brightness: %d\n", ret);
return ret;
}
dev_info(&bl->dev, "Read brightness: %u (0x%x)\n", brightness, brightness);
return brightness & 0xff;
}
static const struct backlight_ops bv050fwm_bl_ops = {
.update_status = bv050fwm_bl_update_status,
.get_brightness = bv050fwm_bl_get_brightness,
};
static struct backlight_device *
bv050fwm_create_backlight(struct mipi_dsi_device *dsi)
{
struct device *dev = &dsi->dev;
const struct backlight_properties props = {
.type = BACKLIGHT_RAW,
.brightness = 255,
.max_brightness = 255,
};
return devm_backlight_device_register(dev, dev_name(dev), dev, dsi,
&bv050fwm_bl_ops, &props);
}
static const struct drm_display_mode bv050fwm_mode_42hz = {
.clock = 25200, // 时钟频率,单位是 kHz
.hdisplay = 480, // 水平可视区域
.hsync_start = 480 + 92, // 水平同步起始 = hactive + hfront-porch
.hsync_end = 480 + 92 + 12, // 水平同步结束 = hsync_start + hsync-len
.htotal = 480 + 92 + 12 + 88, // 水平总长度 = hactive + hfront + hsync-len + hback-porch
.vdisplay = 854, // 垂直可视区域
.vsync_start = 854 + 18, // 垂直同步起始 = vactive + vfront-porch
.vsync_end = 854 + 18 + 4, // 垂直同步结束 = vsync_start + vsync-len
.vtotal = 854 + 18 + 4 + 18, // 垂直总长度 = vactive + vfront-porch + vsync-len + vback-porch
.width_mm = 62,
.height_mm = 110,
};
static const struct drm_display_mode bv050fwm_mode_60hz = {
.clock = 36000, // 或 36046
.hdisplay = 480,
.hsync_start = 480 + 92,
.hsync_end = 480 + 92 + 12,
.htotal = 480 + 92 + 12 + 88,
.vdisplay = 854,
.vsync_start = 854 + 18,
.vsync_end = 854 + 18 + 4,
.vtotal = 854 + 18 + 4 + 18,
.width_mm = 62,
.height_mm = 110,
};
static int bv050fwm_get_modes(struct drm_panel *panel,
struct drm_connector *connector)
{
struct drm_display_mode *mode;
mode = drm_mode_duplicate(connector->dev, &bv050fwm_mode_42hz);
if (!mode) {
dev_err(panel->dev, "failed to add mode\n");
return -ENOMEM;
}
drm_mode_set_name(mode);
mode->type = DRM_MODE_TYPE_DRIVER;
connector->display_info.width_mm = mode->width_mm;
connector->display_info.height_mm = mode->height_mm;
drm_mode_probed_add(connector, mode);
mode = drm_mode_duplicate(connector->dev, &bv050fwm_mode_60hz);
if (!mode) {
dev_err(panel->dev, "failed to add mode\n");
return -ENOMEM;
}
drm_mode_set_name(mode);
mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
connector->display_info.width_mm = mode->width_mm;
connector->display_info.height_mm = mode->height_mm;
drm_mode_probed_add(connector, mode);
return 1;
}
static const struct drm_panel_funcs bv050fwm_panel_funcs = {
.unprepare = bv050fwm_unprepare,
.prepare = bv050fwm_prepare,
.get_modes = bv050fwm_get_modes,
};
static int bv050fwm_probe(struct mipi_dsi_device *dsi)
{
struct device *dev = &dsi->dev;
struct bv050fwm *ctx;
int ret;
// dev_info(dev, "bv050fwm module compiled on: %s %s\n", __DATE__, __TIME__);
ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
ctx->supplies[0].supply = "power";
ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(ctx->supplies),
ctx->supplies);
if (ret < 0) {
dev_err(dev, "Failed to get regulators: %d\n", ret);
return ret;
}
ctx->vci_gpio = devm_gpiod_get(dev, "vci", GPIOD_OUT_HIGH);
if (IS_ERR(ctx->vci_gpio)) {
ret = PTR_ERR(ctx->vci_gpio);
dev_err(dev, "Failed to get vci-gpios: %d\n", ret);
return ret;
}
gpiod_set_value_cansleep(ctx->vci_gpio, 1);
ctx->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(ctx->reset_gpio)) {
ret = PTR_ERR(ctx->reset_gpio);
dev_err(dev, "Failed to get reset-gpios: %d\n", ret);
return ret;
}
ctx->backlight_gpio = devm_gpiod_get(dev, "backlight", GPIOD_OUT_HIGH);
if (IS_ERR(ctx->backlight_gpio)) {
ret = PTR_ERR(ctx->backlight_gpio);
dev_err(dev, "Failed to get backlight-gpios: %d\n", ret);
return ret;
}
ctx->dsi = dsi;
mipi_dsi_set_drvdata(dsi, ctx);
dsi->lanes = 2;
dsi->format = MIPI_DSI_FMT_RGB888;
dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST | MIPI_DSI_MODE_LPM;
drm_panel_init(&ctx->panel, dev, &bv050fwm_panel_funcs,
DRM_MODE_CONNECTOR_DSI);
dev_info(dev, "drm_panel_init ok\n");
ctx->panel.prepare_prev_first = true;
ret = drm_panel_of_backlight(&ctx->panel);
if (ret)
{
dev_err(dev, "Failed to set backlight: %d\n", ret);
return ret;
}
// 使用mipi dcs调光
ctx->panel.backlight = bv050fwm_create_backlight(dsi);
if (IS_ERR(ctx->panel.backlight))
return dev_err_probe(dev, PTR_ERR(ctx->panel.backlight),
"Failed to create backlight\n");
drm_panel_add(&ctx->panel);
dev_info(dev, "drm_panel_add ok\n");
ret = mipi_dsi_attach(dsi);
if (ret < 0) {
dev_err(dev, "Failed to attach to DSI host: %d\n", ret);
drm_panel_remove(&ctx->panel);
return ret;
}
dev_info(dev, "mipi_dsi_attach ok\n");
return 0;
}
static void bv050fwm_remove(struct mipi_dsi_device *dsi)
{
struct bv050fwm *ctx = mipi_dsi_get_drvdata(dsi);
int ret;
ret = mipi_dsi_detach(dsi);
if (ret < 0)
dev_err(&dsi->dev, "Failed to detach from DSI host: %d\n", ret);
drm_panel_remove(&ctx->panel);
}
static const struct of_device_id bv050fwm_of_match[] = {
{ .compatible = "boe,bv050fwm" },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, bv050fwm_of_match);
static struct mipi_dsi_driver bv050fwm_driver = {
.probe = bv050fwm_probe,
.remove = bv050fwm_remove,
.driver = {
.name = "panel-boe-bv050fwm",
.of_match_table = bv050fwm_of_match,
},
};
module_mipi_dsi_driver(bv050fwm_driver);
MODULE_DESCRIPTION("MIPI-DSI based Panel Driver for bv050fwm LCD Display Module");
MODULE_LICENSE("GPL");
编写Makefile文件:
touch Makefile
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
obj-m := panel-boe-bv050fwm.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
开始编译driver:
make
将驱动放到对应目录,并更新驱动模块依赖关系:
cp panel-boe-bv050fwm.ko /lib/modules/$(uname -r)/kernel/drivers/gpu/drm/panel/
depmod -a
update-initramfs -u -c -k $(uname -r)
- 关于驱动部分,我修改了60hz时序的问题, 其
hback-porch 部分不太正确,导致我屏幕默认图形化显示使用60hz一直黑屏显示。
- 关于驱动的初始化部分,寄存器命令也进行了缩减,只保留必要部分。pwm调光部分也进行了解决,我发现其默认的pwm频率为 6.7khz,而 通过 https://github.com/HUAWEI-Y550-Y635/android_kernel_huawei_msm8916/blob/02eea41e4291a3b71c2bc574bae83906ebcf21ca/arch/arm/boot/dts/qcom/huawei_y635_l21_va/hw-panel-boe-otm8019a-5p0-fwvga-video.dtsi#L19
初始化后会变为 28khz。
其对与 屏幕驱动板的 AP3019AKTR 芯片来说 超过3khz的pwm会有异常: 亮度调节回由特别暗变为特别亮,怀疑是电路中的寄生电容导致的高频pwm有影响(尝试加入上拉电阻无效),中间无过度值。所有我花费很长时间去研究如何修改PWM频率,而数据手册OTM8019A 提供的PWM 5个参数对此无效。
无奈我采用二分法进行查找发现(每次都需要更新内核模块并重启、很费时间,因为rmmod内核模块会导致崩溃这个是更复杂的一个问题了):
mipi_dsi_dcs_write_seq(dsi, 0x00, 0xB0);
mipi_dsi_dcs_write_seq(dsi, 0xCA, 0xF0);
可以调节频率,看数据手册好像是CABC对应的阈值,懒得研究了,调试了整整二天一宿,其对应的频率为:255/ 0xF0 = 1.36 KHz
通过这个寄存器设置降低了PWM的频率可以实现完美调光了。
echo 100 > /sys/class/backlight/3f700000.dsi.0/brightness #亮度值为9-250(dts定义的)
注:此pwm为屏幕芯片内部产生无需连接树莓派GPIO, 跳线帽短接 BL_PWM 和 LCD_BL_PWM
3.3 配置config.txt cmdline.txt
vi /boot/firmware/config.txt
加入
dtparam=i2c_vc=on
dtoverlay=vc4-kms-v3d
#disable_fw_kms_setup=1
enable_uart=1
dtoverlay=panel_boe_bv050fwm
display_default_lcd=1
display_auto_detect=0
drm.debug=0x1e
dtdebug=1 #开启设备树调试信息
gpu_mem=256
# 下面这几项为了解决冷重启 图片跃出屏幕问题
hdmi_force_hotplug=1
hdmi_group=2
hdmi_mode=87
hdmi_cvt=480 854 60 3 0 0 0
vi /boot/firmware/cmdline.txt
加入
rd.driver.pre=panel-boe-bv050fwm,gpio-pca953x,i2c-bcm2835 fbcon=map:0
4. 一些调试指令记录
cat /proc/device-tree/chosen/user-warnings # 设备树错误日志与dtdebug配合使用
modetest -M vc4 -s 42:480,572,584,672,854,872,876,894-42 -v #modetest指定时序测试屏幕
sudo dd if=/dev/urandom of=/dev/fb0 bs=1024 count=10 #随机灌入fb0, 测试framebuff
echo 0 > /sys/class/graphics/fbcon/cursor_blink #纯文本终端禁用光标闪烁
modetest -M vc4|grep 480x854 #查看屏幕提供的mode, 以及默认使用的mode
lsinitramfs /boot/initrd.img-$(uname -r) #查看initramfs 包含的内核模块