Rockchip RK3399 - DRM vop驱动程序
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4
开发板
eMMC
:16GB
LPDDR3
:4GB
显示屏 :15.6
英寸HDMI
接口显示屏
u-boot
:2023.04
linux
:6.3
----------------------------------------------------------------------------------------------------------------------------
有关《Rockchip RK3399 - DRM crtc
基础知识》我们已经在前面介绍过了,在linux
内核中Rockchip VOP driver
对应crtc driver
,crtc
负责连接plane
和encoder
。
这里我们介绍一下Rochchip DRM
驱动中与vop
相关的实现,具体实现文件:
drivers/gpu/drm/rockchip/rockchip_drm_vop.c
;drivers/gpu/drm/rockchip/rockchip_vop_reg.c
;drivers/gpu/drm/rockchip/rockchip_drm_vop.h
;drivers/gpu/drm/rockchip/rockchip_vop_reg.h
;
一、设备树配置
1.1 vopb
设备节点
以vopb
设备节点为例:
vopb: vop@ff900000 {
compatible = "rockchip,rk3399-vop-big";
reg = <0x0 0xff900000 0x0 0x2000>, <0x0 0xff902000 0x0 0x1000>;
interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH 0>;
assigned-clocks = <&cru ACLK_VOP0>, <&cru HCLK_VOP0>;
assigned-clock-rates = <400000000>, <100000000>;
clocks = <&cru ACLK_VOP0>, <&cru DCLK_VOP0>, <&cru HCLK_VOP0>;
clock-names = "aclk_vop", "dclk_vop", "hclk_vop";
iommus = <&vopb_mmu>;
power-domains = <&power RK3399_PD_VOPB>;
resets = <&cru SRST_A_VOP0>, <&cru SRST_H_VOP0>, <&cru SRST_D_VOP0>;
reset-names = "axi", "ahb", "dclk";
status = "disabled";
vopb_out: port {
#address-cells = <1>;
#size-cells = <0>;
vopb_out_edp: endpoint@0 {
reg = <0>;
remote-endpoint = <&edp_in_vopb>;
};
vopb_out_mipi: endpoint@1 {
reg = <1>;
remote-endpoint = <&mipi_in_vopb>;
};
vopb_out_hdmi: endpoint@2 {
reg = <2>;
remote-endpoint = <&hdmi_in_vopb>;
};
vopb_out_mipi1: endpoint@3 {
reg = <3>;
remote-endpoint = <&mipi1_in_vopb>;
};
vopb_out_dp: endpoint@4 {
reg = <4>;
remote-endpoint = <&dp_in_vopb>;
};
};
};
1.2 启用vopb
如果我们希望启用vopb
上,则需要在arch/arm64/boot/dts/rockchip/rk3399-evb.dts
中为以下节点新增属性:
# 使能vopb
&vopb {
status = "okay";
};
&vopb_mmu {
status = "okay";
};
&display_subsystem {
status = "okay";
};
二、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(vop_platform_driver, CONFIG_ROCKCHIP_VOP);
......
// 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(vop_platform_driver, CONFIG_ROCKCHIP_VOP);
会将vop_platform_driver
保存到rockchip_sub_drivers
数组中。
并调用platform_register_drivers
遍历rockchip_sub_drivers
数组,多次调用platform_driver_register
注册platform driver
。
2.2 vop_platform_driver
vop_platform_driver
定义在drivers/gpu/drm/rockchip/rockchip_vop_reg.c
;
struct platform_driver vop_platform_driver = {
.probe = vop_probe,
.remove = vop_remove,
.driver = {
.name = "rockchip-vop",
.of_match_table = vop_driver_dt_match,
},
};
2.2.1 of_match_table
其中of_match_table
用于设备树匹配,匹配设备树中compatible = "rockchip,rk3399-vop-big"
或者compatible = "rockchip,rk3399-vop-lit"
的设备节点;
static const struct of_device_id vop_driver_dt_match[] = {
{ .compatible = "rockchip,rk3036-vop",
.data = &rk3036_vop },
{ .compatible = "rockchip,rk3126-vop",
.data = &rk3126_vop },
{ .compatible = "rockchip,px30-vop-big",
.data = &px30_vop_big },
{ .compatible = "rockchip,px30-vop-lit",
.data = &px30_vop_lit },
{ .compatible = "rockchip,rk3066-vop",
.data = &rk3066_vop },
{ .compatible = "rockchip,rk3188-vop",
.data = &rk3188_vop },
{ .compatible = "rockchip,rk3288-vop",
.data = &rk3288_vop },
{ .compatible = "rockchip,rk3368-vop",
.data = &rk3368_vop },
{ .compatible = "rockchip,rk3366-vop",
.data = &rk3366_vop },
{ .compatible = "rockchip,rk3399-vop-big",
.data = &rk3399_vop_big },
{ .compatible = "rockchip,rk3399-vop-lit",
.data = &rk3399_vop_lit },
{ .compatible = "rockchip,rk3228-vop",
.data = &rk3228_vop },
{ .compatible = "rockchip,rk3328-vop",
.data = &rk3328_vop },
{},
};
2.2.2 vop_probe
在plaftorm
总线设备驱动模型中,我们知道当内核中有platform
设备和platform
驱动匹配,会调用到platform_driver
里的成员.probe
,在这里就是vop_probe
函数;
static int vop_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
// 未使用设备树 退出
if (!dev->of_node) {
DRM_DEV_ERROR(dev, "can't find vop devices\n");
return -ENODEV;
}
return component_add(dev, &vop_component_ops);
}
这里代码很简单,就是为设备pdev->dev
向系统注册一个component
,其中组件可执行的初始化操作被设置为了vop_component_ops
,其定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
;
const struct component_ops vop_component_ops = {
.bind = vop_bind,
.unbind = vop_unbind,
};
我们需要重点关注bind
函数的实现,这个函数内容较多我们单独小节介绍。
三、vop
相关数据结构
3.1 struct vop
struct vop
定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
,用于表示显示处理器;
// 描述vop win
struct vop_win {
struct drm_plane base; // 对应的struct drm_plane数据结构
const struct vop_win_data *data; // vop win data
const struct vop_win_yuv2yuv_data *yuv2yuv_data;
struct vop *vop; // 指向vop
};
// 描述显示处理器vop
struct vop {
struct drm_crtc crtc;
struct device *dev;
struct drm_device *drm_dev;
bool is_enabled;
struct completion dsp_hold_completion;
unsigned int win_enabled;
/* protected by dev->event_lock */
struct drm_pending_vblank_event *event;
struct drm_flip_work fb_unref_work;
unsigned long pending;
struct completion line_flag_completion;
const struct vop_data *data;
uint32_t *regsbak;
void __iomem *regs;
void __iomem *lut_regs;
/* physical map length of vop register */
uint32_t len;
/* one time only one process allowed to config the register */
spinlock_t reg_lock;
/* lock vop irq reg */
spinlock_t irq_lock;
/* protects crtc enable/disable */
struct mutex vop_lock;
unsigned int irq;
/* vop AHP clk */
struct clk *hclk;
/* vop dclk */
struct clk *dclk;
/* vop share memory frequency */
struct clk *aclk;
/* vop dclk reset */
struct reset_control *dclk_rst;
/* optional internal rgb encoder */
struct rockchip_rgb *rgb;
struct vop_win win[];
};
其中:
crtc
:指向一个struct drm_crtc
;dev
:这是一个指向设备结构体的指针,通常用于表示与vop
关联的设备;data
:存储与vop
相关的数据;drm_dev
:指向一个struct drm_device
,表示当前drm
设备;regs
:vop
相关寄存器起始虚拟地址;len
:vop
相关寄存器所占内存长度,单位字节;regsbak
:指向一块内存,用于备份vop
相关寄存器;irq
:存储vop
中断IRQ
编号;hclk
、dclk
、aclk
:vop
相关的各种时钟;rgb
:这是一个用于管理RGB
编码器的结构体指针;win
:这是一个表示vop win
的结构体数组,比如RK399 vop
支持4个图层,因此该该数组有4个元素;
3.2 struct vop_data
struct vop_data
定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.h
,用于描述与vop
相关的数据,主要是一些寄存器信息;
struct vop_data {
uint32_t version;
const struct vop_intr *intr;
const struct vop_common *common;
const struct vop_misc *misc;
const struct vop_modeset *modeset; // 模式配置相关寄存器信息
const struct vop_output *output; // 输出配置相关寄存器信息
const struct vop_afbc *afbc;
const struct vop_win_yuv2yuv_data *win_yuv2yuv;
const struct vop_win_data *win; // vop win配置相关寄存器信息
unsigned int win_size; // vop win个数
unsigned int lut_size;
#define VOP_FEATURE_OUTPUT_RGB10 BIT(0)
#define VOP_FEATURE_INTERNAL_RGB BIT(1)
u64 feature;
};
以rk3399_vop_big
为例,定义在drivers/gpu/drm/rockchip/rockchip_vop_reg.c
;
static const struct vop_data rk3399_vop_big = {
.version = VOP_VERSION(3, 5),
.feature = VOP_FEATURE_OUTPUT_RGB10,
.intr = &rk3366_vop_intr,
.common = &rk3399_common,
.modeset = &rk3288_modeset, // 模式配置相关寄存器信息
.output = &rk3399_output, // 输出配置相关寄存器信息
.afbc = &rk3399_vop_afbc,
.misc = &rk3368_misc,
.win = rk3399_vop_win_data, // vop win配置相关寄存器信息
.win_size = ARRAY_SIZE(rk3399_vop_win_data), // vop win个数
.win_yuv2yuv = rk3399_vop_big_win_yuv2yuv_data,
.lut_size = 1024,
};
其中rk3366_vop_intr
、rk3288_modeset
,rk3399_output
、rk3399_common
等配置了大量的寄存器信息。
3.2.1 rk3288_modeset
rk3288_modeset
定义如下:
static const struct vop_modeset rk3288_modeset = {
.htotal_pw = VOP_REG(RK3288_DSP_HTOTAL_HS_END, 0x1fff1fff, 0), // horizontal total size配置
.hact_st_end = VOP_REG(RK3288_DSP_HACT_ST_END, 0x1fff1fff, 0),
.vtotal_pw = VOP_REG(RK3288_DSP_VTOTAL_VS_END, 0x1fff1fff, 0), // vertical total size配置
.vact_st_end = VOP_REG(RK3288_DSP_VACT_ST_END, 0x1fff1fff, 0),
.hpost_st_end = VOP_REG(RK3288_POST_DSP_HACT_INFO, 0x1fff1fff, 0),
.vpost_st_end = VOP_REG(RK3288_POST_DSP_VACT_INFO, 0x1fff1fff, 0),
};
VOP_REG
用于定义寄存器信息:
#define _VOP_REG(off, _mask, _shift, _write_mask, _relaxed) \
{ \
.offset = off, \
.mask = _mask, \
.shift = _shift, \
.write_mask = _write_mask, \
.relaxed = _relaxed, \
}
#define VOP_REG(off, _mask, _shift) \
_VOP_REG(off, _mask, _shift, false, true)
3.2.2 rk3399_common
rk3399_common
定义如下:
static const struct vop_common rk3399_common = {
.standby = VOP_REG_SYNC(RK3399_SYS_CTRL, 0x1, 22),
.gate_en = VOP_REG(RK3399_SYS_CTRL, 0x1, 23),
.mmu_en = VOP_REG(RK3399_SYS_CTRL, 0x1, 20),
.dither_down_sel = VOP_REG(RK3399_DSP_CTRL1, 0x1, 4),
.dither_down_mode = VOP_REG(RK3399_DSP_CTRL1, 0x1, 3),
.dither_down_en = VOP_REG(RK3399_DSP_CTRL1, 0x1, 2),
.pre_dither_down = VOP_REG(RK3399_DSP_CTRL1, 0x1, 1),
.dither_up = VOP_REG(RK3399_DSP_CTRL1, 0x1, 6),
.dsp_lut_en = VOP_REG(RK3399_DSP_CTRL1, 0x1, 0),
.update_gamma_lut = VOP_REG(RK3399_DSP_CTRL1, 0x1, 7),
.lut_buffer_index = VOP_REG(RK3399_DBG_POST_REG1, 0x1, 1),
.data_blank = VOP_REG(RK3399_DSP_CTRL0, 0x1, 19),
.dsp_blank = VOP_REG(RK3399_DSP_CTRL0, 0x3, 18),
.out_mode = VOP_REG(RK3399_DSP_CTRL0, 0xf, 0),
.cfg_done = VOP_REG_SYNC(RK3399_REG_CFG_DONE, 0x1, 0),
};
3.2.3 rk3399_vop_wn_data
rk3399_vop_wn_data
定义如下:
static const struct vop_win_phy rk3399_win01_data = {
.scl = &rk3288_win_full_scl,
.data_formats = formats_win_full,
.nformats = ARRAY_SIZE(formats_win_full),
.format_modifiers = format_modifiers_win_full_afbc,
.enable = VOP_REG(RK3288_WIN0_CTRL0, 0x1, 0),
.format = VOP_REG(RK3288_WIN0_CTRL0, 0x7, 1),
.rb_swap = VOP_REG(RK3288_WIN0_CTRL0, 0x1, 12),
.uv_swap = VOP_REG(RK3288_WIN0_CTRL0, 0x1, 15),
.x_mir_en = VOP_REG(RK3288_WIN0_CTRL0, 0x1, 21), // 水平方向上反转(翻转)使能
.y_mir_en = VOP_REG(RK3288_WIN0_CTRL0, 0x1, 22), // 垂直方向上反转(翻转)使能
.act_info = VOP_REG(RK3288_WIN0_ACT_INFO, 0x1fff1fff, 0),
.dsp_info = VOP_REG(RK3288_WIN0_DSP_INFO, 0x0fff0fff, 0),
.dsp_st = VOP_REG(RK3288_WIN0_DSP_ST, 0x1fff1fff, 0),
.yrgb_mst = VOP_REG(RK3288_WIN0_YRGB_MST, 0xffffffff, 0),
.uv_mst = VOP_REG(RK3288_WIN0_CBR_MST, 0xffffffff, 0),
.yrgb_vir = VOP_REG(RK3288_WIN0_VIR, 0x3fff, 0),
.uv_vir = VOP_REG(RK3288_WIN0_VIR, 0x3fff, 16),
.src_alpha_ctl = VOP_REG(RK3288_WIN0_SRC_ALPHA_CTRL, 0xff, 0),
.dst_alpha_ctl = VOP_REG(RK3288_WIN0_DST_ALPHA_CTRL, 0xff, 0),
.channel = VOP_REG(RK3288_WIN0_CTRL2, 0xff, 0),
};
......
/*
* rk3399 vop big windows register layout is same as rk3288, but we
* have a separate rk3399 win data array here so that we can advertise
* AFBC on the primary plane.
*/
static const struct vop_win_data rk3399_vop_win_data[] = {
{ .base = 0x00, .phy = &rk3399_win01_data,
.type = DRM_PLANE_TYPE_PRIMARY }, // primary plane
{ .base = 0x40, .phy = &rk3368_win01_data,
.type = DRM_PLANE_TYPE_OVERLAY }, // overlay plane
{ .base = 0x00, .phy = &rk3368_win23_data,
.type = DRM_PLANE_TYPE_OVERLAY }, // overlay plane
{ .base = 0x50, .phy = &rk3368_win23_data,
.type = DRM_PLANE_TYPE_CURSOR }, // cursor plane
};
RK3399
其vop
包含4个图层,因此这里存放就是RK3399
的4个图层配置相关寄存器的信息。
3.2.4 rk3399_vop_big_win_yuv2yuv_data
rk3399_vop_big_win_yuv2yuv_data
定义如下:
static const struct vop_win_yuv2yuv_data rk3399_vop_big_win_yuv2yuv_data[] = {
{ .base = 0x00, .phy = &rk3399_yuv2yuv_win01_data,
.y2r_en = VOP_REG(RK3399_YUV2YUV_WIN, 0x1, 1) },
{ .base = 0x60, .phy = &rk3399_yuv2yuv_win01_data,
.y2r_en = VOP_REG(RK3399_YUV2YUV_WIN, 0x1, 9) },
{ .base = 0xC0, .phy = &rk3399_yuv2yuv_win23_data },
{ .base = 0x120, .phy = &rk3399_yuv2yuv_win23_data },
};
3.3 rk3399_output
rk3399_output
定义如下:
static const struct vop_output rk3399_output = {
.dp_dclk_pol = VOP_REG(RK3399_DSP_CTRL1, 0x1, 19),
.rgb_dclk_pol = VOP_REG(RK3368_DSP_CTRL1, 0x1, 19),
.hdmi_dclk_pol = VOP_REG(RK3368_DSP_CTRL1, 0x1, 23),
.edp_dclk_pol = VOP_REG(RK3368_DSP_CTRL1, 0x1, 27),
.mipi_dclk_pol = VOP_REG(RK3368_DSP_CTRL1, 0x1, 31),
// 接口引脚极性相关寄存器位
.dp_pin_pol = VOP_REG(RK3399_DSP_CTRL1, 0x7, 16),
.rgb_pin_pol = VOP_REG(RK3368_DSP_CTRL1, 0x7, 16),
.hdmi_pin_pol = VOP_REG(RK3368_DSP_CTRL1, 0x7, 20),
.edp_pin_pol = VOP_REG(RK3368_DSP_CTRL1, 0x7, 24),
.mipi_pin_pol = VOP_REG(RK3368_DSP_CTRL1, 0x7, 28),
// 接口使能相关寄存器位
.dp_en = VOP_REG(RK3399_SYS_CTRL, 0x1, 11), // dp接口输出使能
.rgb_en = VOP_REG(RK3288_SYS_CTRL, 0x1, 12), // rgb接口输出使能
.hdmi_en = VOP_REG(RK3288_SYS_CTRL, 0x1, 13), // hdmi接口输出使能
.edp_en = VOP_REG(RK3288_SYS_CTRL, 0x1, 14), // edp接口输出使能
.mipi_en = VOP_REG(RK3288_SYS_CTRL, 0x1, 15), // mipi接口输出使能
.mipi_dual_channel_en = VOP_REG(RK3288_SYS_CTRL, 0x1, 3), // mipi1接口输出使能
};
这里以如下代码为例:
#define RK3288_SYS_CTRL 0x0008
.hdmi_en = VOP_REG(RK3288_SYS_CTRL, 0x1, 13),
其中:
RK3288_SYS_CTRL
为offset
,表示寄存器偏移,相对vop
基址,0x1
为mask
:表示掩码;13
为shift
,表示偏移位;
因此,总结下来就是地址为0xff900000+0x0008
=0xff900008
的寄存器的位13用于hdmi
接口的使能。
不幸的是在RK3399 datasheet
没有找到与vop
相关的任何信息,我猜测Rockchip
应该是vop
这部分并不对用户开放。
不过我们可以通过drivers/gpu/drm/rockchip/rockchip_vop_reg.h
中的宏定义猜测出vop
包含了哪些寄存器;
/* rk3399 register definition */
#define RK3399_REG_CFG_DONE 0x0000
#define RK3399_VERSION_INFO 0x0004
#define RK3399_SYS_CTRL 0x0008
#define RK3399_SYS_CTRL1 0x000c
#define RK3399_DSP_CTRL0 0x0010
#define RK3399_DSP_CTRL1 0x0014
#define RK3399_DSP_BG 0x0018
#define RK3399_MCU_CTRL 0x001c
#define RK3399_WB_CTRL0 0x0020
#define RK3399_WB_CTRL1 0x0024
#define RK3399_WB_YRGB_MST 0x0028
#define RK3399_WB_CBR_MST 0x002c
#define RK3399_WIN0_CTRL0 0x0030
#define RK3399_WIN0_CTRL1 0x0034
#define RK3399_WIN0_COLOR_KEY 0x0038
#define RK3399_WIN0_VIR 0x003c
#define RK3399_WIN0_YRGB_MST 0x0040
#define RK3399_WIN0_CBR_MST 0x0044
#define RK3399_WIN0_ACT_INFO 0x0048
#define RK3399_WIN0_DSP_INFO 0x004c
#define RK3399_WIN0_DSP_ST 0x0050
#define RK3399_WIN0_SCL_FACTOR_YRGB 0x0054
#define RK3399_WIN0_SCL_FACTOR_CBR 0x0058
#define RK3399_WIN0_SCL_OFFSET 0x005c
#define RK3399_WIN0_SRC_ALPHA_CTRL 0x0060
#define RK3399_WIN0_DST_ALPHA_CTRL 0x0064
#define RK3399_WIN0_FADING_CTRL 0x0068
#define RK3399_WIN0_CTRL2 0x006c
#define RK3399_WIN1_CTRL0 0x0070
#define RK3399_WIN1_CTRL1 0x0074
#define RK3399_WIN1_COLOR_KEY 0x0078
#define RK3399_WIN1_VIR 0x007c
#define RK3399_WIN1_YRGB_MST 0x0080
#define RK3399_WIN1_CBR_MST 0x0084
#define RK3399_WIN1_ACT_INFO 0x0088
#define RK3399_WIN1_DSP_INFO 0x008c
#define RK3399_WIN1_DSP_ST 0x0090
#define RK3399_WIN1_SCL_FACTOR_YRGB 0x0094
#define RK3399_WIN1_SCL_FACTOR_CBR 0x0098
#define RK3399_WIN1_SCL_OFFSET 0x009c
#define RK3399_WIN1_SRC_ALPHA_CTRL 0x00a0
#define RK3399_WIN1_DST_ALPHA_CTRL 0x00a4
#define RK3399_WIN1_FADING_CTRL 0x00a8
#define RK3399_WIN1_CTRL2 0x00ac
#define RK3399_WIN2_CTRL0 0x00b0
#define RK3399_WIN2_CTRL1 0x00b4
#define RK3399_WIN2_VIR0_1 0x00b8
#define RK3399_WIN2_VIR2_3 0x00bc
#define RK3399_WIN2_MST0 0x00c0
#define RK3399_WIN2_DSP_INFO0 0x00c4
#define RK3399_WIN2_DSP_ST0 0x00c8
#define RK3399_WIN2_COLOR_KEY 0x00cc
#define RK3399_WIN2_MST1 0x00d0
#define RK3399_WIN2_DSP_INFO1 0x00d4
#define RK3399_WIN2_DSP_ST1 0x00d8
#define RK3399_WIN2_SRC_ALPHA_CTRL 0x00dc
#define RK3399_WIN2_MST2 0x00e0
#define RK3399_WIN2_DSP_INFO2 0x00e4
#define RK3399_WIN2_DSP_ST2 0x00e8
#define RK3399_WIN2_DST_ALPHA_CTRL 0x00ec
#define RK3399_WIN2_MST3 0x00f0
#define RK3399_WIN2_DSP_INFO3 0x00f4
#define RK3399_WIN2_DSP_ST3 0x00f8
#define RK3399_WIN2_FADING_CTRL 0x00fc
#define RK3399_WIN3_CTRL0 0x0100
#define RK3399_WIN3_CTRL1 0x0104
#define RK3399_WIN3_VIR0_1 0x0108
#define RK3399_WIN3_VIR2_3 0x010c
#define RK3399_WIN3_MST0 0x0110
#define RK3399_WIN3_DSP_INFO0 0x0114
#define RK3399_WIN3_DSP_ST0 0x0118
#define RK3399_WIN3_COLOR_KEY 0x011c
#define RK3399_WIN3_MST1 0x0120
#define RK3399_WIN3_DSP_INFO1 0x0124
#define RK3399_WIN3_DSP_ST1 0x0128
#define RK3399_WIN3_SRC_ALPHA_CTRL 0x012c
#define RK3399_WIN3_MST2 0x0130
#define RK3399_WIN3_DSP_INFO2 0x0134
#define RK3399_WIN3_DSP_ST2 0x0138
#define RK3399_WIN3_DST_ALPHA_CTRL 0x013c
#define RK3399_WIN3_MST3 0x0140
#define RK3399_WIN3_DSP_INFO3 0x0144
#define RK3399_WIN3_DSP_ST3 0x0148
#define RK3399_WIN3_FADING_CTRL 0x014c
#define RK3399_HWC_CTRL0 0x0150
#define RK3399_HWC_CTRL1 0x0154
#define RK3399_HWC_MST 0x0158
#define RK3399_HWC_DSP_ST 0x015c
#define RK3399_HWC_SRC_ALPHA_CTRL 0x0160
#define RK3399_HWC_DST_ALPHA_CTRL 0x0164
#define RK3399_HWC_FADING_CTRL 0x0168
#define RK3399_HWC_RESERVED1 0x016c
#define RK3399_POST_DSP_HACT_INFO 0x0170
#define RK3399_POST_DSP_VACT_INFO 0x0174
#define RK3399_POST_SCL_FACTOR_YRGB 0x0178
#define RK3399_POST_RESERVED 0x017c
#define RK3399_POST_SCL_CTRL 0x0180
#define RK3399_POST_DSP_VACT_INFO_F1 0x0184
#define RK3399_DSP_HTOTAL_HS_END 0x0188
#define RK3399_DSP_HACT_ST_END 0x018c
#define RK3399_DSP_VTOTAL_VS_END 0x0190
#define RK3399_DSP_VACT_ST_END 0x0194
#define RK3399_DSP_VS_ST_END_F1 0x0198
#define RK3399_DSP_VACT_ST_END_F1 0x019c
#define RK3399_PWM_CTRL 0x01a0
#define RK3399_PWM_PERIOD_HPR 0x01a4
#define RK3399_PWM_DUTY_LPR 0x01a8
#define RK3399_PWM_CNT 0x01ac
#define RK3399_BCSH_COLOR_BAR 0x01b0
#define RK3399_BCSH_BCS 0x01b4
#define RK3399_BCSH_H 0x01b8
#define RK3399_BCSH_CTRL 0x01bc
#define RK3399_CABC_CTRL0 0x01c0
#define RK3399_CABC_CTRL1 0x01c4
#define RK3399_CABC_CTRL2 0x01c8
#define RK3399_CABC_CTRL3 0x01cc
#define RK3399_CABC_GAUSS_LINE0_0 0x01d0
#define RK3399_CABC_GAUSS_LINE0_1 0x01d4
#define RK3399_CABC_GAUSS_LINE1_0 0x01d8
#define RK3399_CABC_GAUSS_LINE1_1 0x01dc
#define RK3399_CABC_GAUSS_LINE2_0 0x01e0
#define RK3399_CABC_GAUSS_LINE2_1 0x01e4
#define RK3399_FRC_LOWER01_0 0x01e8
#define RK3399_FRC_LOWER01_1 0x01ec
#define RK3399_FRC_LOWER10_0 0x01f0
#define RK3399_FRC_LOWER10_1 0x01f4
#define RK3399_FRC_LOWER11_0 0x01f8
#define RK3399_FRC_LOWER11_1 0x01fc
#define RK3399_AFBCD0_CTRL 0x0200
#define RK3399_AFBCD0_HDR_PTR 0x0204
#define RK3399_AFBCD0_PIC_SIZE 0x0208
#define RK3399_AFBCD0_STATUS 0x020c
#define RK3399_AFBCD1_CTRL 0x0220
#define RK3399_AFBCD1_HDR_PTR 0x0224
#define RK3399_AFBCD1_PIC_SIZE 0x0228
#define RK3399_AFBCD1_STATUS 0x022c
#define RK3399_AFBCD2_CTRL 0x0240
#define RK3399_AFBCD2_HDR_PTR 0x0244
#define RK3399_AFBCD2_PIC_SIZE 0x0248
#define RK3399_AFBCD2_STATUS 0x024c
#define RK3399_AFBCD3_CTRL 0x0260
#define RK3399_AFBCD3_HDR_PTR 0x0264
#define RK3399_AFBCD3_PIC_SIZE 0x0268
#define RK3399_AFBCD3_STATUS 0x026c
#define RK3399_INTR_EN0 0x0280
#define RK3399_INTR_CLEAR0 0x0284
#define RK3399_INTR_STATUS0 0x0288
#define RK3399_INTR_RAW_STATUS0 0x028c
#define RK3399_INTR_EN1 0x0290
#define RK3399_INTR_CLEAR1 0x0294
#define RK3399_INTR_STATUS1 0x0298
#define RK3399_INTR_RAW_STATUS1 0x029c
#define RK3399_LINE_FLAG 0x02a0
#define RK3399_VOP_STATUS 0x02a4
#define RK3399_BLANKING_VALUE 0x02a8
#define RK3399_MCU_BYPASS_PORT 0x02ac
#define RK3399_WIN0_DSP_BG 0x02b0
#define RK3399_WIN1_DSP_BG 0x02b4
#define RK3399_WIN2_DSP_BG 0x02b8
#define RK3399_WIN3_DSP_BG 0x02bc
#define RK3399_YUV2YUV_WIN 0x02c0
#define RK3399_YUV2YUV_POST 0x02c4
#define RK3399_AUTO_GATING_EN 0x02cc
#define RK3399_DBG_POST_REG1 0x036c
#define RK3399_WIN0_CSC_COE 0x03a0
#define RK3399_WIN1_CSC_COE 0x03c0
#define RK3399_WIN2_CSC_COE 0x03e0
#define RK3399_WIN3_CSC_COE 0x0400
#define RK3399_HWC_CSC_COE 0x0420
#define RK3399_BCSH_R2Y_CSC_COE 0x0440
#define RK3399_BCSH_Y2R_CSC_COE 0x0460
#define RK3399_POST_YUV2YUV_Y2R_COE 0x0480
#define RK3399_POST_YUV2YUV_3X3_COE 0x04a0
#define RK3399_POST_YUV2YUV_R2Y_COE 0x04c0
#define RK3399_WIN0_YUV2YUV_Y2R 0x04e0
#define RK3399_WIN0_YUV2YUV_3X3 0x0500
#define RK3399_WIN0_YUV2YUV_R2Y 0x0520
#define RK3399_WIN1_YUV2YUV_Y2R 0x0540
#define RK3399_WIN1_YUV2YUV_3X3 0x0560
#define RK3399_WIN1_YUV2YUV_R2Y 0x0580
#define RK3399_WIN2_YUV2YUV_Y2R 0x05a0
#define RK3399_WIN2_YUV2YUV_3X3 0x05c0
#define RK3399_WIN2_YUV2YUV_R2Y 0x05e0
#define RK3399_WIN3_YUV2YUV_Y2R 0x0600
#define RK3399_WIN3_YUV2YUV_3X3 0x0620
#define RK3399_WIN3_YUV2YUV_R2Y 0x0640
#define RK3399_WIN2_LUT_ADDR 0x1000
#define RK3399_WIN3_LUT_ADDR 0x1400
#define RK3399_HWC_LUT_ADDR 0x1800
#define RK3399_CABC_GAMMA_LUT_ADDR 0x1c00
#define RK3399_GAMMA_LUT_ADDR 0x2000
四、vop_bind
vop_bind
函数定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
,涉及到对vop
设备节点的解析,咱们以vopb
设备节点为例进行分析;
static int vop_bind(struct device *dev, struct device *master, void *data)
{
// 1. 获取platform_device
struct platform_device *pdev = to_platform_device(dev);
const struct vop_data *vop_data;
struct drm_device *drm_dev = data;
struct vop *vop;
struct resource *res;
int ret, irq;
// 2. 得到rk3399_vop_big
vop_data = of_device_get_match_data(dev);
if (!vop_data)
return -ENODEV;
/* Allocate vop struct and its vop_win array,初始化vop win */
vop = devm_kzalloc(dev, struct_size(vop, win, vop_data->win_size),
GFP_KERNEL);
if (!vop)
return -ENOMEM;
vop->dev = dev;
vop->data = vop_data;
vop->drm_dev = drm_dev;
// 设置驱动数据为vop
dev_set_drvdata(dev, vop);
// 3. 初始化vop win
vop_win_init(vop);
// 4. 获取第一个内存资源,即<0x0 0xff900000 0x0 0x2000>; VOP_BIG相关寄存器基地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 将VOP_BIG相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址
vop->regs = devm_ioremap_resource(dev, res);
if (IS_ERR(vop->regs))
return PTR_ERR(vop->regs);
vop->len = resource_size(res);
// 5. 获取第二个内存资源,即<0x0 0xff902000 0x0 0x1000>; LUT相关寄存器基地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (res) {
if (vop_data->lut_size != 1024 && vop_data->lut_size != 256) {
DRM_DEV_ERROR(dev, "unsupported gamma LUT size %d\n", vop_data->lut_size);
return -EINVAL;
}
// LUT相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址
vop->lut_regs = devm_ioremap_resource(dev, res);
if (IS_ERR(vop->lut_regs))
return PTR_ERR(vop->lut_regs);
}
vop->regsbak = devm_kzalloc(dev, vop->len, GFP_KERNEL);
if (!vop->regsbak)
return -ENOMEM;
// 6. 获取第1个IRQ编号 interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH 0>;
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
DRM_DEV_ERROR(dev, "cannot find irq for vop\n");
return irq;
}
vop->irq = (unsigned int)irq;
// 初始化自旋锁和互斥锁
spin_lock_init(&vop->reg_lock);
spin_lock_init(&vop->irq_lock);
mutex_init(&vop->vop_lock);
// 7. 创建crtc对象,并和plane关联在一起
ret = vop_create_crtc(vop);
if (ret)
return ret;
// 电源管理相关
pm_runtime_enable(&pdev->dev);
// 8. vop初始化
ret = vop_initial(vop);
if (ret < 0) {
DRM_DEV_ERROR(&pdev->dev,
"cannot initial vop dev - err %d\n", ret);
goto err_disable_pm_runtime;
}
// 9. 申请中断,中断处理函数设置为vop_isr
ret = devm_request_irq(dev, vop->irq, vop_isr,
IRQF_SHARED, dev_name(dev), vop);
if (ret)
goto err_disable_pm_runtime;
// 未设置
if (vop->data->feature & VOP_FEATURE_INTERNAL_RGB) {
vop->rgb = rockchip_rgb_init(dev, &vop->crtc, vop->drm_dev);
if (IS_ERR(vop->rgb)) {
ret = PTR_ERR(vop->rgb);
goto err_disable_pm_runtime;
}
}
// 10. dma初始化
rockchip_drm_dma_init_device(drm_dev, dev);
return 0;
err_disable_pm_runtime:
pm_runtime_disable(&pdev->dev);
vop_destroy_crtc(vop);
return ret;
}
这里我们以设备节点vopb
为例,对这段代码进行分析,主要流程如下:
(1) 调用to_platform_device
获取platform device
,设备节点为vopb
;
(2) 调用of_device_get_match_data
获取与特定设备匹配的数据,这里获取到的数据为rk3399_vop_big
;
(3) 调用vop_win_init
初始化vop win
;
(4) 首先调用platform_get_resource(pdev, IORESOURCE_MEM, 0)
获取第一个内存资源,即:
<0x0 0xff900000 0x0 0x2000>
0xff900000
为VOP_BIG
相关寄存器基地址;
接着调用devm_ioremap_resource(dev, res)
将VOP_BIG
相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址;
(5) 调用platform_get_resource(pdev, IORESOURCE_MEM, 1)
获取第二个内存资源,即:
<0x0 0xff902000 0x0 0x1000>
0xff902000
为LUT
相关寄存器基地址;
接着调用devm_ioremap_resource(dev, res)
将LUT
相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址;
(6) 调用platform_get_irq(pdev, 0)
获取第1个IRQ
编号:
interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH 0>
(7) 调用vop_create_crtc(vop)
初始化crtc
对象,并和plane
关联在一起;
(8) 调用vop_initial(vop)
进行vop
初始化;
(9) 调用devm_request_irq(dev, vop->irq, vop_isr, IRQF_SHARED, dev_name(dev), vop)
申请中断,中断处理函数设置为vop_isr
;
(10) 调用rockchip_drm_dma_init_device(drm_dev, dev)
进行dma
相关的初始化工作;
4.1 vo_win_init
vo_win_init
用于初始化vop win
,那么什么是vop win
,指定就是vop
图层,以RK3399
其vop
包含4个图层;
/*
* Initialize the vop->win array elements.
*/
static void vop_win_init(struct vop *vop)
{
const struct vop_data *vop_data = vop->data;
unsigned int i;
// 遍历每一个图层
for (i = 0; i < vop_data->win_size; i++) {
// 获取第i个vop win
struct vop_win *vop_win = &vop->win[i];
// 获取第i个vop win配置相关寄存器信息
const struct vop_win_data *win_data = &vop_data->win[i];
// 初始化成员
vop_win->data = win_data;
vop_win->vop = vop;
// rk3399定义了win_yuv2yuv,因此初始化yuv2yuv_data
if (vop_data->win_yuv2yuv)
vop_win->yuv2yuv_data = &vop_data->win_yuv2yuv[i];
}
}
4.2 vop_create_crtc
vop_create_crtc
用于初始化crtc
对象,并和plane
关联在一起,这里我们仍然以设备节点vopb
为例进行分析;
static int vop_create_crtc(struct vop *vop)
{
// 获取vop data
const struct vop_data *vop_data = vop->data;
// 获取device,对应的设备节点为vopb
struct device *dev = vop->dev;
// 获取drm device
struct drm_device *drm_dev = vop->drm_dev;
struct drm_plane *primary = NULL, *cursor = NULL, *plane, *tmp;
// 获取drm crtc
struct drm_crtc *crtc = &vop->crtc;
struct device_node *port;
int ret;
int i;
/*
* Create drm_plane for primary and cursor planes first, since we need
* to pass them to drm_crtc_init_with_planes, which sets the
* "possible_crtcs" to the newly initialized crtc.
* 1. 遍历每一个vop win,每个vop win内部包含一个drm_plane,对类型为primary和cursor plane
* 进行初始化
*/
for (i = 0; i < vop_data->win_size; i++) {
// 获取第i个vop win
struct vop_win *vop_win = &vop->win[i];
// 获取第i个vop win data
const struct vop_win_data *win_data = vop_win->data;
// 只处理primary和cursor plane
if (win_data->type != DRM_PLANE_TYPE_PRIMARY &&
win_data->type != DRM_PLANE_TYPE_CURSOR)
continue;
// 进行plane的初始化,其中funcs被设置为vop_plane_funcs
ret = drm_universal_plane_init(vop->drm_dev, // drm设备
&vop_win->base, // 要初始化的plane对象
0, // 可能的CRTCs的位掩码
&vop_plane_funcs, // plane的控制函数集合
win_data->phy->data_formats, // 支持的格式数组(DRM_FORMAT_*)
win_data->phy->nformats, // formats数组的长度
win_data->phy->format_modifiers,
win_data->type, // plane的类型
NULL);
if (ret) {
DRM_DEV_ERROR(vop->dev, "failed to init plane %d\n",
ret);
goto err_cleanup_planes;
}
// 获取当前plane
plane = &vop_win->base;
// 设置plane的辅助函数helper_private为plane_helper_funcs
drm_plane_helper_add(plane, &plane_helper_funcs);
// 如果vop win data配置了x_mir_en/y_mir_en,则调用drm_plane_create_rotation_property为plane附加rotation property
vop_plane_add_properties(plane, win_data);
// 保存primary plane
if (plane->type == DRM_PLANE_TYPE_PRIMARY)
primary = plane;
// 保存cursor plane
else if (plane->type == DRM_PLANE_TYPE_CURSOR)
cursor = plane;
}
// 2. 使用指定的primary and cursor planes初始化的crtc对象,其中crtc回调函数funcs设置为vop_crtc_funcs
ret = drm_crtc_init_with_planes(drm_dev, crtc, primary, cursor,
&vop_crtc_funcs, NULL);
if (ret)
goto err_cleanup_planes;
// 3. 设置crtc的辅助函数helper_private为vop_crtc_helper_funcs
drm_crtc_helper_add(crtc, &vop_crtc_helper_funcs);
// 进入
if (vop->lut_regs) {
drm_mode_crtc_set_gamma_size(crtc, vop_data->lut_size);
drm_crtc_enable_color_mgmt(crtc, 0, false, vop_data->lut_size);
}
/*
* Create drm_planes for overlay windows with possible_crtcs restricted
* to the newly created crtc.
* 4. 遍历每一个vop win,每个vop win内部包含一个drm_plane,对类型为overlay plane
* 进行初始化
*/
for (i = 0; i < vop_data->win_size; i++) {
// 获取第i个vop win
struct vop_win *vop_win = &vop->win[i];
// 获取第i个vop win data
const struct vop_win_data *win_data = vop_win->data;
unsigned long possible_crtcs = drm_crtc_mask(crtc);
// 只处理overlay plane
if (win_data->type != DRM_PLANE_TYPE_OVERLAY)
continue;
// 进行plane的初始化,其中funcs被设置为vop_plane_funcs
ret = drm_universal_plane_init(vop->drm_dev, &vop_win->base,
possible_crtcs,
&vop_plane_funcs,
win_data->phy->data_formats,
win_data->phy->nformats,
win_data->phy->format_modifiers,
win_data->type, NULL);
if (ret) {
DRM_DEV_ERROR(vop->dev, "failed to init overlay %d\n",
ret);
goto err_cleanup_crtc;
}
// 设置plane的辅助函数helper_private为plane_helper_funcs
drm_plane_helper_add(&vop_win->base, &plane_helper_funcs);
// 如果vop win data配置了x_mir_en/y_mir_en,则调用drm_plane_create_rotation_property为plane附加rotation property
vop_plane_add_properties(&vop_win->base, win_data);
}
// 5. 从vopb节点的子节点列表中查找名为port的子节点,也就是vopb_out节点
port = of_get_child_by_name(dev->of_node, "port");
if (!port) {
DRM_DEV_ERROR(vop->dev, "no port node found in %pOF\n",
dev->of_node);
ret = -ENOENT;
goto err_cleanup_crtc;
}
// 6. 初始化工作队列
drm_flip_work_init(&vop->fb_unref_work, "fb_unref",
vop_fb_unref_worker);
// 初始化完成量,
init_completion(&vop->dsp_hold_completion);
init_completion(&vop->line_flag_completion);
// 设置port节点
crtc->port = port;
// 7. 对crtc进行自刷新相关的辅助函数初始化
ret = drm_self_refresh_helper_init(crtc);
if (ret)
DRM_DEV_DEBUG_KMS(vop->dev,
"Failed to init %s with SR helpers %d, ignoring\n",
crtc->name, ret);
return 0;
err_cleanup_crtc:
drm_crtc_cleanup(crtc);
err_cleanup_planes:
list_for_each_entry_safe(plane, tmp, &drm_dev->mode_config.plane_list,
head)
drm_plane_cleanup(plane);
return ret;
err_cleanup_crtc:
drm_crtc_cleanup(crtc);
err_cleanup_planes:
list_for_each_entry_safe(plane, tmp, &drm_dev->mode_config.plane_list,
head)
drm_plane_cleanup(plane);
return ret;
}
以下是该函数的主要步骤:
(1) 遍历每一个vop win
,每个vop win
内部包含一个drm_plane
,对类型为primary
和cursor plane
进行初始化;
具体是调用drm_universal_plane_init
来初始化drm_plane
,并且添加plane
辅助函数、设置属性等;
其中funcs
被设置为vop_plane_funcs
,定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
;
static const struct drm_plane_funcs vop_plane_funcs = {
.update_plane = drm_atomic_helper_update_plane,
.disable_plane = drm_atomic_helper_disable_plane,
.destroy = vop_plane_destroy,
.reset = drm_atomic_helper_plane_reset,
.atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
.format_mod_supported = rockchip_mod_supported,
};
helper_private
被设为plane_helper_funcs
,定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
;
static const struct drm_plane_helper_funcs plane_helper_funcs = {
.atomic_check = vop_plane_atomic_check,
.atomic_update = vop_plane_atomic_update,
.atomic_disable = vop_plane_atomic_disable,
.atomic_async_check = vop_plane_atomic_async_check,
.atomic_async_update = vop_plane_atomic_async_update,
};
(2) 调用drm_crtc_init_with_planes
使用指定的primary and cursor planes
初始化的crtc
对象,其中crtc
回调函数funcs
设置为vop_crtc_funcs
,定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
;
static const struct drm_crtc_funcs vop_crtc_funcs = {
.set_config = drm_atomic_helper_set_config,
.page_flip = drm_atomic_helper_page_flip,
.destroy = vop_crtc_destroy,
.reset = vop_crtc_reset,
.atomic_duplicate_state = vop_crtc_duplicate_state,
.atomic_destroy_state = vop_crtc_destroy_state,
.enable_vblank = vop_crtc_enable_vblank,
.disable_vblank = vop_crtc_disable_vblank,
.set_crc_source = vop_crtc_set_crc_source,
.verify_crc_source = vop_crtc_verify_crc_source,
};
(3) 调用drm_crtc_helper_add
设置crtc
的辅助函数helper_private
为vop_crtc_helper_funcs
,定义在drivers/gpu/drm/rockchip/rockchip_drm_vop.c
;
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
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:你的「微服务管家」又秀新绝活了
2023-02-25 linux驱动移植-SPI控制器驱动