nuttx实战项目:多路串口合并功能之八主app程序设计

利用AI工具生成可直接编译的NuttX应用程序

在当今的开发环境中,从提升公司和个人效率的角度出发,将AI工具用于代码生成无疑是一个高效的选择。

需求提出

我向ChatGPT提出了如下需求,旨在获得一个高质量、可直接使用的解决方案:

请帮我生成一个可直接使用的NuttX系统应用程序。要求如下:

  1. 实现一个串口管理程序,能处理多个UART设备与一个主UART之间的数据路由。
  2. 程序中应尽量多地增加调试信息,并通过 ifdef DEBUG 的方式控制其开启/关闭,便于程序调试。
  3. 请深入思考,提供一个结构清晰、可直接编译且最优化的方案。

AI生成的方案与代码

ChatGPT很好地理解了需求,并提供了一个功能完备的解决方案。

方案核心特点

  • 多串口管理:支持管理四个设备串口(UART_DEVICE_A ~ UART_DEVICE_D)与一个主控串口(UART_MAIN),路径已宏定义,便于修改。
  • 多线程架构
  • 主线程:负责打开串口,并通过 poll() 监听各设备串口的下行数据,封包后上报给主串口。
  • 接收线程:负责接收主串口的下行命令,并转发至对应设备或进行本地处理。
  • 探测线程:每秒执行一次,处理超时释放、未绑定设备探测、设备保活以及向主串口发送统一心跳包。
  • 灵活的调试机制
  • 编译时控制:通过在编译时添加 -DENABLE_DEBUG 参数,来决定是否将调试打印代码编译进二进制文件。
  • 运行时控制:当编译启用调试后,运行程序时使用 -d 参数可以动态开启或关闭运行时的调试输出。
  • 通信协议:实现了简单的累加和CRC校验,封包格式符合要求。串口配置为115200, 8N1, RAW模式。

程序使用说明

  1. 将生成的C代码保存为 .c,可放置于 apps/examples/ 目录下。
  2. 需要在NuttX的构建系统中将此文件注册为一个示例应用程序。
  3. 编译与调试
  • 启用调试信息进行编译。
  • 运行 ./portmgr -d 即可在运行时看到丰富的调试信息,极大便利了开发与问题排查。

通过这个案例,我们可以看到AI工具如何快速响应一个复杂且具体的嵌入式开发需求,并生成结构严谨、具备工程化考虑(如灵活的调试机制)的代码,显著提升了开发效率。

直接可复制的 C 源代码(NuttX app)

/*
* portmgr.c
*
* 串口转发与探测管理(NuttX app 示例)
*
* 编译:
* - 若希望包含调试打印,编译时加入 -DENABLE_DEBUG
* 运行:
* ./portmgr [-d] // -d 启用运行时调试输出(仅在 ENABLE_DEBUG 编译打开时有效)
*
* 放置/集成建议:
* 将此文件放入 apps/examples/portmgr/ 下并添加到 NuttX apps Makefile 中进行构建。
*/

#include <nuttx/config.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <pthread.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <poll.h>

/* ===========================
* 配置项(按需修改)
* =========================== */
#define UART_MAIN "/dev/ttyS1"
#define UART_DEVICE_A "/dev/ttyS2"
#define UART_DEVICE_B "/dev/ttyS3"
#define UART_DEVICE_C "/dev/ttyS4"
#define UART_DEVICE_D "/dev/ttyS5"

#define BAUDRATE B115200

/* 运行时/编译时调试 */
#ifdef ENABLE_DEBUG
static bool debug_enabled = false;
#define LOG_DBG(fmt, ...) do { if (debug_enabled) fprintf(stderr, "[DBG] " fmt, ##__VA_ARGS__); } while (0)
#else
#define LOG_DBG(fmt, ...) ((void)0)
#endif

#define LOG_INFO(fmt, ...) do { fprintf(stderr, "[I] " fmt, ##__VA_ARGS__); } while (0)
#define LOG_WARN(fmt, ...) do { fprintf(stderr, "[W] " fmt, ##__VA_ARGS__); } while (0)
#define LOG_ERR(fmt, ...) do { fprintf(stderr, "[E] " fmt, ##__VA_ARGS__); } while (0)

/* ===========================
* 协议 & 缓冲
* =========================== */
#define HDR0 0xAA
#define HDR1 0xAA
#define HDR2 0xAA
#define TAIL 0xFF

#define MAX_PORTS 4
#define MAX_BUF 4096

/* 设备 ID 定义 */
enum {
DEV_BIS = 0,
DEV_BRAIN_O2 = 1,
DEV_MUSCLE = 2,
DEV_BLOOD = 3,
DEV_UNBOUND = 0xFF
};

typedef struct {
char name[48];
int fd;
uint8_t device_id;
uint64_t last_rx_time; /* ms */
uint8_t rxbuf[MAX_BUF];
size_t rxlen;
bool bound;
} port_device_t;

static port_device_t ports[MAX_PORTS];
static const char *port_paths[MAX_PORTS] = {
UART_DEVICE_A, UART_DEVICE_B, UART_DEVICE_C, UART_DEVICE_D
};

static int fd_main = -1;
static pthread_t tid_detect;
static pthread_t tid_mainrx;

/* Brain_O2 探测包 */
static const uint8_t BRAIN_PROBE[] = {
0xEF,0xEF,0xEF,0x0C,0x11,0x25,0x01,0x41,0x52,0x46,0x4C,0x41,0x4C,0x46,0x4C,0x7B,0xFE,0xFE,0xFE
};
static const size_t BRAIN_PROBE_LEN = sizeof(BRAIN_PROBE);

/* utility: 获取当前 ms 时间戳 */
static uint64_t now_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t)tv.tv_sec * 1000ULL + (tv.tv_usec / 1000ULL);
}

/* 简单 CRC:累加取低 8 位(按需求可替换) */
static uint8_t simple_crc(const uint8_t *buf, size_t len)
{
uint32_t s = 0;
for (size_t i = 0; i < len; ++i) s += buf[i];
return (uint8_t)(s & 0xFF);
}

/* 串口设置(115200, 8N1, raw) */
static int configure_serial(int fd)
{
struct termios tio;
if (tcgetattr(fd, &tio) != 0) {
LOG_ERR("tcgetattr fail: %d\n", errno);
return -1;
}

cfmakeraw(&tio);

cfsetispeed(&tio, BAUDRATE);
cfsetospeed(&tio, BAUDRATE);

tio.c_cflag &= ~CSTOPB;
tio.c_cflag &= ~PARENB;
tio.c_cflag &= ~CSIZE;
tio.c_cflag |= CS8;
tio.c_cflag |= CLOCAL | CREAD;

tio.c_lflag = 0;
tio.c_iflag = 0;
tio.c_oflag = 0;

/* 非阻塞读,最短字符数 = 0,超时 = 1 deci-seconds (0.1s) */
tio.c_cc[VMIN] = 0;
tio.c_cc[VTIME] = 1;

if (tcsetattr(fd, TCSANOW, &tio) != 0) {
LOG_ERR("tcsetattr fail: %d\n", errno);
return -1;
}

tcflush(fd, TCIOFLUSH);
return 0;
}

/* 打开串口设备 */
static int open_port(const char *path)
{
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
LOG_ERR("open %s fail: %d\n", path, errno);
return -1;
}

if (configure_serial(fd) != 0) {
close(fd);
return -1;
}
LOG_INFO("Opened %s fd=%d\n", path, fd);
return fd;
}

/* 可靠写出全部数据 */
static ssize_t write_all(int fd, const void *buf, size_t len)
{
const uint8_t *p = buf;
size_t left = len;
while (left > 0) {
ssize_t w = write(fd, p, left);
if (w < 0) {
if (errno == EINTR) continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(1000);
continue;
}
return -1;
}
left -= (size_t)w;
p += w;
}
return len;
}

/* 组帧并发送到上位机(UART_MAIN)
* payload 指向子负载(例如设备数据),payload_len 表示 payload 的字节数
* 根据约定:length = 1 + payload_len (1 表示 device_id 字节)
* CRC: 对 payload(不含 device_id)做 simple_crc
*/
static int send_frame_to_main(int main_fd, uint8_t device_id,
const uint8_t *payload, size_t payload_len)
{
if (main_fd < 0) return -1;

uint8_t header[3] = { HDR0, HDR1, HDR2 };
uint16_t length = (uint16_t)(1 + payload_len);
uint8_t len_be[2] = { (uint8_t)((length >> 8) & 0xFF), (uint8_t)(length & 0xFF) };

uint8_t crc = 0;
if (payload_len > 0) crc = simple_crc(payload, payload_len);
else crc = 0;

/* 构建分段写,避免栈过大 */
if (write_all(main_fd, header, sizeof(header)) < 0) {
LOG_ERR("write header fail\n"); return -1;
}
if (write_all(main_fd, len_be, 2) < 0) {
LOG_ERR("write len fail\n"); return -1;
}
/* 写 device_id */
if (write_all(main_fd, &device_id, 1) < 0) {
LOG_ERR("write dev id fail\n"); return -1;
}
/* 写 payload */
if (payload_len > 0) {
if (write_all(main_fd, payload, payload_len) < 0) {
LOG_ERR("write payload fail\n"); return -1;
}
}
/* 写 CRC */
if (write_all(main_fd, &crc, 1) < 0) {
LOG_ERR("write crc fail\n"); return -1;
}
/* 写尾 */
uint8_t tail[3] = { TAIL, TAIL, TAIL };
if (write_all(main_fd, tail, 3) < 0) {
LOG_ERR("write tail fail\n"); return -1;
}

LOG_DBG("Sent frame to main: dev=%u payload=%zu crc=0x%02x\n",
device_id, payload_len, crc);
return 0;
}

/* 发送原始字节到某个串口(用于探测帧/保活) */
static int send_raw(int fd, const uint8_t *buf, size_t len)
{
if (fd < 0) return -1;
ssize_t r = write_all(fd, buf, len);
if (r < 0) {
LOG_ERR("send_raw write fail fd=%d err=%d\n", fd, errno);
return -1;
}
LOG_DBG("send_raw fd=%d len=%zu\n", fd, len);
return 0;
}

/* 模式:在 port->rxbuf 中查找特征,返回设备 id 或 DEV_UNBOUND */
static uint8_t detect_device_from_buffer(port_device_t *port)
{
if (!port || port->rxlen == 0) return DEV_UNBOUND;

/* 简单的匹配:查找特征序列 */
/* Brain_O2 响应:前 6 字节为 EF EF EF 11 11 25 */
for (size_t i = 0; i + 6 <= port->rxlen; ++i) {
if (port->rxbuf[i] == 0xEF && port->rxbuf[i+1] == 0xEF && port->rxbuf[i+2] == 0xEF &&
port->rxbuf[i+3] == 0x11 && port->rxbuf[i+4] == 0x11 && port->rxbuf[i+5] == 0x25) {
return DEV_BRAIN_O2;
}
}

/* Muscle: 前 10 byte 固定 */
if (port->rxlen >= 10) {
const uint8_t mus[10] = {0xFA,0x0C,0x19,0x04,0x00,0x00,0x00,0x00,0x00,0x01};
if (memcmp(port->rxbuf, mus, sizeof(mus)) == 0) {
return DEV_MUSCLE;
}
}

/* Blood: 检查一些位置特征 */
if (port->rxlen >= 32) {
if (port->rxbuf[0] == 0xFA && port->rxbuf[1] == 0xFA && port->rxbuf[2] == 0xFA &&
port->rxbuf[7] == 0x00 && port->rxbuf[8] == 0x17 &&
port->rxbuf[29] == 0xFB && port->rxbuf[30] == 0xFB && port->rxbuf[31] == 0xFB) {
return DEV_BLOOD;
}
}

/* BIS: 长帧,查找连续三个 0x66 */
for (size_t i = 0; i + 3 <= port->rxlen; ++i) {
if (port->rxbuf[i] == 0x66 && port->rxbuf[i+1] == 0x66 && port->rxbuf[i+2] == 0x66) {
return DEV_BIS;
}
}

return DEV_UNBOUND;
}

/* 处理设备接收的数据(将读到的数据添加到 port->rxbuf,并尝试识别、上报) */
static void handle_device_input(port_device_t *port, const uint8_t *data, ssize_t len)
{
if (!port || len <= 0) return;

/* 更新 last_rx_time */
port->last_rx_time = now_ms();

/* append to buffer for detection (若超出,截断) */
size_t can = MAX_BUF - port->rxlen;
if ((size_t)len > can) {
/* 若缓冲区快满,则先将旧数据截断,保留末尾 */
if (port->rxlen >= len) {
memmove(port->rxbuf, port->rxbuf + (port->rxlen - (len)), len);
memcpy(port->rxbuf + (len - 0), data, len);
port->rxlen = len;
} else {
/* 直接写到末尾(丢弃超过容量的头部)*/
memcpy(port->rxbuf, data + ((size_t)len - MAX_BUF), MAX_BUF);
port->rxlen = MAX_BUF;
}
} else {
memcpy(port->rxbuf + port->rxlen, data, len);
port->rxlen += (size_t)len;
}

LOG_DBG("port %s got %zd bytes, rxlen=%zu\n", port->name, len, port->rxlen);

/* 若未绑定,尝试识别设备 */
if (!port->bound) {
uint8_t did = detect_device_from_buffer(port);
if (did != DEV_UNBOUND) {
port->device_id = did;
port->bound = true;
LOG_INFO("Port %s bound to device id=%u\n", port->name, (unsigned)did);
}
}

/* 直接将收到的数据作为 payload 发到上位机(不额外解析) */
if (fd_main >= 0 && port->bound) {
/* 将接收到的数据整体作为 payload 上传 */
if (send_frame_to_main(fd_main, port->device_id, data, (size_t)len) != 0) {
LOG_WARN("send_frame_to_main failed for %s\n", port->name);
} else {
LOG_DBG("Forwarded %zd bytes from %s to main as dev %u\n", len, port->name, port->device_id);
}
}
}

/* uart_main 接收线程 (负责接收上位机下发并转发到对应设备或本地处理) */
static void *uart_main_rx_thread(void *arg)
{
(void)arg;
uint8_t buf[1024];
LOG_INFO("uart_main_rx_thread started\n");

while (1) {
ssize_t r = read(fd_main, buf, sizeof(buf));
if (r < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(10000);
continue;
}
LOG_ERR("main read err %d\n", errno);
usleep(100000);
continue;
} else if (r == 0) {
usleep(10000);
continue;
}

LOG_DBG("main_rx read %zd bytes\n", r);

/* 简单解析:尝试从 buf 中找到完整帧(头 + len + ... + tail)并处理
* 为了简洁,下面处理只做一次帧解析(假定每次read读到完整帧或单帧)
*/

/* 查找 header */
ssize_t pos = -1;
for (ssize_t i = 0; i + 2 < r; ++i) {
if (buf[i] == 0xAA && buf[i+1] == 0xAA && buf[i+2] == 0xAA) {
pos = i;
break;
}
}
if (pos < 0) {
LOG_DBG("no header in main rx\n");
continue;
}
if (pos + 5 >= r) {
LOG_DBG("incomplete header/len\n");
continue;
}

size_t idx = (size_t)pos;
uint16_t length = (uint16_t)(buf[idx+3] << 8 | buf[idx+4]);
size_t frame_len = 3 + 2 + length + 1 + 3; /* hdr(3)+len(2)+length+crc(1)+tail(3) */

if (idx + frame_len > (size_t)r) {
LOG_DBG("incomplete frame, need %zu got %zd\n", frame_len, r - idx);
continue;
}

uint8_t device_id = buf[idx+5];
size_t payload_len = (size_t)length - 1;
uint8_t *payload = &buf[idx + 6];

/* CRC 校验 (payload only) */
uint8_t got_crc = buf[idx + 6 + payload_len];
uint8_t calc_crc_v = 0;
if (payload_len > 0) calc_crc_v = simple_crc(payload, payload_len);
if (got_crc != calc_crc_v) {
LOG_WARN("main_rx CRC mismatch got=0x%02x calc=0x%02x\n", got_crc, calc_crc_v);
continue;
}

/* 处理 device_id==0xFF 的本地命令(示例:调试开关等) */
if (device_id == DEV_UNBOUND) {
#ifdef ENABLE_DEBUG
/* payload 可以包含命令,例如 "DBG_ON" / "DBG_OFF" */
if (payload_len >= 3 && memcmp(payload, "DBG", 3) == 0) {
if (payload_len >= 6 && memcmp(payload+4, "ON", 2) == 0) {
debug_enabled = true;
LOG_INFO("Debug enabled via main command\n");
} else if (payload_len >= 7 && memcmp(payload+4, "OFF", 3) == 0) {
debug_enabled = false;
LOG_INFO("Debug disabled via main command\n");
}
}
#endif
continue;
}

/* 转发到对应物理串口(若已绑定) */
bool forwarded = false;
for (int i = 0; i < MAX_PORTS; ++i) {
if (ports[i].bound && ports[i].device_id == device_id) {
send_raw(ports[i].fd, payload, payload_len); /* 直接发送 payload 到设备 */
LOG_DBG("Forwarded command to port %s dev=%u len=%zu\n", ports[i].name, device_id, payload_len);
forwarded = true;
break;
}
}
if (!forwarded) {
LOG_WARN("No bound port for device id=%u\n", device_id);
}
}
return NULL;
}

/* 探测线程:每秒执行一次超时检测、未绑定探测、BrainO2 保活、发送心跳 */
static void *detect_thread(void *arg)
{
(void)arg;
LOG_INFO("detect_thread started\n");
while (1) {
uint64_t now = now_ms();

/* A. 超时释放 */
for (int i = 0; i < MAX_PORTS; ++i) {
if (ports[i].bound) {
if ((now - ports[i].last_rx_time) > 3000ULL) {
LOG_INFO("Port %s bound device %u timeout, unbind\n", ports[i].name, ports[i].device_id);
ports[i].bound = false;
ports[i].device_id = DEV_UNBOUND;
ports[i].rxlen = 0;
}
}
}

/* B. 针对未绑定端口做探测 */
for (int i = 0; i < MAX_PORTS; ++i) {
if (!ports[i].bound) {
/* 如果缓存中已有数据,则尝试识别 */
if (ports[i].rxlen > 0) {
uint8_t d = detect_device_from_buffer(&ports[i]);
if (d != DEV_UNBOUND) {
ports[i].device_id = d;
ports[i].bound = true;
LOG_INFO("Detect thread bound %s -> dev %u (from buffer)\n", ports[i].name, d);
continue;
}
}
/* 否则发送 Brain_O2 探测包并等待 0.5s */
LOG_DBG("Detect: sending Brain probe to %s\n", ports[i].name);
send_raw(ports[i].fd, BRAIN_PROBE, BRAIN_PROBE_LEN);
/* 等待 500ms 去读取响应(在下一秒循环时检测 rxbuf) */
usleep(500000);
/* 读取 any available data to rxbuf to enable detection next cycle (non-blocking read) */
uint8_t tmp[512];
ssize_t rr;
while ((rr = read(ports[i].fd, tmp, sizeof(tmp))) > 0) {
handle_device_input(&ports[i], tmp, rr);
}
}
}

/* C. Brain_O2 保活(已经绑定) */
for (int i = 0; i < MAX_PORTS; ++i) {
if (ports[i].bound && ports[i].device_id == DEV_BRAIN_O2) {
/* 发送一次探测帧作为保活 */
send_raw(ports[i].fd, BRAIN_PROBE, BRAIN_PROBE_LEN);
LOG_DBG("Keepalive: sent brain probe to %s\n", ports[i].name);
}
}

/* D. 汇总发送心跳状态到上位机(按 A–D 端口顺序) */
uint8_t state[4];
for (int i = 0; i < MAX_PORTS; ++i) {
if (!ports[i].bound) state[i] = 0xFF;
else state[i] = ports[i].device_id & 0xFF;
}
/* 构建心跳 payload: subtype(0x00) + state[4] */
uint8_t heartbeat_payload[1 + 4];
heartbeat_payload[0] = 0x00;
memcpy(&heartbeat_payload[1], state, 4);
/* 心跳的 CRC 是对子类型到状态字节累加校验 (即 payload) */
if (fd_main >= 0) {
send_frame_to_main(fd_main, DEV_UNBOUND, heartbeat_payload, sizeof(heartbeat_payload));
LOG_DBG("Heartbeat sent\n");
}

/* sleep 1s */
sleep(1);
}
return NULL;
}

/* 主函数:打开串口、初始化结构、启动线程、poll 监听下行端口并上报 */
int main(int argc, char *argv[])
{
LOG_INFO("portmgr starting...\n");

#ifdef ENABLE_DEBUG
/* 运行时启用调试:可通过 -d 打开 */
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "-d") == 0) {
debug_enabled = true;
LOG_INFO("Runtime debug enabled\n");
}
}
#endif

/* 初始化 ports 结构 */
for (int i = 0; i < MAX_PORTS; ++i) {
snprintf(ports[i].name, sizeof(ports[i].name), "%s", port_paths[i]);
ports[i].fd = -1;
ports[i].device_id = DEV_UNBOUND;
ports[i].last_rx_time = 0;
ports[i].rxlen = 0;
ports[i].bound = false;
}

/* 打开下行串口(可全部打开,即便不存在) */
for (int i = 0; i < MAX_PORTS; ++i) {
int fd = open_port(ports[i].name);
if (fd >= 0) {
ports[i].fd = fd;
} else {
LOG_WARN("Failed opening port %s\n", ports[i].name);
}
}

/* 打开上行串口 / main */
fd_main = open_port(UART_MAIN);
if (fd_main < 0) {
LOG_WARN("Failed to open UART_MAIN (%s). Will continue in degraded mode.\n", UART_MAIN);
}

/* 启动 uart_main_rx 线程 */
if (fd_main >= 0) {
if (pthread_create(&tid_mainrx, NULL, uart_main_rx_thread, NULL) != 0) {
LOG_ERR("Failed to create main rx thread\n");
}
}

/* 启动探测线程 */
if (pthread_create(&tid_detect, NULL, detect_thread, NULL) != 0) {
LOG_ERR("Failed to create detect thread\n");
}

/* 主循环:poll 下行串口,有数据则读取并处理(转发到 main) */
struct pollfd pfds[MAX_PORTS];
while (1) {
int nfd = 0;
for (int i = 0; i < MAX_PORTS; ++i) {
if (ports[i].fd >= 0) {
pfds[nfd].fd = ports[i].fd;
pfds[nfd].events = POLLIN;
pfds[nfd].revents = 0;
++nfd;
}
}
if (nfd == 0) {
/* 没有可用下行串口,休眠并重试 */
sleep(1);
continue;
}

int ret = poll(pfds, nfd, 500); /* 0.5s 超时 */
if (ret < 0) {
if (errno == EINTR) continue;
LOG_ERR("poll error %d\n", errno);
usleep(100000);
continue;
} else if (ret == 0) {
/* timeout */
continue;
} else {
int idx = 0;
for (int i = 0; i < MAX_PORTS; ++i) {
if (ports[i].fd < 0) continue;
if (pfds[idx].revents & POLLIN) {
uint8_t buf[1024];
ssize_t r = read(ports[i].fd, buf, sizeof(buf));
if (r > 0) {
handle_device_input(&ports[i], buf, r);
} else if (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
LOG_ERR("read error on %s: %d\n", ports[i].name, errno);
}
}
++idx;
}
}
}

/* cleanup (实践中不会到这里) */
for (int i = 0; i < MAX_PORTS; ++i) {
if (ports[i].fd >= 0) close(ports[i].fd);
}
if (fd_main >= 0) close(fd_main);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值