单片机程序架构—时间轮片法

单片机程序架构—时间轮片法

程序架构重要性

很多人尤其是初学者在写代码的时候往往都是想一点写一点,最开始没有一个整体的规划,导致后面代码越写越乱,bug不断。最终代码跑起来看似没有问题(有可能也真的没有问题),但是要加一个功能的时候会浪费大量的时间,甚至导致整个代码的崩溃。

所以,在一个项目开始的时候多花一些时间在代码的架构设计上是十分有必要的。代码架构确定好了之后你会发现敲代码的时候会特别快,并且在后期调试的时候也不会像无头苍蝇一样胡乱找问题。当然,调试也是一门技术。

怎样写代码?

单片机代码都是从主函数开始的,很多人会在main函数中写很多代码,这样个人觉得十分不好,因为看起来太累,太丑。俗话说,颜值即正义!只要是能够写出漂亮代码的人,大概率来说他写的代码的bug也比较少。

一般来说,main函数的结构如下:

#include <xxx.h>
#include "xxx.h"
......

extern struct xxx;
extern int/unsigned int/char xxx;
    
int main()
{
    init_function_1();
    inti_function_2();
    ......
    while(1)
    {
        task_1();
        task_2();
        ......
    }
}

这样的结构看起来就十分的简洁,主函数中只作各种外设的或者函数、结构体等的初始化,当然初始化函数也在其它的文件中;然后进入while(1)死循环,处理各个任务。这些任务函数也是定义在其它文件中。

当然,如果一个函数需要间隔特定的时间执行的话怎么办?在while(1)中加延时函数?

答案是否定的,一旦在主循环中加入延时函数后整个主循环都会受到影响,会导致程序完全不能正常执行。还有就是__主循环中的任务函数都不能有延时__。那么这时应该怎么办?

  • 使用操作系统

    使用操作系统,如:ucos,freertos,rt_thread等可以解决这个问题,但是学习操作系统的话需要一定时间,有时代码又没有复杂到需要使用操作系统,有时单片机资源不足以使用操作系统。并且使用操作系统对开发 人员的要求比较高。

  • 使用定时器加全局flag

    这种方式是在定时中判断各个任务的是否到执行时间,如果到了,就将该任务的flag置为1,然后在主函数中判断flag是否为1,如果为1,则执行。这样做看起来确实不错,但是在程序设计中尽量少使用全局变量,而且这样做的话,主函数会看起来十分不美观。

  • 时间轮片法

    所谓时间轮片,其实和上面使用定时器加全局flag没有本质区别,只是将其包装起来,看起来更好看而已。当然,颜值上升了,其战斗力也变强了, 在很多情况下使用起来就十分舒爽。

时间论轮片法

  • 定义任务结构体与任务

    #define TASK_NUM xx	// 要执行的任务数量
    
    typedef enum
    {
        TASK_STOP,
        TASK_RUN
    } TaskRunFlag;
    struct Task
    {
        void (*task_handle)(void);  // 任务函数体,还可以带参数void (*task_handle)(void* para)
        TaskRunFlag task_run_flag; 	// 任务运行状态
        unsigned int task_timer; 	// 任务计时器,为0时该相应任务执行
        unsigned int task_timer_init_val; 	// 任务计时器初值
    };
    typedef struct Task TaskInitTypedef;
    
    static TaskInitTypedef tasks[TASK_NUM] =  // 定义一个结构体数组并初始化各个任务
    {
        {task_1, TASK_STOP, xx,xx}, // task_1为任务1,初始状态为停止状态,每隔xx时钟节拍运行一次
        {task_1, TASK_STOP, xx,xx},
        {task_1, TASK_STOP, xx,xx},
        ......
    	......
    };
    

    一个任务结构体就定义了一个任务,包括任务函数、任务运行状态、执行间隔。

  • 时间轮片

    时间轮片是整个时间轮片法的核心,时间轮片就是在每一个时钟节拍判断是否到了执行该相应任务的时候,如果是,则任务状态为运行,如果不是则计数器减一。

    void task_rhythm() // 该函数放于定时器中断中,进一次中断执行一次,是整个任务系统的心跳(节奏)
    {
        unsigned char i;
        
        for(i = 0; i < TASK_NUM; i++)
        {
            if(tasks[i].task_timer != 0) // 挨个判断任务是否到执行时间
            {
                tasks[i].task_timer --;
                if(tasks[i].task_timer == 0) // 如果到执行时间
                {
                    tasks[i].task_run_flag = TASK_RUN; 
                    tasks[i].task_timer = tasks[i].task_timer_ini_val;
                }
            }
        }
    }
    

    该函数位于定时器的中断中,定时器的定时时间就是整个任务的运行节拍,前面的task_timer参数就是运行节拍的计数器。如:定时器中断时间为10ms,那么该系统中任务的最短间隔就是10ms,也就是隔一个节拍运行一次;如果一个任务想要1s运行一次的话,就将task_timer参数以及task_timer_ini_val参数值设为100即可。

  • 任务处理

    上面已经知道了什么时候该运行相应的任务(就是任务对应的run_flag为TASK_RUN),那么我们就在主函数的大循环中一直检测任务相应的标志位即可。

    void task_process()
    {
        unsigned char i;
        
        for(i = 0; i < TASK_NUM; i ++)
        {
            if(tasks[i].task_run_flag == TASK_RUN)
            {
                tasks[i].task_handle; 	// 调用任务函数
                tasks[i].task_run_flag = TASK_STOP; 	// 结束任务函数
            }
        }
    }
    

    至此,时间轮片法完成。

发布了6 篇原创文章 · 获赞 1 · 访问量 84

猜你喜欢

转载自blog.csdn.net/qq_32290049/article/details/105051362