嵌入式软件开发中常用的三种架构详解

2024-12-02 0 612

大家普遍了解单片机程序,但真正把架构当作重要课题来深入研究的人并不多。随着程序量不断增加,架构的重要性愈发凸显。这其中蕴含着许多对程序优化和避免问题的关键信息。

简单前后台顺序执行程序

初学者学习单片机编程时,通常首选这种方法。这种方法简单易行,无需过多考虑程序架构,只需按照执行顺序编写程序即可。然而,在实际应用中,它的不足之处逐渐显现。例如,在编写校园宿舍防盗系统的代码时,采用这种方法会出现诸多问题,给后续的代码维护带来困扰。此外,这种方法并未充分考虑函数的执行时间,存在许多毫秒级的延迟,这对需要实时响应的程序来说非常不利。

在仅涉及前后台顺序执行程序编写测试的情境中,测试重点通常局限于小功能。例如,读取传感器数据并进行简单处理后展示,这类基本功能可以迅速实现。然而,当项目规模扩大,比如涉及传感器之间的联动关系等复杂场景时,就难以胜任了。

时间片轮询法

这种方法位于常规执行与操作系统之间,独具特色。它在两者之间找到了一个合适的平衡点。对于那些对单片机资源需求不高,但又需要一定程序架构结构的项目来说,这种方法尤为适用。

在开发智能家居温度控制模块时,我们面临任务众多且需实时处理的情况。此时,时间片轮询法能有效在资源有限的前提下,合理调整任务执行顺序。此外,这种方法在减少函数内延时方面表现突出,它更侧重于对任务执行时间的合理规划。

操作系统方法

这种编程方式属于较为高级的水平。在工业和国防系统中,操作系统应用已久。然而,针对单片机,常用的抢占式操作系统有UCOS、RT-Nano和RTX等。

这种需要极高实时性和稳定性的复杂无人机控制系统项目,最好采用操作系统方法。这样做的好处是可以轻松设定任务优先级。然而,对于初学者或是进行简单调试的情况,可能会觉得这种方法有些像用重型武器打小鸟,因为对单片机资源的占用相对较多。

int main(void)

  u8 temperature;       
  u8 humidity;   
 int a;
 delay_init();
 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  I2c_init(); 
 uart2_Init(9600);
 uart_init(9600);//串口初始化为115200  
 TIM3_Int_Init(4999,7199);
 ds1302_init();
 while(DHT11_Init())//DHT11初始化 
 {
  led2=0;
 }
 a1602_init();  
  Ds1302Init();
 EXTIX_Init();   
 GPIOX_Init();
 lcd12864_INIT();
 LcdInit();
 beep_init();
 RED_Init();
 led1=1;
 beep=0;
 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();   //蓝牙处理数据
  usart2_gai();
  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;      
  }
  if(key1==0||FEN-fen!=0||SHI-shi!=0)
  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
   }   
  }
 }
}

单片机程序架构实际问题

以我们学校寝室的防盗系统为例。其中存在不少明显的问题。比如,中断服务函数中的延时现象很成问题,再如,串口发送等模块的配置也存在不合理之处。当时因为对实时性的要求不是特别严格,所以主函数里的毫秒级延时并未引发严重混乱。然而,这些都是我们必须关注的问题。

对于像车辆控制系统这样对实时性要求极高的项目,这些问题会被显著加剧。因此,我们必须认识到,在各个不同情境中,架构的每个部分都有可能带来负面效应。

时间片轮询法的方案设计与应用

这个方案对于较为复杂的嵌入式系统来说,是个不错的选择。不过,它对定时器的需求较高,且对定时时间的设定要求严格。以1毫秒为例,需要平衡中断的频率和持续时间,以确保效率和实时性的协调。此外,还要确保任务函数的执行时间不宜过长。

/**
  * @brief      主函数.
  * @param      None.
  * @return     None.
  */


int main(void)
{
    System_Init();
    
    while (1)
    {
        if (TIM_1msFlag)// 1ms
        {
            CAN_CommTask(); // CAN发送/接收通信任务
            
            TIM_1msFlag = 0;
        }        
        if (TIM_10msFlag)  // 10ms
        {
            KEY_ScanTask(); // 按键扫描处理任务
            
            TIM_10msFlag = 0;
        }        
        if (TIM_20msFlag) // 20ms
        {
            LOGIC_HandleTask();// 逻辑处理任务
 
            TIM_20msFlag = 0;
        }        
        if (TIM_100msFlag) // 100ms
        { 
            LED_CtrlTask(); // 指示灯控制任务
            
            TIM_100msFlag = 0;
        }       
        if (TIM_500msFlag)// 500ms
        {
            TIM_500msFlag = 0;
        }        
        if (TIM_1secFlag) // 1s
        {
            WDog_Task(); // 喂狗任务            
            TIM_1secFlag = 0;
        }  
    }
}
 
/**
  * @brief      定时器3中断服务函数.
  * @param      None.
  * @return     None.
  */

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);  // 清除中断标志位
}

在一家工厂的自动化生产线上,若采用简单的PLC进行控制,涉及电机操控和传感器数据收集等任务。此时,通过时间片轮询技术,可以有效调度各项作业,确保生产线的基本自动化运作。此外,根据开发者是否掌握函数指针这一概念,可以采取不同的实现策略。

/**
  * @brief 任务函数相关信息结构体定义.
  */

typedef struct{
    uint8 m_runFlag;                 /*!< 程序运行标记:0-不运行,1运行 */
    uint16 m_timer;                  /*!< 计时器 */
    uint16 m_itvTime;                /*!< 任务运行间隔时间 */
    void (*m_pTaskHook)(void);       /*!< 要运行的任务函数 */
} TASK_InfoType; 
 
#define TASKS_MAX     5              // 定义任务数目
 
/** 任务函数相关信息 */
static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {
    {011, CAN_CommTask},         // CAN通信任务
    {01010, KEY_ScanTask},       // 按键扫描任务
    {02020, LOGIC_HandleTask},   // 逻辑处理任务
    {0100100, LED_CtrlTask},     // 指示灯控制任务
    {010001000, WDog_Task},      // 喂狗任务
};
 
 
/**
  * @brief      任务函数运行标志处理.
  * @note       该函数由1ms定时器中断调用
  * @param      None.
  * @return     None.
  */

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      任务函数运行处理.
  * @note       该函数由主循环调用
  * @param      None.
  * @return     None.
  */

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
        }
    }   
}
 
/**
  * @brief      主函数.
  * @param      None.
  * @return     None.
  */

int main(void)
{
    System_Init();    
    while (1)
    {
        TASK_Process();
    }
}
 
/**
  * @brief      定时器3中断服务函数.
  * @param      None.
  * @return     None.
  */

void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) //溢出中断
    {
        TASK_Remarks();
    }    
    TIM_ClearITPendingBit(TIM3,TIM_IT_Update);// 清除中断标志位
}

三种方法对比

顺序执行法虽简单,但在复杂情况下易出现故障;操作系统法看似高级,却需更多资源且入门不易;而时间片轮询法则兼顾了两者的长处。就好比设计一个水质监控系统,既要顺序采集传感器数据,又要与云端交互,这时时间片轮询法就显示出其灵活的适应性。若是你设计单片机程序,会优先考虑哪种方法?最后,若觉得这篇文章对您有所帮助,不妨点赞并分享到您的技术圈,让我们共同探讨交流。

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

七爪网 行业资讯 嵌入式软件开发中常用的三种架构详解 https://www.7claw.com/2799063.html

七爪网源码交易平台

相关文章

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

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