Hi there,
So, Moving from the Arduino abstraction layer to Zephyr RTOS (via the nRF Connect SDK) on an advanced chip like the nRF54L15 is a complete paradigm shift.
In Arduino, Serial1.begin(9600) instantly claims the hardware pins, sets up a background ring buffer, and handles everything procedurally. In Zephyr, everything must go through the Devicetree (.overlay) to map hardware, and you must manage the asynchronous nature of the RTOS kernel. It is almost certainly due to Devicetree conflicts or improper UART RX buffer handling.
Here is how to set up the project properly in Zephyr, along with a code example and the critical caveats you must watch out for.
1. The Devicetree Overlay (app.overlay)
The nRF54L15 separates its pins using a specific pinctrl schema. If the default uart20 or uart30 is tied to the USB/Console interface, you must declare a separate UART peripheral instance for the L76K GNSS module (9600 baud) without overlapping.
Create an app.overlay file in your project directory:
&pinctrl {
uart30_default_alt: uart30_default_alt {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 6)>; /* Map to XIAO D6 */
};
group2 {
psels = <NRF_PSEL(UART_RX, 0, 7)>; /* Map to XIAO D7 */
bias-pull-up;
};
};
uart30_sleep_alt: uart30_sleep_alt {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 6)>,
<NRF_PSEL(UART_RX, 0, 7)>;
low-power-enable;
};
};
};
&uart30 {
status = "okay";
current-speed = <9600>;
pinctrl-0 = <&uart30_default_alt>;
pinctrl-1 = <&uart30_sleep_alt>;
pinctrl-names = "default", "sleep";
};
2. The Kconfig Configurations (prj.conf)
You must explicitly instruct the Zephyr kernel to initialize the serial drivers and enable an interrupt-driven approach so you don’t drop incoming NMEA string characters.
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_HEAP_MEM_POOL_SIZE=2048
3. Zephyr C Code (main.c)
Because Zephyr handles things concurrently, the easiest approach is to use an interrupt-driven FIFO callback that reads data as it arrives and pushes it to a thread or handles processing safely.
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <string.h>
/* Get the device binding for the GNSS UART */
#define UART_NODE DT_NODELABEL(uart30)
static const struct device *const uart_dev = DEVICE_DT_GET(UART_NODE);
#define RING_BUF_SIZE 128
static char rx_buffer[RING_BUF_SIZE];
static int rx_buf_pos = 0;
/* UART Interrupt Callback function */
static void uart_callback(const struct device *dev, void *user_data)
{
uint8_t c;
if (!uart_irq_update(dev)) {
return;
}
if (!uart_irq_rx_ready(dev)) {
return;
}
/* Read until the UART FIFO is empty */
while (uart_fifo_read(dev, &c, 1) == 1) {
if (c == '\n' || c == '\r') {
if (rx_buf_pos > 0) {
rx_buffer[rx_buf_pos] = '\0';
/* * CRITICAL: rx_buffer contains a complete NMEA sentence here!
* If using a C/C++ port of TinyGPS++, you pass the string here.
*/
printk("NMEA Sentence: %s\n", rx_buffer);
rx_buf_pos = 0; // Reset buffer pointer
}
} else if (rx_buf_pos < (RING_BUF_SIZE - 1)) {
rx_buffer[rx_buf_pos++] = c;
}
}
}
int main(void)
{
if (!device_is_ready(uart_dev)) {
printk("UART device not ready\n");
return 0;
}
/* Set up the interrupt callback */
uart_irq_callback_user_data_set(uart_dev, uart_callback, NULL);
/* Enable RX Interrupts */
uart_irq_rx_enable(uart_dev);
printk("Zephyr L76K GNSS Listener Started...\n");
while (1) {
k_sleep(K_MSEC(1000));
}
return 0;
}
Critical Zephyr Caveats to Watch For on nRF54L
- The 9600 Baud Glitch: The nRF54L15 architecture utilizes high peripheral clock speeds. Some developers on the Nordic and Seeed forums have flagged that at lower baud rates like 9600 bps, the internal clock dividers can cause timing drift or bit corruption if the HFCLK isn’t fully stable. If you get garbage data, make sure your Devicetree hasn’t disabled or misconfigured the external High-Frequency Crystal Oscillator (
&hfxo).
- Console Contention: Ensure that
uart20 (or whichever peripheral is bound to zephyr,console or zephyr,shell-uart in your default board profile) is not mapping to pins P0.06 and P0.07. If two subsystems attempt to claim the same hardware pins, Zephyr will fail to build or panic silently during the boot sequence.
- TinyGPS++ C++ Compatibility: The stock
TinyGPS++ library is heavily reliant on standard Arduino classes (String, millis()). If you want to use it in Zephyr, you can’t just #include it. You either need to write a simple pure-C custom parser for the $GPRMC and $GPGGA text arrays, or enable full C++ support in Zephyr via CONFIG_CPLUSPLUS=y and stub out an equivalent structure to feed gps.encode().
The Uart gets folks ALWAYS… I don’t expect to have those issues on the “M” varriant.

HTH
GL
PJ 