In this exercise, you will learn through hands-on exercises how to differentiate between thread context and interrupt context, and to understand the allowed operations in each.
The exercise covers the use of the Kernel Timer API. It is used to periodically trigger a function, called an interrupt service routine (ISR), that runs in the System timer interrupt context. The Kernel uses the System timer, which is the RTC1 peripheral on the nRF91, nRF53, and nRF52 Series hardware. On the nRF54 Series, it’s the global real-time counter peripheral (GRTC).
You will practice collecting information about all the threads running in an nRF Connect SDK application: their stack allocation and run-time usage, as well as the priority and state (Running, Ready, Unready). You will also learn how to see the ready queue content at any given time. For both of these, you will use the nRF Debug view in nRF Connect for VS Code.
In addition, you will learn how to use a message queue to pass data safely between threads.
You only need to do steps A to D once throughout the course.
Get the source code of this course as a third-party repository inside the SDK itself. This gives you better integration with the nRF Connect for VS Code extension.
A. Open VS Code, then open the nRF Connect terminal as shown below.
Important
It is important to use the nRF Connect terminal as this is the terminal where the build toolchain, such as west, are sourced and available for use. If you use the other terminals, you will get an error saying that the command is not recognized.
B. Type the command west manifest --path in nRF Connect Terminal.
Hover over the printed file path, hold Ctrl, and left-click to open the file.
C. Include the repo of the course.
Include the repository by adding the following lines inside the west.yml file of your selected SDK version. Add the following information at the end of the # Other third-party repositories section. Make sure to save the new changes in the file.
This repository contains the exercise code base and solutions. Make sure to select the branch that corresponds with the nRF Connect SDK version of your choosing: main: For nRF Connect SDK version v3.0.0 v2.9.0-v2.7.0: For nRF Connect SDK versions v2.9.0 to v2.7.0 v2.6.2-v2.5.2: For nRF Connect SDK versions v2.6.2-v2.5.2
This can be done by changing therevision: value in the above snippet.
Make sure that the copied code’s indentation is as shown in the screenshot below. Otherwise, west will throw an error.
D. Type west update -x devacademy-ncsinter to get the course repository.
With this, the course’s exercises (base code and solutions) are stored locally on your machine in <install_path>\nrf\samples\devacademy\ncs-inter and nRF Connect for VS Code can detect them since they have the right metadata files (sample.yaml).
Note
We added the course repository to the nRF Connect SDK itself purely for educational purposes. The goal is to expose you to west.yaml and some west commands that can pull in third-party repositories. You can also get the course repository using GitHub directly, as done in the nRF Connect SDK Fundamentals course.
Opening the code base
You will need to do this step for every exercise in this course.
A. In nRF Connect for VS Code, create a new application.
Navigate to the nRF Connect for VS Code extension, select Create a new application and then select the Copy a sampleoption as shown below.
Choose the SDK version you plan to use if you have multiple versions installed.
B. Type in the name of the exercise code base.
In the Create New Application from Sample window, type in the name of the exercise. For this exercise, the name is Lesson 1 – Exercise 1. Make sure you select the code base (not the solution), as shown below.
C. Select a folder where you want the code to be stored.
Type in the path to the folder where you want the code to be stored, followed by the name of the code base. The examples in this exercise will store the code in C:\myfw\ncsinter and name it l1_e1 .
D. Press Enter to make a copy of the base code and store it in the specified directory.
Now, you are ready to work on the code.
Building the application
In this exercise, you will develop an application with five threads (producer, consumer, main, logging, idle) and a function that runs periodically every 500 milliseconds (timer) in an interrupt context. The timer will do a simple task of toggling LED0 and LED1 in sequence.
The main thread is a system thread created by the kernel itself, and calls the main() function defined by the user code if one exists. In this exercise, you will define a main() function that will setup the GPIO pins for LED0 and LED1, start a timer, and then terminate normally by returning 0. You will examine the functions by using nRF Debug view in nRF Connect for VS Code.
The logging thread is created by default when enabling the logger module, which uses the deferred mode by default.
The idle thread is a system thread that gets created by the kernel itself, and is responsible for putting the system in a power-saving mode.
The main thread priority is set with CONFIG_MAIN_THREAD_PRIORITY, which has the default value of 0 (the highest priority for a preemptable thread). The stack for the main thread is set to the default value of CONFIG_MAIN_STACK_SIZE, which is 1024 bytes.
You will create a producer thread with a priority of 6. In the producer thread, you will generate emulated sensor values (x,y,z accelerometer emulated data) and put them in a message queue every 2.2 seconds. You will set the stack size for the producer thread to 2048 bytes.
The consumer thread will be created with a priority of 7 and a stack size of 2048 bytes. It will simply sleep until there is some data in the message queue. Once there is data in the message queue, the thread state is changed to Ready and the scheduler will schedule for execution. The consumer thread consumes the data in the message queue and sends it to the logger module. The logger module, once it’s scheduled, sends the data to the logger backend, which is UART0 by default. The scheduler will then have no Ready threads in the ready queue except the idle thread. The idle thread is called, and the system enters a power-saving mode which is automatically managed by the idle thread. This cycle is repeated once the producer thread wakes up from sleep.
Modifying the code
1. Enable debugging.
Add the following lines in the application configuration file (prj.conf):
It’s also possible to enable debugging by selecting the option Optimize for debugging (-Og) in the Optimization level in the Add Build Configuration GUI within nRF Connect for VS Code. Checking this option will pass -DCONFIG_DEBUG_OPTIMIZATIONS:STRING="y" -DCONFIG_DEBUG_THREAD_INFO:STRING="y" to the Cmake build system. Enabling debugging has the same effect, whether enabled in VS Code, prj.conf or both.
2 . Create a timer with a handler function that executes in an interrupt context.
2.1. Define a Timer.
The timer is defined by using the K_TIMER_DEFINE() macro. It takes three parameters:
Name of the timer to be created.
An expiry function that is executed each time the timer expires (fires). This function is executed in the interrupt context by the system timer interrupt handler. If no expiry function is required, a NULL function can be specified. Since this function executes in an interrupt context, it will interrupt anything running. This means that you need to be careful not to call kernel APIs that are intended for the thread context, and absolutely no blocking operation is allowed in this function.
A stop function is executed if the timer is stopped prematurely while running. The function is executed in thread context by the thread that stops the timer. If no stop function is required, a NULL function can be specified.
Add the following code in main.c:
Copy
K_TIMER_DEFINE(timer0, timer0_handler, NULL);
C
The code above creates a timer (timer0) and assigns it the expiry function timer0_handler which we will define in Step 2.3. Since the plan is not to stop the timers prematurely, the stop function is set to NULL.
2.2. Start the timer.
The timer will be started with the k_timer_start() function. Use the function to set the timer as a periodic timer or a one-shot timer. The function takes three parameters:
The timer variable, which you defined in Step 2.1.
The duration, which is the time interval for the timer to expire for the first time. This is a k_timeout_t value that may be initialized using different units, such as K_MSEC() or K_SECONDS().
The period, which is the time interval between all timer expirations after the first one, also a k_timeout_t value that must be non-negative. Setting a period of K_NO_WAIT (equal to zero) or K_FOREVER means that the timer is a one-shot timer that stops after a single expiration.
For example, if a timer is started with a duration of 200 ms and a period of 75 ms, it will first expire after 200 ms and then every 75 ms afterward.
Create a periodic timer that fires up every 0.5 seconds. Add the following code in main():
Copy
/* start periodic timer that expires once every 0.5 second */k_timer_start(&timer0, K_MSEC(500), K_MSEC(500));
C
2.3. Create the expiry function for the timer.
The expiry function is the function that will execute when the timer fires. Since this function runs in an interrupt context (the system timer – ISR), extra care needs to be taken with the code put inside it. In this exercise, you will keep the code inside the expiry functions very short, and simply toggle either LED0 or LED1 every time the timer fires (every 0.5 seconds), making each LED toggle once every second.
3. Pass data between threads using a message queue.
3.1. Define the data type of the message.
Define the message that will be exchanged between the producer and consumer threads as a struct SensorReading of three uint32_t members, as shown below
As covered in the previous topic, use the function k_msgq_put() to write messages to the message queue from the producer thread.
Add the following code inside the producer_func function:
Copy
ret = k_msgq_put(&device_message_queue,&acc_val,K_FOREVER);if (ret){LOG_ERR("Return value from k_msgq_put = %d",ret); }
C
3.4. Read messages from the message queue.
Use the k_msgq_get() function to read messages from the message queue.
Add the following code inside the consumer_func function:
Copy
ret = k_msgq_get(&device_message_queue,&temp,K_FOREVER);if (ret){LOG_ERR("Return value from k_msgq_get = %d",ret); }
C
Note how the timeout option K_FOREVER is used to make the thread wait (change the thread’s state to Unready) until there is data in the message queue.
4. Build your application.
This is done by adding a build configuration from the Applications view.
Debugging and analytics
This section is two sets of instructions: one for threads and another for ISRs. To complete either set, you must first complete the following steps:
1. Start a debugging session.
Press the Debug button in the Actions view.
This will flash your board with the firmware and start a debugging session. Execution will break by default at main().
2. Open the Run and Debug View.
Click on the Run and Debug icon in the primary sidebar.
3. Open the Thread Viewer.
Click on nRF Debug in the Panel View area to view the Thread Viewer.
Using the Thread Viewer, you can find crucial information about all the threads in the application, including:
Number of threads in an nRF Connect SDK application.
Priority of each thread.
State of each thread at any given time.
Allocated stack and real-time stack usage for each thread. This is very useful for optimizing the stack usage and pinpointing possible overflow issues.
Threads control block.
Entry function associated with each thread.
Thread options for each thread.
Complete documentation of the Thread Viewer can be found here.
As you can see in the screenshot above, the application has five threads (main, producer, consumer, logging, idle) with priorities 0, 6, 7, 14, and 15. The currently running thread is the main thread, indicated by the yellow arrow symbol. The stack allocated to each thread is shown in the Thread Viewer, and you can also click the Enable Tracking button to show the stack usage of the stack pointer in real-time as well as the maximum stack usage.
All the entries in the Thread Viewer are clickable. For example, clicking on a thread entry will take you to the code where the function is defined. The same applies for a thread control block.
Threads in an nRF Connect SDK application
Complete the following steps using nRF Debug View in nRF Connect for VS Code to find information about the threads in an nRF Connect SDK application: their stack allocation and run-time usage, as well as the priority and state (Running, Ready, Unready). You can also see the ready queue content.
1. Set two breakpoints in your code.
Set one breakpoint in the producer thread, right after the k_msgq_put() call, and the other in the consumer thread, right after the k_msgq_get() call. Set the breakpoints by clicking on the column next to the line numbers, as shown in the screenshot below.
2. Continue the execution.
Click on the Start/Continue button, or press F5.
3. Watch the message queue and enable stack usage.
The breakpoint in the producer thread will be hit first, because the producer thread will run right after the main thread. This is because it is the thread with the highest priority in the ready queue by the time the main thread terminates.
The first thing to notice in the Thread View is that the main thread no longer exists since the thread has terminated normally by exiting. The producer thread is now the running thread, as indicated by the yellow arrow.
Right-click on the device_message_queue variable, and select Add to Watch. This will add the variable device_message_queue to the Watch window so you can examine what is happening inside the message queue. As you can see in the screenshot below, one message is pushed in the message queue as reflected by the used_msgs: 1 entry.
Also, for the sake of demonstration, enable stack real-time monitoring by pressing on the brush symbol next to the Stack Usage column name.
4. Continue the execution.
Click on the Start/Continue button or press F5, just like in Step 2.
The producer thread will call the k_msleep() kernel function, which will put the thread to sleep and change its state to Unready (Suspended) until the PRODUCER_SLEEP_TIME_MS elapses. The scheduler will pick up the highest-priority thread in the ready queue, which at this moment is the consumer thread. This is shown clearly in the Thread Viewer.
5. Watch the message queue.
Notice the consumer thread has safely consumed the message that the producer thread put in the message queue. You can see that the used_msgs entry is now zero.
Also, note the small up-facing arrow in the stack usage for each thread. This shows you the max stack usage that the stack pointer of each thread has reached in real-time. At the same time, the number shows you the current stack consumption.
6. Continue the execution.
The thread will try to get another item from the message queue (since the k_msgq_get() call is placed inside an infinite loop), but there will be none since the producer thread is already asleep. Therefore, the consumer thread will also be in an Unready state. This leaves only the logging and idle threads in the ready queue.
The scheduler will pick the logging thread, which will send the logs to its backend (UART0 by default). Then the logging thread will no longer have any logs to send out, meaning it will be in the Unready state as well.
The scheduler will then schedule the only thread left in the ready queue, which is the idle thread. The idle thread will be running most of the time, ensuring the system is in power-saving mode.
More on this
The logger thread also has a timeout option which makes it possible go to sleep for some time, defined in CONFIG_LOG_PROCESS_THREAD_SLEEP_MS before it becomes ready.
7. Remove the breakpoints from the producer and consumer threads.
Remove the breakpoints by clicking on them in the column next to the line numbers, similar to how you added them in Step 1.
8. Continue the execution.
9. View the serial output of your development kit.
Switch from the Debug view to the nRF Connect extension, and connect to the serial output of your development kit from the Connected Devices window, as shown below.
You should see an output similar to the one shown below:
[00:02:01.255,950] <inf> Less1_Exer1: Values got from the queue: 155.155.155
[00:02:03.456,054] <inf> Less1_Exer1: Values got from the queue: 156.156.156
[00:02:05.656,158] <inf> Less1_Exer1: Values got from the queue: 157.157.157
[00:02:07.856,262] <inf> Less1_Exer1: Values got from the queue: 158.158.158
[00:02:10.056,365] <inf> Less1_Exer1: Values got from the queue: 159.159.159
[00:02:12.256,469] <inf> Less1_Exer1: Values got from the queue: 160.160.160
[00:02:14.456,573] <inf> Less1_Exer1: Values got from the queue: 161.161.161
ISR in an nRF Connect SDK application
Complete the following steps using nRF Debug View in nRF Connect for VS Code to examine the ISR in an nRF Connect SDK application.
1. Add a breakpoint in the timer0_handler interrupt context function.
This function will run in the System Timer ISR context. Therefore, it will be triggered periodically and interrupt any thread running when they fire. Each time the function is triggered, the breakpoint of the timer will be hit.
Set the breakpoint by clicking on the column next to the line number, as shown in the screenshot below.
2. Continue the execution.
Click on the Start/Continue button or press F5.
3. Examine the debugging call stack.
Notice is that timer0_handler() runs in an interrupt context. In the screenshot above, this can be seen in the Call stack → Exception handler: timer0_handler() has interrupted the idle thread.
4. Remove the breakpoint set in the timer0_handler() interrupt context function.
Remove the breakpoint by clicking on it in the column next to the line numbers, similar to how you added it in Step 1.
Experimenting with the dos and don’ts of the interrupt context
For the sake of demonstration, let’s explore what happens if you call a kernel API that is intended for the thread context inside the timer0_handler() interrupt context function?
1. Add a kernel API call inside the timer0_handler() interrupt context function.
Add the k_msleep(2000); call to the function.
2. Build the application.
The application will build, but it will contain a fatal runtime error.
3. Flash the application to your development kit.
You should observe that the system will keep crashing and resetting as you are never supposed to block inside interrupt context.
If you are using a build target with TF-M (such as nrf9161dk_nrf9161_ns), the error message will be on to the TF-M log output, which is not routed to VCOM by default. So on the application log output, you will see the output: *** Booting nRF Connect SDK v2.5.0 *** [00:00:00.250,854] Less1_Exer1: Values got from the queue: 100.100.100 The board resets, and then the cycle repeats itself.
4. Restore the application.
Remove the k_msleep(2000); call from inside the timer0_handler() function, then rebuild and flash the application to your development kit.
Important
If you want to add a lengthy functionality inside the timer expiry function, you must use a work queue to submit the work.
Nordic Developer Academy Privacy Policy
1. Introduction
In this Privacy Policy you will find information on Nordic Semiconductor ASA (“Nordic Semiconductor”) processes your personal data when you use the Nordic Developer Academy.
References to “we” and “us” in this document refers to Nordic Semiconductor.
2. Our processing of personal data when you use the Nordic Developer Academy
2.1 Nordic Developer Academy
Nordic Semiconductor processes personal data in order to provide you with the features and functionality of the Nordic Developer Academy. Creating a user account is optional, but required if you want to track you progress and view your completed courses and obtained certificates. If you choose to create a user account, we will process the following categories of personal data:
Email
Name
Password (encrypted)
Course progression (e.g. which course you have completely or partly completed)
Certificate information, which consists of name of completed course and the validity of the certificate
Course results
During your use of the Nordic Developer Academy, you may also be asked if you want to provide feedback. If you choose to respond to any such surveys, we will also process the personal data in your responses in that survey.
The legal basis for this processing is GDPR article 6 (1) b. The processing is necessary for Nordic Semiconductor to provide the Nordic Developer Academy under the Terms of Service.
2.2 Analytics
If you consent to analytics, Nordic Semiconductor will use Google Analytics to obtain statistics about how the Nordic Developer Academy is used. This includes collecting information on for example what pages are viewed, the duration of the visit, the way in which the pages are maneuvered, what links are clicked, technical information about your equipment. The information is used to learn how Nordic Developer Academy is used and how the user experience can be further developed.
2.2 Newsletter
You can consent to receive newsletters from Nordic from within the Nordic Developer Academy. How your personal data is processed when you sign up for our newsletters is described in the Nordic Semiconductor Privacy Policy.
3. Retention period
We will store your personal data for as long you use the Nordic Developer Academy. If our systems register that you have not used your account for 36 months, your account will be deleted.
4. Additional information
Additional information on how we process personal data can be found in the Nordic Semiconductor Privacy Policy and Cookie Policy.
Nordic Developer Academy Terms of Service
1. Introduction
These terms and conditions (“Terms of Use”) apply to the use of the Nordic Developer Academy, provided by Nordic Semiconductor ASA, org. nr. 966 011 726, a public limited liability company registered in Norway (“Nordic Semiconductor”).
Nordic Developer Academy allows the user to take technical courses related to Nordic Semiconductor products, software and services, and obtain a certificate certifying completion of these courses. By completing the registration process for the Nordic Developer Academy, you are agreeing to be bound by these Terms of Use.
These Terms of Use are applicable as long as you have a user account giving you access to Nordic Developer Academy.
2. Access to and use of Nordic Developer Academy
Upon acceptance of these Terms of Use you are granted a non-exclusive right of access to, and use of Nordic Developer Academy, as it is provided to you at any time. Nordic Semiconductor provides Nordic Developer Academy to you free of charge, subject to the provisions of these Terms of Use and the Nordic Developer Academy Privacy Policy.
To access select features of Nordic Developer Academy, you need to create a user account. You are solely responsible for the security associated with your user account, including always keeping your login details safe.
You will able to receive an electronic certificate from Nordic Developer Academy upon completion of courses. By issuing you such a certificate, Nordic Semiconductor certifies that you have completed the applicable course, but does not provide any further warrants or endorsements for any particular skills or professional qualifications.
Nordic Semiconductor will continuously develop Nordic Developer Academy with new features and functionality, but reserves the right to remove or alter any existing functions without notice.
3. Acceptable use
You undertake that you will use Nordic Developer Academy in accordance with applicable law and regulations, and in accordance with these Terms of Use. You must not modify, adapt, or hack Nordic Developer Academy or modify another website so as to falsely imply that it is associated with Nordic Developer Academy, Nordic Semiconductor, or any other Nordic Semiconductor product, software or service.
You agree not to reproduce, duplicate, copy, sell, resell or in any other way exploit any portion of Nordic Developer Academy, use of Nordic Developer Academy, or access to Nordic Developer Academy without the express written permission by Nordic Semiconductor. You must not upload, post, host, or transmit unsolicited email, SMS, or \”spam\” messages.
You are responsible for ensuring that the information you post and the content you share does not;
contain false, misleading or otherwise erroneous information
infringe someone else’s copyrights or other intellectual property rights
contain sensitive personal data or
contain information that might be received as offensive or insulting.
Such information may be removed without prior notice.
Nordic Semiconductor reserves the right to at any time determine whether a use of Nordic Developer Academy is in violation of its requirements for acceptable use.
Violation of the at any time applicable requirements for acceptable use may result in termination of your account. We will take reasonable steps to notify you and state the reason for termination in such cases.
4. Routines for planned maintenance
Certain types of maintenance may imply a stop or reduction in availability of Nordic Developer Academy. Nordic Semiconductor does not warrant any level of service availability but will provide its best effort to limit the impact of any planned maintenance on the availability of Nordic Developer Academy.
5. Intellectual property rights
Nordic Semiconductor retains all rights to all elements of Nordic Developer Academy. This includes, but is not limited to, the concept, design, trademarks, know-how, trade secrets, copyrights and all other intellectual property rights.
Nordic Semiconductor receives all rights to all content uploaded or created in Nordic Developer Academy. You do not receive any license or usage rights to Nordic Developer Academy beyond what is explicitly stated in this Agreement.
6. Liability and damages
Nothing within these Terms of Use is intended to limit your statutory data privacy rights as a data subject, as described in the Nordic Developer Academy Privacy Policy. You acknowledge that errors might occur from time to time and waive any right to claim for compensation as a result of errors in Nordic Developer Academy. When an error occurs, you shall notify Nordic Semiconductor of the error and provide a description of the error situation.
You agree to indemnify Nordic Semiconductor for any loss, including indirect loss, arising out of or in connection with your use of Nordic Developer Academy or violations of these Terms of Use. Nordic Semiconductor shall not be held liable for, and does not warrant that (i) Nordic Developer Academy will meet your specific requirements, (ii) Nordic Developer Academy will be uninterrupted, timely, secure, or error-free, (iii) the results that may be obtained from the use of Nordic Developer Academy will be accurate or reliable, (iv) the quality of any products, services, information, or other material purchased or obtained by you through Nordic Developer Academy will meet your expectations, or that (v) any errors in Nordic Developer Academy will be corrected.
You accept that this is a service provided to you without any payment and hence you accept that Nordic Semiconductor will not be held responsible, or liable, for any breaches of these Terms of Use or any loss connected to your use of Nordic Developer Academy. Unless otherwise follows from mandatory law, Nordic Semiconductor will not accept any such responsibility or liability.
7. Change of terms
Nordic Semiconductor may update and change the Terms of Use from time to time. Nordic Semiconductor will seek to notify you about significant changes before such changes come into force and give you a possibility to evaluate the effects of proposed changes. Continued use of Nordic Developer Academy after any such changes shall constitute your acceptance of such changes. You can review the current version of the Terms of Use at any time at https://academy.nordicsemi.com/terms-of-service/
8. Transfer of rights
Nordic Semiconductor is entitled to transfer its rights and obligation pursuant to these Terms of Use to a third party as part of a merger or acquisition process, or as a result of other organizational changes.
9. Third Party Services
To the extent Nordic Developer Academy facilitates access to services provided by a third party, you agree to comply with the terms governing such third party services. Nordic Semiconductor shall not be held liable for any errors, omissions, inaccuracies, etc. related to such third party services.
10. Dispute resolution
The Terms of Use and any other legally binding agreement between yourself and Nordic Semiconductor shall be subject to Norwegian law and Norwegian courts’ exclusive jurisdiction.