系列定位:深化高通嵌入式Linux开发 · 面向工业机器人 / 智能相机 / ROS
本篇将主要探讨嵌入式系统日志与故障诊断,工业设备日志采集、过滤、上报与异常定位。

前言:工业设备的日志体系,和云端/消费级完全不是一回事

如果你是从互联网后端转过来的,第一反应可能是"日志嘛,ELK 一套就完了"。但把同一套思路搬到工业现场,几乎必定翻车。
原因在于工业边缘设备有四个绕不开的约束:

约束具体表现对日志体系的硬性要求
无人值守设备装在产线夹层、机械臂基座、AGV 内部,现场没有工程师日志必须能远程取回,且故障现场必须自持——断网时也得留下证据
故障复现成本极高停机按分钟计损失,客户不允许"你再来一次我看看"必须一次抓全,尤其是崩溃瞬间的现场
存储介质是 eMMC/UFS写寿命有限(TLC 约 1~3K P/E cycle),且工业设备要跑 5~10 年日志不能无限落盘,必须分级 + 配额 + 轮转
异构多核高通平台除 APSS 外还有 ADSP/CDSP/SLPI/AOP/安全岛,日志分散在 6+ 个核必须有跨核汇聚能力,这是高通平台区别于普通 ARM Linux 的核心难点

一句话总结目标:在有限存储和不可靠网络下,把"事故现场"完整地保留下来,并且能在事后 5 分钟内定位到根因。

1. 系统总体架构

先看全局。图 1 给出了一套经过工业现场验证的五层日志与故障诊断架构。
在这里插入图片描述

逐层说明

① 日志源层——先把"日志从哪来"数清楚,这一步漏了就是永久盲区。

  • 内核空间:printk 环形缓冲、dmesg、log_buf_len 容量配置
  • 设备驱动:I2C/SPI/CAN-FD/MIPI CSI 的驱动级报错,工业设备 70% 的现场故障在这一层露出第一手证据
  • 系统服务:systemd、udev、网络与时间同步(时间戳不准 = 多源日志无法对齐)
  • ROS 2 节点:rclcpp/rclpy 日志与 /rosout 话题
  • AI 推理应用:QNN/SNPE 侧日志与 NPU/DSP 调用异常

② 采集汇聚层——统一入口,避免每个应用各写各的日志文件。

③ 处理过滤层——这是省钱省寿命的一层,也是最多人跳过的一层。

④ 存储层——内存优先、落盘分级、崩溃转储单独预留。

⑤ 上报与消费层——云端可观测性 + 本地告警/安全联动,两条通路互为备份。断网时本地告警必须仍能动作,这是功能安全的基本要求(呼应第 6 篇的看门狗与安全岛)。

2. 高通平台的日志源全景

图 1 是通用工业架构,但高通平台真正的坑在图 2:APSS 之外的子系统,日志根本不走 Linux 的 printk。

在这里插入图片描述

2.1 各子系统的日志通路对照

子系统典型用途日志/异常产出APSS 侧获取手段
APSS应用处理器(Kryo 大小核)printk、dmesg、coredumpdmesg/sys/fs/pstorecoredumpctl
ADSP音频、传感器融合(Hexagon)adsprpc 调用日志、ADSP minidumpminidump 节点、subsys-restart 内核日志
CDSP计算 DSP,QNN 推理后端(Hexagon)QNN/SNPE 推理日志、CDSP minidumpFastRPC 报错、/dev/adsprpc-smd、QNN 日志级别
SLPI低功耗传感器岛(Hexagon)采样日志、唤醒与中断事件通常只在崩溃时经 minidump 可见
AOP常开处理器(Cortex-M)aop_log、低功耗状态机事件debugfs aop 节点、minidump
Safety Island安全岛,锁步核(IQ 系列)安全域日志、锁步比对/看门狗事件安全域专用通路 + 内核告警

⚠️ 关键认知:ADSP/CDSP/SLPI 都是 Hexagon 核,跑的是 QuRT 实时系统,没有 Linux 的 printk 机制。它们的日志要靠 GLINK/QMI/QRTR 消息通道或 SMEM 共享内存搬过来。这也解释了为什么很多团队"NPU 一崩就什么日志都没有"——你根本没接上那条通路。

2.2 必须认识的几个内核日志特征串

当远端子系统崩溃时,APSS 侧内核会打印类似下面的特征行,这是你判断"是不是 DSP 挂了"的第一线索

[  412.338120] subsys-restart: subsystem_restart_dev(): Restart sequence requested for adsp, restart_level = SYSTEM.
[  412.350774] subsys-restart: subsystem_shutdown(): [adsp]: Shutting down
[  412.512003] subsys-restart: subsystem_powerup(): [adsp]: Powering up
[  412.671290] subsys-restart: subsystem_restart_dev(): [adsp]: Brought out of reset

字段名随内核版本略有差异(subsys-restart / subsys_notif / subsystem_restart),但形态稳定。判读要点:

  • restart_level = SYSTEM → 整机被拉复位,属于严重故障
  • restart_level = RELATED → 仅重启该子系统,Linux 侧通常表现为应用报错
  • 出现 Powering up没有 Brought out of reset → 子系统起不来,多半是固件加载问题

3. 采集落地:从内核到应用

3.1 内核日志:先把环形缓冲调够

工业现场最常见的低级失误,是崩溃日志被后续日志冲掉。默认 log_buf_len 在 32~128 KiB 量级,机器人高频控制场景下几秒钟就刷满。

# 开机参数(推荐写进 bootargs 或 device tree chosen/bootargs)
log_buf_len=4M printk.time=1

# 运行期确认
cat /proc/sys/kernel/printk        # 形如: 4  4  1  7
#  console_loglevel / default_message_loglevel / minimum / default_console

# 降低控制台刷屏(现场设备通常无串口,控制台输出纯属浪费)
echo "4 4 1 7" > /proc/sys/kernel/printk

# 打开内核 printk 限流,防止单条错误刷爆缓冲
sysctl -w kernel.printk_ratelimit=5
sysctl -w kernel.printk_ratelimit_burst=10

跟随观察(调试期用,生产不建议常开):

dmesg -T -w                       # -T 转可读时间戳,-w 持续跟随
dmesg -T -l err,crit,alert,emerg  # 只看错误级别以上

3.2 崩溃转储:pstore / ramoops

这是整篇文章最重要的一节。设备意外复位后,内存里的东西全没了——除非你提前预留了一块"重启不清零"的内存。

Device Tree 配置(高通平台放在 reserved-memory 下):

/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        ramoops: ramoops@b0000000 {
            compatible = "ramoops";
            reg = <0x0 0xb0000000 0x0 0x00100000>;  /* 预留 1 MiB */

            record-size  = <0x00020000>;   /* 128 KiB:内核 oops/panic 记录 */
            console-size = <0x00040000>;   /* 256 KiB:控制台最后输出 */
            pmsg-size    = <0x00020000>;   /* 128 KiB:用户态 pmsg 通道 */
            ftrace-size  = <0x00020000>;   /* 128 KiB:ftrace 现场 */

            max-reason   = <4>;            /* PANIC | OOPS | EMERG | SHUTDOWN */
        };
    };
};

内核配置

CONFIG_PSTORE=y
CONFIG_PSTORE_DEFLATE_COMPRESS=y   # 压缩后同样容量能存更多现场
CONFIG_PSTORE_RAM=y
CONFIG_PSTORE_CONSOLE=y
CONFIG_PSTORE_PMSG=y
CONFIG_PSTORE_FTRACE=y

读取现场

mount -t pstore pstore /sys/fs/pstore     # systemd 通常已自动挂载
ls -l /sys/fs/pstore/
# -r--r--r-- dmesg-ramoops-0      ← panic 时的内核日志(最有价值)
# -r--r--r-- console-ramoops-0    ← 控制台最后输出,含崩溃前的刷屏
# -r--r--r-- pmsg-ramoops-0       ← 用户态主动写入的上下文
# -r--r--r-- ftrace-ramoops-0     ← ftrace 缓冲区快照

两个必须知道的坑

  1. ramoops 只在"热复位"下存活。 PANIC → 内核 reboot(warm reset)路径内存不掉电,可保留;但看门狗硬复位、掉电、长按电源键属于冷复位,内容会丢。这是功能安全场景下必须与硬件看门狗策略一起设计的原因(见第 6 篇)。
  2. 预留地址要和 bootloader 的内存映射对齐。 只改 DTS 不改 XBL/ABL 侧的内存划分,会出现"内核以为这块是自己的、bootloader 以为可以随便用",表现为随机性数据损坏——非常难查。

用户态补充上下文(崩溃前把关键状态写进 pmsg):

/* 机器人控制节点:每次运动指令下发前记录上下文 */
#include <linux/pstore_ram.h>   /* 或直接写 /dev/pmsg0 */
int fd = open("/dev/pmsg0", O_WRONLY);
dprintf(fd, "cmd=%s joint=%d target=%.4f seq=%u\n",
        cmd_name, joint_id, target_rad, seq++);
close(fd);

这条通道极便宜(一次 write 系统调用),却能在内核 panic 时把"最后一个动作是什么"完整带出来。强烈建议在机器人运动控制、相机触发这类关键路径上默认开启。

3.3 用户态统一入口:systemd-journald

# /etc/systemd/journald.conf
[Journal]
Storage=persistent
Compress=yes

# —— 容量配额:保护 UFS 寿命的核心 ——
SystemMaxUse=64M           # 持久化分区总上限
SystemMaxFileSize=8M
RuntimeMaxUse=16M          # /run 下的内存日志(掉电即失)
MaxRetentionSec=7day

# —— 只有 WARN 及以上才落盘,INFO/DEBUG 留在内存 ——
MaxLevelStore=warning
MaxLevelSyslog=warning

# —— 日志风暴保护 ——
RateLimitIntervalSec=30s
RateLimitBurst=2000

ForwardToSyslog=no         # 由 rsyslog 主动拉取,避免双写放大

MaxLevelStore=warning 这一行的价值被严重低估:它让"全量打日志便于调试"和"不能写坏 UFS"这两个矛盾的需求同时成立——调试期照常打 INFO/DEBUG,它们进内存环形缓冲可以正常 journalctl 查看,只是不落盘

常用检索

journalctl -b                       # 本次启动
journalctl -b -1                    # 上一次启动(复位的上一次!)
journalctl -b -1 -p err             # 上次启动的错误及以上
journalctl -k -b -1                 # 上次启动的内核日志
journalctl -u robot-control.service --since "10 min ago"
journalctl -p warning --since today -o json | jq -r '.MESSAGE'
journalctl --disk-usage

应用崩溃转储

coredumpctl list                    # 列出所有应用 core dump
coredumpctl info 12345              # 单个进程详情
coredumpctl debug 12345             # 直接进 gdb 带符号调试
ls /var/lib/systemd/coredump/

3.4 规则路由:rsyslog

journald 负责"收",rsyslog 负责"分流"。下面这套配置把日志按等级和来源拆成不同文件,同时把严重日志直接推向 MQTT 桥:

# /etc/rsyslog.d/10-industrial.conf
module(load="imjournal" StateFile="imjournal.state")
module(load="omfwd")

template(name="jsonLine" type="string"
  string="{\"ts\":\"%timereported:::date-rfc3339%\",\"host\":\"%hostname%\",\
\"svc\":\"%programname%\",\"sev\":\"%syslogseverity%\",\"pid\":\"%procid%\",\
\"msg\":%msg:json%}\n")

注意配置里不能有反斜杠换行以外的东西,上面仅为排版折行。%msg:json% 需要 rsyslog ≥ 8.1901,它会自动做 JSON 转义——手工拼 JSON 是注入漏洞的常见来源。

# ① CRIT 及以上:高优先级,立即上报云端
if ($syslogseverity <= 3) then {
    action(type="omfile" file="/var/log/industrial/critical.log" template="jsonLine")
    action(type="omfwd" target="127.0.0.1" port="5140"
           protocol="tcp" template="jsonLine" action.resumeRetryCount="-1")
}

# ② 总线类故障:单独归档,便于统计"哪条总线最容易出问题"
if ($programname startswith "i2c" or $programname contains "can" or
    $programname contains "spi") then {
    action(type="omfile" file="/var/log/industrial/bus.log" template="jsonLine")
}

# ③ 子系统重启:单独立项,这是高通平台的高频故障
if ($msg contains "subsys-restart" or $msg contains "subsystem_restart") then {
    action(type="omfile" file="/var/log/industrial/subsys.log" template="jsonLine")
    action(type="omfwd" target="127.0.0.1" port="5140"
           protocol="tcp" template="jsonLine")
}

3.5 ROS 2 日志接入

ROS 2 用 spdlog 作为后端,默认写到 ~/.ros/log/——~ 往往在只读或小容量的 rootfs 上,这是第一个要改的:

# 把 ROS 日志重定向到大容量可写分区
export ROS_LOG_DIR=/var/log/ros
export RCUTILS_LOGGING_USE_STDOUT=0      # 关掉 stdout 重复输出
export RCUTILS_LOGGING_BUFFERED_STREAM=1 # 缓冲写入,降低 I/O 次数
export RCUTILS_COLORIZED_OUTPUT=0        # 落盘日志去 ANSI 色码,否则检索困难
export RCUTILS_CONSOLE_OUTPUT_FORMAT='[{severity}] [{time}] [{name}]: {message}'

代码侧:优先用带节流/条件的宏,而不是裸 INFO

#include "rclcpp/rclcpp.hpp"

class ControlNode : public rclcpp::Node {
public:
  ControlNode() : Node("robot_control") {
    // 高频循环里绝不能用裸 RCLCPP_INFO —— 1000 Hz 控制周期会瞬间刷爆磁盘
    RCLCPP_INFO_THROTTLE(get_logger(), *get_clock(), 5000,
                         "control loop alive, cycle=%ld us", cycle_us_);

    // 只在状态变化时打印
    RCLCPP_WARN_SKIPFIRST_THROTTLE(get_logger(), *get_clock(), 1000,
                                   "joint %d following error %.3f rad",
                                   joint_id, err);

    // 致命错误:FATAL 会直接进 /rosout,务必留给真正需要停机的事件
    if (std::abs(err) > kEmergencyThreshold) {
      RCLCPP_FATAL(get_logger(), "joint %d exceeded limit, requesting e-stop", joint_id);
    }
  }
private:
  long cycle_us_{0};
};

_THROTTLE(按时间节流)、_ONCE(只打一次)、_SKIPFIRST(跳过首次)、_STREAM(流式)四组宏是 ROS 2 日志体系的精髓。工业机器人场景下,裸 RCLCPP_INFO 出现在控制循环里应视为代码缺陷。

运行期调整等级(不用重新编译):

ros2 run pkg node --ros-args --log-level debug
ros2 launch bringup robot.launch.py --ros-args --log-level robot_control:=debug
ros2 topic echo /rosout --field msg

诊断话题(对接图 1 的"ROS 2 诊断"消费端):

#include "diagnostic_updater/diagnostic_updater.hpp"

diagnostic_updater::Updater updater(this);
updater.setHardwareID("iq9100-cam0");

updater.add("can_bus", [this](diagnostic_updater::DiagnosticStatusWrapper &s) {
  if (bus_off_count_ > 0) {
    s.summary(diagnostic_msgs::msg::DiagnosticStatus::WARN,
              "CAN bus-off recovered " + std::to_string(bus_off_count_) + " times");
    s.add("bus_off_count", bus_off_count_);
    s.add("last_recovery_ms", last_recovery_ms_);
  } else {
    s.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "CAN bus healthy");
  }
});

4. 分级、过滤与限流:日志不能自己变成故障源

4.1 等级映射

syslog数值ROS 2语义落盘策略
emerg0系统不可用✅ 立即落盘 + 立即上报
alert1FATAL必须立即处理(如急停触发)✅ 立即落盘 + 立即上报
crit2ERROR严重故障(子系统重启、看门狗触发)✅ 立即落盘 + 立即上报
err3ERROR一般错误(单次通信失败)✅ 落盘(聚合上报)
warning4WARN需要关注(重试成功、降级运行)✅ 落盘(聚合上报)
notice5INFO正常但重要(状态切换)⚠️ 内存缓冲
info6INFO常规运行信息❌ 仅内存
debug7DEBUG调试细节❌ 仅内存 + 按需开关

4.2 日志风暴:工业场景的头号杀手

算一笔账:一路 I2C 传感器在 1 kHz 采样下持续 NAK,如果驱动每帧打一条错误:

1000 条/秒 × 200 字节 = 200 KB/秒
              = 17 GB/天

一块 32 GB 的 eMMC,两天就被写穿,而且真正的故障线索被淹没在噪声里。

四道防线,缺一不可

# 防线 1:内核层限流(内核 printk 自带)
sysctl -w kernel.printk_ratelimit=5
sysctl -w kernel.printk_ratelimit_burst=10

# 防线 2:journald 层限流(见 3.3 的 RateLimitIntervalSec/Burst)
# 防线 3:rsyslog 层聚合——用 action.resumeRetryCount 和磁盘队列限流,
#         配合 `$msg` 去重规则(同一 svc+msg 在窗口内只落一条)
# 防线 4:应用层令牌桶(驱动/应用自己实现)

应用层令牌桶的极简实现(放进公共库,全项目复用):

/* log_limiter.c —— 同一 key 的日志每 interval_ms 最多输出 burst 条 */
typedef struct { uint64_t window_start_ms; int count; } rl_slot_t;

static rl_slot_t slots[64];   /* key 做简单哈希 */

bool log_allow(const char *key, int burst, int interval_ms) {
    uint32_t h = fnv1a(key) & 63;
    rl_slot_t *s = &slots[h];
    uint64_t now = now_ms();

    if (now - s->window_start_ms >= (uint64_t)interval_ms) {
        s->window_start_ms = now;
        s->count = 0;
    }
    if (s->count >= burst) return false;
    s->count++;
    return true;
}

配套的"被抑制条数统计"很关键——上游必须知道发生了什么

static unsigned long suppressed;

void log_err_throttled(const char *key, const char *fmt, ...) {
    if (log_allow(key, 5, 1000)) {
        if (suppressed) {
            real_log("... %lu similar messages suppressed in last window", suppressed);
            suppressed = 0;
        }
        real_log(fmt, ...);
    } else {
        suppressed++;
    }
}

5. 存储策略:为 UFS 寿命做设计

5.1 分区规划建议

分区/路径介质容量建议内容掉电存活
/run/log/journalRAM16 MB全量日志(含 INFO/DEBUG)
/var/log/journalUFS64 MBWARN 及以上
/var/log/industrial/UFS128 MB结构化分流日志
reserved-memory ramoopsRAM(不掉电区)1 MB崩溃现场⚠️ 仅热复位
崩溃转储分区UFS 独立分区512 MBminidump / coredump

不要和 rootfs 共用分区。 日志写满导致 rootfs 无法写入,是现场"设备变砖"的经典成因。独立分区 + 配额,让日志写满只影响日志。

5.2 轮转配置

# /etc/logrotate.d/industrial
/var/log/industrial/*.log {
    daily
    rotate 14                # 保留 14 天
    size 16M                 # 或者单文件到 16 MB 就切
    compress
    delaycompress
    missingok                # 文件不存在不报错(避免日志系统自己产生日志)
    notifempty
    copytruncate             # 不重启写入进程
    maxage 30
    create 0640 root adm
}

5.3 写在落盘之前:脱敏

工业现场的日志里经常夹带不该出厂的数据:产线节拍参数、客户工件尺寸、相机图像元数据里的车牌/人脸。落盘和上报前必须过一遍脱敏:

import re
PATTERNS = [
    (re.compile(r'\b\d{17}[\dXx]\b'), '[ID_CARD]'),          # 身份证
    (re.compile(r'\b1[3-9]\d{9}\b'), '[PHONE]'),              # 手机号
    (re.compile(r'(?i)(token|password|secret)=\S+'), r'\1=[REDACTED]'),
    (re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b'), '[IP]'),     # 按合规要求决定
]

6. 上报:从设备到云端

6.1 分级上报策略

工业现场的流量是要花钱的(4G/5G 物联网卡),而且产线网络经常被隔离。上报必须"吝啬":

等级上报时机传输内容QoS
FATAL/CRIT立即(< 1 s)完整日志 + 崩溃转储摘要QoS 1
ERROR/WARN聚合,30 s 一批结构化字段 + 去重计数QoS 1
INFO/DEBUG默认不上报仅在远程诊断会话中按需拉取QoS 0
心跳60 s设备健康度摘要(CPU/内存/温度/总线错误计数)QoS 0
# MQTT 主题设计
factory/{line_id}/{device_id}/log/{severity}
factory/{line_id}/{device_id}/health
factory/{line_id}/{device_id}/cmd        # 下行:请求拉取详细日志

6.2 断网续传

现场网络中断是常态,不是异常。上报代理必须是磁盘队列 + 有界缓存

# 上报代理的队列上限策略(示意)
MAX_QUEUE_BYTES = 256 * 1024 * 1024      # 磁盘队列上限 256 MB
POLICY = "drop_oldest"                    # 超限丢最旧,绝不让日志撑爆存储

def enqueue(record: dict):
    if disk_queue.size() + len(serialize(record)) > MAX_QUEUE_BYTES:
        disk_queue.drop_oldest()          # 并累加 dropped_counter
        metrics.inc("log_dropped")
    disk_queue.push(serialize(record))

# 重连后按严重级别优先补传
def drain():
    for rec in disk_queue.sorted_by(severity_rank, then=timestamp):
        publish_with_retry(rec)

关键点:超限时必须丢最旧的,并在心跳里上报 log_dropped 计数。一个悄悄丢日志的上报代理比没有上报更危险——运维会以为"没告警就是没问题"。

6.3 压缩

日志是文本,压缩收益极大:

# 典型压缩比:纯文本日志 zstd -3 约 8~15x
zstd -3 -T4 critical.log -o critical.log.zst

按 8 倍压缩比算,上面那份 17 GB/天的噪声日志压完还有 2 GB/天——所以压缩不能替代限流,只能锦上添花

7. 故障诊断流程

采集和存储就绪后,真正的工作是"出问题时怎么查"。图 3 是推荐的标准诊断流程——注意它是一个闭环,最后的复盘会沉淀成故障特征库,用于快速回归。

在这里插入图片描述

7.1 时间线对齐:多源日志的前提

跨子系统日志能对齐,前提是时间戳同源。工业设备的常见坑:

# 1. 确认时间同步(无 RTC 的设备开机后时间会跳变,直接把日志搞乱)
timedatectl status
systemctl status systemd-timesyncd

# 2. 确认内核日志用的是同一个时钟源
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
dmesg -T | head -3          # -T 会把 monotonic 转成 wall clock

# 3. 给跨模块事件加统一的关联 ID(trace id),比纯时间戳可靠得多

强烈建议在机器人控制指令、相机触发、CAN 报文这三条关键路径上带一个全局递增的 seq,并在日志里输出。事后按 seq 对齐,比按时间戳对齐稳健一个量级——尤其是存在 DDS 传输抖动和 DSP 异步的场景。

8. 四类典型故障的实战定位

8.1 内核崩溃 / 意外复位

现象:设备莫名重启,uptime 很短,无任何应用侧报错。

# ① 先确认是"内核主动重启"还是"看门狗/掉电硬复位"
journalctl -b -1 -k | tail -50
last -x reboot | head -5

# ② 读崩溃现场
cat /sys/fs/pstore/console-ramoops-0 | tail -100
cat /sys/fs/pstore/dmesg-ramoops-0

# ③ 符号化(需要与设备上内核完全一致的 vmlinux)
./scripts/faddr2line vmlinux drv_my_sensor_probe+0x1a4/0x320
aarch64-linux-gnu-addr2line -e vmlinux -f -i ffffffc0104a3b2c

判读要点

  • console-ramoops-0 里出现 Kernel panic - not syncing: → 拿后面的 PC is at ... 地址去符号化
  • 出现 Unable to handle kernel NULL pointer dereference → 空指针,看 PC is at 与调用栈
  • pstore 文件为空 ≠ 没有崩溃:很可能发生了冷复位(掉电/看门狗硬复位),内存内容已丢失。此时只能依赖串口抓取外置调试器(JTAG/Coresight)。这也是为什么量产设备强烈建议保留一个串口 header——它是最后的手段。
  • 若日志停在子系统重启序列中间 → 优先怀疑 ADSP/CDSP 固件加载失败,去查 subsys.log

8.2 外设总线异常(I2C / SPI / CAN-FD)

I2C —— 最常见,也最容易定位

# 典型错误串
dmesg | grep -iE "i2c|geni"
# [  88.221] i2c-qcom-geni 984000.i2c: Transfer timed out
# [  88.535] i2c i2c-4: sendbytes: NAK bailout.

# 复现与确认
i2cdetect -y 4                       # 扫描总线上有哪些地址
i2cget -y 4 0x50 0x00 w
i2ctransfer -y 4 w2@0x50 0x00 0x00 r4
错误串含义根因方向
NAK bailout从设备没应答器件未上电 / 地址错 / 器件忙
Transfer timed out时钟被拉低不释放从设备死锁(需硬件复位)/ 上拉电阻不足 / 走线过长
Bus arbitration lost多主竞争总线上有第二个 master

SPI(Qualcomm QUPv3 / GENI)

dmesg | grep -iE "spi-geni|qup"
# spi-geni-qcom a80000.spi: DMA transfer failed
# 排查方向:DMA 通道冲突、片选时序、CS 建立/保持时间

CAN-FD

# 状态与错误计数器——CAN 排查的第一站
ip -details -statistics link show can0
# 关注:state、berr-counter、restart-ms、bus-off 计数

# 典型内核日志
dmesg | grep -i can
# [ 231.117] can: can0: bus-off
# [ 231.118] can: can0: bus error: arbitration lost
# 打开自动恢复(bus-off 后 100 ms 自动重启控制器)
ip link set can0 down
ip link set can0 type can bitrate 1000000 dbitrate 5000000 fd on restart-ms 100
ip link set can0 up

# 抓错误帧
candump -e can0

判读bus-off 频繁出现 → 检查终端电阻(CAN 必须 120Ω × 2)、线缆屏蔽、波特率与采样点是否与对端一致。注意 CAN-FD 的仲裁段与数据段波特率是分开的,只对一半是最常见的低级错误。

8.3 NPU / DSP 推理报错

这是高通平台最"玄学"的一类,因为它跨了 APSS → CDSP 两个世界。

# ① 先看子系统有没有重启过
journalctl -k -b | grep -iE "subsys|cdsp|adsp|fastrpc"
# subsys-restart: subsystem_restart_dev(): [cdsp]: Brought out of reset  ← 崩过

# ② FastRPC 层报错
dmesg | grep -i adsprpc
# adsprpc: fastrpc_init_create_process: Error 0x80000404

# ③ 应用侧打开 QNN 日志(QNN SDK)
export QNN_LOG_LEVEL=verbose
export ADSP_LIBRARY_PATH="/usr/lib/rfsa/adsp;/usr/lib/dsp"

# ④ 提高内核侧 FastRPC 日志
echo 'module adsprpc +p' > /sys/kernel/debug/dynamic_debug/control

排查路径

  1. 模型本身在 PC 侧仿真能否跑通(排除模型/量化问题)
  2. CDSP 固件版本与 QNN SDK 版本是否匹配(版本错配是最常见根因
  3. DSP 侧内存是否被占满(连续推理 + 高分辨率输入容易 OOM)
  4. 是否发生了 subsys-restart —— 若是,去取 CDSP minidump
  5. 温度墙:工业现场高温环境下 DSP 降频/复位,用 cat /sys/class/thermal/thermal_zone*/temp 排查

8.4 ROS 2 节点异常与实时性抖动

# 节点为什么退出?
journalctl -u robot-control.service -b --no-pager | tail -80
systemctl show robot-control.service -p Restart -p NRestarts -p ExecMainStatus

# 是崩溃还是被 kill(OOM)?
coredumpctl list
dmesg | grep -iE "oom|killed process"

# DDS 层问题(节点互相看不见 / 通信超时)——单独打开 RMW 日志
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export CYCLONEDDS_URI=file:///etc/cyclonedds.xml   # 内含 <Verbosity> 配置

# 整机诊断
ros2 doctor --report
ros2 topic hz /joint_states
ros2 topic echo /diagnostics

实时性抖动的日志定位思路:抖动本身不会打日志,要靠主动埋点

// 在控制循环里统计周期抖动,超阈值才打印(避免自身引入抖动)
auto now = std::chrono::steady_clock::now();
auto dt_us = std::chrono::duration_cast<std::chrono::microseconds>(now - last_).count();
if (dt_us > kPeriodUs * 3) {
    RCLCPP_WARN_THROTTLE(get_logger(), *get_clock(), 500,
        "control loop jitter: %ld us (target %ld us, overrun #%u)",
        dt_us, kPeriodUs, ++overrun_count_);
}

配合 ftrace 抓调度延迟:

# 抓取 sched 唤醒延迟
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo 1 > events/sched/sched_wakeup/enable
echo 1 > events/sched/sched_switch/enable
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
cat trace | head -100

9. 落地清单(Checklist)

上线前逐项核对采集:

  • log_buf_len ≥ 2 M,printk.time=1 已开启
  • pstore/ramoops 已配置,且实测验证过能读到 panic 日志(人为触发一次 echo c > /proc/sysrq-trigger
  • ramoops 预留地址与 bootloader 内存映射一致
  • 远端子系统(ADSP/CDSP/SLPI)日志通路已打通并有实际日志验证
  • ROS 2 ROS_LOG_DIR 已改到大容量分区
  • RCUTILS_COLORIZED_OUTPUT=0,避免落盘日志带 ANSI 码

过滤与存储

  • journald MaxLevelStore=warning,容量配额已设
  • rsyslog 按等级/来源分流规则已配
  • 日志分区与 rootfs 物理隔离
  • logrotate 策略已验证(手工触发一次轮转)
  • 日志风暴限流已生效(实测:制造 1 kHz 错误,观察磁盘写入速率)
  • 写寿命估算已做过(日增量 × 365 × 5 年 < eMMC TBW)

上报

  • 断网续传已验证(拔网线 30 min 后恢复,确认日志补传完整)
  • 队列溢出策略为 drop_oldestlog_dropped 计数已进心跳
  • 脱敏规则已过一遍真实日志样本
  • 流量估算已做(月度流量 × 单价在预算内)

诊断能力

  • 每台设备保留串口 header(pstore 失效时的最后手段)
  • 内核 vmlinuxSystem.map 已归档,且与出货固件版本一一对应
  • 关键路径已埋入全局 seq / trace id
  • 时间同步已启用并验证(断网后时钟漂移可接受)
  • 一台设备做过完整的"人为制造故障 → 远程定位"演练

10. 小结

这一篇的核心,其实只有三句话:

  1. 先数清楚日志从哪来——高通平台 6 个以上子系统各自产出日志,漏掉任何一个都是永久盲区;
  2. 崩溃现场必须提前预留——pstore/ramoops 是唯一能在 panic 后存活的通道,且要清醒地知道它撑不过冷复位;
  3. 日志系统本身要能扛住故障——限流、配额、分区隔离、有界队列,四者缺一不可。一个把自己写死设备的日志系统,比没有日志更糟。

对比之前的功能安全:安全机制负责让设备不出事,日志与诊断体系负责出事后能讲清楚发生了什么。两者合起来,才构成工业设备可交付、可维护、可追责的完整闭环。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐