Common Implementation Methods of Software Timers in MCUs

Follow+Star PublicAccount, don’t miss out on exciting contentSource | Internet

In general embedded product design, due to cost, power consumption, etc., the selected MCU is usually resource-constrained, and the number of timers inside is also limited. In our software design, there are often various timing requirements, such as pulse output, key detection, LCD screen switching delay, etc. We cannot open a hardware timer for every timing task; firstly, hardware resources may be insufficient, and secondly, it would make the software overly dependent on the hardware platform, leading to poor portability.

If we have a software timer, all timing tasks depend on the software timer, which not only saves hardware resources but also means that when porting in the future, we only need to modify the parts related to the software timer and hardware, and the other parts do not need to be changed.

Implementation methods of software timers:

1. Implementing Software Timers Using Struct Arrays

Implementing using struct arrays is relatively simple and easy to understand. However, compared to the linked list implementation method that follows, it has no other advantages. But it is still worth introducing the implementation method: define a start flag and a timing duration in the struct array, along with a count variable. These three variables are the most basic, and others can be added as needed, such as running mode, callback function pointers, etc. Each struct array represents a timer, and we need to define how large this struct array will be in advance.

After defining it, when starting the timer, we set the corresponding array’s start flag. In the hardware tick interrupt service function, we check all struct arrays to see if the start flag is set. When we find that the current start is set, we compare the duration and count in this array. If they are equal, it means the timer’s timing has expired; if not, we increment count and continue checking other arrays’ start flags, looping indefinitely.

The obvious drawback of this method is that in the hardware tick interrupt service function, we have to poll all arrays. If our software business requirement is for 20 timing tasks, we have to define 20 arrays in the software timer implementation. The space waste is secondary; the key issue is that the more arrays there are in the hardware tick polling, the longer it takes to execute to a certain array. If there are 50 or 100 timing requirements in the future, it will lead to very inaccurate timing.

2. Implementing Software Timers Using Linked Lists

Given the various drawbacks of using struct arrays to implement software timers, we propose an improvement plan. After analysis, in most timing tasks, it is often only necessary to time once within a certain time period, meaning the timer will start timing and then stop timing. Of course, the timer implemented with arrays can also start and stop timing; it just needs to use the start flag to decide. However, in the array implementation, even if you stop the timer by removing the start flag, the space of the array does not decrease, and the hardware tick still has to poll all arrays.

Therefore, we need to use a linked list to implement the software timer. In the hardware tick, we poll all nodes; when a timer is started, we add a node, and when the timer is stopped, we delete a node. This ensures that at the current moment, we only poll the nodes that need timing, greatly ensuring timing accuracy.

Additionally, it allows users to choose whether to execute directly in the hardware tick when the timing is up or set a flag in the hardware tick and then queue execution in a while loop, which can effectively solve the problem of inaccurate timing for critical business tasks, such as pulse output, which requires precise timing.

Linked List Implementation

Header File:

/**
 * sfor_timer_list.h
 * Linked list implementation of software timer library
 */
#ifndef __SOFT_TIMER_LIST_H
#define __SOFT_TIMER_LIST_H

/**
 * Hardware interrupt tick
 */
#define TIMER_HARD_TICK                      100U    //ms, hardware tick depends on hardware timer interrupt time
#define TIMER_200MS_TICK                     (200U/TIMER_HARD_TICK) //TIMER_HARD_TICK * (2) = 100mS
#define TIMER_SEC_TICK                       (1000U/TIMER_HARD_TICK) //TIMER_HARD_TICK * (20) = 1S

/**
 * Timer mode selection
 */
typedefenum
{
    ONCE_MODE,                            /* Single timing mode, automatically closes the timer after timeout */
    CONTINUE_MODE,                        /* Continuous timing mode, runs indefinitely unless manually closed */
    DEFINE_NUM_MODE,                      /* Defined number mode, closes the timer after running a specified number of times */
}TimerTimingModeType;

/**
 * The callback function that runs after timing timeout can choose to run directly in the interrupt or suspend task polling execution
 * It is only recommended to choose interrupt mode execution when timing requirements are accurate, similar to non-magnetic sensor pulse measurement
 * Applications that involve timeout judgment should be executed in a polling manner
 * The more interrupt execution modes, the less accurate other timers become, as interrupts take time, and querying other timers will have delays
 */
typedefenum
{
    RUN_IN_LOOP_MODE,                      /* Polling execution mode */
    RUN_IN_INTERRUPT_MODE,                 /* Real-time execution mode in interrupt */
}TimerRunModeType;

/**
 * Basic type of software timer
 */
typedefstructSoftTimer
{
    unsignedlong counter;                /* Count              */
    unsignedlong duration;               /* Timing duration          */
    unsignedlong run_num;                /* Custom timing count  */
    BOOL start_flag;                      /* Start flag          */
    BOOL loop_flag;                       /* Polling flag          */
    TimerRunModeType run_mode;
    TimerTimingModeType timing_mode;
    void (*callback_function)(void);      /* Callback function          */
    structSoftTimer *next;
}SoftTimer;

/*
 * Initialize the hardware tick of the software timer
 */
externvoidsoft_timer_tick_init(void);

/*
 * Create a software timer that runs only once and starts timing immediately
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
externvoidcreat_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsignedlong duration, void(*timeout_handler)(void));

/*
 * Create a software timer that runs indefinitely and starts timing immediately
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
externvoidcreat_continue_soft_timer(SoftTimer *p, TimerRunModeType mode, unsignedlong duration, void(*timeout_handler)(void));

/*
 * Create a software timer that runs for a specified number of times and starts timing immediately
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        run_num: The number of times to time, for example, 1 means timing once, 5 means timing 5 times before automatically closing the timer
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
externvoidcreat_limit_num_soft_timer(SoftTimer *p, TimerRunModeType mode,unsignedlong run_num, unsignedlong duration, void(*timeout_handler)(void));

/*
 * Restart the specified single software timer
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
externvoidrestart_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsignedlong duration, void(*timeout_handler)(void));

/**
 * Delete a software timer
 */
externvoidstop_timer(SoftTimer *p);

/**
 * System main loop process, used to execute polling mode callback functions
 */
externvoidsoft_timer_main_loop(void);

/**
 * This function is the tick interrupt service function, which needs to be mounted on an external hardware timer
 * Therefore, the timing accuracy of the software timer is determined by the hardware timing time mounted on this function,
 * For example, if this function is mounted on an external timer with a timing of 50ms, then the timing duration
 * for 20 will be 20*50ms=1S
 */
externvoidsystem_tick_IrqHandler(void);

#endif /* !1__SOFT_TIMER_LIST_H */

C File:

/**
 * sfor_timer_list.c
 * Linked list implementation of software timer library
 */
#define NULL ((void *)0)
typedefenum {FALSE = 0, TRUE = !FALSE} BOOL;

#include "meter_include.h" // Include user hardware timer initialization function
#include "soft_timer_list.h"

/**
 * Internal variables of the software timer
 */
static SoftTimer *head_point = NULL;

staticstruct SoftTimer *creat_node(SoftTimer *node);
staticchardelete_node(SoftTimer *node);
static BOOL is_node_already_creat(SoftTimer *node);

/**
 * Initialize the hardware tick of the software timer
 */
voidsoft_timer_tick_init(void)
{
    R_IT_Create(); /* User initializes a hardware timer, current tick 100ms */
    R_IT_Start();
}

/**
 * System main loop process, used to execute polling mode callback functions
 */
voidsoft_timer_main_loop(void)
{
    structSoftTimer *p1 = head_point;

    while (p1 != NULL) // If the next node is not null
    {
        if(p1->loop_flag == TRUE)
        {
            p1->loop_flag= FALSE;
            p1->callback_function();
            if(p1->start_flag != TRUE)
                delete_node(p1);   /* If the timer is deleted, delete the node */
        }
        /*  Find the next meaningful node  */
        p1 = p1->next;
    }
}

/*
 * Create a software timer that runs only once and starts timing immediately
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
voidcreat_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsignedlong duration, void(*timeout_handler)(void))
{
    if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;

    p->start_flag = TRUE;
    p->counter = 0;
    p->loop_flag = FALSE;
    p->duration = duration;
    if(mode == RUN_IN_LOOP_MODE)
        p->run_mode = RUN_IN_LOOP_MODE;
    else
        p->run_mode = RUN_IN_INTERRUPT_MODE;
    p->callback_function = timeout_handler;
    p->timing_mode = ONCE_MODE;
    p->run_num = 0; /* Only valid in the case of custom run count */
    head_point = creat_node(p);
}

/*
 * Create a software timer that runs indefinitely and starts timing immediately
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
voidcreat_continue_soft_timer(SoftTimer *p, TimerRunModeType mode, unsignedlong duration, void(*timeout_handler)(void))
{
    if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;

    p->start_flag = TRUE;
    p->counter = 0;
    p->loop_flag = FALSE;
    p->duration = duration;
    if(mode == RUN_IN_LOOP_MODE)
        p->run_mode = RUN_IN_LOOP_MODE;
    else
        p->run_mode = RUN_IN_INTERRUPT_MODE;
    p->callback_function = timeout_handler;
    p->timing_mode = CONTINUE_MODE;
    p->run_num = 0;       /* Only valid in the case of custom run count */
    head_point = creat_node(p);
}

/*
 * Create a software timer that runs for a specified number of times and starts timing immediately
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        run_num: The number of times to time, for example, 1 means timing once, 5 means timing 5 times before automatically closing the timer
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
voidcreat_limit_num_soft_timer(SoftTimer *p, TimerRunModeType mode,unsignedlong run_num, unsignedlong duration, void(*timeout_handler)(void))
{
    if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;
    p->start_flag = TRUE;
    p->counter = 0;
    p->loop_flag = FALSE;
    p->duration = duration;
    if(mode == RUN_IN_LOOP_MODE)
        p->run_mode = RUN_IN_LOOP_MODE;
    else
        p->run_mode = RUN_IN_INTERRUPT_MODE;
    p->callback_function = timeout_handler;
    p->timing_mode = DEFINE_NUM_MODE;
    p->run_num = run_num;       /* Only valid in the case of custom run count */
    head_point = creat_node(p);
}

/*
 * Restart the specified single software timer
 * Parameter table: p: Timer structure pointer, created by the user
 *        mode: Select running mode, whether the timer executes directly in the tick interrupt or sets a flag for polling execution in the while loop
 *        duration: The duration to be timed, in hardware interrupt ticks
 *        timeout_handler: Function pointer to be executed after timing
 * return: None
 */
voidrestart_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsignedlong duration, void(*timeout_handler)(void))
{
    if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;

    p->start_flag = TRUE;
    p->counter = 0;
    p->loop_flag = FALSE;
    p->duration = duration;
    if(mode == RUN_IN_LOOP_MODE)
        p->run_mode = RUN_IN_LOOP_MODE;
    else
        p->run_mode = RUN_IN_INTERRUPT_MODE;
    p->callback_function = timeout_handler;
    p->timing_mode = ONCE_MODE;
    p->run_num = 0; /* Only valid in the case of custom run count */
    if (is_node_already_creat(p) != TRUE) /* If the previous node has been deleted, recreate it */
        head_point = creat_node(p);
}

/**
 * Encapsulated for user use
 */
voidstop_timer(SoftTimer *p)
{
    if (p != NULL) {
        p->counter = 0;
        p->start_flag = FALSE;
        delete_node(p);
    }
}

staticstruct SoftTimer *creat_node(SoftTimer *node)
{  
    structSoftTimer *p1;   // p1 saves the address of the current node to check
    if(node == NULL)
        return head_point;

    if(is_node_already_creat(node)!=FALSE) {
        delete_node(node);  /* When the node already exists, choose to exit here or delete and recreate, currently recreating */
    }
    // When the head node is empty, set the incoming node as the head node, return the head node  
    if (head_point == NULL)         {    
        head_point = node; 
        node->next = NULL;
        return head_point;
    }
    p1 = head_point;  
    while(p1->next != NULL)  
    {  
        p1 = p1->next;       // Move to the next node  
    }  

    if(p1->next == NULL)  // Insert the node at the end of the linked list
    {  
        p1->next = node;
        node->next = NULL;
    }  
    else
    {
        
    }
    return head_point;  
}

staticchardelete_node(SoftTimer *node)
{  
    structSoftTimer *p1;   // p1 saves the address of the current node to check  
    structSoftTimer *temp;
    if(node == NULL)
       return1;

    p1 = head_point;
    if(node == head_point) {
        head_point = head_point->next;    /* If the head pointer is to be deleted, move the head pointer back  */
    } else {
        while (p1 != NULL) /* If the head node is not null */
        {
            temp = p1;     /* Record the current node */
            p1 = p1->next; /* Check the next node  */
            if (p1 == NULL) {
                return1;
            }
            if (p1 == node) {
                temp->next = p1->next; /* Delete this node */
                return0;
            }
        }
    }
    return0;
}

static BOOL is_node_already_creat(SoftTimer *node)
{  
    structSoftTimer *p1;   // p1 saves the address of the current node to check  
    if(node == NULL)
       return FALSE;

    p1 = head_point;  
    while(p1 != NULL)  
    {  
        if(p1 == node)
            return TRUE;
        p1 = p1->next;       // Move to the next node  
    }
    return FALSE;
}

/**
 * This function is the tick interrupt service function, which needs to be mounted on an external hardware timer
 * Therefore, the timing accuracy of the software timer is determined by the hardware timing time mounted on this function,
 * For example, if this function is mounted on an external timer with a timing of 50ms, then the timing duration
 * for 20 will be 20*50ms=1S
 */
voidsystem_tick_IrqHandler(void)
{
    structSoftTimer *p1 = head_point;
    close_global_ir();  // Close interrupts, modify according to hardware platform
    while (p1 != NULL) // If the next node is not null
    {
        if(p1->start_flag != FALSE)    /* Check if the current timer is started  */
        {
            if(++p1->counter >= p1->duration)  /* Check if the current timing has reached */
            {
                switch (p1->timing_mode)
                {
                case ONCE_MODE:
                    p1->start_flag = FALSE;
                    break;
                case CONTINUE_MODE:
                    break;
                case DEFINE_NUM_MODE:
                    if (p1->run_num > 0)
                    {
                        if (--p1->run_num == 0)
                        {
                            p1->start_flag = FALSE;
                        }
                    }
                default:
                    break;
                }
                if(p1->run_mode == RUN_IN_INTERRUPT_MODE)
                {
                    p1->callback_function();   /* Directly run the callback function in the interrupt for programs with high real-time requirements */
                    if(p1->start_flag != TRUE)
                        delete_node(p1);
                }
                else
                    p1->loop_flag = TRUE;
                p1->counter = 0;
            }
        }
        /*  Find the next meaningful node  */
        p1 = p1->next;
    }
    open_global_ir();  // Open interrupts, modify according to hardware platform
}

Struct Array Implementation

Finally, here is the software timer implemented using struct arrays for reference.

Header File:

/**
 * sfor_timer_array.h
 * Array implementation of software timer library
 * A software timer solves all timing requirements in the entire project, and the callback function can automatically switch between interrupt real-time operation or non-real-time polling operation based on the application,
 * effectively solving the problems of insufficient hardware resources or inaccurate timing of software timers. The timing error is just a few C language statements; if
 * configured with a maximum of 10 software timers, the error is at most the time of 10 for loops.
 */
#ifndef __SOFT_TIMER_ARRAY_H
#define __SOFT_TIMER_ARRAY_H

/**
 * Define the maximum number of available software timers
 * Theoretically, it can be infinite, but the larger the number, the greater the timing error, so use as many as needed
 * The error lies in polling all timers; the more timers there are, the slower it takes to poll its own timer,
 * Additionally, an increase in quantity will also lead to an increase in space
 */
#define MAX_TIMER_NUM                       10

/**
 * Timer mode selection
 */
typedefenum
{
    ONCE_MODE,                            /* Single timing mode, automatically closes the timer after timeout */
    CONTINUE_MODE,                        /* Continuous timing mode, runs indefinitely unless manually closed */
    DEFINE_NUM_MODE,                      /* Defined number mode, closes the timer after running a specified number of times */
}TimerTimingModeType;

/**
 * The callback function that runs after timing timeout can choose to run directly in the interrupt or suspend task polling execution
 * It is only recommended to choose interrupt mode execution when timing requirements are accurate, similar to non-magnetic sensor pulse measurement
 * Applications that involve timeout judgment should be executed in a polling manner
 * The more interrupt execution modes, the less accurate other timers become, as interrupts take time, and querying other timers will have delays
 */
typedefenum
{
    RUN_IN_LOOP_MODE,                      /* Polling execution mode */
    RUN_IN_INTERRUPT_MODE,                 /* Real-time execution mode in interrupt */
}TimerRunModeType;

/**
 * Basic type of software timer
 */
typedefstruct
{
    unsignedlong counter;                /* Count           */
    unsignedlong duration;               /* Timing duration       */
    unsignedlong run_num;                /* Custom timing count  */
    unsignedchar start_flag;             /* Start flag       */
    unsignedchar loop_flag;              /* Polling flag       */
    TimerRunModeType run_mode;
    TimerTimingModeType timing_mode;
    void (*callback_function)(void);      /* Callback function */
}SoftTimer;

/**
 * Delete a software timer
 */
externvoidstop_timer(void (*callback_function)(void));

/*
 * Create a software timer and start timing immediately
 * return: Returns 1 if there are no free timers, returns 0 if created successfully
 */
externcharsoft_timer_start(TimerTimingModeType timing_mode, 
                                   TimerRunModeType run_mode,
                                       unsignedlong run_num, 
                                      unsignedlong duration,
                              void (*callback_function)(void));

/**
 * System main loop process, used to execute polling mode callback functions
 */
externvoidsoft_timer_main_loop(void);

/**
 * This function is the tick interrupt service function, which needs to be mounted on an external hardware timer
 * Therefore, the timing accuracy of the software timer is determined by the hardware timing time mounted on this function,
 * For example, if this function is mounted on an external timer with a timing of 50ms, then the timing duration
 * for 20 will be 20*50ms=1S
 */
externvoidsystem_tick_IrqHandler(void);

#endif /* !1__SOFT_TIMER_LIB_H */

C File:

/**
 * sfor_timer_array.c
 * Array implementation of software timer library
 * A software timer solves all timing requirements in the entire project, and the callback function can automatically switch between interrupt real-time operation or non-real-time polling operation based on the application,
 * effectively solving the problems of insufficient hardware resources or inaccurate timing of software timers.
 */
#include "soft_timer_array.h"

/**
 * Internal variables of the software timer
 */
static SoftTimer soft_timer[MAX_TIMER_NUM];

/*
 * Create a software timer and start timing immediately
 * return: Returns 1 if there are no free timers, returns 0 if created successfully
 */
charsoft_timer_start(TimerTimingModeType timing_mode, TimerRunModeType run_mode, unsignedlong run_num, unsignedlong duration,
                      void (*callback_function)(void))
{
    unsignedchar i = 0;

    if(!callback_function) return1;
    stop_timer(callback_function);     /* First check if there is a similar timer, if so, delete it first */
    for(i =0; i<MAX_TIMER_NUM;i++)
    {
        if(soft_timer[i].start_flag == 0)    /* Query free timers */
        {
            soft_timer[i].start_flag = 1;
            soft_timer[i].counter = 0;
            soft_timer[i].loop_flag = 0;
            soft_timer[i].duration = duration;
            soft_timer[i].run_mode = run_mode;
            soft_timer[i].callback_function = callback_function;
            if ((soft_timer[i].timing_mode = timing_mode) == DEFINE_NUM_MODE)
                soft_timer[i].run_num = run_num;                     /* Only valid in the case of custom run count */
            break;
        }
        if (i == MAX_TIMER_NUM )    /* No free timers */
            return1;
    }
    return0;
}

/**
 * Delete a software timer
 */
voidstop_timer(void (*callback_function)(void))
{
    unsignedchar i;
    for(i = 0; i <MAX_TIMER_NUM; i++)
    {
        if(soft_timer[i].callback_function == callback_function)
        {
            soft_timer[i].start_flag = 0;
            soft_timer[i].loop_flag = 0;
        }
    }
}

/**
 * System main loop process, used to execute polling mode callback functions
 */
voidsoft_timer_main_loop(void)
{
    unsignedchar i;
    for(i = 0; i <MAX_TIMER_NUM; i++)            /*  Polling execution of callback functions  */
    {
        if(soft_timer[i].loop_flag == 1)
        {
            soft_timer[i].loop_flag= 0;
            soft_timer[i].callback_function();
            // return;                              /*  Exit after running a task to wait for the next round to run the next program, to prevent task accumulation */
        }
    }
}

/**
 * This function is the tick interrupt service function, which needs to be mounted on an external hardware timer
 * Therefore, the timing accuracy of the software timer is determined by the hardware timing time mounted on this function,
 * For example, if this function is mounted on an external timer with a timing of 50ms, then the timing duration
 * for 20 will be 20*50ms=1S
 */
voidsystem_tick_IrqHandler(void)
{
    unsignedchar i = 0;
    
    for(i =0 ;i <MAX_TIMER_NUM; i++)
    {
        if(soft_timer[i].start_flag !=0 )   /* Check if the current timer is started  */
        {
            if(++soft_timer[i].counter >= soft_timer[i].duration)  /* Check if the current timing has reached */
            {
                switch (soft_timer[i].timing_mode)
                {
                case ONCE_MODE:
                    soft_timer[i].start_flag = 0;
                break;
                case CONTINUE_MODE:
                    break;
                case DEFINE_NUM_MODE:
                    if (soft_timer[i].run_num > 0)
                    {
                        if (--soft_timer[i].run_num == 0)
                        {
                            soft_timer[i].start_flag = 0;
                        }
                    }
                default:
                    break;
                }
                if(soft_timer[i].run_mode == RUN_IN_INTERRUPT_MODE)
                    soft_timer[i].callback_function();   /* Directly run the callback function in the interrupt for programs with high real-time requirements */
                else
                    soft_timer[i].loop_flag = 1;
                soft_timer[i].counter = 0;
            }
            // else
            // {
            //     soft_timer[i].counter++;
            // }
            
        }
    }
}

Disclaimer:This article’s material comes from the internet, and the copyright belongs to the original author. If there are any copyright issues, please contact me for deletion.

———— END ————

Common Implementation Methods of Software Timers in MCUs

● Column “Embedded Tools”● Column “Embedded Development”

● Column “Keil Tutorials”

● Selected Tutorials from the Embedded Column

Follow the public account Reply “Add Group” to join the technical exchange group according to the rules, reply “1024” to see more content.

Click “Read the original text” to see more shares.

Leave a Comment