零基础掌握libmodbus开发技术

》》欢迎关注 嵌入式软件客栈 公众号,获取更多实战技巧《《

Modbus是一种广泛应用于工业自动化领域的通信协议,最初由Modicon(现在的施耐德电气)在1979年开发。作为一种开放、简单且健壮的协议,Modbus已成为工业电子设备之间通信的实际标准。

libmodbus是一个免费的、开源的、功能丰富的Modbus协议库,它提供了对各种Modbus变体(如RTU、ASCII和TCP)的支持,可以用于开发主站(客户端)和从站(服务器)应用程序。libmodbus以其高度可移植性、良好的文档和稳定的API而闻名,是开发Modbus通信应用的首选库之一。

Modbus协议基础

在深入了解libmodbus库之前,我们先简要了解Modbus协议的基本概念。

Modbus通信模型

Modbus采用主从(Master-Slave)架构,其中:

  • 主站(Master):也称为客户端,发起请求并等待从站响应
  • 从站(Slave):也称为服务器,接收并处理主站的请求,然后发送响应

通信过程始终由主站发起,从站无法主动发送数据。一个主站可以与多个从站通信,每个从站需要有唯一的地址(1-247)。

功能码简介

Modbus协议使用功能码来指示从站执行特定操作。以下是一些最常用的功能码:

功能码功能名称操作内容
0x01Read Coils读取线圈状态(读位)
0x02Read Discrete Inputs读取离散输入(读位)
0x03Read Holding Registers读取保持寄存器(读字)
0x04Read Input Registers读取输入寄存器(读字)
0x05Write Single Coil写单个线圈(写位)
0x06Write Single Register写单个寄存器(写字)
0x0FWrite Multiple Coils写多个线圈(写位)
0x10Write Multiple Registers写多个寄存器(写字)

数据模型

Modbus定义了四种数据类型:

  1. 线圈(Coils):单个二进制位,可读可写,地址范围:00001-09999
  2. 离散输入(Discrete Inputs):单个二进制位,只读,地址范围:10001-19999
  3. 输入寄存器(Input Registers):16位字,只读,地址范围:30001-39999
  4. 保持寄存器(Holding Registers):16位字,可读可写,地址范围:40001-49999

libmodbus库概述

libmodbus是一个功能全面的Modbus协议库,提供了发送/接收Modbus消息的API,支持RTU、ASCII和TCP等不同通信模式。

主要特性

  • 开源且免费:基于LGPL v2.1+许可证
  • 跨平台:支持Linux、Windows、macOS等多种操作系统
  • 多种通信模式:支持RTU、TCP和TCP PI (支持IPv6)
  • 完整功能:实现了所有标准Modbus功能码
  • 易于使用:提供简单直观的API
  • 良好文档:详细的API文档和示例代码
  • 活跃社区:持续维护和更新

架构设计

libmodbus采用上下文(context)的概念来处理不同的连接和配置。库的主要组件包括:

  • 上下文管理:创建、配置和释放连接上下文
  • 连接函数:建立和关闭连接
  • 数据交换函数:读取和写入各种类型的数据
  • 服务器函数:用于实现Modbus从站
  • 辅助函数:错误处理、调试和数据转换

环境搭建

Linux系统安装

在大多数Linux发行版中,可以直接通过包管理器安装libmodbus:

Debian/Ubuntu:

sudo apt-get install libmodbus-dev

Fedora/RHEL:

sudo dnf install libmodbus-devel

Arch Linux:

sudo pacman -S libmodbus

Windows系统安装

在Windows上,您可以通过以下方法安装libmodbus:

  1. 使用MSYS2/MinGW:
pacman -S mingw-w64-x86_64-libmodbus
  1. 从源码编译:
    • 下载源码:https://github.com/stephane/libmodbus/releases
    • 使用Visual Studio打开项目并编译
    • 配置项目属性:
      • 将Configuration Type设置为Dynamic Library (.dll)
      • 在Additional Dependencies中添加ws2_32.lib

跨平台编译

从源码编译libmodbus:

# 下载源码
git clone https://github.com/stephane/libmodbus.git
cd libmodbus

# 配置和编译
./autogen.sh
./configure
make
sudo make install

验证安装:

pkg-config --libs --cflags libmodbus

如果安装成功,上述命令将输出编译参数,例如:

-I/usr/local/include -L/usr/local/lib -lmodbus

RTU模式通信

RTU(Remote Terminal Unit)模式是Modbus协议最初的实现方式,通过串行线(如RS-232、RS-485)进行通信。

RTU主站实现

以下是一个简单的RTU主站示例,用于读取从站的保持寄存器:

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

int main() {
    modbus_t *ctx;
    uint16_t reg_data[10];
    int rc;
    
    // 创建新的RTU上下文
    ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
    if (ctx == NULL) {
        fprintf(stderr, "无法创建modbus上下文\n");
        return -1;
    }
    
    // 设置从站地址
    modbus_set_slave(ctx, 1);
    
    // 设置调试模式(可选)
    modbus_set_debug(ctx, TRUE);
    
    // 连接设备
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    
    // 读取10个保持寄存器,起始地址为0
    rc = modbus_read_registers(ctx, 0, 10, reg_data);
    if (rc == -1) {
        fprintf(stderr, "读取失败: %s\n", modbus_strerror(errno));
        modbus_close(ctx);
        modbus_free(ctx);
        return -1;
    }
    
    // 打印读取到的数据
    printf("读取到%d个寄存器\n", rc);
    for (int i = 0; i < rc; i++) {
        printf("reg[%d]=%d (0x%X)\n", i, reg_data[i], reg_data[i]);
    }
    
    // 清理资源
    modbus_close(ctx);
    modbus_free(ctx);
    
    return 0;
}

编译命令:

gcc -o rtu_master rtu_master.c -lmodbus

RTU从站实现

下面是一个简单的RTU从站示例,模拟一个具有线圈和寄存器的从站设备:

#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <signal.h>

static int run = 1;

void signal_handler(int sig) {
    run = 0;
}

int main() {
    modbus_t *ctx;
    modbus_mapping_t *mb_mapping;
    uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH];
    int rc;
    
    // 捕获Ctrl+C信号
    signal(SIGINT, signal_handler);
    
    // 创建新的RTU上下文
    ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
    if (ctx == NULL) {
        fprintf(stderr, "无法创建modbus上下文\n");
        return -1;
    }
    
    // 设置从站地址
    modbus_set_slave(ctx, 1);
    
    // 设置调试模式(可选)
    modbus_set_debug(ctx, TRUE);
    
    // 连接设备
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    
    // 分配并初始化Modbus映射
    mb_mapping = modbus_mapping_new(100, 100, 100, 100);
    if (mb_mapping == NULL) {
        fprintf(stderr, "映射分配失败: %s\n", modbus_strerror(errno));
        modbus_close(ctx);
        modbus_free(ctx);
        return -1;
    }
    
    // 设置一些初始值
    mb_mapping->tab_registers[0] = 1234;
    mb_mapping->tab_registers[1] = 5678;
    mb_mapping->tab_bits[0] = 1;
    mb_mapping->tab_bits[1] = 0;
    
    printf("从站已启动,按Ctrl+C退出...\n");
    
    while (run) {
        rc = modbus_receive(ctx, query);
        if (rc > 0) {
            // 处理收到的请求
            modbus_reply(ctx, query, rc, mb_mapping);
        } else if (rc == -1) {
            // 错误处理
            fprintf(stderr, "接收失败: %s\n", modbus_strerror(errno));
        }
    }
    
    // 清理资源
    modbus_mapping_free(mb_mapping);
    modbus_close(ctx);
    modbus_free(ctx);
    
    return 0;
}

编译命令:

gcc -o rtu_slave rtu_slave.c -lmodbus

串口参数配置

使用RTU模式时,正确配置串口参数非常重要:

// 创建RTU上下文
modbus_t *ctx = modbus_new_rtu(
    "/dev/ttyUSB0",  // 串口设备名
    9600,            // 波特率
    'N',             // 奇偶校验: 'N'=无, 'E'=偶校验, 'O'=奇校验
    8,               // 数据位
    1                // 停止位
);

// 设置RTS模式(适用于RS485)
modbus_rtu_set_serial_mode(ctx, MODBUS_RTU_RS485);

// 设置RTS引脚控制
modbus_rtu_set_rts(ctx, MODBUS_RTU_RTS_UP);
modbus_rtu_set_rts_delay(ctx, 2000); // 延迟2毫秒

TCP模式通信

Modbus TCP是Modbus协议的以太网变种,使用TCP/IP协议栈进行通信,端口号通常为502。

TCP主站实现

以下是一个简单的TCP主站示例:

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

int main() {
    modbus_t *ctx;
    uint16_t reg_data[10];
    int rc;
    
    // 创建新的TCP上下文
    ctx = modbus_new_tcp("192.168.1.100", 502);
    if (ctx == NULL) {
        fprintf(stderr, "无法创建modbus上下文\n");
        return -1;
    }
    
    // 设置从站地址(在TCP中通常不需要,但某些设备可能需要)
    modbus_set_slave(ctx, 1);
    
    // 连接设备
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    
    // 读取10个保持寄存器,起始地址为0
    rc = modbus_read_registers(ctx, 0, 10, reg_data);
    if (rc == -1) {
        fprintf(stderr, "读取失败: %s\n", modbus_strerror(errno));
        modbus_close(ctx);
        modbus_free(ctx);
        return -1;
    }
    
    // 打印读取到的数据
    printf("读取到%d个寄存器\n", rc);
    for (int i = 0; i < rc; i++) {
        printf("reg[%d]=%d (0x%X)\n", i, reg_data[i], reg_data[i]);
    }
    
    // 清理资源
    modbus_close(ctx);
    modbus_free(ctx);
    
    return 0;
}

编译命令:

gcc -o tcp_master tcp_master.c -lmodbus

TCP从站实现

下面是一个简单的TCP从站示例:

#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <signal.h>

static int run = 1;

void signal_handler(int sig) {
    run = 0;
}

int main() {
    modbus_t *ctx;
    modbus_mapping_t *mb_mapping;
    int server_socket;
    int rc;
    uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
    
    // 捕获Ctrl+C信号
    signal(SIGINT, signal_handler);
    
    // 创建新的TCP上下文
    ctx = modbus_new_tcp("0.0.0.0", 502);
    if (ctx == NULL) {
        fprintf(stderr, "无法创建modbus上下文\n");
        return -1;
    }
    
    // 分配并初始化Modbus映射
    mb_mapping = modbus_mapping_new(100, 100, 100, 100);
    if (mb_mapping == NULL) {
        fprintf(stderr, "映射分配失败: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    
    // 设置一些初始值
    mb_mapping->tab_registers[0] = 1234;
    mb_mapping->tab_registers[1] = 5678;
    mb_mapping->tab_bits[0] = 1;
    mb_mapping->tab_bits[1] = 0;
    
    // 创建服务器套接字
    server_socket = modbus_tcp_listen(ctx, 1);
    if (server_socket == -1) {
        fprintf(stderr, "监听失败: %s\n", modbus_strerror(errno));
        modbus_mapping_free(mb_mapping);
        modbus_free(ctx);
        return -1;
    }
    
    printf("从站已启动,监听端口502,按Ctrl+C退出...\n");
    
    while (run) {
        // 接受客户端连接
        int client_socket = modbus_tcp_accept(ctx, &server_socket);
        if (client_socket == -1) {
            fprintf(stderr, "接受连接失败: %s\n", modbus_strerror(errno));
            break;
        }
        
        printf("新客户端已连接\n");
        
        while (run) {
            rc = modbus_receive(ctx, query);
            if (rc > 0) {
                // 处理收到的请求
                modbus_reply(ctx, query, rc, mb_mapping);
            } else if (rc == -1) {
                // 连接关闭或错误
                fprintf(stderr, "接收失败: %s\n", modbus_strerror(errno));
                break;
            }
        }
    }
    
    // 清理资源
    modbus_mapping_free(mb_mapping);
    modbus_close(ctx);
    modbus_free(ctx);
    
    return 0;
}

编译命令:

gcc -o tcp_slave tcp_slave.c -lmodbus

网络参数配置

使用TCP模式时的一些高级配置:

// 创建TCP上下文,IPv4
modbus_t *ctx = modbus_new_tcp("192.168.1.100", 502);

// 创建TCP上下文,支持IPv6(TCP PI)
modbus_t *ctx = modbus_new_tcp_pi("::1", "502");

// 设置响应超时
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
modbus_set_response_timeout(ctx, &timeout);

// 启用/禁用TCP长连接
int option = 1;
modbus_set_socket(ctx, &option, sizeof(int));

高级应用

数据类型转换

libmodbus提供了处理不同数据类型的函数,特别是处理浮点数:

// 单个寄存器操作
uint16_t reg_value = 12345;
modbus_write_register(ctx, 0, reg_value);

// 将两个寄存器解析为浮点数(IEEE 754格式)
float float_value;
uint16_t regs[2];
modbus_read_registers(ctx, 0, 2, regs);

// 不同的字节顺序
float_value = modbus_get_float_abcd(regs); // 最常用格式
// 或其他格式
float_value = modbus_get_float_dcba(regs);
float_value = modbus_get_float_badc(regs);
float_value = modbus_get_float_cdab(regs);

// 写入浮点数
float value_to_write = 123.45;
uint16_t regs[2];
modbus_set_float_abcd(value_to_write, regs);
modbus_write_registers(ctx, 0, 2, regs);

异常处理

适当的错误处理对于健壮的Modbus应用至关重要:

// 设置错误恢复模式
modbus_set_error_recovery(ctx, 
    MODBUS_ERROR_RECOVERY_LINK | 
    MODBUS_ERROR_RECOVERY_PROTOCOL);

// 尝试操作并处理错误
int rc = modbus_read_registers(ctx, 0, 10, regs);
if (rc == -1) {
    int err = errno;
    
    switch (err) {
        case EMBXILFUN:
            printf("非法功能\n");
            break;
        case EMBXILADD:
            printf("非法数据地址\n");
            break;
        case EMBXILVAL:
            printf("非法数据值\n");
            break;
        case EMBXSFAIL:
            printf("从站设备故障\n");
            break;
        case EMBXACK:
            printf("确认\n");
            break;
        case EMBXSBUSY:
            printf("从站设备忙\n");
            break;
        case EMBXNACK:
            printf("否认\n");
            break;
        case EMBXMEMPAR:
            printf("内存奇偶校验错误\n");
            break;
        case EMBXGPATH:
            printf("网关路径不可用\n");
            break;
        case EMBXGTAR:
            printf("网关目标设备响应失败\n");
            break;
        case ETIMEDOUT:
            printf("连接超时\n");
            break;
        default:
            printf("未知错误: %s\n", modbus_strerror(err));
    }
}

超时设置

控制通信超时对于网络不稳定环境很重要:

// 获取当前超时设置
struct timeval timeout;
modbus_get_response_timeout(ctx, &timeout);
printf("当前响应超时: %ld秒 %ld微秒\n", timeout.tv_sec, timeout.tv_usec);

// 设置新的响应超时
timeout.tv_sec = 1;  // 1秒
timeout.tv_usec = 500000;  // 500毫秒
modbus_set_response_timeout(ctx, &timeout);

// 设置字节超时(主要用于RTU模式)
timeout.tv_sec = 0;
timeout.tv_usec = 500000;  // 500毫秒
modbus_set_byte_timeout(ctx, &timeout);

多个设备通信

通过设置从站地址,可以实现与多个设备的通信:

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

#define NB_DEVICES 3
#define START_ADDR 1

int main() {
    modbus_t *ctx;
    uint16_t reg_data[10];
    int i, rc;
    
    // 创建新的RTU上下文
    ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
    if (ctx == NULL) {
        fprintf(stderr, "无法创建modbus上下文\n");
        return -1;
    }
    
    // 连接设备
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    
    // 遍历多个从站地址
    for (i = 0; i < NB_DEVICES; i++) {
        int slave_addr = START_ADDR + i;
        
        // 设置从站地址
        if (modbus_set_slave(ctx, slave_addr) == -1) {
            fprintf(stderr, "设置从站地址失败: %s\n", modbus_strerror(errno));
            continue;
        }
        
        printf("查询从站 #%d...\n", slave_addr);
        
        // 读取寄存器
        rc = modbus_read_registers(ctx, 0, 10, reg_data);
        if (rc == -1) {
            fprintf(stderr, "读取失败: %s\n", modbus_strerror(errno));
            continue;
        }
        
        // 打印读取到的数据
        printf("从站 #%d 读取到%d个寄存器\n", slave_addr, rc);
        for (int j = 0; j < rc; j++) {
            printf("reg[%d]=%d (0x%X)\n", j, reg_data[j], reg_data[j]);
        }
        printf("\n");
    }
    
    // 清理资源
    modbus_close(ctx);
    modbus_free(ctx);
    
    return 0;
}

与嵌入式设备通信

ESP32示例

以下是在ESP32上使用Arduino框架实现Modbus TCP从站的示例:

#include <WiFi.h>
#include <ModbusTCP.h>

// WiFi凭据
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";

// Modbus寄存器定义
#define REG_COUNT 10
uint16_t holdingRegisters[REG_COUNT];

// 创建Modbus TCP服务器实例
ModbusTCP modbusTCP;

void setup() {
  Serial.begin(115200);
  
  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("WiFi连接成功!");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
  
  // 初始化一些测试数据
  for (int i = 0; i < REG_COUNT; i++) {
    holdingRegisters[i] = i * 100;
  }
  
  // 配置Modbus服务器
  modbusTCP.server();
  modbusTCP.holdingRegisters(holdingRegisters, REG_COUNT);
  
  // 启动Modbus TCP服务器
  modbusTCP.begin();
  
  Serial.println("Modbus TCP服务器已启动");
}

void loop() {
  // 处理Modbus请求
  modbusTCP.task();
  
  // 更新一些动态数据(例如传感器读数)
  holdingRegisters[0] = analogRead(A0);
  
  delay(10);
}

树莓派示例

以下是在树莓派上使用libmodbus实现RTU从站的示例:

#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <wiringPi.h>

#define LED_PIN 0  // BCM GPIO 17

static int run = 1;

void signal_handler(int sig) {
    run = 0;
}

int main() {
    modbus_t *ctx;
    modbus_mapping_t *mb_mapping;
    uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH];
    int rc;
    
    // 初始化WiringPi
    if (wiringPiSetup() == -1) {
        fprintf(stderr, "WiringPi初始化失败\n");
        return -1;
    }
    
    // 配置GPIO
    pinMode(LED_PIN, OUTPUT);
    
    // 捕获Ctrl+C信号
    signal(SIGINT, signal_handler);
    
    // 创建新的RTU上下文
    ctx = modbus_new_rtu("/dev/ttyAMA0", 9600, 'N', 8, 1);
    if (ctx == NULL) {
        fprintf(stderr, "无法创建modbus上下文\n");
        return -1;
    }
    
    // 设置从站地址
    modbus_set_slave(ctx, 1);
    
    // 连接设备
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    
    // 分配并初始化Modbus映射
    mb_mapping = modbus_mapping_new(10, 0, 10, 0);
    if (mb_mapping == NULL) {
        fprintf(stderr, "映射分配失败: %s\n", modbus_strerror(errno));
        modbus_close(ctx);
        modbus_free(ctx);
        return -1;
    }
    
    printf("Modbus RTU从站已启动 (地址: 1),按Ctrl+C退出...\n");
    
    while (run) {
        rc = modbus_receive(ctx, query);
        if (rc > 0) {
            // 处理收到的请求
            modbus_reply(ctx, query, rc, mb_mapping);
            
            // 根据线圈状态控制LED
            digitalWrite(LED_PIN, mb_mapping->tab_bits[0]);
            
            // 更新寄存器值
            mb_mapping->tab_registers[0] = digitalRead(LED_PIN);
        } else if (rc == -1) {
            // 错误处理
            fprintf(stderr, "接收失败: %s\n", modbus_strerror(errno));
        }
    }
    
    // 清理资源
    modbus_mapping_free(mb_mapping);
    modbus_close(ctx);
    modbus_free(ctx);
    
    return 0;
}

常见问题与解决方案

1. 设备不响应

问题症状:通信超时,设备不响应请求

可能原因

  • 串口/网络配置错误
  • 设备地址不匹配
  • 波特率、数据位、停止位或奇偶校验设置错误
  • 电气连接问题(RS485的A/B线连接反了)

解决方案

  • 检查物理连接
  • 验证串口/网络参数
  • 确认从站地址与程序设置匹配
  • 使用串口调试工具监控通信
  • 启用调试模式:modbus_set_debug(ctx, TRUE);

2. 数据读取错误

问题症状:读取到错误的数据或收到异常响应

可能原因

  • 访问的地址超出设备范围
  • 使用了不支持的功能码
  • 数据格式/字节顺序不正确

解决方案

  • 查阅设备手册,确认正确的地址范围
  • 使用正确的功能码读取数据
  • 尝试不同的字节顺序(ABCD、DCBA、BADC、CDAB)

3. RTU模式的时序问题

问题症状:RTU通信不稳定,偶尔丢失数据

可能原因

  • 字节超时设置不当
  • RS485控制信号(RTS)管理不正确
  • 高速通信下的缓冲区溢出

解决方案

  • 调整字节/响应超时设置
  • 正确配置RTS控制:modbus_rtu_set_serial_mode()modbus_rtu_set_rts()
  • 减小波特率或增加缓冲区大小

4. TCP连接问题

问题症状:无法建立TCP连接

可能原因

  • 防火墙阻止了Modbus TCP端口(通常是502)
  • IP地址错误
  • 设备不支持Modbus TCP

解决方案

  • 检查防火墙设置,确保允许Modbus TCP通信
  • 验证设备的IP地址和端口配置
  • 使用ping测试网络连通性

参考资源

  1. libmodbus官方网站
  2. GitHub仓库
  3. libmodbus API文档
  4. Modbus协议规范
  5. Getting Started with libmodbus

关注 嵌入式软件客栈 公众号,获取更多内容
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Psyduck_ing

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

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

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

打赏作者

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

抵扣说明:

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

余额充值