Getting Started with Apache Mynewt: How to Build and Flash a BLE Peripheral on nRF52

Developer working with nRF52 development board and Apache Mynewt BLE firmware code

The BLE stack you’re probably already using didn’t originate where you think it did. NimBLE, the open-source Bluetooth Low Energy stack now embedded in Zephyr, ESP-IDF, and a growing list of production frameworks, was born inside Apache Mynewt. Yet most firmware engineers have never actually used it in its native habitat. That matters, because running NimBLE on Apache Mynewt means the RTOS, the stack, and the build system all share a single configuration model. No glue layers, no adapter shims, no chasing down which Kconfig symbol broke which subsystem. One syscfg.yml file controls everything from your mbuf pool size to your GAP role.

This tutorial takes you from zero to a working BLE peripheral on an nRF52 DK, a device you can see, connect to, and read from using a phone. You’ll install Mynewt’s newt CLI, scaffold a project, define a custom GATT service in C, and flash it. The whole process takes about thirty minutes.

If you’re coming from Zephyr, you may already know NimBLE. This tutorial focuses on the native Mynewt experience, where the RTOS and stack are one integrated system. If you’re evaluating Mynewt vs. Zephyr for a BLE project, this gives you a concrete basis for comparison.

Prerequisites: nRF52 DK (PCA10040) or nRF52840 DK (PCA10056), J-Link or OpenOCD, Linux or macOS (WSL2 works with USB passthrough), and professional-level C.

Install the Newt Tool and Scaffold Your Project

Mynewt’s newt tool replaces the CMake/west/ninja chain you might be used to. It handles project creation, dependency resolution, target management, building, and flashing, all from one binary.

  1. Install newt. On macOS, the fastest path:
brew tap apache/mynewt
brew install mynewt-newt

On Linux, grab the latest binary release from the Apache Mynewt downloads page, or build from source with go install mynewt.apache.org/newt/newt@latest. Verify with newt version (you want v1.10+).

  1. Create the project:
newt new ble-periph-demo
cd ble-periph-demo
  1. Pull dependencies:
newt install

This fetches apache-mynewt-core and apache-mynewt-nimble into the repos/ directory, pinned to versions specified in project.yml.

Your directory now looks like this:

ble-periph-demo/
├── apps/                ← your application code goes here
├── project.yml          ← repository dependencies and versions
├── repos/               ← pulled by newt install
│   ├── apache-mynewt-core/
│   └── apache-mynewt-nimble/
└── targets/             ← build target definitions

You’ll create the apps/ble_periph/ subdirectory with your source files shortly.

Understand Targets and the syscfg Override System

This is the single most important concept for Mynewt newcomers. A target in Mynewt is a named tuple: an app package, a BSP, and a build profile. You need two targets, a bootloader and your application.

  1. Create the bootloader target:
newt target create boot
newt target set boot app=@apache-mynewt-core/apps/boot
newt target set boot bsp=@apache-mynewt-core/hw/bsp/nrf52dk
newt target set boot build_profile=optimized
  1. Create the application target:
newt target create ble_periph
newt target set ble_periph app=apps/ble_periph
newt target set ble_periph bsp=@apache-mynewt-core/hw/bsp/nrf52dk
newt target set ble_periph build_profile=debug

nRF52840 DK users: substitute nrf52840dk for nrf52dk in both targets.

Mynewt’s syscfg.yml files form a layered override hierarchy:

  Lowest priority                          Highest priority
  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │   BSP    │──▶│ Package  │──▶│   App    │──▶│  Target  │
  │ defaults │  │ defaults │  │ syscfg   │  │ syscfg   │
  └──────────┘  └──────────┘  └──────────┘  └──────────┘

Your app’s syscfg.yml overrides package defaults; the target overrides everything. Here are the critical BLE values to set in apps/ble_periph/syscfg.yml:

syscfg KeyValueWhy
BLE_ROLE_PERIPHERAL1Enable peripheral role
BLE_ROLE_CENTRAL0Not needed, saves ~2 KB RAM
BLE_MAX_CONNECTIONS1Single connection for this demo
MSYS_1_BLOCK_COUNT12mbuf pool for NimBLE packet buffers
MSYS_1_BLOCK_SIZE292Sized for default 256-byte ATT MTU + headers

Create apps/ble_periph/syscfg.yml now and populate it. You also need a pkg.yml declaring your package name and dependencies:

# apps/ble_periph/pkg.yml
pkg.name: apps/ble_periph
pkg.deps:
    - "@apache-mynewt-core/kernel/os"
    - "@apache-mynewt-nimble/nimble/host"
    - "@apache-mynewt-nimble/nimble/host/services/gap"
    - "@apache-mynewt-nimble/nimble/host/services/gatt"
    - "@apache-mynewt-nimble/nimble/transport"

Define a Simple GATT Service with NimBLE

A quick BLE framing if you’re new to the stack: a GATT service groups related characteristics. Each characteristic has a UUID, permissions (read, write, notify), and a callback function that handles access. Your phone’s scanner discovers services by UUID after connecting.

Create apps/ble_periph/src/gatt_svr.c. Here’s the complete service definition, one service with one read/write characteristic:

#include "host/ble_hs.h"
#include <string.h>

/* Custom 128-bit UUIDs */
static const ble_uuid128_t gatt_svr_svc_uuid =
    BLE_UUID128_INIT(0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,
                     0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10);

static const ble_uuid128_t gatt_svr_chr_uuid =
    BLE_UUID128_INIT(0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,
                     0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x20);

static uint8_t gatt_svr_chr_val[64];
static uint16_t gatt_svr_chr_val_len;

static int gatt_svr_chr_access(uint16_t conn_handle,
    uint16_t attr_handle,
    struct ble_gatt_access_ctxt *ctxt, void *arg);

static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = &gatt_svr_svc_uuid.u,
        .characteristics = (struct ble_gatt_chr_def[]) { {
            .uuid = &gatt_svr_chr_uuid.u,
            .access_cb = gatt_svr_chr_access,
            .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE,
        }, { 0 } },   /* zero-terminated */
    },
    { 0 },            /* zero-terminated */
};

The NimBLE pattern: GATT tables are static arrays of ble_gatt_svc_def structs, each terminated by a zeroed entry. Characteristics nest inside services the same way.

The access callback:

static int gatt_svr_chr_access(uint16_t conn_handle,
    uint16_t attr_handle,
    struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    int rc;
    switch (ctxt->op) {
    case BLE_GATT_ACCESS_OP_READ_CHR:
        rc = os_mbuf_append(ctxt->om, gatt_svr_chr_val,
                            gatt_svr_chr_val_len);
        return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;

    case BLE_GATT_ACCESS_OP_WRITE_CHR:
        rc = ble_hs_mbuf_to_flat(ctxt->om, gatt_svr_chr_val,
                                 sizeof(gatt_svr_chr_val),
                                 &gatt_svr_chr_val_len);
        return rc == 0 ? 0 : BLE_ATT_ERR_UNLIKELY;

    default:
        return BLE_ATT_ERR_UNLIKELY;
    }
}

int gatt_svr_init(void) {
    ble_gatts_count_cfg(gatt_svr_svcs);
    ble_gatts_add_svcs(gatt_svr_svcs);
    return 0;
}

gatt_svr_init() is what you’ll call from main(). It registers the service table with the NimBLE host.

Configure Advertising and the NimBLE Host Callbacks

Here’s where the BLE peripheral architecture comes together:

┌─────────────────────────────────────────────┐
│              Your Application               │
│  main.c  ·  gatt_svr.c  ·  callbacks       │
├─────────────────────────────────────────────┤
│         NimBLE Host  (GAP / GATT / SM)      │
├─────────────────────────────────────────────┤
│         NimBLE Controller  (LL / PHY)       │
├─────────────────────────────────────────────┤
│           Mynewt OS  (scheduler, mbufs,     │
│            HAL, BSP for nRF52)              │
└─────────────────────────────────────────────┘

You need two things: an advertising start function and a GAP event handler. Add these to main.c (or a separate file):

static void start_advertise(void) {
    struct ble_gap_adv_params adv_params = { 0 };
    struct ble_hs_adv_fields fields = { 0 };

    fields.flags = BLE_HS_ADV_F_DISC_GEN |
                   BLE_HS_ADV_F_BREDR_NOT_SUP;
    fields.name = (uint8_t *)"MynewtBLE";
    fields.name_len = strlen("MynewtBLE");
    fields.name_is_complete = 1;
    fields.uuids128 = &gatt_svr_svc_uuid;
    fields.num_uuids128 = 1;
    fields.uuids128_is_complete = 1;

    ble_gap_adv_set_fields(&fields);

    adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
    adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
    ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
                      &adv_params, gap_event_cb, NULL);
}

The GAP event callback restarts advertising on disconnect. Without this, your device disappears after the first connection drops:

static int gap_event_cb(struct ble_gap_event *event, void *arg) {
    switch (event->type) {
    case BLE_GAP_EVENT_CONNECT:
        if (event->connect.status != 0)
            start_advertise();  /* connection failed; retry */
        break;
    case BLE_GAP_EVENT_DISCONNECT:
        start_advertise();
        break;
    }
    return 0;
}

The ble_hs_cfg.sync_cb is how NimBLE tells you the controller is initialized and ready. Set it to a function that calls start_advertise(). Until this callback fires, the radio isn’t available.

Wire Up main() and Let Mynewt Handle the Rest

Mynewt’s package system auto-creates the NimBLE host task during sysinit(). Your main() is minimal:

#include "sysinit/sysinit.h"
#include "os/os.h"
#include "host/ble_hs.h"

extern int gatt_svr_init(void);

static void on_sync(void) {
    start_advertise();
}

int main(int argc, char **argv) {
    sysinit();

    ble_hs_cfg.sync_cb = on_sync;
    ble_hs_cfg.gap_event_cb = gap_event_cb;

    gatt_svr_init();

    while (1) {
        os_eventq_run(os_eventq_dflt_get());
    }
    return 0;
}

No manual thread creation. No stack-size tuning for the BLE task. sysinit() reads the package dependency graph and initializes everything in the right order. The infinite loop processes the default event queue, which is where NimBLE dispatches its host events.

Build, Flash, and Verify Over the Air

  1. Build and flash the bootloader (one-time):
newt build boot
newt load boot
  1. Build and flash your app:
newt build ble_periph
newt load ble_periph

Troubleshooting: If newt load fails with “No J-Link found,” ensure your DK is connected and the J-Link software package is installed. If you get a flash-protection error, run nrfjprog --recover first. WSL2 users: pass the J-Link USB device through using usbipd.

  1. Verify with nRF Connect (iOS/Android):

    • Open the app and scan. You should see “MynewtBLE” in the device list.
    • Tap Connect. The GATT table appears, and your custom service UUID shows up under “Unknown Service.”
    • Tap the characteristic. Read it (empty initially). Write a hex value like 0xDEAD. Read again and you’ll see DEAD returned.
  2. Console output: Run newt run ble_periph to launch GDB with RTT output, or connect a UART terminal at 115200 baud to see NimBLE log messages. You’ll see advertising start, connection events, and GATT access logs.

What to Build on This Foundation

You now have a working BLE peripheral with a custom GATT service. Here’s the natural progression:

Notifications and indications. Add BLE_GATT_CHR_F_NOTIFY to your characteristic flags, store the attribute handle from registration, and call ble_gatts_notify_custom() from a timer callback. This is how you push sensor data to a phone without polling.

Pairing and bonding. Enable the BLE_SM_* syscfg keys (BLE_SM_LEGACY, BLE_SM_SC), set ble_hs_cfg.sm_io_cap to your I/O capability, and implement a passkey display or confirm callback. Mynewt stores bonding data in its config subsystem automatically.

OTA DFU. Mynewt has built-in image management: dual image slots, cryptographic signature verification, and rollback. The newtmgr CLI tool handles firmware upload over BLE via the SMP protocol.

Multiple services. Extend the gatt_svr_svcs array with additional ble_gatt_svc_def entries before the zero terminator. Each service gets its own UUID and characteristic set.

For readers who want NimBLE’s stack without the Mynewt RTOS (for example, on ESP-IDF or a bare-metal project) the NimBLE porting guide documents the host-only integration path. The full source for this tutorial is available as a GitHub gist you can clone and flash directly.


Hubble Network connects BLE devices directly to satellite networks — no gateways, no line-of-sight constraints. See how it works →