通过前面两个系列的学习,我们已经了解DSS系统,LCD基本原理,DSS设备树的配置等基本知识。本文简单学习和梳理LCD设备驱动的代码,方便项目中快速bring up和debug。
此系列文章基于TI的AM572x EVM开发板,使用参考代码linux-4.14.67+gitAUTOINC+d315a9bb00-gd315a9bb00
1. 设备的枚举
我们知道linux设备驱动模型里面分为设备和驱动两部分,设备由device tree负责定义,内核代码会解析设备树枚举出设备;但在阅读代码的过程中会发现OMAP相关的LCD驱动代码有3部分,分别位于:
drivers/video/fbdev/omap/
drivers/video/fbdev/omap2/
drivers/gpu/drm/omapdrm/displays/
这3部分代码很类似,前面两个是基于fb,后面一个是基于drm,我们可以通过编译选项来确认到底用到哪部分代码。以config文件tisdk_am57xx-evm_defconfig为例,它定义了如下2个宏控。
CONFIG_MACH_OMAP_GENERIC
CONFIG_SOC_DRA7XX
我们找到machine的初始化代码arch/arm/mach-omap2/board-generic.c,通过上面的2个宏控找到对应的代码入口函数定义: DT_MACHINE_START
#ifdef CONFIG_SOC_DRA7XX
static const char *const dra74x_boards_compat[] __initconst = {
"ti,dra762",
"ti,am5728",
"ti,am5726",
"ti,dra742",
"ti,dra7",
NULL,
};
DT_MACHINE_START(DRA74X_DT, "Generic DRA74X (Flattened Device Tree)")
#if defined(CONFIG_ZONE_DMA) && defined(CONFIG_ARM_LPAE)
.dma_zone_size = SZ_2G,
#endif
.reserve = omap_reserve,
.smp = smp_ops(omap4_smp_ops),
.map_io = dra7xx_map_io,
.init_early = dra7xx_init_early,
.init_late = dra7xx_init_late,
.init_irq = omap_gic_of_init,
.init_machine = omap_generic_init,
.init_time = omap5_realtime_timer_init,
.dt_compat = dra74x_boards_compat,
.restart = omap44xx_restart,
MACHINE_END
static const char *const dra72x_boards_compat[] __initconst = {
"ti,am5718",
"ti,am5716",
"ti,dra722",
"ti,dra718",
NULL,
};
DT_MACHINE_START(DRA72X_DT, "Generic DRA72X (Flattened Device Tree)")
#if defined(CONFIG_ZONE_DMA) && defined(CONFIG_ARM_LPAE)
.dma_zone_size = SZ_2G,
#endif
.reserve = omap_reserve,
.map_io = dra7xx_map_io,
.init_early = dra7xx_init_early,
.init_late = dra7xx_init_late,
.init_irq = omap_gic_of_init,
.init_machine = omap_generic_init,
.init_time = omap5_realtime_timer_init,
.dt_compat = dra72x_boards_compat,
.restart = omap44xx_restart,
MACHINE_END
#endif
其中的omap_generic_init中就包含了dss设备的初始化。其中的pdata_quirks_init函数将枚举platform设备,即dss@58000000 这个设备会被当成平台设备枚举出来;但是它的子设备如ports,dispc@58001000,encoder@58060000等则不会被枚举(参考of_platform_populate的实现)。接下来会调用omapdss_init_of();在omapdss_init_of()函数之中,再一次调用了of_platform_populate(),才将dss中的子设备建立起来。再然后调用omapdss_init_fbdev()进行fb设备的初始化,不过我们可以看到由于没有定义CONFIG_FB_OMAP2,这个函数实现为空。omapdss_init_fbdev()当中会创建fb相关的设备,对应的驱动在drivers/video/fbdev/omap2/omapfb/dss中实现,由于没有枚举设备,对应的驱动自然也不会被加载。当然通过宏的设置也可以确认对于当前的配置,我们使用的代码位于drivers/gpu/drm/omapdrm/displays/。
2. 驱动的加载
既然已经知道dss驱动使用的是drm中,可以很容易找到初始化的地方,位于文件drivers/gpu/drm/omapdrm/dss/dss.c。
/* INIT */
static struct platform_driver * const omap_dss_drivers[] = {
&omap_dsshw_driver,
&omap_dispchw_driver,
#ifdef CONFIG_OMAP2_DSS_DSI
&omap_dsihw_driver,
#endif
#ifdef CONFIG_OMAP2_DSS_VENC
&omap_venchw_driver,
#endif
#ifdef CONFIG_OMAP4_DSS_HDMI
&omapdss_hdmi4hw_driver,
#endif
#ifdef CONFIG_OMAP5_DSS_HDMI
&omapdss_hdmi5hw_driver,
#endif
};
static int __init omap_dss_init(void)
{
return platform_register_drivers(omap_dss_drivers,
ARRAY_SIZE(omap_dss_drivers));
}
根据配置确定驱动如下,到此为止剩下的工作就就是逐个分析这些驱动的实现。
&omap_dsshw_driver,
&omap_dispchw_driver,
&omapdss_hdmi4hw_driver,
&omapdss_hdmi5hw_driver,
2.1 dss hardware driver
该驱动match id是dra7-dss( { .compatible = “ti,dra7-dss”, .data = &dra7xx_dss_feats }),单独增加了的设备数据(dra7xx_dss_feats)作为dss的feature,feature对应了硬件的信息如时钟,ports(系列1中的interface),也可以从如下的驱动数据结构中看出该驱动的主要作用是对硬件进行操作,比如pll,clk,ports等。
static struct {
struct platform_device *pdev;
void __iomem *base;
struct regmap *syscon_pll_ctrl;
u32 syscon_pll_ctrl_offset;
struct clk *parent_clk;
struct clk *dss_clk;
unsigned long dss_clk_rate;
unsigned long cache_req_pck;
unsigned long cache_prate;
struct dispc_clock_info cache_dispc_cinfo;
enum dss_clk_source dsi_clk_source[MAX_NUM_DSI];
enum dss_clk_source dispc_clk_source;
enum dss_clk_source lcd_clk_source[MAX_DSS_LCD_MANAGERS];
bool ctx_valid;
u32 ctx[DSS_SZ_REGS / sizeof(u32)];
const struct dss_features *feat;
struct dss_pll *video1_pll;
struct dss_pll *video2_pll;
} dss;
和大多数驱动一样,probe中主要进行配置、参数初始化,便于后期使用。主要关心如下4个函数。
r = dss_init_ports(pdev);
r = initialize_omapdrm_device();
/* Add all the child devices as components. */
device_for_each_child(&pdev->dev, &match, dss_add_child_component);
r = component_master_add_with_match(&pdev->dev, &dss_component_ops, match);
2.1.1 dss_init_ports
dss_init_ports初始化平台支持的port,这里的feat就是前面驱动data中那个feature,它定义了我们当前平台支持的ports有哪些,而当前平台是通过compatible来确定的(如前述,”ti,dra7-dss”)。因此,可以确认我们使用的平台3个port都是DPI,这和系列文档1中DPI1,2,3是对应的。
static const enum omap_display_type dra7xx_ports[] = {
OMAP_DISPLAY_TYPE_DPI,
OMAP_DISPLAY_TYPE_DPI,
OMAP_DISPLAY_TYPE_DPI,
};
同样feat中4个outputs的定义如下,我们也可以在系列1或者spec中找到对应的硬件模块。
static const enum omap_dss_output_id omap5_dss_supported_outputs[] = {
/* OMAP_DSS_CHANNEL_LCD */
OMAP_DSS_OUTPUT_DPI | OMAP_DSS_OUTPUT_DBI |
OMAP_DSS_OUTPUT_DSI1 | OMAP_DSS_OUTPUT_DSI2,
/* OMAP_DSS_CHANNEL_DIGIT */
OMAP_DSS_OUTPUT_HDMI,
/* OMAP_DSS_CHANNEL_LCD2 */
OMAP_DSS_OUTPUT_DPI | OMAP_DSS_OUTPUT_DBI |
OMAP_DSS_OUTPUT_DSI1,
/* OMAP_DSS_CHANNEL_LCD3 */
OMAP_DSS_OUTPUT_DPI | OMAP_DSS_OUTPUT_DBI |
OMAP_DSS_OUTPUT_DSI2,
};
dss_init_ports的函数如下,我们需要分析一下该函数,因为DPI往下连接的就是LCD,也是我们最关心的配置部分。这个函数首先调用port = of_graph_get_port_by_id(parent, i);得到设备树中定义的port,然后调用dpi_init_port对其进行初始化。即根据设备树定义的port初始化一个dpi_data结构,并通过omapdss_register_output(out);将这个结构加入到output_list链表当中,我们将看到在后面的panel驱动中会查找这个链表。这里有一个配置是data-lines = <0x18>;表示LCD使用的数据宽度,也就是系列1中DPI协议中数据线的宽度。
static int dss_init_ports(struct platform_device *pdev)
{
struct device_node *parent = pdev->dev.of_node;
struct device_node *port;
int i;
for (i = 0; i < dss.feat->num_ports; i++) {
port = of_graph_get_port_by_id(parent, i);
if (!port)
continue;
switch (dss.feat->ports[i]) {
case OMAP_DISPLAY_TYPE_DPI:
dpi_init_port(pdev, port, dss.feat->model);
break;
case OMAP_DISPLAY_TYPE_SDI:
sdi_init_port(pdev, port);
break;
default:
break;
}
}
return 0;
}
2.1.2 initialize_omapdrm_device
这个函数会注册一个omapdrm平台设备,该设备的注册最终会导致对应的设备驱动被加载,可以知道这个驱动位于drivers/gpu/drm/omapdrm/omap_drv.c中。
static int initialize_omapdrm_device(void)
{
omap_drm_device = platform_device_register_simple("omapdrm", 0, NULL, 0);
if (IS_ERR(omap_drm_device))
return PTR_ERR(omap_drm_device);
return 0;
}
从目录名字可以知道该模块属于drm模块而且并不是omap特有的,其实看该模块的实现也能知道,这个模块是drm模块和硬件模块通信的初始化模块,比如probe中的函数drm_dev_register,如注释,这个函数是将自己注册到drm模块当中;该模块的初始化标志着外设模块(OMAP中的dss模块)和drm模块最终联系起来了。
/*
* Register the DRM device with the core and the connectors with
* sysfs.
*/
ret = drm_dev_register(ddev, 0);
在后面的章节学习panel驱动时我们还会看到它和drm驱动模块相关联。但我们将详细的学习放到以后的文章中进行,我们可以先简单看一下probe中drm driver的结构体,当看到MAJOR,MINOR、open,ioctl是不是觉得十分熟悉?
static struct drm_driver omap_drm_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_PRIME |
DRIVER_ATOMIC | DRIVER_RENDER,
.open = dev_open,
.lastclose = dev_lastclose,
#ifdef CONFIG_DEBUG_FS
.debugfs_init = omap_debugfs_init,
#endif
.prime_handle_to_fd = drm_gem_prime_handle_to_fd,
.prime_fd_to_handle = drm_gem_prime_fd_to_handle,
.gem_prime_export = omap_gem_prime_export,
.gem_prime_import = omap_gem_prime_import,
.gem_free_object = omap_gem_free_object,
.gem_vm_ops = &omap_gem_vm_ops,
.dumb_create = omap_gem_dumb_create,
.dumb_map_offset = omap_gem_dumb_map_offset,
.ioctls = ioctls,
.num_ioctls = DRM_OMAP_NUM_IOCTLS,
.fops = &omapdriver_fops,
.name = DRIVER_NAME,
.desc = DRIVER_DESC,
.date = DRIVER_DATE,
.major = DRIVER_MAJOR,
.minor = DRIVER_MINOR,
.patchlevel = DRIVER_PATCHLEVEL,
};
2.1.3 master add with match
这两个函数将dss设备作为一个master,并将其子节点作为componet加到该master的match链表当中,在有componet增加成功的时候相应的回调被调用,并bring up master。
其中device_for_each_child遍历dss节点下的子节点,每个子节点会对应生成一个component_match结构,该结构中的componet_match_array中的data指针即对应着子节点设备,最终所有的子节点的component生成一个match数组;component_master_add_with_match将这个macth数组和master也就是dss设备关联起来,其实就是生成一个struct master结构体,该结构体包括master、match数组和ops回调函数,这个结构体会被挂载到一个全局链表。 component_master_add_with_match函数最后还会调用try_to_bring_up_master,这个函数的目的bringup master,其实就是查看master对应的componet中是否有匹配并调用ops回调函数中的bind函数。同理,对于component来说,在增加component的时候也会调用bind函数(比如我们在下一节中看到的dispc,就是一个component)。
/* Add all the child devices as components. */
device_for_each_child(&pdev->dev, &match, dss_add_child_component);
r = component_master_add_with_match(&pdev->dev, &dss_component_ops, match);
2.2 component driver
dispc驱动位于drivers/gpu/drm/omapdrm/dss/dispc.c当中,它的match列表为 { .compatible = “ti,dra7-dispc”, .data = &omap54xx_dispc_feats }。该模块的probe函数如下,可以看到它直接调用component_add,根据上一节的介绍,这个函数会导致ops中的bind函数被调用,也就是dispc_bind被会被调用,详细分析bind函数在后面再继续学习。同理,hdmi模块等其它componet也类似。
static int dispc_probe(struct platform_device *pdev)
{
return component_add(&pdev->dev, &dispc_component_ops);
}
2.3 dpi driver
在am5728上LCD的接口是dpi,和lcd驱动关系最大的应该算是dpi驱动了。找到对应的文件/drivers/gpu/omapdrm/displays/panle-dpi.c会发现这个模块的match列表为{ .compatible = “omapdss,panel-dpi”, },而相应的设备树中找不到哪个节点的compatible属性和之相同,这是怎么回事呢?
2.3.1 修改match列表
通过查看代码,可以发现在drivers/gpu/drm/omapdrm/dss/omapdss-boot-init.c文件,会修改节点的compatible属性并加上”omapdss”,并且这个模块在驱动模块加载之前就已经初始化完成,这也就使得dpi驱动模块能够有机会被加载。
subsys_initcall(omapdss_boot_init);
在这个模块中会调用omapdss_walk_device遍历dss节点和子节点,找到“ports”或“port”节点,然后根据remote-endpoint找到远程endpoint节点,以实际一个例子(为了方便下面以及后面贴出来的设备树是反编译出来的结果,实际上和按照系列2中找出来节点一样),找到0x248这个节点也即lcd_in这节点,在其parent节点的compatible上增加”omapdss,”,即最终的compatible是”omapdss,osddisplays,osd070t1718-19ts”,”omapdss,panel-dpi”;所以panel-dpi.c中的模块会被加载。
display {
phandle = <0x24b>;
label = "lcd";
enable-gpios = <0xaf 0x5 0x0>;
backlight = <0x246>;
compatible = "osddisplays,osd070t1718-19ts", "panel-dpi";
port {
endpoint {
phandle = <0x248>;
remote-endpoint = <0x247>;
};
};
panel-timing {
vsync-len = <0xd>;
vsync-active = <0x0>;
vfront-porch = <0x16>;
vback-porch = <0xa>;
vactive = <0x1e0>;
pixelclk-active = <0x1>;
hsync-len = <0x1e>;
hsync-active = <0x0>;
hfront-porch = <0xd2>;
hback-porch = <0x10>;
hactive = <0x320>;
de-active = <0x1>;
clock-frequency = <0x1f78a40>;
};
};
2.3.2 注册display
在设备驱动挂载之后probe函数被调用,probe中会解析dispaly中的配置,初始化struct omap_dss_device结构体,并注册到系统,即增加到panel_list列表;omap_dss_device结构体已经指定了panel的类型(如dpi驱动中的类型为OMAP_DISPLAY_TYPE_DPI)和panel的参数(如下面的timing)等。
int omapdss_register_display(struct omap_dss_device *dssdev)
{
struct omap_dss_driver *drv = dssdev->driver;
int id;
/*
* Note: this presumes that all displays either have an DT alias, or
* none has.
*/
id = of_alias_get_id(dssdev->dev->of_node, "display");
if (id < 0)
id = disp_num_counter++;
dssdev->alias_id = id;
/* Use 'label' property for name, if it exists */
of_property_read_string(dssdev->dev->of_node, "label", &dssdev->name);
if (dssdev->name == NULL)
dssdev->name = devm_kasprintf(dssdev->dev, GFP_KERNEL,
"display%d", id);
if (drv && drv->get_timings == NULL)
drv->get_timings = omapdss_default_get_timings;
mutex_lock(&panel_list_mutex);
list_add_tail(&dssdev->panel_list, &panel_list);
mutex_unlock(&panel_list_mutex);
return 0;
}
我们知道display和dpi接口是对应的,代码是怎么样把它们联系起来?查看probe中的函数panel_dpi_probe_of,它会通过reg属性找到dss节点中的port。
in = omapdss_of_find_source_for_first_ep(node);
对于具体的例子来说就是下面这个节点,这样就如设备树中定义的那样(通过remote-endpoint),这两者相互联系起来了。
ports {
#size-cells = <0x0>;
#address-cells = <0x1>;
port {
reg = <0x0>;
endpoint {
phandle = <0x247>;
remote-endpoint = <0x248>;
data-lines = <0x18>;
};
};
};
2.3.3 display的connect
上一节看到display的注册只是将display加到了一个全局链表上,并将dispaly(或者称为LCD模块)和DPI接口联系起来,实际上它们还要通过connect将两者进行初始化。在注册display的时候,我们看到omap_dss_device结构体中有一个driver变量,它被赋值为panel_dpi_ops,该变量中的connect会最终调用DPI中的connect进行初始化,下面继续分析。
static struct omap_dss_driver panel_dpi_ops = {
.connect = panel_dpi_connect,
.disconnect = panel_dpi_disconnect,
.enable = panel_dpi_enable,
.disable = panel_dpi_disable,
.set_timings = panel_dpi_set_timings,
.get_timings = panel_dpi_get_timings,
.check_timings = panel_dpi_check_timings,
};
回到drivers/gpu/drm/omap/drm/omap_drv.c文件,在前面章节中已经知道该模块被会加载,并且调用了drm_dev_register来注册drm设备。在probe当中,我们看ret = omap_connect_dssdevs(ddev);函数的实现,这个函数中的connect是不是就是有前面的panel_dpi_ops呢?跟踪omap_collect_dssdevs()可以最终找到omap_dss_get_next_device函数,而omap_dss_get_next_device函数确实是在查找panel_list列表,猜测正确。
omap_collect_dssdevs(ddev);
for (i = 0; i < priv->num_dssdevs; i++) {
struct omap_dss_device *dssdev = priv->dssdevs[i];
r = dssdev->driver->connect(dssdev);
if (r == -EPROBE_DEFER)
goto cleanup;
else if (r)
dev_warn(dssdev->dev, "could not connect display: %s\n",
dssdev->name);
else
working |= BIT(i);
}
可以看到panel-dpi.c中的connect最终调用的是dpi接口中的connect
static int panel_dpi_connect(struct omap_dss_device *dssdev)
{
struct panel_drv_data *ddata = to_panel_data(dssdev);
struct omap_dss_device *in = ddata->in;
int r;
if (omapdss_device_is_connected(dssdev))
return 0;
r = in->ops.dpi->connect(in, dssdev);
if (r)
return r;
return 0;
}
也就是dss_init_ports中的ops中的connect。具体包括初始化上电,pll,以及dss_mgr_connect等等,后面再学习。
static int dpi_connect(struct omap_dss_device *dssdev,
struct omap_dss_device *dst)
{
struct dpi_data *dpi = dpi_get_data_from_dssdev(dssdev);
enum omap_channel channel = dpi->output.dispc_channel;
int r;
r = dpi_init_regulator(dpi);
if (r)
return r;
dpi_init_pll(dpi);
r = dss_mgr_connect(channel, dssdev);
if (r)
return r;
r = omapdss_output_set_device(dssdev, dst);
if (r) {
DSSERR("failed to connect output to new device: %s\n",
dst->name);
dss_mgr_disconnect(channel, dssdev);
return r;
}
return 0;
}
2.3.4 题外话
目前为止我们只看到dpi接口的LCD驱动,在系列1中我们知道OMAP平台支持其它接口如DSI,DBI等,这些接口的驱动如何学习呢,其实看display/目录下的文件可知,对于其它的LCD驱动,最终目的都是去注册omapdss_register_display,既然已经将流程打通,具体遇到问题或者需要时再来学习不迟。
3. DPI显示屏的配置
通过前面的学习,我可以知道基本所有的配置都在设备树当中,且都在本系列文章中列出,比如电源video-supply,色彩深度data-lines,使能引脚enable-gpios,背光backlight等等。以及最重要的时序部分panel-timing,时序部分需要结合panel的spec进行设置,和系列1中dpi的timing图类似。