嵌入式软件架构设计:任务调度的关键策略与实践指南

2025-01-04 0 523

许多初学编程的新手,若能找到一个无需过多复杂思考、代码简洁的程序框架,会感到非常吸引,仿佛找到了一根救命稻草。这样的方案看似简单明了,实则暗藏一些问题,值得我们深入探究。

优势明显的初学者方案

该方案对新手极具吸引力,主要因为它易于掌握和操作。以初入校园参与项目的学生为例,比如在打造宿舍防盗系统时,只需运用这种基础的主函数代码框架,就能实现基本功能。它并不要求掌握复杂的编程技能,对于逻辑相对简单的项目,这种框架足以应对。从逻辑角度分析,通过反复调用预先编写的函数,这种线性思维模式非常适合初学者的认知水平。在小型且复杂度不高的软件开发中,这种框架能迅速构建起项目的框架结构。

尽管这种设计看起来简单明了,但在处理较为复杂的项目时,就显得有些力不从心。特别是在一些对实时性有较高要求的功能模块中,这个方案显得有些吃力。由于函数中存在一定的延时,即便是极小的毫秒级延迟,也可能导致整个系统的时间精度无法得到保障。

设计方案中的优缺点平衡

从优点来看,其逻辑清晰如同一条直通到底的道路,易于理解。它就如同小作坊制作简单产品,无需繁琐工序,对简单的软件开发任务非常适用。新手面对这样的方案,不会感到特别畏惧。然而,谈及缺点,便成了硬伤。在如嵌入式软件开发等对实时性和并发性要求较高的场合,这种单纯顺序执行的框架显得很不协调。就像骑自行车与汽车竞速,完全无法跟上节奏。当多个函数需协同工作时,每个函数的延迟累积起来,将严重拖累整个系统的运行效率。

 int main(void)
 
{    
    u8 temperature;          
    u8 humidity;   
    int a;
    delay_init();
    uart2_Init(9600);     
    TIM3_Int_Init(4999,7199);
    ds1302_init();
    while(DHT11_Init());    //DHT11初始化    
    a1602_init();        
    lcd12864_INIT();
    LcdInit();

    while(1)
    {
        for(a=0;a<11;a++)
        {
            num[a+3]=At24c02Read(a+2)-208;
            delay_us(10);                    
        }
        for(a=0;a<6;a++)
        {
            shuru[a]=At24c02Read(a+13)-208;
            delay_us(10);                
        }        
        delay_ms(10);  
        RED_Scan();
        Ds1302ReadTime();                    //读取ds1302的日期时间
        shi=At24c02Read(0);                //读取闹钟保存的数据
        delay_ms(10);
        fen=At24c02Read(1);                //读取闹钟保存的数据            
        usart2_scan();                        //蓝牙数据扫描
        usart2_bian();                        //蓝牙处理数据
        nao_scan();
        k++;
        if(k<20)
        {
            if(k==1)
                LcdWriteCom(0x01);  //清屏
            LcdDisplay();                            //显示日期时间
        }
        if(RED==0)
            RED_Scan();

        if(k>=20&&k<30)
        {
            if(k==20)
                LcdWriteCom(0x01);  //清屏
            Lcddisplay();                            //显示温湿度
            LcdWriteCom(0x80+6);    
            DHT11_Read_Data(&temperature,&humidity);    //读取温湿度值    
            Temp=temperature;Humi=humidity;
            LcdWriteData('0'+temperature/10);
            LcdWriteData('0'+temperature%10);
            LcdWriteCom(0x80+0X40+6);    
            LcdWriteData('0'+humidity/10);
            LcdWriteData('0'+humidity%10);
        }
        if(k==30)
            k=0;
        lcd12864();                                //显示防盗闹钟状态

    }        
}


//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{
    int i;
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 
        if(key1==1&&FEN-fen==0&&SHI-shi==0)                //时间一到闹钟响起
        {
            f=1;                        
        }
        else
        {
            f=0;
        }    
        if(USART_RX_BUF[0]=='R'&&USART_RX_BUF[1]=='I'&&USART_RX_BUF[2]=='N'&&USART_RX_BUF[3]=='G')
        {
            key0=1;
            for(i=0;i<17;i++)
            {
                USART_SendData(USART1, num[i]);//向串口1发送数据
                while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
                USART_RX_STA=0;    
            }    
            delay_ms(3000);
            for(i=0;i<3;i++)
            {
                USART_SendData(USART1, num1[i]);//向串口1发送数据
                while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
                USART_RX_STA=0;    
            }            
        }
    }
}

它并非全然没有改进的空间。比如,从时间标识的判断方法来看,这一方案对初学者来说,能提供一条不错的入门路径,帮助他们初步掌握函数调用和顺序控制等基本概念。

曾经的项目问题反思

回想起在校园里制作的宿舍防盗系统,其核心的主函数是基于这一方案设计的。然而,当时并未察觉到其中存在诸多问题。比如,中断服务函数中的延时错误竟然未被察觉。这好比在精密仪器中放入了一粒沙子,尽管当时对实时性的要求不高,问题得以暂时掩盖,但在严谨的编程环境中,这是一个必须改正的重大缺陷。此外,还存在串口发送异常等问题。这些问题都源于当时对这种程序框架缺乏深入的理解,仅仅停留在表面上的简单易用性上。

现在回望过去,在这种框架下编写的代码,在较为复杂的项目中显得极不稳定,宛如即将倒塌的危楼。尤其是随着技术的不断发展和项目复杂度的提升,那些早期编写的代码已无法满足需求。当对代码品质和系统稳定性提出更高要求时,我们不得不重新审视当初所采用的框架。

方案在嵌入式软件开发的定位

void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
    {
        sg_1msTic++;

        sg_1msTic % 1 == 0 ? TIM_1msFlag = 1 : 0;
        sg_1msTic % 10 == 0 ? TIM_10msFlag = 1 : 0;
        sg_1msTic % 20 == 0 ? TIM_20msFlag = 1 : 0;
        sg_1msTic % 100 == 0 ? TIM_100msFlag = 1 : 0;
        sg_1msTic % 500 == 0 ? TIM_500msFlag = 1 : 0;
        sg_1msTic % 1000 == 0 ? (TIM_1secFlag  = 1, sg_1msTic = 0) : 0;
    }

    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}

嵌入式软件开发领域里,该方案位于“前后台顺序执行法”与“操作系统”之间。若当前需求设计无需操作系统的高级功能,该方案便能发挥其优势。尤其是那些对实时性要求不高的小型电子产品程序控制,该方案能够很好地满足需求。

若需提升编程的更高要求,例如提升系统运行的效率和稳定性,那么在任务函数不必持续运行且存在停顿时间的情况下,应当有更优的策略来降低资源浪费。比如在按键防抖过程中,对CPU资源的过度占用,这在追求资源高效使用时是不可行的。

int main(void)
{
    System_Init();

    while (1)
    {

        if (TIM_1msFlag)
        {
            CAN_CommTask();// CAN通信任务

            TIM_1msFlag = 0;
        }

        if (TIM_10msFlag)
        {
            KEY_ScanTask();// 按键扫描任务
            Hmi_Task();// 人机交互任务

            TIM_10msFlag = 0;
        }

        if (TIM_100msFlag)
        { 
            LED_CtrlTask();// 指示灯任务

            TIM_100msFlag = 0;
        }    

        if (TIM_1secFlag)
        {
            WDog_Task();// 喂狗任务

            TIM_1secFlag = 0;
        }  
    }
}

两种不同受众的实现方案


typedef
 struct{
    uint8 m_runFlag;/*!< 程序运行标记:0-不运行,1运行 */
    uint16 m_timer;/*!< 计时器 */
    uint16 m_itvTime;/*!< 任务运行间隔时间 */
    void (*m_pTaskHook)(void);/*!< 要运行的任务函数 */
} TASK_InfoType; 

对于对函数指针不太了解的朋友,这里有一个基于该框架的基础版实现,能帮助他们初步了解编程控制的基本逻辑。而那些希望提高技能的朋友,则可以尝试一个经过改进的方案,这有助于他们深入探究程序执行的根本原理。这就像为不同阶段的旅行者提供了两条不同的路径,使他们都能朝着目标前进。这种根据不同用户需求设计不同版本实现的方法,是这种框架的一大亮点,使得更多不同水平的开发者有机会参与其中。

在主函数的循环部分,我们通过时间标识来决定函数的执行顺序和时间,这样做能有效管理函数的运行。这种做法是对控制执行逻辑的一种简便而实用的探索。

/**
  * @brief      任务函数运行调度管理.
  */

void TASK_Remarks(void)
{
    uint8 i;

    for (i = 0; i < TASKS_MAX; i++)
    {
        if (sg_tTaskInfo[i].m_timer)
        {
            sg_tTaskInfo[i].m_timer--;

            if (0 == sg_tTaskInfo[i].m_timer)
            {
                 sg_tTaskInfo[i].m_timer = sg_tTaskInfo[i].m_itvTime;
                 sg_tTaskInfo[i].m_runFlag = 1;
            }
        }
   }
}

/**
  * @brief      任务函数调度执行.
  */

void TASK_Process(void)
{
    uint8 i;

    for (i = 0; i < TASKS_MAX; i++)
    {
        if (sg_tTaskInfo[i].m_runFlag)
        {
             sg_tTaskInfo[i].m_pTaskHook(); // 运行任务
             sg_tTaskInfo[i].m_runFlag = 0// 标志清0
        }
    }   
}

与操作系统比较的优势与劣势

int main(void)
{
    System_Init();

    while (1)
    {
        TASK_Process();
    }
}

/**
  * @brief      定时器3中断服务函数.
  */

void TIM3_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
    {
        TASK_Remarks();
    }

    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}

操作系统与“时间片论法”相较,拥有独立且严密的作业调度与资源分配机制。它通过设定任务的优先级,完成复杂的调度任务。当高优先级任务准备就绪时,会优先执行,抢占低优先级任务,这种机制能有效应对复杂任务的执行需求。相较之下,时间片轮询法不仅具备前后台顺序执行法的简便性,还借鉴了操作系统在任务调度方面的理念,实现了两者优势的结合。


#define TASKS_MAX     5    // 定义任务数目



/** 任务函数相关信息 */
static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {
    {011, CAN_CommTask},     // CAN通信任务
    {01010, KEY_ScanTask},   // 按键扫描任务
    {01010, LOGIC_HandleTask}, // 逻辑处理任务
    {0100100, LED_CtrlTask},// 指示灯控制任务
    {010001000, WDog_Task},  // 喂狗任务
};

这种我们讨论的程序架构,与那些相比,显得较为原始。它缺乏操作系统中的任务优先级设定,也没有像时间片轮询那样,巧妙地结合了两者的优点。它只是简单地按顺序执行函数。在处理复杂任务时,这种简单直接的方法显得相当有限。

在学习过程中,你觉得哪个编程框架对你的进步最有益?欢迎点赞、转发,并在评论区展开交流。

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

七爪网 行业资讯 嵌入式软件架构设计:任务调度的关键策略与实践指南 https://www.7claw.com/2806827.html

七爪网源码交易平台

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务