Go Down

Topic: Mark3 - New RTOS for AVR, ported to mega328p Arduinos! (Read 13410 times) previous topic - next topic

moslevin

Aug 14, 2013, 05:38 am Last Edit: Oct 17, 2014, 02:53 am by moslevin Reason: 1
(Edit: Added updated version of Mark3, and updated the URL for the website)

Hello!  Long time Arduino (and vanilla AVR) user, first time poster.

To introduce myself...  In 2008 I developed FunkOS for AVR and MSP430 microcontrollers (you can still find it on sourceforge).  That project saw modest success on its own- I used it in a bunch of personal projects, but really, it was more of a learning exercise to help me get into the world of RTOS development.  

While FunkOS was unofficially discontinued in 2010, I never stopped playing around with RTOS concepts in my spare time, and I've spent countless hours since then researching various OS concepts, and learning how to implement features found in high-end commercial offerings.

Over the last year, I've been working on something brand new which I am incredibly proud of - and hope it might be of interest to the Arduino community as well:  

Mark3 is an open-source, object-oriented RTOS built from scratch, written entirely in C++.  

Features of Mark3 include:
-Extremely easy-to-use API; Kernel initialization with two threads in six lines of code.
-Fully-deterministic scheduler with 8 priority levels and *unlimited threads per priority (* number of threads limited by available RAM)
-Round-robin scheduling within each priority level, with customizable thread quanta
-Tickless/Asynchronous kernel design
-Unlimited number of high-precision timers and thread-sleeps (thanks to tickless kernel design)
-Binary & Counting Semaphores
-Mutexes with priority inheritance and recursive lock support
-Robust IPC based on message queues
-High-resolution profiling timers
-Device driver API
-Interrupt-driven, buffered UART driver for AVR
-Meticulous documentation, including a manual integrated with the doxygen docs (including HTML and a 420-page PDF)
-Full kernel validation test suite, including unit-tests and cycle profiling tests
-Large set of examples

While the kernel is the party-piece of Mark3, I've also developed a fairly large library of middleware to support it, including:
-Thread-safe fixed-block heaps
-Lightweight string/memory-manipulation library
-Raster graphics library including bitmap font rendering, and font creation utilities
-Event-driven GUI framework, including a base set of widgets
-Yet another bootloader
-Shell support library
-Filesystem development framework (Nice Little FileSystem - NLFS)
-A DCPU-16 virtual machine
-Support for PSX joysticks
-HID and graphics devices over serial

Some of the middleware needs a bit more work (or at the very least, examples), but the kernel itself is rock-solid and good to go.  At this point, I would have no reservations about putting it up against the likes of FreeRTOS, uCOS, or ThreadX any day of the week.

And, because of how it's been designed (C++/GCC/AVR), porting it to ATMega-based Arduino was trivial - I basically copied the source and headers into a folder, added a keywords.txt file, and that was that.

While I intend for Mark3 to be a stand-alone offering for AVRs, I've become quite interested in making (at least) the kernel available as an Arduino library.  Aside from myself, would anybody be interested in seeing this code make its way over?

In the meantime, if anyone is interested in checking out the code as-is, you can check out the SVN repo from google code at http://www.mark3os.com

EDIT:

Alright, since it was so easy to port I just went ahead wrote a script to cut Arduino releases.

See the attached zip file containing the Mark3 library.  Simply unzip and copy the Mark3 folder to the libraries folder in your Arduino install path.

The zip contains the kernel source, along with full PDF and HTML documentation for the kernel (taken from the doxygen docs).

There are some good examples in the docs, but here's a quick one showing how to configure the kernel with two threads, performing thread sleeps, and using the UART to alternate printing of the words "Off" and "On" every 200 ms.

Code: [Select]

#include <kernel.h>
#include <thread.h>

Thread idle_thread;
Thread app_thread;

uint8_t app_stack[192]; // buffers used as stacks for threads
uint8_t idle_stack[192];

void idle_func(void *param_);
void app_func(void *param_);

int main(void)
{
 Kernel::Init();
 
 app_thread.Init(app_stack, 192, 1, app_func, 0);
 idle_thread.Init(idle_stack, 192, 0, idle_func, 0);
 
 app_thread.Start();
 idle_thread.Start();

 Kernel::Start();

 // !! Kernel::Start() Does not return, it starts the threads
 return 0;
}

void app_func(void *param_)
{
   // application thread code goes here
   Serial.begin(57600);
   while(1)
   {
     Thread::Sleep(200);
     Serial.write("On\n");
     Thread::Sleep(200);
     Serial.write("Off\n");
   }
}

void idle_func(void *param_)
{
   // idle thread/LPM code goes here
   while(1)
   {

   }
}




MarsWarrior

Nice work moslevin!

I like those little RTOSses. They make programming separate functionality so easy! I'm currently using the by fatlib16 ported versions of ChibiOS and NilRTOS on a few Arduino's and I will give Mark3 certainly a testdrive on one of them!

Out of curiosity, I just downloaded the zip file, browsed through the files & documentation and have a couple of questions. I hope there not in these files... :smiley-red:

  • You did te port to Arduino. Does it work for all Arduino's, ie 328/32u4/1284p, etc?

  • I seem to miss (or overlooked) some kind of support for events where on thread or ISR can set an event where multiple threads are waiting for.

  • I see you have support for a tickless kernel. Does that work also on Arduino, and how did you implement that?
    I like that feature a lot for battery powered devices that respond on either a timer (do some work every 3-minutes for instance) or an external interrupt (INT0 etc.) or a port change interrupt. Related to this: does the kernel go to a deep sleep to save power?? Is that configurable?? (as a port change interrupt doesn't work in deep sleep)

  • You use some different naming compared to what I'm used to. I had to read the documentation for instance to understand the Post/Pend (iso Signal/Wait). Same for the mutexes (no Lock/Unlock). Nothing serious, just an observation  :D

  • You did write your own bootloader: is that one also required on Arduino's?

.
Keep up the good work!

moslevin

I'll do my best to answer your questions inline:

Quote
You did te port to Arduino. Does it work for all Arduino's, ie 328/32u4/1284p, etc?


The kernel I attached should work with the 328p based Arduinos.  My kernel also presently supports 644, 1284p, and some of the xmega parts as well.  Adding 1280/2560 support would also not be difficult.  I haven't played around with the USB parts, but porting would just be a matter of refactoring the timer and interrupt code.

Quote
I seem to miss (or overlooked) some kind of support for events where on thread or ISR can set an event where multiple threads are waiting for.


Message queues can be used for that.  Multiple threads can block on the same queue, and interrupts/threads can write to those queues (although, only one thread will take the message).  If you're thinking more of a "broadcast" IPC mechanism, that isn't implemented in Mark3, but it would be super-easy to add.

Quote
I see you have support for a tickless kernel. Does that work also on Arduino, and how did you implement that?


The "tickless" part is baked into the kernel, so that includes support on Arduino.

As for how it works - the short answer is that the timer is only active when there are active timers to process. 

The timer-scheduler maintain a list of all active kernel timer objects, and determines the next-to-expire timer.  From there, it configures the timer to interrupt when that time occurs.  On the interrupt, we subtract that interval from all of the timers in the list and trigger callbacks for everything that's expired.  In the event that the interval is > the range of the timer, we'll interrupt on rollover and subtract that time from each timer's interval - in the default configuration on atmega328p, that means we process the rollover interrupt every 2.X seconds.  We could increase that time at the expense of timer resolution (i.e. adjusting timer prescalars).  On systems with 32-bit timers, you'd be able to go hours or even days between timer interrupts.  Hope that all makes sense.

There's a lot involved to ensure that "overtime" cases are handled properly, that the callback-processing-time doesn't impact the other timers, etc.  Really, the logic involved is really more complex than the rest of the kernel combined. 

Quote
I like that feature a lot for battery powered devices that respond on either a timer (do some work every 3-minutes for instance) or an external interrupt (INT0 etc.) or a port change interrupt. Related to this: does the kernel go to a deep sleep to save power?? Is that configurable?? (as a port change interrupt doesn't work in deep sleep)


We typically need to keep timers and interrupts active, so deep sleep isn't supported by the kernel explicitly - although there are likely ways to do so, depending on the application.  Power saving modes are generally left to the user to implement from within the idle thread, although some examples of typical usage are given in my google code repo.

Quote
You use some different naming compared to what I'm used to. I had to read the documentation for instance to understand the Post/Pend (iso Signal/Wait). Same for the mutexes (no Lock/Unlock). Nothing serious, just an observation  :D


Some of the RTOS' I used in the past used the Post/Pend terminology, and I guess it stuck ;)

Quote
You did write your own bootloader: is that one also required on Arduino's?


You don't have to use my bootloader (although it *is* nice).  I particularly like it because my flashing utility is written in .net and will run cross-platform, but that's just me ;)

moslevin

#3
Sep 01, 2013, 02:29 am Last Edit: Oct 17, 2014, 02:59 am by moslevin Reason: 1
Because there's only a handful of RTOS's on the front page of this forum, I've decided that now is an entirely appropriate time to cut a first release.  This contains everything I had posted in my last update of the thread, plus a new Event Flag object, which (as requested) can be used to trigger multiple threads simultaneously from another thread or an interrupt.

Given there's so many options out there for kernels on Arduino, I don't know how to convince anyone to actually use this, so I'll just put it out there with my own shameless endorsement:

Mark3 is a seriously good, free, commercial-quality RTOS kernel.  I've written a lot of kernels over the years (and seen a lot of really ugly kernel code), and I wouldn't have written Mark3 if I didn't think I could write something cleaner, more-free, easier to use, more powerful, or better documented than any of the other options available.

So hopefully that's gotten a few people curious at least - here's the direct link to the Arduino release of the kernel for mega328p based boards:

http://mark3.googlecode.com/svn/files/Arduino_r1_2013_08_31.zip

And if you're interested in the other middleware and drivers provided by the platform outside of just the kernel, here's a link to the "regular" Mark3 release.

http://mark3.googlecode.com/svn/files/Mark3_R1_2013-08-31.zip

Please see the updated version at the beginning of this thread, or the code hosted on www.mark3os.com


xortan

I know this thread is over a year old but...didn't know where to post this. There also is no other posts regarding this RTOS on the forum it seems...

I'm wondering if anyone has tried Mark3 and gotten it working?  I could really use this for my project I'm working on but when I get errors like:

sketch_oct15a.ino: In function 'int main()':
sketch_oct15a:17: error: no matching function for call to 'Thread::Init(uint8_t [192], int, int, void (&)(void*), int)'
                         thread.h:83: note: candidates are: void Thread::Init(uint32_t*, uint16_t, uint8_t, void (*)(void*), void*)

From the example you gave in this post it makes me wonder if getting this RTOS to work on an Arduino based Atmega328 is as trivial as you say it is.

moslevin

Thanks for giving Mark3 a try!

So - I see what's happening here.

When I ported Mark3 to ARM, I had to abstract out a couple of data-types (K_ADDR and K_WORD) that have different sizes on the various targets.  These values get set properly when building from command-line using the Mark3 build scripts, but they need to be manually set if building through Arduino or another IDE.

This should be a quick fix -- you can #define these types explicitly in kerneltypes.h.

Open that file and look for the following block:
Code: [Select]

#if !defined(K_ADDR)
    #define K_ADDR      uint32_t
#endif
#if !defined(K_WORD)
    #define K_WORD      uint32_t
#endif


Replace that block with the following:
Code: [Select]

#define K_ADDR        uint16_t
#define K_WORD        uint8_t


Try building again -- if you still run into issues, I'm around to help :)

xortan

Hello,

Thanks for the reply! I have made the changes you suggested and new errors emerge!  :)

thread.cpp:In member function 'void Thread::Init(uint32_t*, uint16_t, uint8_t, void (*)(void*), void*)'
                thread.cpp:61: error: cannot convert 'uint8_t*' to 'uint32_t*' in assignment

threadport.cpp:In static member function 'static void ThreadPort::InitStack(Thread*)'
                threadport.cpp:85: error: cannot convert 'uint8_t*' to 'uint32_t*' in assignment

I feel like we're getting closer lol

moslevin

I have a feeling I just need to make some tweaks to the Arduino-specific release.  Judging from the errors you're seeing now, this sounds like something I fixed a long time ago.

I'm going to double-check the latest version of code in the stable branch, and cut another Arduino-specific version.

Note that you can always find the most up-to-date releases from the official Mark3 website at www.mark3os.com, or alternately from http://sourceforge.net/projects/mark3/.

We'll get this working :)

moslevin

I've updated the version of Mark3 attached to the first message of the thread. 

I downloaded a clean install of Arduino, a clean copy of the new Mark3 zip, and copy-pasted the example into the sketch, and it all worked for me - so hopefully this should be click and play for you.

Robin2

I've just noticed this Thread today for the first time.

It seems a lot more complicated than just using millis() as in the demo several things at a time

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

xortan

Thanks for helping me with this! Hmm weird I still get the same errors...in kerneltypes.h K_WORD and K_ADDR are indeed defined as a 16bit and 8bit.

In thread.cpp it is these lines that seem to be giving me the problem...

    // Initialize the thread parameters to their initial values.
    m_pwStack = pwStack_;
    m_pwStackTop = TOP_OF_STACK(pwStack_, usStackSize_);

AND

    // Set the top o' the stack.
    pclThread_->m_pwStackTop = (K_UCHAR*)pucStack;

...I'm using the latest version of Arduino and the board I'm developing on is a Duemilanove w/ Atmega328....could this possibly be why I'm seeing these problems?

Quote
"I've just noticed this Thread today for the first time.

It seems a lot more complicated than just using millis() as in the demo several things at a time

...R"


Apples and oranges man.




moslevin

Hmm...  Weird.  I've been testing Arduino on Linux without any warnings or errors.  I'll try on Windows next chance I get.

I don't see anything wrong with those lines -- from the updated lib, TOP_OF_STACK() casts to K_UCHAR*, and K_UCHAR* == uint8_t* == K_WORD*.

You could try changing the explicit casts in those lines (and the macro) to K_WORD* instead of K_UCHAR*, and see what happens.

moslevin

Just as an update - I've done a clean install of Arduino IDE 1.0.6 on Windows.  With the newest Mark3 library .zip  + sample sketch in this thread, I'm able to get a clean, working build.

Now, I manually extracted Mark3 into the "libraries" folder and restarted Arduino IDE before copy/pasting the code into a fresh sketch (as opposed to importing the library through the IDE), so maybe there's something there...

Try manually removing/adding the library, and starting with a fresh sketch -- I have a feeling this is an environmental issue, since I'm able to get things up and running on a clean install.

moslevin


I've just noticed this Thread today for the first time.

It seems a lot more complicated than just using millis() as in the demo several things at a time

...R


I would expect that the majority of code written on AVRs would use some form of event-driven programming model like this, based on a super-loop.  You can do a lot of useful work with a framework like that, and there's certainly nothing wrong with developing to that.  The main benefit of a framework such as in your example (as I see it), is that it provides fairly sophisticated cooperative multitasking with only a very small initial cost (albeit an incremental cost) -- without having to introduce large amounts of supporting code at a larger, fixed cost.

However, I would contend that as the complexity of a super-loop application grows, the "small incremental costs" in support code that you need to write to maintain this system grows as well -- especially once you start introducing dependencies between tasks, I/O, and external events.  Synchronizing access to shared resources between cooperative tasks can become more and more challenging, forcing developers to design workarounds and maintain increasingly-complex state machines to manage these interactions.

At some point there's a break-even point, where the amount of code you have to write for an application to support cooperative multitasking outweighs the fixed-costs of using an RTOS.  And at that point, you also get to leverage all of the benefits of an RTOS as well (threads, deterministic behavior, virtually unlimited timers, power management, consistent APIs, etc.). 

Anyway - I've written my fair share of firmware, both with and without the use of an RTOS.  My advice would be to just use the best tools for the job, and use tools you can use effectively.  :)



Robin2


However, I would contend that as the complexity of a super-loop application grows, the "small incremental costs" in support code that you need to write to maintain this system grows as well -- especially once you start introducing dependencies between tasks, I/O, and external events.  Synchronizing access to shared resources between cooperative tasks can become more and more challenging, forcing developers to design workarounds and maintain increasingly-complex state machines to manage these interactions.


Sorry if I seem to be throwing buckets of cold water ...

This sounds to me like big-computer-think.

I just can't get my head around the idea that there could be so much going on in an Atmega 328 with 2k of SRAM.

But, hey, if everyone thought the same there would be only one make and model of car.

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Go Up