If you’ve ever built something with Wi-Fi—an LED blinking on an ESP32, a sensor sending data to a dashboard—you’ve already tasted the power of connected devices.
But Wi-Fi has limits. It depends on routers, passwords, and range.
Cellular IoT is different.
It’s what lets a tracker work in the mountains.What keeps a parcel locker online in the middle of a city.What allows machines to talk across countries—without asking for Wi-Fi.
This is your first step into that world.
In this tutorial, you won’t just blink an LED. You’ll create a tiny IoT device reaching out into the invisible infrastructure around you—finding a cell tower, negotiating with it, and joining the internet.
And when it succeeds, LED will light up.
Step 1 — Preparing the HardwareBefore anything happens in code, something important happens physically: your device gets its identity.
The setup:
When you unbox the Nordic nRF9151 DK, you’ll find the board itself along with two SIM cards: one from Onomondo and one from another provider.
Plus, you get a nice sticker.
That sticker went straight to the sticker graveyard on my laptop lid.
If you just unboxed your nRF9151 DK, you might have noticed it comes with two different SIM cards: one from Onomondo and one from Wireless Logic (Conexa).
As a beginner, you might think, "A SIM is a SIM, right? I just plug it in and it connects."
Actually, these two SIMs represent two completely different philosophies in cellular IoT.
The "Magic" SIM: Onomondo
Onomondo is a modern, cloud-native cellular network designed specifically to make IoT development as frictionless as possible.
How it works: Onomondo uses a feature called "APN Override." An APN (Access Point Name) is basically the gateway and password your device uses to talk to the cell tower.
The Developer Experience: If your code forgets to send an APN, sends a blank APN, or even sends a typo like my_broken_apn, the Onomondo network catches the mistake, ignores it, and connects you to the internet anyway.
Verdict: It is completely Plug-and-Play. You don't need to configure any passwords in your firmware. It is the absolute best SIM to use for your very first "Hello World" project.
But for beginners and developers who want to avoid the headache, start your journey with an Onomondo SIM to get your first connection effortlessly.
Insert Onomondo SIM into nRF9151Take the SIM card in your hand for a second.
Important: Check Coverage Link in your country. We will use LTE-M.
Unlike Wi-Fi credentials, this tiny chip is your device’s passport. It tells cellular networks: “This device is allowed to exist.”
Insert the Onomondo nano-SIM into the slot on the board.
Connect the board via USB, and flip the power switch.
Now visit Onomondo app page and register card. I guess you can use 'N/A' for the company and URL fields, and just state that you found them through Hackster.
Onomondo Registration Includes:
- 50MB total data (10MB initial + 40MB additional)
- Free 60-day platform trial
- Access to real-time network insights
- Granular control over device network access
Once you access the portal dashboard, your SIM status should appear as Active, indicated by a green icon.
Congratulations!
You have successfully connected your nRF9151 to the Onomondo network and your device is now online.
Click View to see detailed information about your SIM and its data usage. Feel free to explore the rest of the Onomondo portal to see what else is available.
Cellular IoT is not Arduino-style simplicity. It’s closer to real embedded systems used in production.
That’s why we use:
- nRF Connect SDK
- Zephyr RTOS
- Visual Studio Code
Install nRF Connect for Desktop.
nRF Connect for Desktop serves as the centralized cross-platform gateway for Nordic Semiconductor’s specialized development tools. While the actual coding and compilation occur within VS Code, this desktop suite provides a graphical "app-store" interface for hardware-specific utilities that operate independently of the IDE.
By installing this hub, the developer gains access to a modular suite of applications designed for real-time hardware diagnostics and optimization:
- Cellular Monitor: A high-level diagnostic tool used for real-time modem tracing and AT command evaluation, replacing the legacy LTE Link Monitor.
- Programmer: A visual interface for inspecting memory layouts and flashing compiled
.hexbinaries directly to the SoC. - Power Profiler: An essential utility for analyzing current consumption when used with the Power Profiler Kit (PPK2), critical for low-power Zephyr applications.
- Bluetooth Low Energy: A comprehensive tool for scanning, advertising, and testing GATT services on BLE-enabled devices.
The installation of nRF Connect for Desktop completes the environment by bridging the gap between the Firmware (Zephyr/NCS) and the Physical Hardware, providing the visibility needed to debug cellular connectivity and power efficiency that a standard text editor cannot provide.
Open Toolchain Manager and install Quick Start.
During installation, you will be prompted to name your board. In the third step, you will need to choose which firmware to upload; select AT commands.
The programming process will now begin.
The system will then verify your board.
Then, Quick Start will remind you of the default SIM variants included in the box.
Then you can evaluate AT commands by opening Cellular Monitor.Then, you can evaluate AT commands by opening Cellular Monitor.
Explore the learning resources to find out more.
Now you need to install the SDK. Since I am developing using the VS Code IDE, I will choose Option 1.
Open VS Code with extension.
After the extension, SDK, and toolchain have been successfully installed.
You will then have the correct build configuration within VS Code.
Congratulations! You have successfully finished setting up your development environment.
Now that everything is installed, let's open Cellular Monitor.
Debugging and APN connection via Cellular MonitorSince you have the environment ready, this is where you'll start interacting directly with the hardware.
To get a real-time look at how your device is interacting with the towers, open the Cellular Monitor app. This is the most powerful tool for troubleshooting connection issues or verifying roaming status.
Connect to the device detected by the system by clicking its name in the top-left corner.
Click start to trace.
Then, you will be able to see the tracing process in real-time.
For more detailed debugging install Programmer, Cellular Monitor, Power Profiler if needed.
When you use Nordic’s Cellular Monitor, the dashboard can be a bit deceiving. A green checkmark next to "LTE CONNECTION" on the left side does not mean you are connected to the internet!
To prove you actually bypassed the cell tower's "bouncer" and got an IP address, you need to look at the LTE Network panel in the center of the screen.
Here are the 3 fields you must check to confirm a true connection:
EPS Network Registration Status (The Magic Number)
This is the most important number on your screen.
- Status 2: Searching / Attaching. If you are stuck on 2, the tower sees you, but it is actively deciding whether to let you in (or silently dropping you).
- Status 3: Registration Denied. The tower explicitly rejected your SIM card.
- Status 5 (or 1): Registered Roaming (or Home). This is the goal! In the Onomondo screenshot, you can see it says 5. This means the network authenticated the SIM, accepted the APN, and gave the device a seat at the table.
Activity Status
Right below the RRC status, look at the Activity Status text.
- Bad: Not registered, attaching or searching. (You are locked out).
- Good: Registered, roaming. (You are officially online!).
Essentially, the Zephyr RTOS is already installed as part of the nRF Connect SDK (NCS) setup, so there is no need to install it separately—unless you plan to develop applications independently of the Nordic ecosystem.
In my case on macOS, Zephyr was installed at:
/opt/nordic/ncs/v3.2.4/zephyrNow you’re ready to create something real.
Step 5 — Building Your First nRF9151 Application Using Zephyr RTOS and OnomondoOpen VS Code, navigate to the nRF Connect Extension, and click Create a New Application.
Open VS Code and navigate to the nRF Connect extension. Click Create a new application, where you can choose to copy from an existing sample or browse the nRF Connect SDK Add-on index for external repositories.
While I highly recommend exploring the various samples available in the SDK to understand the breadth of the Zephyr RTOS and Nordic SDK, we will focus specifically on the blank application for now.
Detailed walkthroughs of other samples will be covered in a separate tutorial.
Name your application as you wish and press 'Enter' to finalize the project structure.
You will be prompted to scan for kits; click 'Search'
Once complete, you will be provided with a project template containing all the necessary files for development.
Open the prj.conf file in your project, and paste these lines:
# Enable GPIO (for the LED)
CONFIG_GPIO=y
# Enable the Modem and LTE Link Control
CONFIG_NRF_MODEM_LIB=y
CONFIG_LTE_LINK_CONTROL=y
CONFIG_LTE_NETWORK_MODE_LTE_M=yNow for the code. This is the absolute simplest way to connect to a cell tower in Zephyr.
Open src/main.c, delete the default code, and paste this exact program. It is heavily commented so you know exactly what is happening:
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <modem/nrf_modem_lib.h>
#include <modem/lte_lc.h>
#include <stdio.h>
// Grab LED 0 from the Devicetree
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
int main(void)
{
printf("Starting Cellular Blink!\n");
// 1. Setup the LED pin
if (device_is_ready(led.port)) {
gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);
}
// 2. Initialize the Modem
printf("Initializing modem...\n");
nrf_modem_lib_init();
// 3. Connect to the LTE Network (e.g., Orange/Play/Plus in Poland)
printf("Connecting to Onomondo LTE-M network. This may take a minute...\n");
// Note: This is a "blocking" function. The code pauses here until the network attaches!
lte_lc_connect();
// 4. Success! Turn on the LED to prove we are online.
printf("CONNECTED! Turning on LED.\n");
gpio_pin_set_dt(&led, 1);
// Keep the main thread alive
while (1) {
k_sleep(K_SECONDS(1));
}
}Click on the nRF Connect icon in the Activity Bar. In the Applications panel at the bottom, locate your project and click '+ Add build configuration' to define your hardware targets.
In the Build Configuration menu, ensure that the SDK and Toolchain are both set to the nRF Connect versions you installed.
Then, set the Board target specifically to nrf9151dk/nrf9151/ns to match your hardware.
Scroll to the bottom of the Build Configuration panel and click 'Build Configuration'.
The terminal will open automatically, allowing you to monitor the progress as it compiles your project.
When the build successfully finishes—as it should for nRF Connect SDK v3.2.4 —you will receive a notification in the status bar, and your binary image will be ready to flash to the hardware.
You are now ready to flash the board. Navigate to the Actions tab in the nRF Connect sidebar and click 'Flash' to load the compiled image onto your nRF9151.
The flashing process will now begin using West, Zephyr’s meta-tool. You can monitor the progress in the terminal as it erases, programs, and verifies the binary on your nRF9151.
Navigate to the Connected Devices tab in the nRF Connect sidebar. Here, you will see your nRF9151 DK listed, confirming that the toolchain has a solid connection to your hardware.
Click the name of your nRF9151 DK to expand the dropdown list and view the available serial ports and hardware details.
Click the name of your nRF9151 DK to expand the dropdown list. This will reveal the available serial ports VCOM and specific hardware details, allowing you to interface directly with the board.
- VCOM0: Usually the Application Port. This is where your Zephyr
printkmessages (like "Connecting to Onomondo...") will appear. - VCOM1: Usually the Modem/AT Port. If you want to manually ask the modem
AT+CEREG?to see your registration status, this is the port you'd use.
Locate VCOM0 in the dropdown list and click the 'Plug' (Fork) icon next to it. This will open the Serial Terminal at the bottom of your screen, where you can monitor the application logs in real-time.
Once the port is open, you will receive real-time logs in the terminal. This allows you to monitor the device's boot sequence, modem initialization, and the live connection status as it attaches to the Onomondo network.
If the terminal is blank or you missed the initial startup messages, press the physical RESET button on your nRF9151 DK to refresh the output and view the full boot sequence from the beginning.
Once the device successfully connects to the LTE-M Onomondo network, you will see a confirmation message in the terminal, and LED on the board will light up to indicate an active connection.
Okay, so where’s the blink? The LED will blink while searching for a signal and switch to a stable, solid light once successfully connected to the Onomondo network.
Edit main.c code:
Because the original lte_lc_connect() function is blocking (it completely pauses the code until the modem attaches to the network), you cannot easily blink an LED in the main thread while it runs.
To fix this, we need to switch to lte_lc_connect_async(). This function kicks off the connection process in the background and returns immediately, allowing your main loop to continue running. It takes an event handler (a callback function) that will notify us when the network successfully connects.
Here is the updated code:
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <modem/nrf_modem_lib.h>
#include <modem/lte_lc.h>
#include <stdio.h>
// 1. Setup LED and our "Blink" worker
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static struct k_work_delayable blink_work;
// Worker ONLY handles blinking. It runs every 500ms until told to stop.
static void blink_handler(struct k_work *work)
{
gpio_pin_toggle_dt(&led);
k_work_reschedule(&blink_work, K_MSEC(500));
}
// 2. Network Event Handler
static void lte_handler(const struct lte_lc_evt *const evt)
{
if (evt->type == LTE_LC_EVT_NW_REG_STATUS) {
bool connected = (evt->nw_reg_status == LTE_LC_NW_REG_REGISTERED_HOME ||
evt->nw_reg_status == LTE_LC_NW_REG_REGISTERED_ROAMING);
if (connected) {
printf("\nCONNECTED! Turning LED solid.\n");
// Stop the blinking task, and turn the LED ON
k_work_cancel_delayable(&blink_work);
gpio_pin_set_dt(&led, 1);
} else {
printf("\nSearching for network...\n");
// Start (or restart) the blinking task
k_work_reschedule(&blink_work, K_NO_WAIT);
}
}
}
// 3. Main Application
int main(void)
{
printf("Starting Smart Cellular Blink!\n");
// Initialize LED and Worker
if (!device_is_ready(led.port)) return -1;
gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);
k_work_init_delayable(&blink_work, blink_handler);
// Turn on Modem
if (nrf_modem_lib_init() != 0) {
printf("Modem init failed!\n");
return -1;
}
// Connect in the background (triggers lte_handler on changes)
lte_lc_connect_async(lte_handler);
// Put the main thread to sleep forever. The background worker handles the rest!
k_sleep(K_FOREVER);
return 0;
}Rebuild application
and Flash it again
Open the Serial Terminal, and you will see the log output—similar to what is shown in the video—displaying the real-time status of your connection.
The board’s LED will blink while the modem is searching for a signal; once the connection is established, it will turn solid green to indicate a successful attach to the Onomondo network.
For a more sophisticated implementation, we use a worker function. Here nrf_modem_lib_init() handles the background initialization automatically, allowing you to simply call lte_lc_connect_async() to begin the connection process without blocking your main application.
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <modem/nrf_modem_lib.h>
#include <modem/lte_lc.h>
#include <stdio.h>
// Grab LED 0 from the Devicetree
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
// Declare a delayable work item and a connection state flag
static struct k_work_delayable blink_work;
static volatile bool is_connected = false;
// Worker function: This handles the LED logic without blocking threads
static void blink_work_handler(struct k_work *work)
{
if (is_connected) {
// We are connected: Set LED to stable ON
gpio_pin_set_dt(&led, 1);
// Notice we DO NOT reschedule the worker here.
// This stops the background task and saves power!
} else {
// Not connected: Toggle the LED to blink
gpio_pin_toggle_dt(&led);
// Reschedule this exact worker to run again in 500 milliseconds
k_work_reschedule(&blink_work, K_MSEC(500));
}
}
// Event handler for LTE link control
static void lte_handler(const struct lte_lc_evt *const evt)
{
switch (evt->type) {
case LTE_LC_EVT_NW_REG_STATUS:
if ((evt->nw_reg_status == LTE_LC_NW_REG_REGISTERED_HOME) ||
(evt->nw_reg_status == LTE_LC_NW_REG_REGISTERED_ROAMING)) {
is_connected = true;
printf("\nCONNECTED! Setting LED solid.\n");
// Trigger the worker immediately to apply the "solid ON" state
k_work_reschedule(&blink_work, K_NO_WAIT);
} else {
// If the state changes to not registered (searching/disconnected)
if (is_connected) {
printf("\nDisconnected. Searching again...\n");
}
is_connected = false;
// Restart the blinking worker
k_work_reschedule(&blink_work, K_MSEC(500));
}
break;
default:
break;
}
}
int main(void)
{
int err;
printf("Starting Cellular Blink with Zephyr Worker!\n");
// 1. Setup the LED pin
if (!device_is_ready(led.port)) {
printf("LED device not ready\n");
return -1;
}
gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);
// 2. Initialize the Delayable Worker
k_work_init_delayable(&blink_work, blink_work_handler);
// Start the worker immediately to begin the "searching" blink
k_work_reschedule(&blink_work, K_NO_WAIT);
// 3. Initialize the Modem
printf("Initializing modem...\n");
err = nrf_modem_lib_init();
if (err) {
printf("Failed to initialize modem library, error: %d\n", err);
return err;
}
// 4. Connect asynchronously
printf("Connecting to Onomondo LTE-M network. This may take a minute...\n");
// --> CHANGED HERE: We now just use lte_lc_connect_async() <--
err = lte_lc_connect_async(lte_handler);
if (err) {
printf("Failed to connect to LTE, error: %d\n", err);
return err;
}
// 5. Suspend main thread. The Worker and LTE Callback will handle everything asynchronously.
k_sleep(K_FOREVER);
return 0;
}The Onomondo SIM uses a cloud-native APN Override, meaning it requires zero configuration. You just call lte_lc_connect(). But when you move to a strict, private-network enterprise SIM like Wireless Logic's Conexa, you must manually inject the APN credentials into the modem's memory using standard 3GPP AT commands before you connect. If you don't, the cell tower will reject you with Error 33!
Follow this separate tutorial to try the same setup with a Conexa SIM:
https://www.hackster.io/maxxlife/iot-blink-with-nrf9151-zephyr-and-conexa-sim-defda0


_UoqlmTWtmc.png?auto=compress%2Cformat&w=48&h=48&fit=fill&bg=ffffff)




Comments