利用位域与共用体优化CAN通信数据帧的嵌入式实践

别再只学结构体了!C语言共用体(联合体)在嵌入式开发中的3个实战妙用 本文深入探讨了C语言共用体(联合体)在嵌入式开发中的三大实战应用:高效存储和读取EEPROM中的浮点数、解析通信协议中的多格式数据帧以及实现寄存器位域的便捷访问。通过具体代码示例和对比分析,展示了共用体如何节省内存、提升代码可读性和执行效率,是嵌入式工程师不可或缺的秘密武器。 阅读详情

1. 为什么需要优化CAN通信数据帧

在嵌入式系统开发中,CAN总线通信是非常常见的板间通信方式。但CAN数据帧有个硬性限制:每帧最多只能传输8个字节的数据。在实际项目中,我们经常需要传输各种传感器数据、设备状态、控制指令等信息,这些数据可能包含多个布尔标志、枚举值、整型数值等不同类型的数据。如果直接用普通结构体来组织这些数据,往往会浪费大量宝贵的带宽资源。

我记得在之前的一个机器人项目中,需要传输4个电机的状态标志(每个状态只需要1位表示true/false)、工作模式(0-64范围,需要7位)、以及两个摇杆的模拟量(0-1320范围,需要11位)。如果直接用传统结构体,可能需要十几个字节,但通过位域和共用体技术,我成功将这些数据压缩到了8字节内,完美利用了CAN帧的有限空间。

这种优化不仅仅是节省几个字节那么简单。在实时控制系统中,更小的数据帧意味着更低的通信延迟、更高的实时性,以及更稳定的系统性能。特别是在多节点通信的场合,优化数据帧大小可以直接提升整个系统的响应速度。

2. 位域:精细控制每一位的存储

2.1 位域的基本概念

位域是C语言中一个很实用的特性,它允许我们在结构体中按位来定义成员变量。这意味着你可以精确控制每个成员占用的比特数,而不是按照默认的字节对齐方式。

举个例子,假设我们有三个状态标志需要传输:

  • 电机使能状态(1位,0或1)
  • 错误状态(1位,0或1)
  • 工作模式(3位,0-7共8种状态)

用普通结构体可能需要3个字节,但使用位域只需要1个字节:

typedef struct {
    uint8_t motor_enabled : 1;
    uint8_t error_status : 1;
    uint8_t work_mode : 3;
    uint8_t reserved : 3;  // 保留位
} MotorStatus;

这样我们就用1个字节存储了原本需要3个字节的数据,节省了66%的空间!

2.2 位域的内存布局技巧

位域的内存布局有个很重要的特性:编译器会尽量将相邻的位域成员打包在同一个存储单元中,但如果剩余空间不足,就会分配到下一个存储单元。

我踩过的一个坑是位域成员的顺序会影响最终的内存占用。比如这样的定义:

typedef struct {
    uint16_t a : 12;  // 12位
    uint16_t b : 5;   // 5位
    uint16_t c : 3;   // 3位
} DataStruct;

你可能会觉得总共20位,应该占3个字节对吧?但实际上,因为a占用了12位后,第一个16位单元只剩4位,不够放b的5位,所以b会被放到下一个16位单元。最终这个结构体占用了4个字节而不是3个。

通过调整成员顺序,可以优化内存布局:

typedef struct {
    uint16_t a : 12;  // 12位
    uint16_t c : 3;   // 3位
    uint16_t b : 5;   // 5位
} DataStruct;

这样a和c可以放在同一个16位单元中(12+3=15位),b单独放在下一个单元,总共只需要4个字节。

2.3 实际项目中的位域设计

在我最近的智能小车项目中,需要传输这些数据:

  • 4个电机故障标志(各1位)
  • 急停状态(1位)
  • 运行模式(3位)
  • 电池电量(5位,0-31级)
  • 速度值(8位,0-255)

通过精心设计位域结构,只用了3个字节:

typedef struct {
    uint8_t motor1_fault : 1;
    uint8_t motor2_fault : 1;
    uint8_t motor3_fault : 1;
    uint8_t motor4_fault : 1;
    uint8_t emergency_stop : 1;
    uint8_t operation_mode : 3;
    
    uint8_t battery_level : 5;
    uint8_t reserved1 : 3;
    
    uint8_t speed_value;
} VehicleStatus;

3. 共用体:实现数据的高效转换

3.1 共用体的工作原理

共用体(union)是C语言中另一个强大的特性,它允许不同的成员共享同一块内存空间。这意味着改变一个成员的值会影响其他成员的值,因为它们都指向相同的内存地址。

在CAN通信中,共用体特别有用,因为我们需要将位域结构体转换为字节数组进行传输,接收端再转换回结构体形式。

typedef union {
    struct {
        uint8_t status_byte;
        uint8_t data_byte1;
        uint8_t data_byte2;
    } fields;
    
    uint8_t raw_data[3];
} CanDataUnion;

这样,我们可以通过操作fields成员来设置各个数据字段,然后直接使用raw_data数组进行CAN传输。

3.2 位域与共用体的结合使用

这是最强大的组合!我们可以创建包含位域结构体的共用体,既能享受位域的精确定位,又能方便地进行字节数组转换。

typedef struct {
    uint32_t sensor1_value : 10;  // 10位,0-1023
    uint32_t sensor2_value : 10;  // 10位,0-1023
    uint32_t sensor3_enabled : 1; // 1位
    uint32_t sensor4_enabled : 1; // 1位
    uint32_t operation_mode : 3;  // 3位,0-7
    uint32_t reserved : 7;        // 7位保留
} SensorData;

typedef union {
    SensorData data_fields;
    uint8_t can_data[4];
} SensorDataUnion;

这样,32位的位域结构体正好对应4字节的CAN数据,完美匹配!

3.3 处理大小端问题

在实际项目中,我遇到过一个棘手的问题:不同处理器架构的大小端(字节序)可能不同。比如ARM处理器通常是小端模式,而某些嵌入式处理器可能是大端模式。

为了解决这个问题,我通常在共用体中显式定义字节顺序:

typedef union {
    struct {
        uint8_t byte0;
        uint8_t byte1;
        uint8_t byte2;
        uint8_t byte3;
    } bytes;
    
    uint32_t value;
} EndianSafeUnion;

或者在数据传输前进行字节序转换:

uint32_t convert_endian(uint32_t value) {
    return ((value & 0xFF) << 24) |
           ((value & 0xFF00) << 8) |
           ((value & 0xFF0000) >> 8) |
           ((value & 0xFF000000) >> 24);
}

4. 实战:CAN通信数据帧优化

4.1 设计优化的数据结构

让我们看一个完整的实际例子。假设我们需要通过CAN总线传输这些数据:

  • 4个电机状态(各1位)
  • 2个限位开关状态(各1位)
  • 温度值(10位,0-1023)
  • 湿度值(10位,0-1023)
  • 设备ID(6位,0-63)

首先设计位域结构体:

typedef struct {
    // 字节0
    uint32_t motor1_status : 1;
    uint32_t motor2_status : 1;
    uint32_t motor3_status : 1;
    uint32_t motor4_status : 1;
    uint32_t limit_switch1 : 1;
    uint32_t limit_switch2 : 1;
    uint32_t device_id : 6;
    
    // 字节1-2
    uint32_t temperature : 10;
    
    // 字节2-3
    uint32_t humidity : 10;
    uint32_t reserved : 6;  // 保留位
} SensorDataFrame;

这个结构体总共占用32位(4字节),正好是CAN数据帧的一半容量。

4.2 实现CAN发送和接收

创建共用体用于数据转换:

typedef union {
    SensorDataFrame data;
    uint8_t raw_bytes[4];
} CanDataConverter;

发送端代码:

void send_can_data(void) {
    CanDataConverter converter;
    CAN_TxHeaderTypeDef tx_header;
    uint8_t can_data[8] = {0};
    
    // 设置数据字段
    converter.data.motor1_status = get_motor_status(1);
    converter.data.motor2_status = get_motor_status(2);
    converter.data.temperature = read_temperature();
    // 设置其他字段...
    
    // 拷贝到CAN数据缓冲区
    memcpy(can_data, converter.raw_bytes, 4);
    
    // 设置CAN帧头
    tx_header.StdId = 0x123;  // CAN ID
    tx_header.DLC = 4;        // 数据长度
    tx_header.IDE = CAN_ID_STD;
    tx_header.RTR = CAN_RTR_DATA;
    
    // 发送CAN帧
    HAL_CAN_AddTxMessage(&hcan1, &tx_header, can_data, NULL);
}

接收端代码:

void can_receive_callback(uint8_t* received_data) {
    CanDataConverter converter;
    
    // 将接收到的数据拷贝到共用体
    memcpy(converter.raw_bytes, received_data, 4);
    
    // 使用数据
    if (converter.data.motor1_status) {
        handle_motor1_status();
    }
    
    uint16_t temp = converter.data.temperature;
    process_temperature(temp);
    
    // 处理其他数据...
}

4.3 错误处理和数据验证

在实际应用中,数据验证很重要。我通常会在数据帧中添加校验和:

typedef struct {
    // 数据字段...
    uint32_t checksum : 8;  // 8位校验和
} SafeDataFrame;

uint8_t calculate_checksum(const uint8_t* data, size_t length) {
    uint8_t sum = 0;
    for (size_t i = 0; i < length - 1; i++) {
        sum += data[i];
    }
    return sum;
}

在接收端验证校验和:

bool validate_data(const uint8_t* data, size_t length) {
    uint8_t received_checksum = data[length - 1];
    uint8_t calculated_checksum = calculate_checksum(data, length - 1);
    return received_checksum == calculated_checksum;
}

5. 高级技巧与最佳实践

5.1 处理跨字节位域

当位域成员跨越字节边界时,需要特别小心。比如一个10位的成员可能部分在第一个字节,部分在第二个字节。

我通常使用编译器指令来确保内存布局符合预期:

#pragma pack(push, 1)
typedef struct {
    uint16_t first_part : 6;  // 在第一个字节
    uint16_t second_part : 4; // 跨字节
    uint16_t third_part : 5;  // 在第二个字节
} CrossByteField;
#pragma pack(pop)

5.2 使用宏简化位操作

为了代码更清晰,我定义了一些宏来处理位操作:

#define SET_BIT(byte, bit) ((byte) |= (1 << (bit)))
#define CLEAR_BIT(byte, bit) ((byte) &= ~(1 << (bit)))
#define TOGGLE_BIT(byte, bit) ((byte) ^= (1 << (bit)))
#define CHECK_BIT(byte, bit) ((byte) & (1 << (bit)))

5.3 性能优化考虑

虽然位域很强大,但过度使用可能影响性能。因为访问位域需要额外的移位和掩码操作。

在性能关键的代码段,我有时会直接使用位操作:

// 使用位域
data.field.value = 5;

// 使用直接位操作(更快)
data.raw_value = (data.raw_value & ~(0x7 << 3)) | (5 << 3);

5.4 调试技巧

调试位域数据时,十六进制查看很重要。我经常使用这种调试输出:

void debug_print_data(const uint8_t* data, size_t length) {
    for (size_t i = 0; i < length; i++) {
        printf("%02X ", data[i]);
        if ((i + 1) % 8 == 0) printf("\n");
    }
    printf("\n");
}

还可以打印每个位的状态:

void print_bits(uint8_t byte) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (byte >> i) & 1);
        if (i % 4 == 0) printf(" ");
    }
    printf("\n");
}

6. 常见问题与解决方案

6.1 编译器兼容性问题

不同编译器对位域的处理可能略有不同。GCC、Clang、IAR、Keil等编译器在位域的内存布局上可能有细微差别。

解决方案是在关键项目中使用编译器特定的指令:

// GCC
typedef struct {
    uint32_t field1 : 5 __attribute__((packed));
    uint32_t field2 : 3;
} GccStruct;

// IAR
#pragma bitfield_order(forward)
typedef struct {
    uint32_t field1 : 5;
    uint32_t field2 : 3;
} IarStruct;

6.2 位域的范围检查

位域不会自动进行范围检查,如果赋值超过位域容量,会静默截断:

typedef struct {
    uint8_t value : 3;  // 最大值为7
} SmallField;

SmallField field;
field.value = 10;  // 实际存储值为2(10的二进制是1010,取低3位是010,即2)

我通常添加范围检查函数:

bool set_field_value(SmallField* field, uint8_t value) {
    if (value > 7) {
        return false;  // 超出范围
    }
    field->value = value;
    return true;
}

6.3 位域的符号处理

位域可以是有符号的,但需要小心处理:

typedef struct {
    int8_t value : 4;  // 4位有符号,范围-8到7
} SignedField;

SignedField field;
field.value = -5;  // 正确
field.value = 10;  // 错误,实际存储为-6

7. 实际项目案例分享

7.1 工业控制系统

在某个工业控制项目中,我需要通过CAN总线传输16个数字输入状态、8个模拟量值(每个10位)、设备状态和错误代码。通过精心设计位域结构,将所有这些数据压缩到8个字节中:

typedef struct {
    // 数字输入状态(16位)
    uint64_t digital_inputs : 16;
    
    // 模拟量值(8个,每个10位)
    uint64_t analog1 : 10;
    uint64_t analog2 : 10;
    uint64_t analog3 : 10;
    uint64_t analog4 : 10;
    
    // 状态和错误代码
    uint64_t device_status : 4;
    uint64_t error_code : 4;
} IndustrialData;

7.2 汽车电子系统

在汽车电子项目中,需要传输各种传感器数据和车辆状态:

typedef struct {
    uint32_t engine_speed : 12;     // 0-4095 RPM
    uint32_t vehicle_speed : 10;    // 0-1023 km/h
    uint32_t fuel_level : 8;        // 0-255
    uint32_t door_open : 1;         // 0或1
    uint32_t seatbelt_fastened : 1; // 0或1
    uint32_t reserved : 0;          // 填充位
} VehicleData;

7.3 物联网传感器节点

在物联网应用中,需要高效传输传感器数据以节省功耗:

typedef struct {
    uint32_t temperature : 12;  // -2048到2047.9°C,精度0.1°C
    uint32_t humidity : 10;     // 0-102.3%,精度0.1%
    uint32_t pressure : 14;     // 0-16383 hPa,精度1 hPa
    uint32_t battery_level : 5; // 0-31级
    uint32_t sensor_status : 1; // 0或1
} SensorNodeData;

通过这些实际案例,我深刻体会到位域和共用体在嵌入式系统中的价值。它们不仅节省了宝贵的内存和带宽资源,还让代码更加清晰和易于维护。

从SPI到CAN-FD:MCP2518FD如何重塑嵌入式通信的边界与效率 本文探讨了MCP2518FD如何通过SPI接口实现高性能CAN-FD通信,显著提升嵌入式系统的数据传输效率和带宽。结合STM32G4等微控制器,详细解析了硬件设计、配置策略及性能优化技巧,为工业自动化和汽车电子应用提供可靠的通信解决方案。 阅读详情

相关推荐

深入解析CAN与CANFD数据帧:从基础字段到实战应用

本文深入解析了CAN与CANFD数据帧的格式与核心机制。从传统CAN的帧起始、仲裁域、数据域到CRC校验,再到CANFD的速率切换、扩展数据长度与增强校验,文章结合汽车电子与工业控制实战案例,详细阐述了如何利用数据帧知识进行网络优化、故障排查与性能提升。掌握CANFD数据报文格式,是应对现代高数据量、实时性通信挑战的关键。

weixin_29178069的博客 215

嵌入式C语言环境下的CAN总线通讯协议

本文转载在我的微信公众号:古德曼汽车工业。 希望关注本专栏的朋友,也能一并关注微信公众号。 原文地址:嵌入式C语言环境下的CAN总线通讯协议 相信本公众号的读者对CAN通讯协议肯定都不陌生,各位在采集CAN总线信号、分析数据的时候是否想过,这些信号在电控单元(ECU)中通讯协议是如何实现的。本次内容介绍的是如何将CAN总线的通讯协议在C语言环境中实现。下面的内容需要有一点点的C语言基础,...

wusx2004的专栏 3613

CAN总线标准数据帧详解:从帧起始到ACK段的完整解析(附实例)

本文深入解析CAN总线标准数据帧的结构与工作原理,从帧起始到ACK段逐一详解,并结合汽车电子实例说明其应用。涵盖仲裁机制、控制段设计、数据段处理及错误检测等关键技术点,为嵌入式开发提供实用指南。

weixin_30745553的博客 615

跨越芯片的对话:位域与共用体在分布式系统中的协同设计哲学

本文探讨了位域与共用体在分布式系统设计中的协同应用,特别是在CAN通信等板间通信场景下如何实现数据的高效压缩与传输。通过结构体和共用体的精妙组合,解决了资源受限环境中数据表示的挑战,为嵌入式开发提供了实用的设计方法论。

vim8coder的博客 658

从零构建嵌入式通信框架:STM32H750与RM3508电机的CAN协议深度解析

本文深度解析了基于STM32H750微控制器与RM3508无刷电机构建嵌入式通信框架的CAN协议实现。详细介绍了系统架构设计、硬件配置、数据帧解析及通信可靠性保障机制,重点探讨了CAN总线在工业自动化与机器人控制中的实时性能优化策略与系统集成调试技巧。

salt的博客 735

CAN总线数据帧结构深度解析:从位场原理到故障排查实战

CAN总线作为嵌入式与汽车电子领域的核心通信技术,其本质是一种基于差分信号的串行通信协议,通过非破坏性仲裁机制实现多主节点的高可靠实时通信。理解其数据帧结构是掌握CAN通信原理的关键,这涉及从帧起始、仲裁场、控制场、数据场到CRC校验、应答场等逐比特位的功能定义。深入掌握帧结构的技术价值在于,它不仅是进行嵌入式驱动开发(如STM32 bxCAN配置)的基础,更是实现高效故障定位、通信协议解析(如UDS、CANopen)与总线负载优化的前提。在实际应用场景中,无论是汽车电子网络调试、工业控制设备联调,还是充电

weixin_33785108的博客 487

位域设计的艺术:避开内存对齐陷阱,打造高效通信协议

本文深入探讨C语言位域结构体在嵌入式通信协议设计中的高效应用,重点解析如何避开内存对齐陷阱。通过结合共用体实现CAN通信和板间通信的数据打包与解析,提供优化策略和跨平台兼容性解决方案,帮助开发者打造既节省内存又稳定可靠的通信协议。

p1q2r的博客 717

CAN总线与I2C:从汽车电子到嵌入式系统的通信协议实战解析

本文深入解析CAN总线与I2C两大串行通信协议的核心差异与应用场景。CAN总线凭借其高可靠性和抗干扰能力,成为汽车电子和工业自动化的骨干网络;而I2C则以简洁的布线和高集成度,成为嵌入式系统传感器连接的理想选择。文章通过实战案例对比,帮助工程师根据传输距离、速率和可靠性需求正确选型,提升系统设计效率。

study的专栏 823

CAN FD:从数据帧到非线性编码,一场嵌入式通信的‘静默革命’

CAN FD作为传统CAN总线的升级版本,通过数据帧结构优化、非线性DLC编码和增强型CRC校验机制,实现了通信带宽和可靠性的显著提升。本文深度解析CAN FD的技术革新,包括其与传统CAN的区别、多速率切换机制及实际部署中的兼容性考量,为嵌入式系统开发提供关键技术总结。

pea55的博客 773

CAN通讯理论与实践:调试和优化全讲解

举个例子,我曾参与一个项目,某款新能源汽车的电池管理系统(BMS)用CAN连接了上百个传感器,实时监控电池状态,数据传输零延迟,稳定得像个“老干部”。:一次调试中,我们发现一个节点的CAN帧发送频率太高(每10ms发一次8字节帧),导致总线负载飙到90%。想象一下,在一辆车里,几十个电子控制单元(ECU)需要实时“聊天”,CAN就像一个高效的“群聊管理员”,确保消息不丢、不乱。:2015年,某汽车被黑客通过CAN网络远程控制,原因是车载娱乐系统未隔离CAN总线,黑客通过Wi-Fi入侵后直接发送伪造帧。

大模型大数据攻城狮的专栏 2007

深入解析CAN通信:接收、发送与中断处理

htmltable {th, td {th {pre {简介:CAN(Controller Area Network)是一种在汽车电子和工业自动化领域中广泛使用的高可靠性通信协议。本文将探讨CAN协议的接收与发送过程,包括中断处理和滤波设计的关键技术点。首先解释CAN通信的基本原理,然后详细介绍接收和发送CAN消息的过程,并讨论中断服务程序的编写原则和滤波设计的重要作用。通过分析”CANAPP.C”源文件中的代码示例,加深对CAN通信技术在嵌入式系统中应用的理解。

weixin_42181686的博客 5041

嵌入式通信协议完全解析:UART_SPI_I2C_CAN实战应用指南

CAN(Controller Area Network)是一种专门用于工业控制的串行通信协议。CAN的特点:高可靠性:错误检测和纠正机制完善多主架构:任何节点都可以发送数据优先级仲裁:基于消息ID的优先级长距离:可达数千米高速:可达1Mbps✅ 通信协议的基本原理✅ UART串口通信的配置和应用✅ SPI高速数据传输的实现✅ I2C多设备总线管理✅ CAN工业总线应用✅ 协议选型和系统设计✅ 调试技巧和问题诊断✅ 性能优化和错误处理。

Mumyi_的博客 1453

DSP28335基于CAN通信的自测试实现

本文还有配套的精品资源,点击获取 简介:CAN通信协议广泛用于多个领域,以其高可靠性和实时性著称。本压缩包文件包括DSP28335微处理器上的CAN通信自测试程序。DSP28335是一款高性能的数字信号处理器,集成了CAN控制器,适合于嵌入式系统的CAN通信需求。文件内含的CAN自测试功能包括数据帧发送、错误检测、接收确认等,旨在确保CAN网络稳定运行。理解这些内容对于构建...

weixin_30633869的博客 2629

从零构建CAN通信:STM32 HAL库回调机制与电机控制实战解析

本文深入解析基于STM32 HAL库构建CAN通信框架的实战技巧,重点探讨HAL库回调机制在多电机控制中的应用。通过优化数据帧处理策略、设计高效通信协议及安全实现跨模块数据共享,帮助开发者提升系统稳定性和实时性,避免常见陷阱。

efc123456的博客 717

STM32F103ZET6 CAN通信实践指南

本文还有配套的精品资源,点击获取 简介:STM32F103ZET6是基于ARM Cortex-M3内核的微控制器,广泛应用于工业控制、物联网等领域。该微控制器具备CAN总线接口,适用于高效串行通信。本文档集合了CAN总线开发程序的源代码、配置文件和说明文档,特别针对GPIO引脚28375进行配置,以实现CAN通信功能。本资料包提供了一个CAN环回测试程序示例,帮助开发者理解...

weixin_26854475的博客 2510

STM32F103C8T6 CAN总线通信程序实践

STM32F103C8T6是STMicroelectronics(意法半导体)生产的一款高性能的32位ARM Cortex-M3微控制器(MCU),广泛应用于各种嵌入式系统中。凭借其丰富的功能和灵活的配置,这款MCU成为了工业控制、医疗设备、消费类电子产品以及汽车电子等领域的热门选择。其封装小巧,运行速度快,而且具有多种通信接口,使设计者能够轻松实现复杂的应用。控制器局域网络(Controller Area Network,简称CAN)是一种广泛应用于汽车和工业领域的串行通讯总线技术。

weixin_32661831的博客 2381

CAN-FD自适应DLC机制:优化总线负载与数据帧效率实战解析

本文深入解析CAN-FD自适应DLC机制的核心价值与实战应用。通过动态调整数据帧长度,该机制显著优化总线负载与数据传输效率,减少填充字节,提升带宽利用率。文章结合ADAS等车载网络实例,详细探讨其工作原理、配置优化及兼容性挑战,为工程师提供实用的部署策略与性能提升方案。

pea55的博客 870

STM32 CAN通信实践教程:从基础到显示

CAN是一种多主总线技术,最初由德国Bosch公司为汽车领域开发。由于其出色的抗干扰性和可靠性,它在工业自动化和医疗设备中也得到了广泛应用。STM32的CAN模块可以实现CAN 2.0A和2.0B协议,支持标准和扩展帧格式,是实现复杂网络通信的理想选择。CAN总线协议的物理层定义了通信网络中电气特性和信号的物理特性。它是负责在物理媒介(如双绞线)上传输原始比特流的层。物理层负责位流的发送和接收,确保这些位流能够正确地在控制器和物理媒介之间转换。

weixin_33193177的博客 1235
上一篇: 嵌入式软件工程师如何构建核心竞争力:从基础技能到项目实战
下一篇: openbmc ast2600 jffs2挂载mtd5(rwfs)异常定位与SPI驱动修复
普通网友
博客等级 码龄4年 95粉丝 1459原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值