Linux学习之RV1126视频输入模块VI及实战读取摄像头数据

目录

一.理论前置知识

1.前置知识:V4L2框架

2.VI模块:视频输入模块

3.RV1126的VI模块结构体参数重要参数值

pcVideoNode:视屏流的访问接口或者句柄,一对一的关系。

u32BufCnt:VI捕获视频缓冲区计数,默认是3

VI_CHN_BUF_TYPE:VI捕捉视频的类型

4.RV1126的VI模块初始化API

RK_MPI_VI_SetChnAttr

RK_MPI_VI_EnableChn 是 Rockchip MPI 中用于启动视频输入通道的函数

返回值:

二.实战----读取摄像头数据

1.思路:

  2.VI模块初始化---配置   结构体 const VI_CHN_ATTR_S *   

3.启动VI模块

4.开始读取数据:

5.建立独立的线程去读取VI数据

6.线程读取VI数据

7.使用make对编译脚本文件 Makefile进行编译

三.使用SSH对RV1126进行编译

1.先将文件cp到tmp中,使用 chmod对程序权限修改

2.结果是并没有打印,但vi创建成功,主要原因---程序中并没有终端打印,

3.运行后 要尽快的退出程序,因为vi数据是特别大的

4.nv12文件创建成功,但是并没有写入数据问题排查

5.ffmpeg软件对nv12文件进行播放


一.理论前置知识

1.前置知识:V4L2框架

V4L2(video for linux2):是Linux操作系统上处理视频设备的核心框架。它提供了一套统一的API,允许应用程序与各种视频设备进行交互,无论是USB摄像头、内置摄像头还是复杂的视频采集卡。

  • 它通过一系列 ioctl 调用来控制设备。

  • 其核心流程是:打开 -> 设置格式 -> 申请缓冲 -> 启动流 -> 循环取数据 -> 停止

具体的可以看下这个博主:

https://blog.youkuaiyun.com/welljrj/article/details/105578727?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522de8a161e0aa3706e61108c31a9beed66%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=de8a161e0aa3706e61108c31a9beed66&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-1-105578727-null-null.nonecase&utm_term=v4l2%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6&spm=1018.2226.3001.4450https://blog.youkuaiyun.com/welljrj/article/details/105578727?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522de8a161e0aa3706e61108c31a9beed66%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=de8a161e0aa3706e61108c31a9beed66&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-1-105578727-null-null.nonecase&utm_term=v4l2%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6&spm=1018.2226.3001.4450

2.VI模块:视频输入模块

        作用:读取sensor(摄像头)的数据,本质是对V4L2驱动架构的接口做了封装,

  • 打开 /dev/video0 等设备节点。

  • 使用 ioctl 向V4L2驱动设置格式(如NV12)、申请缓冲区。

  • 启动视频流,并循环从V4L2驱动获取数据帧。

3.RV1126的VI模块结构体参数重要参数值

typedef struct rk_vi_chn_attr {
    // 【必填】视频节点路径,决定使用哪路视频流
    const char *pcVideoNode; // 例如:"rkispp_scale0"

    // 【必填】采集图像的分辨率
    RK_U32 u32Width;         // 宽度,例如:1920
    RK_U32 u32Height;        // 高度,例如:1080

    // 【必填】采集图像的像素格式
    IMAGE_TYPE_E enPixFmt;   // 例如:RK_FMT_YUV420SP (NV12)

    // 【重要】缓冲区数量
    RK_U32 u32BufCnt;        // 例如:3 (默认值)
    
    // 【重要】缓冲区类型
    VI_CHN_BUF_TYPE enBufType; // 例如:VI_CHN_BUF_TYPE_DMA (强烈推荐)

    // 【可选】通道工作模式
    VI_WORK_MODE enWorkMode; // 例如:VI_WORK_MODE_NORMAL
} VI_CHN_ATTR_S;
成员名称描述说明与典型值
pcVideoNodeV4L2视频设备节点路径这是一个字符串参数(如 "/dev/video0")。它告诉VI模块去操作哪个具体的摄像头设备。这是连接VI模块与底层V4L2驱动的桥梁。
u32Width视频图像的宽度,单位像素例如 1920。这必须与Sensor支持的分辨率及V4L2驱动配置相符。
u32Height视频图像的高度,单位像素例如 1080。与宽度共同决定采集图像的分辨率。
enPixFmt视频数据的像素格式这是枚举类型参数。它直接对应到我们之前讨论的YUV格式,例如:
- RK_FMT_YUV420SP (即NV12)
- RK_FMT_YUV420P (即I420/YU12)
- RK_FMT_YUV422SP
- RK_FMT_YUV422P
设置此项,VI模块底层就会发起 VIDIOC_S_FMT 的ioctl调用。
u32BufCntVI捕获视频的缓冲区数量这是一个极其重要的性能参数。它决定了V4L2驱动内部维护的缓冲区队列长度(通过 VIDIOC_REQBUFS 设置)。
典型值:3或4。太少可能导致丢帧(生产者-消费者模型失衡);太多则会增加内存开销和延迟。
VI_CHN_BUF_TYPEVI通道输出数据的存储和传递方式

是一个枚举类型

  1. VI_CHN_BUF_TYPE_MMAP (内存映射类型)

  2. VI_CHN_BUF_TYPE_DMA (DMA缓冲区类型)

enWorkModeVI通道的工作模式这也是枚举类型参数,决定了VI模块如何处理数据流。常见模式:
- VI_WORK_MODE_NORMAL:正常模式,一帧一帧处理。
- VI_WORK_MODE_LINE:行模式(通常用于特定的高性能或算法处理场景,不常用)。

pcVideoNode:视屏流的访问接口或者句柄,一对一的关系。

RV1126的ISPP(硬件图形后处理单元):接收到Sensor传来原始图形,然后在硬件上并行生成多个不同规格的输出流。使用则使用下面四个节点,可以获取不同分辨率的视屏流

节点名称最大宽度       缩放倍数支持的输出格式
rkispp_m_bypassSensor最大宽度不支持NV12 / NV16 / YUYY / FBC0 / FBC2
rkispp_scale0

3264

1-8倍NV12 / NV16 / YUYY
rkispp_scale112802-8倍NV12 / NV16 / YUYY
rkispp_scale212802-8倍NV12 / NV16 / YUYY

你不需要自己去计算缩放倍数。你只需要在代码中指定你想要的目标分辨率(u32Widthu32Height),ISPP硬件会自动完成缩放

举例说明:假设你的Sensor支持输出 3264 x 2448 的图像,你使用 rkispp_scale0 节点。

设置的目标分辨率ISPP硬件执行的缩放操作实际缩放倍数(约)
3264 x 2448不缩放,输出原图1倍
1920 x 1080将原图缩小以适应目标尺寸~1.7倍 (3264/1920 ≈ 1.7)
1280 x 720将原图缩小到1280x720~2.55倍 (3264/1280 ≈ 2.55)
640 x 480将原图缩小到640x480~5.1倍 (3264/640 ≈ 5.1)
416 x 416将原图缩小到416x416~7.85倍 (3264/416 ≈ 7.85)

u32BufCnt:VI捕获视频缓冲区计数,默认是3

VI读取速度更不上摄像头的采集速度,建立多个缓冲器,解决速度不匹配的问题

在硬件(生产者)和应用程序(消费者)之间建立一个“数据缓冲区队列”,以平滑两者之间处理速度的波动,防止丢帧

VI_CHN_BUF_TYPEVI捕捉视频的类型

  • DMA 模式:核心是 “硬件直接内存访问”,专为系统内硬件单元间高效数据传输设计。

  • MMAP 模式:核心是 “用户空间直接映射内核缓冲区”,旨在减少用户态与内核态之间的数据拷贝

  • 特性DMA 模式 (VI_CHN_BUF_TYPE_DMA)MMAP 模式 (VI_CHN_BUF_TYPE_MMAP)
    设计目的异构计算/零拷贝流水线。让视频数据能被其他硬件加速器(VPU/NPU/RGA)直接处理。CPU高效处理。让应用程序(CPU)能以零拷贝的方式访问视频数据。
    性能极高。数据从ISP出来后,存放在物理连续内存中,VPU、NPU等硬件可以直接存取,无需任何格式转换或拷贝,实现了真正的零拷贝流水线。。数据从ISP到应用程序,避免了从内核到用户空间的拷贝开销,但对硬件加速器不友好。
    硬件资源消耗对硬件更友好,整体系统效率更高。它消耗的是特定的DMA内存资源,但正因为使用了这种内存,才解放了CPU,并让硬件加速器得以高效工作,从整个SoC的角度看,资源利用率更高对硬件加速器不友好。如果后续需要VPU编码或NPU推理,驱动需要将数据重新拷贝或映射到一块物理连续的内存中,这会消耗额外的CPU和总线带宽,反而造成了资源浪费
    稳定性/数据丢失更稳定。驱动为DMA缓冲区管理提供了更成熟和可靠的机制,是嵌入式媒体处理的工业标准因为其缓冲区管理更依赖于V4L2驱动和用户空间程序的协调,在系统负载高或处理不及时时,确实有更高的丢帧风险

    4.RV1126的VI模块初始化API

  • RK_MPI_VI_SetChnAttr

  • RK_S32 RK_MPI_VI_SetChnAttr(VI_PIPE ViPipe, VI_CHN ViChn, const VI_CHN_ATTR_S *pstChnAttr);

    功能:配置视频输入通道属性的函数。它在启动视频流之前调用,决定了VI通道如何工作。

  • 参数:

    • 参数类型描述
      ViPipeVI_PIPEVI管道号。代表一个物理的摄像头输入源。例如,如果系统连接了2个摄像头,可能用 01 来区分。
      ViChnVI_CHNVI通道号。一个物理摄像头(Pipe)可以创建多个通道(Chn),每个通道可以输出不同分辨率/格式的视频流。这正是RV1126“一摄多流”架构的体现。通常从 0 开始。
      pstChnAttrconst VI_CHN_ATTR_S *指向通道属性结构体的指针。这是最关键的参数,它包含了我们之前讨论的所有具体设置,如分辨率、格式、缓冲区数量等。

返回值

  • RK_SUCCESS (0):表示函数执行成功,通道属性已按需设置。

  • 非0值:表示函数执行失败。失败原因可以是:

    • 参数错误(如通道号超出范围)。

    • 底层V4L2驱动不支持所设置的分辨率或格式。

    • 系统资源不足(如无法申请到指定数量的DMA缓冲区)。

注意事项:ViPipe VI管道好和ViChn VI通道号的区别

概念比喻在RV1126中的对应
ViPipe (管道)总进水管一个物理摄像头传感器(Sensor),有多少个Sensor就有多少个管道
ViChn (通道)水龙头一个视频流输出(对应一个 pcVideoNode,如 rkispp_scale0),一个管道里面头多个通道,一根总进水管(ViPipe)上,安装多个不同用途的水龙头(ViChn)。

关键结论:一个 ViPipe(摄像头)下面,可以创建多个 ViChn(视频流)。

前提是ViChn没有被占用,如果并占用此通道无法使用

RK_MPI_VI_EnableChn 是 Rockchip MPI 中用于启动视频输入通道的函数

RK_S32 RK_MPI_VI_EnableChn(VI_PIPE ViPipe, VI_CHN ViChn);
参数类型描述
ViPipeVI_PIPEVI管道号。指定要启动的通道属于哪个物理摄像头。
ViChnVI_CHNVI通道号。指定要启动的具体通道。

返回值

  • RK_SUCCESS (0):通道启动成功。

  • 非0值:通道启动失败。失败原因可能包括:

    • 指定的管道或通道不存在。

    • 通道属性未设置或设置无效。

    • 底层资源分配失败(如DMA内存不足、V4L2设备打开失败等)。

    • 该通道已经被启用。

5.从指定通道中获取数据。

MEDIA_BUFFER RK_MPI_SYS_GetMediaBuffer(MOD_ID_E enModID, RK_S32 s32ChnID, RK_S32 s32MilliSec);

参数名称描述输入/输出
enModID模块号。输入
s32ChnID通道号。输入
s32MilliSec-1:阻塞,>=0:阻塞等待时间。输入

返回值:

  • 成功时返回有效的 MEDIA_BUFFER 指针;

  • 失败或超时时返回 NULL

6.获取缓冲区指针

void *RK_MPI_MB_GetPtr(MEDIA_BUFFER mb);

7.获取缓冲区数据长度

int RK_MPI_MB_GetSize(MEDIA_BUFFER mb);

void *ptr  = RK_MPI_MB_GetPtr(mb);   // 拿到地址
int   size = RK_MPI_MB_GetSize(mb);  // 拿到长度
fwrite(ptr, 1, size, file);          // 把一帧完整写盘

二.实战----读取摄像头数据

1.思路:

       VI模块初始化------启动VI模块工作------开启多线程采集VI数据并保存

  2.VI模块初始化---配置   结构体 const VI_CHN_ATTR_S *   

    VI_CHN_ATTR_S vi_chn_attr;
    vi_chn_attr.pcVideoNode = CAMERA_PATH;//配置视频节点路径
    vi_chn_attr.u32Width = 1920;
    vi_chn_attr.u32Height = 1080;
    vi_chn_attr.enPixFmt = IMAGE_TYPE_NV12;//配置像素格式 NV12  
    vi_chn_attr.u32BufCnt = 3;//数据接受缓存buff 
    vi_chn_attr.enBufType = VI_CHN_BUF_TYPE_MMAP;//VI输出数据的存储和传递方式---MMAP 内存映射型
    vi_chn_attr.enWorkMode = VI_WORK_MODE_NORMAL;//工作模式 ---正常模式,一帧一帧处理

ret = RK_MPI_VI_SetChnAttr(PIPE_ID, CHN_ID, &vi_chn_attr);

配置像素格式 NV12:YUV420格式 平面打包格式---先存放所有Y,然后UV交替

https://blog.youkuaiyun.com/ALIANG2000/article/details/154016872?spm=1011.2124.3001.6209https://blog.youkuaiyun.com/ALIANG2000/article/details/154016872?spm=1011.2124.3001.6209

3.启动VI模块

ret = RK_MPI_VI_EnableChn(PIPE_ID, CHN_ID);

4.开始读取数据:

ret = RK_MPI_VI_StartStream(PIPE_ID, CHN_ID);

5.建立独立的线程去读取VI数据

pthread_t pid;
pthread_create(&pid, NULL, get_camera_vi_thread, NULL);
pthread_create(&pid, NULL, get_camera_vi_thread, NULL);创建并启动线程:
① 线程 ID 存到 pid
② 线程属性默认(NULL);
③ 线程入口函数是 get_camera_vi_thread
④ 无额外参数(NULL)。

思路:

  1. 建立一个信号处理函数 SIGINT,检测到SIGINT信号,跳到相对应的函数执行终止程序
  2. 配置vi_chn_attr结构体
  3. 写入vi_chn_attr结构体:ret = RK_MPI_VI_SetChnAttr(PIPE_ID, CHN_ID, &vi_chn_attr);
  4. 使能VI模块: ret = RK_MPI_VI_EnableChn(PIPE_ID, CHN_ID);//使用VI通道
  5. 开始读取VI数据
  6. 开启多线程读取数据 pthread_create(&pid, NULL, get_camera_vi_thread, NULL);
  7. while循环--检测到终止程序,跳出循环,直接步骤8,如果没有终止循环,会在线程中一直读取数据
  8. 关闭通道号

6.线程读取VI数据

void * get_camera_vi_thread(void * args)
{
    MEDIA_BUFFER mb = NULL;
    FILE * nv12_file = fopen("test_camera.nv12", "w+");

    while (!quit)
    {
        mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI, CHN_ID, -1);
        if(!mb)
        {
            printf("get Vi mb break....\n");
            break;
        }

        fwrite(RK_MPI_MB_GetPtr(mb), 1, RK_MPI_MB_GetSize(mb), nv12_file);
        RK_MPI_MB_ReleaseBuffer(mb);
    }
}

MEDIA_BUFFER:

  1. 在 RKMedia / Rockchip 的 IPC 框架里,
    typedef void* MEDIA_BUFFER;
    它只是一个指向内核/驱动层缓冲块的句柄,并非裸指针,更不能直接 memcpy

  2. 该缓冲块里通常包含:

    • 图像或音频裸数据地址(MB_GET_PTR(mb)

    • 长度(MB_GET_SIZE(mb)

    • 时间戳(MB_GET_TIMESTAMP(mb)

    • 内存类型、缓存属性、偏移、文件描述符等元数据(驱动私有)

7.使用make对编译脚本文件 Makefile进行编译

Makefile 是一个 “编译脚本” 文本文件,里面写好了:

  • 哪些源文件要先编译

  • 编译成什么目标文件 / 可执行文件

  • 用什么命令、什么参数去编译

  • 文件改了哪些才需要重新编译(增量编译)

make 这个工具会 自动读取 Makefile,根据规则和时间戳决定 要不要重新编译、链接,最终帮你 一键生成可执行文件或库

Makefile = 把“gcc 源文件 -o 可执行文件”这类手敲命令,写成规则,让 make 帮你批量、增量、自动化地完成编

hide := @                          # 把“@”符号存起来,以后放在命令前面就能**不打印这条命令本身**,make 输出更干净
ECHO := echo                        # 把 echo 命令也存成变量,后面想打印提示时统一用 $(ECHO)

# 告诉 make:以后用 $(G++) 就等价于调用这个交叉编译器(生成 ARM 可执行文件)
G++ := /opt/rv1126_rv1109_linux_sdk_v1.8.0_20210224/prebuilts/gcc/linux-x86/arm/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++

# 下面一堆 -I 就是给编译器“小旗子”:后面跟的是头文件目录,让 #include<> 能找到 .h 文件
CFLAGS := -I./include/rkmedia \
          -I./include/rkaiq/common \
          -I./include/rkaiq/xcore \
          -I./include/rkaiq/uAPI \
          -I./include/rkaiq/algos \
          -I./include/rkaiq/iq_parser \
          -I./rknn_rockx_include \
          -I./im2d_api          \
          -I./arm_libx264/include \
          -I./arm32_ffmpeg_srt/include

# 告诉链接器:找库文件(.so/.a)时,先到 ./rv1126_lib 目录去翻
LIB_FILES := -L./rv1126_lib

# 真正要链接的第三方库名单:pthread、Rockchip MPP、FFmpeg 全家桶、ALSA、V4L2、RGA、RKAIQ 算法库等
LD_FLAGS := -lpthread -leasymedia -ldrm -lrockchip_mpp \
            -lavformat -lavcodec -lswresample -lavutil \
            -lasound -lv4l2 -lv4lconvert -lrga \
            -lRKAP_ANR -lRKAP_Common -lRKAP_3A \
            -lmd_share -lrkaiq -lod_share

# 在 CFLAGS 里再追加一个宏定义:源码里 #ifdef RKAIQ 会因此生效
CFLAGS += -DRKAIQ

# 默认目标:敲 make 或 make all 时执行
all:
	# 真正干活的一行:用交叉编译器把 rv1126_vi_stream.cpp 编译+链接成可执行文件 rv1126_vi_stream
	$(G++) rv1126_vi_stream.cpp $(CFLAGS) $(LIB_FILES) $(LD_FLAGS) -o rv1126_vi_stream
	# 下面一行被注释掉:如果想编译备份文件,把行首 # 去掉即可
	#$(G++) rv1126_vi_stream_bak.cpp $(CFLAGS) $(LIB_FILES) $(LD_FLAGS) -o rv1126_vi_stream_bak
	# 编译结束后偷偷打印一行提示(前面加 @ 所以命令本身不会显示)
	$(hide)$(ECHO) "Build Done ..."

在当前工作文件下:输入 make指令会对Makefile自动编译生成文件

三.使用SSH对RV1126进行编译

1.先将文件cp到tmp中,使用 chmod对程序权限修改

2.结果是并没有打印,但vi创建成功,主要原因---程序中并没有终端打印,

void * get_camera_vi_thread(void * args)
{
    /*
        在 RKMedia / Rockchip 的 IPC 框架里,
    typedef void* MEDIA_BUFFER;
    它只是一个指向内核/驱动层缓冲块的句柄,并非裸指针,更不能直接 memcpy。
    该缓冲块里通常包含:
        图像或音频裸数据地址(MB_GET_PTR(mb))
        长度(MB_GET_SIZE(mb))
        时间戳(MB_GET_TIMESTAMP(mb))
        内存类型、缓存属性、偏移、文件描述符等元数据(驱动私有)
    */
    MEDIA_BUFFER mb = NULL;
    //打开接收文件
    FILE * nv12_file = fopen("test_camera.nv12", "w+");

    while (!quit)
    {
        mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VI, CHN_ID, -1);
        if(!mb)
        {
            printf("get Vi mb break....\n");
            break;
        }
        //将接受到数据写入刚才打开的文件中
        fwrite(RK_MPI_MB_GetPtr(mb), 1, RK_MPI_MB_GetSize(mb), nv12_file);
        //KMedia 框架里释放 MEDIA_BUFFER 的唯一官方接口,一句话:
        RK_MPI_MB_ReleaseBuffer(mb);
    }
}

直接看test_camera.nv12的文件内容

3.运行后 要尽快的退出程序,因为vi数据是特别大的

使用ls -lna *.nv12

  • ls - 列出目录内容

  • -l - 长格式(详细信息)

  • -n - 显示数字化的 UID/GID(而不是用户名)

  • -a - 显示所有文件(包括隐藏文件)

  • *.nv12 - 过滤显示以 .nv12 结尾的文件

  • 你会看到每个 .nv12 文件的详细信息:
  • 文件权限

  • 链接数

  • 所有者 UID(数字)

  • 所属组 GID(数字)

  • 文件大小

  • 修改时间戳

  • 文件名

可以发现这个文件夹是空的,说明打开或者生成一个名为test_camera.nv12文件,但是并没有完里面写入数据

4.nv12文件创建成功,但是并没有写入数据问题排查

使用 lsmod去检查RV1126已加载的模块,发现没有摄像头模块

[root@RV1126_RV1109:/tmp]# lsmod
Module                  Size  Used by    Tainted: G
qmi_wwan_q             24576  0
8188eu               1441792  0
rtc                    16384  0
galcore               319488  0

由于固件整体编译时间比较久,前面并没有编译,使用的是压缩包里现成的文件进行烧录,暂时不知道是不是因为这个原因,决定重新编译固件一下。

5.ffmpeg软件对nv12文件进行播放

下面是对ffmpeg的介绍

资源:

https://blog.youkuaiyun.com/qq_37266079/article/details/90723366?ops_request_misc=&request_id=&biz_id=102&utm_term=ffmpeg&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~sobaiduweb~default-0-90723366.nonecase&spm=1018.2226.3001.4450https://blog.youkuaiyun.com/qq_37266079/article/details/90723366?ops_request_misc=&request_id=&biz_id=102&utm_term=ffmpeg&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~sobaiduweb~default-0-90723366.nonecase&spm=1018.2226.3001.4450将生成的test_camera.nv12放在ffmpeg的bin文件下

在Windows界面下使用cmd进行启动

切换到bin目录后:输入 ffplay -f rawvideo -pix_fmt nv12 -video_size 1920x1080 test_camera.nv12

是用 ffplay(FFmpeg 的极简播放器)直接播放裸 YUV 视频数据文件 test_camera.nv12,而 不需要任何封装格式(如 .mp4/.avi)

参数含义
ffplayFFmpeg 自带的命令行播放器,跨平台,零配置。
-f rawvideo告诉解码器:文件是 裸像素数据,没有头部、没有索引、没有封装。
-pix_fmt nv12指定像素格式为 NV12(YUV 4:2:0,bi-planar,常见于摄像头、编解码)。
-video_size 1920x1080告诉解码器 一帧的宽高,否则它不知道多少字节算一帧。
test_camera.nv12待播放的裸数据文件名,必须 > 0 字节,且总字节数 = 帧数 × 1920×1080×1.5(NV12 每像素 1.5 B)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值