Author: Marco Polo Bao
Original article: https://blog.csdn.net/qq_26904271/article/details/83833168
1. What is a Software Timer
A software timer is a timer simulated by a program, which can be simulated by a hardware timer to create thousands of software timers. This allows the program to use many timers without being limited by insufficient hardware resources, which is an advantage of software timers, as their quantity is not restricted.
However, since software timers are implemented through software, their operation and maintenance consume a certain amount of CPU resources, and their precision is relatively inferior to that of hardware timers.
2. Implementation Principle of Software Timers
Operating systems like Linux, uC/OS, and FreeRTOS all come with software timers, and the principles are quite similar. A typical implementation method is to use a hardware timer to generate a fixed clock tick. Each time the hardware timer interrupts, a global time marker is incremented, and each software timer keeps track of its expiration time.
The program needs to periodically scan all running software timers, comparing their expiration times with the global clock marker to determine if the corresponding software timer has expired. If it has, the corresponding callback function is executed, and the timer is disabled.
The above describes the implementation of a one-shot timer. To implement a periodic timer, which resets after expiration, simply retrieve the current time marker value after executing the callback function, add the delay time as the next expiration time, and continue running the software timer.
3. Software Timer Based on STM32
3.1 Clock Tick
A software timer requires a hardware clock source as a reference, which has a fixed tick (understood as the tick of a second hand). A 32-bit global variable tickCnt is used to record the changes in this tick:
static volatile uint32_t tickCnt = 0; // Software timer clock tick
Each tick increments tickCnt by one (recording how many ticks have occurred):
/* Must be executed in the timer interrupt */
void tickCnt_Update(void)
{
tickCnt++;
}
Once it starts running, tickCnt will keep incrementing, and each software timer records an expiration time. As long as tickCnt is greater than the expiration time, it indicates that the timer has expired.
3.2 Data Structure
The data structure of the software timer determines its performance and functionality. Generally, it can be divided into two types: array structure and linked list structure. What does this mean? It refers to how multiple software timers are stored in memory; they can be stored in an array or a linked list.
The advantages and disadvantages of both are based on the characteristics of these two data structures: the array method allows for faster timer lookups but has a fixed quantity, cannot change dynamically, and can waste memory if the array is too large or may not be sufficient if too small. It is suitable for systems with clearly defined and fixed timing events; the linked list method allows for dynamic addition and removal of timers, but can lead to memory fragmentation (if there is no memory management), and the lookup time overhead is relatively larger than that of arrays. Operating systems like Linux, uC/OS, and FreeRTOS use linked list-based software timers.
This article uses an array structure:
static softTimer timer[TIMER_NUM]; // Software timer array
The array and linked list are the overall data structures of the software timer. When it comes to a single timer, it involves the definition of the software timer structure. The functionalities of the software timer are closely related to its structure definition. Below is the structure definition of the software timer used in this article:
typedef struct softTimer {
uint8_t state; // State
uint8_t mode; // Mode
uint32_t match; // Expiration time
uint32_t period; // Timing period
callback *cb; // Callback function pointer
void *argv; // Parameter pointer
uint16_t argc; // Number of parameters
} softTimer;
There are three states for the timer: the default is stopped, running after starting, and timeout after expiration.
typedef enum tmrState {
SOFT_TIMER_STOPPED = 0, // Stopped
SOFT_TIMER_RUNNING, // Running
SOFT_TIMER_TIMEOUT // Timeout
} tmrState;
There are two modes: the one that stops after expiration is the one-shot mode, and the one that resets after expiration is the periodic mode.
typedef enum tmrMode {
MODE_ONE_SHOT = 0, // One-shot mode
MODE_PERIODIC, // Periodic mode,
} tmrMode;
Regardless of the mode, after the timer expires, the callback function will be executed. Below is the definition of that function, where the parameter pointer argv is of type void pointer, allowing different types of parameters to be passed.
typedef void callback(void *argv, uint16_t argc);
The state and callback function pointer cb in the above structure are optional features. If the system does not require a periodically executing timer or does not need to automatically execute a function after expiration, these two definitions can be removed.
3.3 Timer Operations
3.3.1 Initialization
First, the software timer needs to be initialized by assigning initial values to each member of the timer structure. Although static variables are initialized to 0, I believe it is still necessary to maintain the habit of initializing variables to avoid some strange bugs.
void softTimer_Init(void)
{
uint16_t i;
for(i = 0; i < TIMER_NUM; i++) {
timer[i].state = SOFT_TIMER_STOPPED;
timer[i].mode = MODE_ONE_SHOT;
timer[i].match = 0;
timer[i].period = 0;
timer[i].cb = NULL;
timer[i].argv = NULL;
timer[i].argc = 0;
}
}
3.3.2 Start
To start a software timer, not only must its state be changed to running, but it must also be told when it will expire (the current tickCnt value plus the delay time equals the expiration time), whether it is a one-shot or periodic timer, and which function to execute upon expiration, along with the function’s parameters. Once these are specified, the timer can start running.
void softTimer_Start(uint16_t id, tmrMode mode, uint32_t delay, callback *cb, void *argv, uint16_t argc)
{
assert_param(id < TIMER_NUM);
assert_param(mode == MODE_ONE_SHOT || mode == MODE_PERIODIC);
timer[id].match = tickCnt_Get() + delay;
timer[id].period = delay;
timer[id].state = SOFT_TIMER_RUNNING;
timer[id].mode = mode;
timer[id].cb = cb;
timer[id].argv = argv;
timer[id].argc = argc;
}
The assert_param() function in the above code is used for parameter checking, similar to the library function assert().
3.3.3 Update
This software timer has three states: stopped, running, and timeout, and different states perform different actions. The stopped state is the simplest, doing nothing; the running state needs to continuously check for expiration, executing the callback function upon expiration and entering the timeout state; the timeout state checks the timer’s mode. If it is periodic mode, it updates the expiration time and continues running; if it is one-shot mode, it stops the timer.
These operations are implemented by an update function:
void softTimer_Update(void)
{
uint16_t i;
for(i = 0; i < TIMER_NUM; i++) {
switch (timer[i].state) {
case SOFT_TIMER_STOPPED:
break;
case SOFT_TIMER_RUNNING:
if(timer[i].match <= tickCnt_Get()) {
timer[i].state = SOFT_TIMER_TIMEOUT;
timer[i].cb(timer[i].argv, timer[i].argc); // Execute callback function
}
break;
case SOFT_TIMER_TIMEOUT:
if(timer[i].mode == MODE_ONE_SHOT) {
timer[i].state = SOFT_TIMER_STOPPED;
} else {
timer[i].match = tickCnt_Get() + timer[i].period;
timer[i].state = SOFT_TIMER_RUNNING;
}
break;
default:
printf("timer[%d] state error!\r\n", i);
break;
}
}
}
3.3.4 Stop
If the timer is running and you want to stop it, a stop function is needed. The operation is simple: just change the target timer’s state to stopped:
void softTimer_Stop(uint16_t id)
{
assert_param(id < TIMER_NUM);
timer[id].state = SOFT_TIMER_STOPPED;
}
3.3.5 Read State
If you want to know whether a timer is running or has stopped, it is also simple; just return its state:
uint8_t softTimer_GetState(uint16_t id)
{
return timer[id].state;
}
This may seem strange; why return the state instead of reading it directly? Remember that the timer array defined in section 3.2 is a static global variable, which can only be accessed by the current source file. When external files need to access it, they can only do so through function returns. This is a simple encapsulation that maintains the modularity of the program.
3.4 Testing
Finally, we need to verify whether our software timer achieves the expected functionality.
Define three timers:
The timer TMR_STRING_PRINT executes only once, printing a string on Serial Port 1 after 1 second;
The timer TMR_TWINKLING is a periodic timer with a period of 0.5 seconds, toggling the state of LED0 each time it expires, achieving LED0 blinking;
The timer TMR_DELAY_ON executes once, turning on LED1 after 3 seconds. Unlike the first timer, this timer’s callback function is a no-operation function nop(), and the operation to turn on LED1 is implemented by checking the timer’s state in the main loop. This method may be used in certain situations.
static uint8_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
int main(void)
{
USART1_Init(115200);
TIM4_Init(TIME_BASE_MS);
TIM4_NVIC_Config();
LED_Init();
printf("I just grabbed a spoon.\r\n");
softTimer_Start(TMR_STRING_PRINT, MODE_ONE_SHOT, 1000, stringPrint, data, 5);
softTimer_Start(TMR_TWINKLING, MODE_PERIODIC, 500, LED0_Twinkling, NULL, 0);
softTimer_Start(TMR_DELAY_ON, MODE_ONE_SHOT, 3000, nop, NULL, 0);
while(1) {
softTimer_Update();
if(softTimer_GetState(TMR_DELAY_ON) == SOFT_TIMER_TIMEOUT) {
LED1_On();
}
}
}
Reprint Statement: All reprinted articles must indicate the original source or reprint source (if the reprint does not indicate the original source, please contact for deletion).