【STM32】软件I2C(支持多字节)

I2C简介

I2C总线是一种串行、半双工的总线,主要用于近距离、低速的芯片之间的通信。I2C总线有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步。

在这里插入图片描述

  • 在一个i2c通讯总线中,可连接多个i2c通讯设备(分为主机和从机)。
  • 主机有权发起和结束一次通信,从机只能被动呼叫。当总线上有多个主机同时启动总线时,i2c也具备冲突检测和仲裁的功能来防止错误产生。
  • 每个连接到i2c总线上的器件都有一个唯一的地址(7bit或者10bit),且每一个器件都可以作为主机也可以作为从机(但同一时刻只能有一个主机)。
  • 串行的8位双向数据传输速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。

I2C协议

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

有几点需要重点说明,下面会详细介绍每一个环节

  • SLAVE_ADDRESS有7bit格式或者10bit格式。
  • 除开起始和终止信号,每次发送或者读取一字节数据后,都需要等待ack或者发送ack。
  • 寄存器地址或者值也肯能占多字节,先发送高字节的数据。
起始和终止信号

在这里插入图片描述

起始信号:当SCL在高电平期间SDA从高电平向低电平切换

终止信号:当SCL在高电平期间SDA从低电平向高电平切换

数据有效性

SDA数据线在SCL的每一个时钟周期传输一位数据。

  • SCL高电平期间:SDA表示的数据有效,此时SDA的电平要稳定,SDA为高电平时表示数据“1”,为低电平时表示数据“0”。

  • SCL低电平期间:SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。

    在这里插入图片描述

    数据和地址按8位进行传输,先传输数据的高位,每次传输的字节数不受限制.

I2C地址及数据方向

I2C总线上的每一个设备都有自己的独立地址,主机发起通讯时,通过SDA信号线发送设备地址(SLAVE_ADDRESS)来查找从机.I2C协议规定设备地址可以是7位或者10位,实际中7位地址应用比较广泛.紧跟设备地址的一个数据位用来表示数据传输方向,第8位或第11位.

  • 数据方向位为"1":表示主机由从机读数据
  • 数据方向位为"0":表示主机向从机写数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5s4ho6wg-1687146230817)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230619104122234.png)]

在这里插入图片描述

10bit slave_address要分两个字节传输,高字节需要补上标志位"11110".

I2C设备寄存器地址和数据

上文中,我们默认设备寄存器地址和数据都是一个字节,但是实际项目中寄存器地址可能是两字节,数据可能是两字节或四字节.这种情况下其实和单字节类似,只不过是将多字节拆分为单字节传输,中间还是需要ACK.

举例,16位寄存器地址和32位数据的读时序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k416fjAD-1687146230820)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230619111806577.png)]

Burst模式

burst模式其实就是连续模式,连续写或者读.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bgcq56P6-1687146230821)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230619112048014.png)]

还是只传输一个地址,但是值可以是多个,写入的地址则是在原地址上递增.

写寄存器时序,就不举例了.就是多接收数据,在不要数据的时候发送NACK,否则还是继续发送ACK.理论上,burst的长度是没有限制,但是实际I2C设备是有限制的,具体就需要看I2C设备的说明了.

应答和非应答信号

I2C的数据或者地址传输都带响应.响应包括"ACK"和"NACK"两种信号.作为数据接收端时,当收到I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送"ACK"信号,发送方会继续发送下一个数据;若接收方希望结束数据传输,则向对方发送"NACK"信号.发送方接收到信号后会产生一个停止信号,结束信号传输.

注意一点,在等待应答信号前,需要释放SDA的控制权,将SDA总线置为高电平.

数据发送端会释放SDA的控制权,由数据接收端控制SDA,给发送端传输应答或非应答信号

  • SDA为高电平:表示非应答信号(NACK)

  • SDA为低电平:表示应答信号(ACK)

软件I2C GPIO引脚配置

这里先说一下,为什么要使用软件模拟i2c,硬件上不是已经实现了i2c吗?我们在stm32中直接将引脚配置成i2c,然后使用hal库不就可以实现i2c通信吗?

理论上是的,但是据说,STM32芯片I2C有bug,所以很少人愿意冒险。

所以,要使用软件模拟I2C的时候,GPIO需要配置为开漏输出,至于为什么这样配置,可以参考(2条消息) I2C 开漏输出与上拉电阻_jiangdf的博客-CSDN博客

软件I2C的实现

软件设计的几个目标

  • GPIO配置由外部实现(其实也想放在这里实现的,但是使能GPIO时钟有点绕)
  • 支持7位或10位SLAVE_ADDRESS
  • 支持多字节寄存器地址或数据
  • 支持多个I2C速率

先看头文件

/*
 * @Date: 2023-06-12 11:03:57
 * @LastEditors: zdk
 * @LastEditTime: 2023-06-16 14:30:05
 * @FilePath: \haptics_evb_h7_bootloaderd:\01Project\haptic\code_new\haptics_evb_h7\haptics_evb_h7\Core\Inc\i2c_sw.h
 */
#ifndef I2C_SW_H
#define I2C_SW_H

#ifdef __cplusplus
#define
extern "c"
{
#endif

#include "gpio.h"
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

#define I2C_WRITE (0x00)
#define I2C_READ  (0x01)
#define I2C_10BIT_SLAVE_ADDRESS_INDICATOR (0b11110)

    typedef enum
    {
        Standard_Mode = 100 * 1000,
        Fast_Mode = 400 * 1000,
        Fast_Mode_Plus = 1000 * 1000,
    } I2C_SW_Speed_Mode_e; //i2c速率 unit Hz

    typedef enum
    {
        One_Byte = 1,
        Two_Byte = 2,
        Four_Byte = 4
    } Data_Width_e;//数据长度(寄存器地址或者值的数据长度)

    typedef enum
    {
        Slave_Address_7Bit,
        Slave_Address_10Bit,
    } Slave_Address_Width_e;//i2c slave address的位宽

    typedef struct
    {
        uint16_t scl_pin;
        uint16_t sda_pin;
        GPIO_TypeDef* scl_port;
        GPIO_TypeDef* sda_port;
        I2C_SW_Speed_Mode_e speed_mode;
        uint16_t slave_addr;
        Slave_Address_Width_e slave_address_width;
        Data_Width_e slave_reg_addr_width;
        Data_Width_e slave_reg_value_width;
    } I2C_SW_Handle_t;

    /**
     * @description: 检测slave address的i2c设备是否存在
     * @param {I2C_SW_Handle_t*} handle
     * @return {*}
     */     
    bool i2c_device_exist(I2C_SW_Handle_t* handle);

    /**
     * @description: 读寄存器
     * @param {I2C_SW_Handle_t*} handle
     * @param {uint32_t} reg_addr
     * @param {uint32_t} len
     * @param {uint32_t*} buf
     * @return {*}
     */    
    int i2c_reg_read(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, uint32_t* buf);

    /**
     * @description: 写寄存器
     * @param {I2C_SW_Handle_t*} handle
     * @param {uint32_t} reg_addr
     * @param {uint32_t} len
     * @param {uint32_t*} buf
     * @return {*}
     */    
    int i2c_reg_write(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, const uint32_t* buf);
    
#ifdef __cplusplus
#define
}
#endif

#endif

再看源文件,源文件中有多个本地函数可以介绍一下

/**
 * @description: 延时函数,用于控制i2c频率
 * @param {I2C_SW_Speed_Mode_e} speed_mode
 * @return {*}
 */
static void i2c_delay(I2C_SW_Speed_Mode_e speed_mode);

/**
 * @description: 将时钟信号拉低
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void scl_l(I2C_SW_Handle_t* handle);

/**
 * @description: 将时钟信号拉高
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void scl_h(I2C_SW_Handle_t* handle);

/**
 * @description: 将数据信号拉低
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void sda_l(I2C_SW_Handle_t* handle);

/**
 * @description: 将数据信号拉高
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void sda_h(I2C_SW_Handle_t* handle);

/**
 * @description: 读取数据信号的电平
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static char sda_read(I2C_SW_Handle_t* handle);


/**
 * @description: 产生i2c起始信号
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void i2c_start(I2C_SW_Handle_t* handle);

/**
 * @description: 产生i2c终止信号
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void i2c_stop(I2C_SW_Handle_t* handle);

/**
 * @description: 产生i2c应答信号
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void i2c_ack(I2C_SW_Handle_t* handle);

/**
 * @description: 产生i2c非应答信号
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static void i2c_no_ack(I2C_SW_Handle_t* handle);

/**
 * @description: 等待i2c应答信号
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static bool i2c_wait_ack(I2C_SW_Handle_t* handle);

/**
 * @description: i2c发送一字节数据
 * @param {I2C_SW_Handle_t*} handle
 * @param {char} data
 * @return {*}
 */
static void i2c_send_byte(I2C_SW_Handle_t* handle, char data);

/**
 * @description: i2c读取一字节数据
 * @param {I2C_SW_Handle_t*} handle
 * @return {*}
 */
static char i2c_recv_byte(I2C_SW_Handle_t* handle);

/**
 * @description: 发送slave address信息
 * @param {I2C_SW_Handle_t*} handle
 * @param {char} rw 读或者写方向
 * @return {*}
 */
static bool i2c_send_slave_address_with_wait_ack(I2C_SW_Handle_t* handle, char rw);

/**
 * @description: 发送要读或写的地址信息
 * @param {I2C_SW_Handle_t*} handle
 * @param {uint32_t} address
 * @return {*}
 */
static bool i2c_send_one_reg_address_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t address);

/**
 * @description: 接收寄存器地址值信息
 * @param {I2C_SW_Handle_t*} handle
 * @param {uint32_t*} value
 * @param {bool} no_ack
 * @return {*}
 */
static bool i2c_recv_one_reg_value_with_ack(I2C_SW_Handle_t* handle, uint32_t* value, bool no_ack);

/**
 * @description: 发送寄存器值信息
 * @param {I2C_SW_Handle_t*} handle
 * @param {uint32_t} value
 * @return {*}
 */
static bool i2c_send_one_reg_value_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t value);

static void i2c_delay(I2C_SW_Speed_Mode_e speed_mode)
{
    uint32_t temp;
    SysTick->LOAD = SystemCoreClock / 8 / (speed_mode * 2) - 1;
    SysTick->VAL = 0X00; //
    SysTick->CTRL = 0X01; //
    do
    {
        temp = SysTick->CTRL; //
    }
    while((temp & 0x01) && (!(temp & (1 << 16)))); //
    SysTick->CTRL = 0x00; //
    SysTick->VAL = 0X00; //
}

static void scl_l(I2C_SW_Handle_t* handle)
{
    HAL_GPIO_WritePin(handle->scl_port, handle->scl_pin, GPIO_PIN_RESET);
}

static void scl_h(I2C_SW_Handle_t* handle)
{
    HAL_GPIO_WritePin(handle->scl_port, handle->scl_pin, GPIO_PIN_SET);
}

static void sda_l(I2C_SW_Handle_t* handle)
{
    HAL_GPIO_WritePin(handle->sda_port, handle->sda_pin, GPIO_PIN_RESET);
}

static void sda_h(I2C_SW_Handle_t* handle)
{
    HAL_GPIO_WritePin(handle->sda_port, handle->sda_pin, GPIO_PIN_SET);
}
static char sda_read(I2C_SW_Handle_t* handle)
{
    return HAL_GPIO_ReadPin(handle->sda_port, handle->sda_pin);
}

static void i2c_start(I2C_SW_Handle_t* handle)
{
    scl_h(handle);
    sda_h(handle);
    i2c_delay(handle->speed_mode);

    sda_l(handle);
    i2c_delay(handle->speed_mode);

    scl_l(handle);
    i2c_delay(handle->speed_mode);
}

static void i2c_stop(I2C_SW_Handle_t* handle)
{
    scl_h(handle);
    i2c_delay(handle->speed_mode);

    sda_l(handle);
    i2c_delay(handle->speed_mode);

    sda_h(handle);
    i2c_delay(handle->speed_mode);
}

static void i2c_ack(I2C_SW_Handle_t* handle)
{
    scl_l(handle);
    i2c_delay(handle->speed_mode);

    sda_l(handle);
    i2c_delay(handle->speed_mode);

    scl_h(handle);
    i2c_delay(handle->speed_mode);

    scl_l(handle);
    i2c_delay(handle->speed_mode);

    sda_h(handle);
    i2c_delay(handle->speed_mode);
}

static void i2c_no_ack(I2C_SW_Handle_t* handle)
{
    sda_h(handle);
    i2c_delay(handle->speed_mode);

    scl_h(handle);
    i2c_delay(handle->speed_mode);

    scl_l(handle);
    i2c_delay(handle->speed_mode);
}

static bool i2c_wait_ack(I2C_SW_Handle_t* handle)
{
    sda_h(handle);
    i2c_delay(handle->speed_mode);

    scl_h(handle);
    i2c_delay(handle->speed_mode);

    int retry_cnt = 10;
    while(retry_cnt--)
    {
        if(!sda_read(handle))
        {
            scl_l(handle);
            i2c_delay(handle->speed_mode);
            return true;
        }
    }
    scl_l(handle);
    i2c_delay(handle->speed_mode);

    return false;
}

static void i2c_send_byte(I2C_SW_Handle_t* handle, char data)
{
    for(int offset = 0; offset < 8; offset++)
    {
        scl_l(handle);
        i2c_delay(handle->speed_mode);

        if(data & 0x80)
            sda_h(handle);
        else
            sda_l(handle);
        i2c_delay(handle->speed_mode);

        scl_h(handle);
        i2c_delay(handle->speed_mode);

        data <<= 1;
    }

    scl_l(handle);
    i2c_delay(handle->speed_mode);
}

static char i2c_recv_byte(I2C_SW_Handle_t* handle)
{
    sda_h(handle);
    i2c_delay(handle->speed_mode);

    char value = 0;
    for(int offset = 0; offset < 8; offset++)
    {
        scl_h(handle);
        i2c_delay(handle->speed_mode);

        value <<= 1;
        if(sda_read(handle))
            value++;

        scl_l(handle);
        i2c_delay(handle->speed_mode);
    }

    return value;
}

static bool i2c_send_slave_address_with_wait_ack(I2C_SW_Handle_t* handle, char rw)
{
    if(handle->slave_address_width == Slave_Address_7Bit)
    {
        i2c_send_byte(handle, (handle->slave_addr << 1) | rw);
        if(!i2c_wait_ack(handle))
            return false;
    }
    else if(handle->slave_address_width == Slave_Address_10Bit)
    {
        uint8_t h_addr = (I2C_10BIT_SLAVE_ADDRESS_INDICATOR << 7)
                         | ((handle->slave_addr >> 8 & 0x03) << 2) | rw;
        i2c_send_byte(handle, h_addr);
        if(!i2c_wait_ack(handle))
            return false;

        uint8_t l_addr = handle->slave_addr & 0xff;
        i2c_send_byte(handle, l_addr);
        if(!i2c_wait_ack(handle))
            return false;
    }
    return true;
}

static bool i2c_send_one_reg_address_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t address)
{
    for(int j = 0; j < handle->slave_reg_addr_width; j++)
    {
        uint8_t offset = (handle->slave_reg_addr_width - 1 - j) * 8;
        uint8_t temp_reg_addr = (address >> offset) & 0xff;

        i2c_send_byte(handle, temp_reg_addr);
        if(!i2c_wait_ack(handle))
            return false;
    }
    return true;
}

static bool i2c_recv_one_reg_value_with_ack(I2C_SW_Handle_t* handle, uint32_t* value, bool no_ack)
{
    for(int k = 0; k < handle->slave_reg_value_width; k++)
    {
        uint8_t offset = (handle->slave_reg_value_width - 1 - k) * 8;
        *value |= (i2c_recv_byte(handle) << offset);

        if(no_ack)
            i2c_no_ack(handle);
        else
            i2c_ack(handle);
    }
    return true;
}

static bool i2c_send_one_reg_value_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t value)
{
    for(int k = 0; k < handle->slave_reg_value_width; k++)
    {
        uint8_t offset = (handle->slave_reg_value_width - 1 - k) * 8;

        i2c_send_byte(handle, (value >> offset) & 0xff);
        if(!i2c_wait_ack(handle))
            return false;
    }
    return true;
}

我们可以看到,比其他博客多了好几个函数,那些都是因为需要兼容多字节寄存器地址或者数据使用的.其中i2c_delay打算另外写一篇文章详细介绍(但是目前好像只在100MHz的时候比较准)。
2023.6.27更新,见文章:软件I2C控制频率

对外接口就是这几个:

bool i2c_device_exist(I2C_SW_Handle_t* handle)
{
    bool ret = 0;

    i2c_start(handle);

    ret = i2c_send_slave_address_with_wait_ack(handle, I2C_WRITE);

    i2c_stop(handle);

    return ret;
}

int i2c_reg_read(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, uint32_t* buf)
{
    i2c_start(handle);

    if(!i2c_send_slave_address_with_wait_ack(handle, I2C_WRITE))
        goto fail;

    if(!i2c_send_one_reg_address_with_wait_ack(handle, reg_addr))
        goto fail;

    i2c_start(handle);

    if(!i2c_send_slave_address_with_wait_ack(handle, I2C_READ))
        goto fail;

    for(int i = 0; i < len; i++)
    {
        i2c_recv_one_reg_value_with_ack(handle, &buf[i], len - 1 == i);
    }
    i2c_stop(handle);
    return 0;

fail:
    i2c_stop(handle);
    return -1;
}

int i2c_reg_write(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, const uint32_t* buf)
{
    i2c_start(handle);

    if(!i2c_send_slave_address_with_wait_ack(handle, I2C_WRITE))
        goto fail;

    if(!i2c_send_one_reg_address_with_wait_ack(handle, reg_addr))
        goto fail;

    for(int i = 0; i < len; i++)
    {
        if(! i2c_send_one_reg_value_with_wait_ack(handle, buf[i]))
            goto fail;
    }

    i2c_stop(handle);
    return 0;

fail:
    i2c_stop(handle);
    return -1;
}

那么以上一个软件I2C功能就完成了.

软件I2C的使用

首先,在STM32CubeMX中配置GPIO.

在这里插入图片描述

然后,实例化软件I2C句柄对象.

    I2C_SW_Handle_t* handle;

    handle = malloc(sizeof(I2C_SW_Handle_t));
    handle->scl_pin = TEMP_I2C_SCL_Pin;
    handle->scl_port = TEMP_I2C_SCL_GPIO_Port;
    handle->sda_pin = TEMP_I2C_SDA_Pin;
    handle->sda_port = TEMP_I2C_SDA_GPIO_Port;
    handle->slave_addr = TMP117_I2C_ADDRESS;
	handle->slave_address_width=Slave_Address_7Bit;
    handle->speed_mode = Standard_Mode;
    handle->slave_reg_addr_width = One_Byte;
    handle->slave_reg_value_width = Two_Byte;

最后,调用接口

    if(i2c_device_exist(handle))
        printf("hello i2c_address=0x%02x is exist\r\n", handle->slave_addr);

    uint32_t device_id = 0;
    if(0 == i2c_reg_read(handle, 0x0f, 1, &device_id))
        printf("device_id=0x%04x \r\n", device_id);

    uint32_t temp = 0;
    while(1)
    {
        if(0 == i2c_reg_read(handle, 0x00, 1, &temp))
        {
            printf("read_value=0x%04x  read_temp=%0.5f\r\n", temp, (int32_t)temp * 0.0078125);
        }
    }
  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32软件I2C HAL库是基于STM32 HAL库的一个软件实现的I2C驱动库。它允许开发者在没有硬件I2C接口的情况下,通过GPIO引脚模拟实现I2C通信。 使用软件I2C HAL库,首先需要在STM32CubeMX中配置相应的GPIO引脚作为SDA和SCL线,并启用软件I2C功能。然后,在代码中使用HAL库提供的函数进行初始化、读取和写入操作。 以下是一个简单的示例代码,展示了如何使用软件I2C HAL库进行初始化和读取操作: ```c #include "stm32f4xx_hal.h" I2C_HandleTypeDef hi2c; void I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } void I2C_Init(void) { I2C_GPIO_Init(); hi2c.Instance = I2C1; hi2c.Init.ClockSpeed = 100000; hi2c.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c.Init.OwnAddress1 = 0x00; hi2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c.Init.OwnAddress2 = 0x00; hi2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c) != HAL_OK) { Error_Handler(); } } void I2C_Read(uint8_t address, uint8_t reg, uint8_t *data, uint16_t size) { HAL_I2C_Mem_Read(&hi2c, address, reg, I2C_MEMADD_SIZE_8BIT, data, size, HAL_MAX_DELAY); } int main(void) { HAL_Init(); I2C_Init(); uint8_t data[4]; I2C_Read(0x50, 0x00, data, 4); while (1) { // Your code here } } ``` 在上面的示例代码中,首先通过`I2C_GPIO_Init()`函数初始化了GPIO引脚,然后通过`I2C_Init()`函数初始化了I2C总线。最后,通过`I2C_Read()`函数读取了从地址0x50开始的4个字节数据。 请注意,以上仅为简化的示例代码,实际使用时需要根据具体的硬件和需求进行相应的配置和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值