Abstract: This paper details the kernel code and porting method of μC/OS-II, a real-time embedded operating system with publicly available source code, using AVR mega series microcontrollers as a platform. The performance of the system was also tested, providing a reference for embedded system development on 8-bit microcontrollers.
Keywords: μC/OS-II; AVR mega; porting; system testing
1. Introduction
With the development of technology, the design and application of embedded systems have had a significant impact on people's lives in recent years and will gradually change their future lifestyles. Developing applications on a specific operating system allows developers to ignore many low-level hardware details, making application debugging easier, maintenance simpler, and shortening the development cycle, thus reducing development costs. Therefore, embedded real-time operating systems are highly favored by developers.
μC/OS-II is a preemptive real-time multitasking operating system specifically designed for microprocessors. It features open-source code, portability, customizability, high stability, and reliability. Its kernel primarily provides services such as process management, time management, and memory management. The system supports up to 56 tasks, each with its own priority. Because its kernel is preemptive, the highest-priority task is always executed. The system provides a rich set of API functions for easy inter-process communication and process state transitions. Since μC/OS-II is general-purpose software written for embedded applications, it needs to be ported to different microcontrollers for specific applications. Most of its code is written in standard C, with only the processor-related code written in assembly language, thus possessing strong portability and capable of running on most 8-bit, 16-bit, and 32-bit microcontrollers and digital signal processors.
The AVR mega series microcontrollers are low-power 8-bit microcontrollers based on AVR RISC, with 32 internal general-purpose registers. They can achieve a performance of 1 MIPS/MHz by executing one instruction per clock cycle. The AVR microcontroller core has a rich instruction set, directly connected to the arithmetic logic unit (ALU) through the 32 general-purpose registers, allowing a single instruction to access two independent registers within a single clock cycle. This architecture makes code execution nearly 10 times faster than traditional complex instruction set microprocessors. The AVR mega128 is the most powerful and resource-rich microcontroller in the mega series, featuring 128KB of in-system programmable flash, 4KB of SRAM, and EEPROM, providing a good platform for system porting. To port μC/OS-II to the AVR mega series MCUs, an AVR microcontroller compiler is required; the GNU AVR-GCC compiler is used here.
2. Overview of the transplantation process
The transplantation process mainly includes the following aspects:
● Use #define to set the value of a constant (OS_CPU.H);
● Declare 9 data types (OS_CPU.H);
● Declare three macros using #define (OS_CPU.H);
● Write six functions in C (OS_CPU_C.C);
● Write four assembly language functions (OS_CPU_A.ASM).
To port μC/OS-II, three files must be written: OS_CPU.H, OS_CPU_A.ASM, and OS_CPU_C.C. These three files are related to the chip's hardware characteristics and primarily provide task switching and clock functionality. The rest of the μC/OS-II kernel code is written in C, providing functions such as task management, inter-process communication, time management, and memory management. The μC/OS-II software and hardware architecture is shown in the following figure:
Figure 1. μC/OS-II software and hardware architecture
INCLUDES.H is a header file that is included at the beginning of all files with the .C extension. It primarily contains three files: CFG.H, OS_CPU.H, and UCOS_II.H. For different types of processors, the INCLUDES.H file needs to be rewritten, adding its own header file, but this must be added to the end of the file. OS_CFG.H mainly contains binary constants. By setting these constants to 1 or 0, the kernel can be easily trimmed, which is a prominent advantage of μC/OS-II.
3. Porting Code Analysis
3.1 OS_CPU.H file
OS_CUP.H includes processor-related constants, macros, and type definitions defined using #define. Its general structure is as follows:
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U; /* Unsigned 8-bit character */
typedef signed char INT8S; /* Signed 8-bit character */
typedef unsigned int INT16U; /* Unsigned 16-bit */
typedef signed int INT16S; /* Signed 16-bit integer */
typedef unsigned long INT32U; /* Unsigned 32-bit integer */
typedef signed long INT32S; /* Signed 32-bit integer */
typedef float FP32; /* Single-precision floating-point number */
typedef unsigned char OS_STK; /* Stack entry width is 8 bits */
#define OS_STK_GROWTH 1 /* The stack grows from high address to low address */
#define OS_ENTER_CRITICAL() asm volatile ("cli")/* Disable interrupts*/
#define OS_EXIT_CRITICAL() asm volatile ("sei") /* Enable interrupts */
#define OS_TASK_SW() OSCtxSw() /* Task-level switching function */
(1) Data type
Because different processors have different word lengths, porting μC/OS-II required redefining a series of data structures to ensure portability. Users also needed to declare the correct C data types via OS_STK to inform μC/OS-II of the task stack data type. In the AVR mega series, the stack operates on an 8-bit word length, so the stack data type OS_STK was declared as 8-bit. All task stacks must have their data types declared using OS_STK.
(2) Critical section of code
μC/OS-II requires interrupts to be disabled before entering the critical code section and re-enabled after exiting the critical section. This protects the critical section code from damage caused by multitasking or interrupt service routines. In AVR mega series microcontrollers, this is achieved by setting the interrupt mask bit in the status register SREG. The macro OS_ENTER_CRITICAL() in μC/OS-II clears the interrupt mask bit in the status register to disable all interrupts; OS_EXIT_CRITICAL() sets the interrupt mask bit in the status register to enable all interrupts.
(3) Stack direction
The stack of AVR mega series microprocessors decreases from high address to low address, so OS_STK_GROWTH must be set to 1.
(4) Definition of OS_TASK_SW() function
In μC/OS-II, OS_TASK_SW() is used to implement task switching. When defining macros, it is directly defined as the OSCtxSw() function, which is the task-level switching function.
3.2 OS_CPU_A.ASM file
3.2.1 Related Functions
When porting μC/OS-II, four assembly language functions in OS_CPU_A.ASM need to be rewritten:
● OSStartHighRdy()
● OSCtxSw()
● OSIntCtxSw()
● OSTickISR()
3.2.2 Function Introduction
(1) OSStartHighRdy() function
This function is called by the OSStart() function, and its purpose is to run the highest priority ready task. Before calling OSStart(), the user must call OSInit() and at least create a task. When OSStart() is called, it obtains the address pointer OSTCBHighRdy of the OS_TCB of the highest priority ready task and assigns this pointer to OSTCBCur. OSStartHighRdy() can obtain the stack pointer from the task's control block through OSTCBHighRdy. Since the stack is initialized to simulate an interrupt when the OSTaskCreate() function creates the task, the RET command can switch to the new task after popping all saved registers.
The assembly code and related description of the OSStartHighRdy() function are as follows:
CALL OSTaskSwHook /* Call a user-defined function */
LDS R16,OSRunning /* Sets variables that indicate when the system starts running */
INC R16
STS OSRunning, R16
LDS R30,OSTCBHighRdy /* The Z pointer points to the OS_TCB of the highest priority task */
LDS R31,OSTCBHighRdy+1
LD R28,Z+
out SPL,R28
LD R29,Z+
out SPH,R29 /* Get the stack pointer from OS_TCB */
POPRS /* Restore all registers */
RET /* Task execution begins */
(2) OSCtxSw() function
OSCtxSw() is a task-level task switching function that is only called within a task, unlike the switching function OSIntCtxSw() called in an interrupt routine. In μC/OS-II, if a task calls a function whose execution result might cause system task rescheduling (e.g., attempting to wake up a higher-priority task), a task switch is required. Since the AVR mega128 does not support software interrupt instructions, the OSCtxSw() function was directly defined in the OS_CPU.H file during porting.
The assembly code and related information for the OSCtxSw() function are as follows:
PUSHRS /* Save the current environment */
LDS R30,OSTCBCur
LDS R31,OSTCBCur+1 /* The Z pointer points to the OS_TCB of the current task */
in r28,SPL
ST Z+,R28
in r29,SPH
ST Z+,R29 /* Save the stack pointer to OS_TCB */
CALL OSTaskSwHook /* Call a user-defined function */
LDS R16,OSPrioHighRdy /*OSPrioCur = OSPrioHighRdy */
STS OSPrioCur, R16
LDS R30,OSTCBHighRdy
LDS R31,OSTCBHighRdy /* The z pointer points to the OS_TCB of the high-priority task */
STS OSTCBCur, R30
STS OSTCBCur+1,R31 /* OSTCBCur = OSTCBHighRdy */
LD R28,Z+
out SPL,R28
LD R29,Z+
out SPH,R29 /* Get stack pointer */
POPRS /* Update register contents */
RET /* Task Switch */
(3) OSIntCtxSw() function
In μC/OS-II, since interrupts may trigger task switching, the OSIntExit() function is called at the end of the interrupt service routine to check the task readiness status. If a task switch is required, the OSIntCtxSw() function is called; therefore, OSIntCtxSw() is also known as the interrupt-level task switching function. Because an interrupt has occurred before calling OSIntCtxSw(), the default registers of OSIntCtxSw() are already saved in the stack of the interrupted task. The code for OSIntCtxSw() is mostly the same as that for OSCtxSw(), with the following differences:
● Since the interrupt has already occurred and the registers have been pushed onto the stack, there is no need to save the register contents here;
● When OSIntExit() is called within an interrupt service routine, the return address is pushed onto the stack. Entering the critical function OS_ENTER_CRITICAL() within OSIntExit() may also push the CPU status word onto the stack, depending on how interrupts are disabled. Simultaneously, the return address of OSIntCtxSw() is also pushed onto the stack.
When a task is suspended, the stack structure should be completely consistent with the specifications of μC/OS-II. Therefore, when a task is suspended, OSIntCtxSw() needs to adjust the stack pointer, ensuring that the adjusted stack structure looks identical for all suspended tasks. The adjustment method is simple: just increment the stack pointer by a fixed value.
(4) OSTickISR() function
In μC/OS-II, the clock interrupt is crucial after calling OSStart() to start the multitasking environment. The clock interrupt handles all timer-related tasks, such as task delays and waiting operations. It queries tasks in a waiting state to determine if their delays have ended, allowing for rescheduling. OSTickISR() uses other interrupt service routine prototypes from μC/OS-II. During porting, it utilizes the overflow interrupt of the 8-bit Timer 0 of the AVR mega128 to generate clock ticks, with an overflow time of 10ms. The timer's initial value is reloaded in OSTickISR().
The assembly code and related information for the OSTickISR() function are as follows:
OSTickISR:
SEI /* Re-enable interrupt*/
PUSHRS /* Save all registers */
LDS R16,OSIntNesting /* μC/OS-II Interrupt Service Routine Prototype*/
INC R16
STS OSIntNesting,R16
CALL OSTimeTick /* Call the clock tick handling function */
CALL OSIntExit /* μC/OS-II interrupt service routine prototype*/
LDI R16,256-(11059200/50/1024)
OUT TCNT0,R16 /* Reload timer, timing 10ms */
POPRS /* Restore register value */
RET /* Interrupt return*/
3.3 OS_CPU_C.C file
Porting μC/OS-II requires users to define six functions in OS_CPU_C.C, but only OSStkInit() actually needs to be defined. The other five functions need to be declared, but may not have actual content. These five functions are user-defined. When using them, OS_CPU_HOOKS_EN in OS_CFG.H needs to be set to 1; setting it to zero indicates that these functions are not used. The porting code does not require the use of these functions, so only their nullable functions are defined. These five functions are:
● OSTaskCreateHook() function
● OSTaskDelHook() function
● OSTaskSwHook() function
● OSTaskStatHook() function
● OSTimeTickHook() function
The `OSTaskStkInit()` function, called by the task creation functions `OSTaskCreate()` or `OSTaskCreateExt()`, is used to initialize the task's stack. The initial stack simulates the stack structure after an interrupt. Storage space for each register is reserved according to the order of pushes after the interrupt, and the interrupt return address points to the starting address of the task code. Since the AVR mega128's stack is 8 bits wide, `OSTaskStkInit()` creates a pointer to a memory region in bytes, ensuring the stack pointer points to the top of an empty stack. After stack initialization, `OSTaskStkInit()` returns a new stack pointer, which `OSTaskCreate()` or `OSTaskCreateExt()` saves to the task's `OS_TCB`. Note that `SREG` in the stack is initialized to 0x80, enabling interrupts after task startup; if set to 0x00, interrupts are disabled after task startup. If you choose to allow interrupts after a task starts, then interrupts are allowed during the execution of all tasks; similarly, if you choose to disable interrupts after a task starts, then interrupts are disabled for all tasks, and there is no choice. If a task chooses to disable interrupts after startup, then other tasks need to re-enable interrupts when running. Additionally, the `OSTaskIdle()` and `OSTaskStat()` functions need to be modified to enable interrupts at runtime. If any of these steps fail, the system will crash. Therefore, when initializing the task stack, the status register `SREG` is set to 0x80, which enables interrupts after task startup.
4 System Testing
With the widespread application of embedded products, research on embedded systems is constantly developing, resulting in many real-time systems, such as VxWorks and RT-Linux. However, these operating systems primarily operate on 32-bit processors and require significant cost. Due to the resource limitations of 8-bit microcontrollers, there were previously few operating systems running on 8-bit machines. However, with the increase in functionality and resources of 8-bit machines, many real-time systems have gradually emerged for 8-bit microcontrollers, such as AVRX and NUT/OS on AVR microcontrollers. These operating systems are mainly written in assembly language, which offers relatively high code efficiency, but also has its limitations. As dedicated operating systems, they are difficult to port to other platforms and for use in other contexts. μC/OS-II, an operating system written in C, has been widely ported to various platforms. However, because it is written in C, its performance is slightly inferior to operating systems written in assembly language; therefore, the system was tested after the porting process was completed.
There is a certain delay between a real-time process becoming runnable in the system and the process actually starting its execution; this is called the allocation time. This time is closely related to the kernel. To test this time, only one task was created. This task waits for a semaphore, and upon receiving the semaphore, it simply increments a count value by 1. The process of sending the semaphore to the task is completed using the compare-match interrupt of Timer 2. The AVR microcontroller's timer has a compare-match interrupt mode. When the timer count equals the preset value, it can start counting again from 0. The compare-match interrupt routine uses the μC/OS-II interrupt service routine prototype, and sends a semaphore to the task in the interrupt routine, and defines a variable to record the number of interrupts. If the count value in the task program and the count value in the interrupt routine are equal within the set compare time, it indicates that the task can meet the allocation time within that time. By continuously reducing the compare-match time, it was found that when the time is reduced to 24 microseconds, the task has stopped running, and the count value in the task is 0, indicating that the task did not have time to run and was occupied by the compare-match interrupt routine. When the time exceeds 24 microseconds, it takes 6,000 task scheduling operations to complete. For an 8-bit microcontroller, whose processing speed is relatively slow compared to high-end CPUs, 24 microseconds is sufficient for the application's needs.
In real-time systems, the real-time performance is reflected in the system's responsiveness to external events. The system responds to external events through interrupts, and the user's interrupt service routine should perform as little work as possible, leaving most of the tasks to the task, which is notified to run only through semaphores or message queues. Therefore, the task's response time to external events is a crucial performance indicator, and this time was tested. A task was created in the program and waited on a semaphore. Timer 2 of the mega128 was set to compare-match output mode, generating a periodic pulse output and triggering an interrupt after the matching time. Timer 1 was set to counting mode to count the generated pulse outputs. The semaphore was sent to notify the task to run through the compare-match interrupt service routine of Timer 2. The interrupt was not enabled in the interrupt routine itself, but only after the task received the semaphore and started running, thus synchronizing interrupt handling with task execution. A global counter was incremented in the task to record the number of times the task ran. If, after a period of running, the task's counter and Timer 1's count were equal within the set compare-match time, then the system was fully responsive to external events during that period. When the matching time of Timer 2 is greater than 39 microseconds, the task exits the task loop and stops counting Timer 1 after running 6,000 times. The values of both counters are then equal, both at 6,000. When the matching time is less than 39 microseconds, the count value of Timer 1 is greater than the task's count value of 6,000, meaning the task's processing time cannot fully respond to external events. That is, the system's response and processing time to external events is 39 microseconds, and only when the occurrence of external events exceeds this time can the system's real-time performance be guaranteed. When the matching time decreases to 24 microseconds, which is the system's allocation time, the task counter's value is zero, while Timer 1 continues counting, indicating that the task can no longer be executed, consistent with the aforementioned conclusion.
7. Conclusion
μC/OS-II, as a general-purpose embedded real-time operating system, has been widely used in various applications. Through porting and testing on the AVR Mage series microcontrollers, its feasibility on 8-bit microcontrollers was demonstrated. Furthermore, due to its publicly available source code and the characteristics of 8-bit machines, system customization and expansion can be implemented to achieve better results. This paper provides a fundamental reference for embedded system applications.
References
[1] Geng Degen, Song Jianguo, Ma Chao, Ye Yongjian. Principles and Applications of AVR High-Speed Embedded Microcontrollers. Beijing: Beijing University of Aeronautics and Astronautics Press, 2001.
[2] Jean J. Labrosse, translated by Shao Beibei. μC/OS-II – An Open Source Real-Time Embedded Operating System. Beijing: China Electric Power Press, 2001.
[3] Tu Qi, Tu Lide. Fundamentals of Operating Systems. Beijing: Tsinghua University Press, 2000.