stm32基于HAL库和中断的LED流水灯

前言

掌握STM32的基本开发方法,特别是对GPIO控制、中断机制及HAL库编程的理解,是学习嵌入式系统开发的重要基础。
本实验以STM32F103C8T6最小系统核心板为硬件平台,结合Keil MDK集成开发环境与STM32CubeMX图形化配置工具,采用HAL库进行软件开发,实现LED流水灯的周期性闪烁,并通过外部中断方式控制其运行与停止。实验不仅涵盖了GPIO的基本输入输出控制,还深入探讨了外部中断的工作原理及其在实际应用中的关键问题——如机械抖动带来的误触发现象。
通过本次实践,旨在深入理解STM32的中断响应机制,掌握HAL库中外部中断回调函数的使用方法,熟悉基于CubeMX的初始化代码生成流程,并初步掌握电路仿真工具Proteus 8.15的使用,实现从硬件设计、程序开发到系统仿真的完整闭环。这不仅提升了动手能力与系统调试技巧,也为后续学习定时器、串口通信、RTOS等高级功能打下坚实基础。

  • 本文所用工具:

注:安装Proteus8.15(注意必须不低于此版本,否则不支持仿真stm32F103)

你提供的任务是一个典型的嵌入式系统综合实践项目,涉及 STM32中断机制、HAL库开发、Keil + CubeMX 联合开发、Proteus 电路仿真与联调 等核心技能。下面我将为你系统性地拆解并指导完成该任务的每一步,确保你可以顺利完成作业并深入理解原理。


一、LED 流水灯 + 外部中断控制启停

1、开发环境搭建

所需软件安装清单:

软件 用途 下载建议
STM32CubeMX 图形化配置引脚、时钟、中断、生成HAL初始化代码 官网免费下载
Keil MDK (uVision5) 编写、编译、调试C代码 使用评估版或正版授权
Proteus 8.15 SP2 或更高版本 电路设计与STM32仿真 注意必须支持 STM32F103C8

⚠️ 提示:旧版Proteus不支持STM32仿真,务必确认版本 ≥ 8.15,并安装了 Proteus VSM for STM32 插件。


2、硬件连接设计(物理板 & Proteus)

物理连接参考(最小系统板 STM32F103C8T6):

功能 引脚分配(示例) 说明
LED_RED PA8 共阳极接法(亮=0)
LED_GREEN PB14
LED_BLUE PC13 注意PC13是常用LED引脚
开关输入 PA5 上拉电阻,外部中断输入

💡 LED 接法建议:阴极接地,阳极通过电阻接MCU → MCU输出低电平时点亮(更安全)


3、使用 STM32CubeMX 配置工程

步骤1:创建新项目
1 打开 STM32CubeMX,并点击

在这里插入图片描述
2. 选择芯片:STM32F103C8T6,收藏后点击右上角Start Project
在这里插入图片描述
3.在RCC中选择外部高速晶振(只有选择了这个之后才能在后续配置时钟树)
在这里插入图片描述
4. 在SYS中选择调试接口
在这里插入图片描述

注:如果不选择的话就只能下载一次代码,后续下载之后可能需要点击复位键,比较麻烦

为了增强大家对后续操作的理解,这里简单补充关于时钟树的相关知识
在这里插入图片描述
当然可以!以下是极简版 STM32 时钟树,用一张简单表格告诉你所有关键点,适合初学者快速理解:

名称 是什么 频率 用途
HSI 内部时钟(自带) 8 MHz 开机默认,不用外接
HSE 外部晶振(小水晶) 8 MHz 精准时钟,用于主系统
PLL 锁相环(倍频器) 72 MHz 把 8MHz 放大到 72MHz
SYSCLK 主时钟(系统心跳) 最高 72 MHz 决定 CPU 跑多快
HCLK CPU 时钟 通常 = SYSCLK(72MHz) 给 CPU 和内存用
PCLK1 低速外设时钟 通常 36 MHz 给 I2C、UART、TIM2/3/4 等
PCLK2 高速外设时钟 通常 72 MHz 给 GPIO、USART1、TIM1 等
LSE 外部低速晶振 32.768 kHz 给“闹钟”(RTC)用
LSI 内部低速时钟 ~40 kHz 给看门狗(IWDG)用

其中PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。 通过倍频之后作为系统时钟的时钟源。
所以我们后续选择HSE ,然后PLL进行倍频

步骤2:配置时钟(Clock Configuration)
1、进入 Clock Configuration 标签页
在这里插入图片描述

  • 设置 HSE 为 Crystal/Ceramic Resonator
  • 将系统时钟(SYSCLK)设置为 72MHz(典型值)
    • HCLK = 72MHz
    • PCLK1 = 36MHz, PCLK2 = 72MHz

效果如下
在这里插入图片描述

步骤3:配置 GPIO 引脚

引脚 Mode 备注
PA5 GPIO_EXTI5 外部中断输入
PA8 GPIO_Output 输出控制红灯
PB14 GPIO_Output 输出控制绿灯
PC13 GPIO_Output 输出控制蓝灯

效果如下:
在这里插入图片描述

将PA8、PB14、PC13均配置为以下模式,推挽输出
在这里插入图片描述

将PA5配置为下拉输入
在这里插入图片描述

步骤4:配置 NVIC(中断使能)

中断是指CPU在执行当前程序时系统出现了某种状况,使得CPU必须停止当前程序,而去执行另一段程序来处理的出现的紧急事务,处理结束后CPU再返回到原先暂停的程序继续执行,这个过程就称为中断。
使得计算机系统具备应对对处理突发事件的能力,使其能及时响应紧急事件。
提高处理器效率,如果没有中断系统,CPU就只能按照原来的程序编写的先后顺序,对各个外设进行查询和处理,即轮询工作方式,轮询方法貌似公平,但实际工作效率却很低。

  • 进入 NVIC Settings 标签页
  • 勾选中断
    在这里插入图片描述

在这里插入图片描述

  • Enable Interrupt ✅
  • Preemption Priority: 0
  • Sub Priority: 0

步骤5:生成代码

  • Project Manager → 设置项目名、路径、工具链为 MDK-ARM V5
    在这里插入图片描述

  • Code Generator Options:

    • 勾选 Generate peripheral initialization as a pair of '.c/.h' files per peripheral
      在这里插入图片描述
  • 点击右上角 Generate Code生成代码
    在这里插入图片描述
    在这里插入图片描述
    若出现下面这种情况:
    在这里插入图片描述

这个提示的意思是:

你的项目需要一个特定的固件包(STM32Cube FW_F1 V1.8.6),但你本地的 STM32CubeMX 仓库中没有这个版本。

解决:
点击菜单栏:
Help → Manage Embedded Software Packages
在这里插入图片描述
然后点击Install下载即可


4、Keil 中编写主程序逻辑

修改 main.c 文件中的用户代码区域

uint8_t led_enable = 1;  // 流水灯使能标志:0-周期闪烁,1-流水灯
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */
  // 初始化LED状态:全部熄灭
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
      // 流水灯模式:依次点亮PA8→PB14→PC13
      // 先关闭所有LED
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
      
      // 根据状态索引点亮对应LED
      switch(led_state)
      {
        case 0:
          HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);  // 点亮PA8
          break;
        case 1:
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET); // 点亮PB14
          break;
        case 2:
          HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 点亮PC13
          break;
      }
      led_state = (led_state + 1) % 3;  // 循环切换状态
      HAL_Delay(1000);  // 每个LED保持1s

    /* USER CODE END 3 */
  }
  /* USER CODE END 3 */
}

效果如下:

3LED

添加LED状态索引:
uint8_t led_state = 0; // 流水灯状态索引
加入中断函数:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_5)  // 确认是PA5触发的中断
  {
    // 读取PA5当前电平状态
    GPIO_PinState pin_level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5);
    
    if (pin_level == GPIO_PIN_SET)  // 高电平:启动流水灯
    {
      led_enable = 1;
    }
    else  // 低电平:切换到周期闪烁
    {
      led_enable = 0;
      // 重置LED状态(避免切换时混乱)
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
      led_state = 0;
    }
    
    // 简单消抖(避免机械开关抖动)
    HAL_Delay(10);
  }
}

完整代码:

/* USER CODE BEGIN PV */
uint8_t led_enable = 0;  // 流水灯使能标志:0-周期闪烁,1-流水灯
uint8_t led_state = 0;   // 流水灯状态索引
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */
  // 初始化LED状态:全部熄灭
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    if (led_enable)
    {
      // 流水灯模式:依次点亮PA8→PB14→PC13
      // 先关闭所有LED
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
      
      // 根据状态索引点亮对应LED
      switch(led_state)
      {
        case 0:
          HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);  // 点亮PA8
          break;
        case 1:
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET); // 点亮PB14
          break;
        case 2:
          HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 点亮PC13
          break;
      }
      led_state = (led_state + 1) % 3;  // 循环切换状态
      HAL_Delay(1000);  // 每个LED保持500ms
    }
    else
    {
      // 周期闪烁模式:三灯同时亮灭
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
    }
    /* USER CODE END 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  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();
  }
}

/* USER CODE BEGIN 4 */
/**
  * @brief  外部中断回调函数(处理PA5的电平变化)
  * @param  GPIO_Pin: 触发中断的引脚
  * @retval None
  */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_5)  // 确认是PA5触发的中断
  {
    // 读取PA5当前电平状态
    GPIO_PinState pin_level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5);
    
    if (pin_level == GPIO_PIN_SET)  // 高电平:启动流水灯
    {
      led_enable = 1;
    }
    else  // 低电平:切换到周期闪烁
    {
      led_enable = 0;
      // 重置LED状态(避免切换时混乱)
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
      led_state = 0;
    }
    
    // 简单消抖(避免机械开关抖动)
    HAL_Delay(10);
  }
}
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  // 错误时通过PA8闪烁提示
  while (1)
  {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_8);
    HAL_Delay(500);
  }
  /* USER CODE END Error_Handler_Debug */
}

🔍 关于抖动说明

  • 杜邦线插拔会产生“电平反复跳变”,可能触发多次中断。
  • HAL_Delay(10) 是一种简单的软件消抖方法。
  • 更优方案:定时器+状态机消抖,但本任务中可用此简化处理。

程序效果如下:

中断LED


二、Proteus 仿真

步骤1:打开 Proteus 8.15+

  1. 新建项目,填写工程名称和路径在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. 点击左上角的P搜索器件:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

步骤2:绘制电路图

双击芯片
在这里插入图片描述

添加hex文件路径
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

步骤3:配置 STM32 属性

  • 双击 STM32F103C8 → 设置:
    • Program File: 选择你 Keil 编译生成的 .hex 文件(位于 Project\MDK-ARM\Objects\your_project.hex

步骤4:运行仿真

  • 点击左下角 ▶️ 运行

效果如下:

仿真


三、软件版本控制协议和git工具的使用。

1. 理解版本控制协议

  • 什么是版本控制?
    版本控制是一种记录一个或多个文件内容变化,以便将来查阅特定版本的系统。它能帮助你:
    • 回溯历史版本(比如发现代码出错,可以回到上一个正确的版本)。
    • 协同开发(多人同时修改代码,系统能合并改动)。
    • 分支管理(尝试新功能时,可以创建分支,不影响主代码)。
  • Git 是什么?
    Git 是目前最流行的分布式版本控制系统。它由 Linus Torvalds(Linux 内核创始人)开发,速度快、效率高、安全性好。
  • GitHub / Gitee 是什么?
    • GitHub: 全球最大的代码托管平台,基于 Git 构建。它提供免费的公共仓库和付费的私有仓库。
    • Gitee (码云): 中国本土的代码托管平台,同样基于 Git,访问速度快,对中文用户友好。

2. 注册账号
选择其中一个平台注册账号:

建议:如果你的项目希望被全球开发者看到,选 GitHub;如果更注重国内访问速度和中文社区,选 Gitee。

3. 下载并安装 Git 客户端

  1. 访问 Git 官方网站:https://git-scm.com
  2. 下载对应你操作系统(Windows / macOS / Linux)的版本。
  3. 安装时,大部分选项保持默认即可。安装完成后,你可以通过命令行(终端)使用 git 命令。

4. 配置 Git(首次安装后必须)
打开命令行工具(Windows: Git Bash 或 CMD;macOS/Linux: Terminal),运行以下命令配置你的身份信息:

# 设置你的用户名(可以是真实姓名或昵称)
git config --global user.name "你的名字"

# 设置你的邮箱(必须与 GitHub/Gitee 账号注册邮箱一致)
git config --global user.email "你的邮箱@example.com"

# (可选)设置默认文本编辑器,比如 VS Code
git config --global core.editor "code --wait"

5. 创建本地仓库并提交代码

假设你的项目主目录是 my_project,路径为 C:\Users\YourName\my_project(Windows)或 /Users/YourName/my_project(macOS/Linux)。

步骤 1:初始化本地仓库

# 进入你的项目主目录
cd /path/to/your/my_project

# 初始化 Git 仓库(会在目录下创建一个隐藏的 .git 文件夹)
git init

步骤 2:将文件添加到暂存区

# 将项目主目录下所有文件添加到暂存区
git add .

注意git add . 会添加当前目录下所有文件。如果有些文件不想被版本控制(如编译生成的文件、配置文件等),可以创建 .gitignore 文件来忽略它们。

步骤 3:提交到本地仓库

# 提交更改,并添加提交信息
git commit -m "Initial commit: add all project files"

-m 后面是提交信息,要简洁明了地描述这次提交的内容。

6. 在 GitHub/Gitee 上创建远程仓库

  1. 登录你的 GitHub 或 Gitee 账号。
  2. 点击 “New” 或 “+” 创建一个新的仓库。
  3. 填写仓库名称(如 my_project),选择公开或私有,不要勾选“Initialize this repository with a README”(因为我们已经有本地代码了)。
  4. 创建仓库后,你会看到一个 HTTPS 或 SSH 的仓库地址,例如:
    • HTTPS: https://github.com/你的用户名/my_project.git
    • SSH: git@github.com:你的用户名/my_project.git

7. 连接本地仓库与远程仓库,并推送代码
复制下面的这个地址
在这里插入图片描述

# 添加远程仓库地址(URL 替换为你在 GitHub/Gitee 上看到的地址)
git remote add origin https://github.com/你的用户名/my_project.git

# 推送本地代码到远程仓库的 main 分支
git push -u origin main

在这里插入图片描述

刷新之后即可看到你上传的工程了

在这里插入图片描述

说明

  • origin 是远程仓库的默认名称,你可以自定义。
  • -u 参数会设置上游分支,以后推送可以直接用 git push
  • 如果你的默认分支是 master,则使用 git push -u origin master

首次推送可能需要输入你的 GitHub/Gitee 账号密码。为了更安全和方便,建议配置 SSH 密钥。

8. 常用 Git 命令速查表

命令 作用
git init 初始化一个新的 Git 仓库
git clone <URL> 克隆远程仓库到本地
git add <file> 将文件添加到暂存区
git add . 添加所有修改的文件到暂存区
git commit -m "message" 提交暂存区的更改到本地仓库
git status 查看当前仓库状态(哪些文件被修改、未提交等)
git log 查看提交历史
git push 将本地提交推送到远程仓库
git pull 从远程仓库拉取最新更改并合并到本地
git branch 列出所有分支
git checkout <branch> 切换到指定分支
git merge <branch> 合并指定分支到当前分支

9. 验证是否成功

  1. 刷新你的 GitHub/Gitee 仓库页面。
  2. 你应该能看到你项目中的所有文件,而不是一个压缩包。
  3. 提交历史中会显示你的第一次提交记录。

四、总结

你已完成以下能力训练:

  • ✅ STM32 HAL库开发流程
  • ✅ GPIO输入输出控制
  • ✅ 外部中断 EXTI 编程
  • ✅ 软件消抖基本思想
  • ✅ STM32CubeMX + Keil 联合开发
  • ✅ Proteus 绘图与仿真
  • ✅ 硬件与软件协同调试
  • ✅ 并学会了如何在GitHub上上传自己的项目

📌 如本文对你有帮助,欢迎点赞 ✅、收藏 ⭐、关注 💡,你的支持是我持续创作的最大动力!

💬 有任何问题或建议,欢迎在评论区留言,我会一一回复!
(如需进一步交流,欢迎查看个人主页简介~)

Logo

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

更多推荐