Skip to content
joric edited this page Apr 19, 2024 · 53 revisions
  • Caution: nobody really uses QMK for nrf5x (critical bugs, licensing issues, etc.), go ZMK.

This article is about an experimental nrf52 QMK branch. It wasn't updated for a while. Not recommended in favor of ZMK.

  • There's an article about the new Zephyr-based ZMK firmware, it's work in progress but it's a standard de-facto: ZMK
  • There's an article about alternative firmware by jpconstantineau, it's much simpler than QMK (recommended!): Bluemicro
  • Articles about certain QMK-based keyboards are here: http://github.com/joric/qmk/wiki (Jorne BLE gets most updates)
  • Also read about my take on open-sourcing the BLE-Micro-Pro "Default Firmware" bootloader BMPAPI (doesn't work yet)
  • Also check out Circuitpython article, it's a new hip thing that already kind of works (needs more people!)

If you got your nRFMicro without a preinstalled bootloader, read the Bootloader article first.

Repositories

The nRF5x-compatible nrf52 QMK fork is maintaned by sekigon-gonnoc. My copy is always a little bit outdated, so please base all your keyboards off his repository. You will also need my custom_board.h for the nRFMicro pin definitions. Note that nrf52 fork will never be officially merged into QMK, because QMK apparently сonflicts with the Nordic licensing terms.

Issue tracker: https://github.com/sekigon-gonnoc/qmk_firmware/issues

See other repositories in the Alternative firmware section.

Keyboard firmware

This section is about sekigon's QMK fork.

To flash the keyboard firmware using a standard .uf2 bootloader you have to convert standard .hex to the UF2 format, using uf2conv.py:

uf2conv.py firmware.hex -c -f 0xADA52840

Press reset button twice within 500ms interval to boot to the UF2 bootloader.

Then just copy the resulting flash.uf2 file to the UF2 USB drive (split keyboards require either master or slave firmware file accordingly). The module will automatically reboot after firmware update.

You can get example precompiled firmware here:

Building keyboard firmware

You may try sekigon's nrf52 fork, but it's probably easier to get jorne fork, because it's already fixed:

You will need nRF52 SDK (namely nRF5_SDK_15.0.0_a53641a) to build the firmware:

Unpack it into a directory, then use path in your build scripts, e.g. export NRFSDK15_ROOT=/mnt/c/SDK/nRF5_SDK_15.0.0_a53641a.

Upd. If you're using WSL2 (I am using WSL2 now, it builds much faster), copy everyting to the Linux partition to the home directory, e.g. export NRFSDK15_ROOT=~/nRF5_SDK_15.0.0_a53641a

Linux and Windows 10 (WSL 1)

WSL1 is 10+ times faster than WSL2 if you work with NTFS partition. You will need following packages:

sudo apt-get install make gcc unzip wget zip gcc-avr binutils-avr avr-libc dfu-programmer dfu-util gcc-arm-none-eabi binutils-arm-none-eabi libnewlib-arm-none-eabi

Make sure that arm-gcc --version is at least 5.4.0 (I'm using WSL on Windows 10 with Ubuntu 18.04).

You might need to patch the makefile tmk_core/nrf.mk beforehand if you're using WSL.

  • error: 'for' loop initial declarations are only allowed in C99 or C11 mode:
-CC = arm-none-eabi-gcc
+CC = arm-none-eabi-gcc -std=c99
  • get rid of wchar_t size warnings:
#  LDFLAGS += -L. $(NRFLIB)
+ LDFLAGS += -Wl,--no-wchar-size-warning

You can build nrf52 QMK fork as so (paths are WSL-compatible):

make git-submodule
export NRFSDK15_ROOT=/mnt/c/SDK/nRF5_SDK_15.0.0_a53641a
make ergo42_ble/master
make ergo42_ble/slave

You have to flash Master and Slave firmwares separately, there's no unified firmware for both halves.

See example firmware and all the hardware keyboard shortcuts here: https://github.com/joric/qmk/wiki/jorne_ble

Windows and MSYS

This was not tested with nrf52, so do that on your own risk. I've never tried that.

TL;DR install msys2, read (or run) qmk/util/msys2_install.sh for dependencies.

Windows and ARM toolchain

Couldn't make it work but you could start from here:

The easiest way is to install nRF52 boards from Arduino IDE and add toolchain to the path. "Bleeding edge" ARM toolchain also looks promising, but needs rebuilding.

Mac OS X

Equipment
  • MacOS Mojave 10.14.4
  • MacBook Pro (Retina, 13-inch, Late 2012)
  • nRF52-DK (PCA10040)
Toolchain Setup
  1. Install GCC
brew tap PX4/homebrew-px4
brew update
brew install gcc-arm-none-eabi
  1. Download and unzip inside ~/Development/nRF52/
  1. Symlink command line tools
ln -s ~/Development/nRF52/CLT_9.8.1/nrfjprog/nrfjprog /usr/local/bin/nrfjprog
ln -s ~/Development/nRF52/CLT_9.8.1/mergehex/mergehex /usr/local/bin/mergehex
  1. Change the nRF SDK to use my version of arm-gcc changing the file ~/Development/nRF52/SDK_15.3.0/components/toolchain/gcc/Makefile.posix to reflect the location of my homebrew installed version
GNU_INSTALL_ROOT := /usr/local/Cellar/gcc-arm-none-eabi/20180627/bin
GNU_VERSION := 7.3.1
GNU_PREFIX := arm-none-eabi
  1. Install J-Link Software and Documentation pack for MacOSX 6.44e
Testing
cd ~/Development/nRF52/SDK_15.3.0/examples/peripheral/blinky/pca10040/blank/armgcc
make
make flash
Credits

A concise version of Aaron Eiche's Programming an nRF52 on a Mac

(Mac OS guide taken from here.)

Debugging

Every nrf52 keyboard automatically creates two virtual interfaces on a single USB port - USB HID and USB Serial for debugging. Use terminal at 115200 baud to debug. You can debug both Master and Slave using two USB cables and two ports.

All the nrf info messages are also redirected there. It's also bidirectional so it's a whole debug console. I'm using Putty serial at 115200 baud, you can try something like screen /dev/ttyACM0 115200 on Linux.

Example output (send "dfu" to reboot into bootloader, send "help" for help):

<info> app: Erase all bonds!
<info> app: Fast advertising.
<info> app: Slave keyboard is disconnected
<info> app: Scanning slave keyboard...
<info> app: Connected to the slave keyboard

Example debug string:

NRF_LOG_INFO("process_record_user, keycode: %d\n", keycode);

Reception issues

Stock QMK nrf52 is pretty laggy. Use these settings from let's split BLE config.h to make it better.

#define BLE_NUS_MIN_INTERVAL 30 // default 60
#define BLE_NUS_MAX_INTERVAL 60 // default 75
#define BLE_HID_MAX_INTERVAL 80 // default 90
#define BLE_HID_SLAVE_LATENCY 10 // default 4

It's all usually the smaller the faster, and bigger values save the battery life (e.g. use smaller BLE_HID_SLAVE_LATENCY if master-slave link lags). More info here https://github.com/sekigon-gonnoc/BLE-Micro-Pro (in Japanese). Also this guy talks about lags on QMK nrf52 (in Japanese): https://log.brdr.jp/post/395. Also follow this reddit thread: https://redd.it/brcxha.

RGB sync

How the RGB sync feature works basically: master is NUS (Nordic UART Service) central, slave is peripheral, the connection is bidirectional so along with receiving keystrokes from slave master also can send RGB sync packet to slave using ble_nus_c_string_send and slave can receive that data with ble_nus_recv_bytes. There's no much traffic, it doesn't affect the battery, it sends RGB config once when you change the mode. Related links (the commit adds reverse communication from master to slave and RGB sync, the eeprom file adds eeprom support because sekigon just used a stub):

Pins

Power Pin

Power mosfet (pin 1.09, since version 1.0) disconnects external GND to improve battery life for power hungry switchboards such as HHKB and SK6812 RGB LEDs (each LED consumes ~1mA of current when OFF). Keyboard matrix still works no matter what, because matrix connects to GND internally via MCU. Note that in 1.3 you have to init this pin because mosfet gate is not pulled down. Usage:

#define POWER_PIN GPIO(1,9)
...
nrf_gpio_cfg_output(POWER_PIN);
...
nrf_gpio_pin_clear(POWER_PIN); // set power on
...
nrf_gpio_pin_set(POWER_PIN); // set power off

Note that all the newer versions use logical 1 to turn the power off (1.1 worked in reverse). MCU retains GPIO state so is fine.

Charger Pins

Versions 1.2+ can control charger PROG pin (it's connected via 10K resistor), it can be used for setting charger modes:

#define PROG_PIN GPIO(0,5)
...
nrf_gpio_cfg_output(PROG_PIN); nrf_gpio_pin_clear(PROG_PIN); // PROG to GND via 10K, ~100 mA (80+ mAh batteries)
...
nrf_gpio_cfg_input(PROG_PIN, NRF_GPIO_PIN_NOPULL); // PROG to float/high-z (non-chargeable batteries)
...
nrf_gpio_cfg_input(PROG_PIN, NRF_GPIO_PIN_PULLDONW); // PROG to GND via 10K+13K, ~43 mA (45+ mAh LIR2032 batteries)

Don't disable the charger if the battery is full, charger IC has a built in control for that. Status pin (STAT) is not available in nRFMicro 1.3 revisions (there's no red LED also) but you can check the battery voltage from the battery pin.

Note that In 1.3 you absolutely have to init and set the PROG_PIN properly.

Switch Pin

Pin 0.26 (revisions up to 1.1) is connected to the battery switch via 10K resistor and detects whether the battery switch (SW1) is ON or OFF in wired mode. The switch disconnects the battery from the BT module, but not from the Li-Po charger. Used as wireless/wired mode flag in Jian BLE while USB is plugged in:

#define SWITCH_PIN GPIO(0,26)
...
nrf_gpio_cfg_input(SWITCH_PIN, NRF_GPIO_PIN_PULLDOWN);
...
if (nrf_gpio_pin_read(SWITCH_PIN)) {
    // battery switch is ON
} else {
    // battery switch is OFF
}

Switch pin is obsolete in 1.2+ version because there's no physical on/off switch on board anymore.

Battery Pins

I was under impression 0.26 could be configured as an analog pin but it only detects two logic levels. After bridging 0.26 to analog pin (currently 0.04/AIN2) it's possible to read the actual voltage. If you use 10K resistor and the internal pull-down of 13kOhms, you get a useable range of 2.37V (at 4.2V) to 1.36 (at 2.4V). Newer revisions use fixed 820K/2M divider. Code in question (adc.c):

void adc_start() {
#ifdef USE_BATTERY_PIN
  nrf_saadc_channel_config_t pincfg = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN2); // pin 0.04
#else
  nrf_saadc_channel_config_t pincfg = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_VDD);
#endif
  nrf_drv_saadc_channel_init(0, &pincfg);
  nrf_drv_saadc_buffer_convert(adc_buffer, 1);
  nrf_drv_saadc_sample();
}

uint16_t get_vcc() {
#ifdef USE_BATTERY_PIN
  return ((uint32_t)adc_buffer[0]*6*600/255) * (BATTERY_R1 + BATTERY_R2) / BATTERY_R2;
#else
  return ((uint32_t)adc_buffer[0]*6*600/255);
#endif
}

Bluetooth pairing

One could experience problems with Bluetooth pairing between the halves or to the computer.

Slave to Master

Hardware shortcut that resets bonds on the slave keyboard could be hard to find. Check out the code, it's usually first 3 keys on the row0 (hold while powering on).

Master to computer

Note that in the old version to reconnect master to the computer you had to restart advertising without whitelist yet again, it did not connect automatically (new define ENABLE_STARTUP_ADV_NOLIST fixes that issue). You also need to reset the bonds beforehand (it's easier on master, usually defined in the keymap).

Alternative firmware

Alternative Bluetooth stacks

Videos

nRFMicro 1.1 vs QMK nrf52 - RGB Sync

BMPAPI

This API is specific to the closed source bootloader for the Japanese board, "BLE-Micro-Pro" by sekigon. If you're not interested in reverse-engineering this board and its bootloader just skip this chapter, use ZMK or QMK instead.

BMPAPI stands for BLE-Micro-Pro API. My open source version doesn't work for now, see ZMK or QMK for the working stuff.

There's a QMK dev/ble_micro_pro branch (based on the abandoned nrf52 branch) that can import JSON keyboard layouts from the USB drive. It needs a binary blob that exposes a set of nRF52-based API (BMPAPI) functions at address 0xFDE00 (see apidef.h), this is an attempt to weasel out of the Nordic/QMK licensing issues (are there any?), so the BMPAPI-based QMK branch could be eventially merged into the QMK upstream. Looks like it's totally one way to do it.

The binary blob is distributed as UF2 and it's called "BLE Micro Pro Bootloader". Unfortunately it was closed source last time I checked and it's impossible to proxy because it only gives access to a fixed set of BLE-Micro-Pro pins, so you'd need to rewrite it from scratch. If you want it opensourced, bother @_gonnok here: https://discord.gg/MqufYtS.

As of May 2020 I have no idea if he is going to opensource this closed source blob. I can't even tell if he has slightest intention to do so or not. Closed source aside, there's a duplication of so much stuff so it ends up with its own matrix, encoder, etc. implementation rather than implementing the current GPIO interface, so it still won't get merged in its current form.

Another possible workaround to redefine pins using the standard BMP bootloader would be using nrf_gpio_pin_read() instead of read_pin() in the QMK (see matrix_basic.c file). You can start from my "Older version" setup below (dev/ble_micro_pro QMK branch). It breaks licensing, but you get useful BMP features, such as JSON config files.

BMPAPI-Related repositories

BMPAPI Intro

I got API working at much higher address (0xA0000 instead of 0xFDE00). Looks like Adafruit bootloader is refusing to rewrite higher address which is of course reserved for bootloader itself. So it's either change the API address to a lower one in the QMK firmware or ditch Adafruit bootloader altogether which is kind of a major nuisance.

It's possible to update bootloader from uf2 with rewriting fuses from the userspace app and proper bootstrapping. The easiest way to make it work would be adding BMPAPI to the Adafruit nRF52 Bootloader and rebuilding the bootloader, but it needs a lot of SWD flashing at least at the initial stage.

The way I'm doing it now is just two userspace apps, a hardware layer (bmpapi.uf2), and a QMK app (ble_micro_pro_default.uf2) that I flash next to each other in arbitrary order (it's also really easy to merge them into a single uf2 file).

BMPAPI Status

The main issue for now is that USB CDC doesn't work as two apps, it only works as a single app. I have no idea why it's happening, but USB only works in a monolithic firmware built with origin 0x26000. Tried to change the upload order but it doesn't help. Probably linker script issues.

The latest version is in the nRF5x folder, it's currently has a built in launcher to test USB code. if you want to run it from the another launcher app you just have to set bmpapi.ld back to default (+0x16000 from start):

-  FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD2000
+  FLASH (rx) : ORIGIN = 0x26000+0x16000, LENGTH = 0xD2000-0x16000

That would need a separate launcher app at 0x26000 (max size 0x16000), either my test launcher from the launcher folder or the BMPAPI QMK version. I haven't even tried calling BLE softdevice functions from the blob, probably there would be the same problems as with USB CDC (init calls missing or something, not sure why it doesn't work, blinker app works just fine).

  • 2020-06-25 Trying to run USB CDC in nrf5x (hangs in app_usbd_power_events_enable, only works as a single app)
  • 2020-06-24 Created launcher, QMK, nrf5x and minimal configurations (exported at 0xA0000), calls API, blinks.

Uploading BMPAPI

  • Burn the Adafruit bootloader with the SWD programmer (skip this if you already have a bootloader)
  • Flash QMK as ble_micro_pro_default.uf2 (or use launcher app for testing)
  • Flash BMPAPI as bmpapi.uf2

To flash the .uf2 file, press reset twice withing 500 ms interval, then drag and drop the file to the appeared USB drive.

It works just fine with QMK firmware at 0x26000 with length 0x16000 (90kB) and BMPAPI exported at 0xA0000 (BMPAPI firmware starts from 0x26000+0x16000 with length 0xD2000-0x16000, so it occupies the rest of the FLASH memory).

TL;DR: just flash those two .uf2 files (ble_micro_pro_default.uf2 and bmpapi.uf2) in any order:

Building BMPAPI

Repository: https://github.com/joric/bmpapi

The nRF5 SDK have to be changed in components/toolchain/gcc/Makefile.posix, the line: GNU_INSTALL_ROOT := /usr/local/gcc-arm-none-eabi-4_9-2015q1 replaced with: GNU_INSTALL_ROOT := /usr/bin/ (same as with mitosis firmware).

git clone https://github.com/joric/bmpapi && cd bmpapi
cd minimal
export NRFSDK15_ROOT=~/nRF5_SDK_15.0.0_a53641a && make
python uf2conv.py .build/nrf52840_xxaa.hex -c -f 0xADA52840 -o bmpapi.uf2

The "minimal" version just blinks for now. The nrf5x version is in progress (it's just a matter of porting the nrf52 branch).

Building QMK for BMPAPI

Repository: https://github.com/sekigon-gonnoc/qmk_firmware

It should be only done once. There are "default" and "no_msc" keymaps, the latter disables "Mass Storage Class" (USB drive). The whole point of BMPAPI is that QMK branch (dev/ble_micro_pro) doesn't have any Nordic dependencies, only generic ARM.

git clone https://github.com/sekigon-gonnoc/qmk_firmware && cd qmk_firmware
git checkout dev/ble_micro_pro

Fix keyboards/ble_micro_pro/ld/nrf52840_ao.ld:

  FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0x16000

Fix tmk_core/protocol/nrf/sdk15/apidef.h:

#define BMPAPI ((bmp_api_t*)0xA0000)

You can also use my branch that's already fixed: https://github.com/joric/qmk/tree/dev/ble_micro_pro

Build the rest of QMK:

make git-submodule
make ble_micro_pro:default
python uf2conv.py .build/ble_micro_pro_default.hex -c -f 0xADA52840 -o ble_micro_pro_default.uf2

You can get uf2conv.py here: https://github.com/microsoft/uf2/blob/master/utils/uf2conv.py

Older version

There's also another experimental repository (obsolete):

This firmware is based on the dev/ble_micro_pro branch and my version of the BLE-Micro-Pro "Default Firmware" binary blob (BMPAPI) ported from nrf52 branch, but instead of using a separate binary blob I'm building everything as a single uf2 file, it's a little bit easier to test and debug on initial stages. What it currently does:

  • adds bmpapi folder with all the Nordic SDK-dependent code
  • modifies nrf.mk to add Nordic SDK files to the compilation
  • modifies apidef.h so BMPAPI is an extern symbol and not 0xFDE00
  • modifies bmp.c to add bmpapi_init() call, filling the BMPAPI structure

It does just a few very basic things (will be eventually deleted and merged into bmpapi repository):

  • Pointers to functions are filled, firmware compiles and runs, doesn't crash
  • ws2812-based RGB strip, built in nRFMicro status LED
  • virtual COM port via USB, microshell and dfu handler

Figures the monolithic version can actually make USB work (can't make it work in split version so far).

Misc

GPIO Access

BMPAPI uses its own set of pins (PIN1 ... PIN21) there's no access to arbirary nRF5x pins. See bmp_api_gpio_t structure:

typedef struct {
    bmp_error_t (*set_mode)(uint8_t pin_num, bmp_api_gpio_mode_t const* const);
    uint32_t (*read_pin)(uint8_t pin_num);
    bmp_error_t (*set_pin)(uint8_t pin_num);
    bmp_error_t (*clear_pin)(uint8_t pin_num);
} bmp_api_gpio_t;

That means stock BMPAPI couldn't run on boards other than BLE Micro Pro (pin mapping is almost always different on different boards, there are hardware limitations). This is the main reason for reimplementing this closed source blob from scratch, patching it inplace seems a much harder task than rewriting it (pin references are pretty much scattered across the address space). Also I haven't managed to run this binary blob on the nRFMicro just yet, it just hangs (probably stock Adafruit bootloader issues, but it won't work as intended anyway).

Exporting BMPAPI

First, add this to the linker script:

SECTIONS
{
    .api_table 0xFDE00:
    {
    KEEP(*(.api_table))
    } > FLASH
}

Then prepare the content in C like this:

void bootloader_jump(void) { }
void reset(uint32_t _) { }
void enter_sleep_mode(void) { }

typedef struct
{
//  int32_t (*init)(bmp_api_config_t const * const);
  void (*reset)(uint32_t);
  void (*enter_sleep_mode)(void);
  void (*main_task_start)(void(*main_task)(void*), uint8_t interval_ms);
/*
  void (*process_task)(void);
  bmp_error_t (*push_keystate_change)(bmp_api_key_event_t const *  const key);
  uint32_t (*pop_keystate_change)(bmp_api_key_event_t* key, uint32_t len, uint8_t burst_threshold);
  uint16_t (*keymap_key_to_keycode)(uint8_t layer, bmp_api_keypos_t const * const key);
  bmp_error_t (*set_keymap)(const uint16_t* keymap, uint16_t len, const char * layout_name);
  bmp_error_t (*set_config)(bmp_api_config_t const * const);
  bmp_error_t (*get_keymap_info)(bmp_api_keymap_info_t* const keymap_info);
  const bmp_api_config_t* (*get_config)(void);
  bmp_error_t (*save_file)(uint8_t file_id);
  bmp_error_t (*delete_file)(uint8_t file_id);
  bmp_error_t (*get_file)(uint8_t file_id, uint8_t **buf, uint32_t * const len);
  uint16_t (*get_vcc_mv)(void);
  bmp_error_t (*set_state_change_cb)(bmp_api_state_change_cb_t);
*/
} bmp_api_app_t;

typedef struct
{
  //////DO NOT CHANGE///////
  uint32_t api_version;
  void (*bootloader_jump)(void);
  /////////////////////////
  const char* (*get_bootloader_info)(void);
  bmp_api_app_t app;
/*
  bmp_api_usb_t usb;
  bmp_api_ble_t ble;
  bmp_api_gpio_t gpio;
  bmp_api_i2c_master_t i2cm;
  bmp_api_i2c_slave_t i2cs;
  bmp_api_spi_master_t spim;
  bmp_api_ws2812_t ws2812;
  bmp_api_logger_t logger;
  bmp_api_web_config_t web_config;
  bmp_api_encoder_t encoder;
  bmp_api_adc_t adc;
*/
} bmp_api_t;

__attribute__((section(".api_table")))
bmp_api_t API_TABLE = {
    .api_version = 0x01020304,
    .bootloader_jump = bootloader_jump,
    .app = { reset, enter_sleep_mode, /* ... */ },
    // ...
};

You can export the bmp_api_t structure at 0xFDE00 with the define in the linker ld file, no issues here. All pointers to functions should be filled somehow, either in compile time or dynamically. Mind that there's no init call in the QMK. it just starts from BMPAPI->api_version and then BMPAPI->bootloader_jump() and then right away it's BMPAPI->logger.init() as the next call. It's either linker can set them properly at the compile time or some kind of the init call (similar to dllmain function) should fill up the structures. I'm not sure if everything could be settled up in compile time or I still would need an init call.

You can examine the original bootloader dump:

od -Ax -tx1 ble_micro_pro_bootloader_0_5_1.uf2 | less

  • bootloader starts at 0xE0000 (00 00 0e 00)
  • api table starts from 00 de 0e 00
  • UICR region starts from 0x10001000

Address 0x10001014 (13 UICR — User information configuration registers) is called NRFFW[0] and it is where MBR looks to know the location of the main application. It this case bootloader is the main application. and qmk is sort of child application launched from bootloader.

Use objdump to make sure if it's properly compiled:

arm-none-eabi-objdump -d -D -C _build/nrf52840_xxaa.out | less

Example BMPAPI builds fine with this .ld but doesn't call anything (apparently I also need to modify the UICR region from the bootloader).

MEMORY
{
  FLASH (rx) : ORIGIN = 0xE0000, LENGTH = 0x1E000
  RAM (rwx) :  ORIGIN = 0x20018000, LENGTH = 0x28000
}

SECTIONS
{
    .api_table 0xFDE00:
    {
    KEEP(*(.api_table))
    } > FLASH
}

For ORIGIN = 0x26000, you should make LENGTH = 0xD2000 or something like that so it could compile without overflow.

Memory placement

Default FLASH memory placement for nRF52840-QIAA (see Nordic infocenter):

Usage Start End Size Size (kB)
Master Boot Record (MBR) 0x0 0x1000 0x1000 4 kB
SoftDevice (S140 v6.1.x) 0x1000 0x26000 0x25000 148 kB
Application area (incl. free space) 0x26000 0xF8000 0xD2000 840 kB
Bootloader 0xF8000 0xFE000 0x6000 24 kB
MBR parameter storage 0xFE000 0xFF000 0x1000 4 kB
Bootloader settings 0xFF000 0x100000 0x1000 4 kB
  • Example Nordic App: 0x26000 ... 0xF8000 (length 0xD2000)
  • Original Adafruit Bootloader: 0xF4000 ... 0xFD800 (length 0x9800)
  • Original dev/ble_micro_pro QMK: 0x26000 ... 0xE0000 (length 0xBA000)
  • Original BMPAPI offset: 0xFDE00

There is a separate launcher app (https://github.com/joric/bmpapi/tree/master/launcher) for testing.

Launcher .ld script:

  FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0x16000

BMPAPI .ld script:

  FLASH (rx) : ORIGIN = 0x26000+0x16000, LENGTH = 0xD2000-0x16000

BMPAPI only works starting from 0xA0000 for some reason (0xFDE00 is out of reach for the userspace app, it's a bootloader area). I tried overlapping addresses first but it crashed in the very middle (there still was a subtle LED flash). This is because functions were overwritten by the launcher so I needed to move all the functions out of the launcher memory space as well. I did it with the setup above and two apps now work together just fine.

Picture for clarity (taken from Nordic)

How to align a specific function

GCC linker description file can force symbol to be at specific address, see:

The best read about this is probably https://github.com/adafruit/Adafruit_nRF52_Bootloader it has a lot of stuff aligned at specific addresses.

C file:

#define LOCATE_FUNC  __attribute__((__section__(".mysection")))
...
void LOCATE_FUNC Delay(uint32_t dlyTicks)
{
    ...
)

Linker .ld file:

MEMORY
{
	FLASH (rx) : ORIGIN = 0x0, LENGTH = 128K
	RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32k
	MY_MEMORY (rx) : ORIGIN = 0x20000, LENGTH = 128k
}
...
SECTIONS
{
 .mysection :
  {
    . = ALIGN(4);
    __mysection_start__ = .;
	*(.mysection*)
    __mysection_end__ = .;
  } > MY_MEMORY
}

Clean and build the project again to use the updated linker script, open the .map file to verify that the Delay() function is indeed located at 0x20000.

QMK as a library

All that could be much easier if instead of trying to be an OS, QMK could be a standalone library that reads layouts, parses keyboard matrix and outputs keycodes. There's a setup that tries to use QMK as a library (QMK hardware platform is still the same), but I don't really know its current state, from the look of it it's kind of stalled and wasn't updated for 2 years.

References