基于 STM32 定时器的动态任务调度与函数指针应用
通过这种方式,我们可以根据实际需要动态地添加和修改任务,而且这种方式完全不依赖于硬件中断的次数或顺序,任务的切换和执行都显得特别优雅和高效。这意味着,不同的功能模块可以通过传递不同的任务函数来实现不同的功能,避免了硬编码,使得代码更具灵活性。比如,在需要更换定时任务时,只需调用 Timer1_SetTask() 设置新的任务函数,而不需要修改定时器中断回调函数的代码。通过这次实践,我深刻体会到,嵌
如何使用 STM32 定时器 TIM1 来管理定时任务
在 STM32 中,定时器是一种非常常见的外设,广泛应用于定时控制、PWM 输出、定时任务等场景。本文将展示如何使用 STM32 的定时器 TIM1 来管理定时任务,并通过回调函数实现任务的调度,避免使用传统的延时函数。我们将通过一个简单的例子来说明如何配置定时器、编写任务函数以及如何在 main.c 中调用这些任务。
- 介绍
在嵌入式开发中,定时器广泛应用于定时控制、PWM 输出以及事件驱动的编程。使用定时器中断代替延时函数是一种更高效、更精准的编程方法。STM32 提供了强大的定时器功能,能够精确控制时间并触发事件。本文通过 TIM1 定时器和任务函数指针实现一个简单的定时任务调度器。 - STM32CubeMX 配置 TIM1
首先,我们需要使用 STM32CubeMX 配置 TIM1 定时器,以便它可以周期性地触发中断。下面是配置步骤:
在CUBEMX中配置 TIM1
在 Peripherals 标签页下,找到 TIM1 并点击它。
在 TIM1 配置窗口中:
配置 TIM1 为 “Time Base” 模式(也可以选择 PWM 模式,取决于需求)。
配置 Prescaler 和 Auto-Reload Register (ARR) 以生成合适的时间周期。
假设你希望每 1 毫秒产生一次中断,可以设置:
Prescaler = 72 - 1(将 72 MHz 时钟分频为 1 MHz,即每个定时器周期为 1 微秒)。
ARR = 1000 - 1(定时器每 1 毫秒触发一次中断)。
启用 TIM1 中断:点击 “NVIC” 按钮,勾选 TIM1 update interrupt。
配置完成后,点击 Project -> Generate Code。
选择你使用的 IDE(如 STM32CubeIDE 或 KEIL),点击 Generate。 - 在 main.c 中使用定时器
在 main.c 中,我们需要通过定时器的中断回调函数来执行任务。我们会通过一个简单的例子来演示如何使用定时器管理任务。
步骤 1:包含头文件
首先,包含定时器任务相关的头文件 TIMER_TASKS.h,以便我们可以在 main.c 中使用定时器任务函数。
#include "TIMER_TASKS.h" // 包含定时器任务相关的头文件
步骤 2:定义定时任务
假设我们希望在定时器周期内切换一个 LED 状态,我们可以定义一个 LED_Toggle 函数:
void LED_Toggle(void) {
// 切换 LED 状态
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 假设 LED 连接在 GPIOB_PIN_0
}
然后,在 main.c 中设置定时器回调任务为 LED_Toggle:
int main(void) {
HAL_Init(); // 初始化 HAL 库
// 初始化硬件,如 GPIO、定时器等
SystemClock_Config();
MX_GPIO_Init();
MX_TIM1_Init(); // 初始化 TIM1
// 设置定时器任务
Timer1_SetTask(LED_Toggle);
// 启动 TIM1 定时器中断
HAL_TIM_Base_Start_IT(&htim1); // 启动 TIM1 中断
while (1) {
// 主循环代码
}
}
步骤 3:定时器回调函数
在 TIMER_TASKS.c 文件中,我们已经实现了定时器的回调函数。定时器每次超时后,会调用设置的任务函数。
c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM1) {
if (timer1_task != NULL) {
// 调用定时器任务
timer1_task();
}
}
}
4. 在 TIMER_TASKS.c 中设置任务
TIMER_TASKS.c 文件中通过函数指针管理定时任务。每当定时器触发中断时,我们调用指向任务函数的指针。这样,你可以根据需求灵活地切换不同的任务。
#include "TIMER_TASKS.h"
// 定义定时器任务函数指针
TimerTask_t timer1_task = NULL; // 默认为 NULL,表示没有任务
// 设置定时器任务函数
void Timer1_SetTask(TimerTask_t task) {
timer1_task = task; // 设置定时器任务函数指针
}
// 定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM1) {
if (timer1_task != NULL) {
// 调用定时器任务
timer1_task();
}
}
}
TIMER_TASKS.h
#ifndef _TIMER_TASKS_H
#define _TIMER_TASKS_H
#include "stm32f1xx_hal.h"
typedef void (*TimerTask_t)(void);
extern TimerTask_t timer1_task;
void Timer1_SetTask( TimerTask_t task);
void Timer1_PeriodElapsedCallback(void); // 定时器1超时回调处理
#endif
在这篇文章中,我们通过 TimerTask_t 任务指针的方式,实现了定时器任务的动态配置和调用。对于初次接触这种编程方式的开发者,任务指针的概念可能有点新奇,但它却为嵌入式系统带来了巨大的灵活性和可扩展性。
- 任务指针的灵活性
使用任务指针(函数指针)来处理定时器任务,在我看来,真的是一种非常“神奇”的方式。它能够使得程序的结构更加清晰和模块化。为什么这么说呢?因为:
动态任务切换:通过任务指针,你可以在运行时随时改变要执行的任务函数。这意味着,不同的功能模块可以通过传递不同的任务函数来实现不同的功能,避免了硬编码,使得代码更具灵活性。
比如,在需要更换定时任务时,只需调用 Timer1_SetTask() 设置新的任务函数,而不需要修改定时器中断回调函数的代码。这个机制使得定时任务的配置变得非常方便。
解耦合:通过任务指针,定时器的触发和具体执行的任务完全解耦。你不需要把任务写死在定时器回调函数中,而是可以动态决定每个周期需要执行什么任务。这让程序更易于扩展和维护。
- 任务指针的可扩展性
任务指针不仅限于定时器应用。在许多嵌入式应用中,你都可以用类似的方法管理不同的事件和任务:
按需触发:例如,你可以用这种方式处理外部中断或者事件驱动的任务。每当某个事件发生时,只需要设置相应的回调函数,定时器或中断系统就会在适当的时候自动调用。
状态机和回调机制:这种任务指针机制也可以用来实现更复杂的状态机或者回调机制。例如,系统根据不同的运行状态,动态设置不同的回调函数来处理相应的任务。这在复杂系统中尤为重要,能显著提高代码的可读性和可维护性。
-
高效且简洁的代码结构
通过使用任务指针,我们避免了使用大量的条件判断和硬编码方式来控制定时器执行的任务。定时器只负责周期性地触发中断,而真正的任务逻辑通过任务指针来管理。这种方法可以大大简化代码结构,让主循环代码更加简洁,避免了繁琐的延时处理。 -
初次使用的“神奇感”
正如你提到的,任务指针的方式给我带来了很大的惊讶和“神奇感”。我常常觉得,作为开发者我们总是喜欢追求代码的灵活性和扩展性,而任务指针正是这种设计理念的体现。通过这种方式,我们可以根据实际需要动态地添加和修改任务,而且这种方式完全不依赖于硬件中断的次数或顺序,任务的切换和执行都显得特别优雅和高效。
任务指针提供了一种非常灵活且高效的方式来管理定时任务和事件驱动任务。在 STM32 等嵌入式系统中,任务指针不仅可以用来处理定时器任务,还可以广泛应用于中断管理、状态机控制、回调函数等场景。通过这种方式,我们可以实现更清晰、更简洁、且易于扩展和维护的代码结构。对于复杂项目,尤其是需要灵活应对各种动态任务的项目,任务指针无疑是一种值得采用的强大工具。
通过这次实践,我深刻体会到,嵌入式系统中的编程并不局限于硬编码,而是可以通过灵活的设计方式,提升代码的灵活性和可维护性。任务指针无疑是其中一个非常精妙且实用的技巧。
任务指针的具体使用方法
在嵌入式系统中,任务指针(或函数指针)是一种强大的工具,它允许我们将不同的任务功能动态地与定时器或中断等机制结合起来。这种方式使得我们的代码更加灵活、模块化,且便于扩展。
接下来,我们将具体讲解如何在 STM32 中使用任务指针来管理定时任务。
- 定义任务指针类型
首先,我们需要定义一个类型来表示任务指针。在我们的例子中,我们定义了一个指向 void 函数的指针 TimerTask_t,它将用于保存定时器任务的函数。
typedef void (*TimerTask_t)(void);
这行代码的意思是:TimerTask_t 是一个新的类型,它代表一个 无返回值 (void) 且 没有参数 (void) 的函数指针。TimerTask_t 类型的变量可以存储任何符合签名 void function(void) 的函数指针。简而言之,TimerTask_t 是一个可以指向 void function(void) 函数的指针类型。
具体地:
typedef 关键字用来为现有的类型定义一个新的名字。
(*TimerTask_t) 表示我们定义的是一个函数指针类型。
(void) 是该函数指针所指向的函数的参数类型。
void 是该函数指针所指向的函数的返回值类型。
如果我们想定义一个带有参数的函数指针,只需要修改 typedef 中的函数签名部分。例如,如果你想定义一个函数指针,它指向的函数有一个 int 类型的参数,并返回 void,可以这样定义:
typedef void (*TaskWithParam_t)(int);
这表示:
TaskWithParam_t 是一个新类型,它代表一个接受 int 类型参数并返回 void 的函数指针。
(*TaskWithParam_t) 表示这是一个函数指针。
(int) 表示该函数接收一个 int 类型的参数。
void 表示函数没有返回值。
然后你就可以用 TaskWithParam_t 来声明一个函数指针,指向任何符合 void func(int) 签名的函数。
- 创建任务指针并设置任务
接下来,我们需要定义一个任务指针,初始化为 NULL,表示初始时没有任务设置。然后,我们可以通过一个函数来动态设置这个任务指针。
// 定义定时器任务函数指针,初始为 NULL
TimerTask_t timer1_task = NULL;
// 设置定时器任务函数
void Timer1_SetTask(TimerTask_t task) {
timer1_task = task; // 设置任务函数指针
}
定义好函数指针后,你可以将具体的函数赋给这个指针,并通过指针调用它:
// 函数定义:接受一个 int 参数并打印
void PrintNumber(int num) {
printf("The number is %d\n", num);
}
// 定义一个函数指针
TaskWithParam_t task;
// 在 main 函数中,将函数指针指向 PrintNumber 函数
task = PrintNumber;
// 通过函数指针调用 PrintNumber 函数
task(10); // 输出:The number is 10
这样,task 指针就指向了 PrintNumber 函数,我们可以通过指针间接调用该函数。
在 Timer1_SetTask 函数中,我们接收一个任务函数并将它赋值给 timer1_task,实现了动态地指定任务函数的功能。
- 定时器中断回调函数
然后,我们需要配置一个定时器的中断回调函数,该回调函数会在定时器每次超时时触发。每次定时器中断发生时,我们会检查是否有任务指针被设置,如果有,就执行该任务。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM1) {
if (timer1_task != NULL) {
// 调用定时器任务
timer1_task(); // 执行任务
}
}
}
这里的 HAL_TIM_PeriodElapsedCallback 是定时器中断的回调函数,当 TIM1 的计时周期到达时,会触发该回调函数。如果 timer1_task 不为 NULL,则会执行指向的任务函数。
-
在 main.c 中使用任务指针
在 main.c 中,你可以使用 Timer1_SetTask 函数来动态设置定时器任务函数。这样,你就可以在定时器中断发生时执行不同的任务。 -
定义具体任务函数
例如,我们定义一个任务函数 LED_Toggle,用来切换 LED 的状态:
void LED_Toggle(void) {
// 切换 LED 状态
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 假设 LED 连接在 GPIOB_PIN_0
}
- 设置任务函数
在 main.c 的 main 函数中,我们使用 Timer1_SetTask 来设置定时器任务函数:
int main(void) {
HAL_Init(); // 初始化 HAL 库
// 初始化硬件,如 GPIO、定时器等
SystemClock_Config();
MX_GPIO_Init();
MX_TIM1_Init(); // 初始化 TIM1
// 设置定时器任务
Timer1_SetTask(LED_Toggle); // 设置定时器任务为 LED 切换
// 启动 TIM1 定时器中断
HAL_TIM_Base_Start_IT(&htim1); // 启动 TIM1 中断
while (1) {
// 主循环代码
}
}
-
启动定时器中断
通过 HAL_TIM_Base_Start_IT(&htim1) 启动定时器中断,每当 TIM1 达到设定的周期时,就会调用 HAL_TIM_PeriodElapsedCallback,从而执行 LED_Toggle 函数。 -
更换任务
如果在某个时刻你需要更换定时器执行的任务,可以简单地调用 Timer1_SetTask 来设置一个新的任务。例如,假设你希望定时器在一定时间后执行一个新的任务,比如开启另一个硬件:
void Motor_On(void) {
// 启动电机
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 假设电机连接在 GPIOA_PIN_1
}
// 切换定时器任务为 Motor_On
Timer1_SetTask(Motor_On);
这样,定时器就会开始执行新的任务 Motor_On,而无需修改中断回调函数或定时器的其他配置。
总结
通过任务指针,我们可以让定时器或其他中断机制执行不同的任务,而这些任务的设置和修改都可以在运行时动态完成。这种方式的主要优点是:
高灵活性:可以在任何时候更改定时器执行的任务,不需要修改回调函数的代码。
模块化:任务逻辑和硬件配置分离,使得代码更加模块化、清晰。
易于扩展:通过函数指针,添加新的任务变得非常简单,无需修改主循环或中断回调的实现。
这种方式特别适合那些需要灵活切换不同任务的应用场景,比如周期性数据采集、控制任务调度等。掌握了任务指针的使用,你将能够写出更高效、可维护的嵌入式系统代码。
更多推荐



所有评论(0)