DRM-03: drm_mm的深入分析

1. DRM_MM概述与演进历程

在前面的文章:

DRM-01: DRM GEM显存管理

DRM-02: TTM与GEM的关系

说明了一些核心概念和之间的关系。本文详细论述了drm_mm的功能,概念较多,结合实际用例慢慢消化。

1.1 DRM_MM的核心定位与发展背景

drm_mm(Direct Rendering Manager Memory Manager)是Linux DRM子系统中负责内存管理的核心模块,专门针对GPU内存的特殊需求设计。作为GPU内存管理的中枢,它在现代图形处理和计算场景中扮演着关键角色。DRM_MM的设计目标是高效管理GPU可访问内存(包括显存、共享内存等)的分配、释放与地址映射,以

root@debian11:/home/rpdzkj/pcie_bt1120/build# sudo ./app --card=/dev/dri/card0 --w=1920 --h=1080 --fps=50 --addr=0xB1000000 --src-fmt=RGB888 --src-stride=5760 找不到可用的 connector/CRTC(请确认显示子系统/VOP2 已上线) #include <cstdio> #include <cstdlib> #include <cstdint> #include <cstring> #include <csignal> #include <string> #include <vector> #include <poll.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <errno.h> #include <cmath> // std::abs #include <climits> // INT_MAX // libdrm #include <drm.h> #include <drm_mode.h> #include <xf86drm.h> #include <xf86drmMode.h> #include <drm_fourcc.h> // =========== 选择 VOP 帧缓冲格式 =========== // 大多数 Rockchip VOP2 支持 UYVY;如遇到 plane 不支持,可以切到 XRGB8888 #ifndef FB_USE_UYVY #define FB_USE_UYVY 1 #endif #if FB_USE_UYVY #define FB_FMT DRM_FORMAT_UYVY #define FB_BPP 16 #define FB_BYTES_PER_PIXEL 2 #else #define FB_FMT DRM_FORMAT_XRGB8888 #define FB_BPP 32 #define FB_BYTES_PER_PIXEL 4 #endif // XDMA C 接口 extern "C" { int pcie_init(void); void pcie_deinit(void); void c2h_transfer(unsigned int addr, unsigned int len, unsigned char *dst); } static volatile int running = 1; static void on_sig(int){ running = 0; } static bool sw(const char* s,const char* p){ return std::strncmp(s,p,std::strlen(p))==0; } struct Args { std::string card = "/dev/dri/card0"; int w=1920, h=1080, fps=50; uint64_t xdma_addr = 0xB1000000ULL; std::string src_fmt = "UYVY"; // UYVY | RGB888 int src_stride = 0; // 0 => 自动 w*Bpp int conn_id = 0; // 手动指定连接器(可选) bool list=false; }; static Args parse(int ac,char**av){ Args a; for(int i=1;i<ac;i++){ if(!std::strcmp(av[i],"--list")) a.list=true; else if(sw(av[i],"--card=")) a.card=av[i]+7; else if(sw(av[i],"--w=")) a.w=std::atoi(av[i]+4); else if(sw(av[i],"--h=")) a.h=std::atoi(av[i]+4); else if(sw(av[i],"--fps=")) a.fps=std::atoi(av[i]+6); else if(sw(av[i],"--addr=")) a.xdma_addr=strtoull(av[i]+7,nullptr,0); else if(sw(av[i],"--src-fmt=")) a.src_fmt=av[i]+10; else if(sw(av[i],"--src-stride=")) a.src_stride=std::atoi(av[i]+13); else if(sw(av[i],"--conn-id=")) a.conn_id=std::atoi(av[i]+10); } if(a.fps<=0) a.fps=50; return a; } // clamp static inline uint8_t clamp_u8(int v){ if(v<0) return 0; if(v>255) return 255; return (uint8_t)v; } // 高效 RGB888 -> UYVY(2 像素一组,平均 U/V) static void rgb888_to_uyvy(const uint8_t* src, uint8_t* dst, int w, int h, int src_stride_bytes, int dst_pitch){ const int bpp = 3; for(int y=0;y<h;y++){ const uint8_t* sline = src + y*src_stride_bytes; uint8_t* dline = dst + y*dst_pitch; for(int x=0;x<w; x+=2){ const uint8_t *p0 = sline + x*bpp; const uint8_t *p1 = p0 + bpp; int r0=p0[0], g0=p0[1], b0=p0[2]; int r1=p1[0], g1=p1[1], b1=p1[2]; int Y0 = ( 77*r0 + 150*g0 + 29*b0 + 128) >> 8; // 0.299 0.587 0.114 int Y1 = ( 77*r1 + 150*g1 + 29*b1 + 128) >> 8; // BT.601 近似 int U0 = (-43*r0 - 85*g0 + 128*b0 + 32768) >> 8; int V0 = (128*r0 -107*g0 - 21*b0 + 32768) >> 8; int U1 = (-43*r1 - 85*g1 + 128*b1 + 32768) >> 8; int V1 = (128*r1 -107*g1 - 21*b1 + 32768) >> 8; int U = (U0 + U1) >> 1; int V = (V0 + V1) >> 1; dline[0] = clamp_u8(U); dline[1] = clamp_u8(Y0); dline[2] = clamp_u8(V); dline[3] = clamp_u8(Y1); dline += 4; } } } #if !FB_USE_UYVY // UYVY -> XRGB8888(如改用 XRGB8888) static void uyvy_to_xrgb8888(const uint8_t* src, uint8_t* dst, int w, int h, int src_stride, int dst_pitch){ for(int y=0;y<h;y++){ const uint8_t* s = src + y*src_stride; uint32_t* d = (uint32_t*)(dst + y*dst_pitch); for(int x=0;x<w; x+=2){ uint8_t U = s[0], Y0 = s[1], V = s[2], Y1 = s[3]; s += 4; auto YUV2RGB = [](int Y,int U,int V){ int C = Y - 16, D = U - 128, E = V - 128; int R = (298*C + 409*E + 128) >> 8; int G = (298*C - 100*D - 208*E + 128) >> 8; int B = (298*C + 516*D + 128) >> 8; return (0xFFu<<24) | ((uint8_t)clamp_u8(R)<<16) | ((uint8_t)clamp_u8(G)<<8) | ((uint8_t)clamp_u8(B)<<0); }; d[0] = YUV2RGB(Y0,U,V); d[1] = YUV2RGB(Y1,U,V); d += 2; } } } #endif struct DumbFB { uint32_t fb_id=0, handle=0, pitch=0; uint64_t size=0; uint8_t* map=nullptr; }; static int create_fb(int fd, int w, int h, DumbFB& out){ drm_mode_create_dumb creq{}; creq.width=w; creq.height=h; creq.bpp=FB_BPP; if(ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq)<0){ perror("CREATE_DUMB"); return -1; } uint32_t fb_id=0; uint32_t handles[4]={creq.handle,0,0,0}; uint32_t pitches[4]={creq.pitch,0,0,0}; // ★ 使用内核返回 pitch uint32_t offsets[4]={0,0,0,0}; if(drmModeAddFB2(fd, w, h, FB_FMT, handles, pitches, offsets, &fb_id, 0)!=0){ perror("drmModeAddFB2"); drm_mode_destroy_dumb d{}; d.handle=creq.handle; ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &d); return -2; } drm_mode_map_dumb mreq{}; mreq.handle=creq.handle; if(ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq)<0){ perror("MAP_DUMB"); drmModeRmFB(fd, fb_id); drm_mode_destroy_dumb d{}; d.handle=creq.handle; ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &d); return -3; } void* map = mmap(nullptr, creq.size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mreq.offset); if(map==MAP_FAILED){ perror("mmap"); drmModeRmFB(fd, fb_id); drm_mode_destroy_dumb d{}; d.handle=creq.handle; ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &d); return -4; } out.fb_id=fb_id; out.handle=creq.handle; out.pitch=creq.pitch; out.size=creq.size; out.map=(uint8_t*)map; return 0; } static void destroy_fb(int fd, DumbFB& fb){ if(fb.map && fb.size) munmap(fb.map, fb.size); if(fb.fb_id) drmModeRmFB(fd, fb.fb_id); if(fb.handle){ drm_mode_destroy_dumb d{}; d.handle=fb.handle; ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &d); } fb = DumbFB{}; } static void list_res(int fd){ drmModeRes* r = drmModeGetResources(fd); if(!r){ printf("no resources\n"); return; } printf("CRTCs:"); for(int i=0;i<r->count_crtcs;i++) printf(" %d", r->crtcs[i]); printf("\n"); for(int i=0;i<r->count_connectors;i++){ auto*c = drmModeGetConnector(fd, r->connectors[i]); if(!c) continue; const char* st = (c->connection==DRM_MODE_CONNECTED)?"connected":(c->connection==DRM_MODE_DISCONNECTED)?"disconnected":"unknown"; printf("conn-id=%d type=%d status=%s modes=%d\n", c->connector_id, c->connector_type, st, c->count_modes); for(int m=0;m<c->count_modes;m++){ auto& mm=c->modes[m]; printf(" %s %dx%d@%d\n", mm.name, mm.hdisplay, mm.vdisplay, mm.vrefresh); } drmModeFreeConnector(c); } drmModeFreeResources(r); } // 选择连接器 & 匹配 CRTC(根据 encoder->possible_crtcs) static bool pick_conn_enc_crtc(int fd, int prefer_conn_id, uint32_t& conn_id, uint32_t& enc_id, uint32_t& crtc_id){ drmModeRes* res = drmModeGetResources(fd); if(!res) return false; // 选 connector:优先用户指定,其次已连接,再否则随便一个 drmModeConnector* chosen_conn = nullptr; for(int i=0;i<res->count_connectors;i++){ drmModeConnector* c = drmModeGetConnector(fd, res->connectors[i]); if(!c) continue; if(prefer_conn_id>0 && (int)c->connector_id==prefer_conn_id){ chosen_conn=c; break; } if(!chosen_conn || c->connection==DRM_MODE_CONNECTED) { if(chosen_conn) drmModeFreeConnector(chosen_conn); chosen_conn=c; if(c->connection==DRM_MODE_CONNECTED) break; } else drmModeFreeConnector(c); } if(!chosen_conn){ drmModeFreeResources(res); return false; } // 选 encoder drmModeEncoder* chosen_enc = nullptr; if(chosen_conn->encoder_id){ chosen_enc = drmModeGetEncoder(fd, chosen_conn->encoder_id); } else if (chosen_conn->count_encoders>0){ chosen_enc = drmModeGetEncoder(fd, chosen_conn->encoders[0]); } if(!chosen_enc){ drmModeFreeConnector(chosen_conn); drmModeFreeResources(res); return false; } // 选 crtc:从 possible_crtcs bitmask 中挑第一个 int crtc_index = -1; for(int i=0;i<res->count_crtcs;i++){ if(chosen_enc->possible_crtcs & (1<<i)){ crtc_index = i; break; } } if(crtc_index<0){ // 兜底:用第一个 crtc_index = 0; } conn_id = chosen_conn->connector_id; enc_id = chosen_enc->encoder_id; crtc_id = res->crtcs[crtc_index]; drmModeFreeEncoder(chosen_enc); drmModeFreeConnector(chosen_conn); drmModeFreeResources(res); return conn_id && crtc_id; } // 在连接器 modes 里找一个最合适的(优先首选,其次匹配分辨率,最后第一个) static bool choose_mode(int fd, uint32_t conn_id, int want_w, int want_h, int want_hz, drmModeModeInfo& out){ drmModeConnector* c = drmModeGetConnector(fd, conn_id); if(!c) return false; if(c->count_modes<=0){ drmModeFreeConnector(c); return false; } // 1) Preferred for(int i=0;i<c->count_modes;i++){ auto&m = c->modes[i]; if(m.type & DRM_MODE_TYPE_PREFERRED){ out = m; drmModeFreeConnector(c); return true; } } // 2) 分辨率匹配 && 刷新率最接近 int best = -1, best_delta = INT_MAX; for(int i=0;i<c->count_modes;i++){ auto&m = c->modes[i]; if(m.hdisplay==want_w && m.vdisplay==want_h){ int d = std::abs((int)m.vrefresh - (int)want_hz); // 显式转 int,避免重载歧义 if(d < best_delta){ best = i; best_delta = d; } } } if(best>=0){ out = c->modes[best]; drmModeFreeConnector(c); return true; } // 3) 退而求其次:第一个可用 out = c->modes[0]; drmModeFreeConnector(c); return true; } int main(int ac, char** av){ signal(SIGINT,on_sig); signal(SIGTERM,on_sig); Args args = parse(ac,av); int fd = open(args.card.c_str(), O_RDWR | O_CLOEXEC); if(fd<0){ perror("open card"); return 1; } if(args.list){ list_res(fd); close(fd); return 0; } uint32_t conn_id=0, enc_id=0, crtc_id=0; if(!pick_conn_enc_crtc(fd, args.conn_id, conn_id, enc_id, crtc_id)){ fprintf(stderr,"找不到可用的 connector/CRTC(请确认显示子系统/VOP2 已上线)\n"); close(fd); return 1; } drmModeModeInfo mode{}; if(!choose_mode(fd, conn_id, args.w, args.h, args.fps, mode)){ fprintf(stderr,"未获取到可用显示模式\n"); close(fd); return 1; } fprintf(stderr, "Use mode: %s %dx%d@%d\n", mode.name, mode.hdisplay, mode.vdisplay, mode.vrefresh); // 创建双缓冲 FB DumbFB fb[2]; if(create_fb(fd, mode.hdisplay, mode.vdisplay, fb[0]) || create_fb(fd, mode.hdisplay, mode.vdisplay, fb[1])){ fprintf(stderr,"create dumb fb failed\n"); destroy_fb(fd, fb[0]); destroy_fb(fd, fb[1]); close(fd); return 1; } // 绑定首帧到 CRTC(legacy 模式设置) if(drmModeSetCrtc(fd, crtc_id, fb[0].fb_id, 0,0, &conn_id, 1, &mode)!=0){ perror("drmModeSetCrtc"); destroy_fb(fd, fb[0]); destroy_fb(fd, fb[1]); close(fd); return 1; } // XDMA init if(pcie_init()<0){ fprintf(stderr,"pcie_init failed\n"); destroy_fb(fd, fb[0]); destroy_fb(fd, fb[1]); close(fd); return 1; } const bool src_is_rgb = (args.src_fmt=="RGB888"); const int src_bpp = src_is_rgb? 3 : 2; const int src_stride = (args.src_stride>0)? args.src_stride : (args.w*src_bpp); const size_t pull_bytes = (size_t)src_stride * args.h; std::vector<uint8_t> srcbuf(pull_bytes); const int interval_us = 1000000 / (args.fps?args.fps:50); // page-flip 事件 drmEventContext ev{}; ev.version = DRM_EVENT_CONTEXT_VERSION; auto handler = [](int, unsigned, unsigned, unsigned, void* u){ *(int*)u = 1; }; ev.page_flip_handler = handler; int front = 0; fprintf(stderr, "Start: %dx%d %s -> FB fmt=0x%08x fps=%d addr=0x%llx stride=%d\n", args.w, args.h, src_is_rgb?"RGB888":"UYVY", FB_FMT, args.fps, (unsigned long long)args.xdma_addr, src_stride); while(running){ // 从 FPGA 拉一帧 c2h_transfer((unsigned)args.xdma_addr, (unsigned)pull_bytes, srcbuf.data()); // 写到后备 FB uint8_t* dst = fb[front^1].map; #if FB_USE_UYVY if(src_is_rgb){ rgb888_to_uyvy(srcbuf.data(), dst, args.w, args.h, src_stride, fb[front^1].pitch); }else{ // UYVY -> UYVY:逐行拷贝,照顾 pitch 差异 int copy_bytes_per_line = args.w * 2; for(int y=0;y<args.h;y++){ std::memcpy(dst + y*fb[front^1].pitch, srcbuf.data()+ y*src_stride, copy_bytes_per_line); } } #else if(src_is_rgb){ // RGB888 -> XRGB8888(简单逐像素填充;更快可用 RGA/NEON) for(int y=0;y<args.h;y++){ const uint8_t* s = srcbuf.data() + y*src_stride; uint32_t* d = (uint32_t*)(dst + y*fb[front^1].pitch); for(int x=0;x<args.w;x++){ uint8_t r=s[0], g=s[1], b=s[2]; s+=3; d[x] = 0xFF000000u | (r<<16) | (g<<8) | (b<<0); } } }else{ // UYVY -> XRGB8888 uyvy_to_xrgb8888(srcbuf.data(), dst, args.w, args.h, src_stride, fb[front^1].pitch); } #endif // 提交翻转 int done=0; if(drmModePageFlip(fd, crtc_id, fb[front^1].fb_id, DRM_MODE_PAGE_FLIP_EVENT, &done)!=0){ // 某些驱动可能第一次不支持 flip,退化为直接 setCrtc perror("drmModePageFlip"); drmModeSetCrtc(fd, crtc_id, fb[front^1].fb_id, 0,0, &conn_id, 1, &mode); usleep(interval_us); front ^= 1; }else{ struct pollfd pfd{ .fd=fd, .events=POLLIN, .revents=0 }; int r = poll(&pfd, 1, interval_us/1000); if(r>0 && (pfd.revents & POLLIN)) drmHandleEvent(fd, &ev); front ^= 1; } } pcie_deinit(); destroy_fb(fd, fb[0]); destroy_fb(fd, fb[1]); close(fd); return 0; }
10-21
<think>我们正在处理一个关于运行程序时出现"找不到可用的 connector/CRTC"错误的问题。这个错误通常与显示子系统有关,特别是在使用DRM(Direct Rendering Manager)时。根据用户提供的命令,程序试图使用/dev/dri/card0作为显示卡,并指定了分辨率、帧率等参数。 错误信息表明程序无法找到可用的连接器(connector)或CRTC(CRT Controller)。这可能是由于显示资源已经被占用,或者显示子系统没有正确初始化。 从用户提供的命令来看,程序可能是用来配置显示输出的工具,可能涉及到帧缓冲设备。用户还提到了两个引用,但这两个引用分别是关于编译util-linux和编译固件时安装依赖的,与当前问题关联不大。不过,引用[2]中提到了编译固件前安装依赖,这提醒我们可能需要检查驱动是否安装正确。 可能的原因和解决方案: 1. **显示资源被占用**:另一个进程可能已经占用了显示设备。我们可以尝试关闭其他可能使用显示的程序(比如X服务器),或者检查当前占用DRM设备的进程。 2. **驱动问题**:确保正确的显卡驱动已经安装并且加载。特别是对于Rockchip VOP2(Video Output Processor),需要确认内核是否支持并且已经启用。 3. **内核配置**:需要确认内核配置中启用了VOP2和相关的DRM驱动。如果用户是自己编译内核,可能需要检查配置。 4. **权限问题**:运行程序的用户是否有权限访问/dev/dri/card0?通常需要加入video组。 5. **参数问题**:检查命令参数是否正确,特别是--card指定的设备是否存在,以及分辨率、格式等是否被硬件支持。 具体步骤: 1. 检查设备文件是否存在: ```bash ls -l /dev/dri/card0 ``` 如果不存在,可能是驱动没有加载。 2. 查看驱动加载情况: ```bash lsmod | grep rockchip ``` 或者根据具体芯片查看相关驱动。Rockchip设备通常使用`rockchipdrm`驱动。 3. 检查当前占用DRM设备的进程: ```bash sudo fuser -v /dev/dri/card0 ``` 如果有其他进程占用,尝试停止它们(例如,停止显示管理器:`sudo systemctl stop lightdm` 或 `sudo systemctl stop gdm`)。 4. 确认用户权限: 将当前用户加入video组(可能需要重新登录): ```bash sudo usermod -aG video $USER ``` 5. 检查内核启动日志,查看VOP2是否初始化成功: ```bash dmesg | grep vop dmesg | grep drm ``` 注意是否有错误信息。 6. 如果用户是在嵌入式设备(如Rockchip RK3588)上运行,确认设备树(Device Tree)中是否正确配置了显示输出(如HDMI、eDP等)。 7. 尝试使用标准的DRM测试工具(如modetest)来检查可用的connector和CRTC: ```bash sudo modetest -M rockchip ``` 如果没有modetest,需要安装libdrm-tools。 如果以上步骤不能解决问题,可能需要更深入地查看内核日志或程序源代码。 根据用户之前的命令,程序指定了源格式为RGB888,步长为5760。步长(stride)通常指的是每行像素占用的字节数。对于1920宽度的RGB888(每个像素3字节),理论步长应为1920*3=5760,所以这里参数正确。 最后,如果程序是用户自己编译的,确保编译时依赖的库(如libdrm)版本正确。 由于用户的问题涉及到具体的硬件和内核驱动,可能需要根据具体环境调整解决方案。 相关问题: 1. 如何检查Linux系统中的DRM设备? 2. 如何调试Rockchip VOP2显示问题? 3. 在嵌入式Linux中,如何配置显示输出? </think>针对运行程序时出现**"找不到可用的 connector/CRTC"**错误的分析和解决方案如下: --- ### 错误原因分析 1. **DRM/KMS 子系统未启用** - 错误直接提示"显示子系统/VOP2未上线",表明内核未启用或初始化 Rockchip VOP2(Video Output Processor)显示控制器。 - 常见于嵌入式设备(如 RK3588/RK3568 平台),需要内核启用 `CONFIG_DRM_ROCKCHIP` 和 `CONFIG_ROCKCHIP_VOP2`[^1]。 2. **显示资源被占用** - 另一个进程(如桌面环境、显示管理器)可能已占用 `/dev/dri/card0` 的显示资源,导致程序无法访问 CRTC。 3. **内核驱动缺失** - 缺少 Rockchip DRM 驱动或依赖的 iommu/时钟模块,需验证驱动加载: ```bash lsmod | grep -e rockchip -e dw_hdmi -e analogix_dp ``` 4. **参数配置错误** - `--src-stride=5760` 可能不匹配硬件要求(1920x1080@RGB888 理论 stride=5760,但需确认硬件支持)。 --- ### 解决方案 #### 1. 启用内核显示子系统 - **重新编译内核**,确保启用以下配置: ```kconfig CONFIG_DRM=y CONFIG_DRM_ROCKCHIP=y CONFIG_ROCKCHIP_VOP2=y CONFIG_DRM_DW_HDMI_ROCKCHIP=y # HDMI支持 CONFIG_DRM_ROCKCHIP_ANALOGIX_DP=y # DP支持 ``` - 编译后更新内核并重启[^2]。 #### 2. 释放显示资源 - 关闭可能占用 DRM 的进程: ```bash sudo systemctl stop lightdm/gdm # 禁用桌面环境 sudo pkill -9 Xorg # 终止X服务器 ``` #### 3. 检查硬件连接状态 - 使用 `modetest` 工具验证显示输出: ```bash sudo modetest -M rockchip -c # 检查Connector/CRTC状态 ``` 若无输出,说明驱动未加载或硬件未检测到显示器。 #### 4. 调整程序参数 - 尝试简化命令,排除参数问题: ```bash sudo ./app --card=/dev/dri/card0 --w=1920 --h=1080 ``` - 确认 `--src-stride` 符合公式: $$ \text{stride} = \text{width} \times \text{bytes\_per\_pixel} $$ 例如 RGB888 的 `bytes_per_pixel=3`,则 `1920×3=5760`。 #### 5. 更新固件和驱动 - 安装依赖库(参考用户引用[2]): ```bash sudo apt-get install libdrm2 libdrm-rockchip1 mesa-utils ``` - 升级固件(如适用): ```bash sudo apt-get update && sudo apt-get install rockchip-firmware ``` --- ### 调试步骤 1. **检查内核日志**: ```bash dmesg | grep -i -e drm -e vop -e hdmi ``` 关注是否有 `vop2 iommu` 或 `rockchip-drm display-subsystem` 初始化成功日志。 2. **验证设备权限**: ```bash ls -l /dev/dri/card0 # 确认用户有rw权限 groups # 检查用户是否在video组 ``` 3. **硬件复位**(嵌入式设备): - 断开显示器供电,重启设备后重新连接。 --- ### 根本解决方案 若问题持续,需: 1. 确认设备树(Device Tree)是否正确配置 VOP2 和显示接口(HDMI/DP)。 2. 联系硬件厂商获取适配的 BSP 内核和显示固件。 ---
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值