尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32+FreeRTOS实战:从裸机到多任务开发完整指南

STM32+FreeRTOS实战:从裸机到多任务开发完整指南 很多初学者在接触嵌入式时最初写的代码往往是一个“超级大循环”在main函数的while(1)里不断轮询按键、处理显示、采集传感器。当项目逻辑简单时这种方式足够直接也能正常工作。但一旦功能增多循环体越来越臃肿明明只是按了一下按键却要等传感器采集完才能响应这时就会明显感受到这种“裸机编程”的瓶颈。本篇文章就围绕STM32 FreeRTOS展开从最基础的概念讲起逐步完成环境搭建、CubeMX 生成工程、任务创建、队列与信号量使用再到任务切换原理和常见问题排查。无论你是刚接触 STM32 的初学者还是想从裸机开发过渡到 RTOS 的开发者这篇文章都可以作为一个完整的入门参考。先说清楚这篇文章不是把所有知识点堆在一起而是按照一条“从为什么到怎么做再到如何用好”的路径来组织。读者跟着走一遍就能在 STM32 上跑起自己的第一个 FreeRTOS 多任务程序。1. 为什么嵌入式开发要学 FreeRTOS1.1 裸机开发与 RTOS 的本质差异裸机开发的核心是“前后台系统”main函数里的while(1)是后台中断服务函数是前台。后台代码按顺序执行前台中断可以打断后台。这种模型在简单场景下效率很高但也存在几个明显问题。第一个问题是实时性无法保证。假设主循环里有三个任务按键扫描、OLED 显示、传感器读取。当传感器读取使用了阻塞式延时比如HAL_Delay(100)时整个循环都要等它完成按键响应自然变慢。即使不使用阻塞延时代码执行的顺序和时长也会导致不同任务之间的响应存在不确定性。第二个问题是模块化困难。多个功能模块堆在同一个循环里彼此之间的状态变量、执行顺序耦合在一起。今天加一个功能可能要改动原有代码结构明天换一个需求又要重新整理循环逻辑。FreeRTOS 的解决思路不同。它让每个功能都成为独立的任务Task由系统内核负责调度。每个任务都有自己独立的栈空间和优先级。高优先级任务可以抢占低优先级任务延时函数会把 CPU 让给其他任务而不是空转等待。这样写出来的代码更接近业务逻辑本身模块边界也更清晰。1.2 FreeRTOS 实际解决什么问题举几个嵌入式开发中非常典型的场景。在电机控制应用中控制算法必须在一个严格的周期内执行比如每隔 1ms 计算一次 PID 输出。如果用裸机要么依赖定时器中断要么用主循环计时。而 FreeRTOS 可以用定时器或任务实现固定频率执行配合高优先级确保控制任务不被其他逻辑干扰。在物联网网关类项目中需要同时处理 Wi-Fi 连接、MQTT 协议解析、本地按键输入、状态显示等多个任务。裸机循环一旦遇到网络阻塞其他功能就会卡住。FreeRTOS 将网络协议栈放在低优先级任务中用户交互放在高优先级任务中两者互不阻塞。在数据采集系统中传感器数据需要周期性采集并写入缓冲区另一个任务负责将缓冲区数据发送出去。任务之间通过队列传递数据既完成了解耦又保证了数据同步安全。1.3 为什么选择 FreeRTOS 而不是其他 RTOS嵌入式领域可选的 RTOS 不止一个uC/OS、RT-Thread、ThreadX、Zephyr 等各有特点。FreeRTOS 的优势主要有几点。首先是开源免费MIT 许可协议允许商用不需要担心授权成本。其次是移植性好官方提供了大量 MCU 的移植例程特别是 STM32CubeMX 原生集成了 FreeRTOS 中间件配置界面化生成代码非常方便。第三是资源占用小FreeRTOS 内核经过裁剪后可以做到几 KB 的 Flash 占用适合 STM32F103 这类资源有限的芯片。第四是生态成熟网上资料、书籍、培训课程非常多遇到问题时容易找到参考。有一个常见误区需要说明FreeRTOS 并不是“取代中断”或“取代定时器”它更像是一个管理器把这些硬件资源统一调度起来。中断仍然是实时性最高优先级FreeRTOS 的任务只是在中断之外的“软件并发”世界里做仲裁。2. 开发环境准备与工程生成方式2.1 硬件平台选择本文使用的硬件平台是常见的STM32F103C8T6 最小系统板也就是大家常说的“蓝板”。这颗芯片的资源情况如下Cortex-M3 内核主频最高 72MHzFlash 64KBRAM 20KB丰富的外设USART、SPI、I2C、ADC、定时器等F103 虽然是一颗老芯片但资料多、成本低、上手难度小非常适合第一次接触 FreeRTOS。如果你的开发板是 STM32F407、STM32G431 或 STM32L476操作流程完全一样只是配置界面中外设选项略有不同。需要准备的材料STM32F103C8T6 最小系统板一块ST-Link V2 下载器一个也可以用 J-Link 或串口 ISP 下载USB 转 TTL 模块用于串口调试若干杜邦线、LED、按键Keil MDK 或 STM32CubeIDE2.2 软件工具链需要安装三类软件STM32CubeMX用于图形化配置芯片引脚、时钟、外设以及 FreeRTOS 中间件。它可以根据配置生成初始化代码减少手动编写底层寄存器的工作量。Keil MDK目前国内使用最广泛的 STM32 开发 IDE编译、下载、调试一体化。也可以使用 STM32CubeIDE官方免费且内置了调试器支持二选一即可。串口调试助手用于查看开发板通过串口发送的数据推荐使用 XCOM、SSCOM 或其他同类工具。版本方面STM32CubeMX 建议使用较新的 6.x 版本Keil MDK 建议使用 5.2x 以上版本。不同版本生成的代码在细节上可能会有差异但整体流程一致。如果你使用的是 STM32CubeIDE则不需要额外安装 CubeMXIDE 默认集成了配置功能。2.3 STM32CubeMX 生成 FreeRTOS 基础工程打开 STM32CubeMX新建工程并选择 STM32F103C8T6 芯片。在SYS配置中将 Debug 设置为Serial Wire这样 ST-Link 才能正常调试。接下来是配置时钟。F103 的外部高速晶振在最小系统板上通常是 8MHz在RCC配置中把 HSE 设为Crystal/Ceramic Resonator。进入Clock Configuration页面将 HCLK 设置为 72MHz系统会自动计算分频系数。这个步骤很关键虽然 FreeRTOS 的调度主要依赖 SysTick系统节拍但外设的波特率、定时器频率都建立在正确的系统时钟上。然后开启 USART1用于串口调试。模式选择Asynchronous波特率设置为 115200其他参数保持默认。这一步产生的串口初始化代码会在后续打印任务中使用。最关键的一步在Middleware and Software Packs中找到FreeRTOS勾选启用。在Interface中选择CMSIS_V1或CMSIS_V2。这两者区别在于 CMSIS_V2 是基于 CMSIS-RTOS2 标准API 更新推荐使用。在Tasks and Queues标签页中可以预创建任务。这里我们先创建三个任务分别用于 LED 闪烁、串口打印和按键扫描。任务名称、优先级和栈大小都可以在这个界面里直接设置CubeMX 会生成对应的任务入口函数。配置完成后在Project Manager中设置工程名称、路径和工具链。如果使用 Keil则把 Toolchain 选为MDK-ARM。生成代码后用 Keil 打开工程即可编译下载。3. FreeRTOS 核心机制拆解3.1 任务的状态与状态切换FreeRTOS 中任务可以处于两种主要状态运行态Running和非运行态Not Running。非运行态又细分为就绪态Ready、阻塞态Blocked、挂起态Suspended三种。运行态任务正在使用 CPU在单核处理器中同一时刻只有一个任务处于运行态。就绪态任务具备运行能力也在等待队列中但优先级低于当前运行任务或同优先级任务正在轮转。阻塞态任务正在等待某个事件比如延时到期、队列有数据、信号量可用。阻塞态任务不占用 CPU。挂起态调用vTaskSuspend后任务进入挂起态只能通过vTaskResume恢复。任务从一个状态切换到另一个状态是 FreeRTOS 调度器根据优先级和事件来决定的。最直观的例子是一个高优先级任务调用vTaskDelay主动进入阻塞态调度器立刻切换到下一个最高优先级的就绪任务CPU 不会空转。初学者容易误解的一点是任务函数里那个while(1)并不是“死循环占着 CPU”而是 FreeRTOS 任务的标准写法。任务函数通常不能 return必须包含一个无限循环。调度器通过任务控制块TCBTask Control Block保存任务上下文包括寄存器值、栈指针、优先级等任务切换时就是恢复另一组上下文。3.2 任务优先级与调度策略FreeRTOS 支持两种调度方式优先级抢占式调度Preemptive Scheduling和时间片轮转调度Time Slicing。优先级抢占式调度的规则是高优先级的任务只要处于就绪态就立即抢占低优先级任务的 CPU。因此高优先级任务不能写阻塞式代码比如HAL_Delay否则低优先级任务可能长期得不到执行。同优先级的多个任务采用时间片轮转。每个任务运行一个 tick系统节拍后调度器将 CPU 切换给下一个同优先级任务。这个 tick 时间由configTICK_RATE_HZ决定默认通常是 1000Hz也就是一个 tick 为 1ms。实际工程中优先级分配一般遵循以下原则实时性要求高的任务比如电机控制、传感器高频率采样分配高优先级。实时性要求低的任务比如串口打印调试信息、OLED 刷新分配低优先级。中断服务函数中尽量不要做复杂处理只发送信号量或数据到队列实际逻辑由任务完成。优先级数值越大任务优先级越高。FreeRTOS 默认最高优先级由configMAX_PRIORITIES决定在 CubeMX 中默认设置为 56实际项目一般用不到那么多。3.3 队列与信号量任务间通信是 RTOS 中最重要的内容。如果把任务比作独立的“员工”队列就是他们之间传递纸条的“传送带”信号量则是协调公共资源的“通行证”。队列Queue本质上是一块固定大小的内存缓冲区通过xQueueSend发送数据通过xQueueReceive接收数据。数据是以拷贝方式传递的也就是说发送方拷贝一份数据到队列接收方从队列中再拷贝一份出来。因此队列中不要放大数据结构一般传递指针或小型结构体。队列的一个典型应用场景是串口接收中断收到一帧数据解析后放入队列另一个任务从队列中取出数据进行处理。这样串口中断只做“搬运工”不做“分析师”保证中断服务函数快速退出。信号量Semaphore分为二值信号量和计数信号量。二值信号量可以理解成一个只有 0 和 1 的标记用于任务与中断之间的同步。计数信号量则像一个计数器可以积累多次事件。互斥量Mutex是特殊的信号量支持优先级继承适合保护共享资源。这里有一个经典的需求按键按下后中断里发送一个二值信号量按键处理任务阻塞在xSemaphoreTake上。当信号量有效时任务立刻唤醒并执行按键逻辑。如果不使用信号量任务就得不断轮询按键状态浪费 CPU。4. 实战创建一个多任务 STM32 项目4.1 任务设计与工程结构本节实战的目标是创建一个包含三个任务和一个外部中断的 FreeRTOS 工程任务 1LED 以 500ms 周期闪烁。任务 2通过串口每隔 1 秒打印一次系统运行时间和 FreeRTOS 堆剩余量。任务 3等待按键外部中断发出的信号量按键按下后打印一条消息。外部中断检测按键下降沿释放信号量。这个例子里既有任务调度、延时也有中断与任务之间的同步覆盖了 FreeRTOS 最常用的功能。在 CubeMX 中除了前面配置的 USART1 和 FreeRTOS还需要配置一个 GPIO 外部中断。以 PA0 为例将 PA0 设置为GPIO_EXTI0模式选择下降沿触发并启用内部上拉。NVIC 中使能 EXTI0 中断。然后配置三个任务。在 FreeRTOS 的 Tasks and Queues 中依次创建任务名称入口函数优先级栈大小字功能描述defaultTaskStartDefaultTaskosPriorityNormal (1)128LED 闪烁PrintTaskStartPrintTaskosPriorityBelowNormal (0)128串口打印KeyTaskStartKeyTaskosPriorityHigh (2)128按键处理CubeMX 生成的代码中任务入口函数是空壳真正的逻辑需要我们手动书写。每个任务函数都有一个void *argument参数虽然大多数情况下不使用但函数签名必须保留。4.2 编写任务代码生成工程后在main.c中增加串口重定向代码方便直接使用printf打印信息。需要包含stdio.h头文件并实现fputc函数/* 文件路径Core/Src/main.c */ #include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }接着在main.c文件末尾的任务函数区域填写逻辑。首先是一个简单的 LED 闪烁任务/* LED 闪烁任务 */ void StartDefaultTask(void *argument) { while (1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); vTaskDelay(pdMS_TO_TICKS(500)); } }在 CubeMX 生成的默认工程中LED 引脚可能没有单独命名。如果开发板的 LED 连接在 PC13可以在 CubeMX 中将 PC13 的 User Label 改为LED。这样生成的代码就有LED_GPIO_Port和LED_Pin这两个宏代码可读性更好。接下来是串口打印任务。xPortGetFreeHeapSize()是 FreeRTOS 提供的一个函数用来查询当前堆剩余的水大小常用于调试内存是否泄漏/* 串口打印任务 */ void StartPrintTask(void *argument) { TickType_t lastWakeTime xTaskGetTickCount(); while (1) { printf([FreeRTOS] uptime: %lu ms, free heap: %u bytes\r\n, (unsigned long)xTaskGetTickCount(), (unsigned int)xPortGetFreeHeapSize()); vTaskDelayUntil(lastWakeTime, pdMS_TO_TICKS(1000)); } }这个任务使用了vTaskDelayUntil而不是vTaskDelay目的是实现固定周期的打印。两个函数的区别很重要vTaskDelay是“相对延时”从调用时开始计时但如果任务在执行中被打断实际间隔可能超过设定值vTaskDelayUntil是“绝对延时”以上次唤醒时间为基准计算下次唤醒时间能保证执行周期稳定。4.3 信号量实现中断与任务同步在 CubeMX 中创建二值信号量。切换到 FreeRTOS 配置页在Tasks and Queues旁找到Events或直接在Advanced settings中创建。不同版本 CubeMX 的界面位置略有差异但都能找到添加信号量的入口。这里创建名为BinarySemaphore_Key的二值信号量。然后在main.c中声明外部信号量句柄/* 外部声明CubeMX 生成的定义在 freertos.c 中 */ extern SemaphoreHandle_t BinarySemaphore_Key;按键中断回调函数中释放信号量。这里使用xSemaphoreGiveFromISR注意不能在中断中直接使用xSemaphoreGive因为中断上下文中的 API 有特殊要求/* 按键外部中断回调 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if (GPIO_Pin KEY_Pin) { xSemaphoreGiveFromISR(BinarySemaphore_Key, xHigherPriorityTaskWoken); } }xHigherPriorityTaskWoken这个参数很关键。如果释放信号量之后等待该信号量的任务优先级高于当前被中断的任务那么xHigherPriorityTaskWoken会被设置为pdTRUE。此时需要在中断服务函数末尾调用portYIELD_FROM_ISR(xHigherPriorityTaskWoken)请求调度器进行一次上下文切换。CubeMX 生成的中断回调中也可以手动加上这一句。按键任务阻塞等待信号量/* 按键处理任务 */ void StartKeyTask(void *argument) { while (1) { if (xSemaphoreTake(BinarySemaphore_Key, portMAX_DELAY) pdPASS) { printf([Key] Button pressed!\r\n); } } }这里使用portMAX_DELAY表示无限等待任务会一直阻塞在信号量上不会占用 CPU。占空比为零功耗也更低。当按键触发中断后信号量变为可用任务立即被唤醒。4.4 编译下载与运行验证在 Keil 中点击编译按钮如果代码没有语法错误会生成 HEX 文件。通过 ST-Link 下载到开发板连接串口调试助手波特率设置为 115200可以观察到输出信息。正常情况下串口每一秒输出一行系统信息LED 以 500ms 周期闪烁按下按键后出现[Key] Button pressed!信息。如果按键消息和打印消息同时出现并且按下瞬间能看到输出说明任务调度、信号量同步和中断回调都正常工作。实际运行中任务之间的切换顺序可能和想象中不同。由于三个任务优先级分别为 0、1、2按键任务的优先级最高打印任务优先级最低所以按键触发后可以立即抢占其他任务。5. FreeRTOS 任务切换流程与中断安全5.1 任务切换的完整路径很多读者看到 FreeRTOS 的调度代码时会被汇编部分吓到。理解任务切换不需要逐行分析汇编关键在于理解它的核心路径。任务切换的起点是SysTick异常或PendSV异常。FreeRTOS 使用 SysTick 产生系统节拍每个 tick 都会触发一次中断。在中断服务函数中调度器检查当前任务的时间片是否用完、是否有更高优先级的任务进入就绪态。如果有任务需要切换调度器不会在 SysTick 里直接完成全部上下文切换而是触发起一个 PendSV 异常。PendSV 是专门为上下文切换设计的“可悬挂异常”它的优先级可以被设置为最低从而避免阻塞其他中断。在 PendSV 处理函数中完成以下动作保存当前任务的上下文寄存器、栈指针到当前任务的 TCB。从就绪列表中选取下一个要运行的任务。恢复新任务的上下文到寄存器。跳转到新任务的代码继续执行。整个过程中最核心的数据结构是TCB任务控制块。每个任务都有自己的 TCB 和栈空间任务切换本质上就是栈指针的切换。这套机制保证了任务之间的隔离性一个任务的栈溢出不会立刻破坏其他任务的运行当然栈溢出本身仍然是严重错误需要监控。5.2 中断服务函数中的 API 使用规范FreeRTOS 对中断服务函数中可调用的 API 有严格限制所有带FromISR后缀的函数可以在中断中使用普通版本则不可以。原因在于普通 API 可能会将当前任务阻塞比如调用xQueueReceive时如果队列为空任务会进入阻塞态。但中断不是任务不能被阻塞所以不能调用这些可能导致阻塞的函数。常用的中断安全 API 包括xQueueSendFromISRxQueueReceiveFromISRxSemaphoreGiveFromISRxSemaphoreTakeFromISRxTaskNotifyFromISR此外中断服务函数中要尽量减少处理逻辑把耗时操作放到任务中。中断里只负责“通知”任务负责“做事”。这是嵌入式开发的黄金法则。CubeMX 生成的中断回调中HAL_GPIO_EXTI_Callback本身运行在中断上下文中。如果在这里写了长时间循环会阻塞其他中断甚至影响系统节拍。所以上面的示例代码只做了一件事释放信号量。6. 常见问题与排查思路6.1 编译错误与配置问题问题现象常见原因解决思路编译报错Undefined symbol xTaskCreateFreeRTOS 源文件未添加进工程检查 FreeRTOS/Source 目录下 task.c、queue.c、list.c、timers.c 是否加入编译main.c中找不到xTaskCreate声明未包含 FreeRTOS.h 或 task.h 头文件确认 CubeMX 生成的头文件包含路径是否正确串口输出乱码波特率不匹配或晶振配置错误确认串口助手波特率是否设置为 115200确认 HSE 值是否与硬件一致编译后代码体积很大启用了所有 FreeRTOS 组件在 CubeMX 中裁剪不需要的组件降低configUSE_TRACE_FACILITY等配置6.2 运行期硬件错误与任务异常程序编译通过但运行不稳定通常更难排查。这类问题有几种典型表现。第一种是HardFault。最常见的原因是栈溢出。FreeRTOS 中每个任务都有自己的栈如果任务里定义了大型局部数组或者递归调用层级过深就可能溢出到相邻内存区域。建议在 CubeMX 中给任务分配栈时预留足够余量并在 FreeRTOS 配置中开启栈溢出检测。开启栈溢出检测的方法是设置configCHECK_FOR_STACK_OVERFLOW为 1 或 2并实现vApplicationStackOverflowHook钩子函数/* 文件路径Core/Src/main.c 或 freertos.c */ void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { printf([FATAL] Stack overflow in task: %s\r\n, pcTaskName); Error_Handler(); }第二种是任务完全不执行。优先检查优先级和延时参数。如果高优先级任务在循环中没有调用任何阻塞函数vTaskDelay、xQueueReceive、xSemaphoreTake等调度器就永远没有机会切换到其他任务这种情况称为“任务饿死了其他任务”。第三种是偶尔复位或程序跑飞。排查思路包括检查电源供电是否稳定、确认外部中断引脚是否配置了正确的上下拉、检查是否有数组越界写入。6.3 资源占用与性能问题FreeRTOS 在 STM32F103 上运行时RAM 占用需要考虑。每个任务默认 128 字的栈空间在 32 位处理器上一个字是 4 字节所以每个任务至少占用 512 字节。三个任务再加上 TCB、队列等整体占用在几 KB 以内20KB 的 RAM 完全可以承受。如果发现xPortGetFreeHeapSize()打印出的数值在不断减少说明堆上存在内存碎片或泄漏。常见原因是任务中动态分配了内存但没有释放或者队列/信号量创建后没有删除。7. 工程最佳实践与学习路线建议7.1 从第一天就养成的工程习惯嵌入式开发中工程规范的重要性不亚于代码功能本身。以下几条建议适用于所有 STM32 FreeRTOS 项目。区分任务与中断的职责。中断只做标记、计数、唤醒任务具体的业务逻辑放在任务中执行。这不仅是风格问题而是关系到系统稳定性的设计原则。控制任务的优先级数量。不要一口气设置十几个优先级。优先级数量越多调度行为越复杂越难预测。一般 3 到 5 个优先级足够覆盖绝大多数项目需求。避免在任务中使用阻塞式串口发送。HAL_UART_Transmit是阻塞函数如果串口发送缓冲区满或波特率低任务会长时间占用 CPU。对于调试打印建议使用 DMA 模式或使用非阻塞发送方式。定期监控堆空间和栈使用情况。FreeRTOS 提供了uxTaskGetStackHighWaterMark函数可以查询任务历史最小剩余栈空间。在开发阶段打印这些数据提前发现溢出的风险。保持任务函数的独立性。不同任务之间尽量通过队列传递数据避免使用全局变量作为跨任务数据交换的通道。如果确实需要共享内存应使用互斥量保护访问防止数据竞争。7.2 后续学习路线掌握 FreeRTOS 基础后可以沿着以下方向继续深入学习。第一步学习消息队列的高级用法包括队列集Queue Set和流缓冲区Stream Buffer。队列集可以同时等待多个来源的数据适用于复杂状态机。第二步研究软件定时器。FreeRTOS 软件定时器基于 tick 实现适用于周期性任务和超时控制与硬件定时器互补。第三步学习低功耗 Tickless 模式。对于电池供电的嵌入式设备空闲时让 MCU 进入低功耗模式Tickless 机制能让系统在睡眠状态下保持时间基准。第四步阅读 FreeRTOS 内核源码。重点看task.c中的任务切换实现、queue.c中的队列机制、list.c中的链表操作这对理解嵌入式底层原理帮助极大。第五步接触更高级的嵌入式系统比如 Zephyr RTOS 或嵌入式 Linux。它们与 FreeRTOS 在调度、内存管理、设备驱动模型上有不少共通之处学完 FreeRTOS 再过渡会更加容易。7.3 学习过程中的常见误区不少初学者把 FreeRTOS 当成了“万能解药”不管项目大小都硬套多任务。实际上如果项目只有一个实时任务、没有复杂交互裸机开发反而更简单高效。RTOS 的价值在于解决多个实时任务的协调问题而不是让简单问题复杂化。另一个误区是“任务越多越好”。任务数量的增加意味着上下文切换频率增加、栈空间消耗增加、调试难度增加。合理的做法是依据业务模块划分任务而不是把每个函数都设计成独立任务。还有一点容易被忽略开发板上的 LED 闪烁例子虽然简单但它背后涉及任务创建、调度器启动、时间管理、上下文切换等完整机制。真正理解这个例子后面的进阶学习才能走得更稳。嵌入式学习没有捷径但每写一个完整的例程、每排查一次异常都会沉淀为真正的工程经验。这篇文章提供的是一个起点接下来需要你在开发板上亲手敲代码、改参数、看现象。遇到问题时不要急着搜索先根据现象推断可能的原因再用打印和调试手段去验证。这个过程本身就是嵌入式开发最核心的能力。
返回列表