Operating Systems always struck me as very complex programs. At some point in my life, I imagined that only a small group of elite programmers could potentially develop operating systems. When I first learnt of the kernel as the core of an operating system back in high school, I couldn’t even begin to comprehend what a kernel actually was. It always felt like such a foreign and complex concept to the much younger version of myself. I actually lived for much longer than I’m proud of without proper knowledge of operating system construction.
I was introduced to task scheduling a long time ago, but I’d never have imagined it making so much intuitive sense as it does to me today. The best I could do at the time was imagine what the different types of operating systems could possibly do, and I barely had a clue as to their construction. I could name them reasonably well, from time-sharing operating systems, distributed operating systems, multi-program operating systems and real-time operating systems. That was, however, as deep as my knowledge went when it came to the topic.
I was a week into a new job when my boss told me he’d give me an embedded operating system that he’s written to look at, and I was to figure out how I would write application code to run on the operating system. My mind lit up at the mention of this. I was very intrigued, I’d always dreamt of gaining a reasonable understanding of operating systems. I had so far gained working knowledge of different parts in computing, but the operating system was one of the most elusive. I could understand how transistors work, from my mechatronics engineering coursework, I had a good enough understanding of logic gates, and hardware registers. This gave me a good foundation in writing bare-metal code for different processors, which was always interesting, as much as it was tiring. As a result I had a fairly good understanding of how these low-level components build up into the higher level abstractions that we know and love to use when it comes to application development.
All this was in an embedded environment, however, and I would rather have ignored the middleman (the operating system) altogether and not even begin to go down that rabbit hole. Between figuring out hardware development, bare-metal programming, and high-level application development I didn’t imagine I would begin to explore the depths of operating systems until much further down the line.
My naive brain could not fathom what it would mean to write your own operating system. It kept posing the same question over and over, “What do you mean an operating system that you’ve written?”
I could not wait to get my hands, or rather my eyes on that precious bit of code, that would open so many more possibilities for me. And I did! And it was beautiful! Magical even! Yet, it seemed so obvious. How have I never thought of that? The operating system was a time sharing operating system that distributes processor time between different tasks at different frequencies. The more crucial tasks (with real-time constraints) being scheduled every millisecond or so, and less crucial tasks (without real-time constraints) being scheduled more rarely.
Looking at this system, a couple of things immediately stood out to me. First, that this in itself wholly fits the definition of an operating system, very simple, very intuitive, and very well suited to its purpose. Second, was that we essentially potentially free up a lot of cpu time. Scheduling, say 5 short-running tasks, in 10 milliseconds, 100 milliseconds, 1 second, 2 seconds, and 5 seconds; could mean that we potentially have a lot of unutilized cpu time (with the assumption that the tasks execute in under 1 millisecond – which is a pretty safe assumption for a short running non-blocking task). Third, that we can potentially guarantee (with a reasonable error) that a scheduled task under such conditions will run as often as the scheduled time interval!
I quickly went ahead and adapted a similar method in developing a pseudo-real-time time-sharing operating system for the AVR architecture.
OS Construction
The operating system code is designed to work on the AVR ATMega2560 microcontroller. It can, however, easily be adapted to work with the microcontroller of your choice.
The OS construction is as follows:
- The operating system is based on a time-sharing model, where tasks are regularly scheduled and allocated time-slices to execute.
- A hardware timer is used to generate a systick timer with a 1-millisecond period.
- On every timeout of the hardware timer, the system checks what time-slice allocations are due, depending on the scheduling of the different tasks, and schedules the corresponding tasks.
- During the normal operation, the operating system runs through the time-slices and checks for the scheduled tasks, and executes them one after another.
Below are some of the main functions for the operating system:
- Some utility variables
static uint32_t OS_tickcounter_u32 = 0;
static uint8_t OS_timeslice_reg_u8 = 0;
- Some useful defines
#define OS_REQTIMESLICE(_SLICE_) (OS_timeslice_reg_u8 |= _SLICE_) /*< Request a timeslice (time period elapsed) -> sets a bit in the timeslice register*/
#define OS_ACTIVATE_TIMESLICE(_SLICE_) (OS_timeslice_reg_u8 &= ~_SLICE_) /*< Activates a timeslice (indicates that the timeslice has been executed) -> clears a bit in the timeslice register*/
#define OS_CHECK_TIMESLICE_REQ(_SLICE_) ((OS_timeslice_reg_u8 & _SLICE_) == _SLICE_) /*< Check if a timeslice request has been issued -> Checks if a bit has been set in the timeslice register*/
- Starting the system timer
/**
* @brief This function initializes the systick timer. This timer generates a 1ms
* systick for controlling the time-sharing allocation for the OS
*
* @param none
*
* @return none
*/
void OS_systick_start(void)
{
cli();
/**
* Timer5 to be used as our systick timer, running at a period of 1ms.
* Timer5 is a 16-bit timer.
* We make it into an 8-bit timer, executing our ISR every 1ms
* - Feed it with a prescaler of 64
* - Set an output compare value of 0xFA 250 -> exactly 1ms
* - Clear timer on compare -> reset timer when compare value reached (set our compare value as the max timer value)
*/
TCCR5A = 0x00; /*< Clear timer 5 counter control register (we're not using any configurations) */
TCCR5B |= (1 << CS50) |(1 << CS51) | (1 << WGM52); /*< enable timer5 (prescaler 64) and enable CTC mode */
OCR5A = 0xF9; /*< Set Output compare value */
TIMSK5 |= (1 << OCIE5A); /*< enable timer5 output compare A interrupt */
sei();
}
- Scheduling time slices
/**
* @brief This function allocates time slices to the periodically executed OS functions.
* It determines whether the time has elapsed for the timeslice. If so, it enables
* the timeslice execution
*
* @param none
*
* @return none
*/
void OS_systick_alloc(void)
{
OS_tickcounter_u32++;
/**
* Request a 1ms timeslice
*/
OS_REQTIMESLICE(MASK_TIMESLICE_1MS);
/**
* Request a 2ms timeslice
*/
if ((OS_tickcounter_u32 % OS_TIMESLICE_2MS) == 0)
{
OS_REQTIMESLICE(MASK_TIMESLICE_2MS);
}
}
- Executing time slices
/**
* @brief This function executes the timeslices for the periodically executed OS functions
*
* @param none
*
* @return none
*/
void OS_systick_run(void)
{
/**
* Executes a 1ms timesilce.
*/
if (OS_CHECK_TIMESLICE_REQ(MASK_TIMESLICE_1MS))
{
OS_ACTIVATE_TIMESLICE(MASK_TIMESLICE_1MS);
OS_execTimeSlice_1ms();
}
}
- A single time slice function
/**
* @brief Contains all routines that require a 1ms periodic execution
*
* @param none
*
* @return none
*/
static void OS_execTimeSlice_1ms(void)
{
}
GitHub
You can find the source code for my real-time time-sharing operating system on my GitHub:
See for yourself, how easy it is to create an operating system! 😄