STM32开发项目:软件虚拟定时器的实现

本文介绍了一种用于补充单片机硬件定时器数量不足和难以产生长周期延时的软件定时器库。该库通过虚拟化大量软件定时器,有效解决了复杂项目中的定时需求。文章详细解释了软件定时器的特性、源码实现、基本操作流程及实际项目应用案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

日期作者版本说明
2020.08.18TaoV1.0发布了第一版库函数
2020.09.19TaoV1.1增加了使用指南的内容

背景

在不运行实时操作系统的单片机中,面对一些复杂的项目需求时,仅有的几个硬件定时器显得有些捉襟见肘,这主要体现在两个方面:其一是硬件定时器数量十分有限(一般4~8个);其二是硬件定时器难以直接产生周期很长(秒级)的延时。针对这两个缺陷,笔者编写了一个使用的软件定时器库,通过占用了一个硬件定时器虚拟出大量的软件定时器以供项目工程调用。无数的项目实践证明,本软件定时器库是方便、试用而又可靠的。

软件定时器库特性

软件定时器库主要针对弥补硬件定时器数量十分有限难以直接产生长周期的延时这两个缺陷而设计的。通过封装软件定时器结构体类型,通过声明和初始化一个软件定时器结构体数组就能自由的增减软件定时器数量。通过将一个硬件定时器的更新周期作为时基,可以轻松实现长时间的延时或者长周期的轮询。通过修改软件定时器结构体成员的值,可以十分方便的实现设置循环次数、循环周期、注册处理函数等功能。同时,通过监控软件定时器结构体成员的值,也能实时了解软件定时器的工作状态与所处阶段。

源码介绍

softwaretimer.c

#include "softwaretimer.h"

SoftwareTimerStruct SoftwareTimerList[SOFTWARE_TIMER_NUM] =
{
	{ 0, 0, 0, NULL, 0, 0 },
	{ 1, 0, 0, NULL, 0, 0 },
	{ 2, 0, 0, NULL, 0, 0 },
	{ 3, 0, 0, NULL, 0, 0 },
	{ 4, 0, 0, NULL, 0, 0 },
	{ 5, 0, 0, NULL, 0, 0 },
	{ 6, 0, 0, NULL, 0, 0 },
	{ 7, 0, 0, NULL, 0, 0 },
	{ 8, 0, 0, NULL, 0, 0 },
	{ 9, 0, 0, NULL, 0, 0 },
	{ 10, 0, 0, NULL, 0, 0 },
	{ 11, 0, 0, NULL, 0, 0 },
	{ 12, 0, 0, NULL, 0, 0 },
	{ 13, 0, 0, NULL, 0, 0 },
	{ 14, 0, 0, NULL, 0, 0 },
};

/**
 * @brief Generally placed in the interrupt handler of the hardware timer
 * 	and called periodically.
 * @param none
 * @retval none
 */
void SoftwareTimer_Tick(void)
{
	for(uint8_t i=0;i<SOFTWARE_TIMER_NUM ;i++)
	{
		if(SoftwareTimerList[i].isEnable == 0 || SoftwareTimerList[i].expire == 0)
		{
			continue;
		}

		SoftwareTimerList[i].count++;

		if(SoftwareTimerList[i].count >= SoftwareTimerList[i].expire)
		{
			SoftwareTimerList[i].count = 0;
			if(SoftwareTimerList[i].repeat > 0)
			{
				SoftwareTimerList[i].repeat--;
				if(SoftwareTimerList[i].callback != NULL)
					SoftwareTimerList[i].callback();
			}
			else if(SoftwareTimerList[i].repeat == 0)
			{
				SoftwareTimerList[i].isEnable = 0;
//				memset(&SoftwareTimerList[i], 0, sizeof(SoftwareTimerList[i]));
			}
			else
			{
				if(SoftwareTimerList[i].callback != NULL)
					SoftwareTimerList[i].callback();
			}
		}
	}
}

/**
 * @brief Initialization of software timer.
 * @param id: ID of the timer, which need to be initialized.
 * @param expire: The period of time to execute callback function (trigger timeout event).
 * @param callback: The callback function of timer. When the timer is expired, this function will be called.
 * @param repeat: Repeat times of the timer.
 * 		@arg -1: repeat forever
 * 		@arg 0: invalid
 * 		@arg more than 0: repeat times
 * @retval none
 */
void SoftwareTimer_Init(uint8_t id,uint32_t expire,timeout_handler callback,int16_t repeat)
{
	SoftwareTimerList[id].id = id;
	SoftwareTimerList[id].expire = expire;
	SoftwareTimerList[id].callback = callback;
	SoftwareTimerList[id].repeat = repeat;
	SoftwareTimerList[id].count = 0;
	SoftwareTimerList[id].isEnable = 0;
}

/**
 * @brief Enable the timer, which specified by ID.
 * @param id: Id of the timer.
 * @retval none
 */
void SoftwareTimer_Enable(uint8_t id)
{
	SoftwareTimerList[id].isEnable = 1;
}

/**
 * @brief Disable the timer, which specified by ID.
 * @param id: Id of the timer.
 * @retval none
 */
void SoftwareTimer_Disable(uint8_t id)
{
	SoftwareTimerList[id].isEnable = 0;
}

softwaretimer.h

#ifndef __SOFTWARETIMER_H__
#define __SOFTWARETIMER_H__

#include "stm32f10x.h"

#define SOFTWARE_TIMER_NUM	15

typedef void (*timeout_handler)();

typedef struct
{
	uint8_t id;
	uint32_t count;
	uint32_t expire;
	timeout_handler callback;
	uint8_t isEnable;	//0,disable 1,enable
	int16_t repeat;		//-1,forever
} SoftwareTimerStruct;

extern SoftwareTimerStruct SoftwareTimerList[SOFTWARE_TIMER_NUM];

void SoftwareTimer_Tick(void);
void SoftwareTimer_Init(uint8_t id,uint32_t expire,timeout_handler callback,int16_t repeat);
void SoftwareTimer_Enable(uint8_t id);
void SoftwareTimer_Disable(uint8_t id);


#endif

使用指南

基本操作流程

  • 定义软件定时器结构体数组
    由于通过C语言封装了软件定时器,因此在使用前首先需要定义软件定时器结构体数组。以上一小节提供的源码为例说明:

在源文件中定义结构体数组:

SoftwareTimerStruct SoftwareTimerList[SOFTWARE_TIMER_NUM] =
{
	{ 0, 0, 0, NULL, 0, 0 },
	{ 1, 0, 0, NULL, 0, 0 },
	{ 2, 0, 0, NULL, 0, 0 },
	{ 3, 0, 0, NULL, 0, 0 },
	{ 4, 0, 0, NULL, 0, 0 },
	{ 5, 0, 0, NULL, 0, 0 },
	{ 6, 0, 0, NULL, 0, 0 },
	{ 7, 0, 0, NULL, 0, 0 },
	{ 8, 0, 0, NULL, 0, 0 },
	{ 9, 0, 0, NULL, 0, 0 },
	{ 10, 0, 0, NULL, 0, 0 },
	{ 11, 0, 0, NULL, 0, 0 },
	{ 12, 0, 0, NULL, 0, 0 },
	{ 13, 0, 0, NULL, 0, 0 },
	{ 14, 0, 0, NULL, 0, 0 },
};

在头文件中声明外部变量,以便其他地方可以调用定时器:

extern SoftwareTimerStruct SoftwareTimerList[SOFTWARE_TIMER_NUM];
  • 在程序的合适地方初始化定时器,并为它们注册回调函数
/**
 * @brief 初始化定时器
 */
void User_SetupTimer()
{
	//Timer0 is used for system led flash.
	SoftwareTimer_Init(0, 1, User_SoftwareTimer0_Handler, -1);
	//Timer1 is used for ...
	SoftwareTimer_Init(1, 60, User_SoftwareTimer1_Handler, -1);
}

/**
 * @brief Timer1 回调函数
 */
void User_SoftwareTimer0_Handler()
{
	/*项目相关代码*/
}

/**
 * @brief Timer2 回调函数
 */
void User_SoftwareTimer1_Handler()
{
	/*项目相关代码*/
}
  • 设置时基服务函数
    一般在单片机的硬件定时器中断服务函数中调用软件定时器的时基服务函数void SoftwareTimer_Tick(void),调用周期即为软件定时器的时基
void TIM3_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET)
	{
		SoftwareTimer_Tick();
		
		TIM_ClearFlag(TIM3,TIM_FLAG_Update);
		TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
	}
}
  • 在程序的合适地方启动定时器
	SoftwareTimer_Enable(0);
	SoftwareTimer_Enable(1);
  • 停止使用时手动关闭定时器
	SoftwareTimer_Disable(0);
	SoftwareTimer_Disable(1);

应用项目举例

在某数据采集项目中,需要同时使用单片机的5路串口与7个独立模块进行通讯,如下图所示(其中一个串口是485总线,上面挂载了两个设备;还有一个串口由三态端口缓冲器分时复用为两个串口),同时还需要驱动一个ADS1115进行多通道的数据采集。
串口拓扑结构图
这个项目的固件驱动最大的难点在于串口通讯时,不同模块的响应速度是不一样的,如果在一个刷新函数中依次轮询、等待、处理各个模块数据时,会有大量的延时等待时间,极大的降低了单片机的工作效率。在没有运行实时操作系统的单片机上,程序以类似于单线程的结构运行,无论在主函数的轮询中还是在中断服务函数中,都应该极力避免出现长时间的延时(毫秒级)

采用多个定时器的更新中断以不同的时间周期刷新这些模块是一个较为实用的解决办法。但是这个项目中,由于需要通讯的模块太多,如果每个模块的刷新都单独配备一个硬件定时器,则会比较浪费单片机资源。实际情况是,由于本项目还需要驱动四路步进电机,已经占用了4路硬件定时器,剩下的定时器已经不足以满足分别以不同速度刷新不同模块数据的需要。
在这里插入图片描述
如上图所示,采用本文介绍的软件虚拟定时器则可以较好的解决这个难题,轻松克服了单片机硬件定时器不足的缺陷。部分相关代码如下:

void User_SetupTimer()
{
	//SW_Timer0 for refresh sensor data and modbus action. 	Freq. = 5Hz
	SoftwareTimer_Init(0, 20, User_SoftwareTimer0_Handler, -1);

	//SW_Timer1 for ads1115 data acquisition.	Freq. = 20Hz
	SoftwareTimer_Init(1, 5, User_SoftwareTimer1_Handler, -1);

	//SW_Timer2 for RS485 scan.		Freq. = 10Hz
	SoftwareTimer_Init(2, 10, User_SoftwareTimer2_Handler, -1);

	//SW_Timer3 for MTSICS.		Freq. = 5Hz
	SoftwareTimer_Init(3, 20, User_SoftwareTimer3_Handler, -1);

	//SW_Timer4 for RS100 (Printer has not been used).		Freq. = 5Hz
	SoftwareTimer_Init(4, 20, User_SoftwareTimer4_Handler, -1);

	SoftwareTimer_Enable(0);
	SoftwareTimer_Enable(1);
	SoftwareTimer_Enable(2);
	SoftwareTimer_Enable(3);
	SoftwareTimer_Enable(4);
}

void User_SoftwareTimer0_Handler()
{
	User_RefreshData();
	User_RefreshAction();
}

void User_SoftwareTimer1_Handler()
{
	ADS1115_RefreshAllChannel();
}

void User_SoftwareTimer2_Handler()
{
	static uint8_t chan = 0;

	if(chan == 0)
	{
		//首先解析上一个设备返回的数据
		UART5_RevBuffer_Handler(OmronEC55_PausePV);
		//然后发送下个设备的指令
		SGDAQ_GetPV( );
	}
	else if(chan == 1)
	{
		UART5_RevBuffer_Handler(SGDAQ_PausePV);
		OmronEC55_GetPV( );
	}
	else
	{

	}

	chan++;
	if(chan>1)
		chan = 0;
}

void User_SoftwareTimer3_Handler()
{
	//解析 梅特勒称重模块 返回指令
	USART3_RevBuffer_Handler(MTSICS_PauseWeight);
	//请求 梅特勒称重模块 重量指令
	MTSICS_GetWeight( );
}

void User_SoftwareTimer4_Handler()
{
	//Open port channel 1.
	PORT_SW2 = 1;							// Disable port 2
	PORT_SW1 = 0;							// Enable port 1

	//解析 RS100光栅尺 返回指令
	USART1_RevBuffer_Handler(RS100_PauseCount);
	//请求 RS100光栅尺 长度指令
	RS100_GetCount( );
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全能骑士涛锅锅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值