Abstract: For microcontroller programs, everyone is familiar, but very few actually consider the architecture. With the increasing number of program developments, having an architecture is very necessary.
There are roughly three types of application architectures:
1. Simple front and back sequential execution programs, this method is used by most people, without needing to think about the specific architecture of the program, applications can be written directly through the execution order.
2. Time-slice polling method, this method is a method between sequential execution and operating systems.
3. Operating system, this method should be the highest realm of application program writing.
1. Program Framework Design
1. Front and back sequential execution method
This is a common program framework design scheme for beginners, which does not require much consideration, the code is simple, or the overall real-time and concurrency requirements of the system are not high; after initialization, it continuously calls the functions it has written throughwhile(1){}
orfor(;;)
{}` loops, and basically does not consider the time needed for each function to execute. Most of the time, there are delays of milliseconds in the functions.
-
Advantages: For beginners, this is the easiest and most intuitive program architecture, with simple and clear logic, suitable for software development with simple logic and relatively low complexity.
-
Disadvantages: Low real-time performance. Since each function has delays of milliseconds, even a 1ms delay can cause different execution intervals between other functions. Although it can be managed through timer interrupts, the premise is that the time taken for the interrupt execution function must be short. When the program logic complexity increases, it can lead to confusion for later maintenance personnel, making it difficult to clarify the program’s operating state.
The following is part of the code for a dormitory anti-theft system done during my school years (at that time, there were also some bugs that were not resolved. Looking back now, there are actually many problems, and quite serious ones, such as a 3000ms delay in the interrupt service function, which is terrifying, and serial port sending, etc.; since the real-time requirements are not very high, the millisecond-level delays in the main function do not have much impact on system operation, of course, except for bugs; if maintenance is needed later, it would be a big project, and it would be better to rewrite it completely):
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;
}
}
}
}
2. Time-slice polling method
This is a program architecture design scheme that lies between front and back sequential execution method and operating systems. This design scheme can help embedded software developers to reach a higher level. In the embedded software development process, if the following points are encountered, this design scheme can be said to be the optimal choice, suitable for more complex embedded systems:
-
The current design requirements do not necessitate the use of an operating system. -
Task functions do not need to be executed all the time, there are intervals (for example, buttons usually require software debouncing, the common approach for beginners is to delay about 10ms before judgment, but 10ms greatly wastes CPU resources, during this time the CPU can handle many other tasks) -
Real-time performance has certain requirements.
This design scheme requires the use of a timer, generally set to 1ms (the timing can be arbitrary, but too frequent interrupts reduce efficiency, and too long interrupts reduce real-time performance), so it is necessary to consider the execution time of each task function, and it is recommended not to exceed 1ms (optimizing the program to shorten execution time is best; if it cannot be optimized, it must ensure that the execution period of the task is much greater than the time taken by the task).
“
How to determine the task cycle of each function? It is determined based on the task’s execution time and effect, such as the button scanning task cycle is 10ms (to improve responsiveness), the indicator light control task cycle is 100ms (usually the highest flashing frequency of 100ms is just right, except for special needs), LCD/OLED display cycle is 100ms (the time taken through interfaces like SPI/IIC is about 1~10ms, or even longer, so the task cycle must be much larger than the time taken, and to meet the screen refreshing effect that the human eye can accept, it cannot be too long, a task cycle of 100ms is more appropriate).
”
Below are two different implementation schemes, one for friends who are not familiar with function pointers and another for those who want to learn further.
1. Design without function pointers
/**
* @brief Main function.
* @param None.
* @return None.
*/
int main(void)
{
System_Init();
while (1)
{
if (TIM_1msFlag)// 1ms
{
CAN_CommTask(); // CAN send/receive communication task
TIM_1msFlag = 0;
}
if (TIM_10msFlag) // 10ms
{
KEY_ScanTask(); // Button scanning processing task
TIM_10msFlag = 0;
}
if (TIM_20msFlag) // 20ms
{
LOGIC_HandleTask();// Logic processing task
TIM_20msFlag = 0;
}
if (TIM_100msFlag) // 100ms
{
LED_CtrlTask(); // Indicator light control task
TIM_100msFlag = 0;
}
if (TIM_500msFlag)// 500ms
{
TIM_500msFlag = 0;
}
if (TIM_1secFlag) // 1s
{
WDog_Task(); // Feed dog task
TIM_1secFlag = 0;
}
}
}
/**
* @brief Timer 3 interrupt service function.
* @param None.
* @return None.
*/
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) // Overflow interrupt
{
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); // Clear interrupt flag
}
2. Design with function pointers
/**
* @brief Task function related information structure definition.
*/
typedef struct{
uint8 m_runFlag; /*!< Program running flag: 0-not running, 1-running */
uint16 m_timer; /*!< Timer */
uint16 m_itvTime; /*!< Task running interval time */
void (*m_pTaskHook)(void); /*!< Task function to run */
} TASK_InfoType;
#define TASKS_MAX 5 // Define number of tasks
/** Task function related information */
static TASK_InfoType sg_tTaskInfo[TASKS_MAX] = {
{0, 1, 1, CAN_CommTask}, // CAN communication task
{0, 10, 10, KEY_ScanTask}, // Button scanning task
{0, 20, 20, LOGIC_HandleTask}, // Logic processing task
{0, 100, 100, LED_CtrlTask}, // Indicator light control task
{0, 1000, 1000, WDog_Task}, // Feed dog task
};
/**
* @brief Task function running flag processing.
* @note This function is called by the 1ms timer interrupt
* @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 Task function running processing.
* @note This function is called by the main loop
* @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(); // Run task
sg_tTaskInfo[i].m_runFlag = 0; // Clear flag
}
}
}
/**
* @brief Main function.
* @param None.
* @return None.
*/
int main(void)
{
System_Init();
while (1)
{
TASK_Process();
}
}
/**
* @brief Timer 3 interrupt service function.
* @param None.
* @return None.
*/
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET) // Overflow interrupt
{
TASK_Remarks();
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);// Clear interrupt flag
}
3. Operating System
The embedded operating system EOS (Embedded OperatingSystem
) is a widely used system software, which was previously mainly used in industrial control and defense systems. For microcontrollers, commonly used ones include UCOS, FreeRTOS, RT-Thread
Nano and RTX among other preemptive operating systems (other operating systems like Linux are not suitable for microcontrollers)
Operating systems and time-slice polling methods have no excessive requirements on the time cost of each task in terms of task execution. Each task’s priority can be set, and when a high-priority task is ready, it will preempt a low-priority task; operating systems are relatively complex, so they are not detailed here.
Regarding how to choose a suitable operating system (comparison of uCOS
, FreeRTOS
, RTThread
, RTX
and other RTOS features:
-
uCOS: Plenty of online resources, very suitable for learning, but requires payment for product use. -
FreeRTOS: Free to use, hence widely adopted in many products. -
RT-Thread: A domestic IoT operating system with a wealth of components, also free, documentation available at RT-Thread documentation center. -
RTX: A royalty-free, deterministic real-time operating system designed for ARM and Cortex-M devices.
Here is a comparison chart found online:

4. Conclusion
From the above comparison, it can be seen that the advantage of the time-slice polling method is quite significant, as it combines the advantages of both the front and back sequential execution method and the operating system. The structure is clear, simple, and very easy to understand, making it a common design framework for microcontrollers.