一. LCD 兼容
1.1 lk 阶段启动流程概览
lk 阶段框架不妨从 lk 阶段的 c 代码开始分析,第一个执行的 c 语言函数是 main.c 文件中的 kmain 函数。
1 | // file: src/bootable/bootloader/lk/kernel/main.c |
简化一些与屏启动不相关的流程后的得到的函数调用关系为:
1 | // file: src/bootable/bootloader/lk/kernel/main.c |
这里可以看出,lk 在做完平台端必要的初始化后,会去启动所谓的 app,那么 app 是什么东西?如何定义?又在哪里被执行?将在下一小节中介绍。
1.2 lk 阶段 app 启动流程
先看看 apps_init() 都干了些什么?
1 | // file: src/bootable/bootloader/lk/app/app.c |
从上面的代码可以看出,每一个 app 都使用 app_descriptor 描述,并具有一个 init 函数,一个 entry 函数和一个 flags 标识。
在 apps_init() 中会去遍历一个 app 的列表,分别调用他们的 init 函数,对 app 进行初始化。初始化结束后,还会根据 flags 标识位决定是否调用 start_app(app) 函数创建一个线程运行 app 的 entry 函数。
那么问题来了,这里的 app 的列表是怎么来的?
上面 app 是从 __apps_start 开始取出 app 元素,一直取到 __apps_end 才结束,那么 __apps_start 和 __apps_end 在哪里定义?
代码中居然找不到,总有定义的地方吧,grep 搜一下,原来在链接脚本中。
1 | [wangbing@ubuntu: lk]$ grep -rsn "__apps_start" |
打开链接脚本看看,可以发现四个脚本描述 __apps_start 和 __apps_end 都一样,都是表示 .apps 端的起始结束地址。
1 | .rodata : { |
看到这里疑问就解除了,app 列表是在从 .apps 段中取出来的。但是新的问题又来了,lk 驱动中是怎么将 app 对应的 app_descriptor 结构链接到 .apps 段中呢?
那就好好看看 app.c 和 app.h 文件吧,其中 app.c 上面已经贴出来了,下面仅贴出 app.h 文件。
1 | // file: src/bootable/bootloader/lk/app/app.h |
在 app.h 的最后几行提供了两个用于创建 app 的宏,分别是 APP_START 和 APP_END。搜索一下,看看别人是怎么用他们创建 app 的。
1 | ---- APP_START Matches (7 in 7 files) ---- |
就以第一个 aboot.c 为例吧,看看它是怎么创建的。
1 | // file: src/bootable/bootloader/lk/app/aboot/aboot.c |
将宏定义展开,没错,就是这里指定该结构存放在 .apps 段。
1 | struct app_descriptor _app_aboot __SECTION(".apps") = { .name = aboot, |
总结一下:
通过 APP_START 和 APP_END 两个宏创建一个 app,这种方式创建的 app 会被链接到 .apps 段。
在 lk 启动流程中,最后会遍历 .apps 段中所有的 app,分别执行其中的 init 和 entry 函数。
这里就清楚了, app 对应的 app_descriptor 结构必须放在 apps 段中才可以被启动。
1.3 lk 阶段 lcd 兼容原理
兼容原理概述
先介绍下 lcd 的 id 寄存器:MIPI 组织规定各屏厂生产的 lcd ic 的 id 信息必须记录在 A1 寄存器(RDDDB: Read DDB start)中,A1 寄存器中除了第一个字节的供应商 id 是由 MIPI 组织分配的,其他字节供应商可以自定义,可以记录 ic id、ic version code 什么的。这个随便找一块 lcd 的 datasheet 看一下就明白了。
再简单介绍下 lcd 的兼容原理:原理其实很简单,其实就是代码中已经包含有多块 lcd 驱动程序,在开机过程中,会去读出实际接在手机上的 lcd 模组的 id,将读出的 id 和各个屏驱动中的 id 做对比,如果某一个 lcd 驱动的 id 和读出的 id 一样,则驱动匹配成功,就使用这个匹配上的驱动程序操作 lcd。
基于上面原理性的介绍,我们也就大概明确了稍后分析 lcd 兼容这部分代码需要理清的主线逻辑了,这里列举一下:
- 在哪里读取手机上实际接的 lcd 的 id?
- 在哪里遍历所有的 lcd 驱动,并和实际读取的 id 做比对?
兼容框架分析
接下就是分析代码了,上一小节分析到 lk 阶段最后是启动各个 app,简单看了各个 app 的流程,最后发现 lk 的探测兼容部分是在 aboot 中实现的。
1 | // file: src/bootable/bootloader/lk/app/aboot/aboot.c |
上面贴出来的框图仅仅是将 lcd 兼容函数调用关系贴出来了,一些地方描述不是很详细,下面介绍一下上图中做有标号的地方。
标号1. 依次遍历代码中的兼容的 lcd 驱动做初始化操作
1 | // file: src/bootable/bootloader/lk/target/msm8952/target_display.c |
标号2. 根据 panel_name 解析 supp_panels 数组,得到 panel 的编号
1 | // file: src/bootable/bootloader/lk/dev/gcdb/display/panel_display.h |
标号3. 根据解析出来 panel 编号,绑定对应的 lcd 驱动函数
1 | // file: src/bootable/bootloader/lk/target/msm8952/oem_panel.c |
标号4标号5. 探测位置
标号4和标号5的位置比较简单,不涉及其他没有贴出来的变量或函数,就不再去详细介绍。
1.4 kernel 阶段 lcd 兼容原理
兼容原理概述
kernel 阶段的兼容相比 lk 阶段要简单的多,由于在 lk 阶段已经做过了遍历所有屏驱动探测 lcd 的动作,再加上 bootloader 可以通过 cmdline 传参机制传递信息到 kernel 中。这样的话,完全可以通过 cmdline 将 lk 阶段的探测到的屏名称直接传递到 kernel,kernel 拿到传递过来的 panel name 再去加载对应的屏驱动。
兼容框架分析
lk 阶段将屏信息写入 cmdline。
1 | // file: src/bootable/bootloader/lk/app/aboot/aboot.c |
kernel 阶段解析 cmdline 获取到 lk 传递过来的屏信息。
1 | // file: src/kernel/msm-3.18/drivers/video/msm/mdss/mdss_dsi.c |
lk 阶段在将 panel_config 写进 cmdline,kernel 中解析出出 cmdline 中 panel_config 的 panel_node_id,找到 panel_node_id 同名的 dts 节点。最后将找到的 dts 节点中的 “qcom,mdss-dsi-panel-name” 属性值写进 device info。
总结一下:这样的话就要求 panel_config->panel_node_id 要和屏 dts 节点名要相同。
二. LCD 移植
2.1 lk 阶段 lcd 移植流程
经过上面兼容框架的分析,现在接着思考下在 lk 阶段 porting 一块新的 lcd 需要修改哪些地方。
修改点1. 屏参数头文件
将屏供应商提供的屏参数头文件,如 panel_hx83102b_hsd_video.h 文件添加到 src/bootable/bootloader/lk/dev/gcdb/display/include/ 目录下。
这个屏参数头文件中,包含有屏的启动参数,屏的配置信息等,在 init_panel_data() 函数中会将使用到这些信息。
修改点2. target_display.c
这里需要在 panel_name_my 中追加新添加的 panel name,这样在遍历 lcd 驱动的时候才会取出新添加的屏驱动做探测的动作。
1 | diff --git a/bootable/bootloader/lk/target/msm8952/target_display.c b/bootable/bootloader/lk/target/msm8952/target_display.c |
修改点3. oem_panel.c
这里文件修改点比较多,有 5 修改点,分别是:
- 包含屏参数头文件。
- 在枚举中追加新兼容的屏的 panel id。
- 将新兼容屏的 panel name 和 panel id 匹配上,并追加到 supp_panels 数组中。
- 在 switch (panel_id) 位置绑定屏相关参数。
- 修改目前兼容的 lcd 的数量。
1 | diff --git a/bootable/bootloader/lk/target/msm8952/oem_panel.c b/bootable/bootloader/lk/target/msm8952/oem_panel.c |
2.2 kernel 阶段 lcd 移植流程
修改点1. 添加屏相关的 dtsi 文件
咨询 FAE 要到 LCM 相关的 panel dtsi 文件. 添加到 src/kernel/msm-3.18/arch/arm/boot/dts/qcom/ 目录下:
eg: src/kernel/msm-3.18/arch/arm/boot/dts/qcom/dsi-panel-st7703_boe55-video.dtsi
此 dtsi 仅仅是在 mdss_mdp 节点下追加了一个子节点。
1 | &mdss_mdp { |
问1: msm8937 是 64 位的处理器, 为什么是在 arch/arm/ 下添加文件, 而不是在 arch/arm64 目录下添加?
答1: 其实 arch/arm64 是通过软链接方式链接到 arch/arm 目录下,因此不管在哪个目录修改都是可以生效的。
1 | [wangbing@ubuntu: ~/src/kernel/msm-3.18/arch/arm64/boot/dts]$ ls -l |
修改点2. 包含屏参数 dtsi 文件到 dts 中
在 src/kernel/msm-3.18/arch/arm/boot/dts/qcom/msm8937-mdss-panels.dtsi 包含上一个修改点中添加的 dtsi 文件。目的是为了让追加的 panel 的 dtsi 在编译 dtb 的时候能够被编译到。
1 |
问1: dtb 编译的时候是如何确定哪些文件(哪些 dts 和 dtsi 文件)会被编译呢?
答1: 编译 dtb 文件也是根据 Makefile 规则编译的,在 src/kernel/msm-3.18/arch/arm/boot/dts/qcom/ 目录的 Makefile 决定哪些 dts 文件会编译成 dtb。编译的时候会编译 Makefile 中指定的 dtb 文件, 而这些 dtb 文件对应的源文件(dts文件)会依赖于相关的 dtsi 文件。编译的时候就会将 dts 以及依赖的 dtsi 一起编译(类似于c文件依赖h文件一样)。
问2: dsi-panel-st7703_boe55-video.dtsi 文件的依赖关系是?
答2: 一直根据 grep 搜索对应关键字, 就可以找到依赖关系:
1 | msm8937-mdss-panels.dtsi:34: #include "dsi-panel-st7703_boe55-video.dtsi" |
修改点3. 设置默认屏驱动(可选)
1 | msm8937-mtp.dtsi (kernel\msm-3.18\arch\arm64\boot\dts\qcom) |
三. LCD 背光
3.1 lk 阶段背光驱动
3.1.1 点亮背光位置
1 | // file: src/bootable/bootloader/lk/app/aboot/aboot.c |
3.2 kernel 阶段背光驱动
3.2.1 对上抛出的接口
LCD 背光驱动是通过内核提供的 LED 子系统注册的驱动,因此对应用层抛出的接口创建在 /sys/class/leds/
目录下。
C:\Users\wangbing> adb shell
# 读取手机当前设置的背光等级
android:/ # cat /sys/class/leds/lcd-backlight/brightness
74
# 设置背光等级为 100/255(手机变得更亮)
android:/ # echo 100 > /sys/class/leds/lcd-backlight/brightness
android:/ # cat /sys/class/leds/lcd-backlight/brightness
100
如何查看 logcat log 确认当前设置的背光值, 可以通过 DisplayPowerController: Animating brightness:
关键字确认。
C:\Users\wangbing>adb shell
android:/ # logcat -s DisplayPowerController | grep "brightness"
01-01 01:26:21.588 1206 1246 I DisplayPowerController: Animating brightness: target=102, rate=0, asusAnimator=false
01-01 01:26:21.588 1206 1246 I DisplayPowerController: Animating brightness: target=102, rate=0, asusAnimator=false
01-01 01:26:21.591 1206 1246 I DisplayPowerController: Animating brightness: target=102, rate=0, asusAnimator=false
3.2.2 驱动注册流程
1 | // file: src/kernel/msm-3.18/drivers/video/msm/mdss/mdss_fb.c |
3.2.3 自问自答
问1: 背光驱动没有实现 brightness_get 接口,为什么却可以通过 cat /sys/class/leds/lcd-backlight/brightness 节点获得亮度值?
答1: 看了下读 brightness 节点的函数,在读取 /sys/class/leds/lcd-backlight/brightness 节点时。
如果定义了 brightness_get 函数,则将会获取到的亮度值,传递给 backlight_led->brightness,然后将 backlight_led->brightness 值返回;
如果没有定义 brightness_get 函数,则直接返回 backlight_led->brightness 的值。
问2: 现在的疑问就变成了,为什么没有通过 brightness_get 获取亮度值,backlight_led->brightness 也是准确的?
答2: 看了下写 brightness 节点的函数,在写入亮度值 /sys/class/leds/lcd-backlight/brightness 节点时。
同时将写入的值记录到了 backlight_led->brightness 变量中,因此读取的时候其实获取到的是上一次写入的值。
上述两个疑问涉及到的代码如下:
1 | // file: src/kernel/msm-3.18/drivers/leds/led-class.c |
四. LCD 上电
4.1 LCD 上电前言
每一块 lcd ic 在 spec 上都会详细的描述它们上电的时序要求。以往工厂生产的时候,经常会出现由于上电时序不对或者是各路电之前的延时过短不符合 spec 规范导致的花屏、读不到 panel id 甚至烧坏 ic 的情况。因此在阅读屏驱动框架的时候,必须要准确的找到上电这块的代码位置。
目前我们公司大部分的 LCD 都有两路电,一路是给 ic 供电的 IOVDD,另一路是加在玻璃上的偏置电压(VSP、VSN),同时在 spec 上电时序的描述中,还会多加一路 ic 的 reset 信号,spec 上会给出这三者详细的上电掉电时序要求。
我们要做的就是去 check 这个位置,看看是不是符合 spec 要求,不符合就将其修改。一般来说修改也仅仅是修改偏置电压,交换 reset 和偏置电压的前后顺序,需改时序间的延时之类的。
4.2 lk 阶段开机上电位置
1 | // file: src/bootable/bootloader/lk/app/aboot/aboot.c |
4.3 kernel 阶段上下电位置
1 | mdss_dsi_panel_power_ctrl |
五. LCD 静电
5.1 使能 esd check 功能
屏 dtsi 中增加以下属性以开启 esd check 功能:
&mdss_mdp {
dsi_co55swr8_st7703_720p_video: qcom,dsi_co55swr8_st7703_720p_video {
... ...
/* 使能 esd check 机制 */
qcom,esd-check-enabled;
/* 指定 esd check 的 command,cmd 最后一次字节是 check 的寄存器 */
qcom,mdss-dsi-panel-status-command = [
14 01 00 01 05 00 01 68
06 01 00 01 05 00 01 09
14 01 00 01 05 00 01 CC
14 01 00 01 05 00 01 AF
];
/* 指定 esd check 的命令模式 */
qcom,mdss-dsi-panel-status-command-state = "dsi_lp_mode";
/* 指定 esd check 的校验模式 */
qcom,mdss-dsi-panel-status-check-mode = "reg_read";
/* 在 command 中指定指定过了 check 的寄存器,这里指定 check 对应寄存器的前几个字节 */
qcom,mdss-dsi-panel-status-read-length = <1 3 1 1>;
/* 这个属性的含义还在摸索中 */
qcom,mdss-dsi-panel-max-error-count = <2>;
/* 指定 esd check 读取的寄存器的标准值 */
qcom,mdss-dsi-panel-status-value = <0xC0> , <0x80 0x73 0x04>, <0x0B>,<0xFD>;
... ...
};
};
5.2 追踪 esd check 流程
在 mdss_dsi.c 文件中解析 dts 中屏节点中设置的 esd 相关属性,相关代码流程如下:
// file: src/kernel/msm-3.18/drivers/video/msm/mdss/mdss_dsi.c
static int mdss_dsi_ctrl_probe(struct platform_device *pdev)
mdss_dsi_config_panel(pdev, index);
/* 根据 panel_cfg 指定的 panel 找到对应的 dts 节点 */
mdss_dsi_find_panel_of_node(pdev, panel_cfg);
// file: src/kernel/msm-3.18/drivers/video/msm/mdss/mdss_dsi_panel.c
mdss_dsi_panel_init(dsi_pan_node, ctrl_pdata, ndx);
mdss_panel_parse_dt(node, ctrl_pdata);
mdss_dsi_parse_panel_features(np, ctrl_pdata);
/* 解析 dts 中 esd check 相关的参数 */
mdss_dsi_parse_esd_params(np, ctrl);
dsi_panel_device_register(pdev, dsi_pan_node, ctrl_pdata);
/* 根据 status_mode 绑定对应的 esd check 的函数 */
if (ctrl_pdata->status_mode == ESD_REG)
ctrl_pdata->check_status = mdss_dsi_reg_status_check;
// file: src/kernel/msm-3.18/drivers/video/msm/mdss/mdss_dsi_status.c
module_init(mdss_dsi_status_init);
mdss_dsi_status_init
INIT_DELAYED_WORK(&pstatus_data->check_status, check_dsi_ctrl_status);
check_dsi_ctrl_status
pdsi_status->mfd->mdp.check_dsi_status(work, interval);
> mdss_check_dsi_ctrl_status
ctrl_pdata->check_status(ctrl_pdata);
> mdss_dsi_reg_status_check
/* 读取寄存器状态 */
mdss_dsi_read_status(ctrl_pdata);
/* 校验寄存器状态 */
ctrl_pdata->check_read_status(ctrl_pdata);
> mdss_dsi_gen_read_status(ctrl_pdata);
mdss_dsi_cmp_panel_reg_v2(ctrl_pdata);
1 | static void mdss_dsi_parse_esd_params(struct device_node *np, struct mdss_dsi_ctrl_pdata *ctrl) |
六. LCD 框架
6.1 lk 阶段 lcd 框架分析
对于 i2c、spi、usb 总线下的设备,写驱动程序的时候,涉及到两部分,分别是总线驱动和设备驱动。以 i2c 为例,总线驱动和设备驱动各自有对应的分工,总线驱动负责控制 i2c 控制器(片上外设),将 i2c 控制器寄存器相关的控制逻辑封装成符合 i2c 规范的 start、stop、ask、sendbyte、readbyte 等接口函数。至于设备驱动,只需要根据设备的 datasheet 并按照总线的规范调用总线提供的接口函数,就可以实现和设备的通信。
和我们熟悉的 i2c、spi、usb 一样,mipi dsi 协议也是一样的,也分为总线驱动和设备驱动。我们分析框架,对于总线驱动部分只需要找到对应的接口函数即可,暂不去深入分析 mipi 总线驱动,重点看看点亮一块屏,设备驱动端上电时序,如下 init code,以及让 lcd 显示一帧图片需要如何操作。
未完待续。