Teach you to construct a real-time operating system for 51 single-chip microcomputer

Publisher:芳华逝水Latest update time:2011-08-23 Source: EEWORLD Reading articles on mobile phones Scan QR code
Read articles on your mobile phone anytime, anywhere

At present, most product development is based on some small-capacity single-chip microcomputers. The 51 series single-chip microcomputer is one of the most widely used single-chip microcomputer series in my country. It has a very broad application environment and prospects. With the accumulation of resources over the years, the 51 series single-chip microcomputer is still the first choice for many developers. In response to this situation, many expansion chips based on the 51 core have emerged in recent years, with more and more complete functions and faster speeds, which also shows the vitality of the 51 series single-chip microcomputer in China from one aspect.

For many years, we have been looking for a suitable real-time operating system as the basis for our development. According to the development requirements, we need to integrate some commonly used embedded components to save development time and minimize the development workload; in addition, we need to require that this real-time operating system can be easily embedded in small-capacity chips. After all, large systems are few, while small applications are many and widespread. Obviously, μC/OS-II is not suitable for the above requirements, and the RTX Tiny provided by Keil C does not come with source code and is not transparent, not to mention its FULL version.

1 Keil C51 and reentrancy issues

When it comes to real-time operating systems, we cannot ignore the reentry problem. For a large memory processor like a PC, this does not seem to be a very troublesome problem. To borrow the words of μC/OS-II RTOS, it is required to use local variables in the reentry function. However, the stack space of the 5l series microcontroller is very small, limited to 256 bytes, and it is impossible to allocate a local heap space for each function. It is for this reason that Keil C51 uses the so-called overlay technology:

① Local variables are stored in the global RAM space (without considering the case of extended external memory);

②When compiling and linking, the local variables have been located;

③If there is no direct or indirect calling relationship between functions, their local variable spaces can be overwritten.

It is for the above reasons that in the Keil C51 environment, pure functions cannot be re-implemented without processing (such as adding a simulation stack). So how to make the function re-implementable in the Keil C51 environment? The following is an analysis of the basic structure and mode of tasks under the real-time operating system:

vold TaskA(void *ptr){

UINT8 vaL_a;

//Some other variable definitions

do{

//Actual user task processing code

}while(1);

}

void TaskB(void*ptr){

UINT8 vaLb;

//Some other variable definitions

do{

Funcl();

//Other actual user task processing codes

)while(1);

void Funcl() {

UlNT8 v al_fa;

//Definition of other variables

//Function processing code

}

In the above code, there is no direct or indirect calling relationship between TaskA and TaskB, so their local variables val_a and val_b can be overwritten, that is, they may be located in the same RAM space. In this way, when TaskA runs for a period of time and changes val_a, TaskB may change val_b when it obtains CPU control and runs. Because they point to the same RAM space, when TaskA regains CPU control, the value of val-a has changed, causing the program to run incorrectly, and vice versa. On the other hand, Funcl() has a direct calling relationship with TaskB, so its local variables val_fa and val_b will not be overwritten by each other, but it cannot guarantee that its local variable val_fa will not form an overwriting relationship with the local variables of TaskA or other tasks.

Defining local variables such as val_a, val_b, and val_fa as static variables (adding the static indicator) can solve this problem. But the problem is that defining a large number of static type variables will lead to a large amount of RAM space being occupied, which may directly lead to insufficient RAM space. Especially in some small-capacity microcontrollers, which generally only have 128 or 256 bytes, a large number of static variable definitions are obviously not suitable under such small RAM resource conditions. Therefore, there is another solution, as shown in the following code:

void TaskC(void) {

UINT8 x, v;

whlk(1){

OS_ENTER_CRITICAL();

x = GetX(); (1)

y = GetY(); (2)

//Other codes for the task

OS_EXIT_CRITICAL(); (3)

0SSleep(100); (4)

}

}

In the above code TaskC uses the critical protection method to protect the code from being preempted by interrupts, which effectively solves the problem that the RAM space is too small and it is not suitable to define a large number of static variables. However, if each task adopts this structure, the interrupt will be turned off at the beginning of the task, which will make the real-time performance unable to be guaranteed. It turns out that this delay is quite considerable. To use an example to illustrate, if you want to use a dynamically refreshed LED display in the system, it is difficult to ensure the stability and continuity of the display, even if a separate timer is used in the system to do this work (after entering the critical section, EA=0). Secondly, this structure actually converts the preemptive task scheduling into non-preemptive task scheduling. In fact, if there is no interruption between (3) and (4) and a task scheduling does not happen, it can be understood that the task actively gives up the control of the CPU. If an interrupt happens to occur between (3) and (4) and a task scheduling occurs, it is just an unnecessary task scheduling. And it is not expected that two or more task scheduling will occur after (3). I believe that readers also have this wish.

In addition, we can find a feature of the task: when the task restarts from (1), it does not matter what the values ​​of local variables x and y are, that is, even if x and y change after (3), it is no longer important and will not affect the correctness of the program. In fact, this feature is also a common feature of most tasks, at least most local variables of most tasks - if the task will not give up CPU control (before being preempted) during the entire execution process, then most of its local variables do not need special protection, that is, their scope is only the current execution of the task, and for the above code, it is the code area within the critical protection zone.

At present, most product development is based on some small-capacity single-chip microcomputers. The 51 series single-chip microcomputer is one of the most widely used single-chip microcomputer series in my country. It has a very broad application environment and prospects. With the accumulation of resources over the years, the 51 series single-chip microcomputer is still the first choice for many developers. In response to this situation, many expansion chips based on the 51 core have emerged in recent years, with more and more complete functions and faster speeds, which also shows the vitality of the 51 series single-chip microcomputer in China from one aspect.

For many years, we have been looking for a suitable real-time operating system as the basis for our development. According to the development requirements, we need to integrate some commonly used embedded components to save development time and minimize the development workload; in addition, we need to require that this real-time operating system can be easily embedded in small-capacity chips. After all, large systems are few, while small applications are many and widespread. Obviously, μC/OS-II is not suitable for the above requirements, and the RTX Tiny provided by Keil C does not come with source code and is not transparent, not to mention its FULL version.

1 Keil C51 and reentrancy issues

When it comes to real-time operating systems, we cannot ignore the reentry problem. For a large memory processor like a PC, this does not seem to be a very troublesome problem. To borrow the words of μC/OS-II RTOS, it is required to use local variables in the reentry function. However, the stack space of the 5l series microcontroller is very small, limited to 256 bytes, and it is impossible to allocate a local heap space for each function. It is for this reason that Keil C51 uses the so-called overlay technology:

① Local variables are stored in the global RAM space (without considering the case of extended external memory);

②When compiling and linking, the local variables have been located;

③If there is no direct or indirect calling relationship between functions, their local variable spaces can be overwritten.

It is for the above reasons that in the Keil C51 environment, pure functions cannot be re-implemented without processing (such as adding a simulation stack). So how to make the function re-implementable in the Keil C51 environment? The following is an analysis of the basic structure and mode of tasks under the real-time operating system:

vold TaskA(void *ptr){

UINT8 vaL_a;

//Some other variable definitions

do{

//Actual user task processing code

}while(1);

}

void TaskB(void*ptr){

UINT8 vaLb;

//Some other variable definitions

do{

Funcl();

//Other actual user task processing codes

)while(1);

void Funcl() {

UlNT8 v al_fa;

//Definition of other variables

//Function processing code

}

In the above code, there is no direct or indirect calling relationship between TaskA and TaskB, so their local variables val_a and val_b can be overwritten, that is, they may be located in the same RAM space. In this way, when TaskA runs for a period of time and changes val_a, TaskB may change val_b when it obtains CPU control and runs. Because they point to the same RAM space, when TaskA regains CPU control, the value of val-a has changed, causing the program to run incorrectly, and vice versa. On the other hand, Funcl() has a direct calling relationship with TaskB, so its local variables val_fa and val_b will not be overwritten by each other, but it cannot guarantee that its local variable val_fa will not form an overwriting relationship with the local variables of TaskA or other tasks.

Defining local variables such as val_a, val_b, and val_fa as static variables (adding the static indicator) can solve this problem. But the problem is that defining a large number of static type variables will lead to a large amount of RAM space being occupied, which may directly lead to insufficient RAM space. Especially in some small-capacity microcontrollers, which generally only have 128 or 256 bytes, a large number of static variable definitions are obviously not suitable under such small RAM resource conditions. Therefore, there is another solution, as shown in the following code:

void TaskC(void) {

UINT8 x, v;

whlk(1){

OS_ENTER_CRITICAL();

x = GetX(); (1)

y = GetY(); (2)

//Other codes for the task

OS_EXIT_CRITICAL(); (3)

0SSleep(100); (4)

}

}

In the above code TaskC uses the critical protection method to protect the code from being preempted by interrupts, which effectively solves the problem that the RAM space is too small and it is not suitable to define a large number of static variables. However, if each task adopts this structure, the interrupt will be turned off at the beginning of the task, which will make the real-time performance unable to be guaranteed. It turns out that this delay is quite considerable. To use an example to illustrate, if you want to use a dynamically refreshed LED display in the system, it is difficult to ensure the stability and continuity of the display, even if a separate timer is used in the system to do this work (after entering the critical section, EA=0). Secondly, this structure actually converts the preemptive task scheduling into non-preemptive task scheduling. In fact, if there is no interruption between (3) and (4) and a task scheduling does not happen, it can be understood that the task actively gives up the control of the CPU. If an interrupt happens to occur between (3) and (4) and a task scheduling occurs, it is just an unnecessary task scheduling. And it is not expected that two or more task scheduling will occur after (3). I believe that readers also have this wish.

In addition, we can find a feature of the task: when the task restarts from (1), it does not matter what the values ​​of local variables x and y are, that is, even if x and y change after (3), it is no longer important and will not affect the correctness of the program. In fact, this feature is also a common feature of most tasks, at least most local variables of most tasks - if the task will not give up CPU control (before being preempted) during the entire execution process, then most of its local variables do not need special protection, that is, their scope is only the current execution of the task, and for the above code, it is the code area within the critical protection zone.

2 Should the real-time operating system take precedence?

From the above analysis, if you want to keep a function re-enterable, you have to use static variables, and the system's RAM resources will be a severe test; if you use critical sections to protect the operating environment, the real-time performance of the system cannot be guaranteed, and there is a risk of converting preemptive task scheduling to non-preemptive task scheduling. Obviously, using static variables is simple, but there are more inapplicability, and it is also an obstacle to future functional adjustments, so it is generally not adopted. Then, we can only work on environmental protection, but can we really only sacrifice the real-time performance of the system by entering the critical section to ensure that the task is not preempted? Let's take a look at the basic idea of ​​the critical protection method:

① In a task, if local variables are not preemptively switched within their scope, these variables will not affect the correct execution of the task even if their values ​​are not concerned after the task is deprived of CPU control;

②Using critical area protection can achieve the above-mentioned requirements;

③ The resulting reduction in real-time performance and preemptive switching is acceptable. It can be seen that not being preempted is the key to task protection of local variables. In this case, why not abandon preemptive task scheduling? This is a good starting point. For Keil C51, non-preemptive task scheduling may be a better method to better coordinate the established resources of the 51 series microcontroller. Write such a system below:

①Use non-preemptive task scheduling;

② It can be used in small-capacity chips. The development goal is that even a small chip like 8051 can use this real-time operating system;

③Support priority scheduling to ensure its real-time performance as much as possible.

3 Implementation of Real-time Operating System

Based on the above analysis and purpose, this operating system was recently completed. The management method of RTx is used on the stack, that is, the current task uses all the heap space, as shown in Figure 1.

3.1 Stack initialization and task creation

The stack initialization actually initializes the 0STaskStackBotton array, and designates the current task as an idle task, and the next running task as the highest priority task, that is, the task with a priority of zero. During initialization, the value of SP is stored in OSTaskStackBotton[O], the value of SP+2 is stored in OSTaskStacKBotton[1], and so on. The task is created by calling the 0STa-skCreate function. In fact, it just fills the address of the task (assuming it is task number n) into the corresponding position pointed to by OSTaskStackBotton[n], and moves SP backward by 2 bytes, as shown in Figure 2.

Why is it in this way and not in other ways? This is because after the task is created and before the task is scheduled, the stack of each task is actually its own address, so its stack depth is 2, which is directly filled in for the simplicity of the program.

void main(void) {

OSInit(); /*Initialize OSTaskStackBcBotton queue*/

TMOD=(TMOD&0XFO)│0XOl;

TL0=0xBF;

TH0=0xFC;

TRO=1;

ETO=1;

TFO=O:

OSTaskCreate(TaskA, NULL, 0);

OSTaskCreate(TaskB.NULL, 1);

OSTaskCreate(TaskC, NULL, 2);

OSStart();

In the above code, after all tasks are established, OSStart() is called to start task scheduling. OSStart() is a macro definition as shown below:

#definition OSStart() d0{\

OSTaskCreate(TaskIdle, NULL, OS_MAX_TASKS);\

EA=l:\

return;

}while(O)

First, it creates an idle task and turns on interrupts, then returns. Where does it return to? We know that the idle task is the lowest priority task. When OSTaskCreate is called to create it, its address will be filled in the SP position and SP will be moved back 2 bytes (see Figure 2 and description). Therefore, the idle task Taslddle must be at the top of the stack at this time. This means that the return here will definitely return to the idle task. At this point, the system enters normal operation.

3.2 Task Switching

There are two cases for task switching. When the priority of the current task is lower than the next task to obtain CPU control, the contents between the top of the stack of the next task to obtain CPU control and the top of the stack of the current task are moved to the high end of the RAM space to free up all the RAM space for the next task's stack space. At the same time, the corresponding OSTaskStackBotton is updated to point to the bottom of the stack of the new correct task. If the priority of the current task is higher than the priority of the next task, the opposite movement is performed, as shown in Figures 3 and 4.

All tasks must actively call OSSleep to give up control of the CPU. After a task calls OSSleep, the highest priority ready task will be selected to run.

Conclusion

After the system is completed, the kernel code size is about 400 bytes, occupying 1 timer interrupt and a small amount of memory space. The system is set to have a capacity of 8 tasks, and the actual number of tasks available to users is 7, which can meet general needs and also meet the development requirements for application in small-capacity chips. Since preemptive task scheduling is not adopted, except for some local variables of individual tasks related to the whole process, other local variables no longer have an overwriting relationship. Since the task actively gives up the CPU control, it is also easy to process individual variables that need to be protected separately. In the system, there is no need to repeatedly switch interrupts throughout the process, and the real-time performance is also very good. Except for peripherals with strict timing requirements (such as DSl8820).

Reference address:Teach you to construct a real-time operating system for 51 single-chip microcomputer

Previous article:Design of high-precision electronic scale based on AVR microcontroller
Next article:LED digital large screen display system based on single chip microcomputer

Latest Industrial Control Articles
Change More Related Popular Components

EEWorld
subscription
account

EEWorld
service
account

Automotive
development
circle

About Us Customer Service Contact Information Datasheet Sitemap LatestNews


Room 1530, 15th Floor, Building B, No.18 Zhongguancun Street, Haidian District, Beijing, Postal Code: 100190 China Telephone: 008610 8235 0740

Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号