在嵌入式系统开发中,STM32 微控制器以其高性能、低功耗和丰富的外设资源受到广泛应用。而 FreeRTOS 作为一个开源的实时操作系统,为开发者提供了任务管理、内存管理、中断管理等强大的功能,使得复杂的嵌入式系统开发变得更加高效和可靠。软件定时器是 FreeRTOS 提供的一个重要特性,它允许开发者在不使用硬件定时器的情况下实现定时功能,这在资源有限的系统中尤为有用。本文将详细介绍基于 STM32 HAL 库和 FreeRTOS 的软件定时器的使用方法。
STM32 HAL(Hardware Abstraction Layer)库是 ST 公司为 STM32 微控制器提供的一套硬件抽象层库。它的主要目的是简化开发者对 STM32 硬件的操作,通过提供统一的 API 接口,使得开发者可以在不同型号的 STM32 微控制器上快速移植代码。HAL 库封装了底层的寄存器操作,提供了高级的函数调用,如 GPIO 操作、定时器配置、串口通信等,大大提高了开发效率。
FreeRTOS 是一个开源的、轻量级的实时操作系统内核,适用于各种嵌入式系统。它提供了任务管理、队列、信号量、互斥量等内核服务,支持多任务并发执行。FreeRTOS 的特点包括可裁剪性强、占用资源少、实时性高、易于移植等。在嵌入式系统中,使用 FreeRTOS 可以更好地管理系统资源,提高系统的稳定性和可靠性。
软件定时器是 FreeRTOS 提供的一种机制,它允许开发者在不使用硬件定时器的情况下实现定时功能。软件定时器基于 FreeRTOS 的系统时钟节拍(tick)运行,通过设置定时器的周期和回调函数,当定时器到期时,会自动调用回调函数执行相应的操作。软件定时器可以是一次性的(只执行一次回调函数),也可以是周期性的(每隔一定时间执行一次回调函数)。
本文以 STM32F4 Discovery 开发板为例进行介绍,该开发板采用了 STM32F407VG 微控制器,具有丰富的外设资源,适合进行嵌入式系统开发。
在 STM32CubeMX 中配置工程时,可以选择添加 FreeRTOS 操作系统。具体步骤如下:
在 FreeRTOS 中,使用 xTimerCreate
函数来创建软件定时器。该函数的原型如下:
TimerHandle_t xTimerCreate(
const char * const pcTimerName,
TickType_t xTimerPeriod,
UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction
);
pcTimerName
:定时器的名称,用于调试和日志记录,可为 NULL
。xTimerPeriod
:定时器的周期,以系统时钟节拍为单位。uxAutoReload
:定时器的模式,pdTRUE
表示周期性定时器,pdFALSE
表示一次性定时器。pvTimerID
:定时器的标识符,可用于在回调函数中区分不同的定时器。pxCallbackFunction
:定时器到期时调用的回调函数。示例代码如下:
#include "FreeRTOS.h"
#include "timers.h"
// 定时器回调函数
void vTimerCallback(TimerHandle_t xTimer) {
// 定时器到期时执行的操作
}
// 创建定时器
TimerHandle_t xTimer = xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000), // 定时器周期为 1 秒
pdTRUE, // 周期性定时器
(void *)0, // 定时器标识符
vTimerCallback // 定时器回调函数
);
创建定时器后,需要使用 xTimerStart
函数来启动定时器。该函数的原型如下:
BaseType_t xTimerStart(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
xTimer
:要启动的定时器句柄。xTicksToWait
:如果定时器服务任务处于阻塞状态,等待定时器服务任务解锁的最大时间,以系统时钟节拍为单位。如果设置为 0,表示不等待。示例代码如下:
if (xTimer != NULL) {
// 启动定时器
if (xTimerStart(xTimer, 0) != pdPASS) {
// 启动定时器失败
}
}
如果需要停止定时器的运行,可以使用 xTimerStop
函数。该函数的原型如下:
BaseType_t xTimerStop(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
参数含义与 xTimerStart
函数相同。
示例代码如下:
if (xTimer != NULL) {
// 停止定时器
if (xTimerStop(xTimer, 0) != pdPASS) {
// 停止定时器失败
}
}
当不再需要某个定时器时,可以使用 xTimerDelete
函数来删除定时器。该函数的原型如下:
BaseType_t xTimerDelete(
TimerHandle_t xTimer,
TickType_t xTicksToWait
);
参数含义与 xTimerStart
函数相同。
示例代码如下:
if (xTimer != NULL) {
// 删除定时器
if (xTimerDelete(xTimer, 0) != pdPASS) {
// 删除定时器失败
}
}
在定时器运行过程中,可以使用 xTimerChangePeriod
函数来改变定时器的周期。该函数的原型如下:
BaseType_t xTimerChangePeriod(
TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait
);
xNewPeriod
:新的定时器周期,以系统时钟节拍为单位。示例代码如下:
if (xTimer != NULL) {
// 改变定时器周期为 2 秒
if (xTimerChangePeriod(xTimer, pdMS_TO_TICKS(2000), 0) != pdPASS) {
// 改变定时器周期失败
}
}
本示例代码将创建一个周期性的软件定时器,每隔 1 秒翻转一次 LED 灯的状态。
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
// 定义 LED 引脚
#define LED_PIN GPIO_PIN_12
#define LED_PORT GPIOD
// 定时器回调函数
void vTimerCallback(TimerHandle_t xTimer) {
HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
}
// 系统时钟配置函数
void SystemClock_Config(void);
// GPIO 初始化函数
static void MX_GPIO_Init(void);
// FreeRTOS 初始化函数
static void MX_FREERTOS_Init(void);
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FREERTOS_Init();
// 启动调度器
vTaskStartScheduler();
// 如果调度器启动失败,程序将执行到这里
while (1) {
}
}
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** 初始化 CPU, AHB 和 APB 总线时钟
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/** 初始化 CPU, AHB 和 APB 总线时钟
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) {
Error_Handler();
}
}
static void MX_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOD_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
/*Configure GPIO pin : LED_PIN */
GPIO_InitStruct.Pin = LED_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
}
static void MX_FREERTOS_Init(void) {
// 创建一个周期性定时器,周期为 1000 个 tick
TimerHandle_t xTimer = xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000), // 定时器周期为 1 秒
pdTRUE, // 周期性定时器
(void *)0, // 定时器标识符
vTimerCallback // 定时器回调函数
);
if (xTimer != NULL) {
// 启动定时器
if (xTimerStart(xTimer, 0) != pdPASS) {
// 启动定时器失败
}
}
}
void Error_Handler(void) {
// 错误处理函数
while(1) {
}
}
vTimerCallback
函数在定时器到期时被调用,在该函数中,使用 HAL_GPIO_TogglePin
函数翻转 LED 灯的状态。SystemClock_Config
函数用于配置系统时钟,这里使用内部高速时钟(HSI)作为系统时钟源。MX_GPIO_Init
函数用于初始化 LED 引脚,将其配置为推挽输出模式。MX_FREERTOS_Init
函数用于创建和启动软件定时器。定时器回调函数是在任务上下文中执行的,因此其执行时间应尽量短,避免长时间占用 CPU,影响其他任务的执行。如果回调函数需要执行较长时间的操作,建议将操作封装成一个任务,在回调函数中发送消息或信号量来触发该任务的执行。
FreeRTOS 有一个专门的定时器服务任务,用于处理定时器的到期事件。定时器服务任务的优先级应根据实际需求进行设置,一般建议设置为较高的优先级,以确保定时器事件能够及时处理。
系统时钟节拍的设置会影响定时器的精度。如果需要较高的定时精度,应将系统时钟节拍设置得较小,但同时会增加 CPU 的开销。因此,需要根据实际需求进行权衡。
本文详细介绍了基于 STM32 HAL 库和 FreeRTOS 的软件定时器的使用方法。通过使用 FreeRTOS 软件定时器,开发者可以在不使用硬件定时器的情况下实现定时功能,节省硬件资源,提高系统的灵活性。同时,本文还给出了一个基于 STM32F4 Discovery 开发板的示例代码,帮助开发者更好地理解和应用软件定时器。在实际开发中,开发者应根据具体需求合理使用软件定时器,并注意定时器回调函数的执行时间、定时器服务任务的优先级和系统时钟节拍的设置等问题。