Link Search Menu Expand Document

Hornet OS

Hornet OS is more of a concept. It is an Operating System designed for Microcontrollers (MCU).

The application of an MCU is different than that of a regular CPU in many ways:

  1. MCU is more power sensitive. Some devices are required to run on battery for years.
  2. Though relatively slow and simple than regular CPUs, many applications are real-time and low latency.
  3. MCUs are resource constrained. Many are equipped with only dozens of KB of RAM and hundreds of KB of flash ROM.

A Brief History

In 2015, when I started the Hornet Mesh Network project, it appeared the best choice was FreeRTOS. I needed to build somthing on top of FreeRTOS to overcome some shortcomings.

Simple design

Hornet OS has the simplest design:

  1. It shall be tickless with the SysTick interrupt disabled. We believe SysTick is useless.
  2. The primary timer source shall be the low-power RTC timer (usually 32768Hz) because it is the only low-power timer source in the system.
  3. System shall have very few threads. Threads are preemptable only by priority. Preempting using the time slice should NOT be allowed.
  4. Applications on the OS shall be naturally “low-power,” meaning that most of the time, all threads are “sleeping,” the entire system is in a certain low-power mode waiting for either the hardware timer or other peripheral hardware pins to wake up. All “timed_wait” operations can depend on a single low-power RTC timer mentioned above.

    a. Core OS shall have two “wait/sleep” APIs:

     1. hornet_os_task_wait() - Current thread waits forever until explicitly woke up by another thread.
    
     2. hornet_os_task_timed_wait(uint32_t t)` - Current thread waits until the specified tick is reached or is explicitly awakened by another thread.
    

    b. hornet_os_task_notify(TaskHandle_t handle) - Wake up the specified thread.

    c. Two API calls that change an “always sleeping thread” timeout.”

     1. hornet_os_task_reschedule(TaskHandle_t task, uint32_t t) - Reschedule the timeout ticks of the specified "waiting" thread.
    
     2. hornet_os_task_cancel_timed_wait(TaskHandle_t task) - Cancel the timeout ticks of the specified "waiting" thread. The specified thread shall keep "waiting" forever.
    

Working with FreeRTOS and Zephyr

Kernel Timer Source

Currently, Hornet OS is built on top of FreeRTOS without modifying the kernel.

Zephyr has a timing framework that uses different timing sources on different hardware. If available, it chooses low power RTC timer as a timing source.

FreeRTOS, however, only depends on the old SysTick timer for its kernel timing. It will be nice for OS vendors to integrate this design into the OS kernel.

New Waiting/Sleeping API

Item 4. c above describes APIs that can change a waiting thread’s wait schedule from another thread. As far as I know, these two API calls are not found in any OS design.

That capability is very important for our Thing-App engine. They are used to monitor Thing-App timeout. A Thing-App shall “timeout” if it keeps running for a long time without responding. The monitoring thread is nothing but an infinite loop and a hornet_os_task_wait() call within that loop. And Thing-App thread is of lower priority than the monitoring thread.

Under normal conditions, the monitoring thread shall never wake up. When a Thing-App thread is context-switched to run, the monitoring thread shall be rescheduled with a timeout of the remaining “timeout time” of the Thing-App thread.

If the Thing-App thread responds in time by returning control to the Thing-App scheduler, then the timeout of the monitoring thread is canceled. The monitoring thread remains sleeping forever.

Suppose the Thing-App thread is context-switched out by a higher-priority thread. In that case, the Thing-App timeout will be recalculated (reduced by the last running time), and the timeout of the monitoring thread is canceled, too, because the monitoring thread only monitors the timeout of the Thing-App thread.

If the timeout of the monitoring thread is ever triggered, it means that the Thing-App is not responding in time, i.e., there is a timeout.

It will be nice for Zephyr to add these two APIs to the kernel.

One Hardware Timer Rules All!

Like Zephyr, Hornet OS is designed to only rely on a single hardware sleep timer.

For example, on battery-powered wireless devices, in the MAC layer’s CSMA-CA algorithm, in case a collision is detected while sending a packet and the device needs to wait and retry. If the wait time is long enough (i.e., worth extra saving energy), the OS will put the device into low-power sleep mode to further conserve battery power.

Programming Model

Each thread is either running or waiting. If all threads wait, the OS will put the device into a more energy-saving “sleep” state.

It is natural to treat each thread as an infinite loop. Each loop is a “poll.” During polling, the thread must decide the wait/sleep time at the end of the poll.

Note at the end of a poll; the thread may decide not to wait and perform another round of poll immediately. It is most likely due to some state change during the poll, so the thread needs to poll again to maintain the state machine.

I call this programming model the “poll-wait” model.

Deciding the Next Wakeup Time for Next Poll

There are two methods for deciding the “wait time” for a thread.

  1. Maintain a single “nextWakeUpTicks” variable. It is initialized to a maximum at the beginning of each poll. Within the poll, different parts of the code may call xxxx_thread_set_next_wakeup(uint32_t ticks) to change the next wakeup ticks. Only the value of “earliest future time” will be kept. For example, the function is called twice with thread_set_next_wakeup(now + 100) and thread_set_next_wakeup(now + 200). The second “now + 200” will have no effect; the thread will wake up at “now + 100” because it is the earliest future.

  2. Hornet OS provides the hornet_timer_t struct. It is a timer based on a priority heap. hornet_timer_t can be scheduled or canceled. A thread shall first drive the hornet_timer_t queue to trigger all expired timer callbacks before performing the “poll” operation.

The above two methods shall be combined to provide a powerful “wait” mechanism. At the end of each poll, a thread shall get “nextWakeUpTicks” and “next wakeup timer,” compare the expiration, and use the nearest future value to wait for timeout ticks.

There should be a third API that notifies a “force next poll” flag. If that API is called during the poll, the next poll is forced immediately. No wait timeout will be checked.

In the future, we may standardize the above APIs for each thread. But now, in Hornet implementation, only a special “main thread” defines those APIs.

  • hornet_os_main_thread_set_next_wakeup(uint32_t ticks) - Schedule wait time in ticks.
  • hornet_os_main_thread_notify_update() - Force next poll.
  • hornet_os_main_thread_timer_init(hornet_timer_t *timer, timer_fn fn) - Initialize a timer with a fixed callback function.
  • hornet_os_main_thread_timer_schedule(hornet_timer_t *timer, uint32_t expire_ticks) - Schedule the timer.
  • hornet_os_main_thread_timer_cancel(hornet_timer_t *timer) - Cancel the timer if it is scheduled.

Why not Just Always Use Timers

The protocol stack is a fairly complex state machine.

First, it is natural and easy to recalculate the entire state machine in every poll and schedule the next wakeup based on all timing requirements.

Secondly, some timing requirements are dynamic.

Finally, maintaining hundreds of timers may consume dozens of KB of extra RAM, and the code could be much more complex.

The “Idle” Thread

The “idle” thread has the lowest priority. When the “idle” thread runs, we know that all higher-priority threads are waiting (because otherwise, a higher-priority thread will always preempts the idle thread). The “idle” thread shall calculate the earliest timeout of all waiting threads, then use that earliest value to set the hardware low-power RTC timer.

Hornet OS shall provide a standard implementation of the idle thread. We implemented our own vPortSuppressTicksAndSleep() and the above APIs to “patch into” the OS scheduler.

High-Resolution Clock - sys_clock API

sys_clock is a hardware abstract API to get the current high-resolution ticks, which monotonically increases tick count with the low-power 32768Hz RTC timer.

Header File

The API is already included within hornet.h.

sys_clock Functions

extern uint64_t  sys_clock_ticks64();
extern uint32_t  sys_clock_ticks();
extern uint32_t  sys_clock_ms();
extern uint32_t  sys_clock_10ms();
extern uint32_t  sys_clock_100ms();
extern uint32_t  sys_clock_seconds();

Real-time Clock

Firmware developers can easily maintain a real-time calendar clock using sys_clock_seconds() and RTC API.

Design philosophy

  1. Efficient - IoT devices run on small MCUs with CPU and memory constraints. The OS has to be low latency/quick response with a low memory footprint.
  2. Simple - The programming has to be simple, with as few public APIs as possible. The programming model has to be the same on “big OSes” such as POSIX.
  3. Low Power - The same API must work on low-power battery-powered devices that keep running for years on batteries.

Benefit

  • Naturally low-power. Simple programming model.
  • High precision and preemptive thread scheduling. The low-power 32768Hz RTC timer offers much higher precision than the traditional SysTick timer (up to 30.5 us).