1. 为什么需要定时器中断实现LED流水灯?

很多初学者在接触STM32时,第一个实验往往是用GPIO控制LED闪烁。最常见的方式就是在main函数的while循环里,先点亮LED,然后调用HAL_Delay()延时一段时间,再熄灭LED,再延时,如此循环。这种方式简单直接,容易理解,但存在一个致命问题:在延时函数执行期间,CPU什么也做不了,只能空转等待,这在实际项目中是非常低效的。

想象一下,如果你的系统除了控制LED,还需要检测按键输入、处理传感器数据、进行通信等,使用延时函数就会导致系统响应迟钝。这时候,定时器中断就派上用场了。通过定时器中断,我们可以实现非阻塞式的延时,让CPU在等待时间到达的过程中,能够处理其他任务,大大提高系统效率。

我在实际项目中就遇到过这种情况。早期用延时函数做LED指示,结果系统在处理其他任务时经常卡顿,后来改用定时器中断,不仅LED流畅切换,整个系统的响应速度也提升了不少。这就是中断的魅力所在——它让单片机能够"同时"处理多个任务。

2. 环境准备与工具配置

2.1 所需软硬件工具

要完成这个实验,我们需要准备以下软件工具:

  • STM32CubeMX:这是ST官方推出的图形化配置工具,可以直观地配置STM32的各种外设,自动生成初始化代码,大大简化了开发流程。我用了这么多年,感觉最爽的就是不用再手动查寄存器配置了。
  • Keil MDK-ARM:经典的ARM开发环境,用于编写、编译和调试代码。虽然现在有更多选择,但Keil在STM32开发中还是很主流的。
  • Proteus:电路仿真软件,可以在没有实际硬件的情况下验证代码是否正确。对于初学者来说,这能省下不少买开发板的钱。

硬件方面,如果不用Proteus仿真,就需要一块STM32F103C8T6最小系统板(也就是常说的蓝色药丸板),3个LED灯,几个220欧姆的限流电阻,以及一些杜邦线。

2.2 软件安装注意事项

安装STM32CubeMX时,建议同时安装对应的HAL库,这样在创建新工程时就能直接使用。Keil安装后需要注册,个人学习可以申请免费版本。Proteus安装后要确保有STM32F103C8T6的模型,有些版本可能需要单独安装元件库。

我在安装这些软件时踩过不少坑,比如版本兼容性问题。建议大家都使用较新的稳定版本,太老的版本可能不支持某些功能,太新的又可能有未知的bug。STM32CubeMX我目前用6.11版本,Keil用5.38,Proteus用8.16,这几个版本组合比较稳定。

3. STM32CubeMX工程配置详解

3.1 创建新工程与芯片选择

打开STM32CubeMX,点击"New Project"创建新工程。在芯片选择界面,输入"STM32F103C8"进行搜索,选择STM32F103C8T6型号。这个芯片是STM32F1系列中很经典的型号,性价比高,资源丰富,适合初学者入门。

选中芯片后,右上角会显示芯片的基本信息:64KB Flash、20KB RAM、72MHz主频、37个GPIO口等。点击"Start Project"进入配置界面。

3.2 系统核心配置

在配置界面左侧,首先配置"SYS"(系统设置)。在"Debug"下拉菜单中选择"Serial Wire",这是启用SWD调试接口,这样我们就可以用ST-Link等调试器来下载程序和调试了。

然后是时钟配置,找到"RCC"(复位和时钟控制),在"HSE"(高速外部时钟)中选择"Crystal/Ceramic Resonator",这样我们就使用了外部8MHz晶振作为时钟源。STM32的时钟系统比较复杂,但CubeMX帮我们简化了很多,我们只需要做基本选择即可。

3.3 GPIO引脚配置

接下来配置LED所用的GPIO引脚。根据我们的设计,使用三个LED分别连接在PB9、PA15和PC13引脚。在芯片图形界面上找到这些引脚,点击后选择"GPIO_Output"。

然后进入"GPIO"配置页面,对每个引脚进行详细设置:

  • GPIO output level:High(高电平,初始状态LED熄灭)
  • GPIO mode:Output Push Pull(推挽输出)
  • GPIO Pull-up/Pull-down:No pull-up and no pull-down(无上下拉电阻)
  • Maximum output speed:Low(低速输出,LED控制不需要高速)

这里有个细节需要注意:PA15引脚默认是JTAG功能,需要先解除复用才能配置为普通GPIO。CubeMX会自动帮我们处理这个问题,但如果你手动配置代码,就需要特别注意。

3.4 时钟树配置

点击顶部的"Clock Configuration"进入时钟树配置界面。这里看起来复杂,但其实有规律可循。我们从左侧的HSE(8MHz)开始,经过PLL倍频到72MHz,然后作为系统时钟SYSCLK。

具体配置:HSE选择Crystal Oscillator,PLL Source Mux选择HSE,PLLMUL选择x9倍频,这样8MHz x 9 = 72MHz。APB1 Prescaler选择/2,这样APB1总线时钟是36MHz(不超过最大限制),APB2保持72MHz。

时钟配置直接影响系统性能和功耗,对于LED流水灯这种简单应用,72MHz可能有些奢侈,但作为学习过程,我们体验一下全速运行的感觉也不错。

3.5 定时器中断配置

找到"TIM6"(基本定时器),在Mode中激活时钟源为"Internal Clock"。基本参数配置:

  • Prescaler(预分频值):71
  • Counter Period(自动重装载值):999

这样配置的计算公式是:定时频率 = 72MHz / (71+1) / (999+1) = 1000Hz,也就是每1ms产生一次中断。预分频器71表示72分频(71+1),因为分频系数是从0开始计数的。

在NVIC Settings中勾选"TIM6 global interrupt"使能中断,优先级可以保持默认。如果有多个中断,就需要根据实际需求设置优先级了。

3.6 生成工程代码

点击"Project Manager",设置工程名称和保存路径,在"Toolchain/IDE"中选择"MDK-ARM"(Keil)。在"Code Generator"中,我习惯勾选"Generate peripheral initialization as a pair of '.c/.h' files per peripheral",这样每个外设的初始化代码会分开生成,便于管理。

最后点击"Generate Code",CubeMX就会生成完整的工程代码。如果选择"Open Project",会自动在Keil中打开工程。

4. Keil中编写中断处理代码

4.1 工程结构分析

打开Keil工程后,可以看到CubeMX生成的代码结构。主要关注以下几个文件:

  • main.c:主程序文件,包含main函数和while循环
  • stm32f1xx_it.c:中断服务函数文件
  • gpio.c:GPIO初始化代码
  • tim.c:定时器初始化代码

HAL库的好处是提供了抽象层,我们不需要直接操作寄存器,而是通过调用HAL提供的函数来实现功能。但缺点是代码效率稍低,不过对于大多数应用来说已经足够了。

4.2 全局变量定义

在main.c文件开头,定义我们需要的全局变量:

/* 用户变量定义 */
volatile uint8_t led_state = 0;     // LED状态:0,1,2分别对应三个LED
volatile uint32_t timer_counter = 0; // 定时器计数器

volatile关键字很重要,它告诉编译器这些变量可能会在中断中被修改,不要做优化处理。我曾经遇到过因为没有加volatile而导致程序运行异常的情况,调试了好久才找到原因。

4.3 主函数实现

在main函数中,我们需要启动定时器中断:

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM6_Init();
  
  /* 启动定时器中断 */
  HAL_TIM_Base_Start_IT(&htim6);
  
  while (1)
  {
    /* 主循环可以处理其他任务 */
    /* 比如按键检测、数据传输等 */
  }
}

主循环中现在看起来空空的,但实际上这里可以添加很多其他功能。这就是中断方式的优势——主循环可以空闲出来处理其他任务,而不是被延时函数阻塞。

4.4 中断回调函数实现

HAL库采用了回调函数机制来处理中断,我们需要实现定时器更新中断的回调函数:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM6)
  {
    timer_counter++;
    
    /* 每1000ms(1秒)切换LED状态 */
    if (timer_counter >= 1000)
    {
      timer_counter = 0;
      
      /* 先关闭所有LED */
      HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
      HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
      
      /* 根据状态点亮对应LED */
      switch (led_state)
      {
        case 0:
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
          break;
        case 1:
          HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);
          break;
        case 2:
          HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
          break;
      }
      
      /* 更新状态,循环切换 */
      led_state = (led_state + 1) % 3;
    }
  }
}

这个函数会在每次定时器中断时被调用。我们首先检查是否是TIM6触发的中断(因为有多个定时器时需要区分),然后计数器加1。当计数器达到1000(即1秒)时,重置计数器,切换LED状态。

4.5 编译与生成HEX文件

代码编写完成后,点击Keil工具栏中的"Build"按钮(或按F7)进行编译。如果一切正常,输出窗口会显示"0 Error(s), 0 Warning(s)"。

为了生成Proteus仿真所需的HEX文件,需要点击"Options for Target"按钮(魔术棒图标),在"Output"选项卡中勾选"Create HEX File"。再次编译后,在工程目录的Debug文件夹中就能找到HEX文件了。

5. Proteus仿真验证

5.1 绘制电路原理图

打开Proteus,新建工程,选择合适的模板。在元件库中搜索并添加以下元件:

  • STM32F103C8:主控制器
  • LED-RED:红色LED(3个)
  • RES:电阻(3个,220欧姆)
  • CRYSTAL:晶振(8MHz)
  • CAP:电容(22pF,2个;100nF,1个)
  • BUTTON:按钮(用于复位电路)

按照电路图连接各元件:

  • 三个LED分别通过220欧姆电阻连接到PB9、PA15和PC13
  • LED阴极接地
  • 8MHz晶振连接至OSC_IN和OSC_OUT
  • 复位按钮连接至NRST,另一侧接地
  • 电源和地线连接完整

Proteus中的STM32模型已经包含了内部复位电路和电源管理,所以外部电路可以简化。但在实际硬件中,建议还是按照数据手册推荐电路来设计。

5.2 加载程序并仿真

双击Proteus中的STM32元件,打开属性对话框,在"Program File"中选择之前在Keil中生成的HEX文件。在"Clock Frequency"中设置为72MHz,与代码中配置一致。

点击Proteus左下角的运行按钮开始仿真。可以看到三个LED依次点亮和熄灭,形成流水灯效果。点击暂停按钮可以暂停仿真,观察当前状态。

Proteus仿真虽然不是100%准确,但对于学习基本功能和验证代码逻辑已经足够了。我在教学中经常用Proteus做演示,比实际硬件更方便展示。

5.3 仿真中的常见问题

有时候仿真可能会遇到问题,比如LED不亮或者闪烁不正常。常见原因包括:

  • HEX文件路径错误或未更新
  • 时钟频率设置不正确
  • 电路连接错误
  • 代码中GPIO配置与实际电路不匹配

Proteus提供了调试功能,可以暂停仿真,查看寄存器状态,帮助排查问题。对于中断程序,还可以在中断发生时暂停,查看中断是否正常触发。

6. 延时函数与中断方式对比

6.1 资源占用对比

传统的延时函数方式代码简单,但CPU利用率低。在延时期间,CPU几乎100%被占用,无法执行其他任务。而中断方式虽然代码稍复杂,但CPU利用率高,在等待时间到达的过程中可以处理其他任务。

我们可以通过一个简单的实验来验证:在主循环中添加一个计数器,统计每秒循环执行次数。延时函数方式下,这个计数器基本不会增加,因为CPU大部分时间卡在延时函数中。而中断方式下,计数器会快速增加,证明CPU确实在执行其他任务。

6.2 实时性对比

中断方式的另一个优势是实时性更好。比如我们需要精确控制LED切换时间,延时函数方式可能会因为其他代码的执行而产生累积误差。而定时器中断是基于硬件计时,更加精确可靠。

在实际项目中,我遇到过用延时函数控制多个设备时序,结果因为函数执行时间的不确定性,导致时序逐渐漂移。改用定时器中断后,这个问题就解决了。

6.3 系统扩展性对比

当系统功能越来越复杂时,中断方式的优势更加明显。我们可以添加多个定时器,分别处理不同任务的时间控制,或者使用一个定时器产生多个不同频率的中断。

比如,除了LED流水灯,我们还可以用另一个定时器中断处理按键消抖、传感器数据采集、通信超时检测等。这种多任务系统用传统的延时函数是很难实现的。

7. 进阶应用与优化建议

7.1 多定时器协同工作

在实际项目中,我们经常需要多个定时器协同工作。比如TIM6用于系统心跳计时,TIM7用于LED控制,TIM2用于PWM输出等。STM32F103C8T6有多个定时器,可以充分利用这一特性。

配置多个定时器时,需要注意中断优先级的设置。高优先级的中断可以打断低优先级的中断,这对于实时性要求高的任务很重要。

7.2 低功耗优化

如果项目对功耗有要求,可以进一步优化代码。比如在主循环中没有任务时,可以让CPU进入睡眠模式,有中断时再唤醒。STM32提供了多种低功耗模式,可以根据实际需求选择。

对于LED控制,还可以使用PWM方式调节亮度,而不是简单的开关控制。这样既能实现更丰富的效果,又能降低功耗。

7.3 代码结构优化

随着项目复杂度的增加,良好的代码结构很重要。建议将不同功能模块分开,比如LED控制单独一个文件,定时器中断处理单独一个文件,主程序只负责初始化和调度。

使用HAL库的回调函数机制,可以让代码更加模块化。每个外设的中断处理都在自己的回调函数中完成,互不干扰。

在实际项目中,我还会添加调试信息输出,比如通过串口打印系统状态,这样在出现问题时更容易排查。STM32的硬件调试功能也很强大,可以设置断点、观察变量、单步执行等,熟练使用这些工具能大大提高开发效率。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐