上一篇文章链接:STM32"零代码”实现按键控制LED流水灯
书接上回,在上文中实现了按键控制流水灯工作模式。
/* 主循环 */while (1){/* 检测按键是否按下,按下则切换模式 */if(KEY_Check() == 1){LED_Mode_Switch(); // 切换流水灯模式LED_All_Off(); // 切换模式前熄灭所有LED,避免混亮HAL_Delay(200); // 防连按:短延时避免一次按键切换多次}/* 根据当前模式执行对应流水灯函数 */switch(current_mode){case MODE_WATERFALL_1:LED_Waterfall_1();break;case MODE_WATERFALL_SINGLE:LED_Waterfall();break;case MODE_WATERFALL_GROUP:LED_Waterfall_Group();break;default:current_mode = MODE_WATERFALL_1; // 异常时默认模式1break;}}
以上为main.c文件中,主循环中的代码。
while是 C 语言中的一种循环控制结构,用于在特定条件为真时重复执行一段代码。
while 循环的语法如下:
while (条件表达式) {// 循环体:条件为真时执行的代码}
while (1) while (1)单片机程序中,为什么要写 死循环?你想知道在单片机程序里为什么一定要写死循环,这个问题问到了单片机编程的核心特点,非常关键。简单来说,单片机作为嵌入式系统,它的程序和电脑上的程序运行逻辑完全不同:电脑程序运行完一个任务就会退出,但单片机需要持续上电运行、处理实时任务,死循环正是为了让程序能一直 “活” 着,持续执行我们设定的功能。死循环的核心作用(结合单片机特性)1. 防止程序 “跑飞”,保证系统持续运行单片机的程序是从main() 函数开始执行的,如果没有死循环,main() 函数执行完最后一行代码后,程序计数器会继续指向内存中后面的未知地址,导致:程序执行乱码、硬件行为不可控(比如引脚电平乱跳);触发单片机的复位(不同内核表现不同),系统反复重启,无法稳定工作。死循环(比如while(1))会让程序一直卡在循环体内,避免执行到未知内存区域,保证系统 “不跑偏”。2. 持续处理实时任务单片机的核心价值是实时响应外部事件(比如按键按下、传感器数据变化、串口接收数据等),这些事件都是随机发生的,需要程序一直 “监听” 和处理:3. 适配单片机的 “裸机” 运行模式大部分入门级单片机程序是 “裸机编程”(无操作系统),没有任务调度器,所有逻辑都需要在死循环里靠 “轮询” 或 “中断” 实现:轮询:在死循环里依次检查各个外设 / 事件(比如先查按键,再读传感器,再处理串口);中断:即使在死循环里,外部事件(比如定时器到点、按键中断)也能打断循环,处理完中断后又回到死循环继续运行。
常见的死循环写法
// 最常用(推荐):51/STM32/Arduino等通用while(1) {// 业务逻辑}// 等效写法(可读性稍差)for(;;) {// 业务逻辑}// 极端情况(慎用,可能屏蔽中断)while(1); // 空死循环,一般用于程序出错后“卡死”

HAL_Delay(200); voidLED_Waterfall(void){/* 正序点亮:仅1灯亮 */for (int i = LED0; i <= LED9; i++){LED_All_Off();LED_On((LED_Name)i);HAL_Delay(100);}/* 逆序熄灭:仅1灯亮 */for (int i = LED9; i >= LED0; i--){LED_Off((LED_Name)i);if (i > LED0){LED_On((LED_Name)(i-1));}HAL_Delay(100);}LED_All_Off();}
首先,先给你一个整体结论:这段程序实现了10 个 LED 灯(LED0~LED9)从左到右逐个点亮(仅当前灯亮),再从右到左逐个熄灭(仍仅当前灯亮) 的流水灯效果,最后关闭所有 LED,完成一轮流水灯循环。完整代码逐段解释1. 函数整体说明voidLED_Waterfall(void)这是一个无返回值、无参数的函数,函数名LED_Waterfall 直译就是 “LED 瀑布”,对应流水灯的视觉效果(灯像水流一样依次亮灭)。2. 第一部分:正序点亮(仅 1 灯亮)/* 正序点亮:仅1灯亮 */for (int i = LED0; i <= LED9; i++){LED_All_Off(); // 先关闭所有LEDLED_On((LED_Name)i); // 点亮第i个LEDHAL_Delay(100); // 延时100毫秒,保持当前状态}核心逻辑:让 LED 从LED0 到LED9 逐个 “单独点亮”,形成 “流水向右走” 的效果。逐行拆解:for (int i = LED0; i <= LED9; i++):循环变量i 从 LED0(第一个灯)开始,到 LED9(第十个灯)结束,每次加 1,对应正序遍历所有 LED。LED_All_Off();:每次循环先关闭所有LED,确保同一时间只有 1 个灯亮(避免多个灯同时亮)。LED_On((LED_Name)i);:点亮第 i 个 LED,(LED_Name)i 是把整型 i 强制转换为 LED 名称的枚举类型(比如LED0/LED1 是提前定义的枚举常量),适配 LED_On 函数的参数要求。HAL_Delay(100);:调用 STM32 的 HAL 库延时函数,延时 100ms(0.1 秒),让当前点亮的 LED 保持亮的状态,人眼能清晰看到流水效果。执行效果示例:i=LED0:全灭 → 亮 LED0 → 保持 100msi=LED1:全灭 → 亮 LED1 → 保持 100ms...i=LED9:全灭 → 亮 LED9 → 保持 100ms最终正序结束时,只有 LED9 是亮的。3. 第二部分:逆序熄灭(仅 1 灯亮)/* 逆序熄灭:仅1灯亮 */for (int i = LED9; i >= LED0; i--){LED_Off((LED_Name)i); // 关闭第i个LEDif (i > LED0) // 避免i=LED0时,i-1超出范围{LED_On((LED_Name)(i-1)); // 点亮前一个LED}HAL_Delay(100); // 延时100毫秒,保持当前状态}核心逻辑:从LED9 往 LED0 方向,逐个关闭当前灯、点亮前一个灯,形成 “流水向左走” 的效果。逐行拆解:for (int i = LED9; i >= LED0; i--):循环变量i 从 LED9 开始,到 LED0 结束,每次减 1,对应逆序遍历所有 LED。LED_Off((LED_Name)i);:先关闭第 i 个 LED(比如 i=LED9 时,先灭 LED9)。if (i > LED0):判断是否不是第一个灯(LED0),防止 i-1 变成负数(比如 i=LED0 时,i-1=LED-1,超出定义范围)。LED_On((LED_Name)(i-1));:如果不是第一个灯,点亮前一个 LED(比如 i=LED9 时,点亮 LED8;i=LED8 时,点亮 LED7,以此类推)。HAL_Delay(100);:延时 100ms,保持当前 “仅前一个灯亮” 的状态。执行效果示例(承接正序结束后 LED9 亮的状态):i=LED9:灭 LED9 → 亮 LED8 → 保持 100msi=LED8:灭 LED8 → 亮 LED7 → 保持 100ms...i=LED1:灭 LED1 → 亮 LED0 → 保持 100msi=LED0:灭 LED0 → 无点亮操作(因为 i 不大于 LED0)→ 保持 100ms最终逆序结束时,所有 LED 都灭了(代码里最后还会显式关一次,双重保障)。4. 最后一步:关闭所有 LEDLED_All_Off(); // 确保流水灯循环结束后,所有LED都熄灭这行是 “兜底” 操作,即使逆序循环结束后有个别灯没灭,也能强制关闭所有 LED,保证一轮流水灯结束后状态干净。完整执行流程可视化为了更直观,用 LED0~LED9 的亮灭状态展示一轮完整流程:正序阶段:LED0 亮→LED1 亮→...→LED9 亮(每个灯单独亮,其余灭,每次停 100ms)逆序阶段:LED9 灭、LED8 亮→LED8 灭、LED7 亮→...→LED1 灭、LED0 亮→LED0 灭(每个灯单独亮,其余灭,每次停 100ms)最终状态:所有 LED 熄灭
豆包:用通俗的语言或用生活中的例子说明一下stm32中的中断
你想让我用通俗的语言和生活例子讲明白 STM32 里的中断,这个需求特别好 —— 中断是单片机的核心难点,用生活化的场景类比,一下子就能懂。先给你一个核心结论:STM32 的中断,就像你正在做一件事时,突然有紧急情况需要你立刻处理,处理完后再回到原来的事继续做,本质是 “打断当前任务、优先处理紧急事、处理完回归原任务” 的机制。用生活例子彻底讲透中断我用 “你在家做饭” 这个场景,对应 STM32 的 “主循环执行常规任务”,一步步类比:场景 1:无中断的 “傻等”(轮询方式)假设你正在厨房煮米饭(对应 STM32 主循环里的常规任务,比如while(1)里的 LED 闪烁),同时还要等快递上门(对应需要处理的外部事件,比如按键按下、串口收数据)。如果没有 “中断”(相当于没有手机),你只能:煮米饭煮到一半,跑门口看一眼有没有快递;回到厨房继续煮,煮一会儿又跑门口看;反复跑(对应 STM32 的 “轮询”:while(1)里不停检查按键 / 串口状态)。问题:米饭可能煮糊(主任务被频繁打断,执行不连贯);快递来了可能没及时开门(响应有延迟);你全程累(STM32CPU 空耗,效率低)。场景 2:有中断的 “高效处理”(中断方式)还是煮米饭 + 等快递,但你有手机(对应 STM32 的 “中断控制器”),快递员会给你打电话(对应 “中断触发信号”):你安心在厨房煮米饭(STM32 执行主循环的常规任务,比如控制 LED、读取传感器);快递到了,手机响了(外部事件触发中断,比如按键按下产生电平变化,触发 EXTI 中断);你立刻关火(暂停当前的主任务),接电话、去门口取快递(执行 “中断服务函数”,处理紧急事件);取完快递,回到厨房,重新开火继续煮米饭(中断处理完成,回到主循环暂停的位置,继续执行原任务)。这个过程就是 STM32 中断的核心逻辑:不主动傻等,有紧急事才立刻处理,处理完回归常态。总结中断的核心:优先处理紧急事件,避免 CPU “傻等” 外部事件,提升执行效率和响应速度;生活类比:就像你做事时被紧急事打断,处理完紧急事再回去做原来的事,STM32 的中断就是这个逻辑;关键优势:相比 “轮询”,中断让 CPU 能专注执行主任务,只有真正有需要时才响应外部事件,是单片机处理实时性任务的核心手段。
你希望将代码中原本的轮询方式检测按键、切换流水灯模式,修改为使用外部中断的方式来实现,这样可以避免主循环一直检测按键状态,提升程序的效率。修改思路GPIO 初始化修改:将 PA10 从普通输入模式改为中断输入模式,并配置中断触发方式(下降沿触发,对应按键按下)。中断服务函数配置:编写 EXTI15_10_IRQHandler 中断服务函数(PA10 对应 EXTI10),并调用 HAL 库的中断回调函数。中断回调函数实现:在回调函数中完成模式切换、LED 清零、防连按等逻辑。主循环优化:移除主循环中的按键轮询代码,只保留流水灯模式执行逻辑。
#include"main.h"/* 1. 枚举定义10个LED,便于语义化调用 */typedef enum {LED0 = 0, // 对应PA0LED1, // 对应PA1LED2, // 对应PA2LED3, // 对应PA3LED4, // 对应PA4LED5, // 对应PA5LED6, // 对应PA6LED7, // 对应PA7LED8, // 对应PA8LED9 // 对应PA9} LED_Name;/* 2. 枚举定义3种流水灯模式 */typedef enum {MODE_WATERFALL_1 = 0, // 模式1:正序点亮+逆序熄灭(多灯亮)MODE_WATERFALL_SINGLE, // 模式2:单灯依次亮+逆序灭(仅1灯亮)MODE_WATERFALL_GROUP // 模式3:分组亮灭(前5+后5)} LED_Mode;/* 3. 数组关联枚举与GPIO引脚,方便for循环遍历 */typedef struct {GPIO_TypeDef *GPIOx; // GPIO端口uint16_t GPIO_Pin; // GPIO引脚} LED_HandleTypeDef;const LED_HandleTypeDef LED_Array[] = {{GPIOA, GPIO_PIN_0}, // LED0{GPIOA, GPIO_PIN_1}, // LED1{GPIOA, GPIO_PIN_2}, // LED2{GPIOA, GPIO_PIN_3}, // LED3{GPIOA, GPIO_PIN_4}, // LED4{GPIOA, GPIO_PIN_5}, // LED5{GPIOA, GPIO_PIN_6}, // LED6{GPIOA, GPIO_PIN_7}, // LED7{GPIOA, GPIO_PIN_8}, // LED8{GPIOA, GPIO_PIN_9} // LED9};/* 全局变量:当前流水灯模式(初始为模式1) */LED_Mode current_mode = MODE_WATERFALL_1;/* 函数声明 */voidSystemClock_Config(void);staticvoidMX_GPIO_Init(void);voidLED_On(LED_Name led);voidLED_Off(LED_Name led);voidLED_All_Off(void);voidLED_Mode_Switch(void);// 模式切换函数voidHAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin); // 中断回调函数声明// 3种流水灯模式函数声明voidLED_Waterfall_1(void);voidLED_Waterfall(void);voidLED_Waterfall_Group(void);intmain(void){/* 初始化HAL库 */HAL_Init();/* 配置系统时钟为64MHz */SystemClock_Config();/* 初始化GPIO(LED+按键中断) */MX_GPIO_Init();/* 主循环:仅执行流水灯逻辑,无任何按键轮询代码 */while (1){/* 根据当前模式执行对应流水灯函数 */switch(current_mode){case MODE_WATERFALL_1:LED_Waterfall_1();break;case MODE_WATERFALL_SINGLE:LED_Waterfall();break;case MODE_WATERFALL_GROUP:LED_Waterfall_Group();break;default:current_mode = MODE_WATERFALL_1; // 异常时默认模式1break;}}}/*** @brief 流水灯模式切换函数:循环切换3种模式* @param 无* @retval 无*/voidLED_Mode_Switch(void){current_mode = (LED_Mode)((current_mode + 1) % 3);// 模式循环逻辑:0→1→2→0}/*** @brief 点亮指定LED* @param led:枚举类型的LED编号(LED0~LED9)* @retval 无* @note LED为共阳极接法(低电平点亮,高电平熄灭)*/voidLED_On(LED_Name led){HAL_GPIO_WritePin(LED_Array[led].GPIOx, LED_Array[led].GPIO_Pin, GPIO_PIN_RESET);}/*** @brief 熄灭指定LED* @param led:枚举类型的LED编号(LED0~LED9)* @retval 无*/voidLED_Off(LED_Name led){HAL_GPIO_WritePin(LED_Array[led].GPIOx, LED_Array[led].GPIO_Pin, GPIO_PIN_SET);}/*** @brief 熄灭所有LED* @param 无* @retval 无*/voidLED_All_Off(void){for (int i = LED0; i <= LED9; i++){LED_Off((LED_Name)i);}}/*** @brief 模式1:正序点亮,逆序熄灭,间隔100ms(多灯亮)* @param 无* @retval 无*/voidLED_Waterfall_1(void){/* 第一步:依次点亮10个LED(LED0→LED9) */for (int i = LED0; i <= LED9; i++){LED_On((LED_Name)i);HAL_Delay(100);}/* 第二步:逆序熄灭10个LED(LED9→LED0) */for (int i = LED9; i >= LED0; i--){LED_Off((LED_Name)i);HAL_Delay(100);}}/*** @brief 模式2:单灯依次亮→逆序灭,间隔100ms(仅1灯亮)* @param 无* @retval 无*/voidLED_Waterfall(void){/* 正序点亮:仅1灯亮 */for (int i = LED0; i <= LED9; i++){LED_All_Off();LED_On((LED_Name)i);HAL_Delay(100);}/* 逆序熄灭:仅1灯亮 */for (int i = LED9; i >= LED0; i--){LED_Off((LED_Name)i);if (i > LED0){LED_On((LED_Name)(i-1));}HAL_Delay(100);}LED_All_Off();}/*** @brief 模式3:分组亮灭(前5+后5),间隔200ms* @param 无* @retval 无*/voidLED_Waterfall_Group(void){/* 点亮前5个LED */LED_All_Off();for (int i = LED0; i <= LED4; i++){LED_On((LED_Name)i);}HAL_Delay(200);/* 熄灭前5个LED */for (int i = LED0; i <= LED4; i++){LED_Off((LED_Name)i);}/* 点亮后5个LED */for (int i = LED5; i <= LED9; i++){LED_On((LED_Name)i);}HAL_Delay(200);/* 熄灭后5个LED */for (int i = LED5; i <= LED9; i++){LED_Off((LED_Name)i);}}/*** @brief System Clock Configuration* @retval None*/voidSystemClock_Config(void){RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};/** 初始化RCC振荡器 */RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){Error_Handler();}/** 初始化CPU、AHB、APB总线时钟 */RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK){Error_Handler();}}/*** @brief GPIO初始化(LED输出+PA10中断输入)* @param 无* @retval 无*/staticvoidMX_GPIO_Init(void){GPIO_InitTypeDef GPIO_InitStruct = {0};/* 使能GPIOA时钟 */__HAL_RCC_GPIOA_CLK_ENABLE();/* 配置LED引脚:PA0~PA9 推挽输出、上拉、高速 */GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3| GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7| GPIO_PIN_8 | GPIO_PIN_9;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_PULLUP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* 配置按键引脚:PA10 下降沿中断触发、上拉 */GPIO_InitStruct.Pin = GPIO_PIN_10;GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发(按键按下为低)GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* 配置并使能EXTI15_10中断 */HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0); // 中断优先级(可按需调整)HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); // 使能中断通道/* 初始化熄灭所有LED */LED_All_Off();}/*** @brief EXTI15_10中断服务函数(PA10对应EXTI10)* @param 无* @retval 无*/voidEXTI15_10_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_10); // 调用HAL库中断处理函数,触发回调}/*** @brief GPIO中断回调函数(无消抖,按键按下立即切换模式)* @param GPIO_Pin:触发中断的引脚* @retval 无*/voidHAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){if(GPIO_Pin == GPIO_PIN_10) // 确认是PA10按键中断{LED_Mode_Switch(); // 直接切换流水灯模式LED_All_Off(); // 切换前熄灭所有LED,避免混亮__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_10); // 清除中断标志位}}/*** @brief 错误处理函数* @param 无* @retval 无*/voidError_Handler(void){__disable_irq();while (1){}}#ifdef USE_FULL_ASSERTvoidassert_failed(uint8_t *file, uint32_t line){}#endif/* USE_FULL_ASSERT */
关键修改点说明1. GPIO 初始化修改:将 PA10 的模式从GPIO_MODE_INPUT改为GPIO_MODE_IT_FALLING(下降沿触发中断)。新增HAL_NVIC_SetPriority和HAL_NVIC_EnableIRQ配置并使能 EXTI15_10 中断(PA10 属于 EXTI10,对应 EXTI15_10 中断通道)。2. 新增中断服务函数:EXTI15_10_IRQHandler:EXTI15_10 中断的服务函数,必须按照 STM32 中断向量表的命名规则编写,内部调用 HAL 库的HAL_GPIO_EXTI_IRQHandler触发回调函数。3. 中断回调函数实现:HAL_GPIO_EXTI_Callback:HAL 库的 GPIO 中断回调函数(弱函数,用户可重写),在这里实现按键的消抖、模式切换逻辑。4. 主循环优化:移除了主循环中所有按键轮询相关代码,主循环只专注于执行流水灯逻辑,程序效率更高。5. 总结按键检测从轮询方式改为外部中断方式,主循环无需持续检测按键,CPU 利用率更高。中断触发方式为下降沿触发(对应按键按下)。核心修改点包括:GPIO 中断模式配置、中断服务函数编写、中断回调函数实现、主循环按键逻辑移除。