1.创建软件定时器
1.1 源码解析
功能 :动态创建一个软件定时器实例,分配定时器控制块(Timer_t)所需的内存,完成基础初始化。
注意:创建后的定时器处于休眠状态,不会自动开始计时,必须调用 xTimerStart() 等方法才会激活。
参数:
-
pcTimerName:定时器名称字符串指针,用于调试和运行时统计(可通过pcTimerGetName()获取)。FreeRTOS 不会拷贝该字符串,仅保存指针,因此传入的字符串必须保证在整个定时器生命周期内有效(通常使用字符串字面量即可)。 -
xTimerPeriodInTicks:定时器周期,单位为系统节拍(Tick)。定时器到期后触发回调,若为自动重载模式则以此周期重复触发。 -
xAutoReload:-
pdTRUE:自动重载模式。定时器到期后自动重新启动,周期性地触发回调。 -
pdFALSE:单次模式。定时器到期后进入休眠态,需手动再次调用xTimerStart()才能再次触发。
-
-
pvTimerID:定时器 ID 指针,用户可附加任意数据(如结构体指针),通过pvTimerGetTimerID()在回调中获取。传NULL表示不使用。 -
pxCallbackFunction:定时器回调函数指针。定时器到期时,由定时器服务任务(Timer Task)调用此函数。回调函数必须符合void vCallbackFunction( TimerHandle_t xTimer )的原型。
返回值:
-
成功:返回定时器句柄(
Timer_t *),可用于后续操作(启动、停止、删除等)。 -
失败:返回
NULL(内存分配失败)。TimerHandle_t xTimerCreate( const char * const pcTimerName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const TickType_t xTimerPeriodInTicks,
const BaseType_t xAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )
{
Timer_t * pxNewTimer; //指向新分配的定时器控制块的指针,初始化为 pvPortMalloc 的返回值。/* 内存分配 * 使用 FreeRTOS 的动态内存分配函数分配 Timer_t 结构体所需的内存。分配策略取决于 FreeRTOSConfig.h 中配置的堆管理方案(heap_1.c ~ heap_5.c) * 若内存不足,pvPortMalloc 返回 NULL,函数将直接返回 NULL 给调用者。 */ pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) ); /*lint !e9087 !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack, and the first member of Timer_t is always a pointer to the timer's mame. */ /* 空指针检查与初始化 */ if( pxNewTimer != NULL ) //分配成功 { /* Status is thus far zero as the timer is not created statically * and has not been started. The auto-reload bit may get set in * prvInitialiseNewTimer. */ /* ucStatus 是定时器的状态字节,各 bit 定义如下: * Bit 0 (0x01) --- tmrSTATUS_IS_ACTIVE --- 是否已启动(活跃态) * Bit 1 (0x02) --- tmrSTATUS_IS_STATICALLY_ALLOCATED --- 是否由静态分配创建 * Bit 2 (0x04) --- tmrSTATUS_IS_AUTORELOAD --- 是否自动重载 * * 此处全部清零,表示: * - 未启动(处于休眠态 Dormant) * - 非静态分配(动态创建,后续可安全释放) * - 自动重载位暂为 0,prvInitialiseNewTimer 中会根据 xAutoReload 参数 * 调用宏将该位置位或保持清零 */ /* 注意:这里没有设置静态分配的标记位。在删除定时器时,vTimerDelete 函数会检查 ucStatus 中的静态分配标记;此处默认为 0, * 意味着该定时器没有静态分配标记,可以被安全释放。这与动态创建的任务、队列等行为一致。 */ pxNewTimer->ucStatus = 0x00; /* 完成剩余字段的填充(包括将 xAutoReload 参数最终编码进 ucStatus 的对应位、设置周期、名称指针、ID 指针、回调函数指针,并将定时器插入内核的定时器列表中等)。 */ /* 此时定时器处于休眠态(Dormant),未被加入当前活动定时器列表,等待 xTimerStart 等命令才会真正激活 */ prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, xAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer ); } return pxNewTimer; //返回句柄}
1.2 使用示例
1.2.1 一次性软件定时器
/* 软件定时器的句柄 */
TimerHandle_t timer1_handle;
/* 创建一次性软件定时器 */
timer1_handle = xTimerCreate(
"ergou", // 软件定时器的名称
(TickType_t)500, // 超时时间 = 500个 RTOS的时钟节拍
pdFALSE, // 是否自动重载:pdTRUE --- 周期型; pdFALSE --- 一次性
(void *)1, // 定时器的唯一ID
timer1_callback);
if (timer1_handle != NULL)
{
printf("timer1一次性定时器创建成功...\r\n");
}
/* 回调函数 */
void timer1_callback(TimerHandle_t xTimer)
{
static uint16_t timer1_count = 0;
printf("timer1超时回调=%d次..\r\n",++timer1_count);
}
1.2.2 周期性软件定时器
/* 软件定时器的句柄 */
TimerHandle_t timer2_handle;
/* 创建周期性软件定时器 */
timer2_handle = xTimerCreate(
"gousheng", // 软件定时器的名称
(TickType_t)1000, // 超时时间 = 1000个 RTOS的时钟节拍
pdTRUE, // 是否自动重载:pdTRUE --- 周期型; pdFALSE --- 一次性
(void *)2, // 定时器的唯一ID
timer2_callback);
if (timer2_handle != NULL)
{
printf("timer2周期性定时器创建成功...\r\n");
}
/* 回调函数 */
void timer2_callback(TimerHandle_t xTimer)
{
static uint16_t timer2_count = 0;
printf("timer2超时回调=%d次..\r\n",++timer2_count);
}
2.启动软件定时器
2.1 源码解析
功能 :启动指定的软件定时器。该宏是对 xTimerGenericCommand 的封装,向定时器服务任务发送一条 tmrCOMMAND_START 命令。定时器服务任务收到命令后,将定时器从休眠态切换到活跃态,并根据定时器周期开始计时。
参数:
-
xTimer:要启动的定时器句柄,由xTimerCreate或xTimerCreateStatic返回。 -
xTicksToWait:命令发送时的最大阻塞时间。若定时器命令队列满,调用任务将阻塞等待,直到队列有空位或超时。对于启动定时器这类非紧急操作,通常传0(非阻塞)或较短超时。 -
内部固定展开为:
-
xCommandID=tmrCOMMAND_START,指定操作为"启动定时器"。 -
xOptionalValue=xTaskGetTickCount(),将当前系统节拍计数器值作为附加数据传入。定时器服务任务会利用这个时刻来计算该定时器的首次到期时间(当前时刻 + 定时器周期),从而确保定时器从现在开始计时,而非从创建时刻或某个旧时刻开始。 -
pxHigherPriorityTaskWoken=NULL,表示这是任务级调用,不需要中断唤醒标志。#define xTimerStart( xTimer, xTicksToWait )
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
-
功能 :向定时器服务任务发送一条命令消息,指示其对指定定时器执行 xCommandID 所定义的操作。该函数是 FreeRTOS 定时器 API(xTimerStart、xTimerStop、xTimerReset、xTimerChangePeriod、xTimerDelete 等)的统一底层实现,所有定时器操作均通过此函数将命令封装后发往定时器命令队列。
参数:
-
xTimer:目标定时器句柄。 -
xCommandID:命令标识,决定对定时器执行何种操作。常见命令:-
tmrCOMMAND_START:启动定时器 -
tmrCOMMAND_STOP:停止定时器 -
tmrCOMMAND_RESET:复位定时器 -
tmrCOMMAND_CHANGE_PERIOD:修改定时器周期 -
tmrCOMMAND_DELETE:删除定时器 -
以及对应的
_FROM_ISR版本命令
-
-
xOptionalValue:命令携带的可选值,含义取决于具体命令(例如CHANGE_PERIOD时为新的周期值,START时可以为阻塞时间等)。 -
pxHigherPriorityTaskWoken:仅用于中断版本 (xCommandID >= tmrFIRST_FROM_ISR_COMMAND)。若向队列发送消息导致更高优先级任务就绪,该标志被置为pdTRUE,用于在中断退出时触发上下文切换。任务级调用时该参数被忽略(可为NULL)。 -
xTicksToWait:仅用于任务版本 (xCommandID < tmrFIRST_FROM_ISR_COMMAND)。若队列满,任务等待发送的阻塞时间。
返回值:
-
pdPASS:命令成功发送到队列。 -
pdFAIL:发送失败(例如队列满且阻塞超时,或队列尚未创建)。BaseType_t xTimerGenericCommand( TimerHandle_t xTimer,
const BaseType_t xCommandID,
const TickType_t xOptionalValue,
BaseType_t * const pxHigherPriorityTaskWoken,
const TickType_t xTicksToWait )
{
BaseType_t xReturn = pdFAIL; //返回值,初始化为 pdFAIL,仅在成功发送消息后改为 pdPASS
DaemonTaskMessage_t xMessage; //命令消息结构体,包含命令 ID 和定时器参数,将被发送到定时器命令队列。/* 断言检查,确保传入的定时器句柄有效(非 NULL),需用户自定义实现 */ configASSERT( xTimer ); /* Send a message to the timer service task to perform a particular action * on a particular timer definition. */ /* 队列存在性检查,xTimerQueue 全局的定时器命令队列句柄 */ if( xTimerQueue != NULL ) { /* Send a command to the timer service task to start the xTimer timer. */ xMessage.xMessageID = xCommandID; //消息类型,此处为定时器命令 ID xMessage.u.xTimerParameters.xMessageValue = xOptionalValue; //命令附加数据(如新周期值) xMessage.u.xTimerParameters.pxTimer = xTimer; //目标定时器指针 /* 命令ID划分:FreeRTOS 内部定义了两套命令,一套用于任务调用(如 tmrCOMMAND_START),一套用于中断调用(如 tmrCOMMAND_START_FROM_ISR), * 两者数值不同。tmrFIRST_FROM_ISR_COMMAND 是中断类命令的起始值。所有小于该值的命令为任务版本,否则为中断版本 */ if( xCommandID < tmrFIRST_FROM_ISR_COMMAND ) //任务版本 { /* 若调度器正在运行(taskSCHEDULER_RUNNING),则使用带有阻塞时间的 xQueueSendToBack,允许调用者在队列满时等待。 */ if( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING ) { /* 大部分定时器 API 会传入 0(非阻塞),但在某些内部场景中可能允许短时等待 */ /* 将封装好的命令消息 xMessage 发送到定时器命令队列 xTimerQueue 的尾部(FIFO 入队)。 * * 【队列特性】xTimerQueue 是一个普通队列,仅负责传递命令,不涉及时间排序。 * 它是调用者任务与定时器服务任务之间唯一的通信通道。 * * 【定时器服务任务】定时器服务任务(Timer Service Task / Daemon Task)是 * FreeRTOS 在调度器启动时自动创建的一个后台任务,专门负责处理所有软件定时器。 * 它的核心职责: * - 阻塞读取 xTimerQueue,接收来自其他任务的定时器命令; * - 维护活动定时器列表 pxCurrentTimerList(按到期时间升序排列); * - 检查并处理到期定时器,执行用户注册的回调函数; * - 对周期定时器自动重新计算到期时间并插回列表。 * 所有对定时器列表的修改都集中在该任务中完成,天然线程安全,无需额外加锁。 * * 【回调执行上下文】注意:用户注册的定时器回调函数是在定时器服务任务的上下文中 * 执行的,而非调用 xTimerStart 等 API 的任务。因此回调应简短、非阻塞, * 不能调用会阻塞定时器服务任务的 API。 * * 【发送行为】xQueueSendToBack() 将消息拷贝到队列尾部: * - 若队列有空位:立即入队,返回 pdPASS。 * - 若队列已满: * xTicksToWait > 0 → 当前任务阻塞等待,直到有空位或超时。 * xTicksToWait == 0 → 立即返回 pdFAIL,不阻塞。 * * 【阻塞时间来源】此处 xTicksToWait 由上层定时器 API 宏(如 xTimerStart)传入: * 通常为 0(非阻塞),避免调用任务因队列满而被延迟; * 部分场景可能传入 portMAX_DELAY,确保命令一定送达。 * * 【时间排序的时机】排序不在此处发生。 * 定时器服务任务从队列取出命令后,根据命令类型(如 tmrCOMMAND_START) * 计算定时器的绝对到期时间(当前Tick + 周期), * 并调用 prvInsertTimerInActiveList() 将定时器按到期时间升序插入到 * 活动定时器列表 pxCurrentTimerList 中。 */ xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait ); } /* 若调度器尚未启动或已挂起,则使用非阻塞方式(tmrNO_DELAY),防止死锁或阻塞调度器 */ else { xReturn = xQueueSendToBack( xTimerQueue, &xMessage, tmrNO_DELAY ); } } else //中断版本 { /* 使用 xQueueSendToBackFromISR,这是中断安全的队列发送函数,无阻塞时间参数,但需要 pxHigherPriorityTaskWoken 指针用于返回是否需要上下文切换 */ xReturn = xQueueSendToBackFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken ); } /* 调试宏,记录本次命令发送的详细信息,需用户自定义实现 */ traceTIMER_COMMAND_SEND( xTimer, xCommandID, xOptionalValue, xReturn ); } else //说明定时器服务尚未初始化,无法发送命令,函数直接返回 pdFAIL。 { mtCOVERAGE_TEST_MARKER(); //代码覆盖率测试,实际为空 } return xReturn; //最终返回发送结果}
2.2 使用示例
BaseType_t res = 0;
/* 开启软件定时器 */
res = xTimerStart(timer1_handle, portMAX_DELAY);
if (res != pdFAIL)
{
printf("timer1单次定时器启动成功\r\n");
}
res = xTimerStart(timer2_handle, portMAX_DELAY);
if (res != pdFAIL)
{
printf("timer2周期定时器启动成功\r\n");
}
3.停止软件定时器
3.1 源码解析
功能 :停止指定的软件定时器。该宏是对 xTimerGenericCommand 的封装,向定时器服务任务发送一条 tmrCOMMAND_STOP 命令。定时器服务任务收到后,将定时器从活跃态切换到休眠态,定时器不再计时,回调也不再触发。
参数:
-
xTimer:要停止的定时器句柄。 -
xTicksToWait:命令发送时的最大阻塞时间。若定时器命令队列满,调用任务将阻塞等待。通常传0表示非阻塞。 -
内部固定展开为:
-
xCommandID=tmrCOMMAND_STOP,指定操作为"停止定时器"。 -
xOptionalValue=0U。停止操作不需要附加的时间戳信息,因此传入 0。 -
pxHigherPriorityTaskWoken=NULL,表示任务级调用。
-
【停止软件定时器本质上调用的函数接口和启动软件定时器调用一致,具体该函数解析可查看本篇章2.启动软件定时器】
#define xTimerStop( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, ( xTicksToWait ) )
3.2 使用示例
BaseType_t res = 0;
/* 停止软件定时器 */
res = xTimerStop(timer1_handle, portMAX_DELAY);
if (res != pdFAIL)
{
printf("timer1单次定时器停止成功\r\n");
}
res = xTimerStop(timer2_handle, portMAX_DELAY);
if (res != pdFAIL)
{
printf("timer2周期定时器停止成功\r\n");
}
4.删除软件定时器
4.1 源码解析
功能 :删除指定定时器。向定时器服务任务发送 tmrCOMMAND_DELETE 命令,服务任务收到后从定时器列表中移除该定时器,并根据 ucStatus 中的静态分配标记决定是否释放内存。
参数:
-
xTimer:要停止的定时器句柄。 -
xTicksToWait:命令发送时的最大阻塞时间。若定时器命令队列满,调用任务将阻塞等待。通常传0表示非阻塞。 -
内部固定展开为:
-
xCommandID=tmrCOMMAND_DELETE,指定操作为"删除定时器"。 -
xOptionalValue=0U。停止操作不需要附加的时间戳信息,因此传入 0。 -
pxHigherPriorityTaskWoken=NULL,表示任务级调用。
-
【停止软件定时器本质上调用的函数接口和启动软件定时器调用一致,具体该函数解析可查看本篇章2.启动软件定时器】
#define xTimerDelete( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_DELETE, 0U, NULL, ( xTicksToWait ) )
4.2 使用示例
BaseType_t res = 0;
res = xTimerDelete( timer1_handle, portMAX_DELAY );
if( res != pdFAIL )
{
printf("timer1 定时器删除成功\r\n");
timer1_handle = NULL; // 防止野指针
}
5.其它常用定时器操作
| 操作 | 宏 | 说明 |
|---|---|---|
| 复位定时器 | xTimerReset( xTimer, xTicksToWait ) |
重新从当前时刻开始计时,相当于先 Stop 再 Start |
| 修改周期 | xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) |
动态修改定时器周期,适用于需要变速计时的场景 |
两者底层均调用 xTimerGenericCommand,分别传入 tmrCOMMAND_RESET 和 tmrCOMMAND_CHANGE_PERIOD |
6.定时实现原理
定时器服务任务维护一个按到期时间排序的活动定时器列表:
-
调用者调用
xTimerStart,内部通过xTimerGenericCommand将tmrCOMMAND_START命令和定时器周期值 打包成消息,发送到定时器命令队列xTimerQueue。 -
定时器服务任务从队列取出命令后,读取当前 Tick ,计算绝对到期时间 = 当前Tick + 周期 ,将定时器按到期时间升序插入活动列表
pxCurrentTimerList。 -
服务任务计算最近到期时间 - 当前Tick 作为阻塞时长,调用
xQueueReceive在命令队列上阻塞等待。若阻塞时间到期,队列接收返回errQUEUE_EMPTY,表示有定时器到期(或无新命令)。 -
服务任务检查活动列表中所有到期(绝对到期时间 ≤ 当前 Tick)的定时器,执行其回调。
-
对于自动重载定时器,重新计算下次到期时间并插回列表;对于单次定时器,处理后直接从活动列表移除,不再重新插入。
7.回调函数注意事项
-
回调在定时器服务任务 的上下文中执行,不是调用
xTimerStart的那个任务。 -
回调应简短、非阻塞,不能调用会阻塞定时器服务任务的 API(如
vTaskDelay)。 -
若需要在回调中处理复杂逻辑,建议在回调中向处理任务发送信号量或通知,让处理任务去执行。
8.声明
(1)Copyright (C) 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
(2)文中代码来自FreeRTOS,遵循MIT许可证,许可证可参考:https://opensource.org/licenses/MIT
/*
* FreeRTOS Kernel V10.5.1
* Copyright (C) 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* https://www.FreeRTOS.org
* https://github.com/FreeRTOS
*
*/
【以上内容为个人在学习FreeRTOS过程中的源码解读笔记,欢迎大家在评论区讨论指正。】
【如果本篇内容对你有帮助,不妨点个关注,你的支持是我持续更新的动力!】