零基础掌握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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Psyduck_ing

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

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

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

打赏作者

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

抵扣说明:

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

余额充值