
仅需28元制作5寸MIPI电容触摸触摸全贴合树莓派兼容屏
简介
众所周知,由于出货量少,开发板的屏幕都相当的贵,一款320x480 3.5寸 SPI电阻屏都能卖到100元以上,如果将手机屏幕移植到SBC开发板上,即可低成本,高质量地解决问题
简介:众所周知,由于出货量少,开发板的屏幕都相当的贵,一款320x480 3.5寸 SPI电阻屏都能卖到100元以上,如果将手机屏幕移植到SBC开发板上,即可低成本,高质量地解决问题开源协议
:GPL 3.0
(未经作者授权,禁止转载)描述
项目说明
这是一个将手机屏幕移植到SBC开发板上的案例,仅供参考,手机屏幕屏幕型号种类太多,接口协议也是多种多样的,不同型号需要具体问题具体分析,不具有通用性,一般来说,对于MIPI屏幕,只要搞定了手机屏幕电路图以及屏幕初始化代码,再加上目标平台支持的话,移植就是可以成功的。
项目成本
屏15元,触摸8元,打板免费,板上元件不超过5元,总共不超过30元。
屏幕和触摸参数
屏幕参数
这里我们搞到的是某为Y635上的一款屏幕,屏幕型号是京东方的BV050FWM,通过搜索我们可以找到这款面板的规格书,通过查看规格书我们可以知道:
- 屏幕尺寸: 1.56(W)*109.526(H) 5英寸
- 分辨率: 854x480
- 驱动IC: OTM8019A
- 接口: MIPI DSI接口 2通道

目前主流树莓派以及各种兼容的国产派SBC开发板,所用的通用显示接口均为MIPI DSI 2通道 15-pin FPC 接口(1mm pitch),所以基本上60Hz 720P屏,就已经到达了接口数据传输上限了,但这并不意味着这是SOC支持的上限,某些国产派的私有接口会支持更高分辨率的屏幕,如果看到一片未知型号的屏幕,如果该屏幕的参数超过接口速度上限,就可以直接pass了。
触摸参数
触摸屏是海鲜市场捡到的一块5英寸FT5446 ic,触摸物理分辨率为1280x720,电容式触摸屏,支持多点触控,其他参数未知的触摸屏,触摸屏的驱动相对于屏幕来说比较简单,主要是比较难找到尺寸相符的。

电路设计
关于电路设计,本人希望的目标是只需要接15PIN的排线就可以完成所有的功能,但是目前受限于15PIN的接口没有多余的GPIO引出,无法完成背光亮度调节。当然这个问题也不是不能解决,首选最无损的解决方案是通过mipi dcs指令控制屏幕驱动ic的输出PWM来控制亮度,但是有一个问题,目前还不知道如何控制这个引脚开启输出。次选则是可以选择加装一个i2c转pwm的调光ic,考虑到成本原因,另外还要单独写驱动,比较麻烦,所以次选方案暂时搁置。
屏幕电路设计
通过查看屏幕的规格书,我们可以发现这款屏幕的电路还是比较简单的,除了MIPI DSI必要的时钟线,数据线外,只需要一路2.85v(VCI)的供电,一路1.8v(IOVCC)的供电,再设计一个背光PWM调光电路即可,由于默认15P的通用接口只引出了一个I2C,甚至没有一个多余的GPIO可供使用,所以解决办法只能是:
1. 从SBC开发板的40Pin接口直接引出GPIO以供使用
2. 使用i2c GPIO扩展芯片PCA9536
为了两种方案可以自由切换,必要的IO已经由排针引出,目前只需要贴装好PCA9536,就可以在只接一条15PIN排线的情况下实现显示和触摸均工作正常(但是背光亮度无法调节)。
> 注意:LCD的RESET信号的电平需要和IOVCC的相同,如果没有特别说明的话,SBC开发板的GPIO一般为3.3v,不能直接接到RESET信号上,需要额外进行电平转换(更新使用分压电阻进行电平转换,节省成本)
触摸电路设计
触摸接口共有六个引脚,经过仔细观察以及不断查找,确定这个引脚应该是焊接而不是通过排线座插接。
引脚功能定义部分,供电直接采用3.3v,我们主要需要关注的是RESET引脚和INT引脚,这里RESET引脚没有电平要求,可以直连3.3V GPIO,但是INT中断引脚是比较特殊的,使用扩展io芯片不能很好的实现中断的功能,目前的解决方案是修改驱动部分,由中断触发改为轮询。当然如果还是想使用中断的话,可以将引出的中断引脚使直连到SBC开发板的40PIN任何一个空闲的GPIO上。
背光电路设计
首先通过屏幕的规格书查询,可以找到LED 的Max Forward Voltage是3.2v,共有5个LED,最大电压需要在16V以内,通过可调电压验证好没有问题,经过搜索选用AP3019A作为PWM调光IC,支持2.5v-16v电压调节。调光IC看似是在调压,实际上是在调流,因为调流能更好的符合LED的发光曲线,并可以通过反馈电阻限流来保证LED不会被烧毁。
关于限流电阻的大小计算,参考公式 R=200/20=10欧姆,其中200为固定参数,20指的是LED的饱和电流,单位为ma。
在PWM调光信号产生部分,理论上来说屏幕的驱动IC是会根据mipi dcs指令,输出不同的PWM占空比的信号给PWM调光IC,但是实际无论如何调节初始化指令,查看示波器,屏幕驱动ic的PWM引脚始终无输出,仔细翻阅屏幕驱动ic的datasheet也看不出一个所以然,继续分析屏的初始化指令,很多指令在datasheet中也没有提及,似乎是有隐藏指令,如果有懂的大佬可以指点一下。
转接板走线与布局
走线
走线部分,低速信号部分基本联通了就行,电源部分要求也不高,功耗最大的是背光,但是背光也是高电压低电流,保守估计单路电流不会超过500ma,整板功耗不会超过1.5w。
重点需要关注的还是是mipi高速部分,mipi的走线要求还是比较高的,首先有阻抗要求,需要满足MIPI 100Ω差分阻抗,所以打板时注意选择需要阻抗,层叠方案要选JLC04161H-3313,其他要讲究的还有等长,等距,参考层,包地,远离干扰等等,这些网上随便找一篇文章都说得很详细,这里就不细说了。根据画mipi转接板的经验,目前这块屏幕在MIPI屏中速率还算是比较低的,所以容错空间还算是比较高,如果是1080P或者以上分辨率的屏幕,则必须按照标准走线。
布局
为了更好的固定屏幕和SBC开发板,这里参考了微雪转接板的连接方式:https://www.waveshare.net/shop/5inch-DSI-LCD.htm
但是尽量将板子做小,不必要的电路全部精简掉,转接板背面采用纳米胶带与屏幕贴合,正面按照树莓派标准4孔孔距打孔,使用贴片圆螺母 + 螺柱与SBC开发板进行固定。原版屏幕的排线是直接从背面翻上来的,考虑到排线不够长,还有一些走线的问题,这里直接使用在排线接口处开槽孔的方式连接。
为了尽量减少转接板厚度,排针改用卧贴设计,15PIN排线座可以使用翻盖下接或者抽屉下接,由于触摸排线焊接位置与LCD排线座和螺丝柱位置互相打架,转接板要比树莓派主板稍长一点。

驱动编写
这里使用的是radxa 3a开发板进行的开发和测试,以下的驱动均基于radxa 3a开发板编写,其他平台需要有差异的部分需要自行修改,我会尽量指出需要修改哪一部分。
屏幕驱动
在Linux内核中,要想点亮一块MIPI DSI屏幕,有两种驱动方式可以选择,
- 使用内置的simple-panel-dsi通用驱动,屏初始化序列写入到设备树中
- 直接定制驱动,通过内核api接口直接操作硬件
方案1看似简单,不用编译驱动,直接改设备树就行,但是实际使用真的是一言难尽,一些供电的开启无法控制,无法控制驱动加载顺序导致各种玄学bug,无法实现特殊的要求,非常难调试,另外simple-panel-dsi通用驱动抽象了太多面板,使得代码极其冗长,难以阅读,所以最终还是选择了方案2。
屏幕驱动方面,要让MIPI屏幕能正常显示,其中最关键的两个参数是,屏幕初始化序列,时序,而这两个参数很多屏幕的规格书里面没有写,这块屏幕就属于这种情况,这个时候一般就需要用逻辑分析仪去抓取屏幕上电时的初始化逻辑了,不过好在我们知道这块屏幕是哪个手机的,并且这个手机的内核又正好开源,我们可以采用一个取巧的办法,去开源内核中寻找这几个参数。
内核源代码中有大量重复和冗余的代码,经过不断翻找和排除,终于我们锁定了这串关键的数据:
&mdss_mdp {
dsi_boe_otm8019a_5p0_fwvga_video: qcom,mdss_dsi_boe_otm8019a_5p0_fwvga_video {
qcom,mdss-dsi-panel-name = "BOE_OTM8019A_5P0_FWVGA_VIDEO";
qcom,cont-splash-enabled;
qcom,mdss-dsi-panel-controller = ;
qcom,mdss-dsi-panel-type = "dsi_video_mode";
qcom,mdss-dsi-panel-destination = "display_1";
qcom,mdss-dsi-panel-clockrate = ;
qcom,mdss-dsi-panel-framerate = ;
qcom,mdss-dsi-virtual-channel-id = ;
qcom,mdss-dsi-stream = ;
qcom,mdss-dsi-panel-width = ;
qcom,mdss-dsi-panel-height = ;
qcom,mdss-dsi-h-front-porch = ;
qcom,mdss-dsi-h-back-porch = ;
qcom,mdss-dsi-h-pulse-width = ;
qcom,mdss-dsi-h-sync-skew = ;
qcom,mdss-dsi-v-back-porch = ;
qcom,mdss-dsi-v-front-porch = ;
qcom,mdss-dsi-v-pulse-width = ;
qcom,mdss-dsi-h-left-border = ;
qcom,mdss-dsi-h-right-border = ;
qcom,mdss-dsi-v-top-border = ;
qcom,mdss-dsi-v-bottom-border = ;
qcom,mdss-dsi-bpp = ;
qcom,mdss-dsi-underflow-color = ;
qcom,mdss-dsi-border-color = ;
qcom,mdss-dsi-color-order = "rgb_swap_rgb";
qcom,mdss-dsi-pixel-packing = "tight";
qcom,mdss-dsi-on-command = [29 01 00 00 00 00 02 00 00
29 01 00 00 00 00 04 FF 80 19 01
29 01 00 00 00 00 02 00 80
29 01 00 00 00 00 03 FF 80 19
/* 太长省略 */
05 01 00 00 14 00 02 29 00];
qcom,mdss-dsi-off-command = [05 01 00 00 14 00 02 28 00
05 01 00 00 78 00 02 10 00];
qcom,mdss-dsi-on-command-state = "dsi_lp_mode";
qcom,mdss-dsi-off-command-state = "dsi_hs_mode";
qcom,mdss-dsi-h-sync-pulse = ;
qcom,mdss-dsi-traffic-mode = "burst_mode";
qcom,mdss-dsi-lane-map = "lane_map_3012";
qcom,mdss-dsi-bllp-eof-power-mode;
qcom,mdss-dsi-bllp-power-mode;
qcom,mdss-dsi-lane-0-state;
qcom,mdss-dsi-lane-1-state;
qcom,mdss-dsi-panel-timings = [7a 1a 12 00 40 42 16 1e 14 03 04 00];
qcom,mdss-dsi-t-clk-post = ;
qcom,mdss-dsi-t-clk-pre = ;
qcom,mdss-dsi-bl-min-level = ;
qcom,mdss-dsi-bl-max-level = ;
qcom,mdss-dsi-dma-trigger = "trigger_sw";
qcom,mdss-dsi-mdp-trigger = "none";
qcom,mdss-dsi-bl-pmic-control-type = "bl_ctrl_dcs";
qcom,panel-inverse-off-cmds = [05 01 00 00 00 00 02 20 00];
qcom,panel-inverse-on-cmds = [05 01 00 00 00 00 02 21 00];
qcom,inverse-on-cmds-dsi-state = "dsi_lp_mode";
qcom,inverse-off-cmds-dsi-state = "dsi_lp_mode";
qcom,panel-cabc-ui-cmds = [15 01 00 00 00 00 02 55 01
15 01 00 00 00 00 02 53 24];
qcom,panel-cabc-video-cmds = [15 01 00 00 00 00 02 55 03
15 01 00 00 00 00 02 53 2C];
qcom,cabc-ui-cmds-dsi-state = "dsi_lp_mode";
qcom,cabc-video-cmds-dsi-state = "dsi_lp_mode";
qcom,panel-dot-inversion-mode-cmds = [29 01 00 00 00 00 02 00 00
29 01 00 00 00 00 04 FF 80 19 01
29 01 00 00 00 00 02 00 80
29 01 00 00 00 00 03 FF 80 19
29 01 00 00 00 00 02 00 B4
29 01 00 00 00 00 02 C0 10
29 01 00 00 00 00 02 00 00
29 01 00 00 05 00 02 FB 00
29 01 00 00 00 00 02 00 00
29 01 00 00 00 00 04 FF FF FF FF];
qcom,panel-column-inversion-mode-cmds = [29 01 00 00 00 00 02 00 00
29 01 00 00 00 00 04 FF 80 19 01
29 01 00 00 00 00 02 00 80
29 01 00 00 00 00 03 FF 80 19
29 01 00 00 00 00 02 00 B4
29 01 00 00 00 00 02 C0 00
29 01 00 00 00 00 02 00 00
29 01 00 00 05 00 02 FB 00
29 01 00 00 00 00 02 00 00
29 01 00 00 00 00 04 FF FF FF FF];
qcom,dot-inversion-cmds-dsi-state = "dsi_lp_mode";
qcom,column-inversion-cmds-dsi-state = "dsi_lp_mode";
qcom,mdss-dsi-lp11-init;
qcom,mdss-pan-physical-width-dimension = ;
qcom,mdss-pan-physical-height-dimension = ;
qcom,mdss-dsi-reset-sequence = , , ;
};
};
当然,找到了设备树中的参数只是做完了第一步,实际要根据目标dsi驱动是如何解析这串设备树的参数并发送到屏幕的,一步步跟进去,从而将上述参数转换到我们自己平台的参数,比如说mdss-dsi-on-command的第一行:
29 01 00 00 00 00 02 00 00
要根据高通的格式定义:
- qcom,mdss-dsi-on-command: A byte stream formed by multiple dcs packets base on
qcom dsi controller protocol.
byte 0: dcs data type
byte 1: set to indicate this is an individual packet
(no chain)
byte 2: virtual channel number
byte 3: expect ack from client (dcs read command)
byte 4: wait number of specified ms after dcs command
transmitted
byte 5, 6: 16 bits length in network byte order
byte 7 and beyond: number byte of payload
手动转化成主线内核的格式:mipi_dsi_dcs_write_seq(dsi, 0x29, 0x00, 0x02, 0x00, 0x00);,不难但是很烦,交给ai去做,它也会嫌麻烦,输出几行就罢工了。
屏幕时序部分主要是这几个参数:
qcom,mdss-dsi-h-front-porch = ;
qcom,mdss-dsi-h-back-porch = ;
qcom,mdss-dsi-h-pulse-width = ;
qcom,mdss-dsi-h-sync-skew = ;
qcom,mdss-dsi-v-back-porch = ;
qcom,mdss-dsi-v-front-porch = ;
qcom,mdss-dsi-v-pulse-width = ;
需看着这个图调整:
+-------+----------+-------------------------------------+----------+
| | | ^ | |
| | | |vsync_len | |
| | | v | |
+-------+----------+-------------------------------------+----------+
| | | ^ | |
| | | |vback_porch | |
| | | v | |
+-------+----------#######################################----------+
| | # ^ # |
| | # | # |
| hsync | hback # | # hfront |
| len | porch # | hactive # porch |
||##|
| | # | # |
| | # |vactive # |
| | # | # |
| | # v # |
+-------+----------#######################################----------+
| | | ^ | |
| | | |vfront_porch | |
| | | v | |
+-------+----------+-------------------------------------+----------+
根据对应要求需要转换为这种格式:
static const struct drm_display_mode bv050fwm_mode_42hz = {
.clock = 25000, // 时钟频率,单位是 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,
};
// 后续增加的60hz mode时序
static const struct drm_display_mode bv050fwm_mode_60hz = {
.clock = 36000, // 时钟频率,单位是 kHz
.hdisplay = 480, // 水平可视区域
.hsync_start = 480 + 180, // 水平同步起始 = hactive + hfront-porch
.hsync_end = 480 + 180 + 12, // 水平同步结束 = hsync_start + hsync-len
.htotal = 480 + 180 + 12 + 0, // 水平总长度 = 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,
};
还有一些参数也不能直接套用,比如说这里的clockrate,如果直接套用的话,屏幕显示会发生大偏移,并且颜色也会异常,这块屏幕算是比较简单的,调完以上参数就可以正常显示了,有些不按规范来的屏幕,很多参数都需要根据自己平台手动转换并不断地调整,一些特殊一点的屏幕还需要跟着示波器调整波形:



如果是现代手机这种高分辨率+高刷新率的屏幕,入门级的示波器是一点都吃不消的,只能是摸黑调或者放弃。有兴趣的可以参阅:https://blog.csdn.net/carolven/article/details/121462918
具体详细的调整结果由于篇幅限制,可以直接参考附带的源码,这里仅给出dsi panel的设备树。
> 注意:设备树中不带&符号的节点为定义节点,带&符号的节点指的是对同名定义节点的属性进行覆写
/* Rock 3A 15PIN接口是连接到dsi1的,所以这里定义为 dsi1_panel */
&dsi1 {
status = "okay";
dsi1_panel: dsi-panel@0 {
status = "okay";
compatible = "boe,bv050fwm";
reg = ;
/* 如果使用开发板40PIN的PWM调光需要加上 */
backlight = ;
/* 供电部分,自行修改成控制15PIN 3.3v输出的regulator */
power-supply = ;
backlight-gpios = ;
vci-gpios = ;
reset-gpios = ;
ports {
#address-cells = ;
#size-cells = ;
port@0 {
reg = ;
panel_in_dsi1: endpoint {
remote-endpoint = ;
};
};
};
};
};
驱动默认不启动40PIN的PWM调光,有需要开启的需要编辑bv050fwm_probe中的函数手动开启。
后续补充
拿到面板的初始化序列之后,对于点亮一块面板来说就已经成功一半了,但是在发送面板初始化序列还需要一些额外的工作内容,首先是通过GPIO依次打开屏幕的供电,有几路供电就需要打开几路,同时注意面板规格书中的上电时序,如果不知道时序就慢慢实验,一般的面板对于上电时序是不太敏感的,这时候可以不用太在意。
ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(ctx->supplies),
ctx->supplies);
if (ret 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);
上电之后,一般要给几十ms至上百ms延迟,等待电源稳定,然后就可以对面板进行复位操作了,一般mipi面板的复位都是低电平有效,一般来说,默认上电就是低电平,只需要把GPIO拉高即可,如果不放心的话,可以先拉高再拉低,最后恢复高电平,对于一般的面板,20ms的低电平复位时间就已经是足够的了,但是没必要太精确,时间长一点也问题不大。
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);
}
复位完成后,驱动会自动调用bv050fwm_on函数发送启动序列,启动序列的作用一般为设置面板的参数(包括电压,分辨率,gamma,翻转方向等)一切准备就绪后,一般都是通过mipi_dsi_dcs_exit_sleep_mode函数退出睡眠模式,再通过mipi_dsi_dcs_set_display_on开启面板显示。
ret = mipi_dsi_dcs_exit_sleep_mode(dsi);
if (ret dev, "Unable to request touchscreen IRQ.\n");
client->irq = -1; // 标记为无效
}
error = input_register_device(input);
if (error)
return error;
edt_ft5x06_ts_prepare_debugfs(tsdata, dev_driver_string(&client->dev));
if (client->irq dev, "Using polling mode for touch detection\n");
// 设定轮询标志
tsdata->polling_enabled = true;
// 创建轮询线程
tsdata->poll_thread = kthread_run(edt_ft5x06_poll_thread, tsdata, "edt_poll_thread");
if (IS_ERR(tsdata->poll_thread))
{
dev_err(&client->dev, "Failed to create polling thread\n");
return PTR_ERR(tsdata->poll_thread);
}
}
else {
dev_dbg(&client->dev,
"EDT FT5x06 initialized: IRQ %d, WAKE pin %d, Reset pin %d.\n",
client->irq,
tsdata->wake_gpio ? desc_to_gpio(tsdata->wake_gpio) : -1,
tsdata->reset_gpio ? desc_to_gpio(tsdata->reset_gpio) : -1);
}
return 0;
增加函数edt_ft5x06_poll_thread:
static int edt_ft5x06_poll_thread(void *data)
{
struct edt_ft5x06_ts_data *tsdata = (struct edt_ft5x06_ts_data *)data;
while (!kthread_should_stop()) {
// 手动调用中断函数
edt_ft5x06_ts_isr(0, tsdata);
// 轮询间隔 1ms, 增加间隔可以降低系统负载,但是会影响触摸响应速度
// msleep(1);
}
return 0;
}
其他修改请参考附件代码。
扩展IO驱动
pca9536的驱动内核中也已经内置了,具体代码可参见:https://github.com/torvalds/linux/blob/master/drivers/gpio/gpio-pca953x.c
如果使用了扩展IO,则需要先去开发板原理图中找到15PIN接入的i2c编号,然后去设备树对应i2c节点下配置相关信息并启用,例如这里使用的是i2c3,pca9536默认设备地址是0x41。
&i2c3 {
pinctrl-names = "default";
pinctrl-0 = ;
status = "okay";
pca9536: gpio@41 {
compatible = "nxp,pca9536";
/* 默认设备地址寄存器 */
reg = ;
gpio-controller;
#gpio-cells = ;
status = "okay";
gpio-line-names = "BL_EN", "VCI_EN", "LCD_RST", "TP_RST";
/* 电源控制 */
vcc-supply = ;
};
};
背光驱动
如果需要实现背光亮度调整,则需要40PIN中找到SBC开发板的PWM接口并使用杜邦线将此接口连接到转接板的PWM_BL引脚(如果不需要使用背光调节的话,跳线帽短接BL_EN与PWM_BL)。
驱动部分只需修改设备树即可,同上,先从40PIN排针接口中找到能输出pwm的引脚编号。举例:我使用的Rock3A,翻阅原理图发现第22脚,24脚均可以输出PWM,选择使用第22脚,22脚是pwm2,AP3019A的CTRL引脚的 PWM 输入频率典型上限是 2kHz,所以设定周期为500000ns = 500μs,正好对应频率 2kHz,则pwms = ; 如果是其他pwm,根据实际情况改为对应接口。另外如果pwm2的开启还受GPIO控制的,需要修改enable-gpios的值。
brightness-levels是根据实际亮度设计的一个调光曲线,使亮度调整更加柔和,还可以通过限制亮度范围,避免亮度太低时屏幕熄灭调不回来了。

dsi1_backlight: dsi1-backlight {
status = "okay";
compatible = "pwm-backlight";
pwms = ;
brightness-levels = ;
default-brightness-level = ;
/* 按照实际情况修改 */
enable-gpios = ;
};
驱动编译和加载
如果你的设备已经搭建好有固件的编译环境,那么编译并烧录固件是一件非常简单的事情,只需要在目标源码中修改对应代码,运行make menuconfig,勾选要编译的驱动程序,重新交叉编译烧录固件到设备即可,这里主要简单介绍一下如何在本机编译并加载驱动,这种方式的好处在于可以不影响SBC的原有程序与数据的条件下,快速调试驱动,由于篇幅原因不能写得非常详细,遇到问题可以直接询问各类大模型,ai特别擅长处理此类问题。
驱动编译
这里以Debian系发行版为例:
首先,需要安装内核头文件
sudo apt-get install linux-headers-$(uname -r)
对于其他发行版,使用适合你系统的包管理器来安装内核头文件。
> 注意:如果你是使用自行编译的内核或者固件,这个包不会存在于公共软件源中,需要要去你编译的输出文件夹找,如果实在找不到对应的内核headers,理论上是可以使用相近的内核版本headers,但是需要额外去处理版本号的一些magic问题才能成功加载驱动。
其次,安装编译相关工具
sudo apt install build-essential dkms git libelf-dev
最后,新建一个文件夹,存放你需要编译的驱动,并在其中新建一个最简单的Makefile文本文件
# 设置内核源代码目录
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
# 设置模块名称,如编译 dsi_panel_driver.c,则
obj-m := dsi_panel_driver.o
# 编译目标
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
# 清理生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
如果一切都准备好了的话,运行编译命令: make
如果没有报错的话,会生成以xxx.ko结尾的内核模块文件。
其中dsi驱动,io扩展驱动需要在启动时加载才有效,所以需要将xxx.ko文件复制到路径
/lib/modules/当前使用的内核版本/kernel/drivers/gpu/drm/panel/ 路径下(dsi面板驱动)
/lib/modules/当前使用的内核版本/kernel/drivers/gpio/ 路径下(io扩展驱动)
复制完成之后,需要:
> 更新模块缓存: sudo depmod -a
> 更新内核模块: sudo update-initramfs -u
如果是触摸驱动则可以直接通过运行 sudo insmod xxx.ko 直接加载。
测试没问题之后,可以将触摸驱动放置到路径 /lib/modules/当前使用的内核版本/kernel/drivers/input/touchscreen/ 中,并配置好设备树即可启动时加载。
驱动加载
一般来说,驱动的自动加载需要设备树的支持,加载规则是根据设备树中配置的compatible属性来对具有相同compatible属性的内核模块进行加载的。
所以,要在设备启动时自动加载某个硬件驱动,要将对应硬件的属性写入到设备树中,而手动修改设备树的一个典型的操作流程是:找到你的设备树路径(通常在 /boot/dtb/ 或 /boot/dtbs/),然后找到当前设备名称的dtb文件,备份,反编译出dts文件,编辑dts修改对应节点,再重新编译回dtb,替换新的dtb文件到原路径。 (反编译出来的dts格式与源文件差异很大,最好还是在内核源代码的根目录下使用make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs进行编译,确保已经安装了交叉编译工具链,编译生成的dtb文件会和dts在同一路径下)。另外,如果系统支持的话也可以使用overlay的方式,在不修改dtb的情况下,通过打补丁的方式修改当前使用的dtb文件,这种方式属于高级用法,很难一两句话说清楚,需要自行查阅相关资料。
> 反编译 DTB dtc -I dtb -O dts -o extracted.dts your_device.dtb
> 重新编译 DTB dtc -I dts -O dtb -o new_device.dtb extracted.dts
> 安装交叉编译工具链:sudo apt install gcc-aarch64-linux-gnu
驱动崩溃
在类似dsi这类底层驱动崩溃时,一般也会导致系统崩溃卡死,某些不太严重情况下,执行超时后系统会自动恢复过来,有些情况则会无限循环彻底卡死,如果遇到此类情况,想办法将SBC开发板的存储磁盘接入pc,删除掉有问题的ko文件即可。如果pc系统是windows,可以通过Diskgenius_Pro的文件管理器直接删除,如果是linux可手动挂载磁盘后删除。
rk平台dsi驱动加载的注意事项
其他平台暂无测试,如果你使用的是rk平台,需要注意的是,在主线内核中,相比与bsp内核,设备树会少一些节点,需要手动补全才行,如果没有特殊需求,尽量使用出厂bsp内核,会少很多bug,以下是可能缺少的节点,以及缺少节点之间的连接关系,不同版本内核需要修补的配置不一样,根据实际情况进行修改。
&vp1 {
vp1_out_dsi1: endpoint@ROCKCHIP_VOP2_EP_MIPI1 {
reg = ;
remote-endpoint = ;
};
};
&dsi1_in {
reg = ;
#address-cells = ;
#size-cells = ;
dsi1_in_vp1: endpoint@0 {
reg = ;
remote-endpoint = ;
};
};
&dsi_dphy1 {
status = "okay";
};
&dsi1_out {
reg = ;
#address-cells = ;
#size-cells = ;
dsi1_out_panel: endpoint@0 {
reg = ;
remote-endpoint = ;
};
};
需要补全的节点之间具体连接关系较为复杂,具体请参考下图(其中vp指的是视频处理器,vop指的是视频输出处理器):
graph TD
subgraph DSI
dsi_dphy0 --> dsi0
dsi_dphy1 --> dsi1
dsi0 --> dsi0_in
dsi0 --> dsi0_out
dsi1 --> dsi1_in
dsi1 --> dsi1_out
dsi1 --> dsi1_panel
dsi1_in --> dsi1_in_vp1
dsi1_out dsi1_panel
end
subgraph HDMI
hdmi --> hdmi_in
hdmi --> hdmi_out
hdmi_in --> hdmi_in_vp0
hdmi_out --> hdmi_out_con
end
subgraph VOP
vop --> vp0
vop --> vp1
vop --> vp2
vp0 --> vp0_out_hdmi
vp1 --> vp1_out_dsi1
end
hdmi_in_vp0 vp0_out_hdmi
vp1_out_dsi1 dsi1_in_vp1
另外本人调试的时候遇到的一些玄学问题,这有可能是错误的dsi节点顺序导致的驱动循环依赖,加载异常,如果你也遇到莫名其妙报错、驱动不加载的情况,不妨试试调换一下节点顺序。
调试流程
flowchart TD
start([开始]) --> A[检查3.3v是否有输出]
A -->B[检查VCI和VDDI是否满足2.85v以及1.8v]
B --> C[dmesg检查dsi panel驱动是否正常启动]
C --> D[检查LCD_RST启动时是否有复位信号]
D --> E[检查背光控制PWM_LCD是否有高电平]
E --> F[使用evtest检查触摸是否正常]
F --> G[结束]
贴合
分离的LCD与触摸,不贴合的话用起来不是很方便,普通贴合的话,只需要沿着液晶模组周围打一圈热熔胶或者玻璃胶即可固定。麻烦的是全贴合,如果需要做到全贴合,俗称压盖板,正规的方式是先用oca贴合胶贴合,放入压屏机压实,最后用除泡机除泡。因为没有这些机器,所以这里只能使用一个土办法:即使用UV胶加光固化的方案。
参考操作流程
常用钢化膜uv胶一管大概2g左右,只需在触摸屏中间滴一滴大约0.8g左右的胶(多滴的话容易产生气泡),然后屏幕正面朝下缓慢地将压上去,观察等待uv胶慢慢溢满屏幕,期间准备好纸巾吸走多余的残胶,因为固化时间非常长,所以这步完全不用着急。在没有固化之前都可以微调(但是尽量减少调整次数),如果发现有问题还可直接取下用酒精清洗干净后重新贴,在固化完成之前不要将屏幕翻转避免胶溢出到屏幕中。注意全程不要在阳光下操作,容易被阳光固化。
固化灯最好采用大功率平板灯,从下往上照,如果有光固化3d打印机也是可以使用的。
这个方案需要的动手能力非常高,稍有不慎屏幕就可能进胶或者产生气泡,虽然不至于报废,但是很影响显示效果,当然如果能贴得完美的话,观感与使用oca贴合胶无异,和市面上的UV胶贴膜效果类似。

我这片就没有掌握好位置,贴合有气泡,建议价格合适的话还是交给那些专业人士贴合。
兼容性
理论上这块屏可以在所有带15PIN DSI接口的树莓派、以及树莓派兼容的国产派中使用,包括但不限于树莓派1 2 3 4 5(5引脚定义不同需要特殊排线),RockPi 3 4,NanoPi 4,香橙派3 4 5中使用,目前只测试了rock 3a,具体检测方式是检查自己的开发板原理图是否与转接板的引脚定义相同,如果定义相同,只需要改改设备树基本上都是可以用的,如果只是定义顺序不同,根据定义自己修改一下接口,或者直接定制一下转换排线应该也是可以成功点亮的,具体还是以实机测试结果为准。
参考资料
https://oshwhub.com/cnflysky/RaspberryPi-DSI-Display
https://wiki.t-firefly.com/en/Firefly-RK3399/driver_lcd.html
https://docs.radxa.com/rock3/rock3a/getting-started/download
https://zhuanlan.zhihu.com/p/501762436
https://doc.embedfire.com/linux/imx6/base/zh/latest/linux_app/input_subsystem.html
https://whycan.com/t_4762.html
https://www.waveshare.net/shop/5inch-DSI-LCD.htm
结语
目前板子还在测试中,目前还需要测试的部分有休眠和唤醒,功耗优化,还有触摸部分换用轮询之后是否有潜在的bug等,后期准备再3d打印一个外壳,这样用起来才会舒服。第一次发文,文章所涉及到的知识面较广,有很多资料来自互联网,本人也不够专业,难以鉴别哪些资料是真实可信的,不可避免会有一些错误和遗漏,欢迎各位大佬批评指正。
设计图
未生成预览图,请在编辑器重新保存一次BOM
暂无BOM
克隆工程工程成员
知识产权声明&复刻说明
本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。
请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。


评论