如何在STM32F407ZGT6 RT-Spark开发板上实现硬件定时器中断和前台/后台任务调度
本实验/项目的目标是在基于STM32F407ZGT6的RT-Spark开发板上实现硬件定时器中断以及前台/后台任务调度系统。通过配置两个硬件定时器TIM2和TIM3,以不同速率切换两个内置LED灯;同时在KEY_UP按钮(PC5)上设置外部中断,按下时可立即切换两个LED灯的状态。系统采用循环执行架构:中断服务例程(ISRs)负责处理紧急的后台任务并设置标志位,而主循环则处理前台的实际任务。
注意:原始的资料/文件中建议使用PA0进行按钮中断。经过测试和检查,RT-Spark板上并没有将物理按钮连接到PA0上。
设计与测试
第一部分 — — — 硬件描述
所使用的硬件是基于STM32F407ZGT6微控制器的RT-Spark开发板。由于该板已内置两个LED和方向按钮,因此无需额外的外部元件。引脚分配如下:
第一部分 1.1 — — — 软件设置
•打开 STM32CubeMX
•转到“文件”→“新建”→“STM32 项目”
•在芯片选择器中,搜索 STM32F407ZGT66 并选中它
•点击下一步
•前往项目管理器,设置您偏好的项目名称
•将工具链/IDE设置为STM32CubeIDE
•在左上角的“文件”中保存项目,或直接按 Ctrl + S
第1.2部分 — — 配置GPIO引脚以连接LED
•在引脚分配与配置选项卡中,找到引脚 PF11
•点击并将其设置为 GPIO 输出
•进入系统核心 → GPIO,点击 PF11,并将用户标签设置为 LED_RED
•找到引脚 PF12,将其设置为 GPIO 输出
•将用户标签设置为LED_BLUE
•保存项目并生成代码
第1.3部分 — — 配置系统时钟
在设置定时器之前,系统时钟必须以84 MHz运行。默认情况下,CubeMX生成的代码在16 MHz的原始HSI上运行,这会导致定时器计算完全错误。这是本次实验/项目中遇到的第一个问题:由于ARR被设置为最大值4,294,967,295,而不是9999,LED持续亮着。
•转到时钟配置选项卡
•找到 HCLKCLKCLK 框,输入 84,然后按回车键
•CubeMXMX 将自动配置 PLL 以达到 84 MHz
•保存并重新生成代码
第1.4节 — — 配置TIM2和TIM3
TIM222 每 11 秒(1 Hz)触发一次,TIM333 每 0.55 秒(2 Hz)触发一次。根据第 222 节的定时器周期公式:f_timer = f_APB / (PSC + 1) / (ARR + 1)
对于TIM2:84,000,000 / (8399+1) / (9999+1) = 1 Hz
对于TIM3:84,000,000 / (8399+1) / (4999+1) = 2 Hz
CubeMX 的步骤:
•进入定时器 → TIM2,将时钟源设置为内部时钟
•将预分频器设置为8399,计数周期设置为9999
•在 NVIC 设置中,启用 TIM22 全局中断。
•将优先级设置为0
•进入定时器 → TIM3,将时钟源设置为内部时钟
•将预分频器设置为8399,计数周期设置为4999
•在 NVIC 设置中,启用 TIM33 全局中断。
•将优先级设置为1
•进入系统核心 → NVIC NVIC 并确认优先级
•保存并重新生成代码
第1.5部分 — — 配置调试引脚
•PE000 被配置为 DEBUG_PIN 用于定时测量。
•该引脚在TIM2中断服务程序的开始和结束时被切换。
•以及任务A,允许使用TISR和TTask进行测量
•DWTWT 循环计数器。
•找到引脚 PE0 → 设置为 GPIO 输出
•进入系统核心 → GPIO,点击 PE00 并设置
•用户标签到DEBUG_PIN
•保存并重新生成代码
第1.6部分 — — 配置按钮
实验室最初要求使用PA0,但RT-Spark板上并没有物理按键。在查阅RT-Thread文档后发现,KEY_UP引脚位于PC5,因此改用PC5。实验室还要求使用上升沿+下拉电阻,但该板上的KEY_UP按钮为低电平激活,按下时会连接到GND,而正确的配置应为下降沿+上拉电阻。
•找到引脚 PC5 → 设置为 GPIO_EXTI5
•进入系统核心 → GPIO,点击 PC55 并设置:
•GPIOGPIO 模式:外部中断模式(下降沿)
•GPIO 上拉/下拉:上拉
•进入系统核心 → NVIC,启用 EXTI EXTI EXTI 线[9:5]中断,将优先级设置为 2
第二部分 — — — 软件架构与实现
第二部分 1 — — — 前景/背景架构
系统使用带有中断的循环执行机制。有两个优先级层次:后台(中断服务例程,ISRs)——在被触发时运行,处理紧急任务,并设置标志以通知前台。
前台(主循环)——持续运行,检查标志位并执行实际任务。这比使用 HAL_Delay()() 进行轮询更高效,因为在定时器事件之间,CPUCPU 可以自由进行其他工作。
第2.2部分 — — 全局标志变量
三个标志变量被声明为 volatile,因此编译器不会优化掉主循环中的读取操作:
volatile uint8_t task_adc_ready = 0;
volatile uint8_t task_display_ready = 0;
volatile uint32_t tick_count = 0;
第2.3部分 — — 定时器回调(后台任务)
HAL定时器回调函数在一个函数中处理TIM2和TIM3。htim->Instance字段用于标识是哪个定时器触发了该事件:
第2.4部分 — 主循环(前台任务)
第2.5部分 — — — 按钮中断(事件触发)
第三部分 — 结果
第3.1部分 — LED闪烁输出
刷写代码后,观察到以下情况:
•红色LED每1秒通过TIM2中断闪烁一次
•绿色LED每0.5秒闪烁一次,通过TIM3中断实现
•按下 KEY_UP(PC5)会立即切换两个LED灯。
•前景/背景调度正常工作
第3.2部分 — — 序列图
下方的时序图展示了系统运行3秒的过程,包括TIM2和TIM3以各自的速度触发,标志位传递至主循环,并在约t=1.2秒处发生按钮按下事件。该图表使用Lucidchart创建。
第3.3部分 — 时间测量
使用 ARM Cortex-M4 上的 DWT(数据断点与跟踪)循环计数器获取了真实的时间值。该计数器是一个 32 位寄存器,以 84 MHz 的精度计数 CPU 时钟周期(1 个周期周期 ≈ 11.9 ns)。代码中已启用 DWTWT 计数器:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
在TIM2中断服务例程(ISR)的入口/出口处,以及当task_adc_ready = 1和if (task_adc_ready for TLatency)时设置了断点。每次断点处使用STM32CubeIDE中的表达式窗口读取DWT->CYCCNT寄存器。
TISR测量:
DWT在ISR开始时(第69行):246
DWT在ISR末端(第76行):346
时钟周期:346 - 246 = 10000 周期
时间:100 / 84,000,000 = 1.199 微秒
延迟测量:
DWT 在 task_adc_ready = 1 时:44,094,304
DWT 在 if(task_adc_ready): 50, 141, 984
时钟周期:50,141,984 - 44,094,304 = 6,047,68000 周期
时间:6,047,680 / 84,000,000 = 7222 毫秒
时间表:
72毫秒的延迟高于理论上的20毫秒估算值。这是因为该延迟不仅包含了任务B中的HAL_Delay(20),还包括中断服务例程(ISR)完成、返回主循环以及再次回到任务adc_ready检查的时间。这与系统设计预期的情况非常吻合。
第四部分 - 分析
第五部分 — 结论
我学到的东西:
该实验展示了轮询调度与中断驱动调度之间的实际差异。使用硬件定时器可避免在主循环中使用 HAL_Delay(),从而让 CPU 在事件之间有更多时间处理其他任务。前景/后台架构是一种简洁而简单的方式,可在不依赖完整实时操作系统的情况下处理不同优先级的任务。DWTWT 循环计数器是一个有用的工具——测量到的 TISR 为 1.199 微秒,证实了保持中断服务例程(ISRs)较短的重要性,因为整个 TIM2 中断服务例程仅需 100 个时钟周期即可完成。
面临的问题:
问题1——LED在第一次闪烁后一直保持亮起且无法闪烁。系统时钟使用16 MHz的原始HSI运行,而ARR设置为4,294,967,295,而非9999。这两个问题通过将HCLK设置为84 MHz,并在CubeMX中修正ARR得以解决。
问题2——按钮中断未生效。RT-Spark板上PA0(按要求)没有物理按钮,KEY_UP位于PC5。配置已更改为PC5的下降沿+上拉,并将回调函数更新为检查GPIO_PIN_5。
问题3——在TIM3 NVIC设置选项卡中,NVIC抢占优先级被灰色显示。通过从“系统核心 → NVIC”下的全局NVIC表中修改优先级来解决该问题。
本文编译自hackster.io





