在单片机中实现一个异步的一次性定时器,通常可以使用硬件定时器模块或者嵌入式操作系统中的定时器功能。以下是一个基于硬件定时器的实现方法,适用于没有操作系统的情况:
1.硬件定时器配置:
选择单片机中的一个硬件定时器(如16位或32位定时器)。
配置定时器的时钟源,预分频器,计数模式等,以便产生所需的时间基准。
设置定时器的比较值或重装载值,以确定定时的时长。
2.中断服务例程(ISR)编写:
编写定时器的中断服务例程,当定时器计数达到设定值时,会产生中断。
在中断服务例程中,清除中断标志,停止定时器,以便将其复用为一次性定时器。
调用预先定义好的回调函数,执行定时器到时后的相关任务。
3.回调函数定义:
定义一个函数指针类型,用于指向定时器事件到时的回调函数。
实现具体的回调函数,执行定时器到时后的操作。
4.定时器初始化和启动:
在主程序中初始化定时器,设置定时时间,并将回调函数地址赋值给相应的函数指针。
启动定时器,开始计时。
5.异步处理:
由于定时器中断是异步发生的,因此不会影响CPU的其他工作。
当CPU处理其他任务时,定时器会在后台独立运行,当达到设定的时间后,触发中断,执行回调函数。
示例代码(伪代码):
// 定义回调函数类型
typedef void (*TimerCallback)(void);
// 定时器中断服务例程
void Timer_ISR(void) {
// 清除中断标志
Clear_Timer_Flag();
// 停止定时器
Stop_Timer();
// 调用回调函数
if (callback != NULL) {
callback();
}
}
// 启动一次性定时器
void Start_OneShot_Timer(uint32_t timeout, TimerCallback cb) {
// 设置定时器比较值或重装载值
Set_Timer_Value(timeout);
// 设置回调函数
callback = cb;
// 启动定时器
Start_Timer();
// 使能定时器中断
Enable_Timer_Interrupt();
}
// 主程序
int main(void) {
// 初始化硬件
Initialize_Hardware();
// 设置一次性定时器,10秒后执行Task函数
Start_OneShot_Timer(10000, Task);
// 主循环
while (1) {
// CPU可以执行其他任务
Perform_Other_Tasks();
}
}
// 定时器到时后的任务
void Task(void) {
// 执行定时任务
}
以上例子实现一个常用的定时器功能,不过函数只能执行一次,并且只能绑定一个函数。如果要绑定多个函数,如何实现呢?
要实现多个定时器事件,每个事件在特定时间触发不同的回调函数,可以通过以下方法:
1定时器管理:
如果单片机支持多个定时器,可以使用多个定时器模块来独立管理每个事件。
如果只有单个定时器可用,可以将其配置为周期性触发,然后使用一个软件计数器来跟踪每个事件的时间。
2事件结构:
创建一个事件结构体,用于存储每个事件的参数,包括剩余时间、回调函数和事件状态等。
3事件列表:
维护一个事件列表,用于存储所有注册的事件。
4定时器中断服务例程:
定时器中断服务例程中,遍历事件列表,更新每个事件的剩余时间。
检查是否有事件的剩余时间减到零,如果有的话,调用相应的回调函数,并更新事件状态。
5事件注册函数:
实现一个事件注册函数,允许用户添加新的事件到事件列表中,设置时间、回调等。
6事件处理函数:
实现一个事件处理函数,用于在定时器中断中调用,以处理到时事件。
下面是一个简化的伪代码示例,展示如何实现这个功能:
#include <stdbool.h>
#include <stdint.h>
// 定义回调函数类型
typedef void (*TimerCallback)(void);
// 事件结构体
typedef struct {
uint32_t timeout; // 事件超时时间
uint32_t remainingTime; // 剩余时间
TimerCallback callback; // 回调函数
bool active; // 事件是否激活
} TimerEvent;
// 假设最多支持10个同时进行的事件
#define MAX_TIMER_EVENTS 10
TimerEvent timerEvents[MAX_TIMER_EVENTS];
// 初始化事件列表
void InitializeTimerEvents(void) {
for (int i = 0; i < MAX_TIMER_EVENTS; i++) {
timerEvents[i].active = false;
}
}
// 注册一个新的事件
bool RegisterTimerEvent(uint32_t timeout, TimerCallback callback) {
for (int i = 0; i < MAX_TIMER_EVENTS; i++) {
if (!timerEvents[i].active) {
timerEvents[i].timeout = timeout;
timerEvents[i].remainingTime = timeout;
timerEvents[i].callback = callback;
timerEvents[i].active = true;
return true; // 注册成功
}
}
return false; // 事件列表已满
}
// 定时器中断服务例程
void Timer_ISR(void) {
for (int i = 0; i < MAX_TIMER_EVENTS; i++) {
if (timerEvents[i].active) {
if (--timerEvents[i].remainingTime == 0) {
// 时间到,执行回调函数
timerEvents[i].callback();
// 根据需要,可以在这里禁用事件或重新设置时间
timerEvents[i].active = false;
}
}
}
// 清除中断标志
Clear_Timer_Flag();
}
// 主程序
int main(void) {
// 初始化硬件
Initialize_Hardware();
InitializeTimerEvents();
// 注册两个事件
RegisterTimerEvent(1000, Task1); // 1秒后执行Task1
RegisterTimerEvent(5000, Task2); // 5秒后执行Task2
// 启动定时器
Start_Timer();
// 使能定时器中断
Enable_Timer_Interrupt();
// 主循环
while (1) {
// CPU可以执行其他任务
Perform_Other_Tasks();
}
}
// 定时器到时后的任务
void Task1(void) {
// 执行任务1
}
void Task2(void) {
// 执行任务2
}
在这个示例中,我们使用了一个软件事件列表来管理多个定时器事件。每次定时器中断发生时,我们都会更新所有活动事件的剩余时间,并在必要时调用回调函数。注意,这个示例假设定时器的分辨率足够高,可以用来实现这些短时间的延迟。如果定时器分辨率不够,可能需要在软件中进行更精细的时间管理。
上述实现,可以实现多个回调函数的注册,并且每个回调函数只运行一次,但是问题来了,示例代码中预先指定了10个事件,如果注册的事件数量大于10个怎么办?学过数据结构,链表可以尝试使用。
如果您不想为事件列表预分配固定大小的内存,并且希望动态地管理事件,可以使用链表来实现。链表允许您根据需要动态地创建和销毁事件,从而更加灵活地使用内存。
以下是使用链表实现定时器事件的伪代码示例:
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
// 定义回调函数类型
typedef void (*TimerCallback)(void);
// 事件节点结构体
typedef struct TimerEventNode {
uint32_t timeout; // 事件超时时间
uint32_t remainingTime; // 剩余时间
TimerCallback callback; // 回调函数
struct TimerEventNode *next; // 指向下一个节点的指针
} TimerEventNode;
// 链表头节点
TimerEventNode *head = NULL;
// 注册一个新的事件
bool RegisterTimerEvent(uint32_t timeout, TimerCallback callback) {
TimerEventNode *newNode = (TimerEventNode *)malloc(sizeof(TimerEventNode));
if (newNode == NULL) {
return false; // 内存分配失败
}
newNode->timeout = timeout;
newNode->remainingTime = timeout;
newNode->callback = callback;
newNode->next = NULL;
// 将新节点添加到链表末尾
if (head == NULL) {
head = newNode;
} else {
TimerEventNode *current = head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
return true; // 注册成功
}
// 定时器中断服务例程
void Timer_ISR(void) {
TimerEventNode *current = head;
TimerEventNode *prev = NULL;
while (current != NULL) {
if (--current->remainingTime == 0) {
// 时间到,执行回调函数
current->callback();
// 从链表中移除当前节点
if (prev == NULL) {
head = current->next;
} else {
prev->next = current->next;
}
free(current); // 释放内存
current = (prev == NULL) ? head : prev->next;
} else {
prev = current;
current = current->next;
}
}
// 清除中断标志
Clear_Timer_Flag();
}
// 主程序
int main(void) {
// 初始化硬件
Initialize_Hardware();
// 注册两个事件
RegisterTimerEvent(1000, Task1); // 1秒后执行Task1
RegisterTimerEvent(5000, Task2); // 5秒后执行Task2
// 启动定时器
Start_Timer();
// 使能定时器中断
Enable_Timer_Interrupt();
// 主循环
while (1) {
// CPU可以执行其他任务
Perform_Other_Tasks();
}
}
// 定时器到时后的任务
void Task1(void) {
// 执行任务1
}
void Task2(void) {
// 执行任务2
}
在这个示例中,我们使用了一个单向链表来管理定时器事件。每个事件都是一个节点,包含指向下一个节点的指针。在定时器中断服务例程中,我们遍历链表,更新每个事件的剩余时间,并在事件超时执行回调函数。事件执行后,我们从链表中移除并释放该节点的内存。这种方法允许您根据需要动态地添加和删除事件,而不会浪费内存。
还有一个点要注意,中断回调函数执行事件不应该超过定时器的最小粒度,要求我们最小粒度定义不能太小,比如最小粒度定义为10ms,这个值是很多操作系统的参考值。
2024.3.12 增加一个取消注册函数的操作。
// 取消注册一个事件
int UnregisterTimerEvent(TimerCallback callback) {
TimerEventNode *current = head;
TimerEventNode *prev = NULL;
while (current != NULL) {
if (current->callback == callback) {
// 找到了匹配的事件
if (prev == NULL) {
// 要删除的节点是头节点
head = current->next;
} else {
// 要删除的节点不是头节点
prev->next = current->next;
}
free(current); // 释放内存
return 0; // 取消注册成功
}
prev = current;
current = current->next;
}
// 如果没有找到匹配的事件,返回错误代码
return -1;
}
在这个取消注册函数中,我们遍历链表来查找具有指定回调函数的事件节点。如果找到匹配的节点,我们就将其从链表中移除并释放内存。如果事件已经执行,它将从链表中移除,因此取消注册将返回-1表示失败。
请注意,这个取消注册函数是基于回调函数地址来匹配事件的。如果同一回调函数被注册了多次,这个函数将取消第一个匹配的事件。如果您需要基于其他条件来取消事件(例如,基于事件的数据或额外的标识符),您需要修改链表节点的结构以包含额外的信息,并在取消注册函数中使用这些信息来匹配正确的节点。
此外,由于定时器中断服务例程可能会并发访问链表,您需要在适当的时刻确保取消注册操作的安全性,例如,在禁用定时器中断的情况下执行取消注册操作。
很多搞嵌入式的都很惧怕动态分配内存,总觉得万一分配失败,功能就不正常了。有关嵌入式的动态内存分配,以后再研究。