4
\$\begingroup\$

I know some people will say I don't need to write an OS because there are lots of options out there. But I'm not interested in using an OS to solve an specific problem. I want to write one by myself to learn how to do it.

I know a lot of things about the theory of Operating Systems and I also know how to program microcontrollers in bare metal. But I know very little about the configuration details that are not directly related to programming. I'm talking about link command files, startup code, etc.

My question is: what do I need to know about these "configuration" related things in order to put the OS running? Please link some external content about those things, if possible.

\$\endgroup\$
8
  • 4
    \$\begingroup\$ It's absolutely excellent that you want to take on a project like this. This is exactly how engineers learn and expand, whether it just becomes plans and ideas explored or goes all the way to finished software. It's not an electronics design question though, so you're on the wrong forum. But a brilliant project and goal :-) \$\endgroup\$
    – TonyM
    Commented Mar 6, 2018 at 17:28
  • \$\begingroup\$ @TonyM Thanks for your comment. I post the question here because this is the stackexchange forum that's closer to Embedded Software, RTOS, Embedded OS, Microcontrollers. If I make this question on stackoverflow people will say I'm on the wrong forum too. :D \$\endgroup\$
    – user49894
    Commented Mar 6, 2018 at 17:40
  • 1
    \$\begingroup\$ This is a pretty hefty question, and it may be considered to be too broad to be answered all in one. I'd recommend looking into the documentation for freeRTOS along with the example projects they provide, or a similar embedded OS to see how they do it. \$\endgroup\$
    – Daniel
    Commented Mar 6, 2018 at 17:40
  • 5
    \$\begingroup\$ See also wiki.osdev.org/Main_Page \$\endgroup\$
    – user16324
    Commented Mar 6, 2018 at 17:44
  • 1
    \$\begingroup\$ An OS is just a bare metal program! When trying to write an OS on a PC you have to jump through all sorts of hoops to get your bare metal code to run. But on a microcontroller you already have that ability by design, there are no hoops! You should only need the exact same linker scripts, etc. that you'd need for any other bare metal code (which is hopefully none of them). \$\endgroup\$ Commented Mar 6, 2018 at 23:04

3 Answers 3

7
\$\begingroup\$

Here are some things that spring to mind right away.

  1. You must know all of the details of what happens when the processor comes out of a power-on reset. There will be a number of control registers and these have default values. You need to know these, down cold.
  2. You must know all of the CPU architecture details. All of the registers, special modes using them, etc. These things will give you clues about how to arrange the registers for calling and returning from functions, etc. If you are using C, or want to be compatible with C, you will have to read about the choices made by various C compiler vendors (not always the same choices.)
  3. You must know all of the details regarding various functional units and library code that you will permit to be linked in. Particularly so, if you will support pre-emption. In some cases, you cannot allow pre-emption since there is no way to save the state of some operations that are in progress (the MSP430 multiplier is like this) despite the fact that an interval timer can interrupt that process. In some cases regarding libraries, they also cannot be interrupted since they have static state that would be corrupted by a separate thread accessing the same code. Etc.
  4. Your startup code will have to be linked to the power-on reset address for the MCU. It's job is to do whatever initialization is promised. This might be tabulating memory areas, initializing some memory, and whatever else will be expected in some known state prior to the execution of the first thread/process. If C is involved, this means initializing all initialized static variables per their appropriate values and initializing all other static variables to their semantic equivalent of 0, whatever that may be.
  5. You need to know all of the ways that exceptions can take place and how to differentiate, for example, between a watchdog timer event vs a power on reset event.
  6. The linker input file merely lays out the areas where code and initialized data may be placed and how big they are. It also uses names so that the linking process can assign named code or named initialized data to the appropriate places as indicated in the linker input file. This isn't complicated. But the details do have to be correct. If you are using a C compiler, that C compiler will probably make all kinds of assumptions about the names of sections (segments) and you will have to either obey them or else write a tool that modifies the object code before linking.
  7. You may need to write a special tool for linking, anyway. This can happen if you have to support multiple C compilers generating different named segments in their object files; or an assembler output that also uses differing names; etc. Sometimes, the routine prologues and epilogues don't make the same assumptions, even, and you may need to patch those things prior to linking. You may also need to write a patching tool, anyway, to patch or "fix up" addresses that are specified in the object files prior to linking. (I've had to do this with 6502/65816 processor code destined for ROM file production with weird memory-mappers that are different and also incompatible with each other.)

And that's just a very short list that flows from my fingers without thinking about it. I'm sure that if I spent another 5 minutes, I could double this list. (For example, I've not even addressed anything about what a debugger might want in terms of information and/or modified or inserted code to support its operations. Nor have I discussed the differences involved in Harvard vs von Neumann memory systems. Nor have I discussed the standard "program model" of organization [code; constants; init data; uninit data; heap; stack, etc.])

I'd recommend taking this in slow steps. Since you claim to know already about bare metal programming and know about operating systems, as well, let me simply recommend that you read the very first XINU book by Douglas Comer (it is circa 1984, has a red cover and there is no volume 2, etc.) Then see if you can cobble up a cooperative switching operating system. This means NO PRE-EMPTION. It means just THREADS -- not complete, separate processes -- but threads that share the same code space, constants, static data, and heap; with the only difference being that they have separate stacks. Support a switch() call to make this cooperative thread switching work. It must also support hardware driver events without accidentally overflowing some thread's stack in the process. You need to carefully design how you handle hardware events and their driver code. (I split this into low level hardware response code + high level thread-accessible code separated by buffers to decouple the two from each other so they can operate independently, while also supporting the hardware fully.)

If you want to go the next step, add inter-thread messaging. Use a simple word -- just one word -- for each thread and let any other thread write it. Overwrite it, in fact. Don't make this complicated. See if you can get single word messages going between processes okay.

Then add a sleep queue and provide a timer that can move threads from the sleep queue back onto the run queue.

Then add semaphore queues.

If you can get this far, you will have learned a great deal.

And by the way, it took me less than two working days (Monday, until early Tuesday afternoon) to get all of the above working -- from scratch and without a single line of code from prior projects, so just typing as fast as I could think -- including a complete mixture of assembly and C to handle hardware events and so on. I had run/sleep/semaphore queues, timers, and co-operative threads along with per-thread exception handling added ... in less than two days.

So this is NOT a particularly difficult task ahead.

Have at it.

\$\endgroup\$
5
  • 4
    \$\begingroup\$ It certainly is a difficult task, let's not trivialise it, and will end up taking much longer than two days but it's perfectly achievable which is the point I imagine you're making. Excellent detailed answer, upvoted. Great to see your thrill and satisfaction of climbing that mountain burning just as bright after the years in your words :-) I still treasure getting my first going, a Z80 pre-empter on a 64180. \$\endgroup\$
    – TonyM
    Commented Mar 6, 2018 at 21:05
  • \$\begingroup\$ @TonyM The XINU book provides the basics in as easy to understand form as I've seen in a book. Yes, it will take a new person much longer than two days!! Mostly, I just wanted to point out that they should NOT attempt pre-emption, at first. Keep it simple. Just create separate stacks, maintain a small array, provide a very simple switch() function, and have fun. Add sleep when ready to deal with starting and using a timer (but still don't pre-empt.. just move threads only.) Then add messaging (easy) and later after some fancy semaphore queues. That goes a long way of being useful, I think. \$\endgroup\$
    – jonk
    Commented Mar 6, 2018 at 22:28
  • \$\begingroup\$ I agree and it's great to see your experience, encouragement and enthusiasm on it, very valuable to the OP. I was expecting the usual chorus of 'buy one in, idiot' from the 'make nothing, learn nothing, be proud' merchants that chip in on these things. Your 'try it' is by far the right attitude and you can have a bash at it for such little cost these days. \$\endgroup\$
    – TonyM
    Commented Mar 6, 2018 at 22:54
  • \$\begingroup\$ I have already done this in LabView and Javascript and assmebler. LabView made it easy to stack priority queues and task enables and blocks (executive branch). Then there is inter-task sharing and triggers. Stick with the rules of logic, and it will work just fine. \$\endgroup\$
    – user105652
    Commented Mar 6, 2018 at 23:41
  • 1
    \$\begingroup\$ @TonyM I was consulting at a small company with 1 programmer. His code was a rats' nest and didn't work well, besides. I convinced him to try to write a cooperative switcher (I wasn't contracted to do that for him.) So I sat down and explained how he could approach it. He'd NEVER done anything like this before. I think it wasn't more than two days later he ran into my office. He'd done it!! And it was working. I looked it over and was happy! That company's software got a lot better, soon after. He had ASM experience, so it was easy to teach. He just hadn't been exposed to the ideas, is all. \$\endgroup\$
    – jonk
    Commented Mar 7, 2018 at 0:10
2
\$\begingroup\$

Most microcontrollers aren't really appropriate for this. You want more of a general purpose microprocessor.

Some high end 32 bit micros like ARM and PIC32 might let you write a reasonable operating system. For a general operating system, you need a processor that can run user code in a way that the user code can't harm the system. This is usually done by having a priviledged mode and a user mode. Most micros don't have this.

Another problem is that most micros only execute out of ROM, not RAM. That makes loading arbitrary user code, then running it difficult or impossible. The total RAM and ROM space is also usually fixed, and doesn't and can't be extended externally. That doesn't prevent a operating system, but will make the applications the system can run rather limited. Most micros don't have MMUs that can remap real to logical memory addresses, and cause traps on attempt to access certain memory. That makes paging difficult or impossible.

Check out the PIC32. It can execute from RAM, and has at least a basic MMU. Some variants have enough memory to make useful user-mode programs possible.

As for the details of linking, you really have to read the manuals for the tools. There is no substitute for understanding what the linker does and how to control it. You have to RTFM. There is no shortcut.

\$\endgroup\$
2
  • \$\begingroup\$ * RTFM = Read The Fine Manual \$\endgroup\$
    – user20088
    Commented Mar 6, 2018 at 22:38
  • \$\begingroup\$ Olin Lathrop, my target platform is the TI TM4C1294 Connected Launchpad (EK-TM4C1294XL), which is based on ARM Cortex-M4F (32 bits). The hardware offers good support for an OS. \$\endgroup\$
    – user49894
    Commented Mar 6, 2018 at 22:52
0
\$\begingroup\$

I suspect it could be possible to implement a real-time operating system without any special knowledge or customization of the linker directive script and C run-time startup code.

The linker directive script identifies regions and sections of memory. If the tool vendor provides a default linker script file that works for your bare-metal applications then it could work for your RTOS-based application too. You would customize the linker script if you your application has specialized memory requirements. Examples include an application for a custom board with external memories, an application with specialized memory sections that require specialized initialization, and an application that works in conjunction with a boot loader program. I can't think of a reason why use of an RTOS would force you to customize the linker script. Knowledge on how to customize the linker script is important for any specialized application requiring it regardless of whether an RTOS is used.

The startup code initializes the C run-time environment for your application. If the tool vendor's default startup code is good enough for your bare-metal application then it might be good enough for your RTOS-based application as well. The startup code copies initialization values from ROM to RAM and zeroes out uninitialized data. (For C++ it calls the constructors for statically allocated objects.) You would customize the startup code if you have specialized memory sections that require special initialization. You might also customize the startup code if your board has special hardware initialization that should be performed before the run-time environment initialization (or before main()). I can't think of a reason why using an RTOS would force you to customize the startup code. The RTOS can be initialized from main() (after the startup code is complete). Knowledge on how to customize the startup code is important for any specialized application requiring it regardless of whether an RTOS is used.

However, you will want to know all about the CPU registers and how interrupts are handled by the CPU. You'll need to know which CPU registers should be saved/restored to switch contexts. And interrupts are a prime opportunity for when an RTOS context switch occurs.

In addition to the FreeRTOS documentation, check out the uC/OS-III books from Micrium.

\$\endgroup\$