Rockchip RK3399 - DRM HDMI驱动程序
目录
- 一、设备树配置
- 二、Platform驱动
- 三、HDMI数据结构
- 四、dw_hdmi_rockchip_bind
- 五、dw_hdmi_probe
- 六、drm_add_edid_modes
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4
开发板
eMMC
:16GB
LPDDR3
:4GB
显示屏 :15.6
英寸HDMI
接口显示屏
u-boot
:2023.04
linux
:6.3
----------------------------------------------------------------------------------------------------------------------------
在《Rockchip RK3399 - DRM HDMI
介绍》我们已经对HDMI
协议进行了详细的介绍,本节我们选择DRM HDMI
驱动程序作为分析的对象。
这里我们介绍一下Rochchip DRM
驱动中与hdmi
相关的实现,具体实现文件:
drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
;drivers/gpu/drm/bridge/synopsys/
;
由于Rockchip
采用了Synopsys
的DesignWare HDMI IP
解决方案,因此hdmi
驱动的核心实现是位于drivers/gpu/drm/bridge/synopsys/
目录下的,而Rockchip
仅仅是对其进行一层封装。
对于DesignWare HDMI
控制器,通常会有与之配套的PHY
驱动,以确保HDMI
信号能够正确地通过电缆传输,具体来说:
HDMI
控制器负责视频和音频数据的处理、编码以及通过HDMI
协议传输这些数据;HDMI PHY
则负责将来自控制器的数字信号转换为符合HDMI
规范的电气信号,进行物理层的传输;- 对于当前我使用的
Linux
内核版本,DesignWare HDMI PHY
驱动被集成在drivers/gpu/drm/bridge/synopsys/dw_hdmi.c
驱动文件中,我们在drivers/phy/rockchip/
目录下并没有找到单独的DesignWare HDMI PHY
驱动;
在介绍hdmi
驱动之前,我们首先思考一个问题,假设我们自己是一个画家,现在我手里有一个笔、还有一张纸,然后我打算在纸上绘画一个卡通人物,接下来我们会怎么做呢?
-
首先我们需要对我们绘画使用的纸的尺寸有一个了解;
- 如果是
A3
上的图纸,那么我就会在大脑里构思一个A3
大小的卡通人物; - 如果是
A4
,那么我就会在大脑里构思一个A4
大小的卡通人物; - 总之我们的目的是要让卡通人物占满整张纸;
- 如果是
-
接下来我们就会使用不同颜色的画笔开始在纸上绘画了,而我们的绘画过程呢,就是将大脑中构思的卡通人物按照从左到右、从上到下一笔一笔的勾勒出来;
同样的,类比到DRM
显示子系统中;
hdmi
显示器等价于绘画的纸:LCD
驱动器会将接收到的数据在显示器上显示出来;RK3399 crtc
等价于画笔:crtc
从framebuffer
中读取待显示的图像,并按照响应的格式输出给encoder
;- 对于
crtc
来说,其承担了各种时序参数配置的重任; encoder
实际上就是进行的编码工作,对于HDMI
来说来用的就是TMDS
协议,经过编码之后的数据就可以通过HDMI
线缆输出到HDMI
显示器了;这个输出的过程就类似于我们绘画的过程:从左到右、从上到下;
- 对于
framebuffer
等价于大脑中构思的卡通人物:framebuffer
就是一块驱动和应用层都能访问的内存,这块内存中描述了使用的显示器的分辨率、色彩描述(RGB24
,I420
,YUUV
等等)、以及要真正要显示的内容所在的虚拟地址(通过GEM
分配物理内存);
一、设备树配置
1.1 hdmi
设备节点
设备节点vopb
下的子节点vopb_out_hdmi
通过hdmi_in_vopb
(由remote-endpoint
属性指定)和hdmi
显示接口组成一个连接通路;
设备节点vopl
下的子节点vopl_out_hdmi
通过hdmi_in_vopl
(由remote-endpoint
属性指定)和hdmi
显示接口组成一个连接通路;
hdmi
设备节点定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi
;
hdmi: hdmi@ff940000 {
compatible = "rockchip,rk3399-dw-hdmi";
reg = <0x0 0xff940000 0x0 0x20000>;
interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>;
clocks = <&cru PCLK_HDMI_CTRL>,
<&cru SCLK_HDMI_SFR>,
<&cru SCLK_HDMI_CEC>,
<&cru PCLK_VIO_GRF>,
<&cru PLL_VPLL>;
clock-names = "iahb", "isfr", "cec", "grf", "ref";
power-domains = <&power RK3399_PD_HDCP>;
reg-io-width = <4>;
rockchip,grf = <&grf>;
#sound-dai-cells = <0>;
status = "disabled";
ports {
hdmi_in: port {
#address-cells = <1>;
#size-cells = <0>;
hdmi_in_vopb: endpoint@0 {
reg = <0>;
remote-endpoint = <&vopb_out_hdmi>;
};
hdmi_in_vopl: endpoint@1 {
reg = <1>;
remote-endpoint = <&vopl_out_hdmi>;
};
};
};
};
其中:
- 子节点
ports
:包含2个input endpoint
,分别连接到vopl
和vopb
;也就是在rk3399
上,hdmi
可以和vopl
(只支持 2K)、vopb
(支持 4K)连接;
因此可以得到有2条通路:
vopb_out_hdmi
--->hdmi_in_vopb
;vopl_out_hdmi
--->hdmi_in_vopl
;
需要注意的是:
- 两个
vop
可以分别与两个显示接口绑定(一个显示接口只能和一个vop
绑定),且可以相互交换: - ⼀个显⽰接口在同⼀个时刻只能和⼀个
vop
连接,所以在具体的板级配置中,需要设备树中把要使⽤的通路打开,把不使⽤的通路设置为disabled
状态。
1.2 启用hdmi
如果我们希望hdmi
连接在vopb
上,则需要在arch/arm64/boot/dts/rockchip/rk3399-evb.dts
中为以下节点新增属性:
&i2c7 {
status = "okay";
};
# 使能显示子系统
&display_subsystem {
status = "okay";
};
# 使能vopb
&vopb {
status = "okay";
};
&vopb_mmu {
status = "okay";
};
# 使能hdmi
&hdmi {
ddc-i2c-bus = <&i2c7>;
pinctrl-names = "default";
pinctrl-0 = <&hdmi_cec>;
status = "okay";
};
# hdmi绑定到vopb
&hdmi_in_vopb{
status = "okay";
};
# 禁止hdmi绑定到vopl
&hdmi_in_vopl{
status = "disabled";
};
二、Platform
驱动
2.1 模块入口函数
在rockchip_drm_init
函数中调用:
static int __init rockchip_drm_init(void)
{
int ret;
if (drm_firmware_drivers_only())
return -ENODEV;
// 1. 根据配置来决定是否添加xxx_xxx_driver到数组rockchip_sub_drivers
num_rockchip_sub_drivers = 0;
ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,CONFIG_ROCKCHIP_DW_HDMI);
......
// 2. 注册多个platform driver
ret = platform_register_drivers(rockchip_sub_drivers,
num_rockchip_sub_drivers);
if (ret)
return ret;
// 3. 注册rockchip_drm_platform_driver
ret = platform_driver_register(&rockchip_drm_platform_driver);
if (ret)
goto err_unreg_drivers;
return 0;
......
}
其中:
ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,CONFIG_ROCKCHIP_DW_HDMI);
会将vop_platform_driver
保存到rockchip_sub_drivers
数组中。
并调用platform_register_drivers
遍历rockchip_sub_drivers
数组,多次调用platform_driver_register
注册platform driver
。
2.2 dw_hdmi_rockchip_pltfm_driver
dw_hdmi_rockchip_pltfm_driver
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
;
struct platform_driver dw_hdmi_rockchip_pltfm_driver = {
.probe = dw_hdmi_rockchip_probe,
.remove = dw_hdmi_rockchip_remove,
.driver = {
.name = "dwhdmi-rockchip",
.pm = &dw_hdmi_rockchip_pm,
.of_match_table = dw_hdmi_rockchip_dt_ids, // 用于设备树匹配
},
};
2.2.1 of_match_table
其中of_match_table
用于设备树匹配,匹配设备树中compatible = "rockchip,rk3399-dw-hdmi"
的设备节点;
static const struct dw_hdmi_plat_data rk3399_hdmi_drv_data = {
.mode_valid = dw_hdmi_rockchip_mode_valid,
.mpll_cfg = rockchip_mpll_cfg,
.cur_ctr = rockchip_cur_ctr,
.phy_config = rockchip_phy_config,
.phy_data = &rk3399_chip_data,
.use_drm_infoframe = true,
};
static const struct of_device_id dw_hdmi_rockchip_dt_ids[] = {
{ .compatible = "rockchip,rk3228-dw-hdmi",
.data = &rk3228_hdmi_drv_data
},
{ .compatible = "rockchip,rk3288-dw-hdmi",
.data = &rk3288_hdmi_drv_data
},
{ .compatible = "rockchip,rk3328-dw-hdmi",
.data = &rk3328_hdmi_drv_data
},
{ .compatible = "rockchip,rk3399-dw-hdmi",
.data = &rk3399_hdmi_drv_data
},
{ .compatible = "rockchip,rk3568-dw-hdmi",
.data = &rk3568_hdmi_drv_data
},
{},
};
2.2.2 dw_hdmi_rockchip_probe
在platform
总线设备驱动模型中,我们知道当内核中有platform
设备和platform
驱动匹配,会调用到platform_driver
里的成员.probe
,在这里就是dw_hdmi_rockchip_probe
函数;
static const struct component_ops dw_hdmi_rockchip_ops = {
.bind = dw_hdmi_rockchip_bind,
.unbind = dw_hdmi_rockchip_unbind,
};
static int dw_hdmi_rockchip_probe(struct platform_device *pdev)
{
return component_add(&pdev->dev, &dw_hdmi_rockchip_ops);
}
这里代码很简单,就是为设备pdev->dev
向系统注册一个component
,其中组件可执行的初始化操作被设置为了dw_hdmi_rockchip_ops
,我们需要重点关注bind
函数的实现,这个我们单独小节介绍。
三、HDMI
数据结构
hdmi
相关的数据结构分为两部分:
- 由
DesignWare hdmi
相关驱动定义:比如struct dw_hdmi
、struct dw_hdmi_plat_data
; - 由
Rochchip hdmi
相关驱动定义:比如struct rockchip_hdmi
,struct rockchip_hdmi_chip_data
;
3.1 DesignWare hdmi
3.1.1 struct dw_hdmi
struct dw_hdmi
定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
;
struct dw_hdmi {
struct drm_connector connector;
struct drm_bridge bridge;
struct drm_bridge *next_bridge;
unsigned int version;
struct platform_device *audio;
struct platform_device *cec;
struct device *dev;
struct clk *isfr_clk;
struct clk *iahb_clk;
struct clk *cec_clk;
struct dw_hdmi_i2c *i2c;
struct hdmi_data_info hdmi_data;
const struct dw_hdmi_plat_data *plat_data;
int vic;
u8 edid[HDMI_EDID_LEN];
struct {
const struct dw_hdmi_phy_ops *ops;
const char *name;
void *data;
bool enabled;
} phy;
struct drm_display_mode previous_mode;
struct i2c_adapter *ddc;
void __iomem *regs;
bool sink_is_hdmi;
bool sink_has_audio;
struct pinctrl *pinctrl;
struct pinctrl_state *default_state;
struct pinctrl_state *unwedge_state;
struct mutex mutex; /* for state below and previous_mode */
enum drm_connector_force force; /* mutex-protected force state */
struct drm_connector *curr_conn;/* current connector (only valid when !disabled) */
bool disabled; /* DRM has disabled our bridge */
bool bridge_is_on; /* indicates the bridge is on */
bool rxsense; /* rxsense state */
u8 phy_mask; /* desired phy int mask settings */
u8 mc_clkdis; /* clock disable register */
spinlock_t audio_lock;
struct mutex audio_mutex;
unsigned int sample_non_pcm;
unsigned int sample_width;
unsigned int sample_rate;
unsigned int channels;
unsigned int audio_cts;
unsigned int audio_n;
bool audio_enable;
unsigned int reg_shift;
struct regmap *regm;
void (*enable_audio)(struct dw_hdmi *hdmi);
void (*disable_audio)(struct dw_hdmi *hdmi);
struct mutex cec_notifier_mutex;
struct cec_notifier *cec_notifier;
hdmi_codec_plugged_cb plugged_cb;
struct device *codec_dev;
enum drm_connector_status last_connector_result;
};
其中:
connector
:连接器;bridge
:桥接设备,一般用于注册encoder后面另外再接的转换芯片;audio
:音频platform device
;cec
:CEC platform device
;dev
:hdmi
设备;isfr_clk
、iahb_clk
、cec_clk
:hdmi
相关的时钟;plat_data
:dw hdmi
平台数据;ddc
:存储DDC
通道使用的I2C
总线适配器;edid
:存放edid
信息;regs
:hdmi
相关寄存器基址的虚拟地址;pinctrl
、default_state
、unwedge_state
:引脚状态配置信息;reg_shift
:寄存器地址偏移;sample_width
:音频采样位数;sample_rate
:音频采样率;channels
:通道数;regm
:寄存器映射,用于通过regmap
模型访问hdmi
相关寄存器;enable_audio
:启用音频回调函数;disable_audio
:禁用音频回调函数;
3.1.2 struct dw_hdmi_plat_data
struct dw_hdmi_plat_data
定义在include/drm/bridge/dw_hdmi.h
;
struct dw_hdmi_plat_data {
struct regmap *regm;
unsigned int output_port;
unsigned long input_bus_encoding;
bool use_drm_infoframe;
bool ycbcr_420_allowed;
/*
* Private data passed to all the .mode_valid() and .configure_phy()
* callback functions.
*/
void *priv_data;
/* Platform-specific mode validation (optional). */
enum drm_mode_status (*mode_valid)(struct dw_hdmi *hdmi, void *data,
const struct drm_display_info *info,
const struct drm_display_mode *mode);
/* Platform-specific audio enable/disable (optional) */
void (*enable_audio)(struct dw_hdmi *hdmi, int channel,
int width, int rate, int non_pcm);
void (*disable_audio)(struct dw_hdmi *hdmi);
/* Vendor PHY support */
const struct dw_hdmi_phy_ops *phy_ops;
const char *phy_name;
void *phy_data;
unsigned int phy_force_vendor;
/* Synopsys PHY support */
const struct dw_hdmi_mpll_config *mpll_cfg;
const struct dw_hdmi_curr_ctrl *cur_ctr;
const struct dw_hdmi_phy_config *phy_config;
int (*configure_phy)(struct dw_hdmi *hdmi, void *data,
unsigned long mpixelclock);
unsigned int disable_cec : 1;
};
(1) 结构体struct dw_hdmi_mpll_config
定义在如下:
struct dw_hdmi_mpll_config {
unsigned long mpixelclock;
struct {
u16 cpce;
u16 gmp;
} res[DW_HDMI_RES_MAX];
};
各项参数说明如下:
mpixelclock
:像素时钟;cpce
:OPMODE_PLLCFG
寄存器值;gmp
:PLLGMPCTRL
寄存器值;
3.1.3 struct edid
linux
使用struct edid
描述edid
主块信息;
struct edid {
u8 header[8]; // 0x00~0x07
/* Vendor & product info */
u8 mfg_id[2]; // 0x08~0x09
u8 prod_code[2]; // 0x0A~0x0B
u32 serial; /* FIXME: byte order,0x0C~0X0F */
u8 mfg_week; // 0X10
u8 mfg_year; // 0x11
/* EDID version */
u8 version; // 0x12
u8 revision; // 0x13
/* Display info: */
u8 input; // 0x14
u8 width_cm; // 0x15
u8 height_cm; // 0x16
u8 gamma; // 0x17
u8 features; // 0x18
/* Color characteristics */
u8 red_green_lo; // 0x19
u8 blue_white_lo; // 0x1A
u8 red_x; // 0x1B
u8 red_y; // 0x1c
u8 green_x; // 0x1D
u8 green_y; // 0x1E
u8 blue_x; // 0x1F
u8 blue_y; // 0x20
u8 white_x; // 0x21
u8 white_y; // 0x22
/* Est. timings and mfg rsvd timings*/
struct est_timings established_timings; // 0x23~0x25
/* Standard timings 1-8*/
struct std_timing standard_timings[8]; // 0x26~0X35
/* Detailing timings 1-4 */
struct detailed_timing detailed_timings[4]; // 0X36~0X7D
/* Number of 128 byte ext. blocks */
u8 extensions; // 0x7E
/* Checksum */
u8 checksum; // 0X7F
} __attribute__((packed));
该数据结构保存edit
主块128字节的信息,具体参考《Rockchip RK3399 - DRM HDMI介绍
》。
3.1.4 struct est_timings
edid
的Established Timings
信息在linux
中使用struct est_timings
表示;
struct est_timings {
u8 t1;
u8 t2;
u8 mfg_rsvd;
} __attribute__((packed));
3.1.5 struct std_timing
edid
的Standard Timings
信息在linux
中使用struct std_timing
表示;
/* 00=16:10, 01=4:3, 10=5:4, 11=16:9 */
#define EDID_TIMING_ASPECT_SHIFT 6
#define EDID_TIMING_ASPECT_MASK (0x3 << EDID_TIMING_ASPECT_SHIFT)
/* need to add 60 */
#define EDID_TIMING_VFREQ_SHIFT 0
#define EDID_TIMING_VFREQ_MASK (0x3f << EDID_TIMING_VFREQ_SHIFT)
struct std_timing {
u8 hsize; /* need to multiply by 8 then add 248 */
u8 vfreq_aspect;
} __attribute__((packed));
3.1.6 struct detailed_timing
edid
中的Detailed Timings
,它分为4个块(Block
),每个块占用18个字节,一共72个字节。
每个块既可以是一个时序说明(Timing Descriptor
)也可以是一个显示器描述符(Monitor Descriptor
)。
struct detailed_timing
就是用来描述每一个Detailed Timing
,定义在include/drm/drm_edid.h
;
struct detailed_timing {
__le16 pixel_clock; /* need to multiply by 10 KHz */
union {
struct detailed_pixel_timing pixel_data; // Timing Descriptor
struct detailed_non_pixel other_data; // Monitor Descriptor
} __attribute__((packed)) data;
} __attribute__((packed));
这里需要注意的是pixel_clock
,如果edid
信息中存放的值为0xBCD3
=48339
,在drm_mode_detailed
函数中pixel_clock
的值会被赋值为48339*10=483390
;
实际像素时钟频率为 48339*10000=483390000Hz
,TMDS
时钟频率为483390000Hz*10=4833900KHz
所以pixel_clock
的单位为10kHZ
;
(1) struct detailed_pixel_timing
/* If detailed data is pixel timing */
struct detailed_pixel_timing {
u8 hactive_lo;
u8 hblank_lo;
u8 hactive_hblank_hi;
u8 vactive_lo;
u8 vblank_lo;
u8 vactive_vblank_hi;
u8 hsync_offset_lo;
u8 hsync_pulse_width_lo;
u8 vsync_offset_pulse_width_lo;
u8 hsync_vsync_offset_pulse_width_hi;
u8 width_mm_lo;
u8 height_mm_lo;
u8 width_height_mm_hi;
u8 hborder;
u8 vborder;
u8 misc;
} __attribute__((packed));
(2) struct detailed_non_pixel
struct detailed_non_pixel {
u8 pad1; /* 值为0,标识该block被使用 */
u8 type; /* ff=serial, fe=string, fd=monitor range, fc=monitor name
fb=color point data, fa=standard timing data,
f9=undefined, f8=mfg. reserved */
u8 pad2;
union {
struct detailed_data_string str;
struct detailed_data_monitor_range range;
struct detailed_data_wpindex color;
struct std_timing timings[6]; // type=EDID_DETAIL_STD_MODES=0xfa时生效
struct cvt_timing cvt[4]; // type=EDID_DETAIL_CVT_3BYTE=0xf8时生效
} __attribute__((packed)) data;
} __attribute__((packed));
3.2 Rockchip hdmi
3.2.1 struct rockchip_hdmi
struct rockchip_hdmi
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
,这是Rockchip
平台定义的hdmi
结构体,其对struct dw_hdmi
进行了扩充,用于表示Rockchip
平台上的hdmi
设备。
struct rockchip_hdmi {
struct device *dev;
struct regmap *regmap;
struct rockchip_encoder encoder;
const struct rockchip_hdmi_chip_data *chip_data;
struct clk *ref_clk;
struct clk *grf_clk;
struct dw_hdmi *hdmi;
struct regulator *avdd_0v9;
struct regulator *avdd_1v8;
struct phy *phy;
};
其中:
dev
:指向设备的struct device
的指针;regmap
:指向寄存器映射的struct regmap
d额指针;encoder
:指向Rockchip
平台定义的encoder
指针;chip_data
:指向Rockchip
平台定义的hdmi data
指针;ref_clk
:ref
时钟;grf_clk
:grf
时钟;avdd_0v9
:0.9V
稳压器;avdd_1v8
:1.8V
稳压器;phy
:指向HDMI PHY
的struct phy
的指针;
3.2.2 struct rockchip_hdmi_chip_data
struct rockchip_hdmi_chip_data
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
,用于描述不同型号的Rockchip
芯片的HDMI
接口配置信息;
/**
* struct rockchip_hdmi_chip_data - splite the grf setting of kind of chips
* @lcdsel_grf_reg: grf register offset of lcdc select
* @lcdsel_big: reg value of selecting vop big for HDMI
* @lcdsel_lit: reg value of selecting vop little for HDMI
*/
struct rockchip_hdmi_chip_data {
int lcdsel_grf_reg;
u32 lcdsel_big;
u32 lcdsel_lit;
};
其中:
lcdsel_grf_reg
:表示GRF
寄存器中LCD
控制器选择寄存器的偏移量,该寄存器用于选择使用哪个vop
进行HDMI
输出;lcdsel_big
:表示在GRF
寄存器中lcdsel_grf_reg
偏移位置处设置的值,用于选择vopb
进行HDMI
输出;lcdsel_lit
:表示在GRF
寄存器中lcdsel_grf_reg
偏移位置处设置的值,用于选择vopl
进行HDMI
输出。
3.2.3 struct rockchip_encoder
struct rockchip_encoder
定义在drivers/gpu/drm/rockchip/rockchip_drm_drv.h
,这是Rockchip
平台定义的encoder
结构体,用于表示Rockchip
平台上的编码器设备。其对struct drm_encoder
进行了扩充;
struct rockchip_encoder {
int crtc_endpoint_id;
struct drm_encoder encoder;
};
其中:
crtc_endpoint_id
:表示crtc
端点的ID
,用于标识该编码器设备连接到哪个vop
;drm_encoder encoder
:表示DRM encoder
的相关信息;
四、dw_hdmi_rockchip_bind
dw_hdmi_rockchip_bind
函数定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
,该函数的代码虽然看着那么多,实际上主要就做了以下几件事;
- 解析
hdmi
设备节点,涉及到clocks
、rockchip,grf
、avdd-0v9
、avdd-1v8
、以及endpoint
子节点; - 初始化
encoder
; - 构造
dw_hdmi_bind
函数需要的参数,尤其是第三个参数rk3399_hdmi_drv_data
,最后调用dw_hdmi_bind
进入到DesignWare hdmi
驱动;
具体代码如下:
static int dw_hdmi_rockchip_bind(struct device *dev, struct device *master,
void *data)
{
struct platform_device *pdev = to_platform_device(dev);
struct dw_hdmi_plat_data *plat_data;
const struct of_device_id *match;
struct drm_device *drm = data;
struct drm_encoder *encoder;
struct rockchip_hdmi *hdmi;
int ret;
if (!pdev->dev.of_node)
return -ENODEV;
// 动态分配内存,指向struct rockchip_hdmi
hdmi = devm_kzalloc(&pdev->dev, sizeof(*hdmi), GFP_KERNEL);
if (!hdmi)
return -ENOMEM;
// 根据设备的设备节点和匹配表进行匹配,并返回匹配项
match = of_match_node(dw_hdmi_rockchip_dt_ids, pdev->dev.of_node);
// 分配内存,指向一个struct dw_hdmi_plat_data,并复制rk3399_hdmi_drv_data数据
plat_data = devm_kmemdup(&pdev->dev, match->data,
sizeof(*plat_data), GFP_KERNEL);
if (!plat_data)
return -ENOMEM;
// 设置device设备
hdmi->dev = &pdev->dev;
// 设置数据
hdmi->chip_data = plat_data->phy_data;
plat_data->phy_data = hdmi;
encoder = &hdmi->encoder.encoder;
// 基于hdmi设备节点的信息,确定特定encoder端口可能连接的CRTC
encoder->possible_crtcs = drm_of_find_possible_crtcs(drm, dev->of_node);
// 获取设备节点hdmi子节点hdmi_in_vopb的属性remote-endpoint指定vopb_out_hdmi节点的reg的值,用来初始化encoder->crtc_endpoint_id
rockchip_drm_encoder_set_crtc_endpoint_id(&hdmi->encoder,
dev->of_node, 0, 0);
/*
* If we failed to find the CRTC(s) which this encoder is
* supposed to be connected to, it's because the CRTC has
* not been registered yet. Defer probing, and hope that
* the required CRTC is added later.
*/
if (encoder->possible_crtcs == 0)
return -EPROBE_DEFER;
// 解析hdmi设备节点,比如clocks 、rockchip,grf、avdd-0v9、avdd-1v8等属性;
ret = rockchip_hdmi_parse_dt(hdmi);
if (ret) {
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(hdmi->dev, "Unable to parse OF data\n");
return ret;
}
// 查找并获取一个可选的 PHY(物理层设备)的引用
hdmi->phy = devm_phy_optional_get(dev, "hdmi");
if (IS_ERR(hdmi->phy)) {
ret = PTR_ERR(hdmi->phy);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(hdmi->dev, "failed to get phy\n");
return ret;
}
// 使能AVDD_0V9电源
ret = regulator_enable(hdmi->avdd_0v9);
if (ret) {
DRM_DEV_ERROR(hdmi->dev, "failed to enable avdd0v9: %d\n", ret);
goto err_avdd_0v9;
}
// 使能AVDD_1V8电源
ret = regulator_enable(hdmi->avdd_1v8);
if (ret) {
DRM_DEV_ERROR(hdmi->dev, "failed to enable avdd1v8: %d\n", ret);
goto err_avdd_1v8;
}
// 准备和使能时钟
ret = clk_prepare_enable(hdmi->ref_clk);
if (ret) {
DRM_DEV_ERROR(hdmi->dev, "Failed to enable HDMI reference clock: %d\n",
ret);
goto err_clk;
}
// 不匹配
if (hdmi->chip_data == &rk3568_chip_data) {
regmap_write(hdmi->regmap, RK3568_GRF_VO_CON1,
HIWORD_UPDATE(RK3568_HDMI_SDAIN_MSK |
RK3568_HDMI_SCLIN_MSK,
RK3568_HDMI_SDAIN_MSK |
RK3568_HDMI_SCLIN_MSK));
}
// 设置encoder的辅助函数helper_private为dw_hdmi_rockchip_encoder_helper_funcs
drm_encoder_helper_add(encoder, &dw_hdmi_rockchip_encoder_helper_funcs);
// encoder初始化
drm_simple_encoder_init(drm, encoder, DRM_MODE_ENCODER_TMDS);
// 设置驱动私有数据 pdev->dev.driver_data = hdmi
platform_set_drvdata(pdev, hdmi);
// 初始化HDMI接口
hdmi->hdmi = dw_hdmi_bind(pdev, encoder, plat_data);
/*
* If dw_hdmi_bind() fails we'll never call dw_hdmi_unbind(),
* which would have called the encoder cleanup. Do it manually.
*/
if (IS_ERR(hdmi->hdmi)) {
ret = PTR_ERR(hdmi->hdmi);
goto err_bind;
}
return 0;
err_bind:
drm_encoder_cleanup(encoder);
clk_disable_unprepare(hdmi->ref_clk);
err_clk:
regulator_disable(hdmi->avdd_1v8);
err_avdd_1v8:
regulator_disable(hdmi->avdd_0v9);
err_avdd_0v9:
return ret;
}
4.1 drm_of_find_possible_crtcs
drm_of_find_possible_crtcs
定义在drivers/gpu/drm/drm_of.c
;这个函数的作用是基于hdmi
设备节点中的信息,确定特定encoder
端口可能连接的CRTC
;
/**
* drm_of_find_possible_crtcs - find the possible CRTCs for an encoder port
* @dev: DRM device
* @port: encoder port to scan for endpoints
*
* Scan all endpoints attached to a port, locate their attached CRTCs,
* and generate the DRM mask of CRTCs which may be attached to this
* encoder.
*
* See Documentation/devicetree/bindings/graph.txt for the bindings.
*/
uint32_t drm_of_find_possible_crtcs(struct drm_device *dev,
struct device_node *port) // hdmi设备节点
{
struct device_node *remote_port, *ep;
uint32_t possible_crtcs = 0;
// 遍历port结点下的每个endpoint节点,即hdmi_in_vopb、hdmi_in_vopl设备节点
for_each_endpoint_of_node(port, ep) {
// 首先获取hdmi_in_vopb节点remote-endpoint属性指定的设备节点vopb_out_hdmi,并向上查找port设备节点,即vopb_out
remote_port = of_graph_get_remote_port(ep);
// 无效节点,进入
if (!remote_port) {
of_node_put(ep);
return 0;
}
// 下文介绍,根据remote_port设备节点,查找对应的crtc(这里即vopb)
possible_crtcs |= drm_of_crtc_port_mask(dev, remote_port);
of_node_put(remote_port);
}
return possible_crtcs;
}
以hdmi
节点为例,其有两个endpoint
子节点;
hdmi: hdmi@ff940000 {
......
ports {
hdmi_in: port {
#address-cells = <1>;
#size-cells = <0>;
hdmi_in_vopb: endpoint@0 {
reg = <0>;
remote-endpoint = <&vopb_out_hdmi>;
};
hdmi_in_vopl: endpoint@1 {
reg = <1>;
remote-endpoint = <&vopl_out_hdmi>;
};
};
};
};
第一次遍历时,drm_of_crtc_port_mask
参数一传入的是drm
设备,参数二传入的是vopb_out
设备节点。
drm_of_crtc_port_mask
定义如下:
/**
* DOC: overview
*
* A set of helper functions to aid DRM drivers in parsing standard DT
* properties.
*/
/**
* drm_of_crtc_port_mask - find the mask of a registered CRTC by port OF node
* @dev: DRM device
* @port: port OF node
*
* Given a port OF node, return the possible mask of the corresponding
* CRTC within a device's list of CRTCs. Returns zero if not found.
*/
uint32_t drm_of_crtc_port_mask(struct drm_device *dev,
struct device_node *port) // vopb_out设备节点
{
unsigned int index = 0;
struct drm_crtc *tmp;
// list_for_each_entry(tmp, &(dev)->mode_config.crtc_list, head),遍历crtc链表,赋值给tmp
// 因此这里会依次遍历到vopb、vopl对应的crtc,其中vopb对应的crtc->port被设置为vopb_out设备节点
drm_for_each_crtc(tmp, dev) {
if (tmp->port == port) // tmp为vopb时匹配
return 1 << index;
index++;
}
return 0;
}
4.2 rockchip_drm_encoder_set_crtc_endpoint_id
rockchip_drm_encoder_set_crtc_endpoint_id
定义在drivers/gpu/drm/rockchip/rockchip_drm_drv.c
;函数第二个传入的是hdmi
设备节点,第三个参数port
传入0,第四个参数reg
同样传入0。
这段代码首先获取hdmi
设备节点下port=0
、reg=0
的endpoint
设备节点,然后获取该节点remote-endpoint
属性指定的设备节点的reg
属性的值,并将其赋值给rkencoder->crtc_endpoint_id
;
/*
* Get the endpoint id of the remote endpoint of the given encoder. This
* information is used by the VOP2 driver to identify the encoder.
*
* @rkencoder: The encoder to get the remote endpoint id from
* @np: The encoder device node
* @port: The number of the port leading to the VOP2
* @reg: The endpoint number leading to the VOP2
*/
int rockchip_drm_encoder_set_crtc_endpoint_id(struct rockchip_encoder *rkencoder,
struct device_node *np, int port, int reg)
{
struct of_endpoint ep;
struct device_node *en, *ren;
int ret;
// 通过遍历np设备节点的所有端点节点来查找符合指定port=0和reg=0的端点节点,这里返回的是hdmi_in_vopb设备节点
en = of_graph_get_endpoint_by_regs(np, port, reg);
if (!en)
return -ENOENT;
// 获取hdmi_in_vopb设备节点remote-endpoin属性指定的设备节点,即vopb_out_hdmi设备节点
ren = of_graph_get_remote_endpoint(en);
if (!ren)
return -ENOENT;
// 解析vopb_out_hdmi设备节点的属性,并将解析结果存储到ep
ret = of_graph_parse_endpoint(ren, &ep);
if (ret)
return ret;
// 由于vopb_out_hdmi设备节点的reg属性=2,所以此处赋值为2
rkencoder->crtc_endpoint_id = ep.id;
return 0;
}
4.2.1 of_graph_get_endpoint_by_regs
of_graph_get_endpoint_by_regs
定义在drivers/of/property.c
,其作用就是通过遍历parent
设备节点的所有端点节点来查找符合指定 port_reg
和 reg
标识符的端点节点;
/**
* of_graph_get_endpoint_by_regs() - get endpoint node of specific identifiers
* @parent: pointer to the parent device node
* @port_reg: identifier (value of reg property) of the parent port node
* @reg: identifier (value of reg property) of the endpoint node
*
* Return: An 'endpoint' node pointer which is identified by reg and at the same
* is the child of a port node identified by port_reg. reg and port_reg are
* ignored when they are -1. Use of_node_put() on the pointer when done.
*/
struct device_node *of_graph_get_endpoint_by_regs(
const struct device_node *parent, int port_reg, int reg)
{
struct of_endpoint endpoint;
struct device_node *node = NULL;
// 遍历parent结点下的每个endpoint结点
for_each_endpoint_of_node(parent, node) {
// 解析端点的信息,并将结果存储在endpoint
of_graph_parse_endpoint(node, &endpoint);
// 对比传入的port_reg和reg参数与当前端点节点的属性值,如果匹配则返回该端点节点的指针
if (((port_reg == -1) || (endpoint.port == port_reg)) &&
((reg == -1) || (endpoint.id == reg)))
return node;
}
return NULL;
}
比如我们的hdmi
节点,当调用of_graph_get_endpoint_by_regs(np,0,0)
首先查找reg=0
的port
,即hdmi_in
,然后查找reg=0
的端点节点,也就是hdmi_in_vopb
设备节点;
hdmi: hdmi@ff940000 {
......
ports {
hdmi_in: port { # 该节点和port_reg=0匹配, 节点属性reg的值赋值给endpoint.port,未指定赋值为0
#address-cells = <1>;
#size-cells = <0>;
hdmi_in_vopb: endpoint@0 {
reg = <0>; # 属性reg的值赋值给endpoint.id
remote-endpoint = <&vopb_out_hdmi>;
};
hdmi_in_vopl: endpoint@1 {
reg = <1>;
remote-endpoint = <&vopl_out_hdmi>;
};
};
};
};
4.2.2 of_graph_get_remote_endpoint
of_graph_get_remote_endpoint
定义在drivers/of/property.c
,其作用就是获取与指定本地端点相关联的远程端点节点;
/**
* of_graph_get_remote_endpoint() - get remote endpoint node
* @node: pointer to a local endpoint device_node
*
* Return: Remote endpoint node associated with remote endpoint node linked
* to @node. Use of_node_put() on it when done.
*/
struct device_node *of_graph_get_remote_endpoint(const struct device_node *node)
{
/* Get remote endpoint node. */
return of_parse_phandle(node, "remote-endpoint", 0);
}
以hdmi_in_vopb
设备节点为例,该返回返回remote-endpoin
属性指定的设备节点,即vopb_out_hdmi
;
hdmi_in_vopb: endpoint@0 {
reg = <0>;
remote-endpoint = <&vopb_out_hdmi>;
};
4.2.3 of_graph_parse_endpoint
of_graph_parse_endpoint
定义在drivers/of/property.c
,函数的作用是解析端点节点node
的属性,并将解析结果存储到 endpoint
中;
/**
* of_graph_parse_endpoint() - parse common endpoint node properties
* @node: pointer to endpoint device_node
* @endpoint: pointer to the OF endpoint data structure
*
* The caller should hold a reference to @node.
*/
int of_graph_parse_endpoint(const struct device_node *node,
struct of_endpoint *endpoint)
{
// endpoint的父结点是port结点
struct device_node *port_node = of_get_parent(node);
WARN_ONCE(!port_node, "%s(): endpoint %pOF has no parent node\n",
__func__, node);
// 填充0
memset(endpoint, 0, sizeof(*endpoint));
// 设置endpoint所属的port节点
endpoint->local_node = node;
/*
* It doesn't matter whether the two calls below succeed.
* If they don't then the default value 0 is used.
* port结点下的reg属性值是endpoint->port值
* endpoint节点reg属性值是endpoint->id值
*/
of_property_read_u32(port_node, "reg", &endpoint->port);
of_property_read_u32(node, "reg", &endpoint->id);
of_node_put(port_node);
return 0;
}
以vopb_out_hdmi
设备节点为例:
vopb_out_hdmi: endpoint@2 {
reg = <2>;
remote-endpoint = <&hdmi_in_vopb>;
};
经过of_graph_parse_endpoint
函数处理后:
endpoint->id = 2
;endpoint->port= 0
;
4.3 rockchip_hdmi_parse_dt
rockchip_hdmi_parse_dt
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
,这段代码主要是在解析hdmi
设备节点;比如clocks
、rockchip,grf
、avdd-xxx
等属性;
static int rockchip_hdmi_parse_dt(struct rockchip_hdmi *hdmi)
{
struct device_node *np = hdmi->dev->of_node;
// 根据设备树节点中的rockchip,grf属性获取与GRF相关的寄存器映射 rockchip,grf = <&grf>;
hdmi->regmap = syscon_regmap_lookup_by_phandle(np, "rockchip,grf");
if (IS_ERR(hdmi->regmap)) {
DRM_DEV_ERROR(hdmi->dev, "Unable to get rockchip,grf\n");
return PTR_ERR(hdmi->regmap);
}
// 获取ref时钟 <&cru PLL_VPLL>
hdmi->ref_clk = devm_clk_get_optional(hdmi->dev, "ref");
// 如果获取失败,则尝试获取vpll时钟
if (!hdmi->ref_clk)
hdmi->ref_clk = devm_clk_get_optional(hdmi->dev, "vpll");
// deferred error
if (PTR_ERR(hdmi->ref_clk) == -EPROBE_DEFER) {
return -EPROBE_DEFER;
} else if (IS_ERR(hdmi->ref_clk)) {
DRM_DEV_ERROR(hdmi->dev, "failed to get reference clock\n");
return PTR_ERR(hdmi->ref_clk);
}
// 获取grf相关的时钟 <&cru PCLK_VIO_GRF>
hdmi->grf_clk = devm_clk_get(hdmi->dev, "grf");
if (PTR_ERR(hdmi->grf_clk) == -ENOENT) {
hdmi->grf_clk = NULL;
} else if (PTR_ERR(hdmi->grf_clk) == -EPROBE_DEFER) {
return -EPROBE_DEFER;
} else if (IS_ERR(hdmi->grf_clk)) {
DRM_DEV_ERROR(hdmi->dev, "failed to get grf clock\n");
return PTR_ERR(hdmi->grf_clk);
}
// 获取0.9v稳压器
hdmi->avdd_0v9 = devm_regulator_get(hdmi->dev, "avdd-0v9");
if (IS_ERR(hdmi->avdd_0v9))
return PTR_ERR(hdmi->avdd_0v9);
// 获取1.8v稳压器
hdmi->avdd_1v8 = devm_regulator_get(hdmi->dev, "avdd-1v8");
if (IS_ERR(hdmi->avdd_1v8))
return PTR_ERR(hdmi->avdd_1v8);
return 0;
}
4.3.1 devm_regulator_get
devm_regulator_get
定义在drivers/regulator/devres.c
;
static struct regulator *_devm_regulator_get(struct device *dev, const char *id,
int get_type)
{
struct regulator **ptr, *regulator;
// 为设备分配资源
ptr = devres_alloc(devm_regulator_release, sizeof(*ptr), GFP_KERNEL);
if (!ptr)
return ERR_PTR(-ENOMEM);
// 查找指定名称的regulator,如果找不到对应名字的 regulator,那么就返回 dummy regulator,并且在 kernel log 中输出相关 warning 信息
regulator = _regulator_get(dev, id, get_type);
if (!IS_ERR(regulator)) {
*ptr = regulator;
// 将资源添加到设备的资源链表上。释放资源时,遍历设备资源管理链表,然后调用资源注册的释放函数
devres_add(dev, ptr);
} else {
devres_free(ptr);
}
return regulator;
}
/**
* devm_regulator_get - Resource managed regulator_get()
* @dev: device to supply
* @id: supply name or regulator ID.
*
* Managed regulator_get(). Regulators returned from this function are
* automatically regulator_put() on driver detach. See regulator_get() for more
* information.
*/
struct regulator *devm_regulator_get(struct device *dev, const char *id)
{
return _devm_regulator_get(dev, id, NORMAL_GET); // NORMAL_GET值为0
}
函数内部又调用了_regulator_get
,定义在drivers/regulator/core.c
:
/* Internal regulator request function */
struct regulator *_regulator_get(struct device *dev, const char *id, // 以avdd-0v9为例
enum regulator_get_type get_type) // 传入0
{
struct regulator_dev *rdev;
struct regulator *regulator;
struct device_link *link;
int ret;
// 0 >= 3 不会进入
if (get_type >= MAX_GET_TYPE) {
dev_err(dev, "invalid type %d in %s\n", get_type, __func__);
return ERR_PTR(-EINVAL);
}
// 不会进入
if (id == NULL) {
pr_err("get() with no identifier\n");
return ERR_PTR(-EINVAL);
}
// 首先通过设备树的方式去查找rdev,如果没有找到,在通过regulator_map_list查找rdev,regulator_map_list在regulator_rdev注册的时候初始化的
rdev = regulator_dev_lookup(dev, id);
// 找不到 进入
if (IS_ERR(rdev)) {
ret = PTR_ERR(rdev);
/*
* If regulator_dev_lookup() fails with error other
* than -ENODEV our job here is done, we simply return it.
*/
if (ret != -ENODEV)
return ERR_PTR(ret);
if (!have_full_constraints()) {
dev_warn(dev,
"incomplete constraints, dummy supplies not allowed\n");
return ERR_PTR(-ENODEV);
}
switch (get_type) {
case NORMAL_GET: // 进入,返回一个dummy regulator
/*
* Assume that a regulator is physically present and
* enabled, even if it isn't hooked up, and just
* provide a dummy.
*/
dev_warn(dev, "supply %s not found, using dummy regulator\n", id);
rdev = dummy_regulator_rdev;
get_device(&rdev->dev);
break;
case EXCLUSIVE_GET:
dev_warn(dev,
"dummy supplies not allowed for exclusive requests\n");
fallthrough;
default:
return ERR_PTR(-ENODEV);
}
}
if (rdev->exclusive) {
regulator = ERR_PTR(-EPERM);
put_device(&rdev->dev);
return regulator;
}
if (get_type == EXCLUSIVE_GET && rdev->open_count) {
regulator = ERR_PTR(-EBUSY);
put_device(&rdev->dev);
return regulator;
}
mutex_lock(®ulator_list_mutex);
ret = (rdev->coupling_desc.n_resolved != rdev->coupling_desc.n_coupled);
mutex_unlock(®ulator_list_mutex);
if (ret != 0) {
regulator = ERR_PTR(-EPROBE_DEFER);
put_device(&rdev->dev);
return regulator;
}
ret = regulator_resolve_supply(rdev);
if (ret < 0) {
regulator = ERR_PTR(ret);
put_device(&rdev->dev);
return regulator;
}
if (!try_module_get(rdev->owner)) {
regulator = ERR_PTR(-EPROBE_DEFER);
put_device(&rdev->dev);
return regulator;
}
// 如果找到则调用create_regulator创建regulator
regulator = create_regulator(rdev, dev, id);
if (regulator == NULL) {
regulator = ERR_PTR(-ENOMEM);
module_put(rdev->owner);
put_device(&rdev->dev);
return regulator;
}
rdev->open_count++;
if (get_type == EXCLUSIVE_GET) {
rdev->exclusive = 1;
ret = _regulator_is_enabled(rdev);
if (ret > 0) {
rdev->use_count = 1;
regulator->enable_count = 1;
} else {
rdev->use_count = 0;
regulator->enable_count = 0;
}
}
link = device_link_add(dev, &rdev->dev, DL_FLAG_STATELESS);
if (!IS_ERR_OR_NULL(link))
regulator->device_link = true;
return regulator;
}
以devm_regulator_get(hdmi->dev, "avdd-0v9")
为例,该函数会调用regulator_dev_lookup
查找regulator
;
- 首先通过设备树的方式去查找
rdev
,即在hdmi
设备节点中查找avdd-0v9-supply
属性指定的regulator
设备节点; - 如果没有找到,在通过
regulator_map_list
查找rdev
,regulator_map_list
在regulator_rdev
注册的时候初始化的;
如果找不到将返回dummy regulator
,并输入警告信息。
由于我们并没有在hdmi
设备节点中定义avdd-0v9-supply
属性,同时也没有在设备树定义regulator-name = "avdd-0v9"
的regulator_rdev
,因此 我们内核在启动时会输入如下警告信息:
[ 1.475048] dwhdmi-rockchip ff940000.hdmi: supply avdd-0v9 not found, using dummy regulator
[ 1.484573] dwhdmi-rockchip ff940000.hdmi: supply avdd-1v8 not found, using dummy regulator
4.4 devm_phy_optional_get
devm_phy_optional_get
定义在drivers/phy/phy-core.c
,它用于查找并获取一个可选的PHY
(物理层设备)的引用;
/**
* devm_phy_optional_get() - lookup and obtain a reference to an optional phy.
* @dev: device that requests this phy
* @string: the phy name as given in the dt data or phy device name
* for non-dt case
*
* Gets the phy using phy_get(), and associates a device with it using
* devres. On driver detach, release function is invoked on the devres
* data, then, devres data is freed. This differs to devm_phy_get() in
* that if the phy does not exist, it is not considered an error and
* -ENODEV will not be returned. Instead the NULL phy is returned,
* which can be passed to all other phy consumer calls.
*/
struct phy *devm_phy_optional_get(struct device *dev, const char *string)
{
// 获取PHY
struct phy *phy = devm_phy_get(dev, string);
if (PTR_ERR(phy) == -ENODEV)
phy = NULL;
return phy;
}
4.5 regulator_enable
regulator_enable
定义在drivers/regulator/core.c
,用于使能regulator
输出;
/**
* regulator_enable - enable regulator output
* @regulator: regulator source
*
* Request that the regulator be enabled with the regulator output at
* the predefined voltage or current value. Calls to regulator_enable()
* must be balanced with calls to regulator_disable().
*
* NOTE: the output value can be set by other drivers, boot loader or may be
* hardwired in the regulator.
*/
int regulator_enable(struct regulator *regulator)
{
struct regulator_dev *rdev = regulator->rdev;
struct ww_acquire_ctx ww_ctx;
int ret;
regulator_lock_dependent(rdev, &ww_ctx);
ret = _regulator_enable(regulator);
regulator_unlock_dependent(rdev, &ww_ctx);
return ret;
}
4.6 dw_hdmi_bind
dw_hdmi_bind
定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
;
/* -----------------------------------------------------------------------------
* Bind/unbind API, used from platforms based on the component framework.
*/
struct dw_hdmi *dw_hdmi_bind(struct platform_device *pdev,
struct drm_encoder *encoder,
const struct dw_hdmi_plat_data *plat_data)
{
struct dw_hdmi *hdmi;
int ret;
// dw hdmi探测
hdmi = dw_hdmi_probe(pdev, plat_data);
if (IS_ERR(hdmi))
return hdmi;
// 将bridge连接到encoder的链中
ret = drm_bridge_attach(encoder, &hdmi->bridge, NULL, 0);
if (ret) {
dw_hdmi_remove(hdmi);
return ERR_PTR(ret);
}
return hdmi;
}
调用该函数时,第一个参数传入hdmi
设备节点对应的platform device
,第二个参数传入drm encoder
,第三个参数传入rk3399_hdmi_drv_data
;rk3399_hdmi_drv_data
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
:
static const struct dw_hdmi_plat_data rk3399_hdmi_drv_data = {
.mode_valid = dw_hdmi_rockchip_mode_valid, // 用于校验显示模式是否有效
.mpll_cfg = rockchip_mpll_cfg,
.cur_ctr = rockchip_cur_ctr,
.phy_config = rockchip_phy_config,
.phy_data = &rk3399_chip_data,
.use_drm_infoframe = true,
};
rockchip_mpll_cfg
、rockchip_cur_ctr
、rockchip_phy_config
中存放的都是HDMI PHY
配置参数,会被hdmi_phy_configure_dwc_hdmi_3d_tx
函数使用,用于配置DWC HDMI 3D TX PHY
的物理层(PHY
),该函数位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
;
- 首先从提供的
plat_data
结构中获取mpll_config
、curr_ctrl
和phy_config
的指针。 - 然后,它通过遍历
mpll_config
、curr_ctrl
和phy_config
数组,找到与给定mpixelclock
(像素时钟频率)匹配的配置条目。一旦找到匹配的条目,就会使用dw_hdmi_phy_i2c_write
函数将对应的配置值写入HDMI PHY
寄存器中; - 在最后一部分,代码还覆盖并禁用了时钟终端,并将特定的值写入了相应的
PHY
寄存器;
/*
* PHY configuration function for the DWC HDMI 3D TX PHY. Based on the available
* information the DWC MHL PHY has the same register layout and is thus also
* supported by this function.
*/
static int hdmi_phy_configure_dwc_hdmi_3d_tx(struct dw_hdmi *hdmi,
const struct dw_hdmi_plat_data *pdata,
unsigned long mpixelclock)
{
const struct dw_hdmi_mpll_config *mpll_config = pdata->mpll_cfg;
const struct dw_hdmi_curr_ctrl *curr_ctrl = pdata->cur_ctr;
const struct dw_hdmi_phy_config *phy_config = pdata->phy_config;
/* TOFIX Will need 420 specific PHY configuration tables */
/* PLL/MPLL Cfg - always match on final entry */
for (; mpll_config->mpixelclock != ~0UL; mpll_config++)
if (mpixelclock <= mpll_config->mpixelclock)
break;
for (; curr_ctrl->mpixelclock != ~0UL; curr_ctrl++)
if (mpixelclock <= curr_ctrl->mpixelclock)
break;
for (; phy_config->mpixelclock != ~0UL; phy_config++)
if (mpixelclock <= phy_config->mpixelclock)
break;
if (mpll_config->mpixelclock == ~0UL ||
curr_ctrl->mpixelclock == ~0UL ||
phy_config->mpixelclock == ~0UL)
return -EINVAL;
dw_hdmi_phy_i2c_write(hdmi, mpll_config->res[0].cpce,
HDMI_3D_TX_PHY_CPCE_CTRL);
dw_hdmi_phy_i2c_write(hdmi, mpll_config->res[0].gmp,
HDMI_3D_TX_PHY_GMPCTRL);
dw_hdmi_phy_i2c_write(hdmi, curr_ctrl->curr[0],
HDMI_3D_TX_PHY_CURRCTRL);
dw_hdmi_phy_i2c_write(hdmi, 0, HDMI_3D_TX_PHY_PLLPHBYCTRL);
dw_hdmi_phy_i2c_write(hdmi, HDMI_3D_TX_PHY_MSM_CTRL_CKO_SEL_FB_CLK,
HDMI_3D_TX_PHY_MSM_CTRL);
dw_hdmi_phy_i2c_write(hdmi, phy_config->term, HDMI_3D_TX_PHY_TXTERM);
dw_hdmi_phy_i2c_write(hdmi, phy_config->sym_ctr,
HDMI_3D_TX_PHY_CKSYMTXCTRL);
dw_hdmi_phy_i2c_write(hdmi, phy_config->vlev_ctr,
HDMI_3D_TX_PHY_VLEVCTRL);
/* Override and disable clock termination. */
dw_hdmi_phy_i2c_write(hdmi, HDMI_3D_TX_PHY_CKCALCTRL_OVERRIDE,
HDMI_3D_TX_PHY_CKCALCTRL);
return 0;
}
4.6.1 dw_hdmi_rockchip_mode_valid
dw_hdmi_rockchip_mode_valid
函数用于校验显示模式是否有效;
static enum drm_mode_status
dw_hdmi_rockchip_mode_valid(struct dw_hdmi *hdmi, void *data,
const struct drm_display_info *info,
const struct drm_display_mode *mode)
{
const struct dw_hdmi_mpll_config *mpll_cfg = rockchip_mpll_cfg;
int pclk = mode->clock * 1000; // 计算得到像素时钟频率
bool valid = false;
int i;
// 遍历mpll_cfg像素时钟,查找匹配的时钟
for (i = 0; mpll_cfg[i].mpixelclock != (~0UL); i++) {
if (pclk == mpll_cfg[i].mpixelclock) {
valid = true;
break;
}
}
return (valid) ? MODE_OK : MODE_BAD;
}
4.6.2 rockchip_mpll_cfg
rockchip_mpll_cfg
保存的是RK3399
的 HDMI-PHY-PLL
配置,用于配置HDMI_3D_TX_PHY_CPCE_CTRL
、HDMI_3D_TX_PHY_GMPCTRL
寄存器;
static const struct dw_hdmi_mpll_config rockchip_mpll_cfg[] = {
{
27000000, {
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}, {
36000000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:800x600@56Hz 640x480@85Hz;适用于edid_est_modes中定义的显示模式:800x600@56Hz
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}, {
40000000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:800x600@60Hz;适用于edid_est_modes中定义的显示模式:800x600@60Hz
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}, {
54000000, {
{ 0x0072, 0x0001},
{ 0x2142, 0x0001},
{ 0x40a2, 0x0001},
},
}, {
65000000, { // 适用于edid_est_modes中定义的显示模式:1024x768@70Hz
{ 0x0072, 0x0001},
{ 0x2142, 0x0001},
{ 0x40a2, 0x0001},
},
}, {
66000000, {
{ 0x013e, 0x0003},
{ 0x217e, 0x0002},
{ 0x4061, 0x0002}
},
}, {
74250000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1280x720@60Hz
{ 0x0072, 0x0001},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
83500000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1280x800@60Hz
{ 0x0072, 0x0001},
},
}, {
108000000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1600x900@60Hz 1280x1024@60Hz 1152x864@75Hz 1280x960@60Hz;适用于edid_est_modes中定义的显示模式:1152x864@75Hz
{ 0x0051, 0x0002},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
106500000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1440x900@60Hz、1280x800@75Hz
{ 0x0051, 0x0002},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
146250000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1680x1050@60Hz 1280x800@120Hz RB
{ 0x0051, 0x0002},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
148500000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1920x1080@60Hz 1280x960@85Hz
{ 0x0051, 0x0003},
{ 0x214c, 0x0003},
{ 0x4064, 0x0003}
},
}, {
~0UL, {
{ 0x00a0, 0x000a },
{ 0x2001, 0x000f },
{ 0x4002, 0x000f },
},
}
};
结构体dw_hdmi_mpll_config
定义如下:
struct dw_hdmi_mpll_config {
unsigned long mpixelclock;
struct {
u16 cpce;
u16 gmp;
} res[DW_HDMI_RES_MAX];
};
各项参数说明如下:
mpixelclock
:像素时钟频率;cpce
:OPMODE_PLLCFG
寄存器值;gmp
:PLLGMPCTRL
寄存器值;
以rockchip_mpll_cfg
中的第一项配置为例:
{
27000000, {
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}
27000000
表示像素时钟为27000000
及以下的分辨率适用该项配置, {0x00b3, 0x0000 }
、 { 0x2153, 0x0000 }
、 { 0x40f3, 0x0000 }
三项依次对应色深为 8 BIT
、10BIT
、12 BIT
(目前Rockchip
方案实际只支持8/10 bit
两种模式) 情况下使用的配置。
由于参数的取值需要查阅PHY
的datasheet
获取,若需要新增HDMI-PHY-PLL
配置,可以向FAE
提出所需的像素时钟。然后根据上述的规则,将新增的配置添加到rockchip_mpll_cfg
中。
4.6.3 rockchip_cur_ctr
具体作用不晓得,用于配置HDMI_3D_TX_PHY_CURRCTRL
寄存器;
static const struct dw_hdmi_curr_ctrl rockchip_cur_ctr[] = {
/* pixelclk bpp8 bpp10 bpp12 */
{
40000000, { 0x0018, 0x0018, 0x0018 },
}, {
65000000, { 0x0028, 0x0028, 0x0028 },
}, {
66000000, { 0x0038, 0x0038, 0x0038 },
}, {
74250000, { 0x0028, 0x0038, 0x0038 },
}, {
83500000, { 0x0028, 0x0038, 0x0038 },
}, {
146250000, { 0x0038, 0x0038, 0x0038 },
}, {
148500000, { 0x0000, 0x0038, 0x0038 },
}, {
~0UL, { 0x0000, 0x0000, 0x0000},
}
};
第一个参数和rockchip_mpll_cfg
类似,比如40000000
表示像素时钟为40000000
及以下的分辨率适用该项配置。
4.6.4 rockchip_phy_config
具体作用不晓得,用于配置HDMI_3D_TX_PHY_TXTERM
、HDMI_3D_TX_PHY_CKSYMTXCTRL
、HDMI_3D_TX_PHY_VLEVCTRL
寄存器;
static const struct dw_hdmi_phy_config rockchip_phy_config[] = {
/*pixelclk symbol term vlev*/
{ 74250000, 0x8009, 0x0004, 0x0272},
{ 148500000, 0x802b, 0x0004, 0x028d},
{ 297000000, 0x8039, 0x0005, 0x028d},
{ ~0UL, 0x0000, 0x0000, 0x0000}
};
第一个参数和rockchip_mpll_cfg
类似,比如74250000
表示像素时钟为74250000
及以下的分辨率适用该项配置。
4.6.5 rk3399_chip_data
#define RK3399_GRF_SOC_CON20 0x6250
#define RK3399_HDMI_LCDC_SEL BIT(6)
#define HIWORD_UPDATE(val, mask) (val | (mask) << 16)
static struct rockchip_hdmi_chip_data rk3399_chip_data = {
.lcdsel_grf_reg = RK3399_GRF_SOC_CON20,
.lcdsel_big = HIWORD_UPDATE(0, RK3399_HDMI_LCDC_SEL),
.lcdsel_lit = HIWORD_UPDATE(RK3399_HDMI_LCDC_SEL, RK3399_HDMI_LCDC_SEL),
}
五、dw_hdmi_probe
dw_hdmi_probe
函数可以看做是DesignWare hdmi
驱动的入口函数,从这里开始就告别了Rockchip hdmi
驱动相关的内容,正式进入DesignWare hdmi
的源码分析中;dw_hdmi_probe
函数定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
;
struct dw_hdmi *dw_hdmi_probe(struct platform_device *pdev, // 传入hdmi设备节点所属的platform device
const struct dw_hdmi_plat_data *plat_data) // 传入rk3399_hdmi_drv_data
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct platform_device_info pdevinfo;
struct device_node *ddc_node;
struct dw_hdmi_cec_data cec;
struct dw_hdmi *hdmi;
struct resource *iores = NULL;
int irq;
int ret;
u32 val = 1;
u8 prod_id0;
u8 prod_id1;
u8 config0;
u8 config3;
// 1. 动态分配内存,指向struct dw_hdmi,并进行成员的初始化
hdmi = devm_kzalloc(dev, sizeof(*hdmi), GFP_KERNEL);
if (!hdmi)
return ERR_PTR(-ENOMEM);
hdmi->plat_data = plat_data;
hdmi->dev = dev;
hdmi->sample_rate = 48000;
hdmi->channels = 2;
hdmi->disabled = true;
hdmi->rxsense = true;
hdmi->phy_mask = (u8)~(HDMI_PHY_HPD | HDMI_PHY_RX_SENSE);
hdmi->mc_clkdis = 0x7f;
hdmi->last_connector_result = connector_status_disconnected;
mutex_init(&hdmi->mutex);
mutex_init(&hdmi->audio_mutex);
mutex_init(&hdmi->cec_notifier_mutex);
spin_lock_init(&hdmi->audio_lock);
// 2. 解析hdmi设备节点,初始化hdmi成员
ret = dw_hdmi_parse_dt(hdmi);
if (ret < 0)
return ERR_PTR(ret);
// 3. 获取ddc-i2c-bus设备节点 ddc-i2c-bus = <&i2c7>
ddc_node = of_parse_phandle(np, "ddc-i2c-bus", 0);
if (ddc_node) {
// 获取i2c总线适配器
hdmi->ddc = of_get_i2c_adapter_by_node(ddc_node);
of_node_put(ddc_node);
if (!hdmi->ddc) {
dev_dbg(hdmi->dev, "failed to read ddc node\n");
return ERR_PTR(-EPROBE_DEFER);
}
} else {
dev_dbg(hdmi->dev, "no ddc property found\n");
}
// 4. 为HDMI相关寄存器注册regmap,采用regmap模型访问HDMI相关寄存器
if (!plat_data->regm) {
const struct regmap_config *reg_config;
// 获取hdmi设备节点reg-io-width属性,描述hdmi相关寄存器位宽 reg-io-width = <4>
of_property_read_u32(np, "reg-io-width", &val);
switch (val) {
case 4:
// regmap 配置信息
reg_config = &hdmi_regmap_32bit_config;
hdmi->reg_shift = 2;
break;
case 1:
reg_config = &hdmi_regmap_8bit_config;
break;
default:
dev_err(dev, "reg-io-width must be 1 or 4\n");
return ERR_PTR(-EINVAL);
}
// 获取第一个内存资源,即reg = <0x0 0xff940000 0x0 0x20000> HDMI相关寄存器基地址
iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
hdmi->regs = devm_ioremap_resource(dev, iores);
if (IS_ERR(hdmi->regs)) {
ret = PTR_ERR(hdmi->regs);
goto err_res;
}
// 为内存映射I/O注册regmap
hdmi->regm = devm_regmap_init_mmio(dev, hdmi->regs, reg_config);
if (IS_ERR(hdmi->regm)) {
dev_err(dev, "Failed to configure regmap\n");
ret = PTR_ERR(hdmi->regm);
goto err_res;
}
} else {
hdmi->regm = plat_data->regm;
}
// 根据时钟名称isfr获取时钟,设备节点属性clock-names、clocks,指定了名字为isfr对应的时钟为<&cru SCLK_HDMI_SFR>
hdmi->isfr_clk = devm_clk_get(hdmi->dev, "isfr");
if (IS_ERR(hdmi->isfr_clk)) {
ret = PTR_ERR(hdmi->isfr_clk);
dev_err(hdmi->dev, "Unable to get HDMI isfr clk: %d\n", ret);
goto err_res;
}
// 准备和使能时钟
ret = clk_prepare_enable(hdmi->isfr_clk);
if (ret) {
dev_err(hdmi->dev, "Cannot enable HDMI isfr clock: %d\n", ret);
goto err_res;
}
// 根据时钟名称iahb获取时钟,设备节点属性clock-names、clocks,指定了名字为iahb对应的时钟为<&cru PCLK_HDMI_CTRL>
hdmi->iahb_clk = devm_clk_get(hdmi->dev, "iahb");
if (IS_ERR(hdmi->iahb_clk)) {
ret = PTR_ERR(hdmi->iahb_clk);
dev_err(hdmi->dev, "Unable to get HDMI iahb clk: %d\n", ret);
goto err_isfr;
}
// 准备和使能时钟
ret = clk_prepare_enable(hdmi->iahb_clk);
if (ret) {
dev_err(hdmi->dev, "Cannot enable HDMI iahb clock: %d\n", ret);
goto err_isfr;
}
// 根据时钟名称cec获取时钟,设备节点属性clock-names、clocks,指定了名字为cec对应的时钟为<&cru SCLK_HDMI_CEC>
hdmi->cec_clk = devm_clk_get(hdmi->dev, "cec");
if (PTR_ERR(hdmi->cec_clk) == -ENOENT) {
hdmi->cec_clk = NULL;
} else if (IS_ERR(hdmi->cec_clk)) {
ret = PTR_ERR(hdmi->cec_clk);
if (ret != -EPROBE_DEFER)
dev_err(hdmi->dev, "Cannot get HDMI cec clock: %d\n",
ret);
hdmi->cec_clk = NULL;
goto err_iahb;
} else {
// 准备和使能时钟
ret = clk_prepare_enable(hdmi->cec_clk);
if (ret) {
dev_err(hdmi->dev, "Cannot enable HDMI cec clock: %d\n",
ret);
goto err_iahb;
}
}
/* Product and revision IDs, 获取产品和版本标识信息 */
hdmi->version = (hdmi_readb(hdmi, HDMI_DESIGN_ID) << 8)
| (hdmi_readb(hdmi, HDMI_REVISION_ID) << 0);
prod_id0 = hdmi_readb(hdmi, HDMI_PRODUCT_ID0);
prod_id1 = hdmi_readb(hdmi, HDMI_PRODUCT_ID1);
// 如果发现不支持的HDMI控制器类型,则会打印错误信息并返回-ENODEV错误
if (prod_id0 != HDMI_PRODUCT_ID0_HDMI_TX ||
(prod_id1 & ~HDMI_PRODUCT_ID1_HDCP) != HDMI_PRODUCT_ID1_HDMI_TX) {
dev_err(dev, "Unsupported HDMI controller (%04x:%02x:%02x)\n",
hdmi->version, prod_id0, prod_id1);
ret = -ENODEV;
goto err_iahb;
}
// 5. 检测HDMI的物理层接口
ret = dw_hdmi_detect_phy(hdmi);
if (ret < 0)
goto err_iahb;
dev_info(dev, "Detected HDMI TX controller v%x.%03x %s HDCP (%s)\n",
hdmi->version >> 12, hdmi->version & 0xfff,
prod_id1 & HDMI_PRODUCT_ID1_HDCP ? "with" : "without",
hdmi->phy.name);
// 6. HDMI硬件初始化
dw_hdmi_init_hw(hdmi);
// 7. 获取第1个IRQ编号 interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_iahb;
}
// 8. 申请中断,中断处理函数设置为dw_hdmi_hardirq,中断线程化的处理函数设置为dw_hdmi_irq
ret = devm_request_threaded_irq(dev, irq, dw_hdmi_hardirq,
dw_hdmi_irq, IRQF_SHARED,
dev_name(dev), hdmi);
if (ret)
goto err_iahb;
/*
* To prevent overflows in HDMI_IH_FC_STAT2, set the clk regenerator
* N and cts values before enabling phy
*/
hdmi_init_clk_regenerator(hdmi);
/* If DDC bus is not specified, try to register HDMI I2C bus,不会进入 */
if (!hdmi->ddc) {
/* Look for (optional) stuff related to unwedging */
hdmi->pinctrl = devm_pinctrl_get(dev);
if (!IS_ERR(hdmi->pinctrl)) {
hdmi->unwedge_state =
pinctrl_lookup_state(hdmi->pinctrl, "unwedge");
hdmi->default_state =
pinctrl_lookup_state(hdmi->pinctrl, "default");
if (IS_ERR(hdmi->default_state) ||
IS_ERR(hdmi->unwedge_state)) {
if (!IS_ERR(hdmi->unwedge_state))
dev_warn(dev,
"Unwedge requires default pinctrl\n");
hdmi->default_state = NULL;
hdmi->unwedge_state = NULL;
}
}
hdmi->ddc = dw_hdmi_i2c_adapter(hdmi);
if (IS_ERR(hdmi->ddc))
hdmi->ddc = NULL;
}
// 初始化桥接设备
hdmi->bridge.driver_private = hdmi;
hdmi->bridge.funcs = &dw_hdmi_bridge_funcs;
hdmi->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID
| DRM_BRIDGE_OP_HPD;
hdmi->bridge.interlace_allowed = true;
#ifdef CONFIG_OF
hdmi->bridge.of_node = pdev->dev.of_node;
#endif
memset(&pdevinfo, 0, sizeof(pdevinfo));
pdevinfo.parent = dev;
pdevinfo.id = PLATFORM_DEVID_AUTO;
// 这看起来应该是获取HDMI的配置信息,具体是啥咱也不知道
config0 = hdmi_readb(hdmi, HDMI_CONFIG0_ID);
config3 = hdmi_readb(hdmi, HDMI_CONFIG3_ID);
// AHB DMA音频?
if (iores && config3 & HDMI_CONFIG3_AHBAUDDMA) {
struct dw_hdmi_audio_data audio;
audio.phys = iores->start;
audio.base = hdmi->regs;
audio.irq = irq;
audio.hdmi = hdmi;
audio.get_eld = hdmi_audio_get_eld;
hdmi->enable_audio = dw_hdmi_ahb_audio_enable;
hdmi->disable_audio = dw_hdmi_ahb_audio_disable;
pdevinfo.name = "dw-hdmi-ahb-audio";
pdevinfo.data = &audio;
pdevinfo.size_data = sizeof(audio);
pdevinfo.dma_mask = DMA_BIT_MASK(32);
hdmi->audio = platform_device_register_full(&pdevinfo);
} else if (config0 & HDMI_CONFIG0_I2S) { // I2S音频?
struct dw_hdmi_i2s_audio_data audio;
audio.hdmi = hdmi;
audio.get_eld = hdmi_audio_get_eld;
audio.write = hdmi_writeb;
audio.read = hdmi_readb;
hdmi->enable_audio = dw_hdmi_i2s_audio_enable;
hdmi->disable_audio = dw_hdmi_i2s_audio_disable;
pdevinfo.name = "dw-hdmi-i2s-audio";
pdevinfo.data = &audio;
pdevinfo.size_data = sizeof(audio);
pdevinfo.dma_mask = DMA_BIT_MASK(32);
hdmi->audio = platform_device_register_full(&pdevinfo);
} else if (iores && config3 & HDMI_CONFIG3_GPAUD) { // GP Audiou音频
struct dw_hdmi_audio_data audio;
audio.phys = iores->start;
audio.base = hdmi->regs;
audio.irq = irq;
audio.hdmi = hdmi;
audio.get_eld = hdmi_audio_get_eld;
hdmi->enable_audio = dw_hdmi_gp_audio_enable;
hdmi->disable_audio = dw_hdmi_gp_audio_disable;
pdevinfo.name = "dw-hdmi-gp-audio";
pdevinfo.id = PLATFORM_DEVID_NONE;
pdevinfo.data = &audio;
pdevinfo.size_data = sizeof(audio);
pdevinfo.dma_mask = DMA_BIT_MASK(32);
hdmi->audio = platform_device_register_full(&pdevinfo);
}
// 如果没有禁用CEC,并且HDMI控制器支持CEC
if (!plat_data->disable_cec && (config0 & HDMI_CONFIG0_CEC)) {
cec.hdmi = hdmi;
cec.ops = &dw_hdmi_cec_ops;
cec.irq = irq;
pdevinfo.name = "dw-hdmi-cec";
pdevinfo.data = &cec;
pdevinfo.size_data = sizeof(cec);
pdevinfo.dma_mask = 0;
hdmi->cec = platform_device_register_full(&pdevinfo);
}
// 当前桥接设备到全局链表`bridge_list中
drm_bridge_add(&hdmi->bridge);
return hdmi;
err_iahb:
clk_disable_unprepare(hdmi->iahb_clk);
clk_disable_unprepare(hdmi->cec_clk);
err_isfr:
clk_disable_unprepare(hdmi->isfr_clk);
err_res:
i2c_put_adapter(hdmi->ddc);
return ERR_PTR(ret);
}
这个代码的长度一眼望过去令人窒息。我们也不用去一一解读这段代码干了什么,我们只关注我们想了解的东西,比如与edid
相关的内容,以及connector
初始化相关的内容;
- 动态分配
struct dw_hdmi
对象,并进行hdmi
成员的初始化; - 调用
dw_hdmi_parse_dt
解析hdmi
设备节点,初始化hdmi
成员;实际上由于没有指定hdmi->plat_data->output_port
所以这个函数会直接返回; - 如果指定了
ddc-i2c-bus
属性,则 获取i2c
总线适配器; - 为
HDMI
相关寄存器注册regmap
,采用regmap
模型访问hdmi
相关寄存器; - 获取并使能时钟
isfr_clk
、iahb_clk
、cec_clk
; - 调用
dw_hdmi_detect_phy
检测hdmi
的物理层接口; - 调用
dw_hdmi_init_hw
进行HDMI
硬件初始化; - 注册中断
interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>
,中断处理函数设置为dw_hdmi_hardirq
,中断线程化的处理函数设置为dw_hdmi_irq
; - 初始化桥接设备,设置回调
funcs
为dw_hdmi_bridge_funcs
; - 调用
drm_bridge_add
添加hdmi
桥接设备;
5.1 dw_hdmi_parse_dt
dw_hdmi_parse_dt
函数用于解析hdmi
设备节点,初始化hdmi
成员;
static int dw_hdmi_parse_dt(struct dw_hdmi *hdmi)
{
struct device_node *endpoint;
struct device_node *remote;
// 直接返回
if (!hdmi->plat_data->output_port)
return 0;
// 通过遍历父设备节点的所有子节点(端点节点)来查找符合指定port_reg=0的端点节点,这里返回的是hdmi_in_vopb设备节点
endpoint = of_graph_get_endpoint_by_regs(hdmi->dev->of_node,
hdmi->plat_data->output_port,
-1);
if (!endpoint) {
/*
* On platforms whose bindings don't make the output port
* mandatory (such as Rockchip) the plat_data->output_port
* field isn't set, so it's safe to make this a fatal error.
*/
dev_err(hdmi->dev, "Missing endpoint in port@%u\n",
hdmi->plat_data->output_port);
return -ENODEV;
}
// 首先获取endpoint设备节点remote-endpoin属性指定的设备节点,即vopb_out_hdmi设备节点,并向上查找父设备节点,直至找到vopb设备节点
remote = of_graph_get_remote_port_parent(endpoint);
of_node_put(endpoint);
if (!remote) {
dev_err(hdmi->dev, "Endpoint in port@%u unconnected\n",
hdmi->plat_data->output_port);
return -ENODEV;
}
// 判断这个设备节点是否处于启用状态 即status = "ok"
if (!of_device_is_available(remote)) {
dev_err(hdmi->dev, "port@%u remote device is disabled\n",
hdmi->plat_data->output_port);
of_node_put(remote);
return -ENODEV;
}
// find the bridge corresponding to the device node in the global bridge list bridge_list
hdmi->next_bridge = of_drm_find_bridge(remote);
of_node_put(remote);
if (!hdmi->next_bridge)
return -EPROBE_DEFER;
return 0;
}
函数of_drm_find_bridge
定义在drivers/gpu/drm/drm_bridge.c
:
/**
* of_drm_find_bridge - find the bridge corresponding to the device node in
* the global bridge list
*
* @np: device node
*
* RETURNS:
* drm_bridge control struct on success, NULL on failure
*/
struct drm_bridge *of_drm_find_bridge(struct device_node *np)
{
struct drm_bridge *bridge;
mutex_lock(&bridge_lock);
list_for_each_entry(bridge, &bridge_list, list) {
if (bridge->of_node == np) {
mutex_unlock(&bridge_lock);
return bridge;
}
}
mutex_unlock(&bridge_lock);
return NULL;
}
5.2 dw_hdmi_init_hw
dw_hdmi_init_hw
函数用于初始化I2C
控制器;
static void dw_hdmi_init_hw(struct dw_hdmi *hdmi)
{
initialize_hdmi_ih_mutes(hdmi);
/*
* Reset HDMI DDC I2C master controller and mute I2CM interrupts.
* Even if we are using a separate i2c adapter doing this doesn't
* hurt.
*/
dw_hdmi_i2c_init(hdmi);
// 如果指定了,则执行
if (hdmi->phy.ops->setup_hpd)
hdmi->phy.ops->setup_hpd(hdmi, hdmi->phy.data);
}
5.3 dw_hdmi_hardirq
dw_hdmi_hardirq
为中断处理函数;
static irqreturn_t dw_hdmi_hardirq(int irq, void *dev_id)
{
struct dw_hdmi *hdmi = dev_id;
u8 intr_stat;
irqreturn_t ret = IRQ_NONE;
// 不会进入
if (hdmi->i2c)
ret = dw_hdmi_i2c_irq(hdmi);
// 读取寄存器HDMI_IH_PHY_STAT0的值
intr_stat = hdmi_readb(hdmi, HDMI_IH_PHY_STAT0);
if (intr_stat) {
// 向寄存器HDMI_IH_MUTE_PHY_STAT0写入值
hdmi_writeb(hdmi, ~0, HDMI_IH_MUTE_PHY_STAT0);
return IRQ_WAKE_THREAD;
}
return ret;
}
5.3.1 hdmi_readb
hdmi_readb
实际上是对regmap_read
进行了又一层的包装,用于实现对RK3399 hdmi
相关寄存器进行读操作;
static inline u8 hdmi_readb(struct dw_hdmi *hdmi, int offset)
{
unsigned int val = 0;
regmap_read(hdmi->regm, offset << hdmi->reg_shift, &val);
return val;
}
5.3.2 hdmi_writeb
hdmi_writeb
实际上是对regmap_write
进行了又一层的包装,用于实现对RK3399 hdmi
相关寄存器进行写操作;
static inline void hdmi_writeb(struct dw_hdmi *hdmi, u8 val, int offset)
{
regmap_write(hdmi->regm, offset << hdmi->reg_shift, val);
}
5.4 dw_hdmi_bridge_funcs
(重点)
dw_hdmi_bridge_funcs
定义了bridge
的控制函数,位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
文件;
static const struct drm_bridge_funcs dw_hdmi_bridge_funcs = {
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.attach = dw_hdmi_bridge_attach, // 桥接设备连接到encoder时被调用
.detach = dw_hdmi_bridge_detach,
.atomic_check = dw_hdmi_bridge_atomic_check,
.atomic_get_output_bus_fmts = dw_hdmi_bridge_atomic_get_output_bus_fmts,
.atomic_get_input_bus_fmts = dw_hdmi_bridge_atomic_get_input_bus_fmts,
.atomic_enable = dw_hdmi_bridge_atomic_enable,
.atomic_disable = dw_hdmi_bridge_atomic_disable,
.mode_set = dw_hdmi_bridge_mode_set,
.mode_valid = dw_hdmi_bridge_mode_valid, // 用于校验显示模式是否有效,最终调用dw_hdmi_rockchip_mode_valid
.detect = dw_hdmi_bridge_detect,
.get_edid = dw_hdmi_bridge_get_edid, // 用于获取edid信息
};
其中:
attach
:回调函数在桥接设备连接到encoder
时被调用;detach
:回调函数在桥接设备从encoder
断开时被调用;mode_valid
:用于校验显示模式是否有效,最终调用dw_hdmi_plat_data
的成员mode_valid
,也就是dw_hdmi_rockchip_mode_valid
函数;get_edid
:用于读取连接显示器的edid
数据的首选方法。如果桥接设备支持读取edid
的话,应当实现这个回调函数,并不实现get_modes
回调;
5.4.1 dw_hdmi_bridge_attach
对于briget
而言,dw_hdmi_bridge_attach
函数用于将bridge
连接到encoder
的链中
static int dw_hdmi_bridge_attach(struct drm_bridge *bridge,
enum drm_bridge_attach_flags flags)
{
struct dw_hdmi *hdmi = bridge->driver_private;
// 不会进入
if (flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)
return drm_bridge_attach(bridge->encoder, hdmi->next_bridge,
bridge, flags);
return dw_hdmi_connector_create(hdmi);
}
其中dw_hdmi_connector_create
函数用于初始化 connector
;
static int dw_hdmi_connector_create(struct dw_hdmi *hdmi)
{
struct drm_connector *connector = &hdmi->connector;
struct cec_connector_info conn_info;
struct cec_notifier *notifier;
if (hdmi->version >= 0x200a)
connector->ycbcr_420_allowed =
hdmi->plat_data->ycbcr_420_allowed;
else
connector->ycbcr_420_allowed = false;
connector->interlace_allowed = 1;
connector->polled = DRM_CONNECTOR_POLL_HPD;
// 设置connector的辅助函数helper_private为dw_hdmi_connector_helper_funcs
drm_connector_helper_add(connector, &dw_hdmi_connector_helper_funcs);
// connector初始化, connector的控制函数func设置为dw_hdmi_connector_funcs
drm_connector_init_with_ddc(hdmi->bridge.dev, connector,
&dw_hdmi_connector_funcs,
DRM_MODE_CONNECTOR_HDMIA,
hdmi->ddc);
/*
* drm_connector_attach_max_bpc_property() requires the
* connector to have a state.
*/
drm_atomic_helper_connector_reset(connector);
drm_connector_attach_max_bpc_property(connector, 8, 16);
if (hdmi->version >= 0x200a && hdmi->plat_data->use_drm_infoframe)
drm_connector_attach_hdr_output_metadata_property(connector);
// connector->possible_encoders |= drm_encoder_mask(encoder);
drm_connector_attach_encoder(connector, hdmi->bridge.encoder);
cec_fill_conn_info_from_drm(&conn_info, connector);
notifier = cec_notifier_conn_register(hdmi->dev, NULL, &conn_info);
if (!notifier)
return -ENOMEM;
mutex_lock(&hdmi->cec_notifier_mutex);
hdmi->cec_notifier = notifier;
mutex_unlock(&hdmi->cec_notifier_mutex);
return 0;
}
(1)connector
的辅助函数helper_private
为dw_hdmi_connector_helper_funcs
;
static const struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = {
.get_modes = dw_hdmi_connector_get_modes,
.atomic_check = dw_hdmi_connector_atomic_check,
};
get_modes
用于通过DDC
探测的所有显示模式,并将其添加到connector
的 probed_modes
链表中。
(2)connector
的控制函数func
设置为dw_hdmi_connector_funcs
;
static const struct drm_connector_funcs dw_hdmi_connector_funcs = {
.fill_modes = drm_helper_probe_single_connector_modes,
.detect = dw_hdmi_connector_detect,
.destroy = drm_connector_cleanup,
.force = dw_hdmi_connector_force,
.reset = drm_atomic_helper_connector_reset,
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};
其中fill_modes
中指定的drm_helper_probe_single_connector_modes
函数用于检测并筛选出所有有效的显示模式。该函数旨在作为使用CRTC
辅助函数进行输出模式过滤和检测的驱动程序的drm_connector_funcs.fill_modes
函数的通用实现。基本过程如下:
-
将
connector
的modes
链表中的所有显示模式标记为过时; -
使用
drm_mode_probed_add
将新显示模式添加到connector
的probed_modes
链表中,新显示模式的状态初始值为OK
。显示模式从单个来源按照以下优先顺序添加;drm_connector_helper_funcs.get_modes
回调函数(drm_helper_probe_get_modes
);- 如果
connector
状态为connected
并且drm_helper_probe_get_modes
未获取到显示模式,则自动添加标准的VESA DMT
显示模式,最高分辨率为1024x768
(drm_add_modes_noedid
);
-
通过内核命令行指定的显示模式将与之前的探测结果一起添加(
drm_helper_probe_add_cmdline_mode
),这些模显示模式是使用VESA GTF
/CVT
算法生成的; -
将显示模式从
probed_modes
链表移动到modes
链表中,潜在的重复模式将被合并在一起(drm_connector_list_update
);此步骤完成后,probed_modes
链表将再次为空; -
modes
列表中的任何非过时显示模式都要进行验证,并更新显示模式的状态;drm_mode_validate_basic
执行基本的合法性检查;drm_mode_validate_size
过滤掉大于maxX
和maxY
(如果有指定)的模式;drm_mode_validate_flag
根据基本连接器能力(允许交错,允许双扫描,允许立体)检查模式;- 可选的
drm_connector_helper_funcs.mode_valid
或drm_connector_helper_funcs.mode_valid_ctx
辅助函数可以执行驱动程序和/或显示器特定的检查; - 可选的
drm_crtc_helper_funcs.mode_valid
,drm_bridge_funcs.mode_valid
(会调用dw_hdmi_bridge_mode_valid
函数进行校验)和drm_encoder_helper_funcs.mode_valid
辅助函数可以执行驱动程序和/或源特定的检查,这些辅助函数也由modeset
/atomic
辅助函数执行;
-
从
connector
的modes
链表中去除任何状态不为OK
的模式,同时输出调试消息指示模式被拒绝的原因(drm_mode_prune_invalid
)。
这里我们简单说一下drm_xxx_helper_funcs
和drm_xxx_funcs
的区别;
drm_connector_funcs
是应用层进行drm ioctl
操作是的最终入口,对于大多数的SoC
厂商来说,他们的drm_xxx_funcs
操作流程基本相同,仅仅是在寄存器配置上存在差异,因此开发者将那些通用的操作流程封装了helper
函数,而将那些厂商差异化的代码放到了drm_xxx_helper_funcs
中去,由SoC
厂商自己实现。
比如dw_hdmi_connector_funcs
中的fill_modes
、reset
、atomic_duplicate_state
等都是使用的通用的helper
函数,他们定义在drivers/gpu/drm/drm_probe_helper.c
、drivers/gpu/drm/drm_atomic_state_helper.c
等文件中:这些helper
函数内部实现一般就是回调dw_hdmi_connector_helper_funcs
中的相应方法。
5.4.2 dw_hdmi_bridge_get_edid
dw_hdmi_bridge_get_edid
用于获取edid
信息;
static struct edid *dw_hdmi_bridge_get_edid(struct drm_bridge *bridge,
struct drm_connector *connector)
{
struct dw_hdmi *hdmi = bridge->driver_private;
// 获取edid信息
return dw_hdmi_get_edid(hdmi, connector);
}
5.4.3 分析小结
经过分析我们发现无论是bridget
的funcs
中的get_edid
还是connector
的helper_private
中get_modes
都会调用dw_hdmi_get_edid
获取连接器的edid
信息;
// bridget的funcs被设置为dw_hdmi_bridge_funcs
static const struct drm_bridge_funcs dw_hdmi_bridge_funcs = {
.get_edid = dw_hdmi_bridge_get_edid,
......
};
dw_hdmi_bridge_get_edid(bridge,connector)
dw_hdmi_get_edid(hdmi, connector)
// connector的helper_private被设置为dw_hdmi_connector_helper_funcs
static const struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = {
.get_modes = dw_hdmi_connector_get_modes,
......
};
dw_hdmi_connector_get_modes(connector)
dw_hdmi_get_edid(hdmi, connector)
5.5 drm_bridge_add
drm_bridge_add
函数定义在drivers/gpu/drm/drm_bridge.c
,用于将当前桥接设备到全局链表bridge_list
中;
/**
* drm_bridge_add - add the given bridge to the global bridge list
*
* @bridge: bridge control structure
*/
void drm_bridge_add(struct drm_bridge *bridge)
{
mutex_init(&bridge->hpd_mutex);
mutex_lock(&bridge_lock);
list_add_tail(&bridge->list, &bridge_list);
mutex_unlock(&bridge_lock);
}
六、drm_add_edid_modes
struct drm_connector_helper_funcs
的get_modes
用于通过DDC
探测到connector
的所有显示模式,并将其添加到connector
的 probed_modes
链表中,这些模式还没有经过筛选和过滤;
需要注意的是:在探测阶段,系统可能会探测到一些暂时不可用或不推荐的显示模式,这些模式会先被存储在 probed_modes
中。之后,这些模式可能会经过进一步的处理和筛选,最终加入到 modes
中成为最终可用的显示模式列表。
static const struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = {
.get_modes = dw_hdmi_connector_get_modes,
.atomic_check = dw_hdmi_connector_atomic_check,
};
dw_hdmi_connector_get_modes
主要包括两步:
- 通过
dw_hdmi_get_edid
获取connector
的edid
信息; - 通过
drm_add_edid_modes
函数解析edid
信息,并将其转换为显示模式,添加到connector
的probed_modes
链表;
dw_hdmi_connector_get_modes
函数位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
文件
static int dw_hdmi_connector_get_modes(struct drm_connector *connector)
{
struct dw_hdmi *hdmi = container_of(connector, struct dw_hdmi,
connector);
struct edid *edid;
int ret;
// 获取connector的edid信息
edid = dw_hdmi_get_edid(hdmi, connector);
if (!edid)
return 0;
// 更新连接器edid属性
drm_connector_update_edid_property(connector, edid);
cec_notifier_set_phys_addr_from_edid(hdmi->cec_notifier, edid);
// 解析edid中的显示模式并添加到connector的probed_modes链表
ret = drm_add_edid_modes(connector, edid);
kfree(edid);
return ret;
}
6.1 dw_hdmi_get_edid
dw_hdmi_get_edid
定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
,用于获取connector
的edid
信息;
static struct edid *dw_hdmi_get_edid(struct dw_hdmi *hdmi,
struct drm_connector *connector)
{
struct edid *edid;
if (!hdmi->ddc)
return NULL;
// 通过I2C通信获取edid信息
edid = drm_get_edid(connector, hdmi->ddc);
if (!edid) {
dev_dbg(hdmi->dev, "failed to get edid\n");
return NULL;
}
dev_dbg(hdmi->dev, "got edid: width[%d] x height[%d]\n",
edid->width_cm, edid->height_cm);
hdmi->sink_is_hdmi = drm_detect_hdmi_monitor(edid);
hdmi->sink_has_audio = drm_detect_monitor_audio(edid);
return edid;
}
drm_get_edid
函数位于drivers/gpu/drm/drm_edid.c
;
/**
* drm_get_edid - get EDID data, if available
* @connector: connector we're probing
* @adapter: I2C adapter to use for DDC
*
* Poke the given I2C channel to grab EDID data if possible. If found,
* attach it to the connector.
*
* Return: Pointer to valid EDID or NULL if we couldn't find any.
*/
struct edid *drm_get_edid(struct drm_connector *connector,
struct i2c_adapter *adapter)
{
struct edid *edid;
if (connector->force == DRM_FORCE_OFF)
return NULL;
if (connector->force == DRM_FORCE_UNSPECIFIED && !drm_probe_ddc(adapter))
return NULL;
// 调用drm_do_probe_ddc_edid函数实现通过I2C总线读取edid信息,有兴趣可以看一下i2c_transfer,I2C从设备地址为0x50
edid = _drm_do_get_edid(connector, drm_do_probe_ddc_edid, adapter, NULL);
drm_connector_update_edid_property(connector, edid);
return edid;
}
6.2 drm_add_edid_modes
drm_add_edid_modes
函数用于解析edid
信息,并将其转换为显示模式,添加到connector
的 probed_modes
链表。
在这之前,我们需要大概了解一下CVT
/GTF
/DMT
,因此后面在分析代码讲解如何解析edid
并将其转换为显示模式的时候会有所涉及。CVT
(Coordinated Video Timing
)、GTF
(Generalized Timing Formula
)和DMT
(Display Monitor Timings
)是三种不同的显示器时序规范。它们在计算和定义显示模式参数方面有所不同;
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了