嵌入式开发实战:STM32中断机制 + Proteus仿真

前言

在嵌入式系统学习的道路上,掌握“写代码—仿真验证—版本管理”这一完整闭环,是迈向专业开发的关键一步。本文将围绕一个看似简单的任务——用外部中断控制 LED 流水灯的启停,系统性地完成以下两大模块:

  1. 基于 STM32CubeMX + HAL 库的中断驱动开发
  2. 在 Proteus 8.15 中搭建电路并加载 .hex 文件进行功能仿真

整个过程不仅锻炼了底层驱动能力,也强化了工程思维。下面,我将从原理、实现、调试到管理,一一道来。


(一)任务一:基于STM32 HAL库实现中断控制的LED流水灯

1. 任务目标

  • 使用 STM32F103C8T6 核心板;
  • 控制 3 个 LED实现周期性流水闪烁;
  • 通过 GPIOA 的PA5引脚接入“开关”(用杜邦线模拟);
  • 高电平 → 启动流水灯;低电平 → 停止流水灯
  • 使用 外部中断模式 实现开关检测;
  • 观察并分析 机械抖动导致的多次中断现象

2. 硬件连接设计

元件 STM32 引脚 说明
LED1 PC13 板载 LED,低电平点亮(注意!)
LED2 PB14 外接 LED,高电平点亮
LED3 PA8 外接 LED,高电平点亮
KEY PA5 接杜邦线,另一端可插 3.3V(高)或 GND(低)

3. STM32CubeMX配置详解

3.1 选择芯片—STM32F103C8T6

在这里插入图片描述

3.2 系统时钟设置

在这里插入图片描述
STM32中主要时钟源对比:

时钟源缩写 全称 类型 典型频率 是否需要外部元件 精度 启动时间 主要用途 优点 缺点
HSI High-Speed Internal RC oscillator 内部高速RC振荡器 8 MHz(F1/F4) 16 MHz(部分F0/L系列) ❌ 不需要 ±1% ~ ±2%(常温) 受温度/电压影响较大 快(微秒级) 系统主时钟、启动阶段、无外晶振场景 启动快、无需外部元件、成本低 精度较低,不适合高精度通信(如USB)
HSE High-Speed External crystal/ceramic resonator or clock 外部高速时钟源 4–26 MHz(常用8 MHz或25 MHz) ✅ 需要晶振 + 电容(或外部时钟信号) 高(±20–50 ppm) 较慢(毫秒级) 系统主时钟、PLL输入、USB、高精度外设 高精度、稳定性好 需额外硬件、成本略高、启动慢
PLL Phase-Locked Loop 锁相环(倍频器) 输入:HSI/2 或 HSE 输出:最高72 MHz(F1) 168 MHz(F4)等 取决于输入源(通常用HSE) 与输入源一致 中等(需锁定时间) 提供高于原始时钟的系统频率 可灵活倍频,提升性能 配置复杂,功耗略高
LSI Low-Speed Internal RC oscillator 内部低速RC振荡器 ~40 kHz(典型值32–42 kHz) ❌ 不需要 低(±10%以上) 独立看门狗(IWDG)、RTC备用时钟 无需外部元件、始终可用 频率不稳定,仅用于低精度场景
LSE Low-Speed External crystal 外部低速晶振 32.768 kHz ✅ 需要32.768 kHz晶振 + 电容 非常高(±20 ppm) 慢(数百毫秒) RTC实时时钟、低功耗定时 高精度、低功耗、适合日历/闹钟 需外部元件,启动慢
3.3 调试接口设置

在这里插入图片描述

  1. 🔍 什么是 Serial Wire(SWD)?

Serial Wire Debug(简称 SWD) 是 ARM 公司为 Cortex-M 系列处理器设计的一种轻量级、高效的调试通信协议。

✅ 它是 JTAG 的简化版,专为嵌入式微控制器优化,广泛用于 STM32、NXP、TI 等主流 MCU。

💡 核心特点:

  • 只需 2 根线 即可完成调试与烧录
  • 支持全功能调试(断点、单步、寄存器查看等)
  • 兼容性好,几乎所有现代调试器都支持
  • 比 JTAG 更节省 PCB 引脚资源
  1. 🧩 SWD 的两根线是什么?
引脚 名称 功能
SWCLK Serial Wire Clock 时钟信号,由调试器提供
SWDIO Serial Wire Data I/O 双向数据线,发送和接收数据

👉 在 STM32 上通常对应:

  • PA13 → SWDIO
  • PA14 → SWCLK

⚠️ 注意:这些引脚默认复用为调试用途,如果你不需要调试,可以手动重配置为普通 GPIO。

  1. 🆚 对比:SWD vs JTAG vs 其他调试方式
调试方式 引脚数量 功能支持 优点 缺点 适用场景
No Debug 0 ❌ 无调试 无额外引脚占用 无法调试或烧录 量产产品,仅需刷程序
Serial Wire (SWD) 2 根 ✅ 全功能调试 引脚少、速度快、兼容性强 不支持边界扫描 大多数开发项目首选
JTAG (4 pins) 4 根 ✅ 全功能 + 边界扫描 支持复杂调试、芯片级测试 占用多引脚 工业设备、高可靠性系统
JTAG (5 pins) 5 根 ✅ 全功能 + TMS/TDO/TDI/TCK/TRST 最完整调试能力 引脚最多 老旧芯片、专业测试
Trace Asynchronous Sw 2+ 根 ✅ 实时跟踪日志 可输出运行时日志(如 printf) 需要专用硬件支持 性能分析、实时监控
3.4 配置GPIO输出模式

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

📊 STM32 GPIO 模式对比总表

模式名称(STM32CubeMX 中显示) 中文名称 电气特性 是否可输出高电平 是否可输出低电平 是否需要外部上拉 典型应用场景 注意事项
Input mode 浮空输入 高阻态(无内部上下拉) 一般不推荐使用 易受噪声干扰,电平不确定
Input mode with pull-up 上拉输入 内部 ~40kΩ 上拉电阻 ✅(默认高) ✅(外部拉低) 按键检测(按键接地) 按键按下时读到低电平
Input mode with pull-down 下拉输入 内部 ~40kΩ 下拉电阻 ✅(外部拉高) ✅(默认低) 按键检测(按键接VCC) 按键按下时读到高电平
Output Push Pull 推挽输出 内部 PMOS + NMOS 驱动 ✅(直接驱动高) ✅(直接驱动低) LED、继电器、蜂鸣器、数字信号输出 驱动能力强,无需外部电阻
Output Open Drain 开漏输出 仅 NMOS 驱动(无 PMOS) ❌(需外部上拉) ✅(可拉低) ✅(必须) I²C 总线、多设备共享总线、电平转换 输出高电平时靠外部上拉,速度较慢
Alternate Function Push Pull 复用推挽输出 外设控制的推挽输出 USART_TX、SPI_SCK/MOSI、PWM 输出 用于外设功能引脚,驱动能力强
Alternate Function Open Drain 复用开漏输出 外设控制的开漏输出 ❌(需上拉) ✅(必须) I²C_SCL/SDA、CAN 总线 与标准开漏类似,但由外设自动控制
3.5 STM32 中断配置

在这里插入图片描述这里配置gpio模式为双边沿触发+输入下拉
在这里插入图片描述

  1. 📊 STM32 EXTI 模式对比表
模式名称 类型 触发方式 是否产生中断 是否调用 ISR 是否可屏蔽 典型应用场景 说明
External Interrupt Mode with Rising edge trigger detection 中断模式 上升沿(Low → High) ✅ 是 ✅ 是 ✅ 可通过 NVIC 控制 按键按下检测(上拉 + 下降沿无效) 仅在电平从低变高时触发中断
External Interrupt Mode with Falling edge trigger detection 中断模式 下降沿(High → Low) ✅ 是 ✅ 是 ✅ 可通过 NVIC 控制 按键释放检测(下拉 + 上升沿无效) 仅在电平从高变低时触发中断
External Interrupt Mode with Rising/Falling edge trigger detection 中断模式 上升沿 + 下降沿 ✅ 是 ✅ 是 ✅ 可通过 NVIC 控制 按键单次点击(防抖)、双向信号检测 无论电平如何变化都会触发中断(需软件去抖)
External Event Mode with Rising edge trigger detection 事件模式 上升沿(Low → High) ❌ 否 ❌ 否 ❌ 不受 NVIC 控制 触发 ADC、TIM、DMA 等外设操作 仅产生“事件”,不打断主程序
External Event Mode with Falling edge trigger detection 事件模式 下降沿(High → Low) ❌ 否 ❌ 否 ❌ 不受 NVIC 控制 外部信号同步、定时器触发 用于硬件级触发,无需 CPU 干预
External Event Mode with Rising/Falling edge trigger detection 事件模式 上升沿 + 下降沿 ❌ 否 ❌ 否 ❌ 不受 NVIC 控制 高频信号同步、PWM 触发 任意边沿均可触发外设动作
  1. ✅ 什么是Interrupt Mode(中断模式)?
  • 当引脚电平满足条件时,CPU 会暂停当前任务,跳转到对应的中断服务函数(ISR)执行。
  • 必须配置 NVIC 和编写中断处理函数(如 EXTI5_IRQHandler())。
  • 适合需要 实时响应 的场景,比如按键输入、报警等。
  1. ✅ 什么是Event Mode(事件模式)?
  • 引脚变化只触发一个“事件”,但 不会中断 CPU
  • 通常用于触发其他外设操作,例如:启动 ADC 转换、触发 TIM 定时器更新、启动 DMA 传输
  • 不需要写 ISR,也不影响主程序运行。
3.6 配置中断优先级和使能状态

在这里插入图片描述

📊 NVIC 中断配置对比表

参数 说明 建议值
Enabled 是否启用中断 ✅ 必须勾选
Preemption Priority 抢占优先级(数值越小优先级越高) 0 ~ 15(建议 0~3)
Sub Priority 响应优先级(同级中断中谁先执行) 0 ~ 15(建议 0)
Priority Group 优先级分组方式(4=抢占4位+子优先级4位) 推荐使用 4
Interrupt Source 中断来源 EXTI line[9:5](对应 PA5~PA9)

warning:在本项目中,我们采用 HAL_Delay() 作为流水灯的延时函数,而该函数的正常运行依赖于 SysTick 定时器中断定期更新系统时钟计数(uwTick)。若外部中断(如 EXTI)的抢占优先级高于 SysTick 中断,则当中断服务函数(ISR)执行期间,SysTick 中断将无法被响应,导致 HAL_Delay() 无法获得时间更新,从而陷入无限等待,造成系统卡死。为避免这一问题,我们需要确保 SysTick 中断的抢占优先级不低于外部中断(即 SysTick 优先级数值 ≤ 外部中断),从而保障系统时基的持续更新,使延时函数和整个调度逻辑能够可靠运行。为此,我在此配置SysTick定时器中断的Preemption Priority为14,外部中断优先级为15,如下图所示。
在这里插入图片描述

3.7 时钟树配置

一般情况下,我们只需要将HCLK配置为72,回车即可。
在这里插入图片描述

3.8 项目设置和代码生成设置

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

3.9 将cubemx生成的工程文件导入到Vscode中

这里默认已经配置好Vscode中的EIDE环境,后续我将单独出一个文章,讲解vscode配置流程,如仅需在Keil中编写和烧录代码,则可跳过此部分。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.10 配置芯片支持包在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.11 配置烧录方式

在这里插入图片描述

3.12 配置RAM/FLASH布局

这里点击Reset按钮,在进行保存即可
在这里插入图片描述

3.13 编译/烧录

在完成上述所有配置后,点击 Keil µVision 左下角的 “Build” 按钮编译工程;若编译成功,即可通过 “Flash” 按钮将程序烧录至目标 STM32 芯片中运行。

在这里插入图片描述

4. 代码解析

4.1 流水灯代码
void led_flow(void)
{

  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
  delay_with_check(500);

  if(led_state == 0) return;  // 检查状态
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
  delay_with_check(500);

  if(led_state == 0) return;  // 检查状态
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
  delay_with_check(500);
}
4.2 延时函数
void delay_with_check(uint32_t ms)
{
  for(uint32_t i = 0; i < ms / 10; i++)
  {
    if(led_state == 0) return;  // 如果状态变为0,立即退出
    HAL_Delay(10);
  }
}
4.3 主函数
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 */

  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
    if(led_state == 1)
    {
      led_flow();
    }
    else if(led_state == 0)
    {
      
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
    }
  }
  /* USER CODE END 3 */
}
4.4中断函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_5)
  {
    // 防抖:简单延时(实际项目建议用定时器消抖)
    HAL_Delay(50);
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET)
    {
      led_state = 1;
    }
    else
    {
      led_state = 0;
    }

  }
}
4.5代码流程分析
上电启动
   │
   ├── HAL_Init() → 初始化 HAL 和 SysTick
   ├── SystemClock_Config() → 配置 72MHz 时钟
   └── MX_GPIO_Init() → 初始化 LED 和 按键中断
   │
   └── 进入 while(1) 主循环
          │
          ├── 若 led_state == 0 → 熄灭所有 LED
          │
          └── 若 led_state == 1 → 调用 led_flow()
                  │
                  ├── 点亮 PC13 → delay_with_check(500)
                  │       └── 每 10ms 检查 led_state,可提前退出
                  ├── 点亮 PA8 → 同上
                  └── 点亮 PB14 → 同上
                          │
                          └── 返回主循环,重新检查状态

按键按下/释放
   │
   └── 触发 EXTI 中断 → HAL_GPIO_EXTI_Callback()
          │
          ├── 软件消抖(HAL_Delay(50))
          └── 根据电平翻转 led_state(0 ↔ 1)

5. 现象展示

在这里插入图片描述

(二) 任务二:Proteus 8.15 电路仿真

写在前面:如果你也像我一样,刚接触嵌入式开发,手头没有开发板,或者想在“烧芯片”之前先验证电路逻辑——那么,Proteus + Keil 的组合绝对是你的福音!今天这篇博客,就带大家手把手搭建一个基于 STM32F103C8 的仿真环境,绘制电路、编译代码、加载 HEX、运行仿真,一气呵成!,这里已经默认安装好Proteus软件

1.打开工程并配置供电网

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

2.放置元器件

右键 -> 放置 -> 元件 -> From Libraries
在这里插入图片描述
Keywords中输入ledbuttonstm32f103c8选择对应的元器件
在这里插入图片描述

3. 仿真电路设计

在这里插入图片描述

4.导入hex文件

参考路径:..\STM32TESTPROJECT\Qrs_project\4_STM32_Interrupt_Led\MDK-ARM\4_STM32_Interrupt_Led\4_STM32_Interrupt_Led.hex
在这里插入图片描述

5.运行效果展示

可以看到在PA5端口为低电平,流水灯不工作,在按键按下后,PA5被拉高,触发中断,流水灯工作
在这里插入图片描述

(三)总结

在本次嵌入式开发实战中,我们深入探讨了如何使用 STM32F103C8T6 核心板通过 HAL 库实现中断控制的 LED 流水灯功能,并在 Proteus 8.15 中完成了电路设计与仿真。首先,我们明确了任务目标:通过外部中断控制 LED 实现流水闪烁,并详细规划了硬件连接方案,确保每个元件都能正确接入 STM32 引脚。这一过程不仅强化了对 GPIO 配置的理解,也加深了对外部中断工作原理的认识。

接着,我们利用 STM32CubeMX 工具进行了系统配置,包括时钟树设置、GPIO 输出模式选择以及中断优先级分配等。这些步骤是项目成功的关键,尤其是正确配置中断优先级,避免了因 SysTick 定时器中断被抢占而导致的延时函数失效问题。此外,我们还介绍了 Serial Wire Debug (SWD) 接口的重要性及其与 JTAG 的对比,为后续调试提供了便利。

在代码实现部分,我们展示了如何编写和组织代码以实现预期的功能。特别地,针对按键可能出现的机械抖动现象,我们在中断服务函数中加入了简单的软件消抖机制。同时,为了保证系统的实时响应性,采用了带有状态检查的延时函数,使得 LED 能够根据外部中断的状态及时更新显示效果。

最后,借助 Proteus 8.15 进行电路仿真,验证了整个设计方案的可行性。通过导入 Keil 编译生成的 .hex 文件并在虚拟环境中模拟实际操作,我们能够直观地观察到当 PA5 引脚电平变化时,LED 流水灯的工作状态也随之改变。这一步骤极大地提高了开发效率,减少了反复烧录至真实硬件进行测试的时间成本。

总结来说,这次实践不仅让我们掌握了基于 STM32 的中断驱动开发流程,还体验到了从理论设计到仿真验证再到最终实现的完整项目开发周期。无论是对于初学者还是有一定经验的开发者而言,这样的练习都是非常有价值的,它帮助我们建立起更加全面的工程思维和技术能力。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐