这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【nRF54L15-DK测评】低功耗BLE电脑性能监控副屏——阶段一:从零调通B

共1条 1/1 1 跳转至

【nRF54L15-DK测评】低功耗BLE电脑性能监控副屏——阶段一:从零调通BLE通信

菜鸟
2026-05-17 15:21:58     打赏

系列简介:本系列共三篇,记录我用 Nordic nRF54L15-DK 开发板 + 墨水屏,从零实现一个低功耗 PC 性能监控副屏的完整过程。本篇是第一篇,聚焦 BLE 通信链路的建立:从协议原理讲起,到 Zephyr 侧 GATT Server 实现,再到 PC 端上位机采集与发送,手把手带你调通整条数据链路。


一、为什么做这个项目?

打游戏或者跑深度学习训练的时候,CPU/内存/GPU 占用蹭蹭往上涨,但主屏幕被游戏或 IDE 占满,根本没位置放性能监控悬浮窗。市面上的副屏方案要么用 USB 连接(功耗高、占口),要么 Wi-Fi 方案待机能耗感人。

痛点归纳:

传统方案

问题

USB 小屏

占用 USB 口,常亮功耗高

Wi-Fi 副屏

待机电流 >100mA

软件悬浮窗

遮挡主屏,影响使用

我的方案:

PC ──BLE──► nRF54L15-DK ──SPI──► 墨水屏 (ePaper)
  • BLE:免驱、无线、连接态电流约 5~10mA

  • 墨水屏:刷新后断电也能保留画面,真正实现超低待机

  • nRF54L15:Nordic 最新 Cortex-M33 低功耗 SoC,内置多协议无线


二、硬件平台介绍

2.1 nRF54L15-DK 核心参数

┌───────────────────────────┐
│         nRF54L15              │
│                           │
│  CPU: Cortex-M33 @ 128MHz            │
│  RAM: 256KB                    │
│  Flash: 1MB                      │
│  无线: BLE 5.4 / Zigbee / Thread       │
│  外设: SPI / I2C / UART / PWM / GPIO   │
└───────────────────────────┘

nRF54L15 是 Nordic 2024 年推出的新一代低功耗 SoC,相比 nRF52840 在计算性能和功耗控制上均有显著提升,非常适合这种"平时待机、有数据才刷屏"的场景。

2.2 整体架构图

┌───────────────────────────────────────┐
│               PC (Windows)                 │
│                                       │
│  ┌────────┐    ┌───────────────────┐  │
│  │ psutil 采集 │───►│  Python BLE 上位机 (bleak)   │  │
│  │ CPU/MEM/GPU  │    │  GATT Write Without Response  │  │
│  └────────┘    └───────────┬───────┘  │
└─────────────────────────── │ BLE ───────┘
                             │ (4字节数据包)
                   ┌─────────▼─────────┐
                   │   nRF54L15-DK           │
                   │                   │
                   │  Zephyr RTOS           │
                   │  BLE GATT Server         │
                   │  ble_gatt.c            │
                   └───────────────────┘

三、Zephyr 固件侧实现(GATT Server)

蓝牙数据接收流程图

BLE.jpg


3.1 环境准备(基于nRF Connect SDK)

prj.conf 关键配置:

# BLE 基础配置
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="nRF54L15-Monitor"
CONFIG_BT_DEVICE_NAME_DYNAMIC=n

# GATT
CONFIG_BT_GATT_CLIENT=n

# 日志
CONFIG_LOG=y
CONFIG_BT_LOG_LEVEL_INF=y

# 栈大小(BLE 需要足够的栈)
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_MAIN_STACK_SIZE=4096

3.2 数据结构定义 (ble_gatt.h)

// src/ble_gatt.h
struct pc_stats {
    uint8_t cpu_freq;    /* ×100 MHz,例如 36 = 3.6GHz */
    uint8_t cpu_usage;   /* 0-100 % */
    uint8_t mem_usage;   /* 0-100 % */
    uint8_t gpu_usage;   /* 0-100 % */
    uint32_t last_update; /* 距开机秒数,用于检测数据新鲜度 */
};

int  ble_gatt_init(void);
void ble_get_stats(struct pc_stats *out);
bool ble_is_connected(void);

3.3 GATT Server 核心实现 (ble_gatt.c)

数据通信类型选择 Write Without Response,原因是:

  • 性能监控数据是"fire and forget",不需要 ACK

  • 减少往返时延,降低 BLE 空中包数量

  • 进一步降低功耗(省去 Server 发送 ACK 的时机)

① 定义 UUID

// 自定义 128-bit UUID(随机生成,确保唯一)
static struct bt_uuid_128 service_uuid = BT_UUID_INIT_128(
    BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678,
                       0x1234, 0x56789abcdef0));

static struct bt_uuid_128 char_uuid = BT_UUID_INIT_128(
    BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678,
                       0x1234, 0x56789abcdef1));

小知识:128-bit UUID 是自定义服务的标准做法。BLE SIG 分配的 16-bit UUID(如 0x180D 心率)是官方标准服务专用,自研项目必须用 128-bit。

② 广播数据

// 广播包 (AD): 放 Service UUID,让设备能被发现
static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS,
                  (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    BT_DATA_BYTES(BT_DATA_UUID128_ALL,
        BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678,
                           0x1234, 0x56789abcdef0)),
};

// 扫描响应包 (SD): 放设备名称
static const struct bt_data sd[] = {
    BT_DATA(BT_DATA_NAME_COMPLETE,
            CONFIG_BT_DEVICE_NAME,
            sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};

广播包结构示意:

┌───────────────────────────┐
│ AD (Advertising Data, 31字节上限)        │
│  ├── Flags: General Discoverable       │
│  └── UUID128: 自定义 Service UUID      │
├───────────────────────────┤
│ SD (Scan Response Data, 31字节上限)       │
│  └── Complete Name: "nRF54L15-Monitor"     │
└───────────────────────────┘

③ GATT 特征值 Write 回调

static ssize_t on_write(struct bt_conn *conn,
                        const struct bt_gatt_attr *attr,
                        const void *buf, uint16_t len,
                        uint16_t offset, uint8_t flags)
{
    if (len < 3) {
        // 最少需要 cpu_freq, cpu_usage, mem_usage
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
    }

    const uint8_t *p = (const uint8_t *)buf;

    // 互斥锁保护(BLE 回调可能与主循环并发)
    k_mutex_lock(&stats_mutex, K_FOREVER);
    current_stats.cpu_freq   = p[0];
    current_stats.cpu_usage  = p[1];
    current_stats.mem_usage  = p[2];
    current_stats.gpu_usage  = (len > 3) ? p[3] : 0U;
    current_stats.last_update =
        (uint32_t)(k_uptime_get() / 1000U);
    k_mutex_unlock(&stats_mutex);

    return (ssize_t)len;
}

重点on_write 是在 BLE 协议栈的系统线程上下文中执行的,主循环也会读取 current_stats必须用 mutex 保护,否则会出现数据撕裂(torn read)。

④ GATT 服务注册(Zephyr 静态宏)

BT_GATT_SERVICE_DEFINE(epd_svc,
    // 主服务声明
    BT_GATT_PRIMARY_SERVICE(&service_uuid),
    // 特征值声明:只写、无响应
    BT_GATT_CHARACTERISTIC(&char_uuid.uuid,
        BT_GATT_CHRC_WRITE_WITHOUT_RESP,
        BT_GATT_PERM_WRITE,
        NULL,       // read handler (不需要)
        on_write,   // write handler
        NULL),      // user data
);

Zephyr 的 BT_GATT_SERVICE_DEFINE 宏会在编译期将服务描述符放入专用 linker section,运行时自动注册,无需手动调用注册 API,非常优雅。

⑤ 连接/断开事件处理

static void on_connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        LOG_ERR("Connect failed (%d)", err);
        return;
    }
    current_conn = bt_conn_ref(conn); // 引用计数+1
    is_connected = true;

    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    LOG_INF("Connected: %s", addr);
}

static void on_disconnected(struct bt_conn *conn, uint8_t reason)
{
    LOG_INF("Disconnected (0x%02x)", reason);
    bt_conn_unref(current_conn); // 引用计数-1,防止内存泄漏
    current_conn = NULL;
    is_connected = false;
    start_advertising(); // 断开后重新广播,等待重连
}

// Zephyr 静态注册连接回调
BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected    = on_connected,
    .disconnected = on_disconnected,
};

⑥ 初始化入口

int ble_gatt_init(void)
{
    // 使能 BLE 协议栈(同步等待初始化完成)
    int err = bt_enable(NULL);
    if (err) {
        LOG_ERR("bt_enable failed (%d)", err);
        return err;
    }
    // 开始广播
    return start_advertising();
}

3.4 整体初始化流程

  │
  ├── ble_gatt_init()
  │     ├── bt_enable(NULL)        ← 启动协议栈
  │     └── start_advertising()   ← 开始广播
  │           └── bt_le_adv_start(...)
  │
  └── while(1)
        ├── lv_timer_handler()     ← LVGL 定时刷新
        ├── ble_get_stats(&stats)  ← 读取最新数据(带锁)
        └── k_sleep(K_MSEC(5))    ← 让出 CPU

四、PC 端上位机实现(Python)

PC 端我用 Python 的 bleak 库(跨平台 BLE 客户端)+ psutil 采集系统数据。

4.1 上位机完整代码

# pc_monitor.py
import asyncio
import psutil
import platform
from bleak import BleakClient, BleakScanner

# 与固件端完全一致的 UUID
SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0"
CHAR_UUID    = "12345678-1234-5678-1234-56789abcdef1"
DEVICE_NAME  = "nRF54L15-Monitor"

def get_cpu_freq_encoded() -> int:
    """获取 CPU 频率,编码为 ×100MHz 的 uint8"""
    try:
        freq = psutil.cpu_freq()
        if freq:
            # current 单位 MHz,转换为 ×100MHz
            val = int(freq.current / 100)
            return min(val, 255)
    except Exception:
        pass
    return 0

def get_gpu_usage() -> int:
    """获取 GPU 使用率(0-100),失败返回 0"""
    try:
        import GPUtil
        gpus = GPUtil.getGPUs()
        if gpus:
            return int(gpus[0].load * 100)
    except ImportError:
        pass
    return 0

def build_packet() -> bytes:
    """
    构建 4 字节数据包:
    [cpu_freq×100MHz, cpu_usage%, mem_usage%, gpu_usage%]
    """
    cpu_freq  = get_cpu_freq_encoded()
    cpu_usage = int(psutil.cpu_percent(interval=None))
    mem_usage = int(psutil.virtual_memory().percent)
    gpu_usage = get_gpu_usage()

    # 钳位到 0~255
    packet = bytes([
        min(cpu_freq,  255),
        min(cpu_usage, 100),
        min(mem_usage, 100),
        min(gpu_usage, 100),
    ])
    return packet

async def find_device():
    """扫描并查找目标设备"""
    print(f"[BLE] 扫描设备: {DEVICE_NAME} ...")
    device = await BleakScanner.find_device_by_name(
        DEVICE_NAME, timeout=10.0
    )
    if device is None:
        raise RuntimeError(f"未找到设备: {DEVICE_NAME}")
    print(f"[BLE] 发现设备: {device.address}")
    return device

async def monitor_loop(client: BleakClient):
    """主监控循环,每秒发送一次数据"""
    # 预热 psutil 的 CPU 采样(第一次调用总是返回 0)
    psutil.cpu_percent(interval=None)
    await asyncio.sleep(1)

    print("[Monitor] 开始发送数据...")
    while client.is_connected:
        packet = build_packet()

        # 打印调试信息
        print(f"[Data] freq={packet[0]*100}MHz "
              f"CPU={packet[1]}% "
              f"MEM={packet[2]}% "
              f"GPU={packet[3]}%")

        # Write Without Response,不等待 ACK
        await client.write_gatt_char(
            CHAR_UUID,
            packet,
            response=False
        )
        await asyncio.sleep(1.0)  # 1 秒更新一次

async def main():
    device = await find_device()

    async with BleakClient(device) as client:
        print(f"[BLE] 已连接: {client.address}")
        try:
            await monitor_loop(client)
        except Exception as e:
            print(f"[Error] {e}")
        finally:
            print("[BLE] 断开连接")

if __name__ == "__main__":
    asyncio.run(main())

4.2 数据采集说明

psutil.cpu_percent()   → CPU 总体使用率 (0~100%)
psutil.cpu_freq()      → CPU 当前频率 (MHz)
psutil.virtual_memory().percent → 内存使用率 (0~100%)
GPUtil.getGPUs()[0].load        → GPU 使用率 (0.0~1.0)

五、通信时序与调试

5.1 完整通信时序图

PC端 (bleak)                    nRF54L15 (Zephyr)
    │                       │
    │        ADV_IND 广播包       │
    │◄──────────────────────│
    │                       │
    │   CONNECT_IND (连接请求)        │
    │──────────────────────►│
    │                       │ on_connected() 触发
    │                       │
    │   GATT Service Discovery        │
    │◄─────────────────────►│
    │   (自动发现 UUID)            │
    │                       │
    │   ATT_WRITE_CMD (每 1 秒)       │
    │──────────────────────►│
    │   [36][75][60][45]            │ on_write() 触发
    │                       │ 解包存入 current_stats
    │   ATT_WRITE_CMD             │
    │──────────────────────►│
    │   [36][78][62][48]            │
    │         ...            │
5.2 nRF 侧调试日志(RTT)

使用 nRF Connect for Desktop 的 RTT Viewer 或 VS Code 的串口终端查看:

[00:00:00.123] <inf> ble_gatt: BT stack enabled
[00:00:00.145] <inf> ble_gatt: Advertising started
[00:00:05.312] <inf> ble_gatt: Connected: C0:4E:30:11:22:33 (random)
[00:00:06.401] <inf> main: Tick! CPU=75% MEM=60% GPU=45%
[00:00:07.402] <inf> main: Tick! CPU=78% MEM=62% GPU=48%
[00:00:08.401] <inf> main: Tick! CPU=71% MEM=61% GPU=44%
5.3 用 nRF Connect App 验证

在手机上安装 nRF Connect App,可以不写任何 PC 代码,直接手动向特征值写数据验证固件逻辑:

扫描 → 找到 "nRF54L15-Monitor" → 连接
→ 展开 Custom Service
→ 找到 Characteristic (UUID ...def1)
→ 点击写入按钮
→ 输入: 24 5A 3C 2D  (HEX)
→ 观察 RTT 日志

输入 24 5A 3C 2D 解码为:

  • CPU频率:0x24 = 36 → 3.6 GHz

  • CPU使用率:0x5A = 90 → 90%

  • 内存使用率:0x3C = 60 → 60%

  • GPU使用率:0x2D = 45 → 45%


六、阶段成果展示

第一阶段完成后,效果如下(使用 nRF Connect App 手动测试):

epd.jpg

PC 端上位机运行输出:

[BLE] 扫描设备: nRF54L15-Monitor ...
[BLE] 发现设备: EB:3D:xx:xx:xx:xx
[BLE] 已连接: EB:3D:xx:xx:xx:xx
[Monitor] 开始发送数据...
[Data] freq=3600MHz CPU=23% MEM=58% GPU=12%
[Data] freq=3600MHz CPU=31% MEM=58% GPU=15%
[Data] freq=3600MHz CPU=89% MEM=61% GPU=78%

七、本篇小结与下篇预告

本篇实现了:

  • ✅ Zephyr 下实现 BLE GATT Server,注册自定义服务

  • ✅ 处理连接/断开事件,实现断线自动重广播

  • ✅ Python 上位机采集 CPU/内存/GPU 数据并通过 BLE 发送

下一篇:将在 Zephyr 下移植 Waveshare 2.66 寸 ePaper 驱动,深入解析墨水屏全刷/局刷 LUT 原理,并接入 LVGL 图形库,把数字实实在在地显示到屏幕上。






关键词: nRF54L15-DK     Zephyr     BLE    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]