单片机开发常用的软件架构

本文介绍了单片机编程中从前后台顺序执行的基本架构,到时间片轮训法的任务调度策略,以及使用标志位和函数指针的实现方式,最后提及了嵌入式操作系统的复杂性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于单片机单片机程序来说,大家都不陌生,但是真正使用架构开发,考虑架构的恐怕不多,平时写代码都是想到什么写什么,导致程序代码非常复杂并且不易读懂,下面介绍几种常用的架构开发方案。

一、前后台顺序执行法。

这使初学者常用代码的程序设计框架,不用考虑太多东西,代码简单,或者对系统的整体实时性和并发性要求不高:初始化后通过while(1){}或者for()等循环不断的调用自己编写完成的函数,也基本不考虑每个函数执行所需要的时间,打部分时间在函数中存在毫秒级别延迟。
优点:对初学者来说,这是最容易也是最直观的程序架构,逻辑简单明了,适用于单模块调试或者复杂度较低的软件开发。
缺点:实时性低,由于每个函数或多或少存在毫秒延迟,导致其他函数间隔执行时间不同,虽然可以通过定时器中断方式,但是中断的执行函数必须短。当程序逻辑复杂度提升时,会导致后来维护的人员无法理解,难以分析。

二、时间片轮训法

介于前后台顺序执行和操作系统(RTOS)之间的一种程序设计方案。改设计方案帮助换在学习裸机和操作系统之间的开发者,更好的提高开发效率,主要特点有以下几个:
1:目前的需求设计完全没有必要上操作系统
2:任务的函数无需时刻都执行,存在间隔(比如按键软件防抖,初学者会用延迟去检测按键状态,这部分时间对于CPU来说使比较浪费的,这部分时间完全可以去执行其他任务)
3:对实时性有一定的要求。可以根据任务的重要程度,来规定任务的执行周期。

改设计方案需要使用一个定时器,一般情况下定时1ms就可以了,定时太长影响实时性(频繁的进入中断,效率不高),因此需要考虑每个任务的执行时间。同时要求主函数和任务函数中不能存在毫秒级别的延迟。

下面介绍两种比较常见的任务调度方案

一、使用标志位进行任务切换的设计方案
/*
 * \brief 主函数
 *
 * \param[in] 无
 *
 * \retval  无
 *
 */
int main()
{
 	System_Init();
 	
 	whlie(1)
 	{
 	   if(FLAG0)
      {
        App_Task();
        FLAG0=0;
      }
      if(FLAG1)
      {
        App_Task1();
        FLAG1=0;
      }
      if(FLAG2)
      {
        App_Task2();
        FLAG2=0;
      }
      if(FLAG3)
      {
        App_Task3();
        FLAG3=0;
      }
      if(FLAG4)
      {
        App_Task4();
        FLAG4=0;
      }
 	}

}

/*
 * \brief 定时器中断
 *
 * \param[in] 无
 *
 * \retval  无
 *
 */
void TIM2_IRQHandler(void)
{
	if(TIM_GetITStasus(TIM2,TIM_IT_UPdate) == SET) 
	{
	 timCount++;
	//这里用条件编译,方便代码书写。
	timCount % 1 ==0 ? FLAG0 = 1 : 0;
	timCount % 10 ==0 ? FLAG1 = 1 : 0;
	timCount % 20 ==0 ? FLAG2 = 1 : 0;
	timCount % 100 ==0 ? FLAG3 = 1 : 0;
	timCount % 500 ==0 ? FLAG4 = 1 : 0;
	}
	TIM_ClearITPendingBit(TIM2,TIM_IT_UPdate);
}

如果需要更加精确的检测,放置程序是否跑飞,可以使用看门狗来进行定时喂狗。

二、使用函数指针的设计方案
/*
 * \brief 定义任务相关结构体
 */
typedef struct
{
	uint8_t run;                // 调度标志,1:调度,0:挂起
	uint16_t timCount;          // 时间片计数值
	uint16_t timRload;          // 时间片重载值
	void (*pTaskFuncCb)(void);  // 函数指针变量,用来保存业务功能模块函数地址
} TaskComps_t;
/*
 * \brief 定义任务数量
 */
#define TASK_NUM_MAX   (sizeof(g_taskComps) / sizeof(g_taskComps[0]))

static TaskComps_t g_taskComps[] = 
{
	{0, 1, 1,  AppTask},				
	{0, 10, 10,  AppTask1},				
	{0, 20, 20, AppTask2},			
	{0, 100,100, AppTask3},				
	{0, 500, 500,  AppTask4},					
	/* 添加业务功能模块 */
};
/**
* @brief 任务调度函数
* @param
* @return 
*/
static void TaskHandler(void)
{
	for (uint8_t i = 0; i < TASK_NUM_MAX; i++)
	{
		if (g_taskComps[i].run)                  // 判断时间片标志
		{
			g_taskComps[i].run = 0;              // 标志清零
			g_taskComps[i].pTaskFuncCb();        // 执行调度业务功能模块
		}
	}
}

/**
* @brief 在定时器中断服务函数中被间接调用,设置时间片标记,需要定时器1ms产生1次中断
* @param
* @return 
*/
static void TaskScheduleCb(void)
{
	for (uint8_t i = 0; i < TASK_NUM_MAX; i++)
	{
		if (g_taskComps[i].timCount)
		{
			g_taskComps[i].timCount--;
			if (g_taskComps[i].timCount == 0)
			{
				g_taskComps[i].run = 1;
				g_taskComps[i].timCount = g_taskComps[i].timRload;
			}
		}
	}
}

void TIM2_IRQHandler(void)
{
	if(TIM_GetITStasus(TIM2,TIM_IT_UPdate) == SET) 
	{
		g_pTaskScheduleFunc();
	}
	TIM_ClearITPendingBit(TIM2,TIM_IT_UPdate);
}

static void (*g_pTaskScheduleFunc)(void);  // 函数指针变量,保存任务调度的函数地址

/**
* @brief 注册任务调度回调函数
* @param pFunc, 传入回调函数地址
* @return 
*/
void TaskScheduleCbReg(void (*pFunc)(void))
{
	g_pTaskScheduleFunc = pFunc;
}

int main ()
{
	System_Init();
	
	TaskScheduleCbReg(TaskScheduleCb);
	
	while(1)
	{
		TaskHandler();
	}

}

这里需要注意,在调用的时候需要使用函数指针来实现函数的回调,对函数指针不熟悉的人需要去了解下。

三、操作系统。

嵌入式操作系统,使一种用途广泛的系统软件,对单片机来说,常见的有UCOS、FreeRTOS、RT-Thread、等多种操作系统。操作系统中的时间片轮训法和上面介绍的任务调度有类似的地方,但这类操作系统中往往集成ICP等,复杂功能,初学者接触比较困难。在此不再介绍。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值