Lecture 3

Nikolaus Huber

Outline

  • RTOS Basics
  • Zephyr

Realtime operating systems

Scheduling - Round Robin

Scheduling - Preempt / Coop

Zephyr Kernel

“The Zephyr kernel lies at the heart of every Zephyr application. It provides a low footprint, high performance, multi-threaded execution environment with a rich set of available features. The rest of the Zephyr ecosystem, including device drivers, networking stack, and application-specific code, uses the kernel's features to create a complete application.”
Zephyr Documentation

Zephyr threads

“A thread is a kernel object that is used for application processing that is too lengthy or to complex to be performed by an ISR”
Zephyr Documentation
  • Any number of threads can be defined (limited by RAM)
  • Important parts of a thread
    • Thread ID
    • Stack area (private to each thread)
    • Entry point function (3 arguments)
    • Scheduling priority
    • Start delay
    • (Execution mode)

Thread states

Thread priorities

Zephyr thread anatomy


			void thread (void * arg1, void * arg2, void * arg3)
			{
				/* Thread init */ 

				for(;;)
				{
					/* Thread body */
				}
			}
		

Spawning a thread (dynamically)


			#define MY_STACK_SIZE 500
			#define MY_PRIORITY 5

			extern void my_entry_point(void *, void *, void *);

			K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
			struct k_thread my_thread_data;

			/* in main () */
			k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack_area,
											K_THREAD_STACK_SIZEOF(my_stack_area),
											my_entry_point,
											NULL, NULL, NULL,
											MY_PRIORITY, 0, K_NO_WAIT);
		

Spawning a thread (statically)


			#define MY_STACK_SIZE 500
			#define MY_PRIORITY 5

			extern void my_entry_point(void *, void *, void *);

			K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
							my_entry_point, NULL, NULL, NULL,
							MY_PRIORITY, 0, 0);
		

Terminating a thread


			void my_entry_point(void * arg1, void * arg2, void * arg3)
			{
				for(;;) 
				{
					if (/* some condition */) { return; }
					/* ... */
				}
				/* thread terminates here */
			}
		
  • Usually, threads run forever
  • Use with caution!
  • No data is released automatically at termination!

More information on threads

https://docs.zephyrproject.org/latest/kernel/services/threads/index.html

Inter-task communication

  • Tasks often need to communicate / exchange data
  • Same problem we have already seen with ISR - main communication
  • Two general paradigms
    • Shared memory
    • Message passing

Shared memory

  • Tasks share memory
    • global / static variables
    • same heap location
  • Susceptible to common pitfalls
  • Access needs to be carefully managed

Example - data race


			int sharedData; 

			void task1(void * a1, void * a2, void * a3)
			{
				for(int i = 0; i < 1000000; i++)
				{
					sharedData = sharedData + 1; 
				}	
				k_sleep(K_FOREVER);  	
			}

			void task2(void * a1, void * a2, void * a3)
			{
				for(int i = 0; i < 1000000; i++)
				{
					sharedData = sharedData + 1; 
				}	
				k_sleep(K_FOREVER); 	
			}

		

Solutions

  • Use cooperative tasks (i.e. tasks that cannot be preempted)
    • Must make sure that tasks abort or sleep forever
  • Lock interrupts before accessing shared data
    • Potentially increases response time of the system to important events
  • Use a Mutex

Mutex

  • Allows multiple threads to safely share both hardware and software resources
  • Ensures mutually exclusive access to a resource
  • General idea
    • We request (lock) a mutex before entering a critical section
    • We return (unlock) a mutex when we exit the critical section
Adapted from https://tilics.dmi.unibas.ch/mutex

Mutex in Zephyr


			int sharedData; 

			K_MUTEX_DEFINE(my_mutex); 

			void task1 (void * a1, void * a2, void * a3)
			{
				for(int i = 0; i < 1000000; i++) 
				{
					k_mutex_lock(&my_mutex, K_FOREVER); 
					sharedData = sharedData + 1; 
					k_mutex_unlock(&my_mutex); 
				}
				k_sleep(K_FOREVER); 
			}
		

Mutex pitfalls - priority inversion

  • A low priority L task locks a mutex
  • L get interrupted by higher priority task H
  • H tries to lock mutex
  • L continues, is interrupted by medium priority task M
  • H has to wait for M to finish => priority inversion
  • Solution => Priority inheritance
    • Raise priority of L to same as H

Mutex pitfalls - deadly embrace


			K_MUTEX_DEFINE(mutex_A); 
			K_MUTEX_DEFINE(mutex_B); 

			void task1() {
				k_mutex_lock(&mutex_A, K_FOREVER); 
				k_mutex_lock(&mutex_B, K_FOREVER); 
				/* ... */ 
				k_mutex_unlock(&mutex_B); 
				k_mutex_unlock(&mutex_A); 
			}

			void task2() {
				k_mutex_lock(&mutex_B, K_FOREVER); 
				k_mutex_lock(&mutex_A, K_FOREVER); 
				/* ... */ 
				k_mutex_unlock(&mutex_A); 
				k_mutex_unlock(&mutex_B); 
			}
		

Semaphores

  • Similar to mutex
  • Counting semaphore: can be "locked" multiple times
    • E.g. if multiple instances of a peripheral are available
  • Key properties:
    • Count: how often has the semaphore been taken
    • Limit: how often can the semaphore be successfully taken before we have to wait for another task to give the semaphore

Semaphore vs Mutex

  • Mutex is locked/unlocked, semaphore is taken/given
  • Mutex cannot be successfully locked multiple times
  • Mutex needs to be unlocked by the same task which locked it (i.e. not usable in ISR)
  • Semaphore can be taken / given by different tasks (or ISRs)
  • Semaphore usually used for signaling (producer - consumer) => Deferred interrupt handling
  • For transporting data from ISR to task either use
    • A semaphore
    • Higher level data structure (e.g. message queue)

Reentrant function

  • Function which executes correctly even when called by more than one task
  • Must follow certain rules:
    • Must not use any global variables (at least none that could change)
    • Must not call non-reentrant functions
    • Must not use hardware in a non-atomic way

Reentrant ?


			bool fError; 

			void display (int j)
			{
				if (!fError) 
				{
					printf("here"); 
					j = 0; 
					fError = true; 
				}
				else 
				{
					printf("also here"); 
					fError = false; 
				}
			}
		

Message passing

  • Using dedicated data structures - involves copying data
  • Looser coupling of threads
  • More high-level, often considered better programming style
  • Can be implemented in different ways, depending on the RTOS

Data passing in Zephyr

https://docs.zephyrproject.org/latest/kernel/services/index.html#data-passing

Zephyr RTOS

Zephyr architecture

Zephyr architecture

  • By having arch, soc, and board Zephyr can reuse a lot of code
  • If we want to create our own board, we just need to add it in boards

Zephyr project anatomy

  • project folder
    • CMakeLists.txt
    • prj.conf
    • src
      • main.c

Application configuration

  • By setting CONFIG values in prj.conf
  • Uses a tool called Kconfig
    • Initially developed for the Linux kernel
    • Can use tools like menuconfig or guiconfig to make temporary changes
    • Slowly being replaced by devicetree

Device tree

  • Problem: An OS like Linux must run on different HW platforms
  • For each platform, the right drivers must be available
  • Solution => Put the HW configuration into special data structure

Device tree

  • Hierarchical data structure (tree) that describes HW
  • Defined by independent specification
  • User specifies HW layout in a .dts file
  • For Linux: compiled to binary representation used during boot-up
  • For Zephyr: compiled to header file

DTS file


			/dts-v1/;

			/ {
					a-node {
							subnode_nodelabel: a-sub-node {
									foo = <3>;
							};
					};
			};
		

Example

Deferred interrupt handling

  • To keep the system reactive => fast interrupt routines
  • Use a semaphore in the ISR to unlock a task
  • Actual handling of "the event" happens in the task, not the ISR

Volatile

  • Whenever a variable is shared between two tasks, or a task and an ISR
  • C qualifier volatile
  • Tells the compiler that it cannot rely on previously read values
https://barrgroup.com/embedded-systems/how-to/c-volatile-keyword

Volatile example


		volatile bool global_var = false; 

		void main(void) 
		{
			while(!global_var)
			{
				/* Wait */
			}
		}

		void isr_routine(void)
		{
			global_var = true; 
		}
	

Thanks for today!