许多初学编程的新手,若能找到一个无需过多复杂思考、代码简洁的程序框架,会感到非常吸引,仿佛找到了一根救命稻草。这样的方案看似简单明了,实则暗藏一些问题,值得我们深入探究。
优势明显的初学者方案
该方案对新手极具吸引力,主要因为它易于掌握和操作。以初入校园参与项目的学生为例,比如在打造宿舍防盗系统时,只需运用这种基础的主函数代码框架,就能实现基本功能。它并不要求掌握复杂的编程技能,对于逻辑相对简单的项目,这种框架足以应对。从逻辑角度分析,通过反复调用预先编写的函数,这种线性思维模式非常适合初学者的认知水平。在小型且复杂度不高的软件开发中,这种框架能迅速构建起项目的框架结构。
尽管这种设计看起来简单明了,但在处理较为复杂的项目时,就显得有些力不从心。特别是在一些对实时性有较高要求的功能模块中,这个方案显得有些吃力。由于函数中存在一定的延时,即便是极小的毫秒级延迟,也可能导致整个系统的时间精度无法得到保障。
设计方案中的优缺点平衡
从优点来看,其逻辑清晰如同一条直通到底的道路,易于理解。它就如同小作坊制作简单产品,无需繁琐工序,对简单的软件开发任务非常适用。新手面对这样的方案,不会感到特别畏惧。然而,谈及缺点,便成了硬伤。在如嵌入式软件开发等对实时性和并发性要求较高的场合,这种单纯顺序执行的框架显得很不协调。就像骑自行车与汽车竞速,完全无法跟上节奏。当多个函数需协同工作时,每个函数的延迟累积起来,将严重拖累整个系统的运行效率。
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] = {
{0, 1, 1, CAN_CommTask}, // CAN通信任务
{0, 10, 10, KEY_ScanTask}, // 按键扫描任务
{0, 10, 10, LOGIC_HandleTask}, // 逻辑处理任务
{0, 100, 100, LED_CtrlTask},// 指示灯控制任务
{0, 1000, 1000, WDog_Task}, // 喂狗任务
};
这种我们讨论的程序架构,与那些相比,显得较为原始。它缺乏操作系统中的任务优先级设定,也没有像时间片轮询那样,巧妙地结合了两者的优点。它只是简单地按顺序执行函数。在处理复杂任务时,这种简单直接的方法显得相当有限。
在学习过程中,你觉得哪个编程框架对你的进步最有益?欢迎点赞、转发,并在评论区展开交流。