Zephyr RTOS Embedded C Programming
Zephyr RTOS Embedded C Programming
Embedded
C Programming
Using Embedded RTOS POSIX API
—
Andrew Eliasz
Zephyr RTOS
Embedded C
Programming
Using Embedded RTOS
POSIX API
Andrew Eliasz
Zephyr RTOS Embedded C Programming: Using Embedded
RTOS POSIX API
Andrew Eliasz
First Technology Transfer
Croydon, Surrey, UK
iii
Table of Contents
Counting Semaphore������������������������������������������������������������������������������������������35
Mutual Exclusions Semaphore (Mutex)���������������������������������������������������������������36
Priority Inversion Avoidance��������������������������������������������������������������������������������37
Using Semaphores and Mutexes in Interrupt Service Routines��������������������������38
Semaphore Usage Patterns and Scenarios���������������������������������������������������������38
Wait and Signal Synchronization������������������������������������������������������������������������38
Credit Tracking Synchronization�������������������������������������������������������������������������39
Synchronizing Access to a Shared Resource Using a Binary Semaphore����������40
Message Queueing and Message Queues����������������������������������������������������������41
Interlocked, One-Way Data Communication�������������������������������������������������������43
Interlocked, Two-Way Data Communication�������������������������������������������������������44
Pipes�������������������������������������������������������������������������������������������������������������������45
Event Objects (Event Registers)��������������������������������������������������������������������������47
Condition Variables���������������������������������������������������������������������������������������������48
Interrupts and Exceptions�����������������������������������������������������������������������������������49
Timing and Timers����������������������������������������������������������������������������������������������52
Memory Management�����������������������������������������������������������������������������������������53
Synchronization Patterns and Strategies������������������������������������������������������������56
Communication Patterns�������������������������������������������������������������������������������������58
Patterns Involving the Use of Critical Sections���������������������������������������������������59
Common Activity Synchronization Design Patterns��������������������������������������������59
Common Resource Synchronization Design Patterns�����������������������������������������61
Some More Advanced Thread Interaction Patterns���������������������������������������������62
Handling Multiple Data Items and Multiple Inputs����������������������������������������������65
Sending Urgent/High Priority Data Between Tasks���������������������������������������������66
Device Drivers�����������������������������������������������������������������������������������������������������66
References����������������������������������������������������������������������������������������������������������67
iv
Table of Contents
v
Table of Contents
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
x
Table of Contents
xi
Table of Contents
xii
Table of Contents
xiii
Table of Contents
xiv
Table of Contents
Index�������������������������������������������������������������������������������������������������653
xv
About the Author
Andrew Eliasz is the Founder and Head
at Croydon Tutorial College as well as the
Director of First Technology Transfer Ltd. First
Technology Transfer runs advanced training
courses and consults on advanced projects
in IT, real-time, and embedded systems.
Most courses are tailored to customers’
needs. Croydon Tutorial College evolved
from Carshalton Tutorial College, which
was established to provide classes, distance-level teaching, workshops,
and personal tuition in computer science, maths, and science subjects
at GCSE, A Level, BTEC, undergraduate, and master’s levels. It has now
changed its name and location to Croydon Tutorial College at Weatherill
House, Croydon. In addition to teaching and tutoring, they also provide
mentoring and help for students having difficulties with assignments and
projects (e.g., by suggesting how to add to a project to obtain a better grade
as well as reviewing project content and writing styles).
xvii
About the Technical Reviewer
Jacob Beningo is an embedded software
consultant with over 15 years of experience in
microcontroller-based real-time embedded
systems. After spending over ten years
designing embedded systems for automotive,
defense, and space industries, Jacob founded
Beningo Embedded Group in 2009. He has
worked with clients in more than a dozen
countries to dramatically transform their
businesses by improving product quality, cost, and time to market. Jacob
has published more than 500 articles on embedded software development
techniques and is a sought-after speaker and technical trainer who holds
three degrees that include a Master of Engineering from the University
of Michigan. He is an avid writer, trainer, consultant, and entrepreneur
who transforms the complex into simple and understandable concepts
that accelerate technological innovation. Jacob has demonstrated his
leadership in the embedded systems industry by consulting and training
at companies such as General Motors, Intel, Infineon, and Renesas along
with successfully completing over 50 projects. He holds bachelor’s degrees
in Electronics Engineering, Physics, and Mathematics from Central
Michigan University and a master’s degree in Space Systems Engineering
from the University of Michigan.
In his spare time, Jacob enjoys spending time with his family, reading,
writing, and playing hockey and golf. In clear skies, he can often be found
outside with his telescope, sipping a fine scotch while imaging the sky.
xix
CHAPTER 1
An Introduction
What This Book Is “All About”
This book is a foundational guidebook introducing programming
embedded and IoT/IIoT (Internet of Things/Industrial Internet of Things)
applications in C using the Zephyr RTOS framework. It is for engineers and
programmers planning to embark on a project involving the use of Zephyr
RTOS, or evaluating the potential advantages of using Zephyr RTOS in an
upcoming project.
You, the reader, probably have a digital electronics and embedded
systems programming background building specialized embedded
systems applications in C and assembler. Maybe the requirements of
upcoming applications are such that a classical bare metal programming
approach may not be the best way to go. Maybe you have inherited some
poorly documented complex multitasking code and the developers or
consultants involved in developing this code have left the project and your
company is considering migrating the code to use a real-time multitasking
operating system.
The aims of this book are to show you what Zephyr is capable of and
to introduce you to the basic RTOS programming skills required before
embarking on a real-world real-time RTOS-based project. The book can also
be thought of as a guide to the rich and complex framework that makes up
Zephyr RTOS and to the examples that are part of the Zephyr code repository.
2
Chapter 1 An Introduction
3
Chapter 1 An Introduction
What Is an RTOS?
The OS in RTOS stands for Operating System. An operating system can
be thought of as a collection of modules (libraries) that provide task
scheduling and control services, where a task is code that carries out a
4
Chapter 1 An Introduction
5
Chapter 1 An Introduction
6
Chapter 1 An Introduction
• Safety-oriented architecture
• POSIX-compliant C library
• Certification-friendly interfaces
The mission statement for Zephyr [1] is “to deliver the best-in-class
RTOS for connected resource-constrained devices, built to be secure
and safe.” The Zephyr RTOS website contains presentations describing
the various steps and approaches being followed that follow standard
procedures for developing and testing safety-critical systems software. These
include following the Verification and Validation aspects as formalized in
the V-Model of software development. A useful discussion held during Open
Source Summit Europe 2022 concerning these issues is worth viewing [2].
7
Chapter 1 An Introduction
8
Chapter 1 An Introduction
• IEC 61508
• ISO 26262
9
Chapter 1 An Introduction
An ideal project process that can combine the best aspects of open
source development and critical system certification will be one based
on a split development model having a flexible open instance path and
an auditable instance path [3]. Aligning the auditable path with the open
instance path will be dictated by the need to add new features and the
costs of the certification process.
10
Chapter 1 An Introduction
11
Chapter 1 An Introduction
• Thread isolation
• Stack protection (HW/SW)
• Quality management (QM)
• Build time configuration
• No dynamic memory allocation
• Funtional SAfety (FuSA) (2019)
Security features:
• User-space support
• Crypto support
• Software updates
Cross-platform capabilities:
• Zephyr supports multiple architectures (ARM Cortex
M, RISC-V, ARC, MIPS, Extensa).
• Native porting.
• Applications can be developed on Linux, Windows, and
macOS platforms.
12
Chapter 1 An Introduction
Open source:
Connected:
• Bluetooth controller
• BLE mesh
• Thread support
13
Chapter 1 An Introduction
14
Chapter 1 An Introduction
15
Chapter 1 An Introduction
16
Chapter 1 An Introduction
References
1. www.zephyrproject.org/wp-content/uploads/
sites/38/2022/02/Zephyr-Overview-2022Q1-
Public.pdf
2. Zephyr in Safety Critical Applications www.
zephyrproject.org/zephyr-in-safety-critical-
applications/
4. h ttps://docs.zephyrproject.org/latest/security/
security-overview.html
17
CHAPTER 2
A Review of RTOS
Fundamentals
Traditional embedded systems were stand-alone systems that were not
extensively networked to other systems. Communication, when present,
typically, was over a simple communications serial link such as RS232,
or HART. HART [1] is an industrial protocol that can communicate over
legacy 4–20 mA analog instrumentation current loops, but these days there
is also a wireless version of HART and there are HART modems that can
be used to run HART over RS232. Later specialized serial communications
such as SPI and I2C were developed for communicating with small
specialized devices such as, for example:
• Port expanders
• Digital-to-analog converters
20
Chapter 2 A Review of RTOS Fundamentals
Applications that previously used only buttons and LEDs and possibly
an LCD display for user interaction can, these days, communicate over
channels such as BLE, Zigbee, and LTE.
Adding such features, which superficially might appear quite
straightforward, actually places considerable demands both on the
underlying hardware as well as on software developers. One of the great
challenges faced by modern product and application developers lies in
being able to cope with and manage increased code size and complexity
and the need to provide feature-rich applications and deliver them with
tight time constraints.
Traditionally embedded systems code is developed in a linear fashion.
In such systems, real-time requirements are taken care of with
the help of on-chip and off-chip (board-level) components. As new
requirements and features are added, the ability to fine-tune the system
behavior becomes more and more limited, till a point is reached where a
complete redesign becomes necessary, or the implementation of fixes by,
for example, adding an extra microcontroller to the board. Fine-tuning,
tweaking, and adapting the application code pose serious problems as far
as code maintainability and code reusability are concerned.
A typical standard embedded systems approach uses a “big loop
design” supplemented with interrupts for dealing with “time-critical event
handling.” The basic pattern for this kind of programming is outlined in the
left-hand-side flowchart of Figure 2-1.
21
Chapter 2 A Review of RTOS Fundamentals
22
Chapter 2 A Review of RTOS Fundamentals
24
Chapter 2 A Review of RTOS Fundamentals
25
Chapter 2 A Review of RTOS Fundamentals
The reasons for selecting Zephyr RTOS for many projects in preference
to either AWS FreeRTOS or Azure RTOS ThreadX have already been
discussed previously.
Considerations for choosing to use an (RT)OS for developing
embedded system applications and the kind of applications that would
benefit from this have been discussed previously.
The remainder of this chapter will focus on general patterns of
multitasking (multithreading) and how these patterns can be implemented
using Zephyr RTOS. Wherever possible, the “official” Zephyr RTOS sample
code will be used to illustrate these patterns, adapted and elaborated on
where necessary.
The Zephyr RTOS code framework supports a wide range of
microprocessors and development boards. If you are working for a
company planning to develop a product that will be based on Zephyr
RTOS, then the chances are that a particular processor has already
been chosen, and maybe even a development kit/board to try initial
ideas out on.
Many developers are building products using Nordic Semiconductor
processors such as the nRF52840, or the nRF5340 or the nRF9160. These
processors have on-chip wireless peripherals such as BLE (Bluetooth Low
Energy) or LTE (Long-Term Evolution). The wireless technologies on the
chip depend on the particular processor chosen. Wireless technologies
such as BLE and LTE are widely used in many IoT (Internet of Things)
applications. Although this course will not go into applications involving
wireless networking in depth, if you will be working on such applications,
then it is a good idea to start gaining familiarity with a development system
that supports the wireless technology your application will be using.
All of the Nordic processors mentioned are ARM Cortex M devices. The
nRF52840 processor is an ARM Cortex M4 device, whereas the nRF5340
and nRF9160 processors are ARM Cortex M33–based devices. ARM Cortex
M33 (and ARM Cortex M23) processors are of great interest because
they can include support for cryptographic hardware extensions that can
26
Chapter 2 A Review of RTOS Fundamentals
27
Chapter 2 A Review of RTOS Fundamentals
Zephyr RTOS has also been ported to ESP32 devices and RISC-V
devices, and the examples covered in this course can be run, without too
much modification, on a variety of ESP32 and RISCV32 boards as well.
Instead of using an IDE such as a Microsoft VS Code–based IDE, it is
perfectly possible to develop and test applications using command-
line interface (CLI) tools and text-based editors for editing code and
configuration files. This is possible because the Zephyr RTOS software
development system includes a very effective CLI-based tool called west,
which can be used to drive the build and code flashing processes. The
Zephyr application building approach makes extensive use of CMake.
CMake, though very powerful, is associated with quite a steep learning
curve, and west hides the details working with CMake. The VS Code
IDE has a terminal window in which west commands can be run. The
use of west both within the VS Code IDE and at the command line will
be explained in the context of describing how to build the examples,
and further details describing west can be found in the corresponding
appendix in this book.
Although the various RTOSes mentioned have differing APIs
(Application Programming Interfaces), the underlying RTOS
components and their APIs are functionally and conceptually very
similar. Understanding the various behaviors, patterns, and uses of these
objects will provide a deeper understanding of how to build multitasking
embedded RTOS applications. It will also be useful when evaluating and
comparing different RTOSes and when porting an application from one
RTOS to another.
Zephyr, like other operating systems, makes use of OS abstraction
layers (OSALs). An OSAL provides wrapper function APIs that encapsulate
many of the common system functions provided by the underlying
operating system. OSALs supported by Zephyr are POSIX and CMSIS v2.
Full details of the features supported and how they differ in detail from
the corresponding POSIX and CMSIS v2 APIs can be found in the Zephyr
documentation [2].
28
Chapter 2 A Review of RTOS Fundamentals
29
Chapter 2 A Review of RTOS Fundamentals
• Name
• Unique ID
• A stack
• A task routine
30
Chapter 2 A Review of RTOS Fundamentals
31
Chapter 2 A Review of RTOS Fundamentals
32
Chapter 2 A Review of RTOS Fundamentals
ARunToCompletionTask()
{
InitialiseApplication;
Create a number of infinite loop tasks;
Create required kernel objects/resources
Delete/Suspend this task
}
InfiniteLoopTask()
{
Initialisation steps
Loop Forever
{
body of loop code which, typically
includes one or more blocking calls
}
}
Intertask Communication
Mastering RTOS programming requires an understanding of the various
intertask communication methods supported by the RTOS such as
mutexes, semaphores, queues, message queues, and workqueues.
Every RTOS provides semaphores and mutexes, which are the most
fundamental synchronization and communication mechanisms.
33
Chapter 2 A Review of RTOS Fundamentals
Semaphore
Formally, a semaphore can be described as a kernel object that one or
more threads of execution (tasks) can acquire or release for the purposes
of synchronization or mutual exclusion. The use of binary semaphores for
mutual exclusion is better avoided, and mutexes are used instead. This is
because in modern RTOS such as Zephyr, mutexes implement a priority
inheritance mechanism that prevents a priority inversion situation in
which a low priority process, in effect, blocks a higher priority process
because it holds a resource needed by the higher priority process. Another
important feature provided by modern mutexes is that they function
recursively, thus preventing certain deadlock situations from arising, as
will be discussed later.
When a semaphore object is created, the kernel assigns a data
structure – a semaphore control block (SCB) – to it as well as a unique ID,
a count value, and a task-waiting list. A semaphore can be thought of as a
key which a task must acquire in order to access some resource or other. If
the task can acquire the semaphore, then it can access the resource. If the
task cannot acquire the semaphore, then it must wait till some other task
releases it. From a programming perspective, there are two main kinds
of binary semaphores that can only have a value of 0 or 1 and counting
semaphores that can count over a greater range of numbers. A binary
semaphore can be thought of as a specialized counting semaphore.
Binary Semaphore
A binary semaphore can have only two possible values: 0 and 1. When a
binary semaphore is not held by any task, it has the value 1, and when a
task acquires a semaphore, its value is set to 0. No other task can acquire
the semaphore while its value is 0. In use, a binary semaphore is a global
resource – shared by all tasks that need it, and it is possible for a task
34
Chapter 2 A Review of RTOS Fundamentals
other than the task that initially acquired the semaphore to release the
semaphore. It is the choreography of semaphore usage among tasks that
makes semaphores effective.
Figure 2-5 depicts the behavior of a binary semaphore as an FSM
(Finite State Machine).
Counting Semaphore
A counting semaphore can be acquired or released multiple times. It does
this via a counter. If the count value is 0, the counting semaphore is in
the unavailable state. If the count is greater than 1, then the semaphore is
available. When a counting semaphore is created, its initial count can be
specified. In some operating systems, the maximum count value can also
be specified – that is, the counting semaphore is a bounded semaphore
(the count is bounded). In other operating systems, the count may be
unbounded. Acquiring a counting semaphore reduces the counter
value by 1, and releasing the counting semaphore increases the counter
value by 1.
The behavior of a counting semaphore is described by the FSM shown
in Figure 2-6.
35
Chapter 2 A Review of RTOS Fundamentals
36
Chapter 2 A Review of RTOS Fundamentals
37
Chapter 2 A Review of RTOS Fundamentals
38
Chapter 2 A Review of RTOS Fundamentals
lower priority task (the signalling task) now has a chance to run. At some
point, the signalling task releases the semaphore. The higher priority task
is now eligible to run, and it will preempt the lower priority task. The cycle
is repeated. The higher priority task sets the semaphore to zero. Then at
some later point in time, it tries to acquire the semaphore and will block
(because a binary semaphore is not recursive) and so the signalling cycle
can go around again once more.
tWaitTask()
{
...
39
Chapter 2 A Review of RTOS Fundamentals
tSignalTask()
{
...
Acquire counting semaphore
...
}
tAccessingTask ()
{
...
Acquire binary semaphore
Make use of the shared resource ( e.g. read or
write to it )
40
Chapter 2 A Review of RTOS Fundamentals
A potential risk with this approach is that a task that has not acquired
the binary semaphore may release it. And this is why a safer approach
is to use a mutex instead of a binary semaphore, as only a task that has
ownership of the mutex can release it.
A variant of this pattern for controlling access to multiple, equivalent,
shared resources involves replacing a binary semaphore with a counting
semaphore. In this case, great care must be taken to ensure that a task only
releases a semaphore it has actually acquired.
41
Chapter 2 A Review of RTOS Fundamentals
42
Chapter 2 A Review of RTOS Fundamentals
copies data into the buffer. The Zephyr API also provides a mechanism for
flushing data out of the buffer when it is full, thereby making it possible
to discard older (stale) data and release space for storing new data
(messages).
In practice, message queues can, by implementing the appropriate
application code, be used in various ways (protocols) such as
non-interlocked one-way communication, interlocked one-way
communication, and interlocked two-way communication. It is also
possible, with suitable extensions, to provide message queue functionality
with message broadcast capabilities. In the case of Zephyr RTOS, it is the
Zephyr mailbox that provides enhanced mail queue capabilities.
Interrupt service routines (ISRs) typically use the non-interlocked,
one-way data communication pattern in which the receiving task runs and
waits on a message queue. In this scenario, the ISR, when triggered, places
one or more messages on the message queue. This needs to be done in a
nonblocking way and consideration being given when implementing code
to the possibility that when the message queue is full, then messages may
be lost or overwritten.
43
Chapter 2 A Review of RTOS Fundamentals
receives the message and increments the binary semaphore, which will
unblock the sending task, which can then send the next message. In this
implementation, the semaphore is acting as a simple acknowledgment to
the sender that it is OK to send the next message.
The following pseudocode illustrates a one-way data communication
implementation pattern:
tSendingTask()
{
...
Send message to message queue
Acquire binary semaphore
...
}
tReceivingTask()
{
...
Receive message from message queue
Release binary semaphore
...
}
44
Chapter 2 A Review of RTOS Fundamentals
tClientTask()
{
...
Send message to server's requests message queue
Acquire binary semaphore
...
}
tServerTask()
{
...
Receive message from server's responses message queue
Release binary semaphore
...
}
Pipes
Pipes are kernel objects provided by operating systems such as Unix/Linux
and Windows and, also, Zephyr RTOS. A pipe implements a mechanism
for unidirectional stream-oriented data exchange. A pipe is associated
with two descriptors: one for the reading end and one for the writing end.
Data is held (buffered) in the pipe as an unstructured byte stream and read
from the pipe in FIFO (first in, first out) order. Synchronizing of the reader
and writer process involves the reader process blocking when the pipe is
empty and the writer process blocking when the pipe is full. In contrast
to a message queue, the data in a pipe is not structured, and there is no
mechanism for prioritizing data in a pipe.
Schematically the architecture of a pipe is like that shown in
Figure 2-10.
45
Chapter 2 A Review of RTOS Fundamentals
The creation of a pipe and its associated queues and control blocks
requires allocation of memory. Pipes can be created and destroyed
dynamically. The pipe control block, instantiated when the pipe instance
is created, contains pipe-specific information such as the size of the pipe
buffer, the amount of data (byte count) in the pipe, input and output
position indicators, as well as a list of tasks waiting to write to the pipe
(blocked because the buffer is full) and a list of tasks waiting to read from
the pipe (blocked when the buffer is empty).
Figure 2-11 outlines the behavior of a pipe mechanism.
46
Chapter 2 A Review of RTOS Fundamentals
47
Chapter 2 A Review of RTOS Fundamentals
Condition Variables
A condition variable is a kernel object that is associated with some
shared resource. It is used by one to wait until some other task sets the
shared resource to some specified condition. The condition is deduced
by evaluating some kind of logical expression (predicate). When a task
examines a condition variable, it must have exclusive access to that
variable, and hence, a mutex is used in conjunction with a condition
variable. The task must first acquire the mutex before evaluating the
predicate. If the predicate evaluates to false, the task blocks till the desired
condition is attained. The implementation is such that the operation
of releasing the mutex and block-waiting for the condition is an atomic
(indivisible) operation.
The following pseudocode snippet illustrates the common way of
using condition variables:
//Task 1:
Lock Mutex
Examine shared resource
While ( shared resource is busy )
WAIT ( condition variable )
Mark shared resource as Busy
Unlock Mutex
//Task 2:
Lock Mutex
Mark shared resource as Free
SIGNAL ( condition variable )
Unlock Mutex
48
Chapter 2 A Review of RTOS Fundamentals
49
Chapter 2 A Review of RTOS Fundamentals
50
Chapter 2 A Review of RTOS Fundamentals
51
Chapter 2 A Review of RTOS Fundamentals
52
Chapter 2 A Review of RTOS Fundamentals
Memory Management
In embedded systems with limited memory resources, dynamic memory
allocation and management should be avoided unless really necessary.
In this section, the essential aspects of dynamic memory management in
embedded system applications will be introduced. In a later section, an
example of working with the Zephyr RTOS memory management APIs will
be explored.
An embedded system such as Zephyr RTOS will provide some memory
management services – usually via system/library calls such as malloc()
and free(). A common problem with applications that make frequent,
varying memory size calls to malloc() and free() is that this may lead
to memory fragmentation. In its most basic form, dynamic memory
allocation takes place from a contiguous block of memory called the heap.
A memory management facility maintains information about the heap in
a reserved (control block) area of memory, which includes things such as
the start address and total size of the memory block, and will implement
53
Chapter 2 A Review of RTOS Fundamentals
an allocation table, which tracks the areas of memory that are in use and
those that are free, including the size of each free area of memory.
Memory is normally allocated in multiples of some fixed block size,
for example, 32 bytes. When a request for memory is made, the smallest
number of contiguous blocks that can satisfy that request is allocated. One
possible technique for handling memory fragmentation is to use some
form of memory compaction to combine small free blocks into one larger
block of contiguous memory. The disadvantages of such an approach
include things such as the need for block copying of data and the inability
of an application to access data while it is being block copied. Memory
management also needs to take architecture-specific memory alignment
requirements into account (e.g., multi-byte data items such as long
integers may need to be aligned on an address that is a multiple of 4).
Zephyr provides a variety of memory management approaches such as
shared multi-heaps, memory slab allocation, fixed block size allocation,
and demand paging.
In general, in embedded system applications, it is best to avoid
having a thread allocate memory dynamically. Common good practice
is to allocate dynamic memory at the start of the application so that this
memory will, in effect, appear to be static memory.
When allocating memory dynamically, it is important to consider
whether a thread trying to allocate memory from the heap should
either block indefinitely till memory becomes available, block for some
specified timeout period, or return without blocking when memory is not
available. Best practice here is to keep dynamic memory allocation in the
middle of a running program to a minimum, performing all the dynamic
memory allocation early on in the application and then working as if that
dynamically allocated memory is static from then on.
A common pattern for allowing a thread to acquire extra memory
when really necessary is to use a memory allocation strategy that allocates
memory in fixed-size blocks only and organizes blocks of available
54
Chapter 2 A Review of RTOS Fundamentals
/* Memory Allocation */
Acquire ( counting_semaphore )
Lock ( mutex )
55
Chapter 2 A Review of RTOS Fundamentals
56
Chapter 2 A Review of RTOS Fundamentals
task calls the entry (as an ordinary function call). The issuer of the entry
call is blocked if the call cannot be accepted. The task that defines the
entry, normally, accepts the call, executes it, and returns the results to the
caller. It is possible to have a rendezvous involving bidirectional movement
of data. A rendezvous that does not involve data passing between two
tasks (a simple rendezvous) can be implemented by using two binary
semaphores.
Figure 2-14 illustrates the case of a simple rendezvous.
typedef struct {
mutex_type barrier_lock;
condition_var_type barrier_condition;
int barrier_count;
int number_of_threads;
} barrier_type;
57
Chapter 2 A Review of RTOS Fundamentals
lock_mutex(&(barr->barrier_lock));
barr->barrier_count++;
If(barr->barrier_count < barr->number_of_threads)
condition_wait(&(barr->barrier_condition),
&(barr->barrier_lock));
else {
barr->barrier_count = 0;
condition_broadcast(&(barr->barrier_
condition));
}
unlock_mutex(&(barr->barrier_lock));
}
Communication Patterns
Identifying communication patterns in applications and exploiting such
patterns can lead to faster code implementation as well as help make the
code more maintainable and easier to understand and test.
One way of classifying communication patterns is the distinction
between signal centric and data centric or a combination of the two. In
signal-centric communication, all the necessary information is conveyed
in the event signal itself. By contrast, in data-centric communication,
information is carried in the data transferred.
Another way of classifying communication patterns is to consider
how tightly coupled the communication is. When the communication is
loosely coupled, the data producer does not require a response from the
data consumer, for example, in the case of an ISR posting messages on a
message queue.
On the other hand, in tightly coupled communication, a bidirectional
transfer of data is involved. Typically, the data producer waits
synchronously for a response to its data transfer before continuing
58
Chapter 2 A Review of RTOS Fundamentals
59
Chapter 2 A Review of RTOS Fundamentals
convert this pseudocode into real working code and add extra details to
the working code to generate more interesting behavior.
The code design strategy being exploited here is one that “postpones
the detail” – in other words, it involves sketching out the solution at a high
level and then filling in the details later once you are satisfied with the
high-level outline. You may wish to try the following exercises.
Exercise 1. Synchronizing two tasks using a single binary semaphore.
The code involves two tasks, A and B, and a semaphore. The initial
value of the semaphore is 0. Task B uses the acquire operation on the
shared semaphore, and task A uses the release operation on the shared
semaphore.
Exercise 2. Synchronizing an ISR with a task using a single binary
semaphore. Here, the initial value of the semaphore is 0. The task uses the
acquire operation on the shared semaphore, and the ISR uses the release
operation on the shared semaphore.
Exercises 3 and 4 are variants of 1 and 2 but use event registers instead
of using a binary semaphore.
Exercise 5. Synchronizing an ISR with a task using a counting
semaphore. This exercise is similar to exercise 2 except it makes use of a
counting semaphore that can be used to combine the accumulation of
event occurrences with event signalling. Here, the task can run as long as
the counting semaphore is nonzero.
Exercise 6. This task involves implementing a simple rendezvous with
data passing. It involves two tasks (task A and task B) and two message
queues (message queue A and message queue B). Each message queue
can hold at most one message (such structures are also called mailboxes in
some operating systems). Both message queues are initially empty. When
task A reaches the rendezvous, it puts a message into message queue B and
waits for a message to arrive on message queue A. When task B reaches the
rendezvous, it puts data into message queue A and waits for data to arrive
on message queue B.
60
Chapter 2 A Review of RTOS Fundamentals
The lessons to be learned from these exercises are that there is often
more than one way to tackle a problem and the best solution may not
always be obvious.
61
Chapter 2 A Review of RTOS Fundamentals
62
Chapter 2 A Review of RTOS Fundamentals
the counting semaphore. The producer will block when the value of the
counting semaphore is 0, and the consumer is able to release the counting
semaphore (increase its count value). Initially the counting semaphore
is set to some permissible token value (less than the maximum allowable
token value). The consumer is able to control the flow rate by increasing
the value of the counting semaphore appropriately in relation to its ability
to consume data.
An example of a pattern of handling the asynchronous reception
of data from multiple data communication channels is one involving
multiple ISRs, a semaphore, an interrupt lock, and a daemon task. An
implementation scenario is one where each ISR inserts its data into a
corresponding message queue and performs a release operation on the
semaphore. The daemon task blocks and waits on the semaphore (acquire
operation). When data is available, it takes out an interrupt lock (this
lock is needed to protect against the various multiple interrupt sources),
processes available data, and then releases the interrupt lock.
The following pseudocode snippet illustrates a starting
(nonperformance optimized) approach:
while( acquire(Binary_semaphore))
disable(interrupts)
for each message_queue
get msg_queue_length
for (msg_queue_length)
retrieve message
enable (interrupts)
process message
disable (interrupts )
end for
63
Chapter 2 A Review of RTOS Fundamentals
end for
enable (interrupts)
end while
Event_receive(wanted_events) {
task_cb.wanted_events = wanted_events
while(TRUE)
acquire(task_cb.event_semaphore)
disable(interrupts)
events = wanted_events XOR task_cb.received_events
task_cb.wanted_events = task_cb.wanted_events AND
( NOT events )
enable (interrupts)
if( events is not empty )
return (events)
end if
end while
}
Event_send(events) {
disable(interrupts)
task_cb.received_events = task_cb.received_events
OR events
enable interrupts
release (task_cb.event_semaphore)
}
64
Chapter 2 A Review of RTOS Fundamentals
65
Chapter 2 A Review of RTOS Fundamentals
do_recovery
else
handle_tick_event
end if
end if
end while
Device Drivers
Zephyr RTOS has implementations of device drivers for most of the
devices and peripherals encountered in embedded systems such as GPIO,
I2C, SPI, CAN bus, and ADC. Zephyr RTOS also provides stacks for TCP/
IP, Wi-Fi, and BLE (Bluetooth Low Energy). Some specialist devices such
as the nRF5x devices from Nordic Semiconductor also support proprietary
BLE stacks, in addition to the open source BLE stack included with
66
Chapter 2 A Review of RTOS Fundamentals
References
1. Wikipedia entry describing the HART protocol
https://en.wikipedia.org/wiki/Highway_
Addressable_Remote_Transducer_Protocol
3. https://community.arm.com/arm-community-
blogs/b/architectures-and-processors-blog/
posts/beginner-guide-on-interrupt-latency-and-
interrupt-latency-of-the-arm-cortex-m-processors
67
CHAPTER 3
Zephyr RTOS
Application
Development
Environments and
Zephyr Application
Building Principles
Zephyr applications can be developed using either a command-line
interface–based approach based on the use of tools like west and CMake
from the command line or using an integrated development environment
such as Microsoft VS Code with suitable extensions for Zephyr application
development “plugged in.” At the target platform level, the choice is between
downloading and running code to an actual target board and using a
simulator such as Renode or QEMU. This chapter will cover the setup of
development environments, and the following chapter will cover developing
applications using the simulated target environments Renode and QEMU.
70
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
its 15th edition, is a good starting point for those of you who want to study
CMake in greater depth [1]. The analogy between Ninja and Make is that
“ninja is to assembly” as “make is to a high-level programming language.”
Ninja was developed as a faster build tool than Make when building large
complex projects. The intention was to have developers make use of tools
such as CMake to generate ninja build scripts as opposed to writing such
scripts manually. To dig deeper into Ninja, it is necessary to consult the
official ninja documentation [2].
For large complex projects, the corresponding CMake files can become
quite complex, because they are often built by including CMake parts
from various parts of the project. This is the case with Zephyr projects. The
CMake file in the application directory is usually quite small. The actual
CMake file that drives the build is constructed by “pulling in” CMake files
from other parts of the Zephyr source code tree. CMake has command-line
options for producing verbose output (lots and lots of output in the case of
Zephyr application builds), which can be invoked via the command-
line options --trace, --trace-expand, and --trace-source=some_cmake_
source. When trying to make sense of CMake usage in building Zephyr
applications, a good approach is to learn how to “read and understand”
CMake files and then to explore the various Zephyr CMake files piece
by piece.
71
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
(version 1.4.6 or newer). Installation of these dependencies is greatly
simplified by using the Chocolatey package management tool for Windows
(https://docs.zephyrproject.org/latest/develop/getting_started/
index.html).
The Chocolatey website provides easy-to-follow instructions (https://
chocolatey.org/install) for installing Chocolatey. The Chocolatey
executable, choco, can then be used to install the required packages by
running the following commands, with administrator privileges, in a cmd.
exe terminal window:
The Zephyr SDK and associated tools can then be installed in a cmd.
exe window, running as a regular user. If installing on a workstation
running multiple versions of Python and, also, being used to develop other
projects, then it is a good idea to use a custom Python virtual environment
for working with the various Python packages required for building Zephyr
applications. This can be done in either cmd.exe or PowerShell.
Installation within a Python virtual environment requires creation and
activation of the Python virtual environment. The virtual environment
can be created by changing into a target directory and creating the virtual
environment (venv) as follows:
cd %HOMEPATH%
python -m venv zephyrproject\.venv
72
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The virtual environment needs to be reactivated after deactivation, or
when starting a new shell window in which to work.
Having set up a working Python environment, the next step is to install
the west tool. west is a Python package and is installed, using pip, with the
following command:
Once installed, west can be used to obtain the zephyr source code, as
follows:
west zephyr-export
73
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
to build a Zephyr application. The SDK also provides host tools, such as
custom QEMU (Quick Emulator) and OpenOCD (Open On-Chip Debugger)
builds and software flashing tools that can be used to emulate, flash, and
debug Zephyr applications.
To test the installation, build a basic example, for example, the led
blinking sample example for a particular target board. The build command
template is
cd %HOMEPATH%\zephyrproject\zephyr
west build -p always -b <your-board-name> samples\basic\blinky
west flash
74
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
boards such as the Adafruit nRF52840 ItsyBitsy board and the SparkFun
Pro nRF52840 Mini Bluetooth development board. The BBC Microbit
v2 is worth considering, especially if you are interested in using it for
teaching younger students. Not only does it have good support for block
programming using MakeCode and for Python programming using
MicroPython, it can also be used to teach C programming and RTOS
programming.
Low-cost ARM Cortex M processor–based boards that are not,
primarily, oriented toward BLE are the various STM32 Nucleo boards.
These boards have Arduino-compatible headers and are well supported.
STM also makes various Arduino header format expansion boards that
provide all kinds of extra capabilities, such as motor driver control boards
for DC motors and stepper control motors, and also a board with MEMS
Micro-Electro-Mechanical System) and environmental sensors. STM
Nucleo boards are affordable low-cost boards that are good for learning
and prototyping purposes. There are Nucleo boards for various STM32
processors. For example, the Nucleo-F401RE is shown in Figure 3-1.
75
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
76
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
77
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
78
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
79
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
with Zephyr RTOS include SiFive’s HiFive1 Rev B development kit and
SparkFun’s RED-V SIFIVE RISC-V REDBOARD, shown in Figure 3-6.
Espressif has also developed ESP32 C3 processors, which have RISC-V core
processors. Additionally, Nordic Semiconductor has started rolling out a
multiprocessor family of chips that include ARM Cortex M33 and RISC-V
processor cores, nRF54H20.
80
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
etting Up an nRF Connect SDK
S
Development Environment Using
a Microsoft VS Code–Based IDE
The Nordic Semiconductor’s nRF Connect SDK is based on the Zephyr
RTOS SDK. Essentially it adds a number of extra libraries and samples
specifically oriented at Nordic Semiconductor processors. This SDK
incorporates the Zephyr RTOS SDK and so can also be used to develop
Zephyr RTOS applications targeting other boards supported by the
Zephyr RTOS SDK. Nordic Semiconductor has also developed a desktop
application, nRF Connect for Desktop, that greatly simplifies the
installation and setup of an IDE for developing Zephyr RTOS applications.
Initially developed for Microsoft Windows, there are, now, variants for
Linux and Mac OS X.
nRF Connect for Desktop can be downloaded from the nordicsemi
website (see Figure 3-7): www.nordicsemi.com/Products/Development-
tools/nRF-Connect-for-desktop/Download#infotabs.
81
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The nRF Connect for Desktop framework is a cross-platform tool
framework that facilitates developing applications on nRF devices by
providing apps for monitoring, measuring, optimizing, and programming
applications. It is oriented toward working with Nordic development kits
and dongles.
The apps that can be installed using the nRF Connect for Desktop
include the Bluetooth Low Energy app for Bluetooth Low Energy
connectivity testing, a Direct Test Mode app for performing tests with
Bluetooth Low Energy devices, and a Getting Started Assistant app for
setting up the nRF Connect SDK and toolchain on a Linux computer.
For Mac and Windows development platforms, there is the Toolchain
Manager app.
Additional apps include an LTE Link Monitor app, which is a modem
client application that monitors the LTE modem/link status and activity
using AT commands, and a Power Profiler app, which is used with the
Nordic Power Profiler Kit to analyze and export current consumption
measurements.
Finally, there are a Programmer app for programming Nordic SoCs,
an RSSI Viewer app for scanning the 2.4 GHz spectrum, and a Toolchain
Manager app for managing the nRF Connect SDK and toolchain versions
on Windows and Mac development workstations.
The Toolchain Manager takes care of installing required dependencies,
namely, the Zephyr SDK, CMake, dtc (Device Tree Compiler), Girt, gperf,
ninja, Python, and west.
The Toolchain Manager will install all Python dependencies into
a local environment in the Toolchain Manager app. These include
anytree, canopen, cbor2, click, cryptography, ecdsa, imagesize, intelhex,
packaging, progress, pyelftools, pylint, PyYAML, west, and windows-curses
(only when installing on Windows). Don’t worry, it is not necessary to
understand and be able to use these tools. The nRF Connect SDK and
the Zephyr SDK make extensive use of Python scripts for managing and
building Zephyr RTOS applications.
82
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
After nRF Connect for Desktop has been installed, it can be used to
install the Toolchain Manager by scrolling to the Toolchain Manager app
entry and clicking on the Install button (see Figure 3-8).
83
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
for Desktop. Then in the Toolchain Manager, select the version of the SDK
to install. Usually this will be the latest version unless you are working on a
project that requires an older version (see Figure 3-9).
84
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Having installed the nRF Connect SDK following the previous
approach, there are now two possible ways in which applications can be
developed, either working in Visual Studio Code (VS Code) using the nRF
Connect for VS Code extension or developing using the command line.
To develop using the VS Code IDE, click on the Open VS Code button.
If this is a first installation, then a notification dialog listing missing
extensions that have to be installed will appear. The list will include
extensions from the nRF Connect for Visual Studio Code extension pack.
Once these are installed, clicking on the Open VS Code button will start
up VS Code. The nRF Connect for VS Coe extension together with VS Code
results in a complete IDE in which applications for nRF91, nRF53, and
nRF52 Series Nordic devices can be developed. It includes an interface to
the compiler and linker, an RTOS-aware debugger, as well as an interface
to the nRF Connect SDK, and a serial terminal.
Working in VS Code
Installing the nRF Connect SDK using the nRF Connect for Desktop
toolchain manager also installs the nRF Connect for VS Code extension,
which makes it possible to develop Zephyr projects running on nRF
devices in VS Code. The next few pages, based on the nRF Connect SDK
documentation [3] and a Linux Foundation blog post [4], provide an
introductory overview of working with the nRF Connect SDK in VS Code.
The nRF Connect SDK documentation should be consulted for more
detailed information.
To create an nRF Connect SDK project in VS Code, the first step is to
click on the nRF Connect icon in the Activity Bar (see Figure 3-10).
85
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
86
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Figures 3-12 and 3-13 show enlarged views of the left and right
hand sides.
87
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
88
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
89
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
90
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The Applications View (Figure 3-15) is located below the Welcome
View and lists all the applications in the current workspace. The icons
associated with the currently selected application will be in blue instead
of white.
Global Actions
Hovering over the Applications View will reveal a View Toolbar containing
a number of View Actions, shown in Figure 3-16.
There are four icons in the View Toolbar: an Add Folder as Application
action icon for adding a folder containing preexisting application files
to the project, a Refresh Applications application icon that prompts the
extension to scan the applications folder for new build configurations
if it cannot automatically detect a newly created build folder, a Build
All Configurations action icon for building all configurations for all
applications, and, finally, a Flash All Linked Devices action icon for
flashing builds to all the associated, connected devices.
91
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Application-Specific Actions
When hovering over an application-specific action, icons are displayed as
shown in Figure 3-17.
Build-Specific Actions
Within each application, there are build folder actions that apply to a
single folder (see Figure 3-18).
92
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The Link Build Configuration And Device action is for linking a build
configuration to a specific device, the Edit Configuration is for editing a
preexisting build configuration for the selected application, and the Save
Configuration as Preset action saves the current build configuration as
a preset (which is a shortcut to the nRF Connect: Save Configuration as
Preset command in the Command Palette) and saves the current build
configuration to the CMakePresets.json file. The Copy Build Command
can be used to save a copy of the build command for the selected build
into the device’s clipboard.
Details View
The Details View provides detailed access to the application project
contents. It comes after the Applications View and relates to the currently
selected application. Its three main sections (views) relate to the source
files, input files, and output files. The source files section lists the source
files used by the application, the input files section lists the CMake and
devicetree configuration files, and the output files section lists the output
files generated by the build process (see Figure 3-19). Hovering the mouse
over any of the listed groups or folders and clicking on the magnifying glass
icon will open the VS Code Search View set up (prefilled) to search only
the files in the selected section.
93
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Devicetree View
The Devicetree View located under the Details View is very useful when
having to examine devicetree configuration details for the application. It
makes use of the nRF DeviceTree extension. A devicetree context contains
the basic build target configuration and various overlay files providing use
case–specific configuration details. Hovering the mouse over the View
Toolbar will reveal the Show Complied Devicetree Output button, which
can be used to open a Devicetree View (Figure 3-20).
94
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Actions View
The Actions View, to be found under the Devicetree View, provides access
to common actions associated with building, configuring, debugging,
flashing, and seeing the memory report.
When working with the Zephyr SDK as opposed to the nRF Connect
SDK, similar functionality can be obtained using the PlatformIO IDE for
VS Code: https://docs.platformio.org/en/stable/integration/ide/
vscode.html#ide-vscode.
zephyr\samples\basic\blinky
95
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
In nRF Connect for VS Code, when creating a new application using
“Create new application”. Specify the application name, for example,
my_blinky_1 (this will create an application folder with that name). Finally,
clicking on the Create Application button will add the application code,
without building it.
The next step is to add a build configuration for the application by
hovering the mouse cursor over the application name, clicking on the
build configuration icon, and, in the Add Build Configuration View, adding
the required configuration details. These will include the target board
for which the application is to be built, the project configuration file (the
sample project has one already, prj.conf ), Kconfig fragments (if any) for
the application, extra CMake arguments (if any), and the application build
directory and build directives such as whether the application is to be built
automatically after the application configuration files have been generated
and also whether the application is to include debugging options.
The Board ID strings corresponding to the various Nordic
development kit boards are summarized in Table 3-1.
nRF5340 DK nrf5340dk_nrf5340_cpuapp_ns
nRF52840 DK nrf52840dk_nrf52840
nRF52833 DK nrf52833dk_nrf52833
nRF52 DK nrf52dk_nrf52832
nRF9160 DK nrf9160dk_nrf9160_ns
The build process, which will take a little while to complete, is started
by clicking on the Build Configuration button.
96
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The progress of the build can be viewed in a VS Code terminal
window by clicking on the View ➤ Terminal menu option. When the build
completes successfully, a mini-report showing the memory usage of the
application will be displayed.
During the build process, behind the scenes as it were, services
provided by west such as repository management and the driving of the
application build process will be doing most of the work. These services
are invoked internally by nRF Connect for VS Code.
To flash the application to the target board, the board, of course,
must be connected to the workstation using a USB cable plugged into the
correct USB port, used for programming, on the board, and connected to
a USB port on the development workstation. If correctly plugged in, the
board will be discoverable and will be visible in the boards listed in the
Connected devices view. Clicking the Flash option in the Actions View will
flash the application to the board. The details of the flashing process will
be displayed in the Terminal Panel, if it is open. If all goes well, an LED on
the board will be seen to be blinking.
97
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Windows, or macOS systems. Hence, part of the process of learning Zephyr
RTOS programming is to learn about the Zephyr RTOS device driver model
and its associated APIs. The Zephyr device driver modelling framework
has borrowed the devicetree language from Linux for specifying processor
and board configurations. However, the way devicetree files are used in
Zephyr RTOS is quite different from the way they are used in Linux.
As already mentioned, application configuration involves both
Kconfig and devicetree aspects. As a rule of thumb, the devicetree is used
to describe the hardware and its boot-time configuration such as the
peripherals on a board, the boot-time clock frequencies, interrupt lines,
etc. Kconfig is used to configure which software support to build into
the final image, for example, whether to add networking support, which
drivers are needed by the application, and such.
The devicetree syntax takes a certain amount of effort and practice to
master. For many projects, the devicetree details do not have to be given in
full as the final devicetree can be built up from processor and target board
devicetree files combined with project-specific overlays. The devicetree
textual description is parsed and compiled as part of the build process.
The, textual, devicetree syntax is illustrated in Figure 3-21.
/dts-v1/;
/{
a-node {
subnode_label: a-sub-node {
foo = <3>;
};
};
};
98
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The /dts-v1/; line means the file’s contents are in version 1 of the
DTS syntax. The preceding tree has three nodes: a root node "/", a node
named a-node, which is a child of the root node, and a node named
a-sub-node, which is a child of a-node.
Nodes can have zero or more labels. A label can be thought of as a
unique shorthand that can be used to refer to the labelled node elsewhere
in the devicetree. In the preceding code snippet, a-sub-node has a label
subnode_label. Devicetree nodes have paths that identify their locations
in the tree. A devicetree path is a string separated by slashes (/). The
root node’s path is a single slash “/”. In general, a node path is formed by
concatenating the node’s ancestors’ names with the name of the node
itself. For example, the full path to a-sub-node is /a-node/a-sub-node.
Devicetree nodes can also have properties, which are, quite simply,
name/value pairs. A property value can be any sequence of bytes. A
property can be given as an array of cells, where a cell is simply a 32-bit
unsigned integer. In the preceding code snippet, the node a-sub-node has
a property named foo, whose value is a cell with value 3. The size and type
of foo’s value are implied by the enclosing angle brackets (< and >) in the
DTS. Most often, devicetree nodes correspond to some piece of hardware,
and the node hierarchy reflects the physical arrangement/layout of the
hardware.
The devicetree also provides aliases that can be used to reference other
nodes in the devicetree. It is possible to have collections of aliases. In the
following devicetree snippet, the /aliases node contains properties that are
aliases, in this case just a single alias. The name of the property is the name
of that alias, and the value of the property is a reference to a node in the
devicetree. The & is analogous to C’s address of operator.
/ {
aliases {
subnode_alias = &subnode_label;
};
};
99
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The preceding code snippet assigns the node a-sub-node, referenced
by its label subnode_label to the alias subnode_alias.
The Zephyr devicetree build process generates a C header that
contains the required devicetree data abstracted behind a C macro
API framework. Information about a particular devicetree node can be
obtained via the corresponding C macro, which is referred to as a node
identifier for that device. The two common macros used in practice
are DT_NODELABEL(), which is used to access a node via its label, and
DT_ALIAS(), which is used to access a node via an alias. An alias can be
thought of as an abbreviation of a full node label and can be used as an
easier-to-remember label as opposed to having to provide a full path.
Using the preceding snippet, the node identifier of the a-sub-node could
be obtained via DT_NODELABEL(subnode-label).
The DT_PROP() macro can be used to retrieve the value assigned to a
certain devicetree property. For example, to get the value assigned to the
foo property, the macro DT_PROP(DT_NODELABEL(subnode-label), foo)
could be used.
A specific devicetree node is referenced by the full path to that node in
the devicetree, for example, /external-bus/ethernet@0,0. Where a user
wishes to obtain an answer to a question such as “which device is eth0?”
having to provide a full path may involve time spent searching through the
devicetree. An aliases node can be thought of as providing a shorter user
and application-friendly alias for the full device path, for example:
aliases {
ethernet0 = ð0;
serial0 = &serial0;
};
100
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Writing a Zephyr application may involve getting a driver-level struct
device corresponding to a particular devicetree node. The following
example, based on the Zephyr documentation, illustrates how this can be
done in application code. It is based on the following example devicetree
fragment pertaining to a serial device serial@40002000:
/ {
soc {
serial0: serial@40002000 {
status = "okay";
current-speed = <115200>;
/* ... */
};
};
aliases {
my-serial = &serial0;
};
chosen {
zephyr,console = &serial0;
};
};
The /chosen node is a special node that contains properties that have
values describing system-wide settings, and the DT_CHOSEN() macro can
be used to get the node identifier for a chosen node.
Devicetree nodes can be referenced using the ampersand (&)
character and the label.
To overwrite a property, the node has to be referenced using the
ampersand character and the label. Devicetree entries occurring later in
the devicetree overwrite earlier entries (the sequence order is important).
101
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
To obtain a device-level struct, it is necessary to provide the correct
node identifier. The Zephyr framework provides a number of convenience
macros for this purpose, which include the following:
102
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The generic API approach used by Zephyr borrows many ideas from
Linux such as the character device driver model whose principles are used,
in adapted form, in Zephyr.
Figure 3-22 shows the basic nRF Connect SDK/Zephyr SDK device
driver architecture.
In Zephyr, there are several device driver types. They all have a
similar structure for accessing the properties and methods of a device
type instance. The structure consists of a set of pointers: a pointer to the
device name string, a pointer to a structure containing configuration
details, and a pointer to a structure containing a table of function pointers
defining the behavior of the device and a pointer to data. The actual driver
code functionality is provided by the functions to which the API function
pointers point.
In an application, a device is accessed via a pointer (handle) to a
const struct device instance. This pointer is initialized using the macro
DEVICE_DT_GET(<node_id>), which takes a devicetree node identifier
as an argument. DEVICE_DT_GET() will fail at build time if the device is
not allocated by the driver, because, for example, there is no entry for
such a device in the devicetree (i.e., it does not exist in the devicetree) or
the device status of that device in the devicetree is the disabled status.
The failure will be detected at linker time with a linker error of the kind
undefined reference to __device_dts_ord<N>.
103
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The following code snippet shows a standard pattern for obtaining a
handle to a device and testing for its validity by calling the Zephyr driver
API function. In this example, the device in question is uart0.
Zephyr provides utility macros for specific devices such as, for
example, in the case of a GPIO pin to which an LED might be attached,
the macro GPIO_DT_SPEC_GET can be used to define a device instance data
structure that contains a pointer to a const struct device as one of its
members.
#include <zephyr.h>
#include <drivers/gpio.h>
#define SLEEP_TIME_MS 1000
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_
NODE, gpios);
void main(void) {
int ret;
if (!device_is_ready(led.port)) {
Return;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
104
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
if (ret < 0) {
Return;
}
while (1) {
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
Return;
}
k_msleep(SLEEP_TIME_MS);
}
}
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
105
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
label = "Green LED 0";
};
led1: led_1 {
gpios = <&gpio0 14 GPIO_ACTIVE_LOW>;
label = "Green LED 1";
};
led2: led_2 {
gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
label = "Green LED 2";
};
led3: led_3 {
gpios = <&gpio0 16 GPIO_ACTIVE_LOW>;
label = "Green LED 3";
};
};
GPIO Inputs
Reading input pins can be accomplished in two main ways, namely,
polling the pin level by repeatedly calling gpio_pin_get_dt() to keep
track of the status of a pin so as to be able to detect a state change, or
configuring the pin to generate edge-level change interrupts. Polling a pin
continuously, though conceptually simple and simple to implement, will
106
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
result in an increased power consumption. With edge-triggered interrupts,
the CPU is automatically informed when there is a change in the pin
status. This approach frees up the CPU from having to continually poll the
status of the pin and has the extra potential advantage that the CPU can,
if required, be put into sleep mode in between button presses and only
woken up when there is a change in button state. Edge-triggered interrupts
can only be configured on a GPIO pin configured as an output pin.
The zephyr\samples\basic\button example demonstrates both
polling and interrupt-driven approaches for working with pins, as well as
mirroring the state of a button with the state of a corresponding LED. This
example illustrates the use of a thread and an interrupt in an application.
Setting up an interrupt on a GPIO pin involves configuring an interrupt
on that pin and associating a callback function interrupt service routine
with that interrupt.
Configuring an interrupt on the selected pin uses the gpio_pin_
interrupt_configure_dt() function, passing in the pin specifications
given in the devicetree and the interrupt configuration flags as arguments.
The interrupt configuration flags configure the triggering conditions,
which can be to trigger an interrupt on a rising edge, falling edge, or
both – in other words, whether to trigger on a change to logical level 1,
logical level 0, or both. As an example, gpio_pin_interrupt_configure_
dt(&button,GPIO_INT_EDGE_TO_ACTIVE); will configure an interrupt
being triggered on dev.pin when a change to logical level 1 occurs.
Setting up an interrupt handler callback requires defining (writing the
code for) the interrupt service routing and then associating the callback
function with the interrupt itself. The signature (function prototype) of the
callback function is
107
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
A handler that toggles the state of an LED associated with the button
being pressed could be implemented along the following lines:
gpio_add_callback(button.port, &pin_cb_data);
gpio_add_callback(button.port, &pin_cb_data);
108
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Many embedded processor development boards contain some
kind of interface MCU that is involved not only in assisting with serial
communications between the target board MCU but also with programming,
logging, and debugging. This MCU will have a USB interface for connecting to
the PC workstation and a serial interface connecting it to the core processor
itself. The UART connections on the nRF52840 System on Chip (SoC) that
connect to the interface MCU are summarized in Table 3-2.
The block diagram shown in Figure 3-23 for the nRF52840 DK board
shows just such an approach.
P0.05 RTS
P0.06 TXD
P0.07 CTS
P0.08 RXD
109
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
In principle, a UART only requires two signal lines to successfully
communicate: a TXD (transmit data) and RXD (receive data) line
as well as a common ground line (used as a reference point). When
communicating with another UART device, the TXD line will be attached
to a corresponding RXD line, and vice versa. No clock line is used with
the UART protocol. Rather, users instead specify a particular baud rate
for the two devices to operate at. The UART standard specifies two extra
lines, namely, RTS (ready to send) and CTS (clear to send). The RTS and
CTS lines, if connected, are responsible for flow control. If flow control is
disabled, then these two pins are not used.
The USB connection between the interface MCU and the PC can
forward data sent from the nRF52840 processor to the PC. From the PC’s
point of view, this USB connection is behaving as a serial connection, with
a virtual COM port. The virtual COM port is configurable, with a flexible
baud rate setting up to 1 Mbps. It also supports Dynamic Hardware Flow
Control (HWFC) handling if so configured.
From the perspective of experimenting with various multithreading
coding patterns, the virtual COM port–based UART is valuable because
it is the port used when the Zephyr kernel printk() function is used to
send text strings to a PC console terminal. To a certain extent, the Zephyr
RTOS printk() mimics the corresponding printk() used in Linux kernel
programming. printk() is a printf()-like function that supports a subset
of the printf() format string specifiers. From the application real-time
performance point of view, it is worth remembering that the output of
printk() is not deferred but is immediately sent to the console without
any mutual exclusion or buffering and that printk() will not return until
all the bytes of the message have been sent. Bearing this in mind, the use of
printk() in time-critical applications is best avoided.
110
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Zephyr Logging Module
Another way of sending messages to a PC console (terminal window)
is to make use of the Logger module. The Logger module’s logging API
provides a common interface to process messages issued by the developer
code. Messages are passed through a frontend and are then processed by
active backends. An advantage of working with the Logger module is that it
supports deferred logging, which allows the more time-consuming aspects
of message logging to be run when it is more convenient to do so, instead
of processing and sending the log message immediately.
Additional features of the logging module include runtime filtering of
messages, timestamping of messages, and a data dumping API. Zephyr
RTOS logging borrows ideas from Linux kernel logging and syslog. Filtering
is based on log message severity levels. The severity levels are summarized
in Table 3-3.
111
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Zephyr provides several “convenience” macros for sending log
messages at different severity levels, LOG_INF, LOG_DBG, LOG_WRN, and
LOG_ERR, used as shown in the following example:
00 01 02 03 04 05 06 07 48 65 6c 6c 6f
112
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
For a module to use the logger, it must specify a unique name and
register itself with the logger. Where a module is made up of multiple
source files, registration is performed in one file, but each module file
has to declare the module name. The logging system API can be called
by general application code. The “fine control” logging capabilities of the
Zephyr Logger provide a useful set of logging tuning, management, and
control mechanisms. It is up to the developer to devise effective policies
for the debugging, monitoring, and troubleshooting task being tackled and
to move from a global logging approach to a more “fine-grained” logging
approach. Log messages automatically include a timestamp and report
on what part of the application they come from. The importance of each
message can be specified. This makes it possible to select, at compile time,
which logging messages will be included in the binary. A program can
contain various debug-level messages, and messages can be included or
left out of the build based on setting a build time priority level. This can
be done by setting the value of CONFIG_LOG_MAX_LEVEL in the Kconfig
file. This is the maximal level that is compiled in, and lower-level messages
will not be compiled into the code. At runtime, it is also possible to turn off
the logging of certain messages by increasing the severity threshold.
The Zephyr logging architecture design is quite sophisticated. There
are three main components, namely, a frontend, which is the application
logging interface; the core component, which filters messages and routes
them to a given backend, which may be a network interface to send
log messages to a log server in the cloud. By adding a means to send
commands to the running device remotely, there is also the possibility of
dynamically controlling the level of logging based on the behavior of the
system under various testing scenarios.
113
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
114
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Zephyr Applications Using Renode
Renode was developed by Antmicro, a Swedish-Polish research company,
as a development tool for developing wired and wireless multinode
embedded networks. Its purpose is to enable the development and testing
of IoT systems, with a special emphasis on the security aspects of such
systems. It provides emulators for a wide range of processor architectures
and a means of adding virtual physical devices to the emulated processor
cores, so that unmodified compiled software can be run on virtual boards
constructed using the tools and interfaces provided by Renode. Supported
processor architectures include ARMv7 and ARMv8 Cortex-A and
Cortex-M, x86, and RISC-V.
Renode is implemented in C# and C, and systems can be built by
connecting devices (including memory) to a system bus connected to
a processor core running the corresponding processor instruction set.
The devices themselves can be implemented in C# and connected to the
system bus.
The Renode simulator/emulator can run “networks of devices.”
Renode has a command-line interface (CLI), called the Monitor, via which
it is possible to control the emulation using Renode’s built-in functions,
which provide access to emulation objects such as peripherals, machines,
and external connectors.
Renode provides a number of basic peripheral devices including UART,
Timer, GPIO controller, I2C controller, SPI controller, and I2C sensor and
SPI sensor examples.
Peripherals implemented in Python can also be used for the purposes
of implementing devices with very simple logic.
Python code can be executed directly in the Renode Monitor using the
python command. Peripherals implemented in Python can also be used
for the purposes of implementing devices with very simple logic.
115
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Renode also provides a set of hooks that make it possible to run code
such as Python scripts when certain conditions arise. These hooks provide
specific functionalities and include UART hooks, CPU hooks, system bus
hooks, watchpoint hooks, packet interception hooks, and user state hooks.
Renode supports debugging applications running on emulated
machines using the GDB debugger. It works in Renode by making use of
the GDB remote protocol. Common GDB functions such as breakpoints,
watchpoints, stepping, and memory access are supported.
116
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
The output display in Figure 3-24 shows, schematically, a basic RISC-V
machine that emulates a MiV system.
117
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Table 3-4. Emulators vs. simulators
Purpose Emulator Simulator
118
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Emulator Use Cases
• Checking how application software interacts with the
hardware or a combination of the hardware and an OS
(Operating System).
• CPU load
• Memory consumption
• Network loads
119
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
• Ability to use scalability and take advantage of the
pay-as-you-go benefits of cloud computing to scale up
as and when required, and also take advantage of the
relative ease of cloud deployment
-- Battery performance
-- CPU performance
-- Memory consumption
120
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
Summarizing Renode
• Renode is designed to be a “whole-system emulator”
that can be used in continuous integration and
continuous development (CI/CD) scenarios on
multiple devices.
121
Chapter 3 ZEPHYR RTOS APPLICATION DEVELOPMENT ENVIRONMENTS AND
ZEPHYR APPLICATION BUILDING PRINCIPLES
• Using Renode can lead to faster iteration cycles by
avoiding the flash loading delays inherent when
loading compiled code to actual targets.
The details of getting started with Renode and running Zephyr RTOS
examples in Renode will be covered in a later chapter.
References
1. https://crascit.com/professional-cmake/
2. https://ninja-build.org/manual.html
3. h ttps://nrfconnect.github.io/vscode-nrf-connect/
guides/overview.html
5. h ttps://developer.nordicsemi.com/nRF_Connect_
SDK/doc/latest/nrf/introduction.html
6. https://docs.zephyrproject.org/latest/develop/
toolchains/zephyr_sdk.html
122
CHAPTER 4
Zephyr RTOS
Multithreading
Building Zephyr application requires a basic understanding of the Zephyr
build system. This involves being able to make sense of the Kconfig and
devicetree files used in the sample projects and then using these as a
starting point for your own projects. Mostly, the project-specific files are
relatively simple and build on the Kconfig files and devicetree files in the
various parts of the Zephyr source code tree. Using the west tool hides
much of the underlying complexity.
The Zephyr project provides more than just an operating system
kernel. It utilizes tools for developing, releasing, and maintaining
a firmware application. Tools that include CMake, a toolchain with
compilers, flash and debug tools together with a Zephyr repository that has
the sources for the kernel itself, protocol stacks, device drivers, filesystems,
and other components.
West is a multipurpose tool. It enables the management of multiple
repositories. Zephyr application development makes use of libraries and
features from folders that are cloned from different repositories or projects,
and it is the west tool that keeps control of which commits to use from the
different projects, which, considerably, simplifies the task of adding and
removing modules. The concept underlying the use of west is that of a west
workspace, which contains one manifest repository and multiple projects.
The manifest repository, itself, controls which commits to use from the
different projects on which the application will be based. The Zephyr west
tool can, importantly, also pull in code from other third-party projects
such as cryptographic libraries, hardware abstraction layers (HALs),
protocol stacks, and the MCUboot bootloader.
west is implemented in Python 3 and has plug-in capabilities that
allow extension commands and their associated features such as build
tools, code flashing tools, and debugging tools to be added in.
A west command consists of the top-level west command, which can
take a number of common options, followed by a sub-command to run
and then options and arguments for that sub-command:
Kconfig
The Zephyr project adopted the Kconfig approach used in the Linux
build framework, largely because of the need to tackle the same issues as
those involved in Linux kernel building, namely, having to handle a large
complex code base with many components.
124
Chapter 4 Zephyr RTOS Multithreading
Typical Zephyr application projects will use only a subset of all the
available components, and building a kernel with just the components
required by the application will result in a smaller executable file.
Kconfig is used to configure the Zephyr kernel and subsystems at build
time in order to adapt the resulting kernel for specific application and
platform needs.
A Zephyr application is built by linking the kernel object code formed
by compiling the kernel for the specified configuration with the object
code generated by compiling the code that constitutes the application (see
Figure 4-1).
This is different from Linux, where the kernel is built separately from
the applications that run on a Linux platform. Applications, in Linux,
run in a virtual memory space distinct from the Linux kernel memory
space. They are loaded dynamically and access kernel (operating) system
resources via system calls.
Kconfig files contain details of the required configuration options
(often referred to as symbols). Kconfig files also specify dependencies
between symbols that help determine which configurations are valid.
Symbols can be grouped into hierarchy, which gives rise to a hierarchical
menu and submenu structure. This makes it possible to develop graphical
configuration tools that are simpler to use than an approach based on
editing a large text file. When the project Kconfig file is processed, a header
125
Chapter 4 Zephyr RTOS Multithreading
126
Chapter 4 Zephyr RTOS Multithreading
Multithreading in Zephyr
A key service provided by Zephyr is multithreading. Small 32-bit SoC
devices such as ARM Cortex M processor or RISC-V32 devices do not have
an MMU (Memory Management Unit), and virtualization is not used in
applications built using such devices. Quite a few processors used will
incorporate an MPU (Memory Protection Unit), which can be used to
isolate threads from one another, and Zephyr is designed to accommodate
devices having an MPU. An MPU can be used to prevent threads accessing
certain areas of memory that may contain privileged data and code. This
can be important where, for security reasons, it is necessary to isolate
certain pieces of code from others. MPUs such as those found in ARM
Cortex M33 processors make trusted execution environments possible.
There is much discussion when talking about operating systems about
tasks and threads and the distinction between a task and a thread. In
Linux, running processes are referred to as tasks, and it is tasks which are
127
Chapter 4 Zephyr RTOS Multithreading
scheduled by the Linux kernel. A Linux task runs in its own virtual memory
space. Threads in Linux share the same virtual memory space but, from
the scheduling point of view, are scheduled as tasks.
In Microsoft Windows, on the other hand, when a task is created,
a thread (the primary thread) is also created, and this thread may then
spawn other threads. Here, the task can be thought of as the holder of
resources and the threads are the units of work that are scheduled. In
FreeRTOS, the terms “thread” and “task” are used interchangeably. “Each
task executes within its own context with no coincidental dependency on
other tasks within the system or the RTOS scheduler itself. Only one task
within the application can be executing at any point in time, and the real-
time RTOS scheduler is responsible for deciding which task this should be.”
Where FreeRTOS talks about tasks, Zephyr RTOS talks about threads.
Zephyr threads are the unit of scheduling in Zephyr. In Zephyr, a thread
is a kernel object that is used for application processing. Any number
of threads can be defined by an application (subject to the availability
of sufficient RAM), and each thread will have a unique thread id that is
assigned when the thread is spawned and which can be used to reference
that thread. From the application point of view, a Zephyr thread can be
thought of as a “semi-independent” piece of an application that performs
some specific function (carries out some specific duty) [2].
A thread will have a number of properties such as a stack area, a thread
control block, an entry point function, and a thread scheduling priority.
The stack area is the region of memory used for the thread’s stack. The size
of the stack area can be configured to conform to the actual processing
needs of the thread, and Zephyr provides macros to create and make use
of stack memory regions. A thread control block is used for private kernel
bookkeeping of the thread’s metadata and is realized as an instance of type
k_thread. An entry point function, the function that is invoked when the
thread is started, can take up to three argument values that can be passed
to this function. The scheduling priority of a thread is used in connection
with thread scheduling and the allocation of CPU time to the thread.
128
Chapter 4 Zephyr RTOS Multithreading
A thread will also have a set of thread options that influence thread
handling under certain specific circumstances. These include a start delay
that specifies a waiting time delay before the thread is started and an
execution mode. The execution mode can either be supervisor or user mode.
By default, threads run in supervisor mode, which allows access to privileged
CPU instructions, the entire memory address space, and peripherals.
User mode threads have a reduced set of privileges, depending on the
CONFIG_USERSPACE option, and will have to make appropriate system
calls to access services running at a higher supervisor privilege level.
The details of Zephyr user mode are described in some detail in the
Zephyr documentation [1]. Zephyr support for user mode is important
when developing networked applications. This is especially important
in IoT and IIoT contexts, where security is an important part of the
implementation and design process. The following scenarios illustrate
recommended practice for having applications run user mode threads:
129
Chapter 4 Zephyr RTOS Multithreading
130
Chapter 4 Zephyr RTOS Multithreading
Essentially, the life cycle of a thread involves the following thread life
cycle events: Thread Creation, Thread Termination, Thread Aborting, and
Thread Suspension.
A thread must be created and initialized before it can be used. Each
thread has its own stack buffer for which memory has to be allocated
before the thread is created. The memory allocated may also have a part
that is reserved for memory management structures. For example, if
guard-based stack overflow detection is enabled, a small write-protected
memory management region will be present, immediately preceding the
stack buffer whose purpose is to catch overflows. If userspace is enabled, a
separate fixed-size privilege elevation stack must be reserved to serve as a
private kernel stack for handling system calls. Also, if userspace is enabled,
the thread’s stack buffer must be appropriately sized and aligned in such
a way that a memory protection region may be programmed to exactly
fit. The alignment constraints may be processor architecture dependent.
For example, some MPUs require their regions to be of some power of
two in size and aligned to the MPU size. The consequence is that portable
code cannot simply pass an arbitrary character buffer to the thread create
function, k_thread_create(). Zephyr provides macros to instantiate
(define) stacks, K_KERNEL_STACK_DEFINE for a kernel privilege-level stack
and K_THREAD_STACK_DEFINE for a userspace thread stack. When a thread
is created, the kernel initializes the thread control block and one end of
the stack memory. The remainder of the thread’s stack is typically left
uninitialized. K_THREAD_STACK_SIZEOF() gives the size for a stack object
defined using K_THREAD_STACK, and K_KERNEL_STACK_SIZEOF() gives the
size of a stack object defined using K_KERNEL_STACK.
The start time delay can be specified. Pass the parameter K_NO_WAIT
if the thread is to start execution immediately, or K_FOREVER if the thread
is start suspended and has to be started explicitly. A delayed start can be
cancelled before the thread begins executing by issuing a cancellation
request; however, a thread whose delayed start was successfully cancelled
must be explicitly re-spawned before it can be used.
131
Chapter 4 Zephyr RTOS Multithreading
132
Chapter 4 Zephyr RTOS Multithreading
133
Chapter 4 Zephyr RTOS Multithreading
134
Chapter 4 Zephyr RTOS Multithreading
136
Chapter 4 Zephyr RTOS Multithreading
Python scripts to generate files. Other examples include Python scripts for
processing Kconfig files and devicetree files. A deep dive into the workings
of Zephyr, a complex subject, requires a good understanding of what these
various Python scripts do. For most practical application development
purposes, this “deep knowledge” is not required.
For suitable architectures, Zephyr can provide a memory domain API,
which can be used to grant access to additional blocks of memory to a user
thread. The intricacies of working with MPU partitions is an advanced
topic, which requires an understanding of linker scripts and how they are
generated, and will not be covered in this book. However, as security is an
important aspect of developing connected devices, it is something that
needs to be taken seriously in design, coding, and testing.
137
Chapter 4 Zephyr RTOS Multithreading
and the number of memory regions that can be defined is relatively small.
The size and complexity of an MMU make it unsuited for use in relatively
small memory-constrained embedded systems. For such resource-
constrained systems, an MPU can provide mechanisms for enhancing
application security.
An MPU can only be configured by code running at a privileged level.
Regions are defined by specifying their starting addresses and sizes. A fault
is generated when a memory access that violates permissions is attempted.
The key settings (flags) are as follows:
User syscalls
Zephyr provides a set of system call APIs that can be used by user mode
threads to call kernel services, pass arguments as part of the call, and
obtain return values from the service. This is analogous to using system
calls in Linux user applications to access kernel services such as file
systems, input devices, and networking devices. The details, however, are
not exactly the same. An example showing the use of such system calls will
be explored in a later section. Although not entirely straightforward, it is
possible to design and implement new custom Zephyr system calls.
138
Chapter 4 Zephyr RTOS Multithreading
139
Chapter 4 Zephyr RTOS Multithreading
140
Chapter 4 Zephyr RTOS Multithreading
when scheduling the thread. The K_USER option used when CONFIG_
USERSPACE is enabled will result in a thread with reduced privileges, a user
mode thread being created. Also, if CONFIG_USERSPACE is enabled, the K_
INHERIT_PERMS option has the effect that a child thread will inherit all the
kernel object permissions that the parent thread had, except for the parent
thread object.
Dropping Privileges
If CONFIG_USERSPACE is enabled, a thread running in supervisor mode can
perform a one-way transition to user mode by using the k_thread_user_
mode_enter() API function. This one-way operation will reset and zero the
thread’s stack memory and will mark that thread as non-essential.
141
Chapter 4 Zephyr RTOS Multithreading
Thread Termination
A thread can terminate itself by returning from its entry point function.
If CONFIG_USERSPACE is enabled, aborting a thread will, additionally,
mark that thread and its stack objects as uninitialized so that they may
be reused.
The following code-snippet pattern illustrates a typical scenario
involving thread termination:
System Threads
The Zephyr RTOS startup process differs from the startup process of an
RTOS such as FreeRTOS.
A FreeRTOS application starts executing in the same way as a non-
RTOS bare metal application. Multitasking is started in FreeRTOS by
calling the function vTaskStartScheduler(), which is commonly called
in main().
Zephyr, on the other hand, spawns two threads when starting: a main
thread and an idle thread. The main thread performs kernel initialization
and then calls the application’s main() function (if one is defined).
142
Chapter 4 Zephyr RTOS Multithreading
By default, the main thread uses the highest configured preemptible thread
priority (i.e., 0). If the kernel is not configured to support preemptible
threads, the main thread uses the lowest configured cooperative thread
priority (i.e., -1).
In Zephyr RTOS, the main thread is an essential thread while it is
performing kernel initialization or executing the application’s main()
function. A fatal system error will be raised if the main thread aborts. If
main() is not defined, or if it executes and then performs a normal return,
the main thread terminates normally and no error is raised.
The idle thread executes when there is no other work for the system
to do. If possible, the idle thread should activate the board’s power
management support to save power. Otherwise, the idle thread simply
performs a “do nothing” loop. The idle thread remains in existence as long
as the system is running and never terminates. The idle thread always
uses the lowest configured thread priority. If this makes it a cooperative
thread, then the idle thread repeatedly yields the CPU to allow the
other application threads to run when they need to. The idle thread is
also an essential thread, and a fatal system error is raised if it aborts.
An application-supplied main() function begins executing once kernel
initialization is complete. The kernel does not pass any arguments to the
function. In other words, there are no argc and argv parameters for it.
The following code snippet illustrates a fairly typical implementation
pattern for main().
void main(void) {
/* initialize a semaphore */
...
/* register an ISR that gives the semaphore */
...
/* monitor the semaphore forever */
while (1) {
/* wait for the semaphore to be given by the ISR */
143
Chapter 4 Zephyr RTOS Multithreading
...
/* do whatever processing is now needed */
...
}
}
144
Chapter 4 Zephyr RTOS Multithreading
CONFIG_PRINTK=y
CONFIG_HEAP_MEM_POOL_SIZE=256
CONFIG_ASSERT=y
CONFIG_GPIO=y
145
Chapter 4 Zephyr RTOS Multithreading
This file specifies that the application should include the libraries for
using printk() and for working with the gpio peripherals. The size of the
heap memory pool is specified as 256 bytes, and the use of the __ASSERT()
macro in the kernel code is enabled.
The contents of the CMakeLists.txt file are as follows:
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.13.1)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(threads)
target_sources(app PRIVATE src/main.c)
#include <zephyr.h>
#include <device.h>
#include <drivers/gpio.h>
#include <sys/printk.h>
#include <sys/__assert.h>
#include <string.h>
/* size of stack area used by each thread */
#define STACKSIZE 1024
/* scheduling priority used by each thread */
#define PRIORITY 7
#define LED0_NODE DT_ALIAS(led0)
#define LED1_NODE DT_ALIAS(led1)
struct printk_data_t {
void *fifo_reserved; /* 1st word reserved for use
by fifo */
146
Chapter 4 Zephyr RTOS Multithreading
uint32_t led;
uint32_t cnt;
};
FIFOs in Zephyr
A Zephyr FIFO is a kernel object that implements a (FIFO) queue that
can be used by threads and ISRs to add and remove data items of any
size. The main (typical) use of a FIFO is to asynchronously transfer data
items of arbitrary size in a “first in, first out” manner. The FIFO queue is
implemented as a linked list that, on initialization, is empty. Because the
first word of a data item is a pointer to the next item in the queue, FIFO
data items must be aligned on a word boundary. The FIFO read operation
k_fifo_get() is a blocking operation whose blocking behavior is defined
by the second argument passed to k_fifo_get(). The possible blocking
behaviors are to return at once, block for some specified amount of time,
and block forever. The Zephyr FIFO API consists of the functions/MACROs
K_FIFO_DEFINE, k_fifo_init(), k_fifo_alloc_put(), k_fifo_put(),
k_fifo_put_list(), k_fifo_put_slist(), and k_fifo_get(), and
these are described in the Zephyr documentation.
The Zephyr threads example (zephyr/samples/basic/threads/) defines
the application threads at compile time by including the following macro
calls inside main.c:
147
Chapter 4 Zephyr RTOS Multithreading
The threads all have the same priority, and main.c does not contain a
main() function.
The led blinking functions blink0 and blink1 make use of a led
blinking helper function blink() defined as follows:
148
Chapter 4 Zephyr RTOS Multithreading
k_fifo_put(&printk_fifo, mem_ptr);
k_msleep(sleep_ms);
cnt++;
}
}
The thread entry functions blink0 and blink1 initialize a local variable
containing information about the led they will be flashing and then call the
blink() helper function. Each thread will then be using the same helper
function code independently of the other thread, because each thread has
its own stack.
The code snippet for the LED-associated data structure and for blink0
is shown in the following. blink1 is very similar to that for blink0.
struct led {
const char *gpio_dev_name;
const char *gpio_pin_name;
unsigned int gpio_pin;
unsigned int gpio_flags;
};
void blink0(void) {
const struct led led0 = { #if DT_NODE_HAS_STATUS(LED0_
NODE, okay)
.gpio_dev_name = DT_GPIO_LABEL(LED0_NODE, gpios),
.gpio_pin_name = DT_LABEL(LED0_NODE),
.gpio_pin = DT_GPIO_PIN(LED0_NODE, gpios),
.gpio_flags = GPIO_OUTPUT | DT_GPIO_FLAGS(LED0_
NODE, gpios),
};
blink(&led0, 100, 0);
}
149
Chapter 4 Zephyr RTOS Multithreading
The example uses a fifo for sending data to the fifo object. The uart_
out thread blocks till data to send is available in the application’s global
fifo. The FIFO instance is created as a global variable using the following
macro invocation:
K_FIFO_DEFINE(printk_fifo);
void uart_out(void) {
while (1) {
struct printk_data_t *rx_data =
k_fifo_get(&printk_fifo, K_FOREVER);
printk("Toggled led%d; counter=%d\n",
rx_data->led, rx_data->cnt);
k_free(rx_data);
}
}
Once the message has been sent, the dynamically allocated block of
memory is freed by a call to k_free(rx_data);.
150
Chapter 4 Zephyr RTOS Multithreading
This example can be run on a wide range of boards that are supported
by Zephyr. In the case of boards having only one LED, it may be necessary
to “wire up” the second LED using a breadboard of some sort. This
example can also be run in Renode, using an application targeting a board
that is supported by Renode.
An important lesson to be learned from this code walkthrough is that
implementing multithreaded applications and studying examples of such
applications involve paying careful attention to the various APIs used and
their stateful behavior. The comments in the code and descriptions in the
Zephyr documentation are brief and do not cover all the details in depth.
Understanding and gaining maximum benefit from the various Zephyr
examples requires using the example code as a starting point and then
thinking about various ways in which it might be extended and modified.
Questions to ponder in connection with this example include things
such as the following: What if the priorities of the threads and the UART
are all different? What if the flashing rate of one of the LEDs is very fast?
Can the code be made more robust by handling k_malloc() failures more
gracefully?
151
Chapter 4 Zephyr RTOS Multithreading
152
Chapter 4 Zephyr RTOS Multithreading
153
Chapter 4 Zephyr RTOS Multithreading
associated with the other thread. The two threads, each one running the
code shown, will bring about the “interleaving” of the threads, via mutually
interdependent calls to k_sem_take() and k_sem_give().
#include <zephyr.h>
#include <sys/printk.h>/* size of stack area used by each thread */
#define STACKSIZE 1024
#define PRIORITY 7
#define SLEEPTIME 500
void helloLoop (const char *my_name, struct k_sem *my_sem,
struct k_sem *other_sem) {
const char *tname;
uint8_t cpu;
struct k_thread *current_thread;
while (true) {
k_sem_take(my_sem, K_FOREVER);
current_thread = k_current_get();
tname = k_thread_name_get(current_thread);
cpu = 0;
if (tname == NULL) {
printk("%s: Hello World from cpu %d
on %s!\n",
my_name, cpu, CONFIG_BOARD);
} else {
printk("%s: Hello World from cpu %d
on %s!\n",
tname, cpu, CONFIG_BOARD);
}
k_busy_wait(100000);
k_sem_give(other_sem);
154
Chapter 4 Zephyr RTOS Multithreading
k_msleep(SLEEPTIME);
}
}
K_THREAD_STACK_DEFINE(threadA_stack_area, STACKSIZE);
static struct k_thread threadA_data;
K_THREAD_STACK_DEFINE(threadB_stack_area, STACKSIZE);
static struct k_thread threadB_data;
155
Chapter 4 Zephyr RTOS Multithreading
156
Chapter 4 Zephyr RTOS Multithreading
157
Chapter 4 Zephyr RTOS Multithreading
158
Chapter 4 Zephyr RTOS Multithreading
#include <zephyr.h>
#include <arch/cpu.h>
#include <sys/arch_interface.h>
159
Chapter 4 Zephyr RTOS Multithreading
#define NUM_THREADS 3
#define TCOUNT 10
#define COUNT_LIMIT 12
static int count;
K_MUTEX_DEFINE(count_mutex);
K_CONDVAR_DEFINE(count_threshold_cv);
#define STACK_SIZE (1024)
K_THREAD_STACK_ARRAY_DEFINE(tstacks, NUM_THREADS, STACK_SIZE);
static struct k_thread t[NUM_THREADS];
The threads are created and started in the function main() whose code
is given in this code snippet:
void main(void) {
long t1 = 1, t2 = 2, t3 = 3;
int i;
count = 0;
k_thread_create(&t[0], tstacks[0], STACK_SIZE,
watch_count,
INT_TO_POINTER(t1), NULL, NULL, K_PRIO_PREEMPT(10),
0, K_NO_WAIT);
k_thread_create(&t[1], tstacks[1], STACK_SIZE, inc_count,
INT_TO_POINTER(t2), NULL, NULL, K_PRIO_PREEMPT(10),
0, K_NO_WAIT);
k_thread_create(&t[2], tstacks[2], STACK_SIZE, inc_count,
INT_TO_POINTER(t3), NULL, NULL, K_PRIO_PREEMPT(10),
0, K_NO_WAIT);
/* Wait for all threads to complete */
for (i = 0; i < NUM_THREADS; i++) {
k_thread_join(&t[i], K_FOREVER);
}
160
Chapter 4 Zephyr RTOS Multithreading
When reading the preceding code snippet, note the use of the INT_TO_
POINTER (x) macro. It is used to cast x, a signed integer, to a void* pointer.
When built and run, this application will produce output something
like the following:
161
Chapter 4 Zephyr RTOS Multithreading
162
Chapter 4 Zephyr RTOS Multithreading
163
Chapter 4 Zephyr RTOS Multithreading
A philosopher can only take the fork on their right or the one on their
left as they become available, and they cannot start eating before getting
both forks.
Eating is not limited by the remaining amounts of spaghetti or stomach
space; an infinite supply and an infinite demand are assumed.
This problem was designed to illustrate the challenges involved in
implementing code that avoids deadlock, a system state in which no
progress is possible.
The example solution used in the Zephyr dining philosophers example
is based on the use of multiple preemptible and cooperative threads of
differing priorities, as well as mutexes and thread sleeping. The solution
depends on acquiring resources in a fixed order and then releasing
them in a fixed order. In the example code, a philosopher always tries
to acquire the lowest fork first, and then, when finished eating, giving
back the forks in the reverse order. A philosopher that has two forks is in
the EATING state. Otherwise, the philosopher is in the THINKING state. A
philosopher alternates between the EATING and the THINKING state in a
random manner.
The dining philosophers sample provided in Zephyr is a general-
purpose solution that can be built using one of a number of possible
synchronization strategies, by defining the synchronization mechanism to
use, for example, semaphore, mutex, stack, fifo, or lifo.
The main abstraction used in the sample is the FORK that is #defined
in the example phil_obj_abstract.h file. For example, the following
code snippet shows using mutexes as the underlying synchronization
mechanism:
164
Chapter 4 Zephyr RTOS Multithreading
K_MUTEX_DEFINE(fork2);
K_MUTEX_DEFINE(fork3);
K_MUTEX_DEFINE(fork4);
K_MUTEX_DEFINE(fork5);
#else
#define fork_obj_t struct k_mutex
#define fork_init(x) k_mutex_init(x)
#endif
#define take(x) k_mutex_lock(x, K_FOREVER)
#define drop(x) k_mutex_unlock(x)
#define fork_type_str "mutexes"
The code itself uses the #if 0 ... #endif preprocessor macro
construct to (temporarily) remove segments of code.
In certain contexts, this approach is more effective than using the C
comment-out notation.
For example, to build the sample using mutexes, main.c might start as
follows:
#include <zephyr.h>
#if defined(CONFIG_STDOUT_CONSOLE)
#include <stdio.h>
#else
#include <sys/printk.h>
#endif
#include <sys/__assert.h>
#define MUTEXES 2
/* control the behaviour of the demo **/
#ifndef DEBUG_PRINTF
#define DEBUG_PRINTF 0
#endif
#ifndef NUM_PHIL
165
Chapter 4 Zephyr RTOS Multithreading
#define NUM_PHIL 6
#endif
#ifndef STATIC_OBJS
#define STATIC_OBJS 0
#endif
#ifndef FORKS
#define FORKS MUTEXES
#if 0
#define FORKS SEMAPHORES
#define FORKS STACKS
#define FORKS FIFOS
#define FORKS LIFOS
#endif
#endif
#ifndef SAME_PRIO
#define SAME_PRIO 0
#endif
166
Chapter 4 Zephyr RTOS Multithreading
fork2 = fork(my_id);
} else {
fork1 = fork(my_id);
fork2 = fork(my_id + 1);
}
while (1) {
int32_t delay;
print_phil_state(my_id, " STARVING ", 0);
take(fork1);
print_phil_state(my_id, " HOLDING ONE
FORK ", 0);
take(fork2);
delay = get_random_delay(my_id, 25);
print_phil_state(my_id, " EATING [ %s%d ms ]
", delay);
k_msleep(delay);
drop(fork2);
print_phil_state(my_id, " DROPPED ONE
FORK ", 0);
drop(fork1);
delay = get_random_delay(my_id, 25);
print_phil_state(my_id, " THINKING [ %s%d ms ]
", delay);
k_msleep(delay);
}
}
167
Chapter 4 Zephyr RTOS Multithreading
#if !STATIC_OBJS
for (int i = 0; i < NUM_PHIL; i++) {
fork_init(fork(i));
}
#endif
}
168
Chapter 4 Zephyr RTOS Multithreading
void main(void) {
display_demo_description();
#if CONFIG_TIMESLICING
k_sched_time_slice_set(5000, 0);
#endif
init_objects();
start_threads();
#ifdef CONFIG_COVERAGE
/* Wait a few seconds before main() exit, giving the
sample the opportunity to
dump some output before coverage data gets emitted */
k_sleep(K_MSEC(5000));
#endif
}
Demo Description
----------------
An implementation of a solution to the Dining Philosophers
problem (a classic multi-thread synchronization problem).
169
Chapter 4 Zephyr RTOS Multithreading
170
Chapter 4 Zephyr RTOS Multithreading
T he Zephyr RTOS
Producer-Consumer Example
This section will explore the basic Zephyr RTOS consumer-producer
example [8]. The example is an “example of many parts” and covers many
aspects not covered in simplistic “producer-consumer” examples.
The scenario in this example involves a “sample driver” that receives
incoming data from some source and generates an interrupt with a pointer
to the received data every time a data item is received.
The data is processed by application code, and then the transformed
(processed) data is sent back to the driver. Figure 4-9 illustrates the
movement and processing of data from the device, transforming it and
then writing the transformed data back to the device.
171
Chapter 4 Zephyr RTOS Multithreading
Many Zephyr features are used in the sample code. The features used
include the use of logical applications with their own memory domains,
creating and assigning a kernel system memory heap, a sys_heap, where
the heap is assigned to its own memory partition, and also configuring
and using a thread resource pool. In the application, a message queue (k_
msgq) is used for the transfer of data between the driver and the producer
application, and kernel queues (k_queue) are used for exchanging data
between application threads.
The prod_consumer example involves two applications: application A
and application B.
Application A interfaces with the driver and buffers incoming data,
and application B processes the data. The scenario assumes that the data
involved is untrusted and possibly malicious and, hence, application B is
sandboxed from everything else and has two queues for sending/receiving
data items.
A “pseudocode-like” description of the application code describes the
essential aspects of the producer-consumer example, to help make better
sense of the various code fragments that follow.
Pseudocode for application A
172
Chapter 4 Zephyr RTOS Multithreading
173
Chapter 4 Zephyr RTOS Multithreading
Although the exchange of data and the processing of the data in this
example are relatively uncomplicated, it is the other details such as thread
privileges, assigning permissions for using kernel objects, implementing
a driver, and using system calls that are of particular interest here. This
is what makes this example interesting; it is an “example of many parts”
that demonstrates many of the issues that need to be considered when
implementing “fully fledged” real-world applications. The various parts
covered will extend your knowledge of interesting features of the feature-
rich Zephyr RTOS API.
In the course of exploring this example, the topics covered will include
working with Zephyr RTOS system calls, implementing a driver for a virtual
174
Chapter 4 Zephyr RTOS Multithreading
175
Chapter 4 Zephyr RTOS Multithreading
176
Chapter 4 Zephyr RTOS Multithreading
/* app_syscall.h */
#ifndef APP_SYSCALL_H
177
Chapter 4 Zephyr RTOS Multithreading
#define APP_SYSCALL_H
__syscall int magic_syscall (unsigned int *cookie);
#include <syscalls/app_syscall.h>
#endif /* MAGIC_SYSCALL_H */
Code of app_syscall.c:
#include <syscall_handler.h>
#include <logging/log.h>
LOG_MODULE_REGISTER(app_syscall);
/* magic_syscall() is a custom system call , not part of the
kernel code
* It demonstrates how a syscall can be defined in
application code. */
int z_impl_magic_syscall(unsigned int *cookie) {
LOG_DBG("magic syscall: got a cookie %u", *cookie);
if (*cookie > 42) {
LOG_ERR("bad cookie :(");
return -EINVAL;
}
*cookie = *cookie + 1;
return 0;
}
178
Chapter 4 Zephyr RTOS Multithreading
}
/* Pass *copy* to the implementation the , to
prevent TOCTOU
* (time-of-check to time-of-use) attacks */
ret = z_impl_magic_syscall(&cookie_copy);
if (ret == 0 && z_user_to_copy(cookie, &cookie_copy,
sizeof(*cookie)) != 0) {
return -EPERM;
}
return ret;
}
#include <syscalls/magic_syscall_mrsh.c>
#ifndef ZEPHYR_FAKE_DRIVER_H
#define ZEPHYR_FAKE_DRIVER_H
#include <device.h>
#define SAMPLE_DRIVER_NAME_0 "SAMPLE_DRIVER_0"
#define SAMPLE_DRIVER_MSG_SIZE 128
179
Chapter 4 Zephyr RTOS Multithreading
180
Chapter 4 Zephyr RTOS Multithreading
sample_driver_set_callback_t set_callback;
sample_driver_state_set_t state_set;
};
The fake (virtual) device driver code for the “simple” device type is in
the file simple_driver_foo.c, shown here:
#include "sample_driver.h"
#include <string.h>
#include <kernel.h>
#include <logging/log.h>
LOG_MODULE_REGISTER(sample_driver);
181
Chapter 4 Zephyr RTOS Multithreading
struct sample_driver_foo_dev_data {
const struct device *dev;
sample_driver_callback_t cb;
void *cb_context;
struct k_timer timer; /* to fake 'interrupts' */
uint32_t count;
};
182
Chapter 4 Zephyr RTOS Multithreading
183
Chapter 4 Zephyr RTOS Multithreading
#include <zephyr/kernel.h>
#include <zephyr/syscall_handler.h>
#include "sample_driver.h"
int z_vrfy_sample_driver_state_set(const struct device *dev,
bool active)
{
if (Z_SYSCALL_DRIVER_SAMPLE(dev, state_set)) {
return -EINVAL;
}
184
Chapter 4 Zephyr RTOS Multithreading
#include <syscalls/sample_driver_state_set_mrsh.c>
int z_vrfy_sample_driver_write(const struct device *dev,
void *buf) {
if (Z_SYSCALL_DRIVER_SAMPLE(dev, write)) {
return -EINVAL;
}
if (Z_SYSCALL_MEMORY_READ(buf, SAMPLE_DRIVER_MSG_SIZE)) {
return -EFAULT;
}
return z_impl_sample_driver_write(dev, buf);
}
#include <syscalls/sample_driver_write_mrsh.c>
#ifndef PROD_CONSUMER_APP_A_H
#define PROD_CONSUMER_APP_A_H
#include <kernel.h>
#include <app_memory/app_memdomain.h>
void app_a_entry(void *p1, void *p2, void *p3);
185
Chapter 4 Zephyr RTOS Multithreading
#include "app_shared.h"
/* Define the shared partition, which will contain a memory
region that
* will be accessible by both applications A and B. */
K_APPMEM_PARTITION_DEFINE(shared_partition);
Application A Part
app_a.h file code:
#ifndef PROD_CONSUMER_APP_A_H
#define PROD_CONSUMER_APP_A_H
186
Chapter 4 Zephyr RTOS Multithreading
#include <zephyr/kernel.h>
#include <zephyr/app_memory/app_memdomain.h>
app_a.c file code: The comments in the sample code have been edited
in order to simplify and clarify what the code is doing. Reading through
the commented code should give you an idea of how it works. A deeper
understanding will require reading the documentation and “inventing and
debugging new examples” to test out your understanding.
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/sys/libc-hooks.h>
#include <zephyr/logging/log.h>
#include "sample_driver.h"
#include "app_shared.h"
#include "app_a.h"
#include "app_syscall.h"
LOG_MODULE_REGISTER(app_a);
#define MAX_MSGS 8
/* The K_HEAP_DEFINE macro defined a resource pool to be used
for allocations made by the kernel on behalf of system calls.
It is needed by k_queue_alloc_append() */
K_HEAP_DEFINE(app_a_resource_pool, 256 * 5 + 128);
/* The macro K_APPMEM_PARTITION_DEFINE defines app_a_partition,
which is where the globals for this app will be routed to. The
partition starting address and size are populated by build
187
Chapter 4 Zephyr RTOS Multithreading
188
Chapter 4 Zephyr RTOS Multithreading
/* The monitor thread runs in user mode and pulls the
data out of the message
queue for further writeback.*/
LOG_DBG("monitor thread entered");
ret = sample_driver_state_set (sample_device, true);
if (ret != 0) {
LOG_ERR("couldn't start driver interrupts");
k_oops();
}
189
Chapter 4 Zephyr RTOS Multithreading
190
Chapter 4 Zephyr RTOS Multithreading
191
Chapter 4 Zephyr RTOS Multithreading
sample_driver_write(sample_device, data);
sys_heap_free(&shared_pool, data);
pending_count--;
writeback_count++;
}
192
Chapter 4 Zephyr RTOS Multithreading
/* Set the callback function for the sample driver. This
has to be done from supervisor mode, as this code will
run in supervisor mode in IRQ context. */
sample_driver_set_callback(sample_device, sample_
callback, NULL);
k_thread_create(&writeback_thread, writeback_stack,
K_THREAD_STACK_SIZEOF(writeback_stack),
writeback_entry, NULL, NULL, NULL,
-1, K_USER, K_FOREVER);
193
Chapter 4 Zephyr RTOS Multithreading
k_thread_access_grant(&writeback_thread, &shared_queue_
outgoing, sample_device);
k_thread_start(&writeback_thread);
APP B Part
app_b.h file code:
#ifndef PROD_CONSUMER_APP_B_H
#define PROD_CONSUMER_APP_B_H
#include <zephyr/kernel.h>
#include <zephyr/app_memory/app_memdomain.h>
void app_b_entry(void *p1, void *p2, void *p3);
extern struct k_mem_partition app_b_partition;
194
Chapter 4 Zephyr RTOS Multithreading
#define APP_B_DATA K_APP_DMEM(app_b_partition)
#define APP_B_BSS K_APP_BMEM(app_b_partition)
#endif /* PROD_CONSUMER_APP_B_H */
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/sys/libc-hooks.h>
#include <zephyr/logging/log.h>
#include "app_shared.h"
#include "app_b.h"
LOG_MODULE_REGISTER(app_b);
195
Chapter 4 Zephyr RTOS Multithreading
ARG_UNUSED(p3);
LOG_DBG("processor thread entered");
196
Chapter 4 Zephyr RTOS Multithreading
197
Chapter 4 Zephyr RTOS Multithreading
k_thread_access_grant(k_current_get(), &shared_queue_
incoming, &shared_queue_outgoing);
k_thread_user_mode_enter(processor_thread, NULL,
NULL, NULL);
}
#include <kernel.h>
#include <device.h>
#include <sys/printk.h>
#include <app_memory/app_memdomain.h>
#include <sys/libc-hooks.h>
#include <logging/log.h>
#include "main.h"
#include "sample_driver.h"
#include "app_a.h"
#include "app_b.h"
#define APP_A_STACKSIZE 2048
LOG_MODULE_REGISTER(app_main);
/* Define the shared partition, for a memory region accessible
by both applications A and B.*/
K_APPMEM_PARTITION_DEFINE(shared_partition);
/* Define a memory pool to place in the shared memory area. */
#define BLK_SIZE (SAMPLE_DRIVER_MSG_SIZE + sizeof(void *))
#define HEAP_BYTES (BLK_SIZE * 16)
K_APP_DMEM(shared_partition) struct sys_heap shared_pool;
K_APP_DMEM(shared_partition) uint8_t shared_pool_
mem[HEAP_BYTES];
198
Chapter 4 Zephyr RTOS Multithreading
void main(void) {
LOG_INF("APP A partition: %p %zu", (void *)app_a_
partition.start, (size_t)app_a_partition.size);
LOG_INF("Shared partition: %p %zu", (void *)shared_
partition.start, (size_t)shared_partition.size);
#ifdef Z_LIBC_PARTITION_EXISTS
LOG_INF("libc partition: %p %zu", (void *)z_libc_
partition.start,
(size_t)z_libc_partition.size);
#endif
sys_heap_init(&shared_pool, shared_pool_mem, HEAP_BYTES);
/* Spawn supervisor entry for application A */
k_thread_create(&app_a_thread, app_a_stack, APP_A_
STACKSIZE, app_a_entry, NULL, NULL, NULL, -1,
K_INHERIT_PERMS, K_NO_WAIT);
/* Re-use main for app B supervisor mode setup */
app_b_entry(NULL, NULL, NULL);
}
199
Chapter 4 Zephyr RTOS Multithreading
Memory Partitions
A memory partition’s attributes include a starting memory address, a
size, and access attributes. The purpose of memory partitions is to control
access to system memory. A partition represents a memory region that can
be programmed by the underlying memory management hardware and
conforms to the underlying processor implementation design hardware
200
Chapter 4 Zephyr RTOS Multithreading
201
Chapter 4 Zephyr RTOS Multithreading
#include <zephyr/app_memory/app_memdomain.h>
202
Chapter 4 Zephyr RTOS Multithreading
203
Chapter 4 Zephyr RTOS Multithreading
Any thread may join a memory domain, and a memory domain can
have multiple threads assigned to it. Threads are assigned to memory
domains with the API call:
k_mem_domain_add_thread(&app0_domain, app_thread_id);
204
Chapter 4 Zephyr RTOS Multithreading
205
Chapter 4 Zephyr RTOS Multithreading
PT Sending Message 1
ENC Thread Received Data
ENC PT MSG: PT: message to encrypt
206
Chapter 4 Zephyr RTOS Multithreading
#include <zephyr/sys/__assert.h>
#include <zephyr/sys/libc-hooks.h> /* for z_libc_partition */
#include "main.h"
#include "enc.h"
/* the following definition name prefix is to avoid a
conflict */
#define SAMP_BLOCKSIZE 50
207
Chapter 4 Zephyr RTOS Multithreading
*/
volatile _app_red_b BYTE fBUFIN;
volatile _app_red_b BYTE BUFIN[63];
208
Chapter 4 Zephyr RTOS Multithreading
#endif
_app_ct_d char ctMSG[] = "CT!\n";
void main(void) {
struct k_mem_partition *enc_parts[] = {
#if Z_LIBC_PARTITION_EXISTS
&z_libc_partition,
#endif
&enc_part, &red_part, &blk_part
};
struct k_mem_partition *pt_parts[] = {
#if Z_LIBC_PARTITION_EXISTS
&z_libc_partition,
#endif
&user_part, &red_part
};
k_tid_t tPT, tENC, tCT;
int ret;
fBUFIN = 0; /* clear flags */
fBUFOUT = 0;
209
Chapter 4 Zephyr RTOS Multithreading
k_mem_domain_add_thread(&pt_domain, tPT);
printk("pt_domain Created\n");
210
Chapter 4 Zephyr RTOS Multithreading
ret = k_mem_domain_add_partition(&k_mem_domain_default,
&blk_part);
if (ret != 0) {
printk("Failed to add blk_part to mem domain
(%d)\n", ret);
k_oops();
}
printk("blk partitions installed\n");
k_thread_start(&enc_thread);
/* Start all three threads. with enc going first to
perform an init step */
printk("ENC thread started\n");
k_thread_start(&pt_thread);
printk("PT thread started\n");
k_thread_start(&ct_thread);
211
Chapter 4 Zephyr RTOS Multithreading
k_sem_give(&allforone);
printk("CT thread started\n");
}
212
Chapter 4 Zephyr RTOS Multithreading
break;
}
if (enc_pt[index] >= 'a' && enc_
pt[index] <= 'z') {
enc_ct[index_out] =
(BYTE) enig_enc((BYTE) enc_
pt[index]);
index_out++;
}
}
213
Chapter 4 Zephyr RTOS Multithreading
214
Chapter 4 Zephyr RTOS Multithreading
k_sem_give(&allforone);
}
}
References
1. Zephyr RTOS usermode and overview https://docs.
zephyrproject.org/latest/kernel/usermode/
overview.html)
3. https://embeddedcomputing.com/application/
industrial/industrial-computing/using-a-memory-
protection-unit-with-an-rtos
4. https://events19.linuxfoundation.cn/wp-content/
uploads/2017/11/Retrofitting-Memory-Protection-
in-the-Zephyr-OS_Wayne-Ren-_-Huaqi-Fang.pdf
5. https://docs.zephyrproject.org/latest/kernel/
services/scheduling/index.html
6. https://docs.zephyrproject.org/latest/kernel/
usermode/kernelobjects.html#kernel-objects
215
Chapter 4 Zephyr RTOS Multithreading
216
CHAPTER 5
Message Queues,
Pipes, Mailboxes,
and Workqueues
Message queues, mailboxes, and pipes provide both synchronization
between a producer and a consumer (sender and receiver) as well as
temporary storage of data where necessary. Workqueues represent storage
of requests for work to be done.
Underlying these mechanisms are data structures, algorithms, and
APIs (Application Programming Interfaces).
In the case of a message queue, the maximum size of a message and
the maximum number of messages the circular buffer associated with
the message queue can store are fixed when an instance of the message
queue is created. A mailbox, on the other hand, allows threads to send and
receive messages of any size and makes use of message descriptors, data
structures that contain a pointer to the message in question.
Whereas message queues and mailboxes are concerned with whole
messages, pipes are used for streaming data. Unlike POSIX pipes, Zephyr
pipes are not associated with file descriptors.
Figure 5-1 illustrates a common embedded system application
scenario.
218
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
219
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
220
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
221
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
222
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
When the busy flag is set, pressing the button that triggers receiving a
message will have no effect.
The example demonstrated describes a build for an nRF52840 DK
board, but the example can be built for other boards and also run in
Renode and QEMU.
The buttons are connected to GPIO pins configured to have edge-
triggered interrupts.
Button presses will be acted upon by interrupt handlers detecting
edge-triggered interrupts on the target buttons. The interrupts will trigger
sending or receiving of data by the use of binary counting semaphores.
In this case, the interrupt handlers will control the behavior of the
threads that write and read messages to and from the message queue.
The purpose of this example is to provide some insights into how
Zephyr applications can make use of some of the synchronization and
message passing mechanisms available in Zephyr, some ways of working
with the Zephyr GPIO driver framework, and, additionally, something
about how pseudorandom number generation is supported in Zephyr.
223
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
224
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
225
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
void main(void) {
...
k_msgq_init(
&shd_msgq, shd_msgq_buffer, sizeof(struct data_
item_type), 10U);
...
}
struct data_item_type {
uint32_t num_flashes;
uint32_t which_led;
};
struct patternsCollection {
uint32_t size;
struct data_item_type * patterns;
} patternData;
226
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
void main(void) {
...
init_test_patterns(test_patterns,
sizeof(test_patterns) / sizeof (struct data_
item_type));
patternData.size =
sizeof(test_patterns) / sizeof (struct data_
item_type);
patternData.patterns = test_patterns;
...
}
The code for the global “working” flag and for the button press
interrupt callback functions is defined as shown in the next code snippet:
uint32_t working = 0;
227
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
void main(void) {
...
if (!device_is_ready(button1.port)) {
printk("Error: button1 device %s is not ready\n",
button1.port->name);
return;
}
if (!device_is_ready(button2.port)) {
228
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
ret = gpio_pin_interrupt_configure_dt(&button1,
GPIO_INT_EDGE_TO_ACTIVE);
if (ret != 0) {
printk("Error %d: failed to configure interrupt on
%s pin %d\n",
ret, button1.port->name, button1.pin);
return;
}
ret = gpio_pin_interrupt_configure_dt(&button2,
GPIO_INT_EDGE_TO_ACTIVE);
if (ret != 0) {
229
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
gpio_init_callback(&button1_cb_data, button1_pressed,
BIT(button1.pin));
gpio_add_callback(button1.port, &button1_cb_data);
printk("Set up button at %s pin %d\n", button1.
port->name,
button1.pin);
gpio_init_callback(&button2_cb_data, button2_pressed,
BIT(button2.pin));
gpio_add_callback(button2.port, &button2_cb_data);
printk("Set up button at %s pin %d\n", button2.
port->name,
button2.pin);
...
}
230
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The leds are configured and set up as shown in the following code:
if (led1.port) {
ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT);
if (ret != 0) {
printk("Error %d: failed to configure LED
device %s pin %d\n",
ret, led1.port->name, led1.pin);
led1.port = NULL;
} else {
printk("Set up LED at %s pin %d\n",
led1.port->name, led1.pin);
}
}
if (led2.port && !device_is_ready(led2.port)) {
231
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The code for the sending and receiving threads is not particularly
complicated.
This is the code for the sending thread function (entry point):
while(1) {
k_sem_take(postSem, K_FOREVER);
which_pattern = (sys_rand32_get() % patts->size);
232
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
printk("Sending %d %d \n",
patts->patterns[which_pattern].which_led,
patts->patterns[which_pattern].num_flashes);
res = k_msgq_put(shd_mqueue,
&(patts->patterns[which_pattern]), K_
NO_WAIT);
if (res != 0) {
printk("Mailbox full\n");
}
}
}
And this is the code for the receiving thread function (entry point):
233
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
case 2:
ledp = &led2;
break;
default:
printk("Invalid LED\n");
}
num_flashes = msg.num_flashes;
working = 1;
printk("Led %d, number of flashes %d\n",
msg.which_led, msg.num_flashes);
for (i = 0; i < num_flashes; i++) {
ret = gpio_pin_toggle_dt(ledp);
if (ret < 0) {
return;
}
k_msleep(SLEEP_TIME_MS);
ret = gpio_pin_toggle_dt(ledp);
if (ret < 0) {
return;
}
k_msleep(SLEEP_TIME_MS);
}
working = 0;
}
}
}
234
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
CONFIG_GPIO=y
CONFIG_ENTROPY_GENERATOR=y
CONFIG_TEST_RANDOM_GENERATOR=y
Apart from GPIO, the project also makes use of the Zephyr modules
needed to generate pseudorandom numbers.
Building the project for the nRF52840 target board and using PuTTY to
provide a terminal console produce output such as this:
235
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
Zephyr Mailbox
A mailbox is a kernel object that extends the capabilities of a message
queue object. A mailbox can be used by threads to send and receive
messages of any size synchronously or asynchronously, and the number of
mailboxes that can be defined is limited only by available RAM. A mailbox
instance is referenced by its memory address and has a send queue of
messages that have been sent but not yet received and a receive queue
of threads that are waiting to receive a message. Before starting to use a
mailbox, it must be initialized, setting both of its queues to empty. Because
mailboxes are designed for message exchange between threads, ISRs
(interrupt service routines) cannot use mailboxes to exchange messages,
as an ISR is not associated with any thread.
The use case pattern for mailboxes involves a sending thread sending
a message and a receiving thread receiving the message. A message may
be received by only one thread because the Zephyr implementation does
not support point-to-multipoint (multicast) and broadcast messaging.
Furthermore, the messages exchanged using a mailbox are handled non-
anonymously. The threads participating in an exchange know the identity
236
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
of the other thread. In fact, a thread can even specify the identity of the
thread whose mail messages it is interested in. This feature can be used,
for example, in implementing a managing and monitoring thread that
controls several managed threads on the basis of information received
from these threads. Where there is a requirement to synchronize multiple
tasks, use can be made of Zephyr Events. An event can be used to indicate
that some set of conditions has occurred and can notify multiple threads.
Where only small amounts of data are involved, an event provides a way of
passing small amounts of data to several threads concurrently.
237
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
238
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
239
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
K_MBOX_DEFINE(some_mailbox);
Message Descriptors
Message descriptors are instances of a structure of type k_mbox_msg. This
structure contains fields that can be used in applications as well as other
fields that are only for internal mailbox use. The fields for application use
are info, size, tx_data, tx_target_thread, and rx_source_thread:
240
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
241
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
242
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
Sending a Message
The sequence of steps followed when sending a message is perfectly
logical. To send a message, the sending thread creates (acquires) the data
it wishes to send, if any. It then creates a message descriptor characterizing
the message to be sent.
Finally, the sending thread invokes the mailbox send API to start the
message exchange. In the case that there already is a waiting compatible
thread, the message is passed to that thread straight away; otherwise, the
message is added to the send queue of the mailbox.
The mailbox send queue can contain multiple messages, and these are
sorted according to the priority of the sending thread. Messages of equal
priority are sorted in the order from the oldest to the most recent so that
the oldest message can be received first.
In the case of a synchronous send operation, normal completion
means that the receiving thread has received the message and has actually
retrieved the message data. Where the message is not received before
the timeout specified by the sending thread expires, then the message is
removed from the send queue of the mailbox, and the send operation is
considered to have failed.
When a send operation has completed successfully, the sending thread
can find out which thread actually received the message by reading the
rx_source_thread field of the message descriptor. Similarly, by reading
the corresponding fields, it can find out how much data was actually
exchanged and also the application-defined info value supplied by the
receiving thread.
In synchronous transmission, once a message is received, there is no
limit to the time the receiving thread may take to retrieve the message data
and unblock the sending thread.
Hence, it is possible that a synchronous send operation may block the
sending thread indefinitely, even when the thread specifies a maximum
waiting period. This is because the timeout period only limits the amount
243
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
of time the mailbox will wait for before the message is received by the other
thread. The possibility of such a situation needs to be taken into account
when implementing and testing an application using a mailbox.
In the case of an asynchronous send operation, the operation
completes immediately, and the sending thread can continue doing
something else. A sending thread may optionally specify a semaphore
that the mailbox gives when the message is deleted by the mailbox, for
example, immediately after the message has been received and its data
has been retrieved by a receiving thread. The semaphore can be used as
part of a flow control mechanism that ensures that the mailbox will, at any
given point in time, hold no more than an application-specified number of
messages from a sending thread (or a set of sending threads).
It is also worth noting that where a message is sent asynchronously,
the sending thread has no way of determining which thread received the
message, how much data was exchanged, or the application-defined info
value supplied by the receiving thread.
The following code snippet shows the use of a mailbox to
synchronously pass 4-byte random values to any consuming thread that
needs a random value. In this case, because the message “info” field holds
all of the data required, there is no need to make use of the data portion of
the message.
void producer_thread(void) {
struct k_mbox_msg send_msg;
while (1) {
/* generate random value to send */
uint32_t some_random_value = sys_rand32_get();
244
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
send_msg.tx_target_thread = K_ANY;
/* send message in synchronous mode */
k_mbox_put(&some_mailbox, &send_msg, K_FOREVER);
}
}
void producer_thread(void){
char buffer[128];
int buffer_bytes_used;
struct k_mbox_msg send_msg;
while (1) {
/* generate/acquire the data to send */
...
buffer_bytes_used = ... ;
memcpy(buffer, data_to_send, buffer_bytes_used);
245
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
246
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
247
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The following example will outline the idiom for retrieving data from
the mailbox at receive time by specifying both the actual location and size
of a message buffer when the message is received. In this case, the mailbox
will copy the message data to the specified receive buffer as a part of the
receive operation. If the size of the message buffer is less than the amount
of data sent, then the uncopied data will be lost. Where the amount of data
is less than the size of the buffer, then the unused portion of the buffer is
left unchanged. The mailbox will update the message descriptor of the
receiving thread with the number of bytes (if any) that were copied. The
immediate data retrieval technique is well suited for handling small sized
messages.
The following code snippet demonstrates a scenario involving using
a mailbox to process variable-sized requests from any producing thread,
using immediate data retrieval. The message “info” field provides data
exchange information about the maximum size message buffer that each
thread can handle.
void consumer_thread(void) {
struct k_mbox_msg recv_msg;
char buffer[128];
int i;
int total;
while (1) {
/* Set up the receive message fields */
recv_msg.info = 128;
recv_msg.size = 128;
recv_msg.rx_source_thread = K_ANY;
248
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The next code snippet illustrates a scenario where the retrieval of the
message data takes place at some later point in time.
The receiving thread indicates its intention of retrieving the message
data at some later point in time by specifying a message buffer location of
NULL and a size indicating the maximum amount of data it is willing to
retrieve later.
In this case, the mailbox will not copy any message data sent as part
of the receive operation but will update the message descriptor of the
receiving thread to indicate how many data bytes are available for retrieval.
The response scenario involving the receiving thread can be one of the
following:
249
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The code snippet that follows shows how to use the deferred data
retrieval mechanism to fetch the message data from the producing thread
if the message meets certain conditions. This mechanism, in effect, filters
out unwanted messages. In this example, the message “info” field supplied
by the sender acts as a message classifier.
void consumer_thread(void) {
struct k_mbox_msg recv_msg;
char buffer[10000];
while (1) {
/* prepare to receive message */
recv_msg.size = 10000;
recv_msg.rx_source_thread = K_ANY;
/* get the message, but do not retrieve
its data */
k_mbox_get(&some_mailbox, &recv_msg, NULL,
K_FOREVER);
/* apply a message filter based on the info
field */
if (is_message_type_ok(recv_msg.info)) {
/* retrieve message data and delete message */
k_mbox_data_get(&recv_msg, buffer);
250
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
251
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
char mailbox_send_buffer[256];
char mailbox_receive_buffer[256];
252
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The code for the sender thread will be something like the following:
253
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
send_msg.tx_block.data = NULL;
send_msg.tx_target_thread = mbox_
receiver_tid;
k_mbox_put(shd_mbox_ptr, &send_msg, K_FOREVER);
if (send_msg.size < buffer_bytes_used) {
printk("possible loss of data");
printk("the receiver retrieved %d
bytes", send_msg.info);
}
pos = (pos + 1)%num_parts;
busy_sending = 0;
}
}
In this code, the messages are destined for a specific target thread,
namely, the receiver thread whose thread id is mbox_receiver_tid, and
the data to be sent is copied into a designated buffer, mailbox_send_buffer.
The sending is synchronous as the timeout period is K_FOREVER.
The code for the receiver thread will be something like the following:
254
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
Building and running this program produces output like the following:
255
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
256
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The offsetof() macro is part of the ANSI C library and can be found
in stddef.h. It is used to obtain the offset (in bytes) of a given member of
a struct or union type. It takes two parameters: first a structure name and
secondly the name of a member inside that structure.
A Zephyr application already has a system workqueue, which can be
used without having to create an application-specific workqueue. In an
application, the system workqueue stack size can be configured by setting
the value of CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE to an appropriate value.
The following code snippet demonstrates how to place a work item in
the system workqueue in response to a button press.
257
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
Delayable Work
Zephyr provides a delayable work item that can be added to a workqueue.
Such an item can be used by an ISR or a thread to schedule the processing
of a work item after a specified time delay. A delayable work item contains
a standard work item and a field specifying when and where the item
should be submitted. A delayable work item is initialized and scheduled
to a workqueue using the API functions and macros for delayable work.
When a delayable work item is submitted to workqueue, the kernel starts
a timeout mechanism that is triggered after the specified time delay has
elapsed. When the timeout has triggered, the kernel submits the work item
to workqueue specified. It will, then, be processed as a regular (normal)
work item. The work handler used for delayable work receives a pointer
to the underlying nondelayable work structure, which is not publicly
accessible from k_work_delayable. To access an object that contains the
delayable work object, an idiom such as that shown in the following code
snippet can be used:
258
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
259
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
The button press work handler function that is in the k_work work item
posted to the system workqueue is defined as in this code snippet:
The main() function sets up led1 and button 1 and the button press
interrupt callback function, using the same approach as shown in an
earlier example.
260
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
Building and running this code will send output such as the following
to the console:
261
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
K_WORK_DELAYABLE_DEFINE(delayed_button_press_work,
led_flash_work_handler);
static struct gpio_dt_spec led1 =
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led0), gpios,{0});
K_WORK_DELAYABLE_DEFINE(delayed_button_press_work,
led_flash_work_handler);
k_work_schedule(&delayed_button_press_work,
K_MSEC(DELAY_TIME_MS));
When the application is built and run, the led flash sequence will start
five seconds after the button press. The output sent to the console will be
something like the following:
262
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
parameterised_work_instance parameterised_work_instance1;
parameterised_work_instance parameterised_work_instance2;
K_WORK_DEFINE(button_press_work1, led_flash_work_handler);
K_WORK_DEFINE(button_press_work2, led_flash_work_handler);
263
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
264
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
parameterised_work_instance * pwi_p;
pwi_p = CONTAINER_OF(work_item, struct
parameterised_work,\
work_to_do);
num_flashes = (pwi_p->work_info).num_flashes;
led_num = (pwi_p->work_info).led_num;
struct gpio_dt_spec * pled;
switch (led_num) {
case 1:
pled = &led1;
break;
case 2:
pled = &led2;
break;
default:
pled = NULL;
printk("Invalid led\n");
break;
}
265
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
k_msleep(SLEEP_TIME_MS);
}
}
}
parameterised_work_instance1.work_to_do = button_press_work1;
parameterised_work_instance1.work_info.led_num = 1;
parameterised_work_instance1.work_info.num_flashes = 4;
parameterised_work_instance2.work_to_do = button_press_work2;
parameterised_work_instance2.work_info.led_num = 2;
parameterised_work_instance2.work_info.num_flashes = 7;
When the application is built and run, pressing button 1 will result in
led1 flashing four times, and pressing button 2 will result in led2 flashing
seven times.
The output sent to the console should be something like the following:
266
Chapter 5 Message Queues, Pipes, Mailboxes, and Workqueues
Summary
This chapter has introduced key Zephyr RTOS mechanisms (message
queues, mailboxes, and pipes) for moving data around in multithreading
applications and the synchronization and interprocess communications
made possible by using these mechanisms. The various examples
explored showed coding idioms and patterns that can be used to provide
both synchronization between a producer and a consumer (sender and
receiver) as well as temporary storage of data where necessary, and also
for the movement of data and information from interrupt handles to
threads that consume that data. These idioms and patterns can then be
incorporated into more complex real-world applications.
267
CHAPTER 6
Using Filesystems in
Zephyr Applications
Many IoT and IIoT systems and applications running on them fall into the
“middle ground” where bare metal multitasking has implications from the
point of view of “cross-platform” portability and the use of standardized
APIs (Application Programming Interfaces). Applications using systems in
this “middle ground” have, sometimes, a requirement to store persistent
information in a “structured way” using some kind of file system.
SRAM is a limited resource in the kinds of systems being considered
here, and hence, implementing a small file system in RAM may not be an
optimal solution.
The file systems discussed in this section, FatFs and LittleFS, are,
typically, built on top of QSPI flash memory, or on SD/MMC card-based
systems.
This chapter will begin by introducing QSPI flash and SD/MMC and
how they are supported in Zephyr RTOS. This will be followed by an
introduction and overview of FatFs and LittleFS filesystems and how these
are supported in Zephyr RTOS.
This, in turn, will be followed by an introduction and overview of
Zephyr RTOS support for a “generic POSIX-like” file system API.
Quad-SPI (QSPI)
Quad-SPI [3, 6-9] is, essentially, a serial interface standard, which uses
four data lines to read, write, and erase flash memory chips. A QSPI
device (peripheral) can be used to interface with flash memory that
supports QSPI.
The diagram shown in Figure 6-1 from the ARM mbed-os
documentation (4) illustrates a typical configuration.
SPI as Quad-SPI uses four data lines (I0, I1, I2, and I3) as opposed to
two data lines (MOSI and MISO) in traditional SPI.
SPI has a data transfer rate of up to 16 Mbps, which is quite sufficient
for use cases such as the reading of data from sensors and the sending of
data to actuators or output devices.
However, traditional SPI has limitations when it comes to its use as
a bus for transferring data to and from flash memory. Although flash
memory is cheap and durable (an attractive option when it comes to
embedded system applications), it is inherently slow, and flash devices are
not capable of sending data at the maximum data rates that SPI is capable
of. Prior to the development of QSPI, acceptably high data transfer rates for
transferring data from flash memory were based on using parallel memory
involving 8, 16, or 32 pins (depending on the address range) to connect
external memory devices with the microcontroller. The disadvantages
270
Chapter 6 Using Filesystems in Zephyr Applications
Four bits are transferred every clock cycle, and the bit order is that in
the first clock cycle, IO0 sends bit0, IO1 sends bit1, and in the second clock
cycle, bits 4, 5, 6, and 7 are sent. In effect, a byte can be transmitted in just
two clock cycles.
QSPI also defines a double data rate mode, in which the voltage on
the data line can change on both the rising edge and the falling edge, the
effect of which is to send 2 bits per clock cycle, which doubles the effective
transmission rate.
271
Chapter 6 Using Filesystems in Zephyr Applications
Modern flash chips are, typically, both SPI and QSPI pin compatible.
The advantages of using Quad-SPI are that it involves a smaller pin
count than solutions using a parallel memory bus and that it is possible to
link multiple devices to a single QSPI interface, with a chip select pin being
used to select a particular device.
The details of QSPI on a particular processor are manufacturer
dependent and may include features such as DMA (Direct Memory
Access) support. The low-level chip-specific driver code is accessed
via suitable HAL (Hardware Abstraction Layer) code provided by the
chip vendor.
The next two sections will overview QSPI support in example Nordic
Semiconductor and STM32 devices.
The QSPI peripheral in the Nordic nRF52840 processor is a versatile
device that supports single/dual/quad SPI input/output, has a clock
frequency configurable in the range 2–32 MHz, supports single-word
272
Chapter 6 Using Filesystems in Zephyr Applications
read/write access from/to external flash, and supports DMA (using the
Nordic EasyDMA DMA architecture) for block read and write transfers.
EasyDMA enables a read rate of up to 16 MB/sec EasyDMA read rate.
In addition, the Nordic nRF52840 QSPI peripheral allows for Execute
in Place (XIP) execution of program code directly from external flash. A
key advantage of XIP is that the code to be executed does not need to be
loaded into RAM or processor flash code memory. This is depicted in
Figure 6-4.
In order to use the QSPI peripheral to execute in place (XIP), the start
address of the XIP memory region must be mapped to start at the address
XIPOFFSET of external flash (see Figure 6-5).
273
Chapter 6 Using Filesystems in Zephyr Applications
274
Chapter 6 Using Filesystems in Zephyr Applications
The driver code for working with QSPI flash in Nordic devices is in
the source code file zephyr/drivers/flash/nrf_qspi_nor.c, and that
for working with QSPI flash in STM32 devices is in the source code file
zephyr/drivers/flash/flash_stm32_qspi.c.
275
Chapter 6 Using Filesystems in Zephyr Applications
276
Chapter 6 Using Filesystems in Zephyr Applications
&spi1 {
status = "okay";
cs-gpios = <&porta 27 GPIO_ACTIVE_LOW>;
sdhc0: sdhc@0 {
compatible = "zephyr,sdhc-spi-slot";
reg = <0>;
status = "okay";
277
Chapter 6 Using Filesystems in Zephyr Applications
label = "SDHC_0";
mmc {
compatible = "zephyr,sdmmc-disk";
status = "okay";
label = "SDMMC_0";
};
spi-max-frequency = <24000000>;
};
};
where pdrv is the disk name, data_buf is a pointer to the memory buffer
into which to put the data that is read, start_sector is the starting disk
sector to read from, and num_sector is the number of disk sectors to
be read.
278
Chapter 6 Using Filesystems in Zephyr Applications
The mount point is used as the disk volume name, and it is this which
is used by the file system library when formatting or mounting a disk.
A file system is declared as shown in the following code snippet:
279
Chapter 6 Using Filesystems in Zephyr Applications
.mnt_point = FATFS_MNTP,
.fs_data = &fat_fs,
};
Here, FS_FATFS is the file system type such as FatFs or LittleFS; for
example, FATFS_MNTP is the mount point at which the file system will
be mounted, and fat_fs is the file system data that is used by the fs_
mount() API.
The Zephyr file system API is very similar to the standard POSIX file
system API. The handle to access a given filesystem is a pointer to a data
structure of type struct fs_file_t, and it is initialized, prior to first use,
with fs_file_t_init().
280
Chapter 6 Using Filesystems in Zephyr Applications
281
Chapter 6 Using Filesystems in Zephyr Applications
The fs_sync() function can be used to flush cached write data buffers
of an open file.
A typical use case is to make sure that data gets written to the storage
media immediately, for example, to avoid data loss should the power be
removed unexpectedly.
282
Chapter 6 Using Filesystems in Zephyr Applications
283
Chapter 6 Using Filesystems in Zephyr Applications
284
Chapter 6 Using Filesystems in Zephyr Applications
285
Chapter 6 Using Filesystems in Zephyr Applications
287
Chapter 6 Using Filesystems in Zephyr Applications
288
Chapter 6 Using Filesystems in Zephyr Applications
two files and file that stores a count the number of times the system has
been rebooted, and another file that holds a pattern of byte values that are
modified systematically on each reboot. The purpose of the demonstration
is to show how data can be persisted between reboots and also illustrates
how to work with the Zephyr RTOS filesystem API. This application can
also be run on a board having an SD/MMC card-based file system by
modifying the project configuration files appropriately.
The partition labelled “storage” is used for the file system. If that area
does not already have a compatible LittleFS file system, then its contents
will be replaced with an empty file system.
Building and running the application and then running it and
rebooting several times will produce output something like the following:
289
Chapter 6 Using Filesystems in Zephyr Applications
41 55 55 55 55 55 55 55 42 55 55 55 55 55 55 55
43 55 55 55 55 55 55 55 44 55 55 55 55 55 55 55
45 55 aa
I: /lfs unmounted
/lfs unmount: 0
290
Chapter 6 Using Filesystems in Zephyr Applications
I: FS at flash-controller@4001e000:0xf8000 is 8 0x1000-byte
blocks with 512 cycle
I: sizes: rd 16 ; pr 16 ; ca 64 ; la 32
/lfs mount: 0
/lfs: bsize = 16 ; frsize = 4096 ; blocks = 8 ; bfree = 5
291
Chapter 6 Using Filesystems in Zephyr Applications
3e 55 55 55 55 55 55 55 3f 55 55 55 55 55 55 55
40 55 55 55 55 55 55 55 41 55 55 55 55 55 55 55
42 55 55 55 55 55 55 55 43 55 55 55 55 55 55 55
44 55 55 55 55 55 55 55 45 55 55 55 55 55 55 55
46 55 ab
I: /lfs unmounted
/lfs unmount: 0
292
Chapter 6 Using Filesystems in Zephyr Applications
13 aa aa aa aa aa aa aa 14 aa aa aa aa aa aa aa
15 aa aa aa aa aa aa aa 16 aa aa aa aa aa aa aa
... more lines of output
33 aa aa aa aa aa aa aa 34 aa aa aa aa aa aa aa
35 aa aa aa aa aa aa aa 36 aa aa aa aa aa aa aa
37 aa aa aa aa aa aa aa 38 aa aa aa aa aa aa aa
39 aa aa aa aa aa aa aa 3a aa aa aa aa aa aa aa
3b aa aa aa aa aa aa aa 3c aa aa aa aa aa aa aa
3d aa aa aa aa aa aa aa 3e aa aa aa aa aa aa aa
3f aa aa aa aa aa aa aa 40 aa aa aa aa aa aa aa
41 aa aa aa aa aa aa aa 42 aa aa aa aa aa aa aa
43 aa aa aa aa aa aa aa 44 aa aa aa aa aa aa aa
45 aa aa aa aa aa aa aa 46 aa aa aa aa aa aa aa
47 aa ac
I: /lfs unmounted
/lfs unmount: 0
293
Chapter 6 Using Filesystems in Zephyr Applications
44 55 55 55 55 55 55 55 45 55 55 55 55 55 55 55
46 55 55 55 55 55 55 55 47 55 55 55 55 55 55 55
48 55 ad
I: /lfs unmounted
/lfs unmount: 0
294
Chapter 6 Using Filesystems in Zephyr Applications
The main.c file starts with the required #includes needed for the
filesystem, logging and flash_map APIs and their associated data
structures, the macro for registering main, some #defines for the maximum
length of a path string and the size of the test file used in the example, and
a global byte array used to store the test byte pattern.
#include <stdio.h>
#include <zephyr/zephyr.h>
#include <zephyr/device.h>
#include <zephyr/fs/fs.h>
#include <zephyr/fs/littlefs.h>
#include <zephyr/logging/log.h>
#include <zephyr/storage/flash_map.h>
LOG_MODULE_REGISTER(main);
/* Matches LFS_NAME_MAX */
#define MAX_PATH_LEN 255
#define TEST_FILE_SIZE 547
static uint8_t file_test_pattern[TEST_FILE_SIZE];
295
Chapter 6 Using Filesystems in Zephyr Applications
The filesystem and mount point are declared with the aid of various
helper macros; the corresponding code snippet is the following. If the
required partition node does not exist, then it is created.
296
Chapter 6 Using Filesystems in Zephyr Applications
#ifdef CONFIG_APP_LITTLEFS_STORAGE_FLASH
static int littlefs_flash_erase(unsigned int id) {
const struct flash_area *pfa;
int rc;
rc = flash_area_open(id, &pfa);
if (rc < 0) {
LOG_ERR("FAIL: unable to find flash area %u: %d\n",
id, rc);
return rc;
}
LOG_PRINTK("Area %u at 0x%x on %s for %u bytes\n",
id, (unsigned int)pfa->fa_off, pfa->fa_dev->name,
(unsigned int)pfa->fa_size);
/* Optional wipe flash contents */
if (IS_ENABLED(CONFIG_APP_WIPE_STORAGE)) {
rc = flash_area_erase(pfa, 0, pfa->fa_size);
LOG_ERR("Erasing flash area ... %d", rc);
}
flash_area_close(pfa);
return rc;
}
#ifdef CONFIG_APP_LITTLEFS_STORAGE_FLASH
static int littlefs_mount(struct fs_mount_t *mp) {
int rc;
rc = littlefs_flash_erase((uintptr_t)mp->storage_dev);
if (rc < 0) {
return rc;
}
297
Chapter 6 Using Filesystems in Zephyr Applications
The work of manipulating the two test files in main involves calling
the corresponding functions for manipulating these files as shown in the
following code snippet:
rc = littlefs_increase_infile_value(fname1);
if (rc) {
goto out;
}
rc = littlefs_binary_file_adj(fname2);
if (rc) {
goto out;
}
298
Chapter 6 Using Filesystems in Zephyr Applications
299
Chapter 6 Using Filesystems in Zephyr Applications
goto out;
}
LOG_PRINTK("%s write new boot count %u: [wr:%d]\n", fname,
boot_count, rc);
out:
ret = fs_close(&file);
if (ret < 0) {
LOG_ERR("FAIL: close %s: %d", fname, ret);
return ret;
}
return (rc < 0 ? rc : 0);
}
fs_file_t_init(&file);
rc = fs_open(&file, fname, FS_O_CREATE | FS_O_RDWR);
rc = fs_read(&file, &boot_count, sizeof(boot_count));
LOG_PRINTK("%s read count:%u (bytes: %d)\n", fname, boot_
count, rc);
rc = fs_seek(&file, 0, FS_SEEK_SET);
boot_count += 1;
rc = fs_write(&file, &boot_count, sizeof(boot_count));
LOG_PRINTK("%s write new boot count %u: [wr:%d]\n", fname,
boot_count, rc);
300
Chapter 6 Using Filesystems in Zephyr Applications
301
Chapter 6 Using Filesystems in Zephyr Applications
print_pattern(file_test_pattern, sizeof(file_test_
pattern));
rc = fs_seek(&file, 0, FS_SEEK_SET);
rc = fs_write(&file, file_test_pattern, sizeof(file_test_
pattern));
ret = fs_close(&file);
return (rc < 0 ? rc : 0);
}
Summary
This chapter has overviewed the filesystem APIs that are part of the Zephyr
RTOS framework and walked through a sample application showing how
these APIs can be used with a particular filesystem littlefs. It has also
overviewed the QSPI interface and its uses and the SD/MMC architecture
and its possible uses.
References
1. QSPI — Quad serial peripheral interface
https://infocenter.nordicsemi.com/index.
jsp?topic=%2Fps_nrf52840%2Fqspi.html
2. nordic,nrf-qspi
https://docs.zephyrproject.org/latest/build/
dts/api/bindings/flash_controller/nordic,nrf-
qspi.html
302
Chapter 6 Using Filesystems in Zephyr Applications
4. https://github.com/littlefs-project/
littlefs/blob/master/DESIGN.md
5. zephyr\samples\subsys\fs\littlefs
7. nordic,nrf-qspi
https://docs.zephyrproject.org/latest/build/
dts/api/bindings/flash_controller/nordic,nrf-
qspi.html
8. QuadSPI (QSPI)
https://os.mbed.com/docs/mbed-os/v6.15/apis/
spi-apis.html
Developing Zephyr
BLE Applications
Zephyr RTOS is becoming increasingly important as an RTOS for
developing IoT (Internet of Things) applications, not only smart sensors
but also edge computing devices. In the IoT ecosystem, the importance of
BLE (Bluetooth Low Energy) is as a short-distance radio communications
link for, for example, connecting wearable sensors to mobile devices and
for use in wireless connected computer peripherals such as BLE mice and
BLE keyboards. BLE support and its APIs are, therefore, a very important
part of the Zephyr framework.
This chapter will start by covering BLE (Bluetooth Low Energy)
concepts, terminology, and programming and will also overview the
differences between BLE 4 and the, newer, BLE 5 standards. The next
section will then go on to explore the Nordic Semiconductor BLE-capable
SoC (System on Chip) and Nordic’s closed source BLE stack and how it
is used with Zephyr RTOS, and also introduce Zephyr’s open source BLE
stack, and explore a number of Zephyr RTOS–based BLE applications.
306
Chapter 7 Developing Zephyr BLE Applications
Uses of BLE
BLE can be used for applications such as controlling actuators over a
low bandwidth connection, for personal and wearable devices often in
combination with a BLE-capable smartphone (cell phone), which can
provide a graphical user interface and relay data from a smart BLE sensor
to the cloud. Another use is in the deployment of broadcast-only beacon
devices, which broadcast data that can be discovered and read by other
devices.
BLE Architecture
Like most networking protocols, BLE has a layered architecture.
This is illustrated in Figure 7-2, which shows the various layers and
sublayers of BLE.
307
Chapter 7 Developing Zephyr BLE Applications
There are various acronyms associated with BLE. For the host layer,
these include Generic Access Profile (GAP), Generic Attribute Profile
(GATT), Attribute Protocol (ATT), Security Manager (SM), Logical Link
Control and Adaptation Protocol (L2CAP), and the Host Controller
Interface (HCI) host side. For the link layer, PHY refers to the physical layer,
and HCI refers to the controller side Host Controller Interface.
308
Chapter 7 Developing Zephyr BLE Applications
309
Chapter 7 Developing Zephyr BLE Applications
The three main operational states of a BLE device are the Advertising,
Scanning, and Connected states.
In addition to handling timing, the link layer manages hardware-
accelerated operations such as CRC checksumming, random number
generation, and encryption.
BLE link layer use cases involve role pairs that are played out during
the various phases of discovery/connection. These are the Advertiser/
Scanner (Initiator), Slave/Master, and Broadcaster/Observer role pairs.
BLE distinguishes between unicast (peer-to-peer) and broadcast
connections.
310
Chapter 7 Developing Zephyr BLE Applications
311
Chapter 7 Developing Zephyr BLE Applications
312
Chapter 7 Developing Zephyr BLE Applications
313
Chapter 7 Developing Zephyr BLE Applications
314
Chapter 7 Developing Zephyr BLE Applications
BLE Peripheral
A BLE peripheral device announces its presence by sending out
advertising packets and can accept a connection from another BLE device
(a BLE central), whereas a BLE broadcaster (Beacon) is a device that sends
out advertising packets but does not allow a connection from a central
device. Beacons are commonly used to provide indoor location services.
Broadcasters and peripherals can be distinguished by virtue of the
different types of advertising packets they transmit.
BLE Central
A Central is a device that discovers and listens to other BLE devices that
are advertising and is capable of establishing a connection to a BLE
peripheral. A Central can establish connections with multiple peripherals.
An Observer, on the other hand, discovers and listens to other BLE devices
but cannot initiate connections with a peripheral device.
The features of the various BLE actors are summarized in the
following table:
315
Chapter 7 Developing Zephyr BLE Applications
Does not need to Must have both a Does not need Must have both a
have a radio receiver transmitter and a to have a radio transmitter and a
receiver receiver receiver
There is no Capable of There is no Capable of
bidirectional data bidirectional data bidirectional data bidirectional data
transfer transfer transfer transfer
Can function with Requires a full BLE Can function with Requires a full BLE
reduced hardware software stack reduced hardware software stack
and a reduced and a reduced
BLE software stack BLE software stack
316
Chapter 7 Developing Zephyr BLE Applications
Data Attributes
In BLE, the data exposed by a BLE server is structured in the form of
attributes. An attribute is a generic term for a type of data exposed by the
server and serves to define the structure of that data. An attribute type
is a Universally Unique Identifier or UUID. In the case of a Bluetooth
SIG-Adopted Attribute, it is a 16-bit number. For a custom attribute type
defined by an application developer, it is a 128-bit number. Custom
attribute UUIDs are also referred to as vendor-specific UUIDs.
317
Chapter 7 Developing Zephyr BLE Applications
318
Chapter 7 Developing Zephyr BLE Applications
Characteristics
A characteristic is an essential part of a service and represents a piece of
information/data that the server wants to provide to a client. For example,
the battery level characteristic represents the remaining power level
of a battery in a device, and this information can be read by a client. A
characteristic is made up of Properties and Descriptors, attributes that
are part of the definition of the value held by the characteristic. Properties
are represented by bits that define how a characteristic value can be used,
for example, read, write, write without response, notify, and indicate.
Descriptors contain information related to the characteristic value and
include information such as extended properties, a user description,
fields used for subscribing to notifications and indications, and also a field
defining the presentation of the value such as, for example, the format and
the units of the value.
319
Chapter 7 Developing Zephyr BLE Applications
Profiles
Profiles have a broader scope than services and are used to define various
aspects of the behavior of both the client and server. This covers things
such as services, characteristics, connections, and security requirements.
This needs to be distinguished from pure server-side definitions,
which are concerned only with the implementation of the services and
characteristics on the server side.
There are various BLE SIG-adopted profiles for which official
specifications have been published. A profile specification will generally
contain things such as the definitions of roles and the relationship between
the GATT server and client, required services and service requirements,
and details of how the required services and characteristics are to be used.
A profile specification also provides details of connection
establishment requirements and includes things such as advertising and
connection parameters, and security details.
A service can be specified formally as a GATT XML file. The following
example shows a specification for a Current Time Service:
320
Chapter 7 Developing Zephyr BLE Applications
321
Chapter 7 Developing Zephyr BLE Applications
Attribute Operations
There are six attribute operations. They are Commands, Requests,
Notifications, Responses, Indications, and Confirmations.
• Commands are sent by the client to the server and do
not require a response.
322
Chapter 7 Developing Zephyr BLE Applications
323
Chapter 7 Developing Zephyr BLE Applications
the position at which the sent value should be written within the attribute
value. These sent values are also referred to as prepared values, and they
are used whenever a large value needs to be written that will not fit within
a single message. The prepared values are stored in a buffer on the server
side and not written directly to the attribute. Once all the prepared values
have been sent and received, a write request is used to request the server
to either execute or cancel the write operation of the prepared values. The
server has to respond with a confirmation that the complete attribute value
that was sent to the server has been written.
Bluetooth 5
Bluetooth 5 adds a number of extra features to BLE. These include 2M
PHY, which specifies how to achieve twice the speed of earlier versions of
Bluetooth, and Coded PHY, which makes it possible to extend the range
of earlier versions of Bluetooth and make possible BLE communication
between devices several hundred meters apart. BLE 5 also supports
extended advertisements that make use of secondary advertisement
channels to make it possible for a device to advertise more data than
allowed on the primary advertisement channels. In the case of extended
advertisements, the advertisement packets sent on the primary
advertisement channels provide the information necessary to discover the
offloaded advertisements that are sent on the secondary advertisement
channels. BLE 5 also supports a periodic advertising mode in which two or
more devices communicate in a connectionless manner.
In periodic advertising mode, the peripheral device sends out
synchronization information and other extended advertisement data that
will allow another device to become synchronized with the peripheral
and receive the peripheral deviceʼs extended advertisements at regular,
deterministic intervals.
324
Chapter 7 Developing Zephyr BLE Applications
BLE Security
BLE security is handled by the Security Manager (SM) layer of the
architecture.
The Security Manager defines the protocols and algorithms for
generating and exchanging keys between two devices and involves
mechanisms for pairing, which is the process of creating shared secret
keys between two devices; bonding, which is the process of creating and
storing shared secret keys on each side (central and peripheral) for use
in subsequent connections between the devices; authentication, which
is the process of verifying that the two devices share the same secret
keys; encryption, which is the process of encrypting the data exchanged
between the devices; and, finally, message integrity, which is the process
of signing the data sent and verifying the signature at the receiving end.
These steps are illustrated in the sequence diagram shown in
Figure 7-8.
325
Chapter 7 Developing Zephyr BLE Applications
326
Chapter 7 Developing Zephyr BLE Applications
327
Chapter 7 Developing Zephyr BLE Applications
328
Chapter 7 Developing Zephyr BLE Applications
Note As the nRF Connect for Desktop moves to newer versions, the
screen captures shown here may differ from those actually observed.
329
Chapter 7 Developing Zephyr BLE Applications
Figure 7-10. Starting the nRF Connect for Desktop Bluetooth Low
Energy application
330
Chapter 7 Developing Zephyr BLE Applications
Once the dongle has been programmed, it can be used as either a BLE
central or as a BLE peripheral.
331
Chapter 7 Developing Zephyr BLE Applications
Figure 7-14. Screen captures showing the use of the BLE Dongle in
central mode
332
Chapter 7 Developing Zephyr BLE Applications
333
Chapter 7 Developing Zephyr BLE Applications
334
Chapter 7 Developing Zephyr BLE Applications
335
Chapter 7 Developing Zephyr BLE Applications
336
Chapter 7 Developing Zephyr BLE Applications
337
Chapter 7 Developing Zephyr BLE Applications
338
Chapter 7 Developing Zephyr BLE Applications
339
Chapter 7 Developing Zephyr BLE Applications
The properties for this characteristic such as its initial value and the
read, write, notify, and indicate behaviors can then be set (Figure 7-26).
340
Chapter 7 Developing Zephyr BLE Applications
The last step is to navigate to the Connection Map and click on the gear
button next to the nRF5u dongle listing and then click on Start advertising.
341
Chapter 7 Developing Zephyr BLE Applications
Clicking on the Battery Level will show the details and options for
interacting with the service over the established connection as shown in
the following phone screen image capture.
Clicking on the Battery Service in the Connection Map tab will bring
up a dialog for interacting with the Battery Service on the peripheral.
342
Chapter 7 Developing Zephyr BLE Applications
343
Chapter 7 Developing Zephyr BLE Applications
enum bt_att_chan_opt {
/** Both Enhanced and Unenhanced channels can be used */
BT_ATT_CHAN_OPT_NONE = 0x0,
/** Only Unenhanced channels will be used */
BT_ATT_CHAN_OPT_UNENHANCED_ONLY = BIT(0),
/** Only Enhanced channels will be used */
BT_ATT_CHAN_OPT_ENHANCED_ONLY = BIT(1),
};
344
Chapter 7 Developing Zephyr BLE Applications
345
Chapter 7 Developing Zephyr BLE Applications
struct bt_uuid_128 {
/** UUID generic type. */
struct bt_uuid uuid;
/** UUID value, 128-bit in little-endian format. */
uint8_t val[BT_UUID_SIZE_128];
};
#define BT_UUID_INIT_128(value...) \
{ \
.uuid = { BT_UUID_TYPE_128 }, \
.val = { value }, \
}
#define BT_UUID_DECLARE_128(value...) \
((struct bt_uuid *) ((struct bt_uuid_128[]) \ {BT_UUID_
INIT_128(value)}))
346
Chapter 7 Developing Zephyr BLE Applications
347
Chapter 7 Developing Zephyr BLE Applications
device connects to it, it generates simulated heart rate values. The heart
rate profile is documented on the BLE website at www.bluetooth.com/
specifications/specs/heart-rate-profile-1-0/.
The command can be built using west; the command to build the
application for the nRF52840 DK board is west build -b nrf52840dk_
nrf52840 and then flashed to the target board using the command
west flash.
Running the application and connecting and disconnecting should
produce output something like the following in a terminal console such
as PuTTY:
348
Chapter 7 Developing Zephyr BLE Applications
Connected
[00:01:41.754,089] <wrn> bt_l2cap: Ignoring data for unknown
channel ID 0x003a
[00:01:42.384,185] <inf> hrs: HRS notifications enabled
Disconnected (reason 0x13)
[00:02:31.973,510] <inf> hrs: HRS notifications disabled
The corresponding iPhone screenshots for the nRF Toolbox app are
shown in the following sequence of image captures.
Starting the nRF Toolbox app displays a list of services of possible
interest.
Clicking on the heart rate service (HRS) displays a message saying
there is no connected device and a Connect button.
Clicking on the Connect button brings up a dialogue showing the
result of scanning for services with UUID corresponding to the heart
rate service (see Figure 7-30). Clicking on the heart rate service starts
the connection process and, once connected, plots the heart rate values
as they are sent at regular intervals. The application, as implemented,
also supports the battery service, and the battery level is also displayed.
The connection can be terminated by clicking on the Disconnect button
(Figure 7-31).
349
Chapter 7 Developing Zephyr BLE Applications
350
Chapter 7 Developing Zephyr BLE Applications
351
Chapter 7 Developing Zephyr BLE Applications
The prj.conf file for this project lists the various BLE modules to be
included in the application build.
CONFIG_BT=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_SMP=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DIS=y
CONFIG_BT_DIS_PNP=n
CONFIG_BT_BAS=y
CONFIG_BT_HRS=y
CONFIG_BT_DEVICE_NAME="Zephyr Heartrate Sensor"
CONFIG_BT_DEVICE_APPEARANCE=833
#include <zephyr/types.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/zephyr.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/services/bas.h>
#include <zephyr/bluetooth/services/hrs.h>
352
Chapter 7 Developing Zephyr BLE Applications
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
353
Chapter 7 Developing Zephyr BLE Applications
The battery service (BAS) and heart rate service (HRS) notify functions
that send simulated values are straightforward, as can be seen from the
following code:
bt_hrs_notify(heartrate);
}
354
Chapter 7 Developing Zephyr BLE Applications
ARRAY_SIZE(ad),
NULL, 0);
if (err) {
printk("Advertising failed to start
(err %d)\n", err);
return;
}
printk("Advertising successfully started\n");
}
The application itself, which runs in the thread associated with the
main() function, is relatively straightforward to read. The BLE details are
taken care of by the BLE libraries provided by Zephyr and Nordic (in the
case where the Nordic BLE firmware is used).
void main(void)
{
int err;
355
Chapter 7 Developing Zephyr BLE Applications
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
bt_ready();
bt_conn_auth_cb_register(&auth_cb_display);
while (1) {
k_sleep(K_SECONDS(1));
/* Heartrate measurements simulation */
hrs_notify();
/* Battery level simulation */
bas_notify();
}
}
void main(void)
{
int err;
356
Chapter 7 Developing Zephyr BLE Applications
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
printk("Bluetooth initialized\n");
start_scan();
}
357
Chapter 7 Developing Zephyr BLE Applications
358
Chapter 7 Developing Zephyr BLE Applications
(type == BT_GAP_ADV_TYPE_ADV_IND ||
type == BT_GAP_ADV_TYPE_ADV_DIRECT_IND)
359
Chapter 7 Developing Zephyr BLE Applications
360
Chapter 7 Developing Zephyr BLE Applications
361
Chapter 7 Developing Zephyr BLE Applications
return false;
}
}
return true;
}
The possible AD type values are listed on the Bluetooth SIG website [5].
Here, the type values of interest are the following two.
BT_DATA_UUID16_SOME has a #define of 0x02, which implies that
the data has 16-bit Service Class UUIDs available, but that this is not a
complete list and more are available.
BT_DATA_UUID16_ALL has a #define of 0x03, which implies that the data
is a complete list of 16-bit Service Class UUIDs.
data->data_len % sizeof(uint16_t) != 0U checks that the data is
well formed, which requires it to be a multiple of 2 bytes.
BT_UUID_DECLARE_16(sys_le16_to_cpu(u16)) returns a pointer to a
generic UUID given a 16-bit UUID value in host endian format. sys_le16_
to_cpu takes a 16-bit integer in little-endian format and converts it from
little-endian to host endianness.
UUIDs are compared with bt_uuid_cmp(), which has the function
prototype (defined in the header file uuid.h).
362
Chapter 7 Developing Zephyr BLE Applications
363
Chapter 7 Developing Zephyr BLE Applications
BT_LE_CONN_PARAM(BT_GAP_INIT_CONN_INT_MIN, \
BT_GAP_
INIT_CONN_
INT_MAX, \
0, 400)
BT_CONN_LE_CREATE_PARAM(BT_CONN_LE_OPT_NONE, \
BT_GAP_SCAN_FAST_INTERVAL, \
BT_GAP_SCAN_FAST_INTERVAL)
The connection callbacks for connection events are set up using the
macro BT_CONN_CB_DEFINE as follows:
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
which invokes other macros to carry out the low-level work involved.
The connected function’s main task is to fill out a bt_gatt_discover_
params instance, static struct bt_gatt_discover_params discover_
params, and then to call bt_gatt_discover. struct bt_gatt_discover_
params contains a pointer to the discovery function and is initialized like
this discover_params.func = discover_func;. The job of the discovery
function is to carry out the discovery process by invoking bt_gatt_
discover and then to subscribe to the required service by calling bt_gatt_
364
Chapter 7 Developing Zephyr BLE Applications
365
Chapter 7 Developing Zephyr BLE Applications
}
printk("Connected: %s\n", addr);
if (conn == default_conn) {
memcpy(&uuid, BT_UUID_HRS, sizeof(uuid));
discover_params.uuid = &uuid.uuid;
discover_params.func = discover_func;
discover_params.start_handle = BT_ATT_FIRST_
ATTRIBUTE_HANDLE;
discover_params.end_handle = BT_ATT_LAST_
ATTRIBUTE_HANDLE;
discover_params.type = BT_GATT_DISCOVER_PRIMARY;
err = bt_gatt_discover(default_conn, &discover_
params);
if (err) {
printk("Discover failed(err %d)\n", err);
return;
}
}
}
366
Chapter 7 Developing Zephyr BLE Applications
struct bt_gatt_attr {
const struct bt_uuid *uuid;
bt_gatt_attr_read_func_t read;
bt_gatt_attr_write_func_t write;
void *user_data;
uint16_t handle;
uint16_t perm; /* Will be 0 if returned from bt_gatt_
discover() */
};
367
Chapter 7 Developing Zephyr BLE Applications
discover_params.uuid = &uuid.uuid;
discover_params.start_handle = attr->handle + 1;
discover_params.type = BT_GATT_DISCOVER_
CHARACTERISTIC;
err = bt_gatt_discover(conn, &discover_params);
if (err) {
printk("Discover failed (err %d)\n", err);
}
} else if (!bt_uuid_cmp(discover_params.uuid,
BT_UUID_HRS_MEASUREMENT)) {
memcpy(&uuid, BT_UUID_GATT_CCC, sizeof(uuid));
discover_params.uuid = &uuid.uuid;
discover_params.start_handle = attr->handle + 2;
discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
subscribe_params.value_handle = bt_gatt_attr_value_
handle(attr);
err = bt_gatt_discover(conn, &discover_params);
if (err) {
printk("Discover failed (err %d)\n", err);
}
} else {
subscribe_params.notify = notify_func;
subscribe_params.value = BT_GATT_CCC_NOTIFY;
subscribe_params.ccc_handle = attr->handle;
err = bt_gatt_subscribe(conn, &subscribe_params);
if (err && err != -EALREADY) {
printk("Subscribe failed (err %d)\n", err);
} else {
printk("[SUBSCRIBED]\n");
}
return BT_GATT_ITER_STOP;
368
Chapter 7 Developing Zephyr BLE Applications
}
return BT_GATT_ITER_STOP;
}
subscribe_params.notify = notify_func;
subscribe_params.value = BT_GATT_CCC_NOTIFY;
subscribe_params.ccc_handle = attr->handle;
369
Chapter 7 Developing Zephyr BLE Applications
return BT_GATT_ITER_STOP;
}
printk("[NOTIFICATION] data %p length %u\n", data,
length);
return BT_GATT_ITER_CONTINUE;
}
370
Chapter 7 Developing Zephyr BLE Applications
Bluetooth initialized
Scanning successfully started
[DEVICE]: F6:89:DA:3E:20:DE (random), AD evt type 0, AD data
len 11, RSSI -32
[AD]: 1 data_len 1
[AD]: 3 data_len 6
Connected: F6:89:DA:3E:20:DE (random)
[ATTRIBUTE] handle 25
[ATTRIBUTE] handle 26
[ATTRIBUTE] handle 28
[SUBSCRIBED]
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
[NOTIFICATION] data 0x20007e1b length 2
371
Chapter 7 Developing Zephyr BLE Applications
What Next?
Between them, the Zephyr framework Bluetooth examples and the nRF
Bluetooth examples provided the nRF Connect SDK cover many different
scenarios. The nRF Connect SDK documentation [6] provides links to
descriptions of the nRF Connect SDK Bluetooth samples, and the Zephyr
documentation [7] provides links to descriptions of the Bluetooth samples
that have been contributed to the Zephyr framework. Renode can be
used for multi-node BLE development and debugging purposes, and the
Antmicro blog post [8] describes how to run the central_hr and peripheral_
hr applications together in Renode.
Summary
This chapter has covered the basics of the BLE protocol and explained
key concepts such as central and peripheral, ATT and GATT, advertising,
connecting to a service, and client-server aspects of BLE. It has walked
through the Zephyr RTOS examples showing peripheral and central
application implementation using an nRF52840 DK and an nRF52840
dongle. Some of the important macros associated with the BLE API have
also been overviewed.
References
1. https://docs.nordicsemi.com/bundle/ncs-latest/
page/nrf/protocols/bt/index.html
2. https://microchipdeveloper.com/wireless:ble-
link-layer-discovery
3. https://microchipdeveloper.com/wireless:ble-
link-layer-roles-states
372
Chapter 7 Developing Zephyr BLE Applications
4. https://microchipdeveloper.com/wireless:ble-
link-layer-packet-types
5. www.bluetooth.com/specifications/assigned-
numbers/generic-access-profile
6. https://developer.nordicsemi.com/nRF_Connect_
SDK/doc/latest/nrf/samples/bl.html
7. https://docs.zephyrproject.org/latest/samples/
bluetooth/bluetooth.html
8. h ttps://antmicro.com/blog/2022/04/developing-
and-testing-ble-on-nrf52840-with-renode-
and-zephyr/
373
CHAPTER 8
376
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
377
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
378
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
A network packet priority can then be mapped to the traffic class so that
higher prioritized packets can be processed before lower prioritized ones.
This involves a queue for each priority with the handling for each queue
handled by a separate thread, and a higher traffic class value corresponds to
a lower thread priority value. In the case of transmission classes, where the
number of classes is 0, transmission network traffic is pushed to the driver
directly without any queues. In the case of receive classes, a count value
of 0 means that all the network traffic will be pushed from the driver to the
application thread without any intermediate RX queue. There is a receive
socket queue between the device driver and the application. Disabling the
RX thread has the effect that the network device driver, typically running in
IRQ context, will handle the packet all the way through to the application. A
consequence of this handling is that other incoming packets may be lost if
RX processing is time consuming, relative to the packet arrival rate.
Where USERSPACE support is enabled, then in the current
implementation, at least 1 TX thread and 1 RX thread need to be enabled.
Zephyr can be used to configure promiscuous mode for network
technologies such as Ethernet where this is applicable. If the CONFIG_
NET_PROMISCUOUS_MODE is enabled, then all the network packets
that the network device driver is able to receive will be accepted. If
promiscuous mode is not configured, then only packets destined for
the MAC address of the Ethernet device will be accepted. In general,
promiscuous mode is used when monitoring all traffic. It is not something
that would normally be configured on an embedded system with limited
memory and processing resources.
379
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
states, namely, the administrative state and the operational state. The
administrative state indicates whether an interface is turned ON or OFF. In
Zephyr, it is represented by NET_IF_UP flag, which is controlled by the
application. Its value can be changed by calling the net_if_up() or net_if_
down() function. Just because an interface is up does not mean that it is
ready to transmit or receive packets. The operational state represents the
internal interface status. It is updated whenever an interface is brought up/
down by the application (administrative state changes), or the interface is
notified by the driver/L2 that the PHY (layer 1) status has changed, or the
interface is notified by the driver/L2 that it joined/left a network.
The PHY status is represented with NET_IF_LOWER_UP flag and can
be changed with the functions net_if_carrier_on() and net_if_carrier_off().
By default, this flag is set on for a newly initialized interface. In the case of
Ethernet, for example, the carrier state will be changed when an Ethernet
cable is connected or disconnected.
The network association status is represented with NET_IF_
DORMANT flag and can be changed with net_if_dormant_on() and net_
if_dormant_off(). In the case of Wi-Fi, the dormant state is changed when
the Wi-Fi driver successfully connects to an access point. The Wi-Fi driver
sets the dormant state to ON during initialization, and when it detects that
a connection to a Wi-Fi network has been established, the dormant state is
set to OFF.
The Zephyr network API provides a number of functions for testing
the status of an interface such as net_if_is_admin_up(), net_if_is_
carrier_ok(), and net_if_is_dormant().
Zephyr networking support is quite comprehensive, and the list of
available application layer APIs includes many of the protocols that are
used in IoT applications, such as
• CoAP
• CoAP client
• HTTP client
380
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
• MQTT
• MQTT-SN
• BSD sockets
381
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
382
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
383
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
384
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
385
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
386
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
387
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
The west command to build the server will be something like the
following
and the code can be flashed to the board using the command west
flash --runner jlink.
The board startup messages sent to PuTTY over the USB Serial link are
388
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
389
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
The screenshot in Figure 8-7, meanwhile, shows the use of the net
iface command to obtain details of a particular network interface.
390
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
391
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
Pinging from the PC to the Nucleo board will produce output such as
the following:
392
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
393
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
394
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
# Kernel options
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_ENTROPY_GENERATOR=y
CONFIG_TEST_RANDOM_GENERATOR=y
CONFIG_INIT_STACKS=y
# Logging
CONFIG_NET_LOG=y
CONFIG_LOG=y
CONFIG_NET_STATISTICS=y
CONFIG_PRINTK=y
# Network buffers
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_PKT_TX_COUNT=16
CONFIG_NET_BUF_RX_COUNT=64
CONFIG_NET_BUF_TX_COUNT=64
CONFIG_NET_CONTEXT_NET_PKT_POOL=y
395
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
# IP address options
CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3
CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4
CONFIG_NET_MAX_CONTEXTS=10
# Network shell
CONFIG_NET_SHELL=y
CONFIG_SHELL=y
396
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
397
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
398
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
{
if (mgmt_event == NET_EVENT_IF_xxx) {
/* Handle NET_EVENT_IF_xxx */
} else if (mgmt_event == NET_EVENT_IF_yyy) {
/* Handle NET_EVENT_IF_yyy */
} else if (mgmt_event == NET_EVENT_IPV4_xxx) {
/* Handle NET_EVENT_IPV4_xxx */
} else if (mgmt_event == NET_EVENT_IPV4_yyy) {
/* Handle NET_EVENT_IPV4_yyy */
} else {
/* Spurious (false positive) invocation. */
}
}
void register_cb(void)
{
net_mgmt_init_event_callback(&iface_callback, callback_
handler,
EVENT_IFACE_SET);
net_mgmt_init_event_callback(&ipv4_callback, callback_
handler,
EVENT_IPV4_SET);
net_mgmt_add_event_callback(&iface_callback);
net_mgmt_add_event_callback(&ipv4_callback);
}
ow to Define a Network
H
Management Procedure
Additional management procedures specific to a particular stack
implementation can be provided by defining a handler and registering it
with an associated mgmt_request code.
399
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
400
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
401
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
In connection with the notify function, info and length are disabled if
CONFIG_NET_MGMT_EVENT_INFO is not defined.
The function for waiting synchronously on an event is
402
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
struct net_mgmt_event_callback {
sys_snode_t node;
union {
/* Actual callback function being used to notify
the owner */
net_mgmt_event_handler_t handler;
/* Semaphore meant to be used internally for the
synchronous * net_mgmt_event_wait() function. */
struct k_sem *sync_call;
};
#ifdef CONFIG_NET_MGMT_EVENT_INFO
const void *info;
size_t info_length;
#endif
union {
uint32_t event_mask;
uint32_t raised_event;
};
};
403
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
the actual callback function used to notify the owner, and a struct k_sem
*sync_call, a semaphore used internally for the synchronous net_mgmt_
event_wait() function; and a union made up of a uint32_t event_mask,
a mask of network events on which the handler should be called when
those events occur, and uint32_t raised_event, an internal place holder
for when a synchronous event wait is successfully unlocked on an event.
union net_mgmt_event_callback.[anonymous] [anonymous] is a
mask of network events on which the handler should be called when those
events occur. This kind of mask can be modified as necessary to control
whether a handler will be called or not.
A shell module instance can be connected to various transports for
command input and output. Supported transport layers include Segger
RTT, SMP, Telnet, UART, and USB.
Shell Commands
Shell commands are organized as a tree structure and are grouped into
several categories. Root commands (level 0 commands) are collected
together and organized in an alphabetically sorted manner in a dedicated
memory section. Static subcommands (level > 0) are commands whose
number and syntax are known at compile time and are created in software.
Dynamic subcommands (level > 0) are created dynamically in software,
and their number and syntax do not have to be known at compile time.
404
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
405
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
Dictionary Commands
These are static commands where the command handler processes a key-
value pair that consists of a string (the key) and data corresponding to that
key (the value). The string is typically a description of the given data. The
underlying concept is to use the string as a command prompt syntax and
to have the corresponding data used in the command processing.
406
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
SHELL_SUBCMD_DICT_SET_CREATE(sub_gain, gain_cmd_handler,
(gain1, 1), (gain2, 2), (gain3, 3), (gain4, 4)
);
407
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
# Network shell
CONFIG_NET_SHELL=y
CONFIG_SHELL=y
# Generic networking options
CONFIG_NETWORKING=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_IPV6=y
CONFIG_NET_IPV4=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_SOCKETS_POSIX_NAMES=y
CONFIG_POSIX_MAX_FDS=6
CONFIG_NET_CONNECTION_MANAGER=y
CONFIG_SHELL=y
means that the echo server application will include the shell and the
shell net commands.
The prj.conf statement
CONFIG_NET_CONNECTION_MANAGER=y
means that the application is built with support for IPv4, Ipv6, UDP,
and TCP.
Echo server application initialization is handled by the function
init_app().
If the application is built with TLS support configured in (not the case
here), then code for initializing TLS will be conditionally included. In
the init_app() snippets explored here, the TLS-related code will not be
covered.
408
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
k_sem_init(&quit_lock, 0, K_SEM_MAX_LIMIT);
409
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
k_sem_take(&quit_lock, K_FOREVER);
if (connected) {
stop_udp_and_tcp();
}
}
After the call to k_sem_take, the thread will block till the semaphore is
released. Once unblocked, it will (gracefully) stop the UDP and TCP services.
In main(), after the call to init_app(), the lines of code that follow are
responsible for starting TCP and UDP and the echo server services:
if (!IS_ENABLED(CONFIG_NET_CONNECTION_MANAGER)) {
/* If the config library has not been configured to
* start the app only after we have a connection,
* then we can start it right away.
*/
k_sem_give(&run_app);
}
/* Wait for the connection. */
k_sem_take(&run_app, K_FOREVER);
start_udp_and_tcp();
The code for starting and stopping TCP and UDP is straightforward.
410
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
if (IS_ENABLED(CONFIG_NET_UDP)) {
start_udp();
}
}
static void stop_udp_and_tcp(void) {
LOG_INF("Stopping...");
if (IS_ENABLED(CONFIG_NET_UDP)) {
stop_udp();
}
if (IS_ENABLED(CONFIG_NET_TCP)) {
stop_tcp();
}
}
struct data {
const char *proto;
struct {
int sock;
char recv_buffer[RECV_BUFFER_SIZE];
uint32_t counter;
atomic_t bytes_received;
struct k_work_delayable stats_print;
} udp;
struct {
int sock;
atomic_t bytes_received;
411
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
struct configs {
struct data ipv4;
struct data ipv6;
};
#if defined(CONFIG_USERSPACE)
#include <zephyr/app_memory/app_memdomain.h>
extern struct k_mem_partition app_partition;
412
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
#if defined(CONFIG_USERSPACE)
K_APPMEM_PARTITION_DEFINE(app_partition);
struct k_mem_domain app_domain;
#endif
413
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
void start_udp(void){
if (IS_ENABLED(CONFIG_NET_IPV6)) {
#if defined(CONFIG_USERSPACE)
k_mem_domain_add_thread(&app_domain,
udp6_thread_id);
#endif
k_work_init_delayable(&conf.ipv6.udp.stats_print,
print_stats);
k_thread_name_set(udp6_thread_id, "udp6");
k_thread_start(udp6_thread_id);
}
if (IS_ENABLED(CONFIG_NET_IPV4)) {
#if defined(CONFIG_USERSPACE)
k_mem_domain_add_thread(&app_domain,
udp4_thread_id);
#endif
k_work_init_delayable(&conf.ipv4.udp.stats_print,
print_stats);
k_thread_name_set(udp4_thread_id, "udp4");
k_thread_start(udp4_thread_id);
}
}
414
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
void start_tcp(void) {
int i;
for (i = 0; i < CONFIG_NET_SAMPLE_NUM_HANDLERS; i++) {
conf.ipv6.tcp.accepted[i].sock = -1;
conf.ipv4.tcp.accepted[i].sock = -1;
#if defined(CONFIG_NET_IPV4)
tcp4_handler_in_use[i] = false;
#endif
#if defined(CONFIG_NET_IPV6)
tcp6_handler_in_use[i] = false;
#endif
}
#if defined(CONFIG_NET_IPV6)
#if defined(CONFIG_USERSPACE)
k_mem_domain_add_thread(&app_domain, tcp6_thread_id);
for (i = 0; i < CONFIG_NET_SAMPLE_NUM_HANDLERS; i++) {
k_mem_domain_add_thread(&app_domain,
&tcp6_handler_thread[i]);
k_thread_access_grant(tcp6_thread_id,
&tcp6_handler_thread[i]);
k_thread_access_grant(tcp6_thread_id,
&tcp6_handler_stack[i]);
}
#endif
k_work_init_delayable(&conf.ipv6.tcp.stats_print,
print_stats);
k_thread_start(tcp6_thread_id);
#endif
#if defined(CONFIG_NET_IPV4)
#if defined(CONFIG_USERSPACE)
415
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
k_mem_domain_add_thread(&app_domain, tcp4_thread_id);
for (i = 0; i < CONFIG_NET_SAMPLE_NUM_HANDLERS; i++) {
k_mem_domain_add_thread(&app_domain,
&tcp4_handler_
thread[i]);
k_thread_access_grant(tcp4_thread_id,
&tcp4_handler_
thread[i]);
k_thread_access_grant(tcp4_thread_id,
&tcp4_handler_
stack[i]);
}
#endif
k_work_init_delayable(&conf.ipv4.tcp.stats_print,
print_stats);
k_thread_start(tcp4_thread_id);
#endif
}
K_THREAD_DEFINE(udp4_thread_id, STACK_SIZE,
process_udp4, NULL, NULL, NULL,
THREAD_PRIORITY,
416
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
K_THREAD_DEFINE(udp6_thread_id, STACK_SIZE,
process_udp6, NULL, NULL, NULL,
THREAD_PRIORITY,
IS_ENABLED(CONFIG_USERSPACE) ? K_USER : 0, -1);
and the threads for processing tcp4 and tcp6 are defined similarly
in tcp.c.
K_THREAD_DEFINE(tcp4_thread_id, STACK_SIZE,
process_tcp4, NULL, NULL, NULL,
THREAD_PRIORITY,
IS_ENABLED(CONFIG_USERSPACE) ? K_USER : 0, -1);
K_THREAD_DEFINE(tcp6_thread_id, STACK_SIZE,
process_tcp6, NULL, NULL, NULL,
THREAD_PRIORITY,
IS_ENABLED(CONFIG_USERSPACE) ? K_USER : 0, -1);
417
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
return;
}
k_work_reschedule(&conf.ipv4.udp.stats_print,
K_SECONDS(STATS_
TIMER));
while (ret == 0) {
ret = process_udp(&conf.ipv4);
if (ret < 0) {
quit();
}
}
}
418
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
419
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
data->udp.recv_buffer,
sizeof(data->udp.recv_
buffer), 0,
&client_addr, &client_
addr_len);
if (received < 0) {
/* Socket error */
NET_ERR("UDP (%s): Connection error %d",
data->proto, errno);
ret = -errno;
break;
} else if (received) {
atomic_add(&data->udp.bytes_received,
received);
}
ret = sendto(data->udp.sock, data->udp.recv_buffer,
received, 0, &client_addr, client_addr_len);
if (ret < 0) {
NET_ERR("UDP (%s): Failed to send %d",
data->proto, errno);
ret = -errno;
break;
}
if (++data->udp.counter % 1000 == 0U) {
NET_INFO("%s UDP: Sent %u packets",
data->proto,
data->udp.counter);
}
NET_DBG("UDP (%s): Received and replied with %d
bytes",
data->proto, received);
420
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
The UDP echo server service can be tested out by running a simple
Python script such as the following on a PC:
import socket
testMessageStart = "Test message number"
serverAddressAndPort = ("192.0.2.1", 4242)
bufferSize = 1024
# Create a client side UDP socket
with socket.socket(family=socket.AF_INET, type=socket.
SOCK_DGRAM)\
as UDPClientSocket:
# Send a few test messages to the Echo Server
for num in ("1","2","3","4","5","6","7","8","9") :
message = testMessageStart + " " + num
messageSize = str.encode(message)
UDPClientSocket.sendto(messageSize,
serverAddressAndPort)
serverResponse = UDPClientSocket.recvfrom(bufferSize)
response = "Echo from UDP Server{}" \
.format(serverResponse[0].decode("utf-8"))
print(response)
In the TCP echo server code, in the file tcp.c, the functions process_
tcp4() and process_tcp6() follow the same pattern as process_udp4()
and process_udp6() and ultimately end up calling the tcp processing
function process_tcp(). Because TCP is a connection-oriented streaming
protocol, reading and writing of data over TCP are analogous to reading
and writing to a file. The code for process_tcp() is different to the code for
process_udp().
421
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
The following code snippet shows the code within process_tcp() for
handling IPv4-based TCP requests and responses. The code for handling
IPv4-based TCP requests and responses is similar, and the original source
code can be consulted to see the details.
422
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
&tcp4_handler_thread[slot],
tcp4_handler_stack[slot],
K_THREAD_STACK_SIZEOF(tcp4_handler_
stack[slot]),
(k_thread_entry_t)handle_data,
INT_TO_POINTER(slot), data,
&tcp4_handler_in_use[slot],
THREAD_PRIORITY,
IS_ENABLED(CONFIG_USERSPACE) ? K_USER |
K_INHERIT_PERMS : 0,
K_NO_WAIT);
if (IS_ENABLED(CONFIG_THREAD_NAME)) {
char name[MAX_NAME_LEN];
snprintk(name, sizeof(name),
"tcp4[%d]", slot);
k_thread_name_set(&tcp4_handler_
thread[slot], name);
}
}
return 0;
}
423
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
struct {
int sock;
atomic_t bytes_received;
struct k_work_delayable stats_print;
struct {
int sock;
char recv_buffer[RECV_BUFFER_SIZE];
uint32_t counter;
} accepted[CONFIG_NET_SAMPLE_NUM_HANDLERS];
}
struct {
int sock;
char recv_buffer[RECV_BUFFER_SIZE];
uint32_t counter;
}
424
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
425
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
and the elements in this array are initialized so that for all valid
index values
tcp4_handler_in_use[i] = false;
#if defined(CONFIG_NET_IPV4)
#if defined(CONFIG_USERSPACE)
k_mem_domain_add_thread(&app_domain, tcp4_thread_id);
for (i = 0; i < CONFIG_NET_SAMPLE_NUM_HANDLERS; i++) {
k_mem_domain_add_thread(&app_domain,
&tcp4_handler_thread[i]);
k_thread_access_grant(tcp4_thread_id,
&tcp4_handler_thread[i]);
k_thread_access_grant(tcp4_thread_id,
&tcp4_handler_stack[i]);
}
#endif
426
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
Having located a “free slot,” the next step is to actually use it for
handling the data associated with that TCP connection. Here is the
relevant piece of code, inside the function process_tcp() for carrying out
the handling for an IPv4 connection.
slot = get_free_slot(data);
if (slot < 0) {
LOG_ERR("Cannot accept more connections");
close(client);
return 0;
}
data->tcp.accepted[slot].sock = client;
LOG_INF("TCP (%s): Accepted connection", data->proto);
#if defined(CONFIG_NET_IPV4)
if (client_addr.sin_family == AF_INET) {
tcp4_handler_in_use[slot] = true;
k_thread_create(
&tcp4_handler_thread[slot],
tcp4_handler_stack[slot],
K_THREAD_STACK_SIZEOF(tcp4_handler_
stack[slot]),
(k_thread_entry_t)handle_data,
INT_TO_POINTER(slot), data,
&tcp4_handler_in_use[slot],
THREAD_PRIORITY,
IS_ENABLED(CONFIG_USERSPACE) ? K_USER |
K_INHERIT_PERMS : 0,
K_NO_WAIT);
if (IS_ENABLED(CONFIG_THREAD_NAME)) {
char name[MAX_NAME_LEN];
snprintk(name, sizeof(name),
"tcp4[%d]", slot);
427
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
k_thread_name_set(&tcp4_handler_
thread[slot], name);
}
}
#endif
428
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
break;
} else if (received < 0) {
/* Socket error */
LOG_ERR("TCP (%s): Connection error %d",
data->proto, errno);
break;
} else {
atomic_add(&data->tcp.bytes_received,
received);
}
offset += received;
/* To prevent fragmentation of the response,
reply only
* if buffer is full or there is no more
data to read
*/
if (offset ==
sizeof(data->tcp.accepted[slot].recv_
buffer) ||
(recv(client,
data->tcp.accepted[slot].recv_buffer
+ offset,
sizeof(
data->tcp.accepted[slot].recv_
buffer)-offset,
MSG_PEEK | MSG_DONTWAIT) < 0 &&
(errno == EAGAIN || errno ==
EWOULDBLOCK))) {
ret = sendall(client,
data->tcp.accepted[slot].
recv_buffer,
429
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
offset);
if (ret < 0) {
LOG_ERR("TCP (%s): Failed to send, "
"closing socket", data->proto);
break;
}
LOG_DBG("TCP (%s): \
Received and replied with %d bytes",
data->proto, offset);
if (++data->tcp.accepted[slot].counter %
1000 == 0U
{
LOG_INF("%s TCP: Sent %u packets",
data->proto,
data->tcp.accepted[slot].
counter);
}
offset = 0;
}
} while (true);
*in_use = false;
(void)close(client);
data->tcp.accepted[slot].sock = -1;
}
The TCP server-side code may look complicated, but it is quite logical
and can be considered as providing an idiom for how a TCP server might
be implemented.
430
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
= RESTART: C:\zsdk_Dec_2022\zephyrproject\f767zi_net_prj1-
python-clients\udp_client.py
Echo from UDP Server Test message number 1
Echo from UDP Server Test message number 2
Echo from UDP Server Test message number 3
Echo from UDP Server Test message number 4
Echo from UDP Server Test message number 5
431
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
= RESTART: C:/zsdk_Dec_2022/zephyrproject/f767zi_net_prj1-
python-clients/tcp_client.py
Echo from TCP Server Test message number 1
Echo from TCP Server Test message number 2
Echo from TCP Server Test message number 3
Echo from TCP Server Test message number 4
Echo from TCP Server Test message number 5
Echo from TCP Server Test message number 6
Echo from TCP Server Test message number 7
Echo from TCP Server Test message number 8
Echo from TCP Server Test message number 9
432
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
433
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
S
ummary
In this chapter, the data structures and APIs for implementing and
configuring basic TCP/IP applications, both UDP and TCP, were
overviewed. The example of how to implement a multithreaded TCP
server application was walked through carefully. Additionally the basic
command-line interface provided by Zephyr was introduced.
434
Chapter 8 Zephyr RTOS and Ethernet, Wi-Fi, and TCP/IP
References
1. https://docs.zephyrproject.org/latest/connectivity/
networking/api/index.html
2. zephyr/samples/net/sockets/echo_server/src
3. zephyr/samples/net/sockets/echo_serverprj.cnf
4. https://docs.zephyrproject.org/latest/services/shell/
index.html
435
CHAPTER 9
Understanding and
Working with the
Devicetree in General
and SPI and I2C
in Particular
When developing applications involving a custom board, or adding
peripherals such as I2C or SPI peripherals to an existing board, an
understanding of the Zephyr devicetree and how it is used in application
development is required.
The devicetree concept as used in Zephyr has its origins in Linux,
where the main purpose of the devicetree was to provide a means of
describing nondiscoverable hardware, namely, hardware that is connected
via nondiscoverable protocols such as I2C, SPI, UART, and GPIO. The
kernel, which controls the hardware, needs to know what the connected
devices are and how it is to communicate with these attached devices.
Prior to the introduction of the devicetree approach, information about
such devices had to be hard-coded in the source code. The devicetree
438
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The Zephyr approach to implementing device drivers involves
classifying drivers in various types of driver. A given type of driver has
a generic API (Application Programming Interface) associated with it.
Zephyr defines a device model and the mechanisms for configuring the
drivers used in an application.
The Zephyr device model provides a consistent device model for
configuring the drivers that are part of a system. The device model is
responsible for initializing all the drivers configured into the system.
The API can be thought of as a collection of interface functions for the
corresponding device type, and an instance of a given type of device is
defined by a variable of type struct device, which has the following
structure:
struct device {
const char *name;
const void *config;
const void *api;
void * const data;
};
439
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
that type of device. This has the consequence that when an application
uses a device instance, it does not need to know the details of the internal
implementations of the various API functions involved. An approach like
this goes a long way toward simplifying the task of porting applications
from one architecture to another.
The following code snippets illustrate this approach:
440
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The real API for a particular subsystem will be realized by code that
implements the actual API functions. The corresponding function pointer
values will be used in the initialization of the collection of function
pointers data structure for the API. In the case of the code snippet shown
previously, suppose that the actual driver functions were defined as
follows:
441
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
config and data elements. Yet other complex topics involve scenarios
where drivers may depend on other drivers being initialized first, or where
a driver requires the use of kernel services, or where a driver involves
multiple MMIO (Memory-Mapped Input/Output) regions. Scenarios such
as these are advanced topics that are not covered in this book.
442
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
Although serial protocols such as SPI, I2C, and UART are significantly
slower than higher-speed serial protocols such as USB, Ethernet,
Bluetooth, and Wi-Fi, they have the advantage of being much simpler and
more economical in the use of code and hardware. They are well suited
for scenarios involving communications between microcontrollers or
between microcontrollers and sensors where the amount of data to be
transferred is relatively small and where high data transmission rates are
not needed.
In serial protocols, the actual data bits are sent through a single wire,
and there are two main strategies for sending the serial data, namely,
asynchronous vs. synchronous. In a synchronous protocol as well as
sending the data bits along one wire, a clocking signal is sent along another
wire. The clocking signal tells the receiver when to read the signal level
on the data wire. In an asynchronous protocol, only the data bits are sent,
and the sender and receiver need to have agreed on a common data rate.
Additionally, in asynchronous protocols, it is necessary to also send start
bits and stop bits to “frame” the data bits.
SPI Explained
The Serial Peripheral Interface (SPI) is widely used in embedded system
applications. It is used, for example, in SD memory card modules, RFID
card reader modules, and also in 2.4 GHz wireless transmitter/receivers
in order to communicate with a microcontroller. SPI is a synchronous
protocol. This means that a clock signal is sent on one wire and a data
signal on another wire. This makes it possible to send data bits as a
continuous stream rather than as individual framed data packets. The SPI
protocol has been around for quite some time, being developed, originally,
by Motorola in the mid-1980s.
443
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Multiple SPI devices can be attached to an SPI bus; however, at any
point in time, only two devices can be involved in the movement of data.
In the case of SPI, one device is the master, and it controls the clock signal.
The other device is a slave, and it can only send data when permitted to
do so by the master. In order to select from one of several attached slave
devices, each device has a “slave select” (SS) pin, and a “slave select” line
goes from a pin on the master device to the “select” pin on the slave device.
Hence, a master can control multiple slaves. However, the addition of an
extra slave consumes an extra pin on the microcontroller.
444
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The setup for a simple SPI network involving a master and three slaves
in a multidrop bus configuration is shown in Figure 9-1 [1].
SPI is a “full duplex” protocol because data can be sent from master
to slave and slave to master concurrently. The data is sent on two separate
lines, namely, the MOSI and MISO lines.
The maximum speed for SPI is 10 Mbps; however, in practice,
lower speeds are used. The maximum number of slaves is theoretically
unlimited; however, in practice, only a few SPI slaves are attached to a
microcontroller at a time in real-world applications.
Data transmission requires that the bus master configures the SPI
clock to a frequency that the slave device supports, typically up to several
MHz. Transmission itself involves the master selecting a slave device by
asserting a logic level of 0 on the chosen select line. After a suitable delay, if
required, for example, where the device needs time to complete an analog-
to-digital conversion, the master starts the transmission by beginning to
issue clock cycles. On each SPI clock cycle, a full duplex data transmission
takes place. The master sends a bit on the MOSI line and the slave reads it,
and concurrently, the slave sends a bit on the MISO line, which the master
reads. This sequence is used even when only one-directional data transfer
is being performed.
445
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Advantages and Disadvantages of SPI
Advantages
Disadvantages
I2C Explained
I2C was developed by Philips in the early 1980s as a simple serial protocol
that supported a two-wire interface that could be used to connect
relatively low-speed devices such as microcontrollers, EEPROMs, A/D
and D/A converters, I/O interfaces, and various other peripheral chips in
embedded systems. Because multiple I2C devices could be attached to the
446
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
same bus, and there were no select pins, each attached device had to have
a unique address. The original version of I2C could only run at speeds up
to 100 kHz, and addresses were 7 bits wide. Later, in 1992, in the first public
specification, the specification included a 400 kHz fast-mode and an
enhanced 10-bit-wide address space. Further additions to the specification
included three extra modes: a fast mode, at 1 MHz; a high-speed mode, at
3.4 MHz; and an ultra-fast mode, at 5 MHz, although, such speeds are not
commonly found in standard microcontrollers.
There is also an I2C variant of I2C that was developed by Intel called
SMBus (System Management Bus), which was, originally, designed to
provide more predictable communications between ICs deployed on PC
motherboards. SMBus is relatively low speed with speeds from 10 kHz
to 100 kHz. In addition to “vanilla” I2C, Intel introduced a variant in
1995 called “System Management Bus” (SMBus). SMBus also has a clock
timeout mode designed to make very low-speed operations illegal.
The I2C bus uses two lines: SDA (data line) and SCL (clock line). Data
transmission between master and slave is half-duplex, either from master
to slave or from slave to master. Data transfer between slave and master is
always initiated by the master. The master initiates the data transfer and
generates all the synchronization signals. A slave will only start sending
data when requested to do so by the master. In addition to multiple slaves,
it is also possible to have more than one master on a bus. However, only
one master can be active at any one time.
I2C devices are connected to the bus via an open collector or open
drain. This means that a device may output either a logic zero or nothing
at all (the output is in the high impedance state). If the outputs of all the
connected devices are in the high-impedance state, two external pull-up
resistors, Rp, which must be present, will hold the lines at a high voltage
level (logic 1 state). The resistors have values, typically, in the range from
1K to 10K. The schematic in Figure 9-2 shows a typical setup [2].
447
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Devicetree Configuration
The aforementioned introduction to I2C and SPI devices should provide
a sufficient background to be able to follow the I2C and SPI devicetree
configuration and programming examples that will be introduced later in
this chapter. First, however, it is necessary to acquire some familiarity with
the devicetree syntax and how devicetrees are processed during a Zephyr
application build.
The whole point of devicetrees is to minimize the number of separate
application code projects involving devices and boards in applications that
are broadly similar but which differ in the details of the devices being used.
A devicetree is, essentially, a hierarchical data structure that describes
hardware.
The devicetree specification defines both source and binary
representations. In Zephyr projects, devicetrees are used to describe
the hardware available on supported Boards, as well as the initial
configuration of that hardware. Two types of devicetree input files are
involved, namely, the devicetree sources, which contain the devicetree
itself, and the devicetree bindings, which describe its contents, including
data types.
448
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The Zephyr build system uses devicetree sources and bindings to
produce a “generated” C header. The contents of this generated header are
abstracted by the devicetree.h API, which contains a collection of macros
that can be used to extract device-related information from the project’s
devicetree. This usage pattern is outlined in the schematic in Figure 9-3.
Zephyr and application source code files can include and use
devicetree.h. In Zephyr projects, the devicetree is used in conjunction
with Kconfig. With Kconfig, overrides of default values taken from
devicetree are possible, and furthermore, devicetree information can be
referenced from Kconfig by the use of Kconfig functions.
449
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
The example in Figure 9-4 shows a very simple devicetree [4].
The /dts-v1/; statement in the first line states that the file’s contents
conform to version 1 of the DTS syntax.
The tree has three nodes:
• A root node: /
A node label can be used as a unique shorthand for the node that can
be used to refer to that node from some other place in the devicetree. A
devicetree node can, in fact, have zero or more node labels. The location
of a node in a devicetree is given by the path from the root to that node. A
devicetree path is made up of strings separated by slashes (/).
The path of the root node is a single slash: /. For a non-root node,
the devicetree path to that node is formed by concatenating the node’s
ancestors’ names with the name of the node itself. In the case of the
example devicetree given, the full path to a-sub-node is
/a-node/a-sub-node
450
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The properties of a devicetree node are name/value pairs. A property
value is a sequence of bytes, and a value can be an array of cells, where
a cell is simply a 32-bit unsigned integer. In the preceding devicetree
snippet, the node a-sub-node has a property named foo, and the value of
foo is a cell with a value of 3. The size and type of the value associated with
foo are implied by the enclosing angle brackets (< and >) in the DTS.
Normally, in a devicetree, devicetree nodes correspond to hardware
components, and the node hierarchy is related to the physical organization
of the hardware.
For example, in the case of a board with three I2C peripherals
connected to an I2C bus controller built into an SoC , this could be
represented in the devicetree fragment shown in the next figure.
The I2C peripheral nodes would be children of the bus controller node,
and the corresponding DTS describing this setup would look something
like that shown in Figure 9-5 [4].
451
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
452
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
Unit Addresses and the Devicetree
Understanding and working with unit addresses is the key to the successful
use of devicetrees. Unit addresses are those parts of node names after an
“at” sign (@), such as 40003000 in i2c@40003000, or 39 in apds9960@39.
Unit addresses are optional; for example, in the soc node in the
devicetree example shown previously, there is no unit address associated
with the SoC node. Technically, in a devicetree, the unit address is the
address of a node in the address space of the parent node of that node. A
unit address must be appropriate for the hardware type of the component
that is associated with that node.
In the case of a memory-mapped peripheral, the unit address is the
register map base address of the peripheral. For example, in the case of a
node named i2c@40003000, the name represents an I2C controller whose
register map base address is 0x40003000. In the case of an I2C peripheral,
the address is the address of the peripheral on the I2C bus.
Thus, for a child node apds9960@39 of the I2C controller, the address
being referred to is an I2C bus address. For an SPI peripheral, the address
would be an index representing the chip select line number for that
particular peripheral. (If there is no chip select line, 0 is used.) When
referring to memory, the address is the start address of a physical block of
memory. For example, in the case of a node named memory@2000000, the
address represents RAM starting at the physical address 0x2000000.
Memory, such as flash memory, for example, can be partitioned. The
devicetree snippet shown in Figure 9-7 demonstrates how partitions are
handled in a devicetree [4].
453
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
454
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
values are useful in situations where the device is a specific instance of a more
general family. The use of additional values makes it possible, during the build
process, to match from the most- to the least-specific device drivers. Formally
speaking, according to Zephyr’s bindings syntax, the compatible property has
type string-array.
The label property is the name of a device according to Zephyr’s
Device Driver Model. Its value can be passed to the function device_get_
binding() to retrieve the corresponding driver-level struct device*
pointer. This pointer can then be passed to the correct driver API in the
application code used when interacting with the device. For example,
calling device_get_binding("I2C_0") should return a pointer to a
device structure, which can be passed to I2C API functions such as i2c_
transfer(). The generated C header will contain a macro that expands to
this string.
The reg devicetree node property holds the information used to
address the device. The value is specific to the device (it depends on the
compatible property). The reg property itself is a sequence of (address,
length) pairs. Each pair is called a “register block.” In the case of devices
accessed via memory-mapped I/O registers such as i2c@40003000, for
example, the address is normally the base address of the I/O register space
for that device, and the length is the number of bytes occupied by the
registers.
In the case of I2C devices, such as apds9960@39 and its siblings,
the address is a slave address on the I2C bus. In the case of SPI devices,
the address is a chip select line number, and in this case, there is no
length value.
The devicetree status property is a string that describes whether the
node is enabled or not. The permitted values for this property, according
to the devicetree specifications, are "okay", "disabled", "reserved",
"fail", and "fail-sss". In the case of Zephyr, only the values "okay" and
"disabled" are used. In Zephyr, a node is considered enabled if its status
property is either "okay" or it is not defined (i.e., it does not exist in the
455
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
devicetree source). Nodes with status "disabled" are explicitly disabled.
Devicetree nodes that correspond to physical devices must be enabled
for the corresponding struct device in the Zephyr driver model to be
allocated and initialized.
The interrupts property has information about interrupts generated
by the device, encoded as an array of one or more interrupt specifiers.
Each interrupt specifier has a number of cells, and each cell in an interrupt
specifier can be given a name.
The following table provides a summary of property value types and
how they are represented in the devicetree source.
456
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
In a Zephyr devicetree, a node can also be specified without having to
use its entire path by the use of aliases or chosen nodes. The /aliases and
/chosen nodes do not refer to an actual hardware device – they are used to
specify some other node in the devicetree.
The /chosen node’s properties can be used to configure system- or
subsystem-wide values. This is illustrated in the following example
code snippet in which my-uart is an alias for the node with path /soc/
serial@12340000.
Using its node label uart0, that node can be set as the value of the
chosen zephyr,console node, as shown in the devicetree snippet in
Figure 9-8.
Devicetree Processing
Devicetree processing is fairly complicated, and only an outline will be
given here. It, typically, involves multiple input files and generates multiple
output files.
There are four types of devicetree input files, namely, sources (.dts
file extension), includes (.dtsi file extension), overlays (.overlay file
extension), and bindings (.yaml file extension). In the Zephyr repository
457
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
board, specific devicetree files are to be found in directories described
by the patterns boards/<ARCH>/<BOARD>/<BOARD>.dts, dts/common/
skeleton.dtsi, dts/<ARCH>/.../<SOC>.dtsi, and dts/bindings/.../
binding.yaml.
Typically, a supported board has a BOARD.dts file describing its
hardware, which includes one or more .dtsi files such as .dtsi files
describing the CPU or System on Chip that Zephyr runs on, and these, in
turn, may include other .dtsi files. A BOARD.dts file also describes the
board’s specific hardware.
Devicetree processing is outlined in the schematic in Figure 9-9
provided in the Zephyr documentation [4].
458
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
At the heart of devicetree processing lies the dts/common directory
that contains skeleton.dtsi, a minimal include file for defining a
complete devicetree. Architecture-specific subdirectories (dts/<ARCH>)
contain .dtsi files for CPUs or SoCs that extend skeleton.dtsi. The C
preprocessor is run on all devicetree files to expand macro references.
Includes are typically via #include <filename> directives. DTS also
supports a /include/ "<filename>" syntax. A BOARD.dts file can be
extended or modified using overlays. An overlay is also a DTS file, and it is
used to adapt the base devicetree for specific target purposes.
Zephyr applications can use overlays for various purposes such
as enabling a peripheral that is disabled by default or for selecting a
sensor on a target board for some application-specific purpose. By using
the devicetree together with Kconfig, it is possible to reconfigure the
kernel and the device drivers without needing to modify the application
source code. Overlays can also be used for setting up shields attached to
development boards or kits.
The Zephyr build system automatically picks up overlay files stored
from certain defined locations. A list of overlays to include can also be
specified explicitly via the DTC_OVERLAY_FILE CMake variable. The build
system combines BOARD.dts and .overlay files by concatenating them.
The overlays are added in last. This process makes use of the DTS syntax,
which permits merging overlapping definitions of nodes in the devicetree.
Devicetree bindings are YAML files. Their purpose is to describe
the contents of devicetree sources, includes, and overlays that can be
considered valid. Devicetree bindings are used by the build system
to generate C macros that can then be used in device driver and
application code.
Much of the work of processing devicetree and devicetree-related
files is performed by Python scripts. The scripts/dts/ directory and
the scripts/dts/python-devicetree directories contain the libraries
and scripts used to create output files from input files in the course of
459
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
devicetree processing. They include dtlib.py, a low-level DTS parsing
library; edtlib.py, a library layered on top of dtlib that uses bindings to
interpret properties and provide a higher-level view of the devicetree; and
gen_defines.py, which uses edtlib to generate C preprocessor macros
from the devicetree and bindings. The standard dtc (devicetree compiler)
tool, if installed, can be run on the final devicetree to catch errors or
warnings.
The output files generated from devicetree processing during a
Zephyr application build are created in the application’s build directory.
These include header files that are not intended to be used directly but,
rather, to be accessed from C/C++ code. The header files in question are
devicetree_unfixed.h and device_fixups.h, which are included via
#include statements at the start of devicetree.h. devicetree.h is part of
the Zephyr distribution source code and can be found at zephyr/include/
zephyr/devicetree.h.
From the previous overview, it can be seen that devicetree
construction and building as part of the Zephyr application build process
is quite complex and involves the use of some “powerful and nontrivial”
Python scripts. In the “great scheme of things,” the advantages of this
approach are that it makes for flexibility, API consistency, code reuse, and
portability.
Devicetree Bindings
Devicetree bindings provide more detailed hardware descriptions by
declaring requirements that the contents of a node must satisfy and
also semantic information about the contents of a node. During the
configuration phase of the build process, the build system attempts to
match each node in the devicetree to a binding file. The information in the
binding file is used both to validate the contents of a node and also in the
process of generating macros for working with that node in an application.
460
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The following basic example, based on the Zephyr documentation,
illustrates various aspects of the binding process. It concerns a
hypothetical DTS node bar-device and a matching YAML binding.
Suppose the node entry in a DTS file for bar-device is
bar-device {
compatible = "foo-company,bar-device";
num-foos = <3>;
};
compatible: "foo-company,bar-device"
Properties:
Num-foos:
type: int
required: true
The build system will match the bar-device node to its YAML
binding because the node’s compatible property matches the binding’s
compatible: line. When converting the devicetree’s contents into a
generated devicetree_unfixed.h header file, the build system will use
the given binding to check that the required num-foos property is present
in the bar-device node and that its value, <3>, has the correct type. The
build system will then generate a macro for the bar-device node’s num-foos
property, which will expand to the integer literal 3. This macro can be used
to get the value of that property in the C/C++ application program code.
Where a node has more than one string in its compatible property, the
build system looks for compatible bindings in the listed order and uses the first
match it finds. In the case of a node that does not have compatible properties,
matching will be attempted on bindings associated with the parent node. In the
case where a node describes hardware on a bus, such as an I2C or SPI bus, then
the bus type is also taken into account when matching the node to its binding.
461
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
The build system looks for bindings in dts/bindings subdirectories
in the Zephyr repository, the application source directory, the board
directory of the application as well as directories specified in the DTS_ROOT
CMake variable, and also in any module that defines a dts_root in its
Build settings. When matching nodes to bindings, the build system
will consider any YAML file in any of these locations, including their
subdirectories.
462
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
# Used to match nodes to this binding as discussed above:
compatible: "manufacturer,foo-device"
Properties:
# Requirements for and descriptions of the properties
that this
# binding's nodes need to satisfy go here.
Child-binding:
# You can constrain the children of the nodes
matching this
# binding using this key.
# If the node describes bus hardware, like an SPI bus
controller
# on an SoC, use 'bus:' to say which one, like this:
bus: spi
# If the node instead appears as a device on a bus, like an
external
# SPI memory chip, use 'on-bus:' to say what type of bus,
like this.
# Like 'compatible', this key also influences the way
nodes match
# bindings.
on-bus: spi
Foo-cells:
# "Specifier" cell names for the 'foo' domain go here;
# example 'foo' values are 'gpio', 'pwm', and 'dma'.
# See below for more information.
The Properties key describes the properties that nodes which match
the binding can contain. The following example illustrates what the
binding for a UART peripheral might look like:
compatible: "manufacturer,serial"
463
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Properties:
Reg:
type: array
description: UART peripheral MMIO register space
required: true
Current-speed:
type: int
description: current baud rate
required: true
Label:
type: string
description: human-readable name
required: false
my-serial@abcdcdef {
compatible = "manufacturer,serial";
reg = <0x abcdcdef 0x1000>;
current-speed = <115200>;
label = "UART_0";
};
<property name>:
required: <true | false>
type: <string | int | boolean | array | uint8-array |
string-array | phandle | phandles | phandle-array
464
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
| path |
compound>
deprecated: <true | false>
default: <default>
description: <description of the property>
Enum:
- <item1>
- <item2>
...
- <itemN>
const: <string | int>
The following snippets illustrate how this syntax might be used in practice:
Properties:
# Describes a property, 'current-speed = <115200>;'
which is
# obligatory for this example node, so, set
'required: true'.
Current-speed:
type: int
required: true
description: Initial baud rate for bar-device
# 'keys = "foo", "bar";' describes an optional property
Keys:
type: string-array
required: false
description: Keys for bar-device
465
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
type: string
required: false
description: Configures USB controllers to work up
to a specific speed.
enum:
- "low-speed"
- "full-speed"
- "high-speed"
- "super-speed"
# The next entry describes an optional property, here,
'resolution = <16>;'
# the enum specifies known values that the int property
may take
resolution:
type: int
required: false
enum:
- 8
- 16
- 24
- 32
Array-with-default:
type: array
required: false
default: [1, 2, 3] # Same as 'array-with-default =
<1 2 3>'
String-with-default:
type: string
required: false
default: "foo"
String-array-with-default:
466
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
type: string-array
required: false
default: ["foo", "bar"] # Same as
# 'string-array-with-default = "foo", "bar"'
uint8-array-with-default:
type: uint8-array
required: false
default: [0x12, 0x34] # Same as 'uint8-array-with-
default = [12 34]'
In the case of default values for properties, YAML data types are used
for the default value, and it only makes sense to combine default: with
required: false. Combining default with required: true will raise an
error. A possible risk in using default is that the value in the binding may be
incorrect for some particular board or hardware configuration. In such cases,
a better approach might be to make the property required: true, which will
require the devicetree maintainer to provide the required value explicitly.
Where a node has children that share the same properties, a child-
binding can be used. In this case, each child will be assigned the contents
of the child-binding as its binding. However, an explicit compatible = ...
on the child node will take precedence, if a binding for it can be found. The
following example shows a binding for a PWM LED node where the child
nodes are required to have a pwms property:
pwmleds {
compatible = "pwm-leds";
red_pwm_led {
pwms = <&pwm3 4 15625000>;
};
green_pwm_led {
pwms = <&pwm3 0 15625000>;
};
467
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
/* ... */
};
compatible: "pwm-leds"
child-binding:
description: LED that uses PWM
properties:
pwms:
type: phandle-array
required: true
compatible: foo
child-binding:
child-binding:
Properties:
My-property:
type: int
required: true
will apply to the grandchild node in the code snippet shown here:
parent {
compatible = "foo";
child {
grandchild {
my-property = <123>;
};
};
};
468
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
Binding and Bus Controller Nodes
Where a node is a bus controller, bus: is used in the binding to specify the
type of the bus. For example, a binding for an SPI peripheral on an SoC
would be written as
compatible: "manufacturer,spi-peripheral"
bus: spi
The presence of the bus key in the binding tells the build system that
the children of any node matching this binding correspond to devices
appearing on this type of bus. This influences the way on-bus: is used to
match bindings for the child nodes. Where a node appears as a device on
a bus, the on-bus: key is used in the binding of that node to specify the
type of bus involved, for example, on-bus: spi in the case of an external
SPI, or on-bus: i2c in the case of an I2C-based temperature sensor.
When searching for a binding for a node attached to a bus, the build
system checks to see if the binding for the parent node contains bus: <bus
type>. If it does, then only bindings with a matching on-bus: <bus type>
and bindings without an explicit on-bus key will be considered.
Bindings with an explicit on-bus: <bus type> are searched for before
bindings without an explicit on-bus. Hence, it is possible for the same
device to have different bindings depending on what bus it appears on; for
example, a sensor device with a compatible manufacturer,sensor value
could be used via either I2C or SPI. This is illustrated in the next devicetree
fragment code snippet:
spi-bus@0 {
/* ... some compatible with 'bus: spi', etc. ... */
sensor@0 {
compatible = "manufacturer,sensor";
reg = <0>;
/* ... */
469
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
};
};
i2c-bus@0 {
/* ... some compatible with 'bus: i2c', etc. ... */
sensor@79 {
compatible = "manufacturer,sensor";
reg = <79>;
/* ... */
};
};
# manufacturer,sensor-spi.yaml,
# which matches sensor@0 on the SPI bus:
compatible: "manufacturer,sensor"
on-bus: spi
# manufacturer,sensor-i2c.yaml,
# which matches sensor@79 on the I2C bus:
compatible: "manufacturer,sensor"
properties:
uses-clock-stretching:
type: boolean
required: false
on-bus: i2c
470
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
handles, Phandle-Array Type Properties,
P
and Specifier Cell Names
In devicetree terminology, a phandle value provides a means to reference
another node in the devicetree. Any node that can be referenced defines a
phandle property with a unique <u32> value, and that number is used for
the value of properties with a phandle value type. A phandle-array is a list
of comma-separated p-values. For example a phandle-array describing
pulse width modulation values for a device might be written as follows:
my-device {
pwms = <&pwm0 1 2>, <&pwm3 4>;
};
When an entry such as this is being processed, the tooling strips away
the final s from the property name of this property, resulting in pwm. Then
the value of the #pwm-cells property is looked up in each of the PWM
controller nodes pwm0 and pwm3, as shown here:
pwm0: pwm@0 {
compatible = "foo,pwm";
#pwm-cells = <2>;
};
pwm3: pwm@3 {
compatible = "bar,pwm";
#pwm-cells = <1>;
};
The &pwm0 1 2 part of the property value has two cells, 1 and 2, which
matches #pwm-cells = <2>;, so these cells are considered as the specifier
associated with pwm0 in the phandle array. Similarly, cell 4 is the specifier
associated with pwm3.
471
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
The number of PWM cells in the specifiers in pwms must match the
corresponding #pwm-cells values, as is the case in the preceding example.
An error will be raised if there is a mismatch. A node such as the following,
for example, will result in an error:
my-bad-device {
/* wrong: 2 cells given in the specifier,
but #pwm-cells is 1 in pwm3. */
pwms = <&pwm3 5 6>;
};
The binding for each PWM controller must also have a *-cells key,
where * in an actual file will be replaced with some particular string. In the
pwm example given, it will be pwm-cells. It is this which provides names to
the cells in each specifier.
The following .yaml file snippets show the kinds of bindings that can
be written:
# foo,pwm.yaml
compatible: "foo,pwm"
...
pwm-cells:
- channel
- period
# bar,pwm.yaml
compatible: "bar,pwm"
...
pwm-cells:
- period
472
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
CHANNEL_BY_NAME, for example. Because other property names are derived
from the name of the property by removing the final s, the property name
must end in s, and an error will be raised if it does not. A special case to be
aware of concerns *-gpios properties. Here, something like foo-gpios will
resolve to gpio-cells rather than foo-gpio-cells.
In the case of phandle-array type properties, these support
mapping through
473
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Multiple files can be included by specifying a list of files using either
the syntax
Include:
- foo.yaml
- bar.yaml
Include:
- foo.yaml
- name: bar.yaml
Property-blocklist:
- do-not-include-this-one
- or-this-one
include:
- name: bar.yaml
Child-binding:
Property-allowlist:
- child-prop-to-allow
474
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
ccessing the Devicetree in C and C++
A
Application Code
The Zephyr devicetree build process generates a C header in which all the
required devicetree data is abstracted behind a macro API framework.
Information about a particular devicetree node is obtained by using a
corresponding C macro, which is referred to as a node identifier for that
device. The devicetree build process that goes from a devicetree to struct
device definitions is sometimes referred to as the “Cheshire Cat model of
setting up devices” [5]. This is a metaphor based on Lewis Carroll’s Alice
in Wonderland in which a character, the Cheshire Cat, fades away and all
that remains is its smile. Loosely speaking, the Zephyr approach to setting
up devices involves “messing around with the devicetree,” then “fighting
with the build system” so that when the build system is run, the devicetree
“disappears” and all that is left are the “devices.”
A Zephyr application uses APIs to ask drivers to do real work on real
hardware. The APIs are to be found in C headers, and the drivers are to be
found in C files. This is shown schematically in Figure 9-10.
The key device driver abstraction in Zephyr is that, from the device
perspective, everything is a struct device, and particular kinds of struct
device correspond to particular kinds of driver.
An application will have a .conf Kconfig file specifying which drivers
are needed. For a supported board, there will be a base devicetree in the
Zephyr source code. This base devicetree can be modified for
475
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
application-specific purposes by providing a devicetree overlay file. The
final devicetree is consumed by the device driver source code files, which
will use it to allocate the corresponding struct device structures. Access
to the devices in the application code will be via pointers to actual device
structure instances.
476
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
They will be based on the following dts example code snippet:
/dts-v1/;
/ {
aliases {
sensor-controller = &i2c1;
};
soc {
i2c1: i2c@40002000 {
compatible = "vnd,soc-i2c";
label = "I2C_1";
reg = <0x40002000 0x1000>;
status = "okay";
clock-frequency = < 100000 >;
};
};
};
DT_PATH(soc, i2c_40002000)
DT_NODELABEL(i2c1)
DT_ALIAS(sensor_controller)
DT_INST(x, vnd_soc_i2c) for some unknown number x.
477
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Statements such as the following will result in compiler errors:
void *i2c_0;
unsigned int i2c_1;
long my_i2;
the Zephyr way to initialize these variables is to use C macros, for example:
DT_NODE_HAS_PROP(DT_NODELABEL(i2c1), clock_frequency)
/*expands to 1 */
DT_NODE_HAS_PROP(DT_NODELABEL(i2c1), not_a_property) /*
expands to 0 */
478
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
Recall that the DTS property clock-frequency is spelled clock_
frequency in C, because of the rule that replaces special characters with
underscores and converts names to all lowercase.
The DT_PROP() macro expands to a string literal in the case of strings
and the number 0 or 1 in the case of booleans, for example:
foo: foo@1234 {
a = <1000 2000 3000>; /* array */
b = [aa bb cc dd]; /* uint8-array */
c = "bar", "baz"; /* string-array */
};
479
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Working with reg and interrupts Properties
There are special (specific) macros for working with reg and interrupts
properties.
For a node that only has one register block, DT_REG_ADDR(node_id)
can be used to obtain the register block address for that node, and DT_REG_
SIZE(node_id) can be used to obtain its size.
Where a node has multiple register blocks, the macro DT_REG_ADDR_
BY_IDX(node_id, idx) or DT_REG_SIZE_BY_IDX(node_id, idx) should
be used instead. DT_REG_ADDR_BY_IDX(node_id, idx) gives the address
of register block at index idx, and DT_REG_SIZE_BY_IDX(node_id,
idx) gives the size of the block at index idx. The idx argument must be
an integer literal or a macro that expands to an integer literal without
requiring any arithmetic. In particular, idx cannot be a variable. The
following code snippet will result in a compiler error:
480
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
Working with Devices
The skills to master include knowing how to allocate and configure a
device and how to create a device instance and use it in an application. For
illustrative purposes, a scenario involving a Bosch BME280 environmental
sensor and a nRF52840-based board will be considered. The BME280 is quite
a versatile sensor and is a popular component in “weather station” projects.
It is a humidity sensor that not only measures relative humidity but also
barometric pressure and ambient temperature. It was developed for use in
mobile applications and wearables where size and low power consumption
are key design parameters. The humidity sensor has a low power
consumption and has a high accuracy over a fairly wide temperature range.
An overlay file for working with the BME sensor could be written along
the following lines:
&spi3 {
compatible = "nordic,nrf-spim";
status = "okay";
cs-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
mysensor: bme280@0 {
compatible = "bosch, bme280";
label = "BME280";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
481
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
The Zephyr documentation provides information about bindings. The
top-level web page for API documentation can be found at https://docs.
zephyrproject.org/latest/develop/api/overview.html. It has a table
that lists the various Zephyr APIs and has links to information about them.
The I2C link takes you to the I2C documentation API, which contains
details of the Zephyr I2C API.
Information about bindings can be found in the devicetree bindings
web page at https://docs.zephyrproject.org/latest/build/dts/api/
bindings.html. This page has a section that contains an index of hardware
vendors. Clicking on a vendor’s name will bring up a list of bindings for
that vendor. Clicking on the Bosch Sensortec GmbH (bosch) goes to the
subsection with links to various bosch sensors, including bosch,bme280
(on SPI bus) and bosch,bme280 (on I2C bus). Clicking on a link leads to a
page containing a table of base properties and a table of properties that are
not inherited from the base binding file.
For the bosch,bme280 on an SPI bus, the base properties table is as
follows:
482
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
483
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
and for the bosch bme280 on an SPI bus, the node-specific properties
table is as follows:
485
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Nordic nRF processors support a Nordic-specific form of DMA
(Direct Memory Access) called EasyDMA. An SPI master with EasyDMA
is a nordic,nrf-spim device. This device is an SPI bus node. The base
properties in the previous table are those of base for SPI devices. The
table that follows summarizes the node-specific properties for Nordic nRF
family SPIM (SPI master with EasyDMA) nodes.
486
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
overrun- int The overrun character (ORC) is used when all bytes from
character the TX buffer are sent, but the transfer continues due to
RX. Defaults to 0xff (line high), the most common value
used in SPI transfers.
Default value: 255
clock- int Clock frequency the SPI peripheral is being driven at, in Hz.
frequency
487
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
cs-gpios phandle- An array of chip select GPIOs to use. Each element in the
array array specifies a GPIO. The index in the array corresponds to
the child node that the CS gpio controls.
Example:
spi@... {
cs-gpios = <&gpio0 23 GPIO_ACTIVE_LOW>,
<&gpio1 10 GPIO_ACTIVE_LOW>,
...;
spi-device@0 {
reg = <0>;
...
};
spi-device@1 {
reg = <1>;
...
};
...
};
pinctrl-0 phandles Pin configuration/s for the first state. Content is specific to
the selected pin controller driver implementation.
pinctrl-1 phandles Pin configuration/s for the second state. Content is specific
to the selected pin controller driver implementation.
pinctrl-2 phandles Pin configuration/s for the third state. Content is specific to
the selected pin controller driver implementation.
pinctrl-3 phandles Pin configuration/s for the fourth state. Content is specific to
the selected pin controller driver implementation.
pinctrl-4 phandles Pin configuration/s for the fifth state. Content is specific to
the selected pin controller driver implementation.
pinctrl- string- Names for the provided states. The number of names needs
names array to match the number of states.
memory- phandle- List of memory region phandles
regions array
memory- string- A list of names, one for each corresponding phandle in
region- array memory-region
names
For the cs-gpios property, the GPIO port and pin number and flags for
the chip select must be given, for example, in the following code snippet:
&spi3 {
compatible = "nordic,nrf-spim";
status = "okay";
cs-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
mysensor: bme280@0 {
compatible = "bosch, bme280";
label = "BME280";
reg = <0>;
489
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
spi-max-frequency = <1000000>;
};
};
• GPIO_ACTIVE_LOW is a flag
From the preceding snippet, it can be deduced that active low drives
the chip select. Zephyr has a driver compatible for bme280 sensor, but the
sensor is not built into the board. The node is a child of the bus controller.
With an SPI device, it is necessary to specify the chip select for the
particular sensor, and this is taken care of by the reg property. Here, reg =
<0> means use index 0 of the cs-gpios property.
mysensor (on line 5) is a node label ... and is not to be confused with
the label property (line 6). Either of these can be used to get the device
pointer when implementing application code.
The following code snippet shows how to write the code to get a
pointer to the device structure and then make use of the device. It shows
the use of the macro DEVICE_DT_GET and of the DT_NODELABEL macro.
490
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
If using the I2C interface on an nRF5 device and using EasyDMA,
the node will be an “i2c” bus node of the Nordic nRF family TWIM (TWI
master with EasyDMA), namely, nordic,nrf-twim. The node specific
properties for this type of node can be found in Zephyr documentation [8]
and the nRF Connect SDK documentation [9].
The overlay for the BME280 sensor attached to an I2C bus controller is
shown as follows:
&i2c0 {
compatible = "nordic, nrf-twim";
status = "okay";
sda_pin = <26>;
scl-pin = <27>;
mysensor: bme280@77 {
compatible = "bosch, bme280";
reg = <0x77>;
label = "BME280";
};
};
When setting up the sensor device attached to the I2C bus, the sensor
is a child of the I2C bus controller – where the bus node is i2c0. The
BME280 node has the same compatible as for the SPI example covered
previously, but because of the bus tree hierarchy, the driver will know
that the communication is for I2C and that I2C should be used. Here,
reg = <0x77>, which is the address to be used by the i2c0 bus master to
communicate with the sensor.
Using the preceding overlay, the code for using the I2C sensor that
corresponds to that shown previously for the SPI sensor will be the same:
491
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
struct sensor_value temp;
sensor_sample_fetch(dev);
sensor_channel_get (dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
printk("temperature in degrees C: %d.%06d\n", temp.val1,
temp.val2);
expands to
492
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
In the file zephyr/drivers/sensor/bme280/bme280.c, the following
snippet can be found:
#define BME280_DEFINE(inst) \
static struct bme280_data bme280_data_##inst; \
static const struct bme280_config bme280_config_##inst = \
COND_CODE_1(DT_INST_ON_BUS(inst, spi), \
(BME280_CONFIG_SPI(inst)), \
(BME280_CONFIG_I2C(inst))); \
\
PM_DEVICE_DT_INST_DEFINE(inst, bme280_pm_action); \
\
DEVICE_DT_INST_DEFINE(inst, \
bme280_chip_init, \
PM_DEVICE_DT_INST_REF(inst), \
&bme280_data_##inst, \
&bme280_config_##inst, \
POST_KERNEL, \
CONFIG_SENSOR_INIT_PRIORITY, \
&bme280_api_funcs);
/* Create the struct device for every status "okay" node in the
devicetree. */
DT_INST_FOREACH_STATUS_OKAY(BME280_DEFINE)
493
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
Expanding the macro
DEVICE_DT_INST_DEFINE(inst, \
bme280_chip_init, \
PM_DEVICE_DT_INST_REF(inst), \
&bme280_data_##inst, \
&bme280_config_##inst, \
POST_KERNEL, \
CONFIG_SENSOR_INIT_PRIORITY, \
&bme280_api_funcs);
in the driver, so that the global variable token contains the ordinal
number 63.
The ordinal number is the result of enumerating the nodes in the
devicetree. Each node will have a distinct ordinal number associated with
it, and this ensures that devices will be associated with uniquely named
global variables.
In the case of a multi-instance driver application, there will be multiple
different struct devices from different devicetree nodes, all with the same
compatible. This is illustrated in the following example involving two
compatibles, one for a device attached to an I2C bus and the other for a
device attached to an SPI bus.
494
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
&spi3 {
/* ... */
onspi: bme280@0 {
compatible = = "bosch, bme280";
label = "BME280";
reg = <0>;
spi-max-frequency = <1000000>
};
};
It should be noted that instance numbers are not the same thing as
ordinal numbers. In this, next, example, the instance numbers are 0 and 1.
In the application, there will be two devices produced as a result of macro
expansion. The macro expansion of
495
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
The devicetree processing generates a header file that pre-declares
all the potential devices in that header file so that they do not have to be
declared by the application developer before taking their addresses.
Codewise the results will have the same footprint as one involving an
access to a global data structure. However, the macro-based approach
combines a hierarchical configuration language without incurring the
footprint and performance penalty of having to explicitly carry out
tree-walking.
In summary, the devicetree-based design achieves the following
outcome:
496
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
I2C Case Study Example
This example is based on the X-NUCLEO-IKS01A2 board, which is a
motion MEMS and environmental sensor expansion board for the STM32
Nucleo with an Arduino UNO R3 connector layout. It contains a selection
of sensors, namely, an LSM6DSL 3D accelerometer and 3D gyroscope,
an LSM303AGR 3D accelerometer and 3D magnetometer, an HTS221
humidity and temperature sensor, and an LPS22HB pressure sensor. The
X-NUCLEO-IKS01A2 interfaces with the STM32 microcontroller via I2C.
An image of this board is shown in Figure 9-11.
497
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
connected to an external main board by an I2C bus; all other devices are
slaves connected to LSM6DSL via I2Caux. This requires configuring the
board by setting some jumpers on it as in Figure 9-12, JP7: 2-3 (I2C1 = I2Cx)
and JP8: 2-3 (I2C1 = I2Cx).
498
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
The output sent by the application over the serial link will look
something like the following:
CONFIG_LOG=y
CONFIG_STDOUT_CONSOLE=y
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_SENSOR_LOG_LEVEL_DBG=y
CONFIG_LSM6DSL=y
CONFIG_LSM6DSL_TRIGGER_OWN_THREAD=y
# The LSM6DSL shub driver only permits one sensor
# connected at a time
CONFIG_LSM6DSL_SENSORHUB=y
CONFIG_LSM6DSL_EXT0_LPS22HB=n
CONFIG_LSM6DSL_EXT0_LIS2MDL=y
CONFIG_CBPRINTF_FP_SUPPORT=y
The CMakeLists.txt file script used for this project is shown next:
cmake_minimum_required(VERSION 3.20.0)
499
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
project(x_nucleo_iks01a2_sensorhub)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})
and the overlay file for the project, which is to be found at zephyr\
boards\shields\x_nucleo_iks01a2\x_nucleo_iks01a2.overlay
its contents are shown as follows:
&arduino_i2c {
hts221@5f {
compatible = "st,hts221";
reg = <0x5f>;
label = "HTS221";
};
lps22hb-press@5d {
compatible = "st,lps22hb-press";
reg = <0x5d>;
label = "LPS22HB";
};
lsm6dsl@6b {
compatible = "st,lsm6dsl";
reg = <0x6b>;
label = "LSM6DSL";
irq-gpios = <&arduino_header 10 GPIO_ACTIVE_
HIGH>; /*D4*/
};
lsm303agr-magn@1e {
compatible = "st,lis2mdl","st,lsm303agr-magn";
reg = <0x1e>;
label = "LSM303AGR-MAGN";
irq-gpios = <&arduino_header 3 GPIO_ACTIVE_
HIGH>; /*A3*/
};
500
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
lsm303agr-accel@19 {
compatible = "st,lis2dh", "st,lsm303agr-accel";
reg = <0x19>;
label = "LSM303AGR-ACCEL";
irq-gpios = <&arduino_header 3 GPIO_ACTIVE_
HIGH>; /*A3*/
};
};
#include <zephyr.h>
#include <device.h>
#include <drivers/sensor.h>
#include <stdio.h>
#include <sys/util.h>
#ifdef CONFIG_LSM6DSL_TRIGGER
static int lsm6dsl_trig_cnt;
static void lsm6dsl_trigger_handler(const struct device *dev,
struct sensor_trigger *trig) {
sensor_sample_fetch_chan(dev, SENSOR_CHAN_ALL);
lsm6dsl_trig_cnt++;
}
#endif
501
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
#define LSM6DSL_DEVNAME DT_LABEL(DT_INST(0, st_lsm6dsl))
void main(void) {
#ifdef CONFIG_LSM6DSL_TRIGGER
int cnt = 1;
#endif
#ifdef CONFIG_LSM6DSL_EXT0_LPS22HB
struct sensor_value temp, press;
#endif
#ifdef CONFIG_LSM6DSL_EXT0_LIS2MDL
struct sensor_value magn[3];
#endif
struct sensor_value accel[3];
struct sensor_value gyro[3];
const struct device *lsm6dsl =
device_get_binding(LSM6DSL_DEVNAME);
if (lsm6dsl == NULL) {
printf("Could not get LSM6DSL device\n");
return;
}
/* set LSM6DSL accel/gyro sampling frequency to 104 Hz */
struct sensor_value odr_attr;
odr_attr.val1 = 104;
odr_attr.val2 = 0;
if (sensor_attr_set(lsm6dsl, SENSOR_CHAN_ACCEL_XYZ,
SENSOR_ATTR_SAMPLING_FREQUENCY, &odr_attr) < 0) {
printk("Cannot set sampling frequency for
accelerometer\n");
return;
}
if (sensor_attr_set(lsm6dsl, SENSOR_CHAN_GYRO_XYZ,
SENSOR_ATTR_SAMPLING_FREQUENCY, &odr_
attr) < 0) {
502
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
printk("Cannot set sampling frequency for gyro.\n");
return;
}
#ifdef CONFIG_LSM6DSL_TRIGGER
struct sensor_trigger trig;
trig.type = SENSOR_TRIG_DATA_READY;
trig.chan = SENSOR_CHAN_ACCEL_XYZ;
sensor_trigger_set(lsm6dsl, &trig, lsm6dsl_trigger_handler);
#endif
while (1) { /* Get sensor samples */
#ifndef CONFIG_LSM6DSL_TRIGGER
if (sensor_sample_fetch(lsm6dsl) < 0) {
printf("LSM6DSL Sensor sample update error\n");
return;
}
#endif
/* Get sensor data */
sensor_channel_get(lsm6dsl,
SENSOR_CHAN_ACCEL_XYZ, accel);
sensor_channel_get(lsm6dsl, SENSOR_CHAN_GYRO_
XYZ, gyro);
#ifdef CONFIG_LSM6DSL_EXT0_LPS22HB
sensor_channel_get(lsm6dsl,
SENSOR_CHAN_AMBIENT_TEMP, &temp);
sensor_channel_get(lsm6dsl, SENSOR_CHAN_PRESS,
&press);
#endif
#ifdef CONFIG_LSM6DSL_EXT0_LIS2MDL
sensor_channel_get(lsm6dsl, SENSOR_CHAN_MAGN_
XYZ, magn);
503
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
#endif
/* Display sensor data and erase previous data */
printf("\0033\014");
printf("X-NUCLEO-IKS01A2 sensor dashboard\n\n");
/* lsm6dsl accel */
printf("LSM6DSL: Accel (m.s-2):"
" x: %.1f, y: %.1f, z: %.1f\n",
sensor_value_to_double(&accel[0]),
sensor_value_to_
double(&accel[1]),
sensor_value_to_double(&accel[2]));
/* lsm6dsl gyro */
printf("LSM6DSL: Gyro (dps):"
" x: %.3f, y: %.3f, z: %.3f\n",
sensor_value_to_double(&gyro[0]),
sensor_value_to_double(&gyro[1]),
sensor_value_to_double(&gyro[2]));
#ifdef CONFIG_LSM6DSL_EXT0_LPS22HB
printf("LSM6DSL: Temperature: %.1f C\n",
sensor_value_to_double(&temp));
printf("LSM6DSL: Pressure:%.3f kpa\n",
sensor_value_to_double(&press));
#endif
#ifdef CONFIG_LSM6DSL_EXT0_LIS2MDL
printf("LSM6DSL: Magn (gauss):"
" x: %.3f, y: %.3f, z: %.3f\n",
sensor_value_to_double(&magn[0]),
sensor_value_to_double(&magn[1]),
sensor_value_to_double(&magn[2]));
504
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND SPI AND
I2C IN PARTICULAR
#endif
#ifdef CONFIG_LSM6DSL_TRIGGER
printk("%d:: lsm6dsl acc trig %d\n",
cnt++, lsm6dsl_trig_cnt);
#endif
k_sleep(K_MSEC(2000));
}
}
Summary
This chapter introduced the Zephyr RTOS device driver model and the key
role played in it by devicetrees. The devicetree syntax was reviewed and
also the use of binding files (which are written in YAML syntax) to describe
the rules to be obeyed by the devicetree contents, including data types.
The process of generating the devicetree–based device header files and
how these are used in actual applications was reviewed. The basics of the
I2C and SPI protocols were reviewed, and, finally, an example involving an
application using an actual I2C sensor device was explored.
References
1. By Em3rgent0rdr – Own work, CC0 https://commons.
wikimedia.org/w/index.php?curid=134759387
2. www.electronicshub.org/how-to-use-i2c-in-
stm32f103c8t6/
3. https://github.com/devicetree-org/devicetree-
specification/releases/download/v0.4-rc1/
devicetree-specification-v0.4-rc1.pdf
505
Chapter 9 UNDERSTANDING AND WORKING WITH THE DEVICETREE IN GENERAL AND
SPI AND I2C IN PARTICULAR
4. Zephyr project devicetree documentation https://
docs.zephyrproject.org/2.7.5/guides/dts/
intro.html
6. Zephyr\samples\shields\x_nucleo_iks01a2\sensorhub\
7. www.st.com/resource/en/datasheet/lsm6dsl.pdf
8. https://docs.zephyrproject.org/latest/build/dts/
api/bindings/i2c/nordic,nrf-twim.html
9. https://developer.nordicsemi.com/nRF_Connect_
SDK/doc/latest/nrfx/drivers/twim/index.html
506
CHAPTER 10
Emulator Simulator
508
Chapter 10 Building Zephyr RTOS Applications Using Renode
Emulator Simulator
509
Chapter 10 Building Zephyr RTOS Applications Using Renode
• CPU load
• Memory consumption
• Network loads
510
Chapter 10 Building Zephyr RTOS Applications Using Renode
• Battery performance
• CPU performance
• Memory consumption
Renode
• Renode [3] is designed to be a “whole-system
emulator” that can be used in continuous integration
and continuous development (CI/CD) scenarios on
multiple devices.
• It is capable of simulating systems with multiple cores
and multiple heterogenous CPUs.
511
Chapter 10 Building Zephyr RTOS Applications Using Renode
512
Chapter 10 Building Zephyr RTOS Applications Using Renode
Renode Installation
Renode is implemented in C# and requires a recent version of .Net to run
on Windows.
.Net is normally already installed in Windows 10.
On Linux systems and macOS, a recent version of Mono (> 5.2) is
required.
The simplest way to install Renode on Windows is to download a .msi
installer from the Renode GitHub site. When the Microsoft installation is
started by clicking on the .msi file in File Explorer, a warning is displayed.
Installation in Program Files requires administrator privileges – activated
by allowing installation in the modal dialog box that appears.
513
Chapter 10 Building Zephyr RTOS Applications Using Renode
514
Chapter 10 Building Zephyr RTOS Applications Using Renode
Clicking the Next button again brings up the dialog to start installation.
Clicking the Install button starts the installation process during which
a dialog with a progress bar is displayed. Finally, a dialog confirming
successful installation is displayed, and clicking the Finish button
completes the installation process.
515
Chapter 10 Building Zephyr RTOS Applications Using Renode
516
Chapter 10 Building Zephyr RTOS Applications Using Renode
PS C:\> renode
15:01:46.3231 [INFO] Loaded monitor commands from: C:\Program
Files\Renode\scripts/monitor.py
517
Chapter 10 Building Zephyr RTOS Applications Using Renode
Typing help at the monitor prompt will list the available commands.
Typing in help <some command> will provide some help information
about that command, for example:
start [ s ]
starts the emulation.
Usage:
start - starts the whole emulation
start @path - executes the script and starts the emulation
start - s
quit - q
519
Chapter 10 Building Zephyr RTOS Applications Using Renode
Renode Scripts
Renode is a scripting language.
• The simplest scripts are one-line Renode commands.
• Scripts can be constructed by combining commands.
• Scripts, in turn, can include both commands and other
scripts.
Renode scripts are concerned with creating and running emulations of
a single emulated (guest) platform or a networked collection of platforms.
Renode commands can be used to interact with the external interfaces
(peripherals) of the emulated machines such as UARTs or Ethernet
controllers.
When running Renode interactively, the user would normally start
with creating the emulation through a sequence of commands building
up, configuring, and connecting the relevant emulated (guest) platform or
platforms (called “machines”).
This is normally done using nested .resc scripts, which help
encapsulate some of the repeatable elements in this activity (normally, the
user will want to create the same platform over and over again in between
runs, or even script the execution entirely).
When the emulation is created and all the necessary elements
(including, e.g., binaries to be executed) are loaded, the emulation itself
can be started – to do this, the start command is entered in the Monitor.
At this point, it is possible to see a lot of information about the
operation of the emulated environment in the log window, extract
additional information, and manipulate the running emulation using
the Monitor (or plug-ins such as Wireshark) – as well as interact with
the external interfaces of the emulated machines like UARTs or Ethernet
controllers.
Renode scripts can be saved in files, which, typically, have a .resc
extension.
520
Chapter 10 Building Zephyr RTOS Applications Using Renode
include @/path/to/script.resc
521
Chapter 10 Building Zephyr RTOS Applications Using Renode
522
Chapter 10 Building Zephyr RTOS Applications Using Renode
523
Chapter 10 Building Zephyr RTOS Applications Using Renode
Clicking on the zephyr (blue kite) icon brings up the Zephyr device
driver source code.
Clicking on a link in the second column of the table in the Peripherals
tab (where one exists) will bring up the emulator code in C#, for example,
for uart0.
Studying the device driver source code and the emulator source code
is a good way to learn about how to write driver code for new devices and
also how to write emulator code for these devices.
525
Chapter 10 Building Zephyr RTOS Applications Using Renode
The west tool creates a build directory in which the project is built.
If all goes well, it will contain a zephyr subdirectory in which the
compiled application, called zephyr.elf, will be located.
To run the application code in Renode, it is necessary to start renode
and then, in the monitor window, enter individual commands, or,
alternatively, invoke a renode script file that contains multiple commands.
In the directory containing renode, run the renode command, which
brings up the renode monitor window.
As commands are entered into the monitor window, output is written
to the PowerShell window from which renode was started.
526
Chapter 10 Building Zephyr RTOS Applications Using Renode
• showAnalyzer sysbus.uart0
527
Chapter 10 Building Zephyr RTOS Applications Using Renode
528
Chapter 10 Building Zephyr RTOS Applications Using Renode
529
Chapter 10 Building Zephyr RTOS Applications Using Renode
References
1. https://en.wikipedia.org/wiki/Pmod_Interface
2. www.mikroe.com/click-boards
3. https://renode.io/
4. https://renode.readthedocs.io/en/latest/
advanced/writing-peripherals.html
5. https://renode.readthedocs.io/en/latest/
introduction/testing.html
6. h ttps://renode.readthedocs.io/en/latest/basic/
using-python.html
530
CHAPTER 11
Understanding
and Using the Zephyr
ZBus in Application
Development
Zephyr ZBus
ZBus has many concepts in common with DBus, which is used in Linux-
based systems. Whereas DBus is concerned with communication between
processes running in separate virtual memory spaces, ZBus is concerned
with communication between Zephyr RTOS threads.
The pattern underlying ZBus is an observer pattern in which a thread
can broadcast messages to all observers interested in receiving such
messages. ZBus can be used for many-to-many communication purpose
as well. The communication patterns supported by ZBus are publish/
subscribe and message passing. ZBus-based communication makes use of
shared memory and can be either synchronous or asynchronous in nature.
ZBus Architecture
The architecture of the ZBus, as outlined in the following schematic,
consists of
532
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
The ZBus actions that can be performed over a channel are publish,
read, and subscribe.
Publishing and reading cannot be used in an ISR (interrupt service
routine) context because the underlying operations may block. This is
because publishing and copying involve a mutex locking step followed by a
memory copy to or from a shared memory region.
The registration of a ZBus observer can be either static or dynamic.
A static observer registration is defined at compile time and cannot be
removed. It can, however, be suppressed via a call to the zbus_obs_set_
enable() method. Dynamic observer registrations can be added and
removed at runtime as required.
The Zephyr documentation provides an example of a fairly typical
possible use of the ZBus in a sensor-based application. The scenario
underlying this example is illustrated in Figure 11-3.
533
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
534
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
535
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
• Runtime subscribers
536
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
struct acc_msg {
int x;
int y;
int z;
};
537
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
ZBUS_CHAN_DEFINE(acc_chan, /* Name */
struct acc_msg, /* Message type */
NULL, /* Validator */
NULL, /* User Data */
ZBUS_OBSERVERS(my_listener, my_subscriber), /* observers */
ZBUS_MSG_INIT(.x = 0, .y = 0, .z = 0) /* Initial
value {0} */
);
struct zbus_channel {
const char *const name;
void *const message;
const size_t message_size;
void *const user_data;
bool (*const validator)(const void *msg, size_t
msg_size);
struct zbus_channel_data *const data;
};
538
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
539
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
Zephyr provides the following utility macros for defining listeners and
subscribers.
ZBUS_LISTENER_DEFINE for defining listeners and ZBUS_SUBSCRIBER_
DEFINE for defining subscribers.
ZBUS_SUBSCRIBER_DEFINE(_name, _queue_size) defines and
initializes a subscriber.
It defines a subscriber type of observer and the message queue where
the subscriber will asynchronously receive the notification, by initializing
the struct zbus_observer instance that defines the subscriber.
_name is the subscriber name.
_queue_size is the size of the notification queue.
ZBUS_LISTENER_DEFINE(_name, _cb) defines and initializes a listener,
providing the callback function to use when the listener is notified, by
initializing the struct zbus_observer instance that defines the observer.
_name is the listener’s name.
_cb is the callback function.
The struct zbus_observer structure source code is full of Doxygen
documentation, which can be conditionally included by defining
DOXYGEN. This documentation code, associated with conditional
includes #if defined(__DOXYGEN__), is edited out in most of the code
snippets that follow.
struct zbus_observer {
#if defined(CONFIG_ZBUS_OBSERVER_NAME)
const char *const name;
#endif
enum zbus_observer_type type;
bool enabled;
union {
struct k_msgq *const queue;
void (*const callback)(const struct zbus_
channel *chan);
540
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
#if defined(CONFIG_ZBUS_MSG_SUBSCRIBER)
struct k_fifo *const message_fifo;
#endif /* CONFIG_ZBUS_MSG_SUBSCRIBER */
};
};
In this structure:
The actual details of the structure are dependent on the outcome of the
preprocessing stage and will depend on whether CONFIG_ZBUS_OBSERVER_
NAME and CONFIG_ZBUS_MSG_SUBSCRIBER have been #defined.
The following code snippets from the Zephyr documentation show
how these helper macros can be used:
ZBUS_LISTENER_DEFINE(my_listener, listener_callback_example);
ZBUS_SUBSCRIBER_DEFINE(my_subscriber, 4);
541
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
• 0 – Notification received.
542
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
struct control_msg {
int move;
};
bool control_validator(const void* msg, size_t msg_size) {
const struct control_msg* cm = msg;
bool is_valid = (cm->move == -1) || (cm->move == 0) || (cm-
>move == 1);
return is_valid;
}
static int message_count = 0;
ZBUS_CHAN_DEFINE(control_chan, /* Name */
struct control_msg, /* Message type */
control_validator, /* Validator */
&message_count, /* User data */
ZBUS_OBSERVERS_EMPTY, /* observers */
ZBUS_MSG_INIT(.move = 0) /* Initial value {.move=0} */
);
543
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
• Publishing to a channel
• Reading from a channel
544
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
545
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
if (!zbus_chan_claim(&acc_chan, K_MSEC(200))) {
int *message_counting = (int *) zbus_chan_user_
data(acc_chan);
*message_counting += 1;
zbus_chan_finish(&acc_chan);
}
Because claiming and finishing are operations that can block, they
should not be used in ISRs.
546
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
547
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(chan->observers,
obs_nd, tmp, node) {
LOG_INF(" - %s", obs_nd->obs->name);
}
return true;
}
548
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
++(*count);
return true;
}
int main(void) {
int count = 0;
LOG_INF("Channel list:");
zbus_iterate_over_channels_with_user_data(print_channel_
data_iterator, &count);
count = 0;
LOG_INF("Observers list:");
zbus_iterate_over_observers_with_user_data(print_
observer_data_iterator, &count);
return 0;
}
Running the preceding code will produce output such as the following:
D: Channel list:
D: 0 - Channel acc_chan:
D: Message size: 12
D: Observers:
D: - my_listener
D: - my_subscriber
D: 1 - Channel version_chan:
D: Message size: 4
D: Observers:
D: Observers list:
D: 0 - Listener my_listener
D: 1 - Subscriber my_subscriber
549
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
550
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
551
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
ZBUS_CHAN_DEFINE(a_chan, /* Name */
struct a_msg, /* Message type */
NULL, /* Validator */
NULL, /* User Data */
ZBUS_OBSERVERS(L1, L2, MS1, MS2, S1), /* observers */
ZBUS_MSG_INIT(0) /* Initial value {0} */
);
552
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
553
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
554
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
555
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
556
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
CONFIG_LOG=y
CONFIG_LOG_MODE_MINIMAL=y
CONFIG_BOOT_BANNER=n
CONFIG_MAIN_THREAD_PRIORITY=5
CONFIG_ZBUS=y
CONFIG_ZBUS_LOG_LEVEL_INF=y
CONFIG_ZBUS_CHANNEL_NAME=y
CONFIG_ZBUS_OBSERVER_NAME=y
CONFIG_ZBUS_RUNTIME_OBSERVERS=y
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(hello_world)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})
557
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
#include <stdint.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/zbus/zbus.h>
LOG_MODULE_DECLARE(zbus, CONFIG_ZBUS_LOG_LEVEL);
struct version_msg {
uint8_t major;
uint8_t minor;
uint16_t build;
};
struct acc_msg {
int x;
int y;
int z;
};
One of the message types provides version and build information; the
other message provides accelerometer sensor reading information.
The three channels involved in this example are version_chan for
use with version information, acc_data_chan for use with accelerometer
sensor reading information, and simple_chan, which is a hard channel
because it has a validator. The data type for simple_chan is int.
These are defined as shown in the next code snippet:
ZBUS_CHAN_DEFINE(version_chan, /* Name */
struct version_msg, /* Message type */
NULL, /* Validator */
NULL, /* User data */
ZBUS_OBSERVERS_EMPTY, /* observers */
ZBUS_MSG_INIT(.major = 0, .minor = 1,
.build = 2) /* Initial value major 0,
minor 1, build 2 */
);
558
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
ZBUS_CHAN_DEFINE(acc_data_chan, /* Name */
struct acc_msg, /* Message type */
NULL, /* Validator */
NULL, /* User data */
ZBUS_OBSERVERS(foo_lis, bar_sub), /* observers */
ZBUS_MSG_INIT(.x = 0, .y = 0, .z = 0) /* Initial
value */
);
ZBUS_CHAN_DEFINE(simple_chan, /* Name */
int, /* Message type */
simple_chan_validator, /* Validator */
NULL, /* User data */
ZBUS_OBSERVERS_EMPTY, /* observers */
0 /* Initial value is 0 */
);
559
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
A subscriber task and a thread running this task can be set up as shown
in this example code snippet:
560
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
561
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
obs_nd,tmp, node) {
LOG_INF(" - %s", obs_nd->obs->name);
}
return true;
}
This function prints out information about a channel starting with its
name and the message size, by the string "Observers:", followed by the
count value, which is post incremented, and then the observers observing
on that channel. The for loop performs an array traversal. The SYS_SLIST_
FOR_EACH_CONTAINER_SAFE is used for ZBus observers.
The observer data iterator function is shown next:
This counts the number of observers and also prints out information
about the type of observer (listener or subscriber) and the observer name.
The application’s main() function code is as follows:
int main(void) {
int err, value;
struct acc_msg acc1 = {.x = 1, .y = 1, .z = 1};
const struct version_msg *v = zbus_chan_const_msg
(&version_chan);
562
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
int count = 0;
LOG_INF("Channel list:");
zbus_iterate_over_channels_with_user_data (print_channel_
data_iterator, &count);
count = 0;
LOG_INF("Observers list:");
zbus_iterate_over_observers_with_user_data(print_observer_
data_iterator, &count);
zbus_chan_pub(&acc_data_chan, &acc1, K_SECONDS(1));
k_msleep(1000);
acc1.x = 2;
acc1.y = 2;
acc1.z = 2;
zbus_chan_pub(&acc_data_chan, &(acc1), K_SECONDS(1));
value = 5;
err = zbus_chan_pub(&simple_chan, &value, K_MSEC(200));
if (err == 0) {
LOG_INF("Pub a valid value to a channel with validator
successfully.");
}
value = 15;
err = zbus_chan_pub(&simple_chan, &value, K_MSEC(200));
if (err == -ENOMSG) {
LOG_INF("Pub an invalid value to a channel validator
successfully.");
}
return 0;
}
563
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
I: Channel list:
I: 0 - Channel acc_data_chan:
I: Message size: 12
I: Observers:
I: - foo_lis
I: - bar_sub
I: 1 - Channel simple_chan:
I: Message size: 4
I: Observers:
I: 2 - Channel version_chan:
I: Message size: 4
564
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
I: Observers:
I: Observers list:
I: 0 - Subscriber bar_sub
I: 1 - Listener foo_lis
I: From listener -> Acc x=1, y=1, z=1
I: From subscriber -> Acc x=1, y=1, z=1
I: From listener -> Acc x=2, y=2, z=2
I: From subscriber -> Acc x=2, y=2, z=2
I: Pub a valid value to a channel with validator successfully.
I: Pub an invalid value to a channel with validator successfully.
struct version_msg {
uint8_t major;
uint8_t minor;
uint16_t build;
};
565
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
struct sensor_msg {
uint32_t temp;
uint32_t press;
uint32_t humidity;
};
// sensors.c
#include <zephyr/logging/log.h>
#include <zephyr/zbus/zbus.h>
LOG_MODULE_DECLARE(zbus, CONFIG_ZBUS_LOG_LEVEL);
ZBUS_CHAN_DECLARE(sensor_data_chan);
void peripheral_thread (void) {
struct sensor_msg sm = {0};
while (1) {
LOG_DBG("Sending sensor data...");
sm.press += 1;
sm.temp += 10;
sm.humidity += 100;
zbus_chan_pub(&sensor_data_chan, &sm, K_MSEC(250));
k_msleep(500);
}
}
566
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
#include <stdint.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/zbus/zbus.h>
LOG_MODULE_DECLARE(zbus, CONFIG_ZBUS_LOG_LEVEL);
ZBUS_CHAN_DEFINE(sensor_data_chan, /* Name */
struct sensor_msg, /* Message type */
NULL, /* Validator */
NULL, /* User data */
ZBUS_OBSERVERS(fast_handler1_lis, fast_handler2_lis, fast_
handler3_lis,
delay_handler1_lis, delay_handler2_lis, delay_
handler3_lis,
thread_handler1_sub, thread_handler2_sub,
thread_handler3_sub), /* observers */
ZBUS_MSG_INIT(0) /* Initial value {0} */
);
567
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
The version data channel has no observers, and the sensor data
channel has a variety of observers, both listeners and subscribers. The
listener callbacks demonstrate various ways of handling messages, and
the various subscribers show various ways of implementing subscriber
behavior.
The callback code for the three fast handlers is essentially the same; all
three print out logging information using LOG_INF.
The following code snippet is that for the fast_handler1_lis listener
and also shows how that listener is defined:
The three “delay” handlers submit work to the system workqueue, and
all run esentially the same code.
The following code snippet is that for the delay_handler1_lis listener
and also shows how that listener is defined and how the workqueue and
channel callbacks are set up:
struct sensor_wq_info {
struct k_work work;
const struct zbus_channel *chan;
uint8_t handle;
};
static struct sensor_wq_info wq_handler1 = {.handle = 1};
568
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
ZBUS_LISTENER_DEFINE(delay_handler1_lis, dh1_cb);
The three subscriber observers and their associated threads are also
essentially the same, and the following code snippet shows how one of
these subscribers is set up:
569
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
The threads for each subscriber task are created at compile time using
K_THREAD_DEFINE.
The code for main(), shown here, is fairly simple:
int main(void) {
k_work_init(&wq_handler1.work, wq_dh_cb);
k_work_init(&wq_handler2.work, wq_dh_cb);
k_work_init(&wq_handler3.work, wq_dh_cb);
struct version_msg *v = zbus_chan_msg(&version_chan);
LOG_DBG("Sensor sample started, version %u.%u-%u!",
v->major, v->minor, v->build);
return 0;
}
570
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
571
Chapter 11 Understanding and Using the Zephyr ZBus in Application Development
572
CHAPTER 12
The problems (scenarios) that result in the Wi-Fi MAC protocol being
the way it is include the following [1]:
574
Chapter 12 Zephyr RTOS Wi-Fi Applications
Security Issues
IEEE 802.11 involves authentication and encryption. Authentication
proves that a particular station is the one it claims to be and, therefore, is
authorized to communicate with a second station in a given coverage area.
In the infrastructure mode, authentication is established between an AP
(access point) and each station.
802.11 specifies two methods of authentication: open system or
shared key.
An open system allows any client to authenticate as long as it
conforms to any MAC address filter policies that may have been set. All
authentication packets are transmitted without encryption.
575
Chapter 12 Zephyr RTOS Wi-Fi Applications
576
Chapter 12 Zephyr RTOS Wi-Fi Applications
• WPA3-Personal
• WPA3-Enterprise
577
Chapter 12 Zephyr RTOS Wi-Fi Applications
should move over entirely to using the 5 GHz spectrum, in practice, this is
unlikely to happen any time soon because there are so many Wi-Fi devices
out there that work at 2.4 GHz.
Wi-Fi has been evolving continually in a never ceasing effort to provide
faster data rates, better security, and lower power consumption. The
standards have both IEEE names and Wi-Fi Alliance names. For example,
802.11ac is also referred to as Wi-Fi 5.
The evolution of Wi-Fi is summarized in the next several bullet
points [3]:
578
Chapter 12 Zephyr RTOS Wi-Fi Applications
• Management frames
• Control frames
• Data frames
579
Chapter 12 Zephyr RTOS Wi-Fi Applications
Access Points
An access point can be thought of as a bridge that bridges traffic between
a client (mobile station) and other devices on an accessible network.
Before a mobile station can send traffic through an AP, it must set up an
association that puts it into an appropriate connection state [4].
580
Chapter 12 Zephyr RTOS Wi-Fi Applications
581
Chapter 12 Zephyr RTOS Wi-Fi Applications
582
Chapter 12 Zephyr RTOS Wi-Fi Applications
583
Chapter 12 Zephyr RTOS Wi-Fi Applications
584
Chapter 12 Zephyr RTOS Wi-Fi Applications
585
Chapter 12 Zephyr RTOS Wi-Fi Applications
586
Chapter 12 Zephyr RTOS Wi-Fi Applications
The nRF5340 SoC is a dual core ARM Cortex M33 processor and has
support for ARM TrustZone security.
587
Chapter 12 Zephyr RTOS Wi-Fi Applications
• Default scan
• Active scan
• Passive scan
588
Chapter 12 Zephyr RTOS Wi-Fi Applications
any level in the network protocol stack. These APIs can, for example, be
used to invoke scans on a Wi-Fi- or Bluetooth-based network interface, or
to request notification if the IP address of a network interface changes.
Because Zephyr applications are monolithic and include the operating
system itself, only those routines that are required are compiled into
the application, and application-specific network management code
is provided by defining and registering handlers using a NET_MGMT_
REGISTER_REQUEST_HANDLER macro. Procedure requests are then
invoked with a single net_mgmt() API that invokes the handler that has
been registered for the corresponding request. Function prototypes and
macros for collecting information concerning receive and transmit packets
are defined in the net_private.h header file located in the zephyr/
subsys/net/ip folder.
589
Chapter 12 Zephyr RTOS Wi-Fi Applications
590
Chapter 12 Zephyr RTOS Wi-Fi Applications
591
Chapter 12 Zephyr RTOS Wi-Fi Applications
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(scan, CONFIG_LOG_DEFAULT_LEVEL);
#include <zephyr/kernel.h>
#if defined(CLOCK_FEATURE_HFCLK_DIVIDE_PRESENT) || NRF_CLOCK_HAS_
HFCLK192M
#include <nrfx_clock.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <zephyr/shell/shell.h>
#include <zephyr/sys/printk.h>
#include <zephyr/init.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/wifi_utils.h>
#include <zephyr/net/net_event.h>
#include <zephyr/net/ethernet.h>
592
Chapter 12 Zephyr RTOS Wi-Fi Applications
#include <zephyr/net/ethernet_mgmt.h>
#include "net_private.h"
#define WIFI_SHELL_MODULE "wifi"
#ifdef CONFIG_WIFI_MGMT_RAW_SCAN_RESULTS_ONLY
#define WIFI_SHELL_MGMT_EVENTS (NET_EVENT_WIFI_RAW_SCAN_RESULT | \
NET_EVENT_WIFI_SCAN_DONE)
#else
#define WIFI_SHELL_MGMT_EVENTS (NET_EVENT_WIFI_SCAN_RESULT | \
NET_EVENT_WIFI_SCAN_DONE | \
NET_EVENT_WIFI_RAW_SCAN_RESULT)
#endif
#define SCAN_TIMEOUT_MS 10000
593
Chapter 12 Zephyr RTOS Wi-Fi Applications
The callback structure details are held in the global variable wifi_
shell_mgmt_cb defined as shown here:
struct net_mgmt_event_callback {
sys_snode_t node;
union {
net_mgmt_event_handler_t handler;
struct k_sem *sync_call;
};
#ifdef CONFIG_NET_MGMT_EVENT_INFO
const void *info;
size_t info_length;
#endif
union {
uint32_t event_mask;
uint32_t raised_event;
};
};
594
Chapter 12 Zephyr RTOS Wi-Fi Applications
595
Chapter 12 Zephyr RTOS Wi-Fi Applications
net_sprint_ll_addr_buf(entry->mac, WIFI_MAC_
ADDR_LEN,
mac_string_buf, sizeof(mac_string_
buf)) : ""));
}
596
Chapter 12 Zephyr RTOS Wi-Fi Applications
return band;
}
597
Chapter 12 Zephyr RTOS Wi-Fi Applications
598
Chapter 12 Zephyr RTOS Wi-Fi Applications
599
Chapter 12 Zephyr RTOS Wi-Fi Applications
if (band_str_len - 1) {
char *buf = malloc(band_str_len);
if (!buf) {
LOG_ERR("Malloc Failed");
return -EINVAL;
}
strcpy(buf, CONFIG_WIFI_SCAN_BANDS_LIST);
if (wifi_utils_parse_scan_bands(buf, ¶ms.
bands)) {
LOG_ERR("Incorrect value(s) in
CONFIG_WIFI_SCAN_BANDS_LIST: %s",
CONFIG_WIFI_SCAN_BANDS_LIST);
free(buf);
return -ENOEXEC;
}
free(buf);
}
if (sizeof(CONFIG_WIFI_SCAN_CHAN_LIST) - 1) {
if (wifi_utils_parse_scan_chan(CONFIG_WIFI_SCAN_
CHAN_LIST,
params.chan)) {
LOG_ERR("Incorrect value(s) in
CONFIG_WIFI_SCAN_CHAN_LIST: %s",
CONFIG_WIFI_SCAN_CHAN_LIST);
return -ENOEXEC;
}
}
if (net_mgmt(NET_REQUEST_WIFI_SCAN, iface, ¶ms,
sizeof(struct wifi_scan_params))) {
LOG_ERR("Scan request failed");
return -ENOEXEC;
}
600
Chapter 12 Zephyr RTOS Wi-Fi Applications
printk("Scan requested\n");
k_sem_take(&scan_sem, K_MSEC(SCAN_TIMEOUT_MS));
return 0;
}
int main(void) {
scan_result = 0U;
net_mgmt_init_event_callback(&wifi_shell_mgmt_cb,
wifi_mgmt_event_handler, WIFI_SHELL_MGMT_EVENTS);
net_mgmt_add_event_callback(&wifi_shell_mgmt_cb);
601
Chapter 12 Zephyr RTOS Wi-Fi Applications
602
Chapter 12 Zephyr RTOS Wi-Fi Applications
net_mgmt(NET_REQUEST_ETHERNET_SET_MAC_
ADDRESS, iface,
¶ms, sizeof(params));
ret = net_if_up(iface);
if (ret) {
LOG_ERR("Cannot bring up iface (%d)", ret);
return ret;
}
The output when the basic scanning options are chosen should be
something like the following:
603
Chapter 12 Zephyr RTOS Wi-Fi Applications
604
Chapter 12 Zephyr RTOS Wi-Fi Applications
The prj.conf file shows the configuration details to use for this kind of
application.
CONFIG_WIFI=y
CONFIG_WIFI_NRF700X=y
CONFIG_NET_L2_WIFI_MGMT=y
CONFIG_HEAP_MEM_POOL_SIZE=25000
# System settings
CONFIG_NEWLIB_LIBC=y
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_L2_ETHERNET=y
CONFIG_NET_NATIVE=n
CONFIG_NET_OFFLOAD=y
CONFIG_INIT_STACKS=y
# Memories
CONFIG_MAIN_STACK_SIZE=4096
# Debugging
CONFIG_STACK_SENTINEL=y
CONFIG_DEBUG_COREDUMP=y
CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y
CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y
605
Chapter 12 Zephyr RTOS Wi-Fi Applications
# Logging
CONFIG_LOG=y
CONFIG_PRINTK=y
# If below config is enabled, printk logs are
# buffered. For unbuffered messages, disable this.
CONFIG_LOG_PRINTK=n
CONFIG_WIFI_MGMT_SCAN_DWELL_TIME_ACTIVE=50
CONFIG_WIFI_MGMT_SCAN_DWELL_TIME_PASSIVE=130
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrf_wifi_scan)
target_include_directories(app PUBLIC ${ZEPHYR_BASE}/
subsys/net/ip)
target_sources(app PRIVATE
src/main.c
)
606
Chapter 12 Zephyr RTOS Wi-Fi Applications
uart:~$
607
Chapter 12 Zephyr RTOS Wi-Fi Applications
608
Chapter 12 Zephyr RTOS Wi-Fi Applications
BSSID: E8:AD:A6:E0:81:22
Band: 2.4GHz
Channel: 11
Security: WPA2-PSK
MFP: Optional
RSSI: -73
Beacon Interval: 100
DTIM: 3
TWT: Not supported
uart:~$ net ipv4
IPv4 support : enabled
IPv4 fragmentation support : disabled
Max number of IPv4 network interfaces in the
system : 1
Max number of unicast IPv4 addresses per network
interface : 1
Max number of multicast IPv4 addresses per network
interface : 1
609
Chapter 12 Zephyr RTOS Wi-Fi Applications
#ifdef CONFIG_NET_CONFIG_SETTINGS
/* With the code as written here
DHCPv4 always starts on the Wi-Fi interface
independent of the ordering.
*/
const struct device *dev = device_get_binding("wlan0");
610
Chapter 12 Zephyr RTOS Wi-Fi Applications
CONFIG_WIFI=y
CONFIG_WIFI_NRF700X=y
# WPA supplicant
CONFIG_WPA_SUPP=y
CONFIG_NET_L2_WIFI_SHELL=y
# System settings
CONFIG_NEWLIB_LIBC=y
CONFIG_NEWLIB_LIBC_NANO=n
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_LOG=y
CONFIG_NET_IPV6=y
CONFIG_NET_IPV4=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_DHCPV4=y
CONFIG_DNS_RESOLVER=y
CONFIG_NET_STATISTICS=y
CONFIG_NET_STATISTICS_WIFI=y
CONFIG_NET_STATISTICS_USER_API=y
611
Chapter 12 Zephyr RTOS Wi-Fi Applications
CONFIG_NET_PKT_RX_COUNT=8
CONFIG_NET_PKT_TX_COUNT=8
CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=4
CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=5
CONFIG_NET_MAX_CONTEXTS=5
CONFIG_NET_CONTEXT_SYNC_RECV=y
CONFIG_INIT_STACKS=y
CONFIG_NET_L2_ETHERNET=y
CONFIG_NET_SHELL=y
# Memories
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SHELL_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_NET_TX_STACK_SIZE=4096
CONFIG_NET_RX_STACK_SIZE=4096
612
Chapter 12 Zephyr RTOS Wi-Fi Applications
# Debugging
CONFIG_STACK_SENTINEL=y
CONFIG_DEBUG_COREDUMP=y
CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y
CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y
CONFIG_SHELL_CMDS_RESIZE=n
#CONFIG_DEBUG=y
CONFIG_WPA_SUPP_LOG_LEVEL_INF=y
# Kernel options
CONFIG_ENTROPY_GENERATOR=y
# Logging
CONFIG_LOG=y
CONFIG_PRINTK=y
CONFIG_SHELL=y
CONFIG_SHELL_GETOPT=y
CONFIG_DEVICE_SHELL=y
CONFIG_POSIX_CLOCK=y
CONFIG_DATE_SHELL=y
CONFIG_NET_CONFIG_AUTO_INIT=n
CONFIG_WIFI_MGMT_EXT=y
CONFIG_WIFI_CREDENTIALS=y
CONFIG_WIFI_CREDENTIALS_BACKEND_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
613
Chapter 12 Zephyr RTOS Wi-Fi Applications
614
Chapter 12 Zephyr RTOS Wi-Fi Applications
segments to the main network. Using this approach, a large Wi-Fi sensor
network can be deployed, for example, in an office or warehouse building.
Once an nRF7002-based device has been assigned an IP address,
it is possible to build regular TCP/IP applications and both client and
server applications and run them on that device. In the following
section, applications implementing some relatively simple TCP and UDP
clients and servers will be explored. The examples are based on the nRF
Connect SDK sta (station) example and the BSD sockets programming
case study from “abluethinginthecloud” (https://github.com/
abluethinginthecloud/nrf7002-bsd-sockets-example).
615
Chapter 12 Zephyr RTOS Wi-Fi Applications
• Password
CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.98"
CONFIG_NET_CONFIG_MY_IPV4_NETMASK="255.255.255.0"
CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.1.1"
CONFIG_WIFI=y
CONFIG_WIFI_NRF700X=y
# WPA supplicant
CONFIG_WPA_SUPP=y
# Choose the setting that matches the security capabilities
of the AP
616
Chapter 12 Zephyr RTOS Wi-Fi Applications
# CONFIG_STA_KEY_MGMT_NONE=y
# CONFIG_STA_KEY_MGMT_WPA2=y
# CONFIG_STA_KEY_MGMT_WPA2_256=y
# CONFIG_STA_KEY_MGMT_WPA3=y
CONFIG_STA_SAMPLE_SSID="Myssid"
CONFIG_STA_SAMPLE_PASSWORD="Mypassword"
# System settings
CONFIG_NEWLIB_LIBC=y
CONFIG_NEWLIB_LIBC_NANO=n
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_LOG=y
CONFIG_NET_IPV4=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_DHCPV4=y
CONFIG_NET_PKT_RX_COUNT=8
CONFIG_NET_PKT_TX_COUNT=8
# These configurations influence SRAM usage for Wi-Fi
# and can be tuned as necessary based on performance
requirements
CONFIG_NET_BUF_RX_COUNT=16
CONFIG_NET_BUF_TX_COUNT=16
CONFIG_NET_BUF_DATA_SIZE=128
CONFIG_HEAP_MEM_POOL_SIZE=153600
CONFIG_NET_TC_TX_COUNT=1
CONFIG_NET_IF_UNICAST_IPV4_ADDR_COUNT=1
CONFIG_NET_MAX_CONTEXTS=5
CONFIG_NET_CONTEXT_SYNC_RECV=y
CONFIG_INIT_STACKS=y
617
Chapter 12 Zephyr RTOS Wi-Fi Applications
CONFIG_NET_L2_ETHERNET=y
CONFIG_NET_CONFIG_SETTINGS=y
CONFIG_NET_CONFIG_INIT_TIMEOUT=0
CONFIG_NET_SOCKETS_POLL_MAX=6
# Memories
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_NET_TX_STACK_SIZE=4096
CONFIG_NET_RX_STACK_SIZE=4096
# Debugging
CONFIG_STACK_SENTINEL=y
CONFIG_DEBUG_COREDUMP=y
CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y
CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y
CONFIG_SHELL_CMDS_RESIZE=n
# Kernel options
CONFIG_ENTROPY_GENERATOR=y
# Logging
CONFIG_LOG=y
CONFIG_LOG_BUFFER_SIZE=2048
CONFIG_POSIX_CLOCK=y
CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.99"
CONFIG_NET_CONFIG_MY_IPV4_NETMASK="255.255.255.0"
CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.1.1"
# printing of scan results puts pressure on queues in
new locking
# design in net_mgmt. So, use a higher timeout for a crowded
environment.
CONFIG_NET_MGMT_EVENT_QUEUE_TIMEOUT=5000
618
Chapter 12 Zephyr RTOS Wi-Fi Applications
The project build requires the IP networking code, and the location
needs to be given in the CMakeLists.txt file:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrf_wifi_sta)
target_include_directories(app PUBLIC ${ZEPHYR_BASE}/
subsys/net/ip)
target_sources(app PRIVATE
src/main.c
)
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sta, CONFIG_LOG_DEFAULT_LEVEL);
#include <nrfx_clock.h>
#include <zephyr/kernel.h>
#include <stdio.h>
#include <stdlib.h>
#include <zephyr/shell/shell.h>
#include <zephyr/sys/printk.h>
#include <zephyr/init.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/net_event.h>
#include <zephyr/drivers/gpio.h>
#include <qspi_if.h>
#include "net_private.h"
619
Chapter 12 Zephyr RTOS Wi-Fi Applications
Next are some global variables for things such as context, event
callbacks, and devicetree spec.
static struct {
const struct shell *sh;
union {
struct {
uint8_t connected : 1;
uint8_t connect_result : 1;
uint8_t disconnect_requested : 1;
uint8_t _unused : 5;
};
uint8_t all;
};
} context;
620
Chapter 12 Zephyr RTOS Wi-Fi Applications
Next the toggle led task and its associated thread are defined. The
thread infinite polling loop checks the value of the context variable
periodically and switches to led toggling or led off mode as appropriate.
void toggle_led(void) {
int ret;
if (!device_is_ready(led.port)) {
LOG_ERR("LED device is not ready");
return;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
LOG_ERR("Error %d: failed to configure LED
pin", ret);
return;
}
while (1) {
if (context.connected) {
gpio_pin_toggle_dt(&led);
k_msleep(LED_SLEEP_TIME_MS);
} else {
gpio_pin_set_dt(&led, 0);
k_msleep(LED_SLEEP_TIME_MS);
}
}
}
621
Chapter 12 Zephyr RTOS Wi-Fi Applications
622
Chapter 12 Zephyr RTOS Wi-Fi Applications
623
Chapter 12 Zephyr RTOS Wi-Fi Applications
if (!context.connected) {
return;
}
if (context.disconnect_requested) {
LOG_INF("Disconnection request %s (%d)",
status->status ? "failed" : "done",
status->status);
context.disconnect_requested = false;
} else {
LOG_INF("Received Disconnected");
context.connected = false;
}
cmd_wifi_status();
}
624
Chapter 12 Zephyr RTOS Wi-Fi Applications
625
Chapter 12 Zephyr RTOS Wi-Fi Applications
if (params->timeout == 0) {
params->timeout = SYS_FOREVER_MS;
}
/* SSID */
params->ssid = CONFIG_STA_SAMPLE_SSID;
params->ssid_length = strlen(params->ssid);
#if defined(CONFIG_STA_KEY_MGMT_WPA2)
params->security = 1;
#elif defined(CONFIG_STA_KEY_MGMT_WPA2_256)
params->security = 2;
#elif defined(CONFIG_STA_KEY_MGMT_WPA3)
params->security = 3;
#else
params->security = 0;
#endif
#if !defined(CONFIG_STA_KEY_MGMT_NONE)
params->psk = CONFIG_STA_SAMPLE_PASSWORD;
params->psk_length = strlen(params->psk);
#endif
params->channel = WIFI_CHANNEL_ANY;
/* MFP (optional) */
params->mfp = WIFI_MFP_OPTIONAL;
return 0;
}
626
Chapter 12 Zephyr RTOS Wi-Fi Applications
627
Chapter 12 Zephyr RTOS Wi-Fi Applications
int main(void)
{
memset(&context, 0, sizeof(context));
net_mgmt_init_event_callback(&wifi_shell_mgmt_cb,
wifi_mgmt_event_handler,
WIFI_SHELL_MGMT_EVENTS);
net_mgmt_add_event_callback(&wifi_shell_mgmt_cb);
net_mgmt_init_event_callback(&net_shell_mgmt_cb,
net_mgmt_event_handler, NET_EVENT_IPV4_
DHCP_BOUND);
net_mgmt_add_event_callback(&net_shell_mgmt_cb);
LOG_INF("Starting %s with CPU frequency: %d MHz",
CONFIG_BOARD, SystemCoreClock/MHZ(1));
k_sleep(K_SECONDS(1));
#if defined(CONFIG_BOARD_NRF7002DK_NRF7001_NRF5340_CPUAPP) || \
defined(CONFIG_BOARD_NRF7002DK_NRF5340_CPUAPP)
if (strlen(CONFIG_NRF700X_QSPI_ENCRYPTION_KEY)) {
char key[QSPI_KEY_LEN_BYTES];
int ret;
ret = bytes_from_str(CONFIG_NRF700X_QSPI_
ENCRYPTION_KEY, key, sizeof(key));
if (ret) {
LOG_ERR("Failed to parse encryption key:
%d\n", ret);
return 0;
}
628
Chapter 12 Zephyr RTOS Wi-Fi Applications
629
Chapter 12 Zephyr RTOS Wi-Fi Applications
The default clock frequency is 64 MHz, but it can be set to 128 MHz, if
necessary, by calling the function
nrfx_clock_divider_set(NRF_CLOCK_DOMAIN_HFCLK,
NRF_CLOCK_HFCLK_DIV_1);
OK
*** BooOK
...
OK
OK
ting nRF Connect SDK v2.5.0 ***
[00:00:00.446,105] <OK
inf> net_config: Initializing network
[00:00:00.446,105] <inf> net_config: Waiting interface 1
(0x20001478) to be up...
[00:00:00.446,228] <inf> net_config: IPv4 address: 192.168.1.99
[00:00:00.446,289] <inf> net_config: Running dhcpv4 client...
[00:00:00.446,563] <inf> sta: Starting nrf7002dk_nrf5340_cpuapp
with CPU frequency: 128 MHz
[00:00:01.446,594] <inf> sta: QSPI Encryption disabled
[00:00:01.446,624] <inf> sta: Static IP address (overridable):
192.168.1.99/255.255.255.0 -> 192.168.1.1
[00:00:02.230,560] <inf> sta: Connection requested
[00:00:02.230,590] <inf> sta: ==================
[00:00:02.230,590] <inf> sta: State: SCANNING
[00:00:02.530,700] <inf> sta: ==================
....
630
Chapter 12 Zephyr RTOS Wi-Fi Applications
631
Chapter 12 Zephyr RTOS Wi-Fi Applications
components will be Python echo client and echo server applications, both
UDP and TCP variants running on a PC and using a Wi-Fi interface that
uses the same access point as used by the nRF7002 DK board.
The importance of this exercise is to show the basic techniques
involved in implementing TCP and UDP applications running over a Wi-Fi
interface and how to test them out using Python script–based applications
running on a PC. Instead of using a PC, an embedded Linux system such
as a Raspberry Pi can also be used.
The main() function of the application simply creates and starts a
number of threads that do all the work, as shown in the following code
snippet:
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sta, CONFIG_LOG_DEFAULT_LEVEL);
#include <zephyr/kernel.h>
#include "Task/Wifi_Setup.h"
#include "Task/Led.h"
#include "Task/TCP_Server.h"
#include "Task/TCP_Client.h"
#include "Task/UDP_Server.h"
#include "Task/UDP_Client.h"
#include "Task/deviceInformation.h"
// added
#include <nrfx_clock.h>
#include <zephyr/device.h>
#include <zephyr/net/net_config.h>
void main( void ) {
nrfx_clock_divider_set(NRF_CLOCK_DOMAIN_HFCLK,
NRF_CLOCK_HFCLK_DIV_1);
printk("Starting %s with CPU frequency: %d MHz\n",
CONFIG_BOARD, SystemCoreClock/MHZ(1));
632
Chapter 12 Zephyr RTOS Wi-Fi Applications
target_sources(app PRIVATE
src/main.c
src/Task/Led.c
src/Task/TCP_Client.c
src/Task/TCP_Server.c
src/Task/UDP_Client.c
src/Task/UDP_Server.c
src/Task/Wifi_Setup.c
)
633
Chapter 12 Zephyr RTOS Wi-Fi Applications
Whether to place the code for setting up a Wi-Fi connection into the
application’s main() function or whether to run it as a separate thread
is a design decision. In this example, the work is delegated to a separate
thread, and the thread-related code is in the file Wifi_Setup.c. Wifi_
Setup.c has the code for Wi-Fi event handling, utilities for printing out
logging and error information, as well as the details of DHCP interface
configuration.
The initialization and creation of the thread that sets up and manages
the Wi-Fi connection are carried out by the function ask_Wifi_Setup_
Init(), the code for which is shown in the following code snippet:
634
Chapter 12 Zephyr RTOS Wi-Fi Applications
WIFI_PRIORITY, \
0, \
K_NO_WAIT);
k_thread_name_set(&wifiThread, "wifiSetup");
k_thread_start(&wifiThread);
}
635
Chapter 12 Zephyr RTOS Wi-Fi Applications
CONFIG_NET_CONFIG_MY_IPV4_ADDR, \
CONFIG_NET_CONFIG_MY_IPV4_NETMASK, \
CONFIG_NET_CONFIG_MY_IPV4_GW );
while ( 1 ) {
Wifi_Connect();
for ( i = 0; i < TIMEOUT_MS; i++ ) {
k_sleep( K_MSEC( STATUS_POLLING_MS ));
Cmd_Wifi_Status();
if ( context.connect_result ) {
break;
}
}
if ( context.connected ) {
LOG_INF( "============" );
k_sleep( K_FOREVER );
}
else if ( !context.connect_result ) {
LOG_ERR( "Connection Timed Out" );
}
}
}
636
Chapter 12 Zephyr RTOS Wi-Fi Applications
while( 1 ) {
if( context.connected ) {
gpio_pin_toggle_dt( &led );
k_msleep ( LED_SLEEP_TIME_MS );
} else {
gpio_pin_set_dt( &led, 0 );
k_msleep( LED_SLEEP_TIME_MS );
}
}
}
637
Chapter 12 Zephyr RTOS Wi-Fi Applications
that simply echoes back any messages it receives to the client that sent the
message. The code shown in the following is a standard implementation
using the BSD Sockets API as supported by Zephyr RTOS. Essentially it
creates and initializes a socket, binds the socket, and waits for connections.
For each connection, it extracts the sent data and sends it back to the
client. The following code shows the implementation used in the example,
here. At the start of the function, the code polls (waits) till a connection
is established with the access point and DHCP assigns an IP address to
the board.
void UDP_Server(void) {
int udpServerSocket;
struct sockaddr_in bindingAddress;
int bindingResult;
struct sockaddr clientAddress;
socklen_t clientAddressLength;
int receivedBytes;
int sentBytes = 0;
uint8_t *pTransmitterBuffer;
// Poll intermittently till a DHCP IP address is
assigned to the board
while( !context.connected ) {
k_msleep( UDP_SERVER_SLEEP_TIME_MS );
}
638
Chapter 12 Zephyr RTOS Wi-Fi Applications
639
Chapter 12 Zephyr RTOS Wi-Fi Applications
&clientAddress, \
&clientAddressLength ); \
if ( receivedBytes <= 0 ) {
if( receivedBytes < 0 ) {
LOG_ERR( "UDP Server: Connection
error %d",
errno );
sentBytes = -errno;
}
break;
}
} else {
pTransmitterBuffer += sentBytes;
receivedBytes -= sentBytes;
}
LOG_INF( "UDP Server mode: Received and
replied with %d " \
640
Chapter 12 Zephyr RTOS Wi-Fi Applications
"bytes", sentBytes );
} while ( true );
if ( sentBytes < 0 ) {
k_sleep( K_FOREVER );
}
}
}
import socket
import sys
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('192.168.1.87', 31337)
message = b'This is the message. It will be repeated.'
try:
# Send data
print ('sending "%s"' % message)
sent = sock.sendto(message, server_address)
# Receive response
print ('waiting to receive')
data, server = sock.recvfrom(4096)
print ('received "%s"' % data)
finally:
print('closing socket')
sock.close()
641
Chapter 12 Zephyr RTOS Wi-Fi Applications
The IP address and the port number could either be hardwired into the
code, as shown here, or could be passed in via command-line arguments
by modifying the script accordingly.
642
Chapter 12 Zephyr RTOS Wi-Fi Applications
bindingResult = bind( \
tcpServerSocket, \
( struct sockaddr * )&bindAddress, \
sizeof( bindAddress ));
if ( bindingResult < 0 ) {
LOG_ERR( "TCP Server error: bind: %d\n", errno );
k_sleep( K_FOREVER );
}
643
Chapter 12 Zephyr RTOS Wi-Fi Applications
// Send
pTransmitterBuffer = receiverBuffer;
do {
// Echoing back the received message bytes
sentBytes = send ( tcpClientSocket,
pTransmitterBuffer,
receivedBytes, 0 ) ;
if ( sentBytes < 0 ) {
LOG_ERR( "TCP Server error: send: %d\n",
errno );
close( tcpClientSocket );
LOG_ERR( "TCP server: Connection from %s
closed\n",
addressString );
644
Chapter 12 Zephyr RTOS Wi-Fi Applications
}
pTransmitterBuffer += sentBytes;
receivedBytes -= sentBytes;
LOG_INF( "TCP Server mode. "
"Received and replied with %d bytes",
sentBytes );
} while ( receivedBytes );
}
}
}
import socket
HOST = "192.168.1.87" # The server's hostname or IP address
PORT = 31337 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
print("Sending data ",b"Hello, world - test message")
s.sendall(b"Hello, world - test message")
data = s.recv(1024)
print(f"Received {data!r}")
645
Chapter 12 Zephyr RTOS Wi-Fi Applications
void UDP_Client() {
struct sockaddr_in serverAddress;
int connectionResult;
int sentBytes = 0;
int errorCode = 0;
// Starve the thread until a DHCP IP is assigned to
the board
while( !context.connected ) {
k_msleep( UDP_CLIENT_SLEEP_TIME_MS );
}
// Server IPV4 address configuration
serverAddress.sin_family = AF_INET;
//serverAddress.sin_port = htons( UDP_CLIENT_PORT );
// port number and ip address should be altered as
necessary
serverAddress.sin_port = htons( 31337 );
errorCode = inet_pton( AF_INET, "192.168.1.86", \
&serverAddress.sin_addr );
// Create client socket
udpClientSocket = socket( serverAddress.sin_family,
SOCK_DGRAM,
IPPROTO_UDP );
if ( udpClientSocket < 0 ) {
LOG_ERR( "UDP Client error: socket: %d\n", errno );
k_sleep( K_FOREVER );
}
646
Chapter 12 Zephyr RTOS Wi-Fi Applications
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = '192.168.1.86'
server_port = 31337
server = (server_address, server_port)
sock.bind(server)
647
Chapter 12 Zephyr RTOS Wi-Fi Applications
648
Chapter 12 Zephyr RTOS Wi-Fi Applications
649
Chapter 12 Zephyr RTOS Wi-Fi Applications
missingBytesToSend -= sentBytes;
} while ( missingBytesToSend );
}
import socket
import sys
import string
import random
server_ip_address = '192.168.1.86'
size = 1024
server_port = 31337
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (server_ip_address, server_port)
sock.bind(server_address)
# Listen for incoming connections
sock.listen(1)
while True:
print('waiting for a connection')
connection, client_address = sock.accept()
try:
print('connection from', client_address)
# Receive the data pieces and retransmit them as
they arrive
while True:
data = connection.recv(size)
if data:
print("data received: ", data)
connection.send(data)
finally:
connection.close()
650
Chapter 12 Zephyr RTOS Wi-Fi Applications
References
1. https://www3.nd.edu/~mhaenggi/NET/
wireless/802.11b/Data%20Link%20Layer.htm
2. www.makeuseof.com/tag/wep-wpa-wpa2-wpa3-
explained/
3. https://standards.ieee.org/beyond-standards/
the-evolution-of-wi-fi-technology-and-
standards/
4. https://documentation.meraki.com/General_
Administration/Tools_and_Troubleshooting/
Analyzing_Wireless_Packet_Captures
5. https://developer.nordicsemi.com/nRF_
Connect_SDK/doc/latest/zephyr/connectivity/
networking/api/net_mgmt.html
6. https://documentation.meraki.com/MR/Wi-Fi_
Basics_and_Best_Practices/Extending_the_LAN_
with_a_Wireless_Mesh_Link
651
Chapter 12 Zephyr RTOS Wi-Fi Applications
7. https://documentation.meraki.com/MR/
Wi-Fi_Basics_and_Best_Practices/802.11w_
Management_Frame_Protection_MFP
8. www.nordicsemi.com/-/media/Software-and-
other-downloads/Product-Briefs/nRF7002-DK_
Product-Brief-v1.0.pdf
9. https://docs.nordicsemi.com/bundle/ncs-
latest/page/nrf/gsg_guides/nrf7002_gs.html
652
Index
A ARM Cortex M7 processor, 381, 382
ARM Cortex M devices, 26
Access point, 580
ARM Cortex M microcontroller, 51
mobile station, 580
ARM processor–based boards, 126
Acknowledgment (ACK)
ARM TrustZone security, 587
scheme, 575
__ASSERT() macro, 146
Actions View, 95, 97
Asynchronous exceptions, 50
Active scanning, 358
Asynchronous protocol, 443
Activity synchronization, 56, 59, 60
Attribute protocol (ATT), 308, 314,
Actual driver functions, 105, 441
317, 318, 343, 344, 347, 372
ADC (Analog to Digital)
Automatic memory partitions,
conversion, 3, 20, 445
202, 413
Advertising Data (AD), 359, 362
AVR, 19
An entry, 56, 128, 471
AWS FreeRTOS, 25, 26
APP_BMEM and APP_DMEM, 413
Azure RTOS ThreadX, 25, 26
APP_DMEM, 412
Application Programming
Interfaces (APIs), 217, B
439, 441
Applications View, 91 Bar-device, 461
app_memory subsystem, 205 Barrier synchronization, 56, 57
_arch_syscall_invoke0(), 177 Battery service (BAS)
_arch_syscall_invoke6(), 177 characteristics, 339, 340
Architecture-specific Client Characteristic
subdirectories Configuration
(dts/<ARCH>), 459 Descriptor, 341
Arduino connectors, 507 configuring dongle to
ARG_UNUSED, 156, 560 emulate, 339
654
INDEX
655
INDEX
656
INDEX
657
INDEX
658
INDEX
659
INDEX
660
INDEX
661
INDEX
662
INDEX
663
INDEX
Multithreading in Zephyr N
FreeRTOS, 128 NET_DEVICE_INIT() macro, 378
guard-based stack overflow net_if object, 397
detection, 131 net_if_ipv4_addr_add(), 378
K_FOREVER, 131 NET_IF_UP flag, 380
K_KERNEL_STACK macros, 132 net_mgmt(), 398
K_KERNEL_STACK_ net_mgmt() API, 397
DEFINE, 131 net_mgmt_add_event_callback()
K_NO_WAIT, 131 function, 398
k_thread_create(), 131 net_mgmt_del_event_callback()
K_THREAD_DEFINE function, 398
macro, 132 net_mgmt_event_callback()
K_THREAD_STACK_ function, 401, 404
DEFINE, 131 net_mgmt_event_callback
life cycle, 131 structure, 403
Microsoft Windows, 128 net_mgmt_event_callback()
MPU, 127 function, 401
operating system, 127 net_mgmt_event_handler_t
properties, 128 handler, 403
small 32-bit SoC net_mgmt_event_init(void), 403
devices, 127 net_mgmt_event_notify()
stack area, 128 function, 400
stack buffer, 131 net_mgmt_event_wait()
thread control block, 128 function, 404
thread options, 129 net_mgmt_init_event_callback()
_THREAD_STACK macros, 132 helper function, 398
thread states, 130 net_mgmt_request_handler_t, 400
user mode threads, 129 Network Address Translation
Zephyr framework, 130 (NAT), 385
mutex/semaphore, 169 Network connection map, BLE
Mutual exclusions semaphore feature, 333, 334
(Mutex), 36–38
664
INDEX
665
INDEX
666
INDEX
667
INDEX
668
INDEX
669
INDEX
670
INDEX
671
INDEX
T ThreadX, 25
Time-critical event handling, 21
Task flexibility, 24
Timers, 52, 53
Task (thread), 29–33
Time-sensitive activities, 52
TCP, 423
Timing, 52, 53
TCP_Client() function, 648
Toggle_Led(), 636
TCP echo server, 385, 421, 650
Toolchain Manager, 82, 83
TCP/IP network–based
Two-way data
application, 381
communication, 44, 45
TCP/IP server-side connections
data structures, 424, 425
thread structures pool, 426–430 U
TCP_Server.c file, 642
UART Communications
Thread, 62–64, 218, 533
MCU, 108
Thread custom data, 141
nRF52840 DK board, 109
Thread priorities
TXD line, 110
cooperative thread, 139
USB connection, 110
integer value, 139
uart_out thread blocks, 150
K_ESSENTIAL option, 140
UART peripheral, 463
K_FP_REGS option, 140
UART thread entry function, 150
K_INHERIT_PERMS option, 141
UDP_Client(), 646
K_USER option, 141
UDP echo server service, 421
limits, 139
UDP_Server.c file, 637
preemptible thread, 139
UML sequence diagram, 242
preemptive multitasking
Unit addresses, device trees
operating systems, 139
/aliases and /chosen nodes, 457
priority ranges
/chosen node’s properties, 457
cooperative thread, 140
Compatible property, 454
preemptive thread, 140
interrupts property, 456
Zephyr documentation,
label property, 455
cooperative threads, 139
reg devicetree node
Zephyr kernel, 139
property, 455
Thread prioritization policies, 168
Unit addresses, device trees
Thread termination, 142
additional values, 454
672
INDEX
673
INDEX
674
INDEX
675
INDEX
676
INDEX
677