news 2026/9/6 6:29:51

初识状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
初识状态机

1.前言

在STM32的入门项目中,按键控制LED大概是最常见的练手实验了。但很多初学者习惯在中断服务函数(ISR)里直接做翻转LED、延时消抖等操作——这在工程上其实是不推荐的写法。中断服务函数应该遵循 “短、小、快” 的原则。

本文从一个实际项目出发,逐步讲解如何用 “中断置标志 + 主循环状态机” 的架构,优雅地处理多个按键的中断事件。这套模式在嵌入式开发中非常经典

2.实验器材

STM32F407开发板(标准库)

三个按键:KEY_UP(PA0)、KEY0(PE4)、KEY1(PE3)

两个LED:LED1(PF9)、LED2(PF10)

3.功能需求

KEY_UP 按下 → 两个LED同时翻转

KEY0 按下 → 仅翻转 LED1(PF9)

KEY1 按下 → 仅翻转 LED2(PF10)

4.设计思路

传统的写法是在每个按键的中断服务函数里直接调用 GPIO_ToggleBits() 翻转LED。这样做虽然简单,但有两个问题:

中断函数变“重”:翻转LED虽然不耗时,但如果后续需要加入更复杂的逻辑(比如长按/短按判断、组合键等),中断函数会越来越臃肿。

违反“短小快”原则:中断服务函数应该只做最轻量级的工作。

更优雅的做法是 “中断只负责通知,主循环负责干活”:

中断服务函数:检测到按键按下 → 设置一个全局标志位 → 立即退出

主循环(while(1)):检测标志位 → 执行对应的LED翻转动作 → 清除标志位

这就是 事件驱动架构 的雏形

5.volatile关键字的作用

在中断和主循环之间共享的变量,必须使用 volatile 关键字修饰。

原因:编译器默认会对变量进行优化,可能会把变量的值缓存在寄存器中,而不是每次从内存读取。但在中断程序中,变量的值可能在任意时刻被异步修改——编译器并不知道这一点。volatile 的作用就是告诉编译器:“这个变量随时可能被外部改变,每次使用时都必须从内存重新读取,不要做任何优化”。

// 正确用法volatileuint8_tkey_event=0;// 错误用法(编译器可能优化,导致主循环永远读不到变化)uint8_tkey_event=0;

6.完整代码

6.1全局变量定义

#include"stm32f4xx.h"#include"led.h"#include"delay.h"// 全局事件标志(volatile 防止编译器优化)// 0=无事件,1=KEY_UP,2=KEY0,3=KEY1volatileuint8_tkey_event=0;

6.2按键GPIO初始化

voidKEY_Init(void){GPIO_InitTypeDef GPIO_InitStructure;// 使能 GPIOA 和 GPIOE 时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);// ----- KEY_UP (PA0) 下拉输入(高电平有效)-----GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_DOWN;// 下拉GPIO_Init(GPIOA,&GPIO_InitStructure);// ----- KEY0 (PE4) 上拉输入(低电平有效)-----GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;// 上拉GPIO_Init(GPIOE,&GPIO_InitStructure);// ----- KEY1 (PE3) 上拉输入(低电平有效)-----GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;// 上拉GPIO_Init(GPIOE,&GPIO_InitStructure);}

6.3EXTI配置

voidEXTI_Config(void){EXTI_InitTypeDef EXTI_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;// 使能 SYSCFG 时钟(用于外部中断映射)RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);// 映射引脚到 EXTI 通道SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0);// PA0 → EXTI0SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource4);// PE4 → EXTI4SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource3);// PE3 → EXTI3// ----- EXTI0(PA0,上升沿触发)-----EXTI_InitStructure.EXTI_Line=EXTI_Line0;EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising;// 高有效 → 上升沿EXTI_InitStructure.EXTI_LineCmd=ENABLE;EXTI_Init(&EXTI_InitStructure);// ----- EXTI4(PE4,下降沿触发)-----EXTI_InitStructure.EXTI_Line=EXTI_Line4;EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;// 低有效 → 下降沿EXTI_Init(&EXTI_InitStructure);// ----- EXTI3(PE3,下降沿触发)-----EXTI_InitStructure.EXTI_Line=EXTI_Line3;EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;EXTI_Init(&EXTI_InitStructure);// ----- NVIC 优先级配置(分组2:2位抢占 + 2位响应)-----NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// KEY_UP 优先级最高(抢占=0)NVIC_InitStructure.NVIC_IRQChannel=EXTI0_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);// KEY0(抢占=1,响应=0)NVIC_InitStructure.NVIC_IRQChannel=EXTI4_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_Init(&NVIC_InitStructure);// KEY1(抢占=1,响应=1)NVIC_InitStructure.NVIC_IRQChannel=EXTI3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);}

6.4主函数(状态机事件分发)

intmain(void){// 系统初始化delay_init(168);LED_Init();KEY_Init();EXTI_Config();while(1){// ---- 状态机核心:事件检测与分发 ----if(key_event!=0){switch(key_event){case1:// KEY_UP:两个LED同时翻转LED1_TOGGLE();LED2_TOGGLE();break;case2:// KEY0:仅翻转 LED1LED1_TOGGLE();break;case3:// KEY1:仅翻转 LED2LED2_TOGGLE();break;default:break;}// 事件处理完毕,清除标志key_event=0;}// 主循环可以执行其它任务...}}

6.5LED驱动(led.h)

#ifndef__LED_H#define__LED_H#include"stm32f4xx.h"#defineLED1_PINGPIO_Pin_9#defineLED2_PINGPIO_Pin_10#defineLED1_ON()GPIO_SetBits(GPIOF,LED1_PIN)#defineLED1_OFF()GPIO_ResetBits(GPIOF,LED1_PIN)#defineLED1_TOGGLE()GPIO_ToggleBits(GPIOF,LED1_PIN)#defineLED2_ON()GPIO_SetBits(GPIOF,LED2_PIN)#defineLED2_OFF()GPIO_ResetBits(GPIOF,LED2_PIN)#defineLED2_TOGGLE()GPIO_ToggleBits(GPIOF,LED2_PIN)voidLED_Init(void);#endif
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 6:28:13

CUPT2027赛题选题策略:五维评分法与实验路径详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 6:25:31

从Shanks洛克瞬秒看中单技能链与进场时机

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 6:17:54

2026年9月GEO内容优化公司推荐:5家候选机构应该如何验收?

验收GEO内容,不能只看稿子写得顺不顺——真正该核对的是内容跟诊断结果对不对得上、发布状态能不能追踪。第一步:内容方向是否绑定具体缺口每篇稿子的选题应该能说清楚它对应的是哪类问题——是补实体识别的事实,还是补决策理由,还…

作者头像 李华
网站建设 2026/9/6 6:17:03

AI服务标准化封装:从API调用到Agent工具的完整解决方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 6:16:53

AI编程助手Qoder、WorkBuddy、Trae安装配置与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 6:16:21

软件测试行业趋势与就业指南:从自动化到AI的高阶技能路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华