🐜 MetadataYou’ve probably used digitalWrite(LED_BUILTIN, HIGH); to blink an LED on Arduino — simple and fun. But in the embedded industry, things work a bit differently.In this guide, we’ll blink the onboard LED on the Thingy:91X using Zephyr RTOS through the nRF Connect SDK — the same tools used in real-world, production-grade firmware.
----------------------------------------------------------------------------------------------------------------------
👋 Welcome Aboard MakerTrain: A Hands-on Nordic Thingy:91X WorkshopYou're about to begin an exciting journey using the powerful Nordic Thingy:91X. This is a self-paced, async-style workshop designed for makers, developers, and students who want to learn by building a real-world project step by step.
This workshop is structured across three interconnected Hackster projects:
🔗 Workshop Modules:
- 🚆 1️⃣ MakerTrain: Pre-Departure Checklist – nRF SDK Setup
- 🚆 2️⃣ Introduction to Thingy:91X & nRF Connect SDK – Learn about the board, run your first app. (you are here!)
- 🚆 3️⃣ Build an Environmental Monitoring System – Put it all together in a real-world project.
Each module builds on the previous one, so it's best to follow them in order. While this workshop is async (move at your own pace), we’ll also walk through each part during the live session.
Let's get started 🎉.
----------------------------------------------------------------------------------------------------------------------
🧰 Pre-requisites- Thingy:91X development kit
- USB-C cable
- A Laptop with installed pre-requisites tools - MakerTrain: Pre-Departure Checklist - nRF SDK Setup.
We'll start by exploring the features of the Thingy:91X, then install the nRF Connect SDK with VS Code, and finally build our first application.
Nordic Thingy:91 XThe Nordic Thingy:91 X is a battery-operated prototyping platform for cellular IoT based on the nRF9151 System-in-Package (SiP) supporting LTE-M, NB-IoT, GNSS and NR+, and certified for global operation
It is the ideal platform for rapidly developing a prototype for any cellular IoT concept, and is especially suited for asset-tracking applications.
An exhaustive set of sensors is included to gather data about the environment, and the movement of the Nordic Thingy:91 X. Temperature, humidity, air quality, air pressure, magnetic field, acceleration and movement can easily be extracted for local or remote analysis.
Additional functionality can be easily added through the debug board connector or the expansion board connector, which is compatible with Qwiic, STEMMA QT, and Grove.
For user input, Nordic Thingy:91 X offers two user-programmable buttons. Visual output is achieved with user-programmable RGB LEDs. USB connectivity is done via nRF5340 System-on-Chip (SoC), which serves as the board controller, and also supports Bluetooth Low Energy (LE) for selected use cases. The nRF7002 Wi-Fi companion IC enables Wi-Fi locationing.
Key features
- Battery-operated prototyping platform for the nRF9151 SiP
- Certifications: FCC (USA), CE (EUR)
- LTE-M/NB-IoT/NR+, GNSS and Bluetooth LE/Wi-Fi antennas
- Wi-Fi locationing enabled by nRF7002
- User-programmable buttons and LEDs
- Environmental sensor for temperature, humidity, airquality, and air pressure, plus magnetometer.
- Low-power 3-axis accelerometer
- 6-axis IMU with gyroscope
- Rechargeable Li-Po battery with 1350 mAh capacity - nPM1300 PMIC for battery charging and fuel gauging
- Board controller: nRF5340 for connectivity between USB interface and nRF9151
The nRF Connect SDK is a software development kit for building low-power wireless applications based on Nordic Semiconductor’s nRF54, nRF53, nRF52, nRF70, or nRF91 Series devices.
It integrates the Zephyr Real-Time Operating System (RTOS) and a wide range of complete applications, samples, and protocol stacks such as Bluetooth Low Energy, Bluetooth mesh, Wi-Fi,Matter, Thread/Zigbee and LTE-M/NB-IoT/GPS, TCP/IP.
It also includes middleware such as CoAP, MQTT, LwM2M, various libraries, hardware drivers, Trusted Firmware-M for security, and a secure bootloader (MCUBoot).
The nRF Connect SDK offers a single code base for all of Nordic’s devices and software components. It simplifies porting modules, libraries, and drivers from one application to another, thus reducing development time.
Internally, the nRF Connect SDK code is organized into four main repositories:
- nrf – Applications, samples, connectivity protocols (Nordic)
- nrfxlib – Common libraries and stacks (Nordic)
- Zephyr – RTOS & Board configurations (open source)
- MCUBoot – Secure Bootloader (open source)
Zephyr RTOS is an open-source real-time operating system for connected and resource-constrained embedded devices.
It includes a scheduler that ensures predictable/deterministic execution patterns and abstracts out the timing requirements.
It also comes with a rich set of fundamental libraries and middleware that simplifies development and helps reduce a product’s time to market
🧰 ToolchainThe nRF Connect SDK development is based on Zephyr toolchain.
- Kconfig: generates definitions that configure the whole system, for example, which wireless protocol or which libraries to include in your application.
- Devicetree: Describes the hardware.
- CMake: Uses the information from Kconfig and the devicetree to generate build files.
- Ninja: will use to build the program (comparable to make).
- GCC: The compiler system is used to create the executables.
Now that we have an understanding of the content and structure of the nRF Connect SDK.
✨Let's Start ProgrammingI hope you already installed the - MakerTrain: Pre-Departure Checklist - nRF SDK Setup. If not, please do before going the next step.
Next, We will
- create a new application based on a template(blink).
- Build an application
- Flash an application to a board.
We will create a program using the template blinky
to do to toggle an LED on our board Step 1.1 Open VS Code and Create a new Application.
In VS Code, click on the nRF Connect Extension icon. In the WELCOME View, click on Create a new application.
Step 1.2 In the Create new application, select Copy a sample
You will be presented with three options: Create a blank application will create a blank application with an empty main()
function. While the Copy a sample will present you all the templates “samples” that come from the different modules in nRF Connect SDK and enable you to create an application based on a template. Note that if you have multiple SDK versions installed on your machine, you will be prompted to select which SDK version to copy a sample from
Step 1.3 Choose Blinky Sample Project
Step 1.4 Save the Sample Project ina Folder
Select where you want to store your application and give the folder your project name - I used as - blinkynRF91x
⚠️ For windows Create a folder on the C drive in C:\myfw\ncsfund. Avoid storing your applications in locations with long paths, as the build system might fail on some operating systems (Windows) if the application path is too long. Also, avoid using whitespaces and special characters in the path.
After that, VS Code will ask if you want to open the application in the same VS Code instance or open a new VS Code instance. Select Open to open the application in the same VS Code instance.
in Mac or Linux
or in Windows
Step 1.5 Add a build configurationThis will make a copy of the template selected “Blinky sample”, store it in the application directory you specified, and add an unbuilt application to VS Code as shown below:
Click the NCSlogo (Nordic connect SDK)to see the project configuration window.
Then select the "Add build configuration" -
Here, we need to specific few things as above.
- SDK - The nRF Connect SDK Version - Choose v3.0.0 or the one you installed
- Toolchain - The nRF Connect SDK Tool Chain - Choose v3.0.0 or the one you installed
- Board Target - Choose thingy91x/nrf9151/ns from the dropdown list.
- Build directory name - The name for the build directory - We don't need to change that from build
- Generate and Build - Finally click this to generate the configuration files.
Step 1.6 ExploreThe Build Files.
Right click the Project name under the APPLICATION Window and choose "Show in Explorer"
Here you can see the build files we created and to get back to the nRF Connect window, click in the nRF SDK Icon
Next, we can flash the blink program to the board. Since we are using the Thingy:91 X and they do not have an onboard debugger, the flash procedure is different, and the device will not show up under Connected Devices View in the nRF Connect for VS Code extension.
Step 2.1 Connect the Thingy:91 X to your computer with a micro-USB cable.
Step 2.2. Switch the Thingy:91 X by switching SW1 to the ON position.
Step 2.3: Modify the Kconfig file
CONFIG_BOOTLOADER_MCUBOOT=y
In Visual Studio Code, add the following Kconfig to the prj.conf
file of the application you want to flash to the Thingy:91 X, to will enable MCUboot In the application.Step 2.4. Open a new terminal in the build folder
Click on the terminal icon from the ACTION section.
Step 2.5 Grab the nRF:91x Serial Code
Enter the following command to list the connected devices and their traits:
nrfutil device list
Output should be
THINGY91X_863474028C2
Product Thingy:91 X UART
Ports /dev/tty.usbmodem12102, vcom: 0
/dev/tty.usbmodem12105, vcom: 1
Traits mcuBoot, modem, serialPorts, usb, nordicUsb
Supported devices found: 1
The Nordic Thingy:91 X will be listed as a Thingy:91 X UART product and have the following details: mcuboot, nordicUsb, serialPorts, and usb traits.
A 21-character J-Link serial number, for example, THINGY91X_863474028C2 Step 2.6 Flash the program with command
Enter the following command to program the application binary to the nRF9151 application core:
nrfutil device program --firmware dfu_application.zip --serial-number <J-Link Serial number> --traits mcuboot --x-family nrf91 --core Application
replace the <J-Link Serial number> with your nRF:91x Serial number obtained from "Step 2.4 Grab the nRF:91x Serial Code"For example
nrfutil device program --firmware dfu_application.zip --serial-number THINGY91X_863474028C2 --traits mcuboot --x-family nrf91 --core Application
Output
[00:00:07] ###### 100% [4/4 THINGY91X_863474028C2] Programmed
🎉 The First Blinkif everything goes well, we can see the nRF:91x onboard LED is blinking
🥳 CONGRATULATIONS
Next - We can explore the code under the src [Source] folder. For the blink project, we can look into the main.c file.
The code
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/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);
int main(void)
{
int ret;
bool led_state = true;
if (!gpio_is_ready_dt(&led)) {
return 0;
}
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
return 0;
}
while (1) {
ret = gpio_pin_toggle_dt(&led);
if (ret < 0) {
return 0;
}
led_state = !led_state;
printf("LED state: %s\n", led_state ? "ON" : "OFF");
k_msleep(SLEEP_TIME_MS);
}
return 0;
}
Step 3.1 Code Explanation
- #include <zephyr/kernel.h> - Imports Zephyr's core kernel functions, including k_msleep() for delays.
- #include <zephyr/drivers/gpio.h> - Imports GPIO API definitions so you can configure and control GPIO pins.
- ss#define SLEEP_TIME_MS 1000 - Defines a constant for how long the program should wait between toggling the LED (1000ms = 1 second).
- #define LED0_NODE DT_ALIAS(led0) - Refers to the device tree alias led0, which points to a physical LED pin. This is how Zephyr knows which pin is connected to the LED.
Right click on the led0 and choose the "Go to Definition" to see the device tree details.
here you can find the onboard LED configurations.
- static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); - Extracts the GPIO configuration (pin number, port, flags) for led0 from the device tree and stores it in led.
- if (!gpio_is_ready_dt(&led)) { return 0; } - Checks if the GPIO controller is ready before using it. Exits if it's not.
- ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); - Configures the pin as an output and initially sets it high (LED on). GPIO_OUTPUT_ACTIVE sets the pin state according to the device tree configuration.
- while (1) {... } - Infinite loop that keeps toggling the LED.
- gpio_pin_toggle_dt(&led); - Toggles the LED pin (ON → OFF or OFF → ON).
- led_state = !led_state; - Updates the led_state boolean to reflect the new state after toggling.
- printf("LED state: %s\n", led_state ? "ON" : "OFF"); - Prints the LED state to the console for debugging.
- k_msleep(SLEEP_TIME_MS); - Pauses the loop for 1 second before the next toggle.
Don't worry, we will learn more about these on the go 🤗.
🧱Step 4: Make some modifications in the code.Try to do some modifications in the code and build again with build option and flash again with flash command. Step 4.1 Remember to build again after each modification before uploading the code.
You can build the code using the build option under the ACTIONS sections Build.
📋 Task One: Change LED Blink FREQ to make it faster.
📋 Task Two: Change LED Color to GREEN
try to do your own and refer my steps below.
----------------------------------------------------------------------------------------------------------------------
📋 Task One: Change LED Blink FREQ to make it faster.change the SLEEP_TIME_MS 1000 to 100 shorter delay means, faster blinks.
#define SLEEP_TIME_MS 100
after changing the code, build the project and flash.
📋 Task Two: Change LED Color to GREENAs we see in the device tree, we can find the led0 is assigned to red and led1 is assigned to green.
So, let's change it and rebuild and flash to change the LED color.
#define LED0_NODE DT_ALIAS(led1)
Output
Replace your main.c code with the one below and observe the output. Before running it, try to read through the code and predict what the output will be.
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
/* Delay in milliseconds */
#define SLEEP_TIME_MS 100
/* DeviceTree aliases for the LEDs */
#define LED0_NODE DT_ALIAS(led0)
#define LED1_NODE DT_ALIAS(led1)
#define LED2_NODE DT_ALIAS(led2)
/* Collect all LED GPIO specs into an array */
static const struct gpio_dt_spec leds[] = {
GPIO_DT_SPEC_GET(LED0_NODE, gpios),
GPIO_DT_SPEC_GET(LED1_NODE, gpios),
GPIO_DT_SPEC_GET(LED2_NODE, gpios),
};
#define NUM_LEDS (sizeof(leds) / sizeof(leds[0]))
int main(void)
{
int ret;
/* Initialize all LEDs */
for (int i = 0; i < NUM_LEDS; i++) {
if (!gpio_is_ready_dt(&leds[i])) {
printf("LED %d not ready\n", i);
return 0;
}
ret = gpio_pin_configure_dt(&leds[i], GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
printf("Failed to configure LED %d\n", i);
return 0;
}
}
/* Loop through LEDs one by one */
while (1) {
for (int i = 0; i < NUM_LEDS; i++) {
/* Turn on current LED */
gpio_pin_set_dt(&leds[i], 1);
printf("LED %d ON\n", i);
k_msleep(SLEEP_TIME_MS);
/* Turn off current LED */
gpio_pin_set_dt(&leds[i], 0);
printf("LED %d OFF\n", i);
}
}
return 0;
}
🎉 Congratulations! You've successfully explored:✅ The nRF91x DevKit and its key features✅ The nRF Connect SDK ecosystem✅ The basics of Zephyr RTOS✅ How to program nRF91 series devices using the SDK and VS Code
Well done! 💪
Next Step - 3️⃣🚆Build a Environmental Monitoring System with Thingy:91
Comments