Bluetooth Audio Knob (nRF52840)

by nilnull in Workshop > 3D Printing

55 Views, 0 Favorites, 0 Comments

Bluetooth Audio Knob (nRF52840)

knob1.jpg
knob blutooth2.jpg
IMG_20260415_200645.jpg

A wireless rotary encoder knob that connects via Bluetooth to a computer or Android device and controls audio.

  1. Rotate → Volume Up / Down
  2. Press → Mute + Play/Pause

Uses standard BLE HID commands — no additional software required.

⚠️ On Windows, there is occasional instability when the battery is low. The device may need to be removed and re-paired.

Features

  1. 🔊 BLE HID media controller — works as a Bluetooth keyboard (media keys)
  2. Low power design — battery lasts 7–10 days
  3. 🔄 Interrupt-based encoder reading — smooth and responsive
  4. 🖥️ Compatible with Windows & Android — no drivers required
  5. 🖨️ 3D printable enclosure included


Supplies

nrf52840.jpg
103450 3.7V 2000mAh.jpg
encoder.jpg
Slide Switch 125VAC.jpg
knob blutooth3.jpg
  1. EC11 Rotary Encoder ~1.90 EUR
  2. 103450 3.7V 2000mAh Battery ~2.50 EUR
  3. nRF52840 Pro Micro (Tenstar Robot) ~6.81 EUR
  4. DPDT Slide Switch 2P2T 125VAC (Toggle Switch 2 Position 6 Pins, 5mm handle) ~1.70 EUR / 10 pcs

Power Optimization

IMG_20260403_170251.jpg

The project was initially built using an ESP32 Ultra Mini, but battery life was only ~24 hours.

👉 OLD Volume Control Project

After switching to nRF52840:

  1. Significantly lower power consumption
  2. Battery life: ~7–10 days (depending on signal strength)


Electrical Diagram

electrical diagram.jpg

Pin Mapping Issue

The board is an Adafruit clone — pin mapping does not match standard documentation.

Discovered mapping:

TEST_PIN 12 -> 0.08
11 -> 0.06
7 -> 1.02
3 -> 1.15 (D18)
2 -> 0.10 (D16)
1 -> 0.24 (D5)


Source Code

https://github.com/jasenpashov/volume-nRF52

#include <Arduino.h>
#include <bluefruit.h>

// ── Pin definitions (clone board — pins differ from standard Adafruit docs) ──
#define ENCODER_CLK 11 // GPIO 0.06
#define ENCODER_DT 12 // GPIO 0.08
#define ENCODER_SW 1 // GPIO 0.24
#define LED_PIN 15

// Minimum ms between button presses to avoid bouncing
#define SW_DEBOUNCE_MS 350

// ── BLE service objects ──
BLEDis bledis; // Device Information Service (manufacturer, model)
BLEHidAdafruit blehid; // HID service — sends media keys to the host
BLEBas blebas; // Battery Level Service

// ── Encoder state (volatile — modified inside interrupt) ──
volatile int encoderDelta = 0; // Accumulated rotation steps (+/-)
volatile int lastClkState; // Last known CLK pin state
bool isMuted = false; // Tracks mute/unmute toggle state

volatile uint32_t lastSwPress = 0; // Timestamp of last button press (debounce)

// ── Interrupt handler — called on every CLK edge ──
// Compares CLK and DT to determine rotation direction
void readEncoder()
{
int clkState = digitalRead(ENCODER_CLK);
if (clkState != lastClkState)
{
if (digitalRead(ENCODER_DT) != clkState)
encoderDelta++; // Clockwise → volume up
else
encoderDelta--; // Counter-clockwise → volume down

lastClkState = clkState;
}
}

// ── Setup — runs once on power-on / reset ──
void setup()
{
delay(2000); // Give USB/serial time to initialize

// LED off by default
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);

// Configure encoder pins
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_SW, INPUT_PULLUP); // Button: LOW = pressed

// Capture initial CLK state and attach interrupt
lastClkState = digitalRead(ENCODER_CLK);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), readEncoder, CHANGE);

// ── BLE initialization ──
Bluefruit.begin();
Bluefruit.setTxPower(6); // TX power in dBm (range: -40 … +8)
Bluefruit.setName("nRF52-Volume-LowPower");

// Connection interval: 100–200 ms (balances latency vs. power consumption)
Bluefruit.Periph.setConnInterval(80, 160);

// Device info (appears in OS Bluetooth device list)
bledis.setManufacturer("Logitech");
bledis.setModel("nRF52-HID");
bledis.begin();

// Battery service — reports 100% (static, no actual measurement)
blebas.begin();
blebas.write(100);

// HID service — handles all media key presses
blehid.begin();

// ── BLE advertising setup ──
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addAppearance(0x03C1); // HID Keyboard appearance
Bluefruit.Advertising.addService(blehid);
Bluefruit.Advertising.addService(blebas);
Bluefruit.ScanResponse.addName();

Bluefruit.Advertising.restartOnDisconnect(true); // Auto re-advertise on disconnect
Bluefruit.Advertising.setInterval(2048, 2048); // Slow advertising = lower power
Bluefruit.Advertising.start(0); // Advertise indefinitely
}

// ── Main loop — runs repeatedly ──
void loop()
{
// Sleep until event if not connected (saves power)
if (!Bluefruit.connected())
{
sd_app_evt_wait();
return;
}

// ── Button handler — Mute + Play/Pause toggle ──
if (digitalRead(ENCODER_SW) == LOW)
{
uint32_t now = millis();
if ((now - lastSwPress) > SW_DEBOUNCE_MS)
{
lastSwPress = now;

// Both mute and unmute send the same two keys — toggle tracked in isMuted
blehid.consumerKeyPress(HID_USAGE_CONSUMER_MUTE);
delay(10);
blehid.consumerKeyRelease();
delay(10);
blehid.consumerKeyPress(HID_USAGE_CONSUMER_PLAY_PAUSE);
delay(10);
blehid.consumerKeyRelease();

isMuted = !isMuted;
}
}

// ── Encoder handler — Volume Up / Down ──
if (abs(encoderDelta) >= 1)
{
// Safely read and reset delta outside interrupt context
noInterrupts();
int delta = encoderDelta;
encoderDelta = 0;
interrupts();

uint16_t key = (delta > 0)
? HID_USAGE_CONSUMER_VOLUME_INCREMENT
: HID_USAGE_CONSUMER_VOLUME_DECREMENT;

blehid.consumerKeyPress(key);
delay(5);
blehid.consumerKeyRelease();
}

// Sleep until next BLE event (SoftDevice low-power wait)
sd_app_evt_wait();
}

Build & Development

This project uses PlatformIO with the Adafruit nRF52 framework.

[env:nrf52840]
platform = nordicnrf52
board = adafruit_feather_nrf52840
framework = arduino

3D Models

Resources