CAN --- 控制器局域网总线

一.CAN基础知识介绍

<一>.CAN介绍

1.什么是CAN

CAN(Controller Area Network),是ISO国际标准化的串行通信协议

为了满足汽车产业的“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需求。

  • 低速CAN(ISO11519)通信速率10~125Kbps,总线长度可达1000米;
  • 高速(经典)CAN(ISO11898)通信速率125Kbps~1Mbps,总线长度≤40米;
  • CAN FD通信速率可达5Mbps,并且兼并经典CAN,遵循ISO 11898-1做数据收发。

CAN总线拓扑图 

高速CAN是一个闭环总线。

终端电阻,用于阻抗匹配,以减少回波反射,CAN总线由两根线(CANL和CANH)组成,允许挂载多个设备节点(低速CAN:20和高速CAN:30)。

2.CAN总线特点

  • 多主控制:每个设备都可以主动发送数据;
  • 系统的柔软性:没有类似地址的信息,添加设备不改变原来总线的状态;
  • 通信速度:速度快,距离远;
  • 错误检测&错误通知&错误恢复功能;
  • 故障封闭:判断故障类型,并且进行隔离;
  • 连接节点多:速度与数量找个平衡。

3.CAN应用场景

CAN总线协议已广泛应用在汽车电子、工业自动化、船舶、医疗设备、工业设备等方面。

<二>.CAN物理层

1.CAN物理层特性

  • CAN使用差分信号进行数据传输,根据CAN_H和CAN_L上的电位差来判断总线电平;
  • 总线电平分为显性电平(逻辑0)和隐性电平(逻辑1),二者必居其一;
  • 显性电平具有优先权;
  • 发送方通过使总线电平发生变化,将消息发送给接收方。

 

电平高速CAN低速CAN
显性电平(0)U(CAN_H) - U(CAN_L) = 2VU(CAN_H) - U(CAN_L) = 3V
隐性电平(1)U(CAN_H) - U(CAN_L) = 0VU(CAN_H) - U(CAN_L) = -1,.5V

2.CAN收发器芯片介绍

CAN收发器芯片:TJA1050、TJA1042、SIT1050T(支持高速CAN,传输速率可达1Mbps)。

D:CAN发送引脚;           R:CAN接收引脚;

CANL:低电平CAN电压输入输出端;            CANH:高电位CAN电压输入输出端;

                  Vref:参考电压输出;                              RS:高速/静音模式选择(低电平为高速)。

<三>.CAN协议层

1.CAN帧种类介绍

2.CAN数据帧介绍(最常用)

3.CAN位时序介绍

CAN总线以“位同步”机制,实现对电平的正确采样。位数据都由四段组成:同步段(SS)、传播时间段(PTS)、相位缓冲段1(PSB1)和相位缓冲段2(PSB2),每段又由多个位时序Tq组成。

采样点是指读取总线电平,并将读到的电平作为位值的点,根据位时序,就可以计算CAN通信的波特率。 

注意:节点监测到总线上信号的跳变在SS段范围内(采样点的前提),表示节点与总线的时序是同步,此时采样点的电平即该位的电平。

<1>.数据同步过程

CAN为了实现对总线电平信号的正确采样,数据同步分为硬件同步和再同步。

硬件同步

节点通过CAN总线发送数据,一开始发送帧起始信号。总线上其他节点会检测帧起始信号在不在位数据的SS段内,判断内部时序与总线是否同步。

假如不在SS段内,这种情况下,采样点获得的电平状态是不正确的。所以,节点会使用硬件同步方式调整,把自己的SS段平移到检测到边沿的地方,获得同步,同步情况下,采样点获得的电平状态才是正确的。

再同步 

再同步利用普通数据位的边沿信号(帧起始信号是特殊的边沿信号)进行同步。

再同步的方式分为两种情况:超前和滞后,即边沿信号与SS段的相对位置。

再同步时,PSB1和PSB2中增加或者减少的时间被称为“再同步补偿宽度(SJW)”,其范围:1~4Tq。

限制了SJW值后,再同步时,不能增加限定长度的SJW值。 SJW值较大时,吸收误差能力更强,但是通讯速度会下降。

4.CAN总线仲裁(优先级决定)

CAN总线处于空闲状态,最先开始发送消息的单元获得发送权。

多个单元同时开始发送时,从仲裁段(报文ID)的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送,即首先出现隐性电平的单元失去对总线的占有权变为接收。

竞争失败单元,会自动检测总线空闲,在第一时间再次尝试发送。 

二.STM32 CAN控制器介绍

<一>CAN控制器介绍

STM32 CAN控制器(bxCAN),支持CAN 2.0A和CAN 2.0B Active版本协议。

CAN 2.0A 只能处理标准数据帧且扩展帧的内容会识别错误,而CAN 2.0B Active 可以处理标准数据帧和扩展数据帧。CAN 2.0B Passive只能处理标准数据帧且扩展帧的内容会忽略。

BxCAN主要特点:

  • 波特率最高可达1M bps;
  • 支持时间触发通信(CAN的硬件内部定时器可以在TX/RX的帧起始位的采样点位置生成时间戳);
  • 具有3级发送邮箱;
  • 具有3级深度的2个接收FIFO;
  • 可变的过滤器组(最多28个)。

<二>CAN控制器模式

CAN控制器的工作模式有三种:初始化模式、正常模式和睡眠模式。

CAN控制器的测试模式有三种:静默模式、环回模式和环回静默模式。(初始化模式下进行配置)

<三>CAN控制器框图(F1)

接收过滤器

当总线上报文数据量很大时,总线上的设备会频繁获取报文,占用CPU。过滤器的存在,选择性接收有效报文,减轻系统负担。

每个过滤器组都有两个32位寄存器CAN_FxR1和CAN_FxR2。根据过滤器组的工作模式不同(位宽和选择模式),寄存器的作用不尽相同。

位宽可设置32位或16位,寄存器存储的内容就有所区别。

过滤器组Reg32位16位(寄存器由两部分组成)
CAN_FxR1STDID[10:0]、EXTID[17:0]、IDE、RTRSTDID[10:0]、EXTID[17:15]、IDE、RTR
CAN_FxR2STDID[10:0]、EXTID[17:0]、IDE、RTRSTDID[10:0]、EXTID[17:15]、IDE、RTR

选择模式可设置屏蔽位模式标识符列表模式,寄存器内容的功能就有所区别。

屏蔽位模式,可以选择出一组符合条件的报文。寄存器内容功能相当于是否符合条件。

注意:REG中bit值代表的是匹配与否:1必须匹配,0不用关心。

<四>CAN控制器位时序

 

三.CAN相关寄存器介绍

 

 

四.CAN 相关HAL库驱动介绍

驱动函数关联寄存器功能描述
__HAL_RCC_CANx_CLK_ENBALE()使能CAN时钟
HAL_CAN_Init()MCR/BTR初始化CAN
HAL_CAN_ConfigFilter()过滤器寄存器配置CAN接收过滤器
HAL_CAN_Start()MCR/MSR启动CAN设备
HAL_CAN_ActivateNotification()IER使能中断
__HAL_CAN_ENABLE_IT()IER使能CAN中断允许
HAL_CAN_AddTxMessages()TSR/TIxTR/TDTxR/TDLxR/TDHxR发送消息
HAL_CAN_GetTxMailboxesFreeLevel()TSR等待发送完成
HAL_CAN_GetRxFifoFillLevel()RF0R/RF1R等待接收完成
HAL_CAN_GetRxMessage()RF0R/RF1R/TDLxR/TDHxR接收消息

 

五.CAN基本驱动步骤

<一>.CAN参数初始化 --- 工作模式、波特率等(HAL_CAN_Init)

<二>.使能CAN时钟和初始化相关引脚 --- GPIO模式设为复用功能模式(HAL_CAN_MspInit)

<三>.设置过滤器 --- HAL_CAN_ConfigFilter 完成过滤器的初始化

<四>.CAN数据接收和发送 --- HAL_CAN_AddTxMessage 发送消息;HAL_CAN_GetRxMessage 接收消息

<五>.使能CAN相关中断/设置NVIC/编写中断服务函数(可选) --- __HAL_CAN_ENABLE_IT

六.代码

使用回环模式实现自发自收

can.c

#include "./BSP/CAN/can.h"
#include "./BSP/LED/led.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"

CAN_HandleTypeDef   g_canx_handler;     /* CANx句柄 */
CAN_TxHeaderTypeDef g_canx_txheader;    /* 发送参数句柄 */
CAN_RxHeaderTypeDef g_canx_rxheader;    /* 接收参数句柄 */

/**
 * @brief       CAN初始化
 * @param       tsjw    : 重新同步跳跃时间单元.范围: 1~3;
 * @param       tbs2    : 时间段2的时间单元.范围: 1~8;
 * @param       tbs1    : 时间段1的时间单元.范围: 1~16;
 * @param       brp     : 波特率分频器.范围: 1~1024;
 *   @note      以上4个参数, 在函数内部会减1, 所以, 任何一个参数都不能等于0
 *              CAN挂在APB1上面, 其输入时钟频率为 Fpclk1 = PCLK1 = 36Mhz
 *              tq     = brp * tpclk1;
 *              波特率 = Fpclk1 / ((tbs1 + tbs2 + 1) * brp);
 *              我们设置 can_init(1, 8, 9, 4, 1), 则CAN波特率为:
 *              36M / ((8 + 9 + 1) * 4) = 500Kbps
 *
 * @param       mode    : CAN_MODE_NORMAL,  正常模式;
                          CAN_MODE_LOOPBACK,回环模式;
 * @retval      0,  初始化成功; 其他, 初始化失败;
 */
uint8_t can_init(uint32_t tsjw, uint32_t tbs2, uint32_t tbs1, uint16_t brp, uint32_t mode)
{
  g_canx_handler.Instance = CAN1;
  g_canx_handler.Init.Prescaler = brp;                /* 分频系数(Fdiv)为brp+1 */
  g_canx_handler.Init.Mode = mode;                    /* 模式设置 */
  g_canx_handler.Init.SyncJumpWidth = tsjw;           /* 重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1TQ~CAN_SJW_4TQ */
  g_canx_handler.Init.TimeSeg1 = tbs1;                /* tbs1范围CAN_BS1_1TQ~CAN_BS1_16TQ */
  g_canx_handler.Init.TimeSeg2 = tbs2;                /* tbs2范围CAN_BS2_1TQ~CAN_BS2_8TQ */
  g_canx_handler.Init.TimeTriggeredMode = DISABLE;    /* 非时间触发通信模式 */
  g_canx_handler.Init.AutoBusOff = DISABLE;           /* 软件自动离线管理 */
  g_canx_handler.Init.AutoWakeUp = DISABLE;           /* 睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位) */
  g_canx_handler.Init.AutoRetransmission = ENABLE;    /* 禁止报文自动传送 */
  g_canx_handler.Init.ReceiveFifoLocked = DISABLE;    /* 报文不锁定,新的覆盖旧的 */
  g_canx_handler.Init.TransmitFifoPriority = DISABLE; /* 优先级由报文标识符决定 */
    
  if (HAL_CAN_Init(&g_canx_handler) != HAL_OK)
  {
    return 1;
  }

#if CAN_RX0_INT_ENABLE
  /* 使用中断接收 */
  __HAL_CAN_ENABLE_IT(&g_canx_handler, CAN_IT_RX_FIFO0_MSG_PENDING); /* FIFO0消息挂号中断允许 */
  HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);                          /* 使能CAN中断 */
  HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 1, 0);                  /* 抢占优先级1,子优先级0 */
#endif

  CAN_FilterTypeDef sFilterConfig;

  /*配置CAN过滤器*/
  sFilterConfig.FilterBank = 0;                         /* 过滤器0 */
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;     /* 标识符屏蔽位模式 */
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;    /* 长度32位位宽*/
  sFilterConfig.FilterIdHigh = 0x0000;                  /* 32位ID */
  sFilterConfig.FilterIdLow = 0x0000;
  sFilterConfig.FilterMaskIdHigh = 0x0000;              /* 32位MASK */
  sFilterConfig.FilterMaskIdLow = 0x0000;
  sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;    /* 过滤器0关联到FIFO0 */
  sFilterConfig.FilterActivation = CAN_FILTER_ENABLE;       /* 激活滤波器0 */
  sFilterConfig.SlaveStartFilterBank = 14;

  /* 过滤器配置 */
  if (HAL_CAN_ConfigFilter(&g_canx_handler, &sFilterConfig) != HAL_OK)
  {
    return 2;
  }

  /* 启动CAN外围设备 */
  if (HAL_CAN_Start(&g_canx_handler) != HAL_OK)
  {
    return 3;
  }

  return 0;
}

/**
 * @brief       CAN底层驱动,引脚配置,时钟配置,中断配置
                此函数会被HAL_CAN_Init()调用
 * @param       hcan:CAN句柄
 * @retval      无
 */
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan)
{
  if (CAN1 == hcan->Instance)
  {
    CAN_RX_GPIO_CLK_ENABLE();       /* CAN_RX脚时钟使能 */
    CAN_TX_GPIO_CLK_ENABLE();       /* CAN_TX脚时钟使能 */
    __HAL_RCC_CAN1_CLK_ENABLE();    /* 使能CAN1时钟 */

    GPIO_InitTypeDef gpio_initure;

    gpio_initure.Pin = CAN_TX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(CAN_TX_GPIO_PORT, &gpio_initure); /* CAN_TX脚 模式设置 */

    gpio_initure.Pin = CAN_RX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_INPUT;
    HAL_GPIO_Init(CAN_RX_GPIO_PORT, &gpio_initure); /* CAN_RX脚 必须设置成输入模式 */
  }
}

#if CAN_RX0_INT_ENABLE /* 使能RX0中断 */

/**
 * @brief       CAN RX0 中断服务函数
 *   @note      处理CAN FIFO0的接收中断
 * @param       无
 * @retval      无
 */
void USB_LP_CAN1_RX0_IRQHandler(void)
{
  uint8_t rxbuf[8];
  uint32_t id;
  uint8_t ide, rtr, len;
    
  can_receive_msg(id, rxbuf);
  printf("id:%d\r\n", g_canx_rxheader.StdId);
  printf("ide:%d\r\n", g_canx_rxheader.IDE);
  printf("rtr:%d\r\n", g_canx_rxheader.RTR);
  printf("len:%d\r\n", g_canx_rxheader.DLC);
  printf("rxbuf[0]:%d\r\n", rxbuf[0]);
  printf("rxbuf[1]:%d\r\n", rxbuf[1]);
  printf("rxbuf[2]:%d\r\n", rxbuf[2]);
  printf("rxbuf[3]:%d\r\n", rxbuf[3]);
  printf("rxbuf[4]:%d\r\n", rxbuf[4]);
  printf("rxbuf[5]:%d\r\n", rxbuf[5]);
  printf("rxbuf[6]:%d\r\n", rxbuf[6]);
  printf("rxbuf[7]:%d\r\n", rxbuf[7]);
}

#endif

/**
 * @brief       CAN 发送一组数据
 *   @note      发送格式固定为: 标准ID, 数据帧
 * @param       id      : 标准ID(11位)
 * @retval      发送状态 0, 成功; 1, 失败;
 */
uint8_t can_send_msg(uint32_t id, uint8_t *msg, uint8_t len)
{
  uint32_t TxMailbox = CAN_TX_MAILBOX0;
    
  g_canx_txheader.StdId = id;         /* 标准标识符 */
  g_canx_txheader.ExtId = id;         /* 扩展标识符(29位) 标准标识符情况下,该成员无效*/
  g_canx_txheader.IDE = CAN_ID_STD;   /* 使用标准标识符 */
  g_canx_txheader.RTR = CAN_RTR_DATA; /* 数据帧 */
  g_canx_txheader.DLC = len;

  if (HAL_CAN_AddTxMessage(&g_canx_handler, &g_canx_txheader, msg, &TxMailbox) != HAL_OK) /* 发送消息 */
  {
    return 1;
  }
  
  while (HAL_CAN_GetTxMailboxesFreeLevel(&g_canx_handler) != 3); /* 等待发送完成,所有邮箱(有三个邮箱)为空 */
  
  return 0;
}

/**
 * @brief       CAN 接收数据查询
 *   @note      接收数据格式固定为: 标准ID, 数据帧
 * @param       id      : 要查询的 标准ID(11位)
 * @param       buf     : 数据缓存区
 * @retval      接收结果
 *   @arg       0   , 无数据被接收到;
 *   @arg       其他, 接收的数据长度
 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf)
{
  if (HAL_CAN_GetRxFifoFillLevel(&g_canx_handler, CAN_RX_FIFO0) == 0)     /* 没有接收到数据 */
  {
    return 0;
  }

  if (HAL_CAN_GetRxMessage(&g_canx_handler, CAN_RX_FIFO0, &g_canx_rxheader, buf) != HAL_OK)  /* 读取数据 */
  {
    return 0;
  }
  
  if (g_canx_rxheader.StdId!= id || g_canx_rxheader.IDE != CAN_ID_STD || g_canx_rxheader.RTR != CAN_RTR_DATA)       /* 接收到的ID不对 / 不是标准帧 / 不是数据帧 */
  {
    return 0;    
  }

  return g_canx_rxheader.DLC;
}

can.h

#ifndef __CAN_H
#define __CAN_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* CAN 引脚 定义 */

#define CAN_RX_GPIO_PORT                GPIOA
#define CAN_RX_GPIO_PIN                 GPIO_PIN_11
#define CAN_RX_GPIO_CLK_ENABLE()        do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

#define CAN_TX_GPIO_PORT                GPIOA
#define CAN_TX_GPIO_PIN                 GPIO_PIN_12
#define CAN_TX_GPIO_CLK_ENABLE()        do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

/******************************************************************************************/

/* CAN接收RX0中断使能 */
#define CAN_RX0_INT_ENABLE      0               /* 0,不使能; 1,使能; */

/* 对外接口函数 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf);             /* CAN接收数据, 查询 */
uint8_t can_send_msg(uint32_t id, uint8_t *msg, uint8_t len);   /* CAN发送数据 */
uint8_t can_init(uint32_t tsjw,uint32_t tbs2,uint32_t tbs1,uint16_t brp,uint32_t mode); /* CAN初始化 */

#endif

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/CAN/can.h"


int main(void)
{
    uint8_t key;
    uint8_t i = 0, t = 0;
    uint8_t cnt = 0;
    uint8_t canbuf[8];
    uint8_t rxlen = 0;
    uint8_t res;
    uint8_t mode = 1; /* CAN工作模式: 0,正常模式; 1,回环模式 */

    HAL_Init();                                                            /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);                                    /* 设置时钟, 72Mhz */
    delay_init(72);                                                        /* 延时初始化 */
    usart_init(115200);                                                    /* 串口初始化为115200 */
    usmart_dev.init(72);                                                   /* 初始化USMART */
    led_init();                                                            /* 初始化LED */
    lcd_init();                                                            /* 初始化LCD */
    key_init();                                                            /* 初始化按键 */
    can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ, 4, CAN_MODE_LOOPBACK); /* CAN初始化, 回环模式, 波特率500Kbps */

    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "CAN TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "LoopBack Mode", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY0:Send KEK_UP:Mode", RED); /* 显示提示信息 */

    lcd_show_string(30, 150, 200, 16, 16, "Count:", RED);        /* 显示当前计数值 */
    lcd_show_string(30, 170, 200, 16, 16, "Send Data:", RED);    /* 提示发送的数据 */
    lcd_show_string(30, 230, 200, 16, 16, "Receive Data:", RED); /* 提示接收到的数据 */

    while (1)
    {
        key = key_scan(0);

        if (key == KEY0_PRES) /* KEY0按下,发送一次数据 */
        {
            for (i = 0; i < 8; i++)
            {
                canbuf[i] = cnt + i; /* 填充发送缓冲区 */

                if (i < 4)
                {
                    lcd_show_xnum(30 + i * 32, 190, canbuf[i], 3, 16, 0X80, BLUE); /* 显示数据 */
                }
                else
                {
                    lcd_show_xnum(30 + (i - 4) * 32, 210, canbuf[i], 3, 16, 0X80, BLUE); /* 显示数据 */
                }
            }

            res = can_send_msg(0X12, canbuf, 8); /* ID = 0X12, 发送8个字节 */

            if (res)
            {
                lcd_show_string(30 + 80, 170, 200, 16, 16, "Failed", BLUE); /* 提示发送失败 */
            }
            else
            {
                lcd_show_string(30 + 80, 170, 200, 16, 16, "OK    ", BLUE); /* 提示发送成功 */
            }
        }
        else if (key == WKUP_PRES) /* WK_UP按下,改变CAN的工作模式 */
        {
            mode = !mode;

            if (mode == 0) /* 正常模式,需要2个开发板 */
            {
                can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ, 4, CAN_MODE_NORMAL);    /* CAN正常模式初始化, 正常模式, 波特率500Kbps */
                lcd_show_string(30, 110, 200, 16, 16, "Nnormal Mode ", RED);
            }
            else /* 回环模式,一个开发板就可以测试了. */
            {
                can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ, 4, CAN_MODE_LOOPBACK);  /* CAN回环模式初始化, 回环模式, 波特率500Kbps */
                lcd_show_string(30, 110, 200, 16, 16, "LoopBack Mode", RED);
            }
        }

        rxlen = can_receive_msg(0X12, canbuf); /* CAN ID = 0X12, 接收数据查询 */

        if (rxlen) /* 接收到有数据 */
        {
            lcd_fill(30, 270, 130, 310, WHITE); /* 清除之前的显示 */

            for (i = 0; i < rxlen; i++)
            {
                if (i < 4)
                {
                    lcd_show_xnum(30 + i * 32, 250, canbuf[i], 3, 16, 0X80, BLUE); /* 显示数据 */
                }
                else
                {
                    lcd_show_xnum(30 + (i - 4) * 32, 270, canbuf[i], 3, 16, 0X80, BLUE); /* 显示数据 */
                }
            }
        }

        t++;
        delay_ms(10);

        if (t == 20)
        {
            LED0_TOGGLE(); /* 提示系统正在运行 */
            t = 0;
            cnt++;
            lcd_show_xnum(30 + 48, 150, cnt, 3, 16, 0X80, BLUE); /* 显示数据 */
        }
    }
}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值