打造自定义CAN数据捕获工具——can-utils实践指南

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:CAN协议广泛应用于汽车电子设备的嵌入式系统中,"can-utils"提供了两个关键命令行工具:"candump"用于监控CAN总线数据包,"cansend"用于发送自定义数据帧。通过学习和实践这些工具的源码,开发者可以创建自己的CAN数据包监控程序,这需要对CAN协议、操作系统I/O操作和编程技能有深入理解。

1. CAN协议基础知识

CAN协议(Controller Area Network)是一种被广泛应用于工业自动化、汽车电子以及医疗设备中的高速通信协议。其设计初衷是为了解决日益复杂的汽车电子网络中日益增加的线束数量,它支持分布式实时控制,并具有强大的错误处理能力。

1.1 CAN协议概述

1.1.1 CAN协议的起源与发展

CAN协议最初由德国Bosch公司在1980年代早期为汽车应用而设计。它的第一版标准是ISO 11898-1:2003,随后不断发展演进,以适应不同领域的需求。例如,在工业应用中,ISO 11898-2和ISO 11898-3标准分别定义了高速和低速的CAN网络。

1.1.2 CAN协议在工业领域的应用

CAN协议在工业自动化领域应用广泛,特别是在实时控制系统中。它能够在恶劣的电气环境下可靠地传输数据,保证了工业通信的稳定性。CAN总线网络因其高性能和强大的错误检测机制,已成为工厂自动化和过程自动化中首选的通信技术之一。

下一章节将深入探讨CAN协议的物理层和数据链路层的工作原理及其帧结构和通信机制。

2.1 Linux内核对CAN设备的支持

Linux操作系统以其开源和高度可定制的特点,在工业自动化和嵌入式系统中得到了广泛的应用。在这些领域中,CAN(Controller Area Network)总线协议作为一种强健的实时通信标准,在设备互联中扮演着关键角色。Linux内核对CAN设备的支持,使得开发者可以在用户空间轻松地进行CAN通信。

2.1.1 Linux CAN驱动架构

Linux内核中的CAN驱动架构主要由以下几部分组成:CAN核心、CAN总线驱动、CAN网络协议和CAN设备驱动。

  • CAN核心 :作为驱动的最顶层,负责与CAN网络协议栈的交互,提供了一组标准的API供上层使用。同时,CAN核心对下层的CAN总线驱动抽象出了一致的接口。
  • CAN总线驱动 :这一层主要负责与具体的硬件进行交互,处理CAN控制器的初始化、数据帧的发送和接收等。硬件制造商通常会提供相应的CAN总线驱动。
  • CAN网络协议 :对用户空间提供SocketCAN接口,该接口允许用户空间的应用程序像操作普通网络套接字一样操作CAN接口。此外,该层还负责处理CAN协议层面的事务,如帧过滤和错误处理。
  • CAN设备驱动 :为特定的CAN设备提供驱动代码,这些驱动负责将CAN核心的API转换为具体硬件能理解的命令,如初始化设备、注册网络接口等。

2.1.2 CAN设备驱动的加载和配置

在Linux系统中,通过使用modprobe命令可以动态地加载CAN设备驱动,而无需重新编译内核。例如,加载一个名为 canYSIS 的驱动模块可以使用以下命令:

modprobe canYSIS

加载CAN设备驱动后,接下来需要配置CAN接口。在早期的Linux版本中,通常需要手动配置接口,包括设置波特率、位定时和过滤器等。但随着 iproute2 工具的更新,现在的配置变得非常简单。使用 ip 命令配置CAN接口的基本格式如下:

ip link set can0 type can bitrate 500000 dbitrate 500000 sample-point 0.875
ip link set can0 up

这里, bitrate 参数用于设置CAN总线的位速率, dbitrate 用于设置CAN总线的双采样速率, sample-point 参数用于设置采样点的位置,这些参数的配置依赖于具体的CAN网络环境。

2.1.3 CAN网络接口的激活和监控

一旦CAN接口配置完毕,就可以使用 ip 命令或 ifconfig 命令将其激活:

ifconfig can0 up

要查看CAN接口的状态,可以使用 ip 命令:

ip link show type can

以上命令将列出系统中所有已配置的CAN接口,并显示它们的状态信息,如是否激活、速率设置等。如果接口未激活或配置参数不符合预期,需要重新检查配置命令或加载的模块是否有误。

通过这些步骤,开发者可以利用Linux内核对CAN设备的支持,将CAN设备与应用程序进行有效连接,并进行后续的数据传输和处理。这种机制提高了开发的灵活性和系统的可扩展性,使得在Linux环境下进行CAN通信变得高效和方便。

3. CAN消息结构解析

3.1 CAN帧的结构和组成

CAN协议定义了两种不同类型的帧:数据帧和远程帧,用于在CAN网络上发送信息。在深入了解如何在Linux环境下发送和接收CAN消息之前,有必要先对CAN帧的结构进行详细解析。

标识符、控制段和数据段的解析

每一个CAN帧都由以下部分组成:

  • 标识符(Identifier) :用于表示消息的优先级和内容。在标准CAN帧中,标识符为11位,而在扩展CAN帧中为29位。
  • 控制段(Control Field) :包含标识符扩展位、IDE位、保留位以及4位的DLC(数据长度代码)字段,后者表示数据字段中有效字节的数量。
  • 数据段(Data Field) :包含0到8个字节的有效数据。
flowchart LR
    id[标识符] --> control[控制段]
    control --> data[数据段]
  • 帧类型和帧格式的区别

CAN协议支持两种帧格式:

  • 标准帧(Standard Frame) :具有11位标识符,用于大多数标准应用。
  • 扩展帧(Extended Frame) :具有29位标识符,适用于更复杂的应用,允许更多的消息ID。

一个帧的首字节包含标识符的最高位(标准帧)或次高位(扩展帧),以及控制段的前五个字节。数据段紧随其后,如果DLC小于或等于8字节,则数据段不会超过8个字节。

3.2 CAN消息的发送与接收流程

发送消息的步骤和注意事项

在Linux环境下,通过SocketCAN接口发送消息可以遵循以下步骤:

  1. 打开CAN设备 :使用socket()函数创建一个新的CAN原始套接字。
  2. 配置CAN接口 :通过setsockopt()函数配置CAN接口的属性,如比特率、过滤器等。
  3. 创建CAN帧结构 :定义一个帧结构体,并填充标识符、DLC和数据字段。
  4. 发送CAN帧 :使用write()函数将CAN帧写入到CAN原始套接字。
  5. 关闭套接字 :传输完成或者不再需要时,关闭套接字。
struct can_frame {
    canid_t can_id;  /* 32 bit CAN_ID + EFF/RTR/ERR flags */
    __u8 can_dlc;    /* data length code: 0 ... 8 */
    __u8 data[8] __attribute__((aligned(8)));
};

// 示例代码
int s;
struct sockaddr_can addr;
struct can_frame frame;

s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
addr.can_family = AF_CAN;
addr.can_ifindex = if_nametoindex("can0");

bind(s, (struct sockaddr *)&addr, sizeof(addr));

frame.can_id = 0x123; // 标识符
frame.can_dlc = 2;    // 数据长度为2
frame.data[0] = 0x11; // 数据字段
frame.data[1] = 0x22; // 数据字段

write(s, &frame, sizeof(struct can_frame));

close(s);

注意事项:

  • 网络延迟 :传输之前应该考虑网络延迟,特别是在实时系统中。
  • 错误处理 :务必实施错误检测和处理机制。
接收消息的处理机制

接收CAN消息是一个异步操作,通常会有一个循环来持续监听CAN原始套接字。

  1. 打开CAN设备 :和发送消息一样,首先打开CAN设备。
  2. 绑定套接字 :将套接字绑定到CAN接口上。
  3. 配置监听 :使用setsockopt()函数配置套接字以接收过滤后的CAN消息。
  4. 接收CAN帧 :使用recv()函数接收CAN消息。
  5. 处理数据 :根据CAN帧中的数据执行适当的操作。
  6. 关闭套接字 :当不再需要监听消息时,关闭套接字。
int s;
struct sockaddr_can addr;
struct can_frame frame;
struct ifreq ifr;
struct iovec iov;
struct msghdr msg;

s = socket(PF_CAN, SOCK_RAW, CAN_RAW);

strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);

addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

bind(s, (struct sockaddr *)&addr, sizeof(addr));

iov.iov_base = &frame; // 指向数据缓冲区的指针
msg.msg_name = (void *)&addr; // 指定套接字地址结构体的指针
msg.msg_iov = &iov; // 指向 iovec 结构体的指针
msg.msg_iovlen = 1;

while (1) {
    int numbytes = recvmsg(s, &msg, 0); // 接收CAN消息
    if (numbytes < 0) perror("recvmsg");
    else if (numbytes < sizeof(struct can_frame))
        fprintf(stderr, "Incomplete CAN frame\n");
    else
        printf("Received: 0x%X\n", frame.can_id);
}

close(s);

3.3 CAN协议的扩展和配置

标准CAN与扩展CAN的对比

标准CAN帧支持11位标识符,而扩展CAN帧支持29位标识符。扩展CAN允许更多的设备在同一网络中操作,因为提供更多的标识符选择。然而,增加标识符的长度同时也增加了帧的开销和网络的复杂性。

| 标准CAN | 扩展CAN | |:-------:|:-------:| | 11位标识符 | 29位标识符 | | 4字节帧大小 | 8字节帧大小(如果使用29位ID) | | 简单 | 更复杂 | | 低开销 | 高开销 |

位定时配置和消息过滤设置

位定时参数对于确保CAN网络上的设备同步至关重要,它包括同步段、传播时间段、相位缓冲段1和相位缓冲段2等。配置这些参数是通过CAN控制器的寄存器或在SocketCAN接口中使用setsockopt()函数来实现的。

struct can_bittiming bt;
memset(&bt, 0, sizeof(bt));

/* 假设的位定时参数 */
bt.bitrate = 500000; // 500 kbit/s
bt.sample_point = 875; // 87.5%的样本点位置

setsockopt(s, SOL_CAN_RAW, CAN_RAW_BITTIMING, &bt, sizeof(bt));

消息过滤设置允许用户确定哪些CAN消息应该被接收。在SocketCAN中,可以设置硬件过滤器或者软件过滤器。

  • 硬件过滤器 :由CAN控制器硬件直接处理,效率高,但灵活性较差。
  • 软件过滤器 :在用户空间通过套接字选项来设置,灵活性更高,但占用CPU资源。

过滤规则的设置可以通过setsockopt()函数来实现:

struct can_filter rfilter;

rfilter.can_id = 0x123; // 接收ID为0x123的消息
rfilter.can_mask = 0x7FF; // 接收该ID的任何消息

setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

过滤器的精确配置依赖于网络的特定需求和应用的上下文,例如,可以设置过滤器以只接收来自特定设备的消息,或者忽略某些优先级较低的控制消息。

4. 多线程编程在数据接收和显示中的应用

4.1 多线程编程基础

4.1.1 线程的概念和作用

在现代操作系统中,线程是程序执行的最小单位,而进程则是系统进行资源分配和调度的一个独立单位。每个进程可以包含一个或多个线程,线程之间共享进程资源,但执行流相互独立,允许并发执行多个任务。

线程的作用体现在其对系统资源的要求相对较低,并且线程切换的开销要比进程切换小得多。在需要同时处理多个任务时,例如在CAN数据处理中,多线程可以显著提高系统的响应速度和处理效率。通过线程的并发执行,可以实现数据的实时采集、处理和显示。

4.1.2 线程创建和同步机制

在多线程编程中,线程的创建通常涉及定义一个线程函数,该函数作为线程的入口点。在C语言中,可以使用POSIX线程库(pthread)创建线程,示例如下:

#include <pthread.h>
#include <stdio.h>

void* thread_function(void* arg) {
    // 线程函数的具体实现
    return NULL;
}

int main() {
    pthread_t thread;
    int result = pthread_create(&thread, NULL, thread_function, NULL);
    if (result != 0) {
        // 处理错误情况
        return 1;
    }
    // 其他主线程的处理逻辑
    pthread_join(thread, NULL);
    return 0;
}

在上述代码中, pthread_create 用于创建新线程, pthread_join 用于等待线程完成执行。同步机制如互斥锁(mutexes)、条件变量(condition variables)和信号量(semaphores)被用于解决线程间的同步问题,保证线程间共享资源的访问不会出现冲突。

4.2 Linux下的多线程实现

4.2.1 POSIX线程库的使用

在Linux系统中,多线程应用主要依赖于POSIX线程库(pthread)。pthread 提供了一套完整的API来处理线程的创建、执行、同步等。

以下是一个使用pthread实现的简单多线程程序的示例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NUM_THREADS 5

void* perform_work(void* argument) {
    int passed_in_value;

    passed_in_value = *((int*) argument);
    printf("Hello from thread %d\n", passed_in_value);
    return NULL;
}

int main(int argc, char* argv[]) {
    pthread_t threads[NUM_THREADS];
    int thread_args[NUM_THREADS];

    for (int i = 0; i < NUM_THREADS; ++i) {
        printf("In main: creating thread %d\n", i);
        thread_args[i] = i;
        if (pthread_create(&threads[i], NULL, perform_work, (void*)&thread_args[i])) {
            printf("ERROR; return code from pthread_create() is %d\n", i);
            exit(-1);
        }
    }

    printf("All threads created\n");
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

4.2.2 线程池的管理与优化

线程池是一种通过预先创建一定数量的工作线程,来管理和优化线程使用的技术。线程池可以避免频繁创建和销毁线程带来的开销,提高系统的稳定性和性能。

线程池通常需要管理任务队列,当有新任务到来时,线程池中的线程可以从中取出任务并执行。这样可以保证系统资源的合理使用,并通过线程数量的控制,避免过多的线程消耗大量系统资源。

graph LR
A[任务提交] --> B{任务队列}
B -->|未满| C[加入队列]
B -->|已满| D[等待或拒绝]
C --> E[线程池工作线程]
D -->|继续等待| B
E --> F[执行任务]
F --> G[返回结果]
G --> H{线程池回收}
H -->|可重用| E
H -->|不可重用| I[销毁线程]
I --> J[回收资源]

4.3 多线程在CAN数据处理中的应用

4.3.1 数据接收线程的设计与实现

在CAN数据处理中,多线程可以用于分别处理数据接收和数据显示。数据接收线程可以不断轮询CAN接口以接收最新数据,并将其放入队列中供其他线程处理。

void* receive_can_data(void* arg) {
    // 初始化CAN设备和接口参数
    while (1) {
        CAN_frame frame;
        int ret = read(can_fd, &frame, sizeof(frame));
        if (ret == sizeof(frame)) {
            pthread_mutex_lock(&queue_mutex);
            // 将接收到的CAN帧加入到消息队列
            enqueue(frame);
            pthread_mutex_unlock(&queue_mutex);
        }
    }
}

4.3.2 数据显示和处理的线程同步

数据的显示和进一步处理可以通过另一个线程来完成,该线程从同一个共享队列中取出数据进行显示。由于涉及共享数据,需要使用互斥锁来保证数据访问的安全性。

void* display_can_data(void* arg) {
    while (1) {
        pthread_mutex_lock(&queue_mutex);
        // 从消息队列中取出CAN帧数据
        CAN_frame frame = dequeue();
        pthread_mutex_unlock(&queue_mutex);

        // 显示CAN帧数据
        display_frame(frame);
    }
}

通过合理设计线程间的同步机制,可以确保数据的接收、处理和显示在多线程环境下稳定高效地进行。这不仅提高了系统的响应速度,还优化了资源的利用效率。

5. 数据过滤机制实现

随着CAN网络中节点数量和数据量的增加,有效地过滤掉不需要的数据变得越来越重要。本章将深入探讨数据过滤的概念,Linux下CAN设备过滤器的配置方法,以及过滤器在实践中的应用案例。

5.1 数据过滤的概念和重要性

5.1.1 数据过滤的目的和应用场景

数据过滤是指按照特定的条件筛选出所需数据,忽略不必要信息的过程。在CAN网络中,数据过滤可以降低接收端的处理负担,提高系统效率。例如,在一个汽车内部网络中,发动机控制器只需要关心与发动机状态相关的信息,对其他不相关数据如空调状态则可以过滤掉。

5.1.2 过滤器的类型和设置方法

过滤器主要分为硬件过滤器和软件过滤器。硬件过滤器通过硬件电路实现,对数据帧的标识符进行过滤,只允许匹配的帧通过并被主机接收。软件过滤器则是在数据传入用户空间后,通过编程逻辑对数据进行处理。设置方法包括硬编码过滤规则和动态加载配置文件等方式。

5.2 Linux下CAN设备过滤器配置

5.2.1 硬件过滤器和软件过滤器的区别

硬件过滤器是集成在CAN控制器中的,可高效地进行实时过滤,但设置后不易更改。软件过滤器则提供了更大的灵活性,可以在任何时候根据应用需求动态调整过滤规则。然而,软件过滤也可能增加延迟和CPU负担。

5.2.2 过滤规则的编程实现

在Linux环境下,使用SocketCAN框架来编程实现过滤器是常见做法。可以通过 setsockopt() 系统调用设置CAN过滤器的规则。以下是一个简单的代码示例:

struct can_filter rfilter[2] = {
    // 过滤标准帧
    {
        .can_id   = 0x123, // 要匹配的标识符
        .can_mask = 0x7FF, // 掩码,用于确定哪些位必须匹配
    },
    // 过滤扩展帧
    {
        .can_id   = 0x12345678,
        .can_mask = 0x1FFFFFFF,
    },
};

struct sockaddr_can addr;
struct ifreq ifr;

fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);

strcpy(ifr.ifr_name, "can0");
ioctl(fd, SIOCGIFINDEX, &ifr);

addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

bind(fd, (struct sockaddr *)&addr, sizeof(addr));

// 设置过滤器规则
setsockopt(fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

在上述代码中, rfilter 数组定义了两个过滤规则,一个用于标准帧,另一个用于扩展帧。 setsockopt 用于应用这些过滤规则,过滤不匹配的数据帧。

5.3 过滤器在实践中的应用案例

5.3.1 实际项目中过滤器的配置示例

假设在一个工业自动化项目中,需要监控设备的温度和压力数据,但不关心其他设备发送的数据。可以设置过滤器只接收特定标识符对应的数据帧:

struct can_filter filter = {
    .can_id   = 0x123, // 设备标识符
    .can_mask = 0x7FF, // 广泛匹配所有标准帧
};

setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER, &filter, sizeof(filter));

5.3.2 过滤器性能测试与调优

在实际部署过滤器后,进行性能测试是至关重要的。可以使用 candump 工具记录数据,并通过分析工具检查过滤器是否正确地过滤了不需要的数据帧。性能调优可能包括增加缓存大小,或者在硬件过滤器无法满足需求时转向软件过滤器。

candump -l can0 | grep "123"

以上命令会列出所有标识符为 123 的数据帧,可作为性能测试的一部分。

通过本章节的介绍,您应该已经获得了关于CAN数据过滤机制的深入理解,以及如何在Linux环境下配置和应用过滤器的知识。在下一章节中,我们将探讨candump和cansend工具的使用和源码学习,以便更深入地掌握CAN网络的数据处理和发送。

6. candump和cansend工具源码学习

在这一章节中,我们将深入了解并分析两个在CAN数据处理中常用的Linux工具:candump和cansend。这些工具的源码不仅可以提供丰富的学习材料,而且可以通过源码修改与定制化开发,来满足特定的项目需求。
## 6.1 candump工具的使用和分析
    6.1.1 candump工具的功能和参数
        candump是一个用于捕获CAN总线上的数据包并将其记录到文件中的工具。用户可以通过指定过滤规则来只捕获感兴趣的数据包。candump支持多种参数选项,以适应不同的使用场景。例如,使用`-l`参数可以实现循环缓冲区的记录,而`-n`参数则可以指定记录的包数。candump的输出格式为时间戳、扩展帧标识符、数据长度码和数据内容。
        用法示例:
        ```bash
        candump -v can0
        ```
        此命令会捕获can0接口上的所有CAN总线数据包,并输出到标准输出。
    6.1.2 candump源码结构解析
        从源码级别理解candump的工作原理,可以帮助我们更好地控制数据捕获的过程。candump的源代码中,主要包含了数据包捕获、时间戳生成、文件写入和用户界面这几个部分。
        源码简析:
        ```c
        // 示例:数据包捕获核心循环部分
        while (1) {
            canfd_frame frame;
            int res = read(s, &frame, sizeof(canfd_frame));
            if (res < 0) break;
            // ...处理捕获到的CAN帧...
            if (write(ofd, &frame, res) != res)
                break;
        }
        ```
## 6.2 cansend工具的使用和分析
    6.2.1 cansend工具的功能和参数
        cansend工具允许用户向CAN总线发送指定的CAN帧。它同样支持多种参数来定义CAN帧的发送行为,如指定接口、帧ID和数据内容等。使用`-i`参数可以指定数据文件,该文件中包含了要发送的CAN帧数据。
        用法示例:
        ```bash
        cansend can0 123#1122334455667788
        ```
        此命令会向can0接口发送一个标准帧,帧ID为0x123,数据为`11 22 33 44 55 66 77 88`。
    6.2.2 cansend源码结构解析
        cansend的源码相对简单,主要包括解析用户输入的CAN帧数据和通过CAN接口发送CAN帧两个主要部分。源码中使用了socketCAN接口,通过标准的socket操作来发送数据。
        源码简析:
        ```c
        // 示例:CAN帧发送逻辑
        int s = open DeviceName, O_RDWR);
        struct sockaddr_can addr;
        struct can_frame frame;
        // ...用户输入的数据转换为frame结构体...
        if (write(s, &frame, sizeof(struct can_frame)) < 0) {
            perror("CAN write error");
            return 1;
        }
        ```
## 6.3 基于candump/cansend的高级应用
    6.3.1 自定义数据捕获和回放
        自定义工具可以基于candump的捕获机制来设计,支持特定的数据格式和过滤选项。回放功能可以利用cansend的源码,对之前记录的数据文件进行回放操作,以实现特定的测试场景模拟。
        实现步骤:
        - 扩展candump的过滤器以支持新的数据格式。
        - 修改cansend,使其能够读取特定格式的数据文件并按照时间戳顺序回放数据。
    6.3.2 源码修改与定制化开发
        根据项目的具体需求,我们可能需要对candump和cansend的源码进行修改。例如,可以在candump中加入额外的日志记录,或者对cansend添加批量发送的选项。定制化开发允许我们根据需要灵活调整工具的功能。
        开发示例:
        ```c
        // 示例:向candump添加自定义日志信息
        fprintf(ofd, "%d.%06d [%d] 0x%03X#%s\n", stamp.tv_sec, stamp.tv_usec, frame.can_id, frame.can_dlc, data);
        ```

通过深入学习candump和cansend的源码,不仅可以提高对CAN协议工具的理解,而且能够通过源码修改与定制化开发来实现更高效的数据处理和协议分析。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:CAN协议广泛应用于汽车电子设备的嵌入式系统中,"can-utils"提供了两个关键命令行工具:"candump"用于监控CAN总线数据包,"cansend"用于发送自定义数据帧。通过学习和实践这些工具的源码,开发者可以创建自己的CAN数据包监控程序,这需要对CAN协议、操作系统I/O操作和编程技能有深入理解。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值