前言:
该内容是工作一年来通过上网或其他方式不断搜索、实践、总结出来的嵌入式软件开发经验(本文仅适用于单片机的裸机开发),希望能帮到正在学习这方面的朋友,如有不好的地方,请多多见谅。
在嵌入式软件开发过程中,程序架构的搭建尤为重要,下面介绍三种常用的程序框架设计方案:
前后台顺序执行法、时间片论法、操作系统
1、前后台顺序执行法:
这是初学者们常用的程序框架设计方案,不用考虑太多东西,代码简单,或者对系统的整体实时性和并发性要求不高;初始化后通过while(1){ }或for(;;){ }循环不断调用自己编写完成的函数,也基本不考虑每个函数执行所需要的时间,大部分情况下函数中或多或少都存在毫秒级别的延时等待。
优点:对于初学者来说,这是最容易也是最直观的程序架构,逻辑简单明了,适用于逻辑简单,复杂度比较低的软件开发。
缺点:实时性低,由于每个函数或多或少存在毫秒级别的延时,即使是1ms,也会造成其他函数间隔执行时间的不同,虽然可通过定时器中断的方式,但是前提是中断执行函数花的时间必须短。当程序逻辑复杂度提升时,会导致后来维护人员的大脑混乱,很难理清楚该程序的运行状态。
以下是我在学校期间做的寝室防盗系统的部分代码(当时也存在部分BUG,没有解决。现在再看,其实很多问题,而且比较严重,比如中断服务函数内竟然有3000ms延时,这太可怕了,还有串口发送等等;由于实时性要求不算太高,因此主函数中的毫秒级别延时对系统运行没有多大影响,当然除BUG外;若是后期需要维护,那就是一个大工程,还不如推翻重写):
int main(void)
{
u8 temperature;
u8 humidity;
int a;
delay_init();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,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;
}
}
}
}
2、时间片论法:
介于“前后台顺序执行法”和“操作系统”之间的一种程序架构设计方案。该设计方案需能帮助嵌入式软件开发者更上一层楼,在嵌入式软件开发过程中,若遇到以下几点,那么该设计方案可以说是最优选择,适用于程序较复杂的嵌入式系统;
a. 没有必要上操作系统
b. 任务函数无需时刻执行,存在间隔时间(比如按键,一般情况下,都需要软件防抖,初学者的做法通常是延时10ms左右再去判断,但10ms极大浪费了CPU的资源,在这段时间内CPU完全可以处理很多其他事情)
c. 实时性有一定的要求
该设计方案需要使用一个定时器,一般情况下定时1ms即可(定时时间可随意定,但中断过于频繁效率就低,中断太长,实时性差),因此需要考虑到每个任务函数的执行时间,建议不能超过1ms(能通过程序优化缩短执行时间则最好优化),这就要求主循环或任务函数中不能存在毫秒级别的延时,。
以下介绍两种不同的实现方案,分别针对没有函数指针概念的朋友和想进一步学习的朋友。
/*****************************************************************************
第一种设计方案
*****************************************************************************/
/**
* @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); // 清除中断标志位
}
/*****************************************************************************
第二种设计方案
*****************************************************************************/
/**
* @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] = {
{0, 1, 1, CAN_CommTask}, // CAN通信任务
{0, 10, 10, KEY_ScanTask}, // 按键扫描任务
{0, 20, 20, LOGIC_HandleTask}, // 逻辑处理任务
{0, 100, 100, LED_CtrlTask}, // 指示灯控制任务
{0, 1000, 1000, 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); // 清除中断标志位
}
3、操作系统:
嵌入式操作系统EOS(Embedded OperatingSystem)是一种用途广泛的系统软件,过去它主要应用于工业控制和国防系统领域。常见的嵌入式操作系统有wince、PALM OS、linux、Android、FreeRTOS。
参考内容:
浅谈单片机应用程序架构 : http://bbs.eeworld.com.cn/thread-311494-1-1.html