1. 项目概述从点灯到理解嵌入式系统对于每一位踏入嵌入式开发领域的新手来说“流水灯”实验几乎是一个绕不开的起点。它就像编程界的“Hello World”看似简单却蕴含着嵌入式系统最核心的交互逻辑——通过程序控制硬件引脚的电平变化从而驱动外部设备。这个实验的目标非常明确让连接到微控制器MCUGPIO引脚上的多个LED灯按照预设的顺序和时间间隔依次点亮和熄灭形成如同流水般动态的视觉效果。为什么这个实验如此重要因为它直接触及了嵌入式开发的本质软件与硬件的对话。你写的每一行C代码最终都要转化为芯片内部寄存器的一个个二进制位这些位通过硬件电路驱动着外部世界的LED、电机、传感器。流水灯实验正是这个过程的完美缩影。通过它你将首次亲手完成从软件逻辑设计、硬件电路理解到驱动代码编写、调试下载的全流程。这不仅是对STM32 GPIO通用输入输出模块最直观的入门更是建立嵌入式开发思维框架的关键一步。本实验将基于经典的STM32F103C8T6俗称“蓝桥杯”或“最小系统板”展开使用主流的STM32CubeMX进行图形化配置并结合Keil MDK-ARM进行代码编写与调试。无论你是电子相关专业的学生还是希望转型嵌入式的开发者跟随本文一步步操作你不仅能实现流水灯效果更能深刻理解其背后的寄存器操作、时钟配置、延时原理以及工程管理逻辑为后续更复杂的嵌入式项目打下坚实基础。2. 实验核心思路与硬件设计解析2.1 核心思路状态机与定时控制流水灯的控制逻辑本质上是一个状态机的循环。每个LED的状态亮或灭以及状态切换的时机构成了整个系统的行为。最直接的实现思路是顺序控制先点亮LED1延时一段时间后熄灭LED1并点亮LED2如此循环。这里的关键在于“延时”的实现和LED“点亮”的定义。从硬件角度看点亮一个LED意味着让电流流过它。在常见的共阴极接法LED负极接地中我们需要在对应的GPIO引脚上输出一个高电平逻辑‘1’使引脚与地之间形成电压差驱动LED发光。反之输出低电平逻辑‘0’则熄灭LED。因此我们的程序任务就是周期性地改变指定GPIO引脚的电平状态。实现延时有两种主流方法阻塞式延时Delay使用芯片的SysTick系统滴答定时器或简单的软件循环让CPU“空转”等待。这种方法简单直观适合初学者理解定时概念但在延时期间CPU无法执行其他任务效率低下。非阻塞式延时基于定时器中断配置一个硬件定时器使其产生周期性的中断。在中断服务函数中更新一个全局的时间戳或计数器主程序通过检查这个计数器的值来判断是否该切换LED状态。这种方法允许CPU在等待期间处理其他事务是实际项目中的首选。对于入门实验我们将从阻塞式延时开始因为它能让我们更专注于GPIO的控制逻辑本身。2.2 硬件电路设计要点在动手写代码之前必须清楚硬件是如何连接的。一个典型的LED驱动电路如下STM32 GPIO Pin (e.g., PA0) ---[220Ω Resistor]--- LED() --- LED(-) --- GND关键元件作用限流电阻220Ω这是绝对必要的。STM32的GPIO引脚最大输出电流有限通常单个引脚约20-25mA。不加电阻直接连接LED过大的电流可能损坏LED或更严重的损坏MCU的GPIO输出电路。220Ω电阻在3.3V系统电压下能将电流限制在安全范围内约(3.3V - LED压降2V)/220Ω ≈ 6mA。LED方向长脚为正极阳极短脚为负极阴极。务必确认连接正确。GPIO工作模式选择对于驱动LED我们需要引脚输出可控的电平。因此GPIO应配置为**推挽输出Push-Pull Output**模式。推挽输出能力强可以直接输出明确的高电平3.3V或低电平0V是驱动LED、蜂鸣器等小功率器件的标准模式。注意切勿将GPIO配置为开漏输出Open-Drain模式来直接驱动共阴极LED。在开漏模式下引脚只能主动拉低到地无法主动输出高电平需要外部上拉电阻。如果错误配置LED可能无法点亮或亮度异常。3. 开发环境搭建与工程创建3.1 工具链准备工欲善其事必先利其器。进行STM32开发你需要一套完整的工具链集成开发环境IDEKeil MDK-ARMuVision5。这是ARM官方推荐的商业IDE生态完善调试功能强大。对于学生和爱好者可以使用其有代码大小限制的免费版本Keil MDK-Lite。图形化配置工具STM32CubeMX。这是ST官方推出的神器通过图形界面配置芯片时钟、外设、引脚并能自动生成初始化代码框架极大降低了入门门槛和配置出错率。芯片支持包在Keil中需要安装对应芯片系列如STM32F1xx的Device Family PackDFP。调试器/编程器ST-LINK/V2是最常见且性价比高的选择。它通过SWDSerial Wire Debug接口与STM32连接用于下载程序和在线调试。3.2 使用STM32CubeMX创建工程我们以STM32F103C8T6为例演示标准流程新建项目打开CubeMX点击“New Project”。在芯片选择器中输入“STM32F103C8T6”在右侧出现的型号上双击。系统核心SYS配置在“Pinout Configuration”标签页左侧找到“System Core” - “SYS”。将“Debug”选项改为“Serial Wire”。这使能了SWD调试接口否则芯片可能被锁死无法再次编程。时钟RCC配置找到“System Core” - “RCC”。将“High Speed Clock (HSE)”选择为“Crystal/Ceramic Resonator”。我们的开发板外部通常有一个8MHz的晶振需要在此告知芯片。GPIO引脚配置假设我们使用PA0, PA1, PA2三个引脚连接LED。在芯片图形上找到PA0引脚左键点击选择“GPIO_Output”。同样操作配置PA1和PA2为“GPIO_Output”。在左侧“System Core” - “GPIO”中可以点击每个引脚进行详细设置。通常保持默认的“推挽输出Push Pull”、“低电平Low”、“低速Low”即可。可以为每个引脚设置一个用户标签User Label如“LED0”、“LED1”、“LED2”这样生成的代码可读性更好。时钟树配置点击“Clock Configuration”标签。这是一个关键步骤。STM32的时钟源可以倍频以提高系统性能。对于F103通常的配置是HSE8MHz - PLL倍频 - 系统时钟SYSCLK设为72MHz。你可以直接点击“HCLK”输入框输入72然后回车CubeMX会自动完成倍频系数的计算和配置。工程管理设置点击“Project Manager”标签。“Project”子标签设置项目名称和存储路径。注意路径不要有中文和空格。“Toolchain / IDE”选择“MDK-ARM V5”。“Code Generator”子标签强烈建议勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这会将每个外设的代码生成独立的文件结构清晰。同时勾选“Copy all used libraries into the project folder”这样工程可以独立移植。生成代码点击右上角的“GENERATE CODE”。CubeMX会生成一个完整的Keil工程文件.uvprojx及所有初始化代码。3.3 在Keil中打开并理解工程结构用Keil打开生成的工程文件你会看到如下主要目录结构Core/Inc/和Core/Src/用户应用程序代码和头文件存放处。我们主要修改这里的main.c。Drivers/STM32 HAL库硬件抽象层驱动文件。MDK-ARM/Keil IDE相关的项目文件及启动汇编文件。STM32F103C8Tx_FLASH.ld或类似链接脚本定义了内存布局。打开Core/Src/main.c找到main函数。在/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间的注释块内是我们可以安全添加自己代码的地方。HAL库的初始化代码MX_GPIO_Init,MX_...已经由CubeMX生成并调用。4. 核心代码实现与原理剖析4.1 阻塞式延时实现流水灯我们先实现最简单的阻塞式延时版本。HAL库提供了一个毫秒级延时函数HAL_Delay()它基于SysTick定时器。在main函数的while (1)循环中添加如下代码/* USER CODE BEGIN WHILE */ while (1) { // 点亮LED0 (PA0) 熄灭LED1, LED2 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // PA0 高电平 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // PA1 低电平 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); // PA2 低电平 HAL_Delay(500); // 延时500毫秒 // 点亮LED1 (PA1) 熄灭LED0, LED2 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); HAL_Delay(500); // 点亮LED2 (PA2) 熄灭LED0, LED1 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); HAL_Delay(500); /* USER CODE END WHILE */ }代码解析HAL_GPIO_WritePin(Port, Pin, PinState)这是HAL库提供的GPIO写函数。GPIO_PIN_SET代表高电平1GPIO_PIN_RESET代表低电平0。HAL_Delay()该函数会使程序在此处等待指定的毫秒数。在此期间CPU循环检查SysTick定时器的计数值无法执行其他任务。编译与下载点击Keil工具栏的“Build”F7编译工程若无错误连接好ST-LINK和开发板点击“Load”F8将程序下载到芯片。复位后你应该能看到三个LED依次点亮每个持续0.5秒。4.2 进阶使用位操作与宏定义优化代码上面的代码重复且冗长。我们可以利用C语言的位操作和宏定义来优化使代码更简洁、易维护。在main.c文件开头的/* USER CODE BEGIN Includes */之后定义引脚宏和模式宏/* USER CODE BEGIN Includes */ #include “stdio.h” // 如果后续需要打印调试信息 /* USER CODE END Includes */ /* USER CODE BEGIN PV */ // 定义LED引脚宏方便修改 #define LED0_PIN GPIO_PIN_0 #define LED1_PIN GPIO_PIN_1 #define LED2_PIN GPIO_PIN_2 #define LED_PORT GPIOA // 定义LED操作宏提高可读性 #define LED_ON(PIN) HAL_GPIO_WritePin(LED_PORT, PIN, GPIO_PIN_RESET) // 假设低电平点亮共阳接法时 #define LED_OFF(PIN) HAL_GPIO_WritePin(LED_PORT, PIN, GPIO_PIN_SET) // 注意这里假设是共阳接法LED正极接VCC负极接GPIO。如果你的硬件是共阴极LED负极接GND正极接GPIO则宏定义应反过来 // #define LED_ON(PIN) HAL_GPIO_WritePin(LED_PORT, PIN, GPIO_PIN_SET) // #define LED_OFF(PIN) HAL_GPIO_WritePin(LED_PORT, PIN, GPIO_PIN_RESET) // 或者定义一个切换宏 #define LED_TOGGLE(PIN) HAL_GPIO_TogglePin(LED_PORT, PIN) /* USER CODE END PV */然后在while循环中代码可以简化为while (1) { LED_ON(LED0_PIN); LED_OFF(LED1_PIN); LED_OFF(LED2_PIN); HAL_Delay(500); LED_OFF(LED0_PIN); LED_ON(LED1_PIN); LED_OFF(LED2_PIN); HAL_Delay(500); LED_OFF(LED0_PIN); LED_OFF(LED1_PIN); LED_ON(LED2_PIN); HAL_Delay(500); }更进一步我们可以使用一个数组来循环遍历LED实现更通用的流水效果/* USER CODE BEGIN PV */ uint16_t led_pins[] {LED0_PIN, LED1_PIN, LED2_PIN}; uint8_t led_count sizeof(led_pins) / sizeof(led_pins[0]); uint8_t current_led 0; /* USER CODE END PV */ /* USER CODE BEGIN WHILE */ while (1) { // 关闭所有LED for(int i0; iled_count; i) { LED_OFF(led_pins[i]); } // 点亮当前LED LED_ON(led_pins[current_led]); HAL_Delay(500); // 更新索引实现循环 current_led; if(current_led led_count) { current_led 0; } }这种写法将控制逻辑与硬件引脚分离如果需要增加或减少LED数量只需修改led_pins数组即可大大提高了代码的扩展性。4.3 深入原理GPIO寄存器操作探秘HAL库的HAL_GPIO_WritePin函数封装了对底层寄存器的操作。理解寄存器操作有助于你深入掌握硬件并在某些对性能要求极高的场合进行优化。以STM32F1的GPIO为例主要控制寄存器有GPIOx_CRL/CRH端口配置寄存器控制引脚模式输入/输出/复用/模拟及输出类型推挽/开漏、速度。GPIOx_ODR端口输出数据寄存器直接写入该寄存器可以控制引脚输出电平1为高0为低。GPIOx_BSRR端口位设置/清除寄存器。这是一个非常实用的寄存器可以原子操作不会被中断打断地设置或清除某个引脚。向BSRR的低16位某位写1对应引脚置高Set向高16位某位写1对应引脚置低Reset。使用寄存器方式点亮PA0引脚假设已配置为输出的代码是// 方法1操作ODR寄存器 GPIOA-ODR | (1 0); // 将PA0置1可能影响其他位 GPIOA-ODR ~(1 0); // 将PA0清0 // 方法2操作BSRR寄存器推荐更高效安全 GPIOA-BSRR GPIO_BSRR_BS0; // 置位PA0 (Bit Set) GPIOA-BSRR GPIO_BSRR_BR0; // 复位PA0 (Bit Reset)在HAL库中HAL_GPIO_WritePin函数内部就是通过操作BSRR寄存器来实现的。了解这些当你在阅读库源码或进行底层调试时就能做到心中有数。5. 常见问题排查与调试技巧实录即使按照步骤操作第一次实验也难免遇到问题。下面是我在教学中遇到的高频问题及解决方法。5.1 LED完全不亮这是最令人沮丧的情况。请按照以下清单逐项排查硬件连接首要检查用万用表电压档测量LED两端电压。当程序设定为点亮时LED正极连接GPIO端电压应接近3.3V负极接地端接近0V。如果电压异常检查电路焊接/连接。限流电阻确认已串联电阻阻值在220Ω-1kΩ之间。没有电阻或电阻短路是烧坏LED或IO口的常见原因。LED极性确认LED方向没有接反。长脚正极接GPIO通过电阻短脚负极接地共阴接法。电源开发板供电是否正常稳压芯片是否发热测量板载3.3V和GND之间电压。软件配置引脚配置错误在CubeMX中双击确认PA0/1/2是否已配置为“GPIO_Output”。生成的MX_GPIO_Init函数是否正确。时钟未使能STM32的任何外设包括GPIOA在使用前都必须使能其时钟。检查main.c中SystemClock_Config()函数是否被调用以及MX_GPIO_Init()函数内部是否有__HAL_RCC_GPIOA_CLK_ENABLE()语句。CubeMX通常会自动生成。程序未运行下载程序后是否按了复位键检查启动模式BOOT0/BOOT1引脚是否设置为从主闪存启动通常两者都接地。5.2 LED常亮或常灭不受程序控制电平逻辑混淆这是最高频的错误。你的代码逻辑和硬件电路不匹配。症状程序里写了LED_ON但LED却灭了或者相反。根源LED_ON宏定义是SET还是RESET取决于你的硬件是共阴还是共阳接法。判断与解决将程序中的LED_ON宏暂时改为直接操作HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET)下载测试。如果LED亮了说明你的硬件是共阴接法高电平点亮。那么你的LED_ON宏就应该定义为SET。反之如果SET时LED灭RESET时LED亮说明是共阳接法低电平点亮LED_ON宏应定义为RESET。务必根据实际硬件修改宏定义引脚冲突某些引脚有默认复用功能。例如PA13, PA14, PA15默认是JTAG/SWD调试接口的引脚。如果你错误地将这些引脚配置为普通GPIO输出可能会与调试器冲突导致输出异常甚至无法下载程序。在CubeMX中配置这些引脚时会有明确警告。对于流水灯实验建议避开PA13, PA14, PA15, PB3, PB4这几个调试引脚。5.3 流水速度异常快或慢这几乎肯定是延时函数HAL_Delay()的问题。系统时钟SYSCLK配置错误HAL_Delay()依赖于SysTick定时器而SysTick的时钟源是系统时钟AHB时钟。如果你在CubeMX的时钟树里没有正确配置导致系统时钟不是预期的72MHz比如误用了内部8MHz的HSI那么HAL_Delay(500)实际的延时时间就会偏差很大。检查在main函数开始可以添加一句printf(“System Clock: %ld Hz\n”, HAL_RCC_GetSysClockFreq());并通过串口打印出来需先配置好串口看是否是7200000072MHz。中断干扰如果程序中开启了其他中断并且中断服务函数执行时间过长可能会打断HAL_Delay()所依赖的SysTick中断导致延时变长。对于本实验此情况较少。5.4 使用调试器进行在线调试当逻辑复杂时仅靠“看灯”很难定位问题。Keil的在线调试功能是强大的帮手。进入调试模式点击Keil的“Start/Stop Debug Session”CtrlF5。查看外设寄存器在菜单栏选择View - System Viewer - GPIO - GPIOA。这里可以实时看到GPIOA所有寄存器的值包括IDR输入数据、ODR输出数据、CRL/CRH配置寄存器。单步执行你的代码观察ODR寄存器相应位的变化可以最准确地判断程序是否按预期控制引脚。逻辑分析仪Logic Analyzer这是分析时序的终极武器。在调试模式下点击View - Analysis Windows - Logic Analyzer。点击“Setup”添加要观察的信号例如GPIOA.0,GPIOA.1。然后全速运行程序你就能看到这些引脚电平随时间变化的波形图清晰展示流水灯的时序是否符合500ms间隔。5.5 问题速查表现象可能原因排查步骤LED完全不亮1. 硬件断路/短路2. 电源异常3. GPIO时钟未使能4. 程序未下载/运行1. 万用表测通路和电压2. 查供电和3.3V电压3. 检查MX_GPIO_Init函数4. 检查下载器连接、启动模式LED常亮/常灭1. 电平逻辑定义反共阴/共阳2. 引脚配置模式错误如配成输入3. 引脚冲突如用了调试脚1. 用SET/RESET直接测试修正宏定义2. 在CubeMX中确认引脚模式为GPIO_Output3. 更换非调试引脚测试流水速度不对1. 系统时钟配置错误2.HAL_Delay被其他中断打断1. 检查CubeMX时钟树配置确认SYSCLK频率2. 简化代码屏蔽其他中断测试只有部分LED亮1. 特定LED或电阻损坏2. 特定引脚配置或代码错误1. 交换LED和电阻测试2. 单独测试该引脚的输出代码6. 从阻塞延时到中断驱动的进阶优化前面的阻塞式延时虽然简单但浪费了CPU资源。在实际项目中我们更倾向于使用定时器中断来实现非阻塞的定时控制让CPU在LED切换的间隔里可以处理其他任务如扫描按键、读取传感器。6.1 使用基本定时器实现非阻塞流水灯我们以STM32F103的通用定时器TIM2为例。CubeMX配置定时器在“Pinout Configuration”页左侧“Timers”中选择“TIM2”。将“Clock Source”设为“Internal Clock”内部时钟。在“Parameter Settings”中Prescaler预分频器71。定时器时钟APB1总线上的TIM2时钟通常是72MHz。Prescaler 72 - 1目的是将时钟分频为1MHz。计算72MHz / (711) 1MHz。Counter ModeUp向上计数。Counter Period自动重装载值ARR999。定时器从0计数到999共1000个计数触发一次更新中断Update Interrupt。结合1MHz的计数频率中断周期为 1000 / 1MHz 1ms。auto-reload preloadEnable使能自动重装载。在“NVIC Settings”中勾选“TIM2 global interrupt”使能中断。生成代码并修改生成代码后在stm32f1xx_it.c文件中找到TIM2_IRQHandler中断服务函数。在/* USER CODE BEGIN TIM2_IRQn 0 */和/* USER CODE END TIM2_IRQn 0 */之间添加中断处理逻辑。更优雅的做法是在main.c中编写一个回调函数。首先在main.c的/* USER CODE BEGIN 0 */处声明一个全局的毫秒计数器volatile uint32_t g_millis_tick 0; // volatile 防止编译器优化然后重写定时器更新中断回调函数HAL库机制。在main.c的/* USER CODE BEGIN 4 */区域添加void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim-Instance TIM2) { g_millis_tick; // 每1ms增加1 } }在main函数的/* USER CODE BEGIN 2 */区域启动定时器中断HAL_TIM_Base_Start_IT(htim2);实现非阻塞延时和流水灯在main.c中定义非阻塞延时函数和状态控制变量/* USER CODE BEGIN PV */ volatile uint32_t g_millis_tick 0; uint32_t led_last_time 0; uint8_t led_state 0; // 0: LED0亮, 1: LED1亮, 2: LED2亮 /* USER CODE END PV */ /* USER CODE BEGIN 4 */ // 非阻塞延时检查函数 uint8_t check_time_elapsed(uint32_t *last_time, uint32_t interval) { uint32_t current g_millis_tick; if(current - *last_time interval) { *last_time current; return 1; } return 0; } /* USER CODE END 4 */修改while(1)循环while (1) { // 检查500ms是否到达 if(check_time_elapsed(led_last_time, 500)) { // 关闭所有LED LED_OFF(LED0_PIN); LED_OFF(LED1_PIN); LED_OFF(LED2_PIN); // 根据状态点亮对应LED switch(led_state) { case 0: LED_ON(LED0_PIN); break; case 1: LED_ON(LED1_PIN); break; case 2: LED_ON(LED2_PIN); break; } // 更新状态 led_state; if(led_state 3) led_state 0; } // 此处CPU可以执行其他任务例如按键扫描 // HAL_Delay(10); // 如果还有其他短延时任务可以用小延时但不会阻塞500ms }现在while循环不再被HAL_Delay(500)阻塞。它快速循环每次检查是否到了该切换LED的时间点。在等待的500ms内CPU可以高效地执行// 此处...注释位置的其他任务代码。这才是嵌入式系统多任务协作的雏形。6.2 使用PWM实现呼吸灯效果掌握了定时器中断就可以轻松进阶到PWM脉冲宽度调制控制实现LED的亮度渐变呼吸灯。PWM通过快速开关引脚改变一个周期内高电平所占的比例占空比来模拟不同的平均电压从而控制LED亮度。CubeMX配置PWM仍然使用TIM2选择一个通道如Channel 1对应到某个GPIO引脚如PA0。模式选择“PWM Generation CH1”。参数设置Prescaler711MHz计数频率Counter Period999PWM周期为1ms即1kHz频率对人眼无闪烁感。此时“Pulse”参数就是占空比初始值可以先设为50050%占空比。生成代码与实现呼吸生成代码后在main.c的/* USER CODE BEGIN 2 */启动PWMHAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);在while(1)循环中通过改变TIM2-CCR1寄存器捕获/比较寄存器1的值来改变占空比实现呼吸效果uint16_t pwm_val 0; int8_t dir 1; // 方向1为增加-1为减少 while (1) { HAL_Delay(10); // 每10ms改变一次亮度控制呼吸速度 pwm_val dir * 10; // 每次改变10 if(pwm_val 1000) { pwm_val 1000; dir -1; } else if(pwm_val 0) { pwm_val 0; dir 1; } __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pwm_val); // 设置新的占空比 }这样连接到PA0的LED就会呈现从暗到亮再从亮到暗的循环呼吸效果。通过这个实验你将GPIO的简单开关控制升级到了模拟输出控制为后续控制电机速度、舵机角度等应用打下了基础。从点亮一个LED到让它按你的意愿流动、呼吸这个过程正是嵌入式开发乐趣与成就感的源泉。每一次调试成功的背后都是对硬件原理和软件逻辑更深一层的理解。希望这篇超详细的流水灯实验指南能成为你嵌入式探索之路上一块坚实的垫脚石。记住所有复杂的系统都是由这样一个个基础模块构建起来的。当你下次看到绚丽的灯效时不妨想想它也许就是成千上万个“流水灯”精妙协作的结果。