[1] rt-thread nano移植 - 基于STM32H750XBH+STM32Cubemx+HAL库+KEIL

gitee代码:

https://gitee.com/xuwenqiang_1989/rt-thread-nano

本移植参考了【野火rt-thread内核应用开发】

1.简介

RT-Thread Nano 是一个极简版的硬实时内核,它是由 C 语言开发,采用面向对象的编程思维,具有良好的代码风格,是一款可裁剪的、抢占式实时多任务的 RTOS。其内存资源占用极小,功能包括任务处理、软件定时器、信号量、邮箱和实时调度等相对完整的实时操作系统特性。适用于家电、消费电子、医疗设备、工控等领域大量使用的 32 位 ARM 入门级 MCU 的场合。

2.软件准备

STM32Cubemx 下载地址:

https://www.st.com/zh/development-tools/stm32cubemx.html

KEIL下载地址:

https://www.keil.com/demo/eval/arm.htm

开发板芯片为STM32H750XBH6,提前安装好安装Keil.STM32H7xx_DFP.3.1.1.pack系列器件支持包

支持包下载地址:

https://www.keil.arm.com/packs/stm32h7xx_dfp-keil/boards/

3.硬件准备

EVB_AIoT_STM32开发板,STM32H750XBH6主控芯片

4.rt-thread nano移植

4.1.新建裸机工程

参考【开放原子训练营(第四季)TobudOS-基于STM32H750XBH+STM32CubeMX+HAL库+KEIL 移植 TobudOS】中3.1部分。

http://t.csdnimg.cn/cxPay

本工程中引脚对应:

UART5_RX = PB5

UART5_TX = PB13

LED1_OUT = PB1

LED2_OUT = PC13

KEY1_IN = PJ3

KEY2_IN = PJ4

注意取消生成中断源有变化,多取消了Memory management fault

4.2.设置KEIL

参考【开放原子训练营(第四季)TobudOS-基于STM32H750XBH+STM32CubeMX+HAL库+KEIL 移植 TobudOS】中3.2部分。

http://t.csdnimg.cn/cxPay

4.3.准备rt-thread nano内核源码

rt-thread nano v3.1.5  下载地址:

https://github.com/RT-Thread/rtthread-nano/archive/refs/heads/master.zip

4.4. rt-thread nano 内核移植

4.4.1.新建文件夹

在工程下新建user和rt-thread文件夹。

user用于存放用户驱动,rt-thread用于存放内核。

将rt-thread内核代码复制到新建的rt-thread文件夹中

将rt-thread\bsp\board.c和rt-thread\bsp\rtconfig.h复制到user文件夹中。

board.c用于修改与本工程相关的函数。

rtconfig.h用于修改对RTOS的设置。

在user文件夹下新建一个board.h用于board.c的声明

4.4.2.给工程添加groups

添加user,rtt/source,rtt/ports的groups

user用于存放用户工程相关文件,目前只有user\board.c

rtt/source用于存放rt-thread\src\内所有rt-thread内核源文件

rtt/ports用于存放rt-thread\libcpu\arm\cortex-m7\内所有与硬件相关的源文件

4.4.3.给工程指定头文件目录

4.4.4.修改rtconfig.h

注释掉 #include "RTE_Components.h"

/* RT-Thread config file */

#ifndef __RTTHREAD_CFG_H__
#define __RTTHREAD_CFG_H__

#include <rtthread.h>

#if defined(__CC_ARM) || defined(__CLANG_ARM)
// #include "RTE_Components.h"

#if defined(RTE_USING_FINSH)
#define RT_USING_FINSH
#endif // RTE_USING_FINSH

#endif //(__CC_ARM) || (__CLANG_ARM)

// <<< Use Configuration Wizard in Context Menu >>>
// <h>Basic Configuration
// <o>Maximal level of thread priority <8-256>
//  <i>Default: 32
#define RT_THREAD_PRIORITY_MAX 8
// <o>OS tick per second
//  <i>Default: 1000   (1ms)
#define RT_TICK_PER_SECOND 1000
// <o>Alignment size for CPU architecture data access
//  <i>Default: 4
#define RT_ALIGN_SIZE 4
// <o>the max length of object name<2-16>
//  <i>Default: 8
#define RT_NAME_MAX 8
// <c1>Using RT-Thread components initialization
//  <i>Using RT-Thread components initialization
#define RT_USING_COMPONENTS_INIT
// </c>

#define RT_USING_USER_MAIN

// <o>the stack size of main thread<1-4086>
//  <i>Default: 512
#define RT_MAIN_THREAD_STACK_SIZE 512

// </h>

// <h>Debug Configuration
// <c1>enable kernel debug configuration
//  <i>Default: enable kernel debug configuration
// #define RT_DEBUG
// </c>
// <o>enable components initialization debug configuration<0-1>
//  <i>Default: 0
#define RT_DEBUG_INIT 0
// <c1>thread stack over flow detect
//  <i> Diable Thread stack over flow detect
// #define RT_USING_OVERFLOW_CHECK
// </c>
// </h>

// <h>Hook Configuration
// <c1>using hook
//  <i>using hook
// #define RT_USING_HOOK
// </c>
// <c1>using idle hook
//  <i>using idle hook
#define RT_USING_IDLE_HOOK
// </c>
// </h>

// <e>Software timers Configuration
// <i> Enables user timers
#define RT_USING_TIMER_SOFT 0
#if RT_USING_TIMER_SOFT == 0
#undef RT_USING_TIMER_SOFT
#endif
// <o>The priority level of timer thread <0-31>
//  <i>Default: 4
#define RT_TIMER_THREAD_PRIO 4
// <o>The stack size of timer thread <0-8192>
//  <i>Default: 512
#define RT_TIMER_THREAD_STACK_SIZE 512
// </e>

// <h>IPC(Inter-process communication) Configuration
// <c1>Using Semaphore
//  <i>Using Semaphore
#define RT_USING_SEMAPHORE
// </c>
// <c1>Using Mutex
//  <i>Using Mutex
#define RT_USING_MUTEX
// </c>
// <c1>Using Event
//  <i>Using Event
#define RT_USING_EVENT
// </c>
// <c1>Using MailBox
//  <i>Using MailBox
#define RT_USING_MAILBOX
// </c>
// <c1>Using Message Queue
//  <i>Using Message Queue
#define RT_USING_MESSAGEQUEUE
// </c>
// </h>

// <h>Memory Management Configuration
// #define RT_USING_MEMPOOL
// <c1>Dynamic Heap Management
//  <i>Dynamic Heap Management
#define RT_USING_HEAP
// </c>
// <c1>using small memory
//  <i>using small memory
#define RT_USING_SMALL_MEM
// </c>
// <c1>using tiny size of memory
//  <i>using tiny size of memory
// #define RT_USING_TINY_SIZE
// </c>
// </h>

// <h>Console Configuration
// <c1>Using console
//  <i>Using console
#define RT_USING_CONSOLE
// </c>
// <o>the buffer size of console <1-1024>
//  <i>the buffer size of console
//  <i>Default: 128  (128Byte)
#define RT_CONSOLEBUF_SIZE 128
// </h>

#if defined(RT_USING_FINSH)
#define FINSH_USING_MSH
#define FINSH_USING_MSH_ONLY
// <h>Finsh Configuration
// <o>the priority of finsh thread <1-7>
//  <i>the priority of finsh thread
//  <i>Default: 6
#define __FINSH_THREAD_PRIORITY 5
#define FINSH_THREAD_PRIORITY (RT_THREAD_PRIORITY_MAX / 8 * __FINSH_THREAD_PRIORITY + 1)
// <o>the stack of finsh thread <1-4096>
//  <i>the stack of finsh thread
//  <i>Default: 4096  (4096Byte)
#define FINSH_THREAD_STACK_SIZE 512
// <o>the history lines of finsh thread <1-32>
//  <i>the history lines of finsh thread
//  <i>Default: 5
#define FINSH_HISTORY_LINES 1

#define FINSH_USING_SYMTAB
// </h>
#endif

// <<< end of configuration section >>>

#endif

4.4.5修改board.c和board.h

我们使用STM32Cubemx生成的时钟初始化,所以注释掉rt-thread自己的时钟初始化和rt_hw_board_init函数中的时钟初始化

将main.c开头由STM32Cubemx生成的头文件复制到board.h中

#ifndef __BOARD_H__
#define __BOARD_H__

/*
*************************************************************************
*                             包含的头文件
*************************************************************************
*/
/* STM32 固件库头文件 */
#include "main.h"
#include "usart.h"
#include "gpio.h"
/*
*************************************************************************
*                               函数声明
*************************************************************************
*/
void rt_hw_board_init(void);
void SysTick_Handler(void);

#endif /* __BOARD_H__ */

将main.c中时钟初始化函数SystemClock_Config在board.c中引用

用SystemClock_Config和

HAL_SYSTICK_Config(HAL_RCC_GetSysClockFreq() / RT_TICK_PER_SECOND)

初始化时钟,并将main.c开始部分的硬件初始化复制过来,将main函数中的初始化全部注释掉。

这样以后由STM32Cubemx生成的代码,都可以复制到这里初始化。

最好用#if 0...#endif 注释,这样以后生成新的代码,不用重复注释。

自定义rt_hw_console_output函数,实现串口的重映射到rt_kprintf()

/**
 * @brief  重映射串口DEBUG_USARTx到rt_kprintf()函数
 *   Note:DEBUG_USARTx是在bsp_usart.h中定义的宏,默认使用串口1
 * @param  str:要输出到串口的字符串
 * @retval 无
 *
 * @attention
 *
 */
void rt_hw_console_output(const char *str)
{
    uint8_t char_r = '\r';
    /* 进入临界段 */
    rt_enter_critical();

    /* 直到字符串结束 */
    while (*str != '\0')
    {
        /* 换行 */
        if (*str == '\n')
        {
            HAL_UART_Transmit(&huart5, &char_r, 1, 1000);
        }
        HAL_UART_Transmit(&huart5, (uint8_t *)(str++), 1, 1000);
    }

    /* 退出临界段 */
    rt_exit_critical();
}

附上修改完毕的board.c全部源码

/*
 * Copyright (c) 2006-2019, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2017-07-24     Tanek        the first version
 * 2018-11-12     Ernest Chen  modify copyright
 */

#include "board.h"

#include <stdint.h>
#include <rthw.h>
#include <rtthread.h>

#if 0
#define _SCB_BASE (0xE000E010UL)
#define _SYSTICK_CTRL (*(rt_uint32_t *)(_SCB_BASE + 0x0))
#define _SYSTICK_LOAD (*(rt_uint32_t *)(_SCB_BASE + 0x4))
#define _SYSTICK_VAL (*(rt_uint32_t *)(_SCB_BASE + 0x8))
#define _SYSTICK_CALIB (*(rt_uint32_t *)(_SCB_BASE + 0xC))
#define _SYSTICK_PRI (*(rt_uint8_t *)(0xE000ED23UL))

// Updates the variable SystemCoreClock and must be called 
// whenever the core clock is changed during program execution.
extern void SystemCoreClockUpdate(void);

// Holds the system core clock, which is the system clock 
// frequency supplied to the SysTick timer and the processor 
// core clock.
extern uint32_t SystemCoreClock;

static uint32_t _SysTick_Config(rt_uint32_t ticks)
{
    if ((ticks - 1) > 0xFFFFFF)
    {
        return 1;
    }
    
    _SYSTICK_LOAD = ticks - 1; 
    _SYSTICK_PRI = 0xFF;
    _SYSTICK_VAL  = 0;
    _SYSTICK_CTRL = 0x07;  
    
    return 0;
}
#endif

#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
#define RT_HEAP_SIZE 1024
static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4)
RT_WEAK void *rt_heap_begin_get(void)
{
    return rt_heap;
}

RT_WEAK void *rt_heap_end_get(void)
{
    return rt_heap + RT_HEAP_SIZE;
}
#endif

/**
 * This function will initial your board.
 */
extern void SystemClock_Config(void);
void rt_hw_board_init()
{
#if 0
    /* System Clock Update */
    SystemCoreClockUpdate();
    
    /* System Tick Configuration */
    _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
#endif
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();
    SystemClock_Config();
    HAL_SYSTICK_Config(HAL_RCC_GetSysClockFreq() / RT_TICK_PER_SECOND);

    MX_GPIO_Init();
    MX_UART5_Init();

    /* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
    rt_components_board_init();
#endif

#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}

void SysTick_Handler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}

/**
 * @brief  重映射串口DEBUG_USARTx到rt_kprintf()函数
 *   Note:DEBUG_USARTx是在bsp_usart.h中定义的宏,默认使用串口1
 * @param  str:要输出到串口的字符串
 * @retval 无
 *
 * @attention
 *
 */
void rt_hw_console_output(const char *str)
{
    uint8_t char_r = '\r';
    /* 进入临界段 */
    rt_enter_critical();

    /* 直到字符串结束 */
    while (*str != '\0')
    {
        /* 换行 */
        if (*str == '\n')
        {
            HAL_UART_Transmit(&huart5, &char_r, 1, 1000);
        }
        HAL_UART_Transmit(&huart5, (uint8_t *)(str++), 1, 1000);
    }

    /* 退出临界段 */
    rt_exit_critical();
}

4.4.7.编译下载

编译下载,无错误,到此rt-thread内核移植完成。

5.测试

5.1.动态创建线程

创建一个LED线程,让LED间隔1000ms亮灭,并打印任务信息

static rt_thread_t led_thread = RT_NULL; //led线程句柄

static void led_thread_entry(void *parameter) //led线程入口函数
{
  while (1)
  {
    HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); //开启led
    rt_kprintf("led_thread running,LED ON\n"); //打印信息
    rt_thread_delay(1000); //阻塞延时

    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); //关闭led
    rt_kprintf("led_thread running,LED OFF\n"); //打印信息
    rt_thread_delay(1000); //阻塞延时
  }
}

创建一个KEY线程,让按键单击时,打印单击信息

static rt_thread_t key1_thread = RT_NULL; //key1线程句柄

static void key1_thread_entry(void *parameter) //key1线程入口函数
{
  uint8_t press = 0; //按键记录
  while (1)
  {

    if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET) // KEY按下
    {
      if (press == 0)
      {
        press = 1;                                      // 记录KEY状态
        rt_kprintf("key1_thread runing,KEY被单击\r\n"); // 打印信息
      }
    }
    else
      press = 0;

    rt_thread_delay(20);
  }
}

线程创建并启动

  // 创建led线程
  led_thread = rt_thread_create("led_thread",     // 线程名称
                                led_thread_entry, // 入口函数
                                RT_NULL,          // 传入参数
                                512,              // 内存大小
                                3,                // 优先级
                                20);              // 时间片
  if (led_thread != RT_NULL)
    rt_thread_startup(led_thread); // 启动线程
  else
    return -1;

  key1_thread = rt_thread_create("key1_thread",
                                 key1_thread_entry,
                                 RT_NULL,
                                 512,
                                 2,
                                 20);
  if (key1_thread != RT_NULL)
    rt_thread_startup(key1_thread);
  else
    return -1;

附上main.c完整代码

/* USER CODE BEGIN Header */
/**
 ******************************************************************************
 * @file           : main.c
 * @brief          : Main program body
 ******************************************************************************
 * @attention
 *
 * Copyright (c) 2023 STMicroelectronics.
 * All rights reserved.
 *
 * This software is licensed under terms that can be found in the LICENSE file
 * in the root directory of this software component.
 * If no LICENSE file comes with this software, it is provided AS-IS.
 *
 ******************************************************************************
 */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "board.h"
#include "rtthread.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
static rt_thread_t led_thread = RT_NULL;  // led线程句柄
static rt_thread_t key1_thread = RT_NULL; // key1线程句柄

static void led_thread_entry(void *parameter) // led线程入口函数
{
  while (1)
  {
    HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); // 开启led
    rt_kprintf("led_thread running,LED ON\n");                 // 打印信息
    rt_thread_delay(1000);                                     // 阻塞延时

    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); // 关闭led
    rt_kprintf("led_thread running,LED OFF\n");                  // 打印信息
    rt_thread_delay(1000);                                       // 阻塞延时
  }
}

static void key1_thread_entry(void *parameter) // key1线程入口函数
{
  uint8_t press = 0; // 按键记录
  while (1)
  {

    if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET) // KEY按下
    {
      if (press == 0)
      {
        press = 1;                                      // 记录KEY状态
        rt_kprintf("key1_thread runing,KEY被单击\r\n"); // 打印信息
      }
    }
    else
      press = 0;

    rt_thread_delay(20);
  }
}

/* USER CODE END 0 */

/**
 * @brief  The application entry point.
 * @retval int
 */
int main(void)
{
  /* USER CODE BEGIN 1 */
#if 0
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_UART5_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
#endif

  // 创建led线程
  led_thread = rt_thread_create("led_thread",     // 线程名称
                                led_thread_entry, // 入口函数
                                RT_NULL,          // 传入参数
                                512,              // 内存大小
                                3,                // 优先级
                                20);              // 时间片
  if (led_thread != RT_NULL)
    rt_thread_startup(led_thread); // 启动线程
  else
    return -1;

  key1_thread = rt_thread_create("key1_thread",
                                 key1_thread_entry,
                                 RT_NULL,
                                 512,
                                 2,
                                 20);
  if (key1_thread != RT_NULL)
    rt_thread_startup(key1_thread);
  else
    return -1;

  /* USER CODE END 3 */
}

/**
 * @brief System Clock Configuration
 * @retval None
 */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Supply configuration update enable
   */
  HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);

  /** Configure the main internal regulator output voltage
   */
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE3);

  while (!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY))
  {
  }

  /** Initializes the RCC Oscillators according to the specified parameters
   * in the RCC_OscInitTypeDef structure.
   */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 2;
  RCC_OscInitStruct.PLL.PLLN = 32;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 2;
  RCC_OscInitStruct.PLL.PLLR = 2;
  RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_3;
  RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
  RCC_OscInitStruct.PLL.PLLFRACN = 0;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
   */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 | RCC_CLOCKTYPE_D3PCLK1 | RCC_CLOCKTYPE_D1PCLK1;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2;
  RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
 * @brief  This function is executed in case of error occurrence.
 * @retval None
 */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef USE_FULL_ASSERT
/**
 * @brief  Reports the name of the source file and the source line number
 *         where the assert_param error has occurred.
 * @param  file: pointer to the source file name
 * @param  line: assert_param error line source number
 * @retval None
 */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

5.2.编译下载

编译下载,查看串口信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值