1. Home
  2. 博客

    nRF Connect SDK 基础课程第四课:控制台消息输出与日志记录

nRF Connect SDK 基础课程第四课:控制台消息输出与日志记录

Nordic Semiconductor

在嵌入式固件开发中,控制台打印与日志输出是调试程序、观测运行状态、定位故障的核心手段。nRF Connect SDK Fundamentals 第四课《控制台消息输出与日志记录》,专注于调试输出能力的专项提升,从极简打印函数 printk() 入手,深入讲解 SDK 官方推荐的高级 Logger 日志模块,搭配阶梯式实操练习,完整覆盖基础打印、格式化输出、十六进制转储与日志系统配置,为后续复杂外设开发筑牢调试基础。

课程定位:打通嵌入式调试的基础输出环节

前序课程已完成 SDK 环境搭建、硬件交互操作、工程构建配置,而本课作为调试能力的专项课程,是连接基础开发与实战调试的关键节点。无论是验证硬件驱动、排查代码逻辑,还是观测程序运行流程,控制台输出都是最直观高效的手段。本课全程围绕 “调试输出” 展开,兼顾轻量化临时打印与产品级标准化日志,既满足快速调试需求,也适配正式产品的日志管理需求,适配嵌入式软件工程师、固件开发人员的日常开发场景。

核心学习目标:吃透两类打印方式与日志配置

完成本课学习后,开发者将全面掌握 nRF Connect SDK 调试输出体系,核心达成以下目标:

  • 熟练使用 printk() 函数实现字符串、格式化字符串的控制台打印,明晰基础用法与适用场景
  • 精准识别 printk() 的功能局限,区分其与 Logger 模块的使用场景差异
  • 掌握 Logger 模块的配置与使用,实现分级日志、格式化字符串的标准化输出
  • 学会通过 Logger 模块完成变量十六进制转储(hexdump),高效解析二进制数据
  • 深度探索 Logger 模块核心特性,掌握日志系统的裁剪与配置方法
  • 通过实操练习,熟练掌握软件模块的使能与配置流程

核心知识模块:两类打印方式全解析

本课核心知识分为两大模块,从轻量化基础打印到产品级高级日志,层层递进覆盖全场景调试需求。

模块一:基础打印函数 printk ()

printk() 是 nRF Connect SDK 内置的极简控制台输出函数,语法贴近标准 C 库 printf(),专为嵌入式轻量场景优化,是入门级调试的首选工具。

1. 支持格式符

该函数支持基础格式符,可满足简单数值与字符串打印需求:

  • 有符号十进制:%d、%i
  • 无符号十进制:%u
  • 十六进制:%x
  • 指针:%p
  • 字符串:%s
  • 字符:%c
  • 百分号:%%

2. 核心特点与局限

  • 同步阻塞输出:调用后必须等待所有字节发送完成才返回,阻塞业务线程
  • 无缓冲机制:无线程互斥保护,多线程场景易出现输出混乱
  • 不支持浮点数:默认不支持 %f 等浮点数格式
  • 资源占用极小:适合早期启动、中断场景的简单输出

3. 使用前置条件

  1. 在 prj.conf 中使能控制台:CONFIG_CONSOLE=y
  2. 包含头文件:#include <zephyr/sys/printk.h>
  3. 选定输出后端(UART/RTT),默认使用 UART

模块二:高级 Logger 日志模块

Logger 模块是 nRF Connect SDK 官方推荐的产品级日志方案,功能丰富、可裁剪性强,适配正式产品的日志管理与调试需求。

1. 核心优势

  • 异步延迟输出:内置低优先级日志处理线程,不阻塞业务线程
  • 四级日志分级:ERR(错误)、WRN(警告)、INF(信息)、DBG(调试),可按需过滤
  • 高级特性支持:时间戳、彩色输出、模块注册、十六进制转储
  • 可裁剪设计:支持编译期 / 运行期裁剪,灵活节省 Flash 与 RAM

2. 核心配置与 API

  • 前置配置:在 prj.conf 中使能:CONFIG_LOG=y
  • 头文件引入:#include <zephyr/logging/log.h>
  • 模块注册:LOG_MODULE_REGISTER(模块名, 默认日志等级)
  • 核心 API
    • 分级打印:LOG_ERR()、LOG_WRN()、LOG_INF()、LOG_DBG()
    • 十六进制转储:LOG_HEXDUMP_ERR()、LOG_HEXDUMP_WRN()、LOG_HEXDUMP_INF()、LOG_HEXDUMP_DBG()

3. 默认启用特性

  • 延迟模式:LOG_MODE_DEFERRED
  • 时间戳格式化
  • 彩色输出(错误红色、警告黄色)
  • UART 后端输出
  • 缓冲区满时覆盖旧日志

实操练习:三步进阶掌握调试输出技能

本课设置三个阶梯式实操练习,从基础 printk() 使用到 Logger 高级配置。

练习 1:使用 printk () 实现按键触发打印

本练习基于第二课的按键中断工程拓展,核心实现按键按下后,计算 1~10 的阶乘并通过 printk() 打印结果。

操作步骤

  • 创建工程文件
    • 新建工程目录 lesson4_ex1
    • 创建核心文件:CMakeLists.txt、prj.conf、src/main.c
  • 配置工程文件
    • CMakeLists.txt(最小配置)
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(lesson4_ex1)
target_sources(app PRIVATE src/main.c)
  • prj.conf(基础配置,使能控制台与按键中断)
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_GPIO=y
CONFIG_SERIAL=y
  1. 编写 main.c 代码
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/gpio.h>
/* 定义按键引脚(以nRF52833 DK为例,按键1对应P0.11) */
#define BUTTON_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(BUTTON_NODE, gpios);
static struct gpio_callback button_cb_data;
/* 按键中断回调函数 */
static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    printk("nRF Connect SDK Fundamentals - Lesson 4 - Exercise 1\n");
    printk("Calculating the factorials of numbers from 1 to 10:\n");
    /* 计算1~10的阶乘并打印 */
    for (int i = 1; i <= 10; i++) {
        long long factorial = 1;
        for (int j = 1; j <= i; j++) {
            factorial *= j;
        }
        printk("The factorial of %2d = %lld\n", i, factorial);
    }
    printk("\n");
}
int main(void)
{
    int ret;
    /* 检查GPIO设备是否就绪 */
    if (!device_is_ready(button.port)) {
        printk("GPIO device not ready!\n");
        return 0;
    }
    /* 配置按键引脚为输入,使能上拉 */
    ret = gpio_pin_configure_dt(&button, GPIO_INPUT | GPIO_PULL_UP);
    if (ret < 0) {
        printk("Failed to configure button pin: %d\n", ret);
        return 0;
    }
    /* 初始化中断回调 */
    gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
    ret = gpio_add_callback(button.port, &button_cb_data);
    if (ret < 0) {
        printk("Failed to add button callback: %d\n", ret);
        return 0;
    }
    /* 使能按键中断(下降沿触发) */
    ret = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);
    if (ret < 0) {
        printk("Failed to configure button interrupt: %d\n", ret);
        return 0;
    }
    printk("Lesson 4 Exercise 1 ready! Press the button to calculate factorials.\n");
    while (1) {
        /* 主循环,保持线程运行 */
        k_msleep(1000);
    }
}
  1. 编译与烧录
    • 使用 nRF Connect for VS Code 扩展包,选择对应开发板(如 nrf52833dk_nrf52833)
    • 点击「Build Project」编译工程
    • 连接开发板,点击「Flash Firmware」烧录
    • 打开串口终端(波特率 115200),按下按键,观测阶乘输出

预期输出

Lesson 4 Exercise 1 ready! Press the button to calculate factorials.
nRF Connect SDK Fundamentals - Lesson 4 - Exercise 1
Calculating the factorials of numbers from 1 to 10:
The factorial of  1 = 1
The factorial of  2 = 2
The factorial of  3 = 6
The factorial of  4 = 24
The factorial of  5 = 120
The factorial of  6 = 720
The factorial of  7 = 5040
The factorial of  8 = 40320
The factorial of  9 = 362880
The factorial of 10 = 3628800

练习 2:Logger 模块实现分级日志与十六进制转储

本练习将练习 1 的 printk() 替换为 Logger 模块实现,完整体验分级日志、时间戳、十六进制转储功能。

操作步骤

  1. 创建工程文件
    • 新建工程目录 lesson4_ex2
    • 创建核心文件:CMakeLists.txt、prj.conf、src/main.c
  2. 配置工程文件
    • CMakeLists.txt(与练习 1 一致)
    • cmake_minimum_required(VERSION 3.20.0)
      
      find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
      
      project(lesson4_ex2)
      
      target_sources(app PRIVATE src/main.c)
      
    • prj.conf(使能 Logger 模块、控制台与 GPIO)
    • CONFIG_CONSOLE=y
      CONFIG_UART_CONSOLE=y
      CONFIG_GPIO=y
      CONFIG_SERIAL=y
      CONFIG_LOG=y  # 使能Logger模块
      CONFIG_LOG_MODE_DEFERRED=y  # 延迟模式
      CONFIG_LOG_LEVEL_INF=y  # 默认日志等级为信息
      
  3. 编写 main.c 代码
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>

/* 注册Logger模块,模块名lesson4_ex2,默认等级INF */
LOG_MODULE_REGISTER(lesson4_ex2, LOG_LEVEL_INF);

/* 定义按键引脚 */
#define BUTTON_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(BUTTON_NODE, gpios);
static struct gpio_callback button_cb_data;

/* 测试用二进制数据,用于十六进制转储 */
static uint8_t test_data[] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0};

/* 按键中断回调函数 */
static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    LOG_INF("Calculating the factorials of numbers from 1 to 10:");

    /* 计算1~10的阶乘并打印 */
    for (int i = 1; i <= 10; i++) {
        long long factorial = 1;
        for (int j = 1; j <= i; j++) {
            factorial *= j;
        }
        LOG_INF("The factorial of %2d = %lld", i, factorial);
    }

    /* 十六进制转储测试数据 */
    LOG_HEXDUMP_INF(test_data, sizeof(test_data), "Test Data Hex Dump");
    LOG_INF("\n");
}

int main(void)
{
    int ret;

    if (!device_is_ready(button.port)) {
        LOG_ERR("GPIO device not ready!");
        return 0;
    }

    /* 配置按键引脚为输入,上拉 */
    ret = gpio_pin_configure_dt(&button, GPIO_INPUT | GPIO_PULL_UP);
    if (ret < 0) {
        LOG_ERR("Failed to configure button pin: %d", ret);
        return 0;
    }

    /* 初始化中断回调 */
    gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
    ret = gpio_add_callback(button.port, &button_cb_data);
    if (ret < 0) {
        LOG_ERR("Failed to add button callback: %d", ret);
        return 0;
    }

    /* 使能按键中断 */
    ret = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);
    if (ret < 0) {
        LOG_ERR("Failed to configure button interrupt: %d", ret);
        return 0;
    }

    LOG_INF("Lesson 4 Exercise 2 ready! Press the button to calculate factorials.");

    while (1) {
        k_msleep(1000);
    }
}
  1. 编译与烧录
    • 编译工程:west build -b <board> .
    • 烧录固件:west flash
    • 打开串口终端,按下按键,观测分级日志输出

预期输出

[00:00:01.200,000] <inf> lesson4_ex2: Lesson 4 Exercise 2 ready! Press the button to calculate factorials.

[00:00:05.300,500] <inf> lesson4_ex2: Calculating the factorials of numbers from 1 to 10:

[00:00:05.300,530] <inf> lesson4_ex2: The factorial of 1 = 1

[00:00:05.300,560] <inf> lesson4_ex2: The factorial of 2 = 2

...

[00:00:05.300,700] <inf> lesson4_ex2: The factorial of 10 = 3628800

[00:00:05.300,730] <inf> lesson4_ex2: Test Data Hex Dump

00 12 34 56 78 9A BC DE F0 |.4Vx....|

练习 3:Logger 模块特性深度探索与配置裁剪

本练习聚焦 Logger 模块的灵活配置能力,通过修改 Kconfig 实现日志特性的调试与裁剪。

操作步骤

  1. 关闭彩色输出
    • 打开 nRF Kconfig GUI(VS Code → nRF Connect → Actions → Open Kconfig)
    • 搜索并关闭 CONFIG_LOG_BACKEND_SHOW_COLOR(取消勾选)
    • 点击「Save to file」,将修改保存至 prj.conf
    • 重新编译并烧录,观测日志不再显示彩色
  2. 切换极简模式
    • 在 Kconfig GUI 中开启 CONFIG_LOG_MODE_MINIMAL
    • 保存修改至 prj.conf
    • 重新编译烧录,查看极简模式输出

极简模式配置(prj.conf)

CONFIG_LOG=y

CONFIG_LOG_MODE_MINIMAL=y

CONFIG_LOG_BACKEND_SHOW_COLOR=n

极简模式预期输出

I: Lesson 4 Exercise 2 ready! Press the button to calculate factorials.

I: Calculating the factorials of numbers from 1 to 10:

I: The factorial of 1 = 1

I: The factorial of 2 = 2

课后测评

本课配套课后测验(Quiz 4),测验内容围绕本课核心知识点展开,包括printk()的用法与局限、Logger模块的使用与配置、十六进制转储功能、软件模块使能方法等,用于检验学习效果,巩固课程核心内容。

本课学习价值

本课严格围绕控制台打印与日志功能展开,掌握printk()与Logger模块的用法,是嵌入式开发调试的基础技能,能够帮助开发者实现程序运行状态观测、故障排查,为后续UART、I2C等外设开发与RTOS进阶学习打下基础。

短距离

适用于短距离物联网的蓝牙低功耗及多协议系统级芯片(支持Thread、Matter、Zigbee协议)

长距离

适用于LTE-M/NB-IoT、GNSS、DECT NR+和NTN的蜂窝物联网系统级封装

Wi-Fi

低功耗Wi-Fi 6协同ICs,支持2.4 GHz/5 GHz频段选项及WPA3加密协议

电源管理ICs

适用于电池供电设备的电源管理IC(PMIC),nPM系列提供充电与稳压功能选项。

AI及软件工具

工具与NPU加速边缘人工智能开发和部署

订阅Nordic新闻简报

了解最新信息!订阅后即可获取最新Nordic及物联网资讯

立即订阅