上一篇 下一篇
软件定时器


Tickless 低功耗模式

1)背景

很多应用场合对于功耗的要求很严格,比如可穿戴低功耗产品、物联网低功耗产品等。一般 MCU 都有相应的低功耗模式,裸机开发时可以使用 MCU 的低功耗模式。FreeRTOS 也提供了一个叫 Tickless 的低功耗模式,方便带 FreeRTOS 操作系统的应用开发。

STM32 裸机的低功耗模式有三种:睡眠模式、停止模式、待机模式,详情如下:

在这里插入图片描述

Tickless 低功耗模式的本质是通过调用指令 WFI 指令实现睡眠模式 这种模式下,系统时钟仍然开启,只是 HCLK 对 CPU 内核的时钟输入被关闭了,也就是说这种模式下,除了 CPU 内核时钟关闭了以外,其他的时钟源以及外设全部正常运行(对裸机睡眠模式不了解的也没关系,FreeRTOS 源码将模式都处理好了,只需要配置即可)。

2)Tickless 模式详解

模式详解熟悉一下就行,主要看后面的配置

之前的任务运行时间统计实验的一个示例结果如下:

在这里插入图片描述

可以看出,在整个系统的运行过程中,其实大部分时间是在执行空闲任务的,是在系统中的所有其它任务都阻塞或被挂起时才运行的。

为了可以降低功耗,又不影响系统运行,该如何做?可以在本该空闲任务执行的期间,让 MCU 进入相应的低功耗模式;当其他任务准备运行的时候,唤醒 MCU 退出低功耗模式。

不过这样会有两个难点:

  • 进入低功耗之后,多久唤醒?也就是下一个要运行的任务如何被准确唤醒?
  • 任何中断均可唤醒 MCU,若滴答定时器频繁中断,会影响低功耗的效果(滴答定时器的中断作为 RTOS 的心脏是不能关的)

解决方法是:

  • 将滴答定时器的中断周期修改为低功耗运行时间
  • 退出低功耗后,需补上系统时钟节拍数
  • 比如说:所有任务都阻塞了,最小阻塞时间为 20ms,则将滴答定时器的中断周期修改为 20ms,然后进入低功耗 20ms 后,会被滴答定时器的中断唤醒,从而退出低功耗模式,然后需要补上系统时钟节拍数(假设节拍是 1ms 一次,那么就会调用 vTaskStepTick(20) ,一下将系统节拍数 +20 ),最后将滴答定时器的中断周期改回到初始状态( 1ms 一次)。
  • 当然,如果是由其他外设中断唤醒的,那么也需要补上系统时钟节拍数。不过不同中断唤醒,所需要补的系统节拍数的计算方式是不一样的。

值得庆幸的是:FreeRTOS 的低功耗 Tickless 模式机制已经处理好了这些难点,只要自己配置即可。

2.1)内部机制实现简要介绍

了解即可

相关的 Tickless 低功耗模式机制的实现大部分是在 IDLE_task 空闲任务的内部实现的。具体图解如下:

在这里插入图片描述

2.2)最大单次睡眠时间

configUSE_TICKLESS_IDLE = 1

依赖 SysTick(24位计数器),最大时间由内核在初始化时计算:

// FreeRTOS源码 (port.c)
ulTimerCountsForOneTick = configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ; // 每节拍计数值
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick; // 最大抑制节拍数

计算公式
最大时间 = ( 2 24 − 1 ) / ( S Y S C L K / T I C K _ R A T E ) × ( 1 / T I C K _ R A T E ) (单位:秒) 最大时间 = (2^{24} - 1) / (SYSCLK / TICK\_RATE) × (1 / TICK\_RATE) (单位:秒) 最大时间=(2241)/(SYSCLK/TICK_RATE)×(1/TICK_RATE)(单位:秒)
2 24 − 1 2^{24} - 1 2241 = 0xFFFFFF

典型示例(STM32F4)

参数
SYSCLK(HCLK) 168 MHz
TICK_RATE 1000 Hz (1ms)
每节拍计数值 168,000
最大抑制节拍 16,777,215 / 168,000 ≈ 99
最大单次睡眠 99 ms

典型示例(STM32F1)

参数
SYSCLK(HCLK) 72 MHz
TICK_RATE 1000 Hz (1ms)
每节拍计数值 72,000
最大抑制节拍 16,777,215 / 72,000 ≈ 233
最大单次睡眠 233 ms

最大单次睡眠时间到了之后,会进行:系统精准唤醒 → 补偿时间 → 重新评估 → 继续分段睡眠(若仍空闲)。

核心流程(以 200ms 空闲、F4 的最大单次睡眠 99ms 为例,F1的同理):

在这里插入图片描述

3)Tickless 模式相关配置项

  • configUSE_TICKLESS_IDLE

    此宏用于使能低功耗 Tickless 模式(1:使能,默认 0:失能,2:由用户自己实现低功耗模式 ),在 FreeRTOSConfig.h 中定义 。

  • configEXPECTED_IDLE_TIME_BEFORE_SLEEP

    此宏用于定义系统进入相应低功耗模式的最短时长(默认为 2 ,单位是系统节拍),在 FreeRTOS.h 中定义。

  • configPRE_SLEEP_PROCESSING(x),声明了但无内容,需要自己写内容

    此宏用于定义需要在系统进入低功耗模式前执行的事务,如:进入低功耗前关闭外设时钟,以达到进一步降低功耗的目的。

  • configPOST_SLEEP_PROCESSING(x),声明了但无内容,需要自己写内容

    此宏用于定义需要在系统退出低功耗模式后执行的事务,如:退出低功耗后开启之前关闭的外设时钟,以使系统能够正常运行。

注意①: 使用起来很简单,就是要将第一个宏置 1 ,然后有需要的话手动补充后两个宏,后两个宏的函数内容也就是开关外设时钟。

注意②: 如果不使用后面两个宏的话(不额外关闭一些外设的时钟的话),单单关闭 CPU 内核的时钟,其实低功耗的程度并不明显。并且如果额外关闭的外设本身功耗就比较小的话,其实效果也不大。

4)实验

① 实验目的: 学习使用 FreeRTOS 中的低功耗 Tickless 模式,并观察该模式是否对功耗有明显降低。

② 实验设计: 将在原先二值信号量的课堂源码中,加入低功耗模式,最后对比这个两个实验的功耗结果,观察 Tickless 模式对于降低功耗是否有用。

一般的习惯是:在 FreeRTOSConfig.h 中声明,然后在 FreeRTOS_Code.c 中定义具体内容,不要忘记互相引用头文件。

③ 代码:

FreeRTOSConfig.h 文件代码如下(自定义的两个部分在最后面):

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/* 头文件 */
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include <stdint.h>
#include "FreeRTOS_Code.h"

extern uint32_t SystemCoreClock;

/* 基础配置项 */
#define configUSE_PREEMPTION                            1                       /* 1: 抢占式调度器, 0: 协程式调度器, 无默认需定义 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION         1                       /* 1: 使用硬件计算下一个要运行的任务, 0: 使用软件算法计算下一个要运行的任务, 默认: 0 */
#define configUSE_TICKLESS_IDLE                         1                       /* 1: 使能tickless低功耗模式, 默认: 0 */
#define configCPU_CLOCK_HZ                              SystemCoreClock         /* 定义CPU主频, 单位: Hz, 无默认需定义 */
#define configSYSTICK_CLOCK_HZ                          (configCPU_CLOCK_HZ / 8)/* 定义SysTick时钟频率,当SysTick时钟频率与内核时钟频率不同时才可以定义, 单位: Hz, 默认: 不定义 */
#define configTICK_RATE_HZ                              1000                    /* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */
#define configMAX_PRIORITIES                            32                      /* 定义最大优先级数, 最大优先级=configMAX_PRIORITIES-1, 无默认需定义 */
#define configMINIMAL_STACK_SIZE                        128                     /* 定义空闲任务的栈空间大小, 单位: Word, 无默认需定义 */
#define configMAX_TASK_NAME_LEN                         16                      /* 定义任务名最大字符数, 默认: 16 */
#define configUSE_16_BIT_TICKS                          0                       /* 1: 定义系统时钟节拍计数器的数据类型为16位无符号数, 无默认需定义 */
#define configIDLE_SHOULD_YIELD                         1                       /* 1: 使能在抢占式调度下,同优先级的任务能抢占空闲任务, 默认: 1 */
#define configUSE_TASK_NOTIFICATIONS                    1                       /* 1: 使能任务间直接的消息传递,包括信号量、事件标志组和消息邮箱, 默认: 1 */
#define configTASK_NOTIFICATION_ARRAY_ENTRIES           1                       /* 定义任务通知数组的大小, 默认: 1 */
#define configUSE_MUTEXES                               1                       /* 1: 使能互斥信号量, 默认: 0 */
#define configUSE_RECURSIVE_MUTEXES                     1                       /* 1: 使能递归互斥信号量, 默认: 0 */
#define configUSE_COUNTING_SEMAPHORES                   1                       /* 1: 使能计数信号量, 默认: 0 */
#define configUSE_ALTERNATIVE_API                       0                       /* 已弃用!!! */
#define configQUEUE_REGISTRY_SIZE                       8                       /* 定义可以注册的信号量和消息队列的个数, 默认: 0 */
#define configUSE_QUEUE_SETS                            1                       /* 1: 使能队列集, 默认: 0 */
#define configUSE_TIME_SLICING                          1                       /* 1: 使能时间片调度, 默认: 1 */
#define configUSE_NEWLIB_REENTRANT                      0                       /* 1: 任务创建时分配Newlib的重入结构体, 默认: 0 */
#define configENABLE_BACKWARD_COMPATIBILITY             0                       /* 1: 使能兼容老版本, 默认: 1 */
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS         0                       /* 定义线程本地存储指针的个数, 默认: 0 */
#define configSTACK_DEPTH_TYPE                          uint16_t                /* 定义任务堆栈深度的数据类型, 默认: uint16_t */
#define configMESSAGE_BUFFER_LENGTH_TYPE                size_t                  /* 定义消息缓冲区中消息长度的数据类型, 默认: size_t */

/* 内存分配相关定义 */
#define configSUPPORT_STATIC_ALLOCATION                 0                       /* 1: 支持静态申请内存, 默认: 0 */
#define configSUPPORT_DYNAMIC_ALLOCATION                1                       /* 1: 支持动态申请内存, 默认: 1 */
#define configTOTAL_HEAP_SIZE                           ((size_t)(10 * 1024))   /* FreeRTOS堆中可用的RAM总量, 单位: Byte, 无默认需定义 */
#define configAPPLICATION_ALLOCATED_HEAP                0                       /* 1: 用户手动分配FreeRTOS内存堆(ucHeap), 默认: 0 */
#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP       0                       /* 1: 用户自行实现任务创建时使用的内存申请与释放函数, 默认: 0 */

/* 钩子函数相关定义 */
#define configUSE_IDLE_HOOK                             0                       /* 1: 使能空闲任务钩子函数, 无默认需定义  */
#define configUSE_TICK_HOOK                             0                       /* 1: 使能系统时钟节拍中断钩子函数, 无默认需定义 */
#define configCHECK_FOR_STACK_OVERFLOW                  0                       /* 1: 使能栈溢出检测方法1, 2: 使能栈溢出检测方法2, 默认: 0 */
#define configUSE_MALLOC_FAILED_HOOK                    0                       /* 1: 使能动态内存申请失败钩子函数, 默认: 0 */
#define configUSE_DAEMON_TASK_STARTUP_HOOK              0                       /* 1: 使能定时器服务任务首次执行前的钩子函数, 默认: 0 */

/* 运行时间和任务状态统计相关定义 */
#define configGENERATE_RUN_TIME_STATS                   1                       /* 1: 使能任务运行时间统计功能, 默认: 0 */
#if configGENERATE_RUN_TIME_STATS
#include "timer.h"
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()        ConfigureTimeForRunTimeStats()
extern uint32_t FreeRTOSRunTimeTicks;
#define portGET_RUN_TIME_COUNTER_VALUE()                FreeRTOSRunTimeTicks
#endif
#define configUSE_TRACE_FACILITY                        1                       /* 1: 使能可视化跟踪调试, 默认: 0 */
#define configUSE_STATS_FORMATTING_FUNCTIONS            1                       /* 1: configUSE_TRACE_FACILITY为1时,会编译vTaskList()和vTaskGetRunTimeStats()函数, 默认: 0 */

/* 协程相关定义 */
#define configUSE_CO_ROUTINES                           0                       /* 1: 启用协程, 默认: 0 */
#define configMAX_CO_ROUTINE_PRIORITIES                 2                       /* 定义协程的最大优先级, 最大优先级=configMAX_CO_ROUTINE_PRIORITIES-1, 无默认configUSE_CO_ROUTINES为1时需定义 */

/* 软件定时器相关定义 */
#define configUSE_TIMERS                                1                               /* 1: 使能软件定时器, 默认: 0 */
#define configTIMER_TASK_PRIORITY                       ( configMAX_PRIORITIES - 1 )    /* 定义软件定时器任务的优先级, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_QUEUE_LENGTH                        5                               /* 定义软件定时器命令队列的长度, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_TASK_STACK_DEPTH                    ( configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小, 无默认configUSE_TIMERS为1时需定义 */

/* 可选函数, 1: 使能 */
#define INCLUDE_vTaskPrioritySet                        1                       /* 设置任务优先级 */
#define INCLUDE_uxTaskPriorityGet                       1                       /* 获取任务优先级 */
#define INCLUDE_vTaskDelete                             1                       /* 删除任务 */
#define INCLUDE_vTaskSuspend                            1                       /* 挂起任务 */
#define INCLUDE_xResumeFromISR                          1                       /* 恢复在中断中挂起的任务 */
#define INCLUDE_vTaskDelayUntil                         1                       /* 任务绝对延时 */
#define INCLUDE_vTaskDelay                              1                       /* 任务延时 */
#define INCLUDE_xTaskGetSchedulerState                  1                       /* 获取任务调度器状态 */
#define INCLUDE_xTaskGetCurrentTaskHandle               1                       /* 获取当前任务的任务句柄 */
#define INCLUDE_uxTaskGetStackHighWaterMark             1                       /* 获取任务堆栈历史剩余最小值 */
#define INCLUDE_xTaskGetIdleTaskHandle                  1                       /* 获取空闲任务的任务句柄 */
#define INCLUDE_eTaskGetState                           1                       /* 获取任务状态 */
#define INCLUDE_xEventGroupSetBitFromISR                1                       /* 在中断中设置事件标志位 */
#define INCLUDE_xTimerPendFunctionCall                  1                       /* 将函数的执行挂到定时器服务任务 */
#define INCLUDE_xTaskAbortDelay                         1                       /* 中断任务延时 */
#define INCLUDE_xTaskGetHandle                          1                       /* 通过任务名获取任务句柄 */
#define INCLUDE_xTaskResumeFromISR                      1                       /* 恢复在中断中挂起的任务 */

/* 中断嵌套行为配置 */
#ifdef __NVIC_PRIO_BITS
    #define configPRIO_BITS __NVIC_PRIO_BITS
#else
    #define configPRIO_BITS 4
#endif

#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY         15                  /* 中断最低优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5                   /* FreeRTOS可管理的最高中断优先级 */
#define configKERNEL_INTERRUPT_PRIORITY                 ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY            ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_API_CALL_INTERRUPT_PRIORITY           configMAX_SYSCALL_INTERRUPT_PRIORITY

/* FreeRTOS中断服务函数相关定义 */
#define xPortPendSVHandler                              PendSV_Handler
#define vPortSVCHandler                                 SVC_Handler

/* 断言 */
#define vAssertCalled(char, int) printf("Error: %s, %d\r\n", char, int)
#define configASSERT( x ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ )

/* Tickless 低功耗模式用户定义函数 */
#define configPRE_SLEEP_PROCESSING( x )                 PRE_SLEEP_PROCESSING()          /* 需要在系统进入低功耗模式前执行的事务 */
#define configPOST_SLEEP_PROCESSING( x )                POST_SLEEP_PROCESSING()         /* 需要在系统进入低功耗模式后执行的事务 */


/* FreeRTOS MPU 特殊定义 */
//#define configINCLUDE_APPLICATION_DEFINED_PRIVILEGED_FUNCTIONS 0
//#define configTOTAL_MPU_REGIONS                                8
//#define configTEX_S_C_B_FLASH                                  0x07UL
//#define configTEX_S_C_B_SRAM                                   0x07UL
//#define configENFORCE_SYSTEM_CALLS_FROM_KERNEL_ONLY            1
//#define configALLOW_UNPRIVILEGED_CRITICAL_SECTIONS             1

/* ARMv8-M 安全侧端口相关定义。 */
//#define secureconfigMAX_SECURE_CONTEXTS         5

#endif /* FREERTOS_CONFIG_H */

FreeRTOS_Code.c 文件代码如下:

#include "sys.h" 
#include "delay.h" 
#include "usart.h" 
#include "led.h" 
#include "key.h"
#include "FreeRTOS.h" 
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "FreeRTOS_Code.h"


/* ----------------------------------------------------------------------------------- */

#define START_TASK_PRIO  1           // 任务优先级 
#define START_STK_SIZE   128         // 任务堆栈大小  
TaskHandle_t StartTask_Handler;      // 任务句柄 
void start_task(void *pvParameters); // 任务函数 
 
#define TASK1_PRIO   2               // 任务优先级 
#define TSAK1_STK_SIZE    128        // 任务堆栈大小  
TaskHandle_t Task1_Handler;          // 任务句柄 
void task1(void *p_arg);             // 任务函数 
 
#define TASK2_PRIO   3               // 任务优先级 
#define TSAK2_STK_SIZE    128        // 任务堆栈大小  
TaskHandle_t Task2_Handler;          // 任务句柄 
void task2(void *p_arg);             // 任务函数 

/* ----------------------------------------------------------------------------------- */

QueueHandle_t semphore_handle;      // 信号量句柄

/**
 * @brief       进入低功耗前所需要执行的操作
 * @param       无
 * @retval      无
 */
void PRE_SLEEP_PROCESSING(void)
{
    __HAL_RCC_GPIOA_CLK_DISABLE();
    __HAL_RCC_GPIOB_CLK_DISABLE();
    __HAL_RCC_GPIOC_CLK_DISABLE();
    __HAL_RCC_GPIOD_CLK_DISABLE();
    __HAL_RCC_GPIOE_CLK_DISABLE();
    __HAL_RCC_GPIOF_CLK_DISABLE();
    __HAL_RCC_GPIOG_CLK_DISABLE();
}

/**
 * @brief       退出低功耗后所需要执行的操作
 * @param       无
 * @retval      无
 */
void POST_SLEEP_PROCESSING(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    __HAL_RCC_GPIOE_CLK_ENABLE();
    __HAL_RCC_GPIOF_CLK_ENABLE();
    __HAL_RCC_GPIOG_CLK_ENABLE();
}

/* ----------------------------------- 主函数 ---------------------------------------- */

void freertos_code(void)
{
    /* 创建二值信号量 */
    semphore_handle = xSemaphoreCreateBinary();
    if(semphore_handle != NULL)
    {
        printf("二值信号量创建成功!!!\r\n");
    }
    
    /* 创建开始任务 */
    xTaskCreate((TaskFunction_t  )start_task,           // 任务函数 
                (const char*     )"start_task",         // 任务名称 
                (uint16_t        )START_STK_SIZE,       // 任务堆栈大小 
                (void*           )NULL,                 // 传递给任务函数的参数 
                (UBaseType_t     )START_TASK_PRIO,      // 任务优先级 
                (TaskHandle_t*   )&StartTask_Handler);  // 任务句柄               
    vTaskStartScheduler();                              // 开启任务调度器 
}

/* ---------------------------------- 任务函数 --------------------------------------- */

/**
 * @brief       开始任务函数
 * @param       无
 * @retval      无
 */
void start_task(void *pvParameters) 
{ 
    taskENTER_CRITICAL();           // 进入临界区 -----------

    /* 创建 TASK1 任务 */
    xTaskCreate((TaskFunction_t  )task1,       
                (const char*     )"task1",     
                (uint16_t        )TSAK1_STK_SIZE,  
                (void*           )NULL,     
                (UBaseType_t     )TASK1_PRIO,  
                (TaskHandle_t*   )&Task1_Handler);

    /* 创建 TASK2 任务 */
    xTaskCreate((TaskFunction_t  )task2,       
                (const char*     )"task2",     
                (uint16_t        )TSAK2_STK_SIZE,  
                (void*           )NULL,     
                (UBaseType_t     )TASK2_PRIO,  
                (TaskHandle_t*   )&Task2_Handler);

    vTaskDelete(StartTask_Handler);   // 删除开始任务 
    
    taskEXIT_CRITICAL();            // 退出临界区 -----------
}


/**
 * @brief       TASK1 任务函数
 * @param       无
 * @retval      无
 * @note        释放信号量
 */
void task1(void *pvParameters) 
{ 
    uint8_t key = 0;
    BaseType_t err;
    while(1) 
    {
        key = KEY_Scan(0);
        if(key == KEY0_PRES)
        {
            if(semphore_handle != NULL)
            {
                err = xSemaphoreGive(semphore_handle);
                if(err == pdPASS)
                {
                    printf("信号量释放成功!!\r\n");
                }else printf("信号量释放失败!!\r\n");
            }
            
        }
        vTaskDelay(10);
    }
}


/**
 * @brief       TASK2 任务函数
 * @param       无
 * @retval      无
 * @note        键值队列出队
 */
void task2(void *pvParameters) 
{
    uint32_t i = 0;
    BaseType_t err;
    while(1)
    {
        err = xSemaphoreTake(semphore_handle,portMAX_DELAY); /* 获取信号量并死等 */
        if(err == pdTRUE)
        {
            printf("获取信号量成功\r\n");
        }else printf("已超时%d\r\n",++i);
        
    }
}

FreeRTOS_Code.h 文件代码如下:

#ifndef __FREERTOS_CODE_H
#define __FREERTOS_CODE_H

void freertos_code(void);

#endif

main.c 文件代码如下:

#include "sys.h" 
#include "delay.h" 
#include "usart.h" 
#include "led.h" 
#include "key.h"
#include "timer.h"
#include "FreeRTOS.h" 
#include "task.h"
#include "FreeRTOS_Code.h"


int main(void) 
{ 
    HAL_Init();
    sys_stm32_clock_init(RCC_PLL_MUL9);        // 设置时钟,72M
    delay_init(72);       // 延时函数初始化    
    usart_init(115200);   // 初始化串口 
    LED_Init();           // 初始化LED 
    KEY_Init();           // 按键初始化
 
    freertos_code();      //FreeRTOS代码
} 

实验结果如下:

在这里插入图片描述

串口打印的结果和之前的二值信号量的结果一样,但由于没有功耗测试仪,所以功耗省了多少看不到。


Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐