找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3442

积分

0

好友

456

主题
发表于 1 小时前 | 查看: 3| 回复: 0

你有没有经历过这种绝望——代码改了一个字,系统活了;加了一句 printf,Bug 消失了;换了台电脑,程序不跑了。

你没疯。这就是嵌入式。

在嵌入式这行混久了,你会发现一个残酷的真相:很多“诡异现象”其实并不诡异,它只是在考验你对底层的理解够不够深。无论你是写裸机 FreeRTOS 的固件工程师,还是在 RK3588、i.MX8 上折腾 Linux 驱动的 BSP 工程师,这些坑,大家都逃不掉。今天,我们来扒一扒这些“黑暗经验”,从 MCU 到嵌入式 Linux,一网打尽。

第一坑:加了 printf / printk,Bug 消失了

这大概是嵌入式里最“玄学”的现象之一。明明 Bug 稳定复现,你一加调试打印,它就消失了;你把打印删掉,它又回来了。

真相是:你改变了时序。

裸机 / RTOS 场景

printf 不是免费的。在裸机或 RTOS 环境里,一次 printf 调用可能会:

  • 占用几百微秒的 CPU 时间
  • 触发 UART DMA 传输,修改中断优先级响应窗口
  • 让某个 tight loop 的执行时间发生变化
// 典型场景:两个任务竞争同一资源
void Task_Sensor(void) {
    while (1) {
        read_sensor(&data);           // 耗时 50us
        // printf("sensor: %d\n", data); // 加这行 = 多 300us
        process(data);                // 对时序敏感
    }
}

void Task_Control(void) {
    while (1) {
        // 如果 sensor 处理太快,control 读到脏数据
        use_data(data);
    }
}

加了 printfTask_Sensor 变慢,Task_Control 读到数据恰好"对了"——但这只是侥幸,不是修复。

嵌入式 Linux 场景

在内核驱动里,printk 有同样的“副作用”,甚至更隐蔽。

// 驱动里的竞态:加了 printk 才稳定
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    // printk(KERN_DEBUG "irq fired\n");  // 加这行 bug 消失
    complete(&dev->done);   // 唤醒等待的进程
    return IRQ_HANDLED;
}

static int my_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
    // 问题:没有正确同步,complete() 可能在 wait_for_completion 之前到达
    wait_for_completion_timeout(&dev->done, HZ);
    copy_to_user(buf, dev->data, len);
    return len;
}

printk 拖慢了中断处理,让 wait_for_completion 有时间先进入等待状态——掩盖了真正的竞态。Linux 里还有一个更隐蔽的版本:你加了 printk,触发了与 CONFIG_PRINTK_SAFE_LOG_BUF_SHIFT 相关的内存屏障副作用,让原本乱序的内存访问恰好“对了”。

正确做法:

  • 裸机用逻辑分析仪或 ITM Trace
  • Linux 驱动用 ftrace / perf 抓函数调用时序,或用 lockdep 检测锁依赖
# 用 ftrace 追踪驱动函数,不影响时序
echo function > /sys/kernel/debug/tracing/current_tracer
echo my_irq_handler > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace

第二坑:开了 -O2 就崩溃

这个坑踩过的人,绝对不在少数。Debug 版本跑得好好的,一换 Release(开优化)就死机。这时候新人往往第一反应是:“是不是编译器 Bug?”

99% 不是。是你的代码有未定义行为(UB,Undefined Behavior)。

编译器在优化时,有权假设你的代码没有 UB,然后做各种激进变换。常见的 UB 陷阱:

// 陷阱1:volatile 缺失,编译器直接优化掉轮询
uint32_t flag = 0;

void IRQ_Handler(void) {
    flag = 1;  // 中断里设置
}

void main_loop(void) {
    while (flag == 0);  // 编译器:flag 不可能变,死循环优化掉了
}

// 修复:加 volatile
volatile uint32_t flag = 0;
// 陷阱2:有符号整数溢出(UB!)
int32_t counter = INT32_MAX;
counter++;  // 未定义行为,编译器可能直接删掉这个判断

if (counter < 0) {
    handle_overflow();  // 永远不会执行?
}

// 修复:改用无符号,或用 __builtin_add_overflow
// 陷阱3:访问未对齐内存(ARM Cortex-M0 会硬 Fault)
uint8_t buf[10];
uint32_t *p = (uint32_t *)&buf[1];  // 非 4 字节对齐!
uint32_t val = *p;                   // 💥 HardFault

嵌入式 Linux 的 -O2 专属坑

Linux 内核驱动默认就是 -O2,而且内核里大量使用 barrier()READ_ONCE()WRITE_ONCE() 这些宏——不是在秀肌肉,是血的教训换来的:

// 错误:直接读共享变量,编译器可能缓存在寄存器里
while (dev->state != STATE_READY)
    cpu_relax();

// 正确:READ_ONCE 强制每次从内存读,等价于 volatile 读
while (READ_ONCE(dev->state) != STATE_READY)
    cpu_relax();

还有一个更隐蔽的坑:内联汇编约束写错

// 错误的内联汇编(-O0 下正常,-O2 下寄存器被复用导致结果错误)
static inline uint32_t read_reg(void __iomem *addr)
{
    uint32_t val;
    asm("ldr %0, [%1]" : "=r"(val) : "r"(addr));  // 缺少 memory clobber
    return val;
}

// 正确写法
asm volatile("ldr %0, [%1]" : "=r"(val) : "r"(addr) : "memory");

遇到 -O2 崩溃的调试步骤

  1. -fsanitize=undefined 跑一遍 Host 版本(Linux 应用层可以直接跑)
  2. 检查所有共享变量,裸机用 volatile,Linux 驱动用 READ_ONCE/WRITE_ONCE
  3. 检查内联汇编是否有 volatile 和正确的 clobber 列表
  4. 检查指针强转是否存在对齐问题

第三坑:偶发死机,重启就好

这是最折磨人的一类 Bug——复现概率 5%,每次死机现场还不一样。

裸机 / RTOS:栈溢出 + 内存破坏

FreeRTOS 里每个任务的栈大小是你自己设的,设小了就是定时炸弹:

// 创建任务时,栈深度单位是"字"(4 字节)
xTaskCreate(vMyTask, "MyTask", 
            128,   // ← 128 * 4 = 512 字节,够吗?
            NULL, 1, NULL);

void vMyTask(void *pvParam) {
    // 如果函数里有大数组、递归、printf...分分钟栈溢出
    char buf[256];        // 光这个就 256 字节了
    sprintf(buf, "...");  // sprintf 内部还要用栈
}

检测方法:开启 FreeRTOS 的栈水位检测:

UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
// 如果这个值接近 0,你离死机只差一次函数调用

嵌入式 Linux:Kernel Oops / 内存踩踏

Linux 上的偶发死机,往往比裸机更难定位,因为内核有 MMU,但问题更复杂。

场景一:驱动里的 use-after-free

// 危险:设备移除时,中断处理还在跑
static int my_remove(struct platform_device *pdev)
{
    struct my_dev *dev = platform_get_drvdata(pdev);

    free_irq(dev->irq, dev);  // 注销中断
    kfree(dev);               // 释放内存
    // 问题:free_irq 返回时,正在运行的中断处理可能未结束
    // IRQ handler 里访问 dev 指针 → use-after-free → Oops
}

// 正确:用同步机制确保 handler 退出
static int my_remove(struct platform_device *pdev)
{
    struct my_dev *dev = platform_get_drvdata(pdev);
    disable_irq(dev->irq);    // 等待正在执行的 handler 完成
    free_irq(dev->irq, dev);
    kfree(dev);
    return 0;
}

场景二:DMA 内存未对齐导致的随机踩踏

// 错误:用普通 kmalloc 分配 DMA buffer
char *buf = kmalloc(1024, GFP_KERNEL);
dma_addr_t dma_addr = dma_map_single(dev, buf, 1024, DMA_FROM_DEVICE);
// 问题:kmalloc 不保证 cache line 对齐
// DMA 完成后 invalidate cache,可能破坏相邻内存

// 正确:用 dma_alloc_coherent 或保证对齐
char *buf = dma_alloc_coherent(dev, 1024, &dma_addr, GFP_KERNEL);

Linux 内存问题诊断工具链

# 开启 KASAN(内核地址检测,类似 ASan)
# 在内核配置里开启 CONFIG_KASAN=y,重新编译

# 查看 Oops 调用栈(系统崩溃后在串口 / dmesg 里)
dmesg | grep -A 30 "BUG:"

# 用 addr2line 把崩溃地址转换成源码行号
addr2line -e vmlinux -s ffffffc0087a3f28

# kmemleak:检测内核内存泄漏
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak

第四坑:串口乱码,换个波特率就好了?

串口乱码,大多数人第一反应是波特率配错了。但有一类乱码,换波特率没用,那可能是时钟问题

裸机场景

实际波特率 = APB_CLK / (USART_BRR的分频值)

如果你的 HSE 晶振焊虚了,或者 PLL 配置出错,系统时钟可能跑在一个“差不多”的频率上——CPU 还能跑,但串口波特率已经偏了。

// STM32 典型配置:检查 RCC 状态
if (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET) {
    // HSE 没起来!系统在跑 HSI(内部 8MHz)
    // 但你的代码按外部 8MHz + 9倍 PLL = 72MHz 配的
    // 实际只有 64MHz,串口全乱
}

嵌入式 Linux 串口的专属乱码坑

Linux 上的串口问题,往往不是硬件,是 termios 配置没清干净。

// 应用层打开串口,忘了设 raw 模式
int fd = open("/dev/ttyS1", O_RDWR);

// 错误:直接读写,termios 还是 canonical 模式
// canonical 模式下,'\n' 才触发读返回,且会做字符转换
read(fd, buf, len);  // 可能读到奇怪的东西

// 正确:设置 raw 模式,关闭所有字符处理
struct termios tty;
tcgetattr(fd, &tty);
cfmakeraw(&tty);             // 一键 raw 模式
cfsetspeed(&tty, B115200);
tty.c_cc[VMIN]  = 1;        // 至少读到 1 个字节才返回
tty.c_cc[VTIME] = 0;
tcsetattr(fd, TCSANOW, &tty);

还有一个让人崩溃的场景:RS485 半双工切换没做好

Linux 驱动里有 SER_RS485_ENABLED 标志,如果你没启用,或者 RTS 引脚极性配反了,发送时接收端会收到自己发出去的回显,看起来就是乱码:

// 用 ioctl 配置 RS485 模式
struct serial_rs485 rs485conf = {
    .flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
    .delay_rts_before_send = 0,
    .delay_rts_after_send  = 0,
};
ioctl(fd, TIOCSRS485, &rs485conf);

排查清单

  • 裸机:示波器量 VCC 纹波 → 验证时钟频率
  • Linux:用 stty -F /dev/ttyS1 -a 检查当前 termios 状态
  • RS485:确认 RTS 极性,用示波器抓发送时序

第五坑:实验室没问题,现场必出事

你在实验室测了三天,完美。到了现场——工厂车间、户外机柜、汽车发动机舱——死机、乱跑、数据异常……

两个主要凶手:EMI(电磁干扰)和温度。

EMI 软件防护

// 关键数据加冗余校验
typedef struct {
    uint32_t value;
    uint32_t value_inv;
} SafeU32_t;

void safe_write(SafeU32_t *s, uint32_t v) {
    s->value     = v;
    s->value_inv = ~v;
}

bool safe_read(SafeU32_t *s, uint32_t *out) {
    if ((s->value ^ s->value_inv) == 0xFFFFFFFF) {
        *out = s->value;
        return true;
    }
    return false;
}

嵌入式 Linux 的 EMI 专属坑:GPIO 中断抖动打爆内核

工业现场 EMI 严重时,一个外部 GPIO 中断可能在几毫秒内触发数千次。Linux 上如果处理不当,会把整个系统的中断处理栈打爆:

// 错误:直接在中断里做耗时操作
static irqreturn_t gpio_irq_handler(int irq, void *data)
{
    // EMI 环境下这个函数可能每秒被调用 10000 次
    process_event();   // 耗时操作,系统卡死
    return IRQ_HANDLED;
}

// 正确:中断只唤醒线程,耗时操作放到内核线程里
// 申请中断时用 IRQF_ONESHOT + request_threaded_irq
static irqreturn_t gpio_irq_handler(int irq, void *data)
{
    return IRQ_WAKE_THREAD;  // 立刻返回,唤醒处理线程
}

static irqreturn_t gpio_irq_thread(int irq, void *data)
{
    // 在内核线程上下文里安全处理
    process_event();
    return IRQ_HANDLED;
}

request_threaded_irq(irq, gpio_irq_handler, gpio_irq_thread,
                     IRQF_TRIGGER_RISING | IRQF_ONESHOT, "my_gpio", dev);

软件消抖在 Linux 驱动里也可以做:

// 用 delayed_work 实现软件消抖
static void gpio_debounce_work(struct work_struct *work)
{
    struct my_dev *dev = container_of(work, struct my_dev, debounce.work);
    int val = gpiod_get_value(dev->gpio);
    // 消抖后读取稳定值
    handle_stable_gpio(dev, val);
}

static irqreturn_t gpio_irq_handler(int irq, void *data)
{
    struct my_dev *dev = data;
    // 每次中断触发,重新延迟 20ms 执行
    mod_delayed_work(system_wq, &dev->debounce, msecs_to_jiffies(20));
    return IRQ_HANDLED;
}

温度

-40°C 到 85°C,元器件的参数会漂。

嵌入式 Linux 平台(如 RK3399、i.MX6)上,高温还有个特殊问题:CPU 温控降频

# 查看当前 CPU 频率和温度
cat /sys/class/thermal/thermal_zone0/temp   # 温度,单位 millidegree
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq

# 如果系统性能突然下降,可能是触发了 thermal throttling
# 查看 thermal 策略
cat /sys/class/thermal/thermal_zone0/policy

你的实时控制任务在 25°C 下跑得很顺,到了 70°C 高温环境,CPU 从 1.8GHz 降到 600MHz,任务周期直接超时。

处理方法:在设计阶段就要评估最低频率下的任务执行时间,或者调整 thermal 策略,不要把 throttling 当意外。

第六坑:加个 delay / sleep 就好了

这是新人最常用的“解决方案”,也是最危险的一种。

裸机场景

// 问题:I2C 偶发 NACK
HAL_I2C_Master_Transmit(...);
HAL_Delay(10);  // 加个 delay,好了!——掩盖问题
HAL_I2C_Master_Receive(...);

真正的原因可能是总线没释放、器件内部操作未完成、时序不符合规范。

// 正确做法:检查总线状态,必要时复位
HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(..., 100);
if (status != HAL_OK) {
    if (hi2c->ErrorCode & HAL_I2C_ERROR_BERR) {
        I2C_Bus_Reset(&hi2c1);
    }
}

嵌入式 Linux 的 sleep 陷阱

Linux 驱动里,msleepusleep_range 是高频操作,但用错了一样会埋坑:

// 错误用法1:在中断上下文里调用 msleep(会 BUG!)
static irqreturn_t my_handler(int irq, void *data)
{
    msleep(10);   // 💥 BUG: sleeping function called from invalid context
    return IRQ_HANDLED;
}

// 错误用法2:用 mdelay 做长时间延时(硬等待,CPU 100% 占用,影响实时性)
mdelay(100);  // 100ms 硬等待,这段时间系统啥都干不了

// 正确:区分场景
// 中断上下文 → 只能用 udelay(< 10us)
// 进程上下文、短延时 → usleep_range(min, max)
// 进程上下文、长延时 → msleep 或 schedule_timeout
usleep_range(9500, 10500);  // 比 msleep(10) 更精确,还省 CPU

还有一个经典坑:用 sleep 等待硬件就绪,而不是用 completion 或 wait_event

// 反面教材:轮询等待 DMA 完成
start_dma_transfer();
msleep(50);                  // 假设 50ms 够用?
read_dma_result();           // 万一 DMA 没完成呢?

// 正确:用 completion 等待中断通知
init_completion(&dev->dma_done);
start_dma_transfer();
// DMA 完成中断里:complete(&dev->dma_done);
if (!wait_for_completion_timeout(&dev->dma_done, msecs_to_jiffies(200))) {
    dev_err(dev->dev, "DMA timeout!\n");
    return -ETIMEDOUT;
}

第七坑:看门狗让你误以为系统在“自愈”

“我们加了看门狗,系统稳定多了,偶尔重启一下无所谓。”

听到这句话,老手心里一凉。

裸机看门狗

// 危险的看门狗使用模式——掩盖真实崩溃
void main_loop(void) {
    while (1) {
        do_task_A();
        do_task_B();
        IWDG_ReloadCounter();  // 如果 task_A 死循环,喂不到狗,重启,继续死循环
    }
}

// 正确:记录重启原因
void System_Init(void) {
    if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST)) {
        log_error("WATCHDOG RESET at tick=%u", last_tick);
        __HAL_RCC_CLEAR_RESET_FLAGS();
    }
}

嵌入式 Linux 的 watchdog 黑坑

Linux 有内核 watchdog 驱动框架(/dev/watchdog),但有几个坑:

坑1:守护进程死了,但喂狗的线程还活着

// 错误:一个独立线程无脑喂狗
void *watchdog_thread(void *arg) {
    int fd = open("/dev/watchdog", O_WRONLY);
    while (1) {
        write(fd, "1", 1);   // 喂狗
        sleep(5);
    }
}
// 问题:核心业务进程 OOM 被杀了,watchdog 线程还活得好好的
// 系统看起来正常,实际已经"死"了
// 正确:喂狗和业务心跳绑定
void *watchdog_thread(void *arg) {
    int fd = open("/dev/watchdog", O_WRONLY);
    while (1) {
        if (!all_tasks_healthy()) {
            // 故意不喂狗,让系统重启
            close(fd);  // 关闭 fd 但不写 magic close sequence
            sleep(60);  // 等待 watchdog 超时重启
        }
        write(fd, "1", 1);
        sleep(5);
    }
}

坑2:关闭 /dev/watchdog 时,系统自动重启

Linux watchdog 驱动有“魔数关闭”机制,如果你的进程意外退出(不是写 'V' 再 close),watchdog 不会停止,倒计时到期直接重启:

// 正常退出 watchdog:必须先写 'V'
write(watchdog_fd, "V", 1);
close(watchdog_fd);

// 如果进程被 SIGKILL 直接杀死 → close 没执行 → watchdog 触发重启
// 这个行为是设计如此:防止看门狗守护进程挂了系统还撑着

嵌入式 Linux 上推荐的 watchdog 方案:用 systemd 的 WatchdogSec= + sd_notify(WATCHDOG=1),让 systemd 统一管理喂狗,业务服务只需定期发心跳通知。

Linux 专属黑坑一:设备树(DTS)改了,驱动不认

这是嵌入式 Linux 工程师入门必踩的一坑。你改了 DTS,重新编译内核,烧录——驱动还是用的老配置。

可能的原因

# 原因1:你改的是错误的 DTS 文件
# ARM64 平台,DTS 可能在多个目录有同名文件
find arch/ -name "my-board.dts"

# 原因2:DTB 没有更新(只更新了 zImage,没更新 dtb)
# 烧录时确认 dtb 文件也更新了
ls -la /boot/*.dtb

# 原因3:bootloader 用的是内置 DTB,不是你烧的
# U-Boot 里检查
printenv fdt_addr
fdt addr ${fdt_addr}
fdt print /

# 原因4:overlays 机制,有 dtbo 覆盖了你的修改
ls /boot/overlays/

还有一个让人崩溃的现象:DTS 里 compatible 字符串拼错了,驱动静默失败

/* 错误:少了一个字母 */
compatible = "mycompany,my-sensor-v1";

/* 驱动里注册的 */
static const struct of_device_id my_ids[] = {
    { .compatible = "mycompany,my-sensorv1" },  /* 和 DTS 不一致! */
    {}
};

驱动加载了,设备也枚举了,但 probe 函数永远不会被调用。没有任何报错。

# 排查:检查设备是否被驱动认领
ls /sys/bus/platform/drivers/my_driver/
cat /sys/bus/platform/devices/my_device/uevent

# 检查 compatible 匹配情况
grep -r "my-sensor" /sys/firmware/devicetree/

Linux 专属黑坑二:mmap 操作硬件寄存器,偶发数据错误

嵌入式 Linux 应用层有时会用 mmap 直接映射物理地址操作寄存器,看起来很爽,但很容易踩坑:

// 用 /dev/mem 映射寄存器
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *reg_base = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                      MAP_SHARED, fd, PERIPHERAL_BASE);

// 错误:直接用指针操作,编译器可能优化掉读写
uint32_t *ctrl = (uint32_t *)(reg_base + CTRL_OFFSET);
*ctrl = 0x01;                // 写寄存器
while (!(*ctrl & BIT(7)));   // 等待完成位

// 问题1:没有 volatile,编译器认为 ctrl 没变,优化成死循环
// 问题2:CPU 乱序执行,写和读可能被重排
// 应用层 mmap 操作:至少加 volatile + 内存屏障
volatile uint32_t *ctrl = (volatile uint32_t *)(reg_base + CTRL_OFFSET);
*ctrl = 0x01;
__sync_synchronize();         // GCC 内置内存屏障
while (!(*ctrl & BIT(7)));

// 正确做法(在内核驱动里):使用 ioread/iowrite
void __iomem *base = ioremap(PERIPHERAL_BASE, 0x1000);
iowrite32(0x01, base + CTRL_OFFSET);
// ioread/iowrite 内部包含必要的内存屏障

还有一个更隐蔽的坑:mmap 映射时忘了 O_SYNC。没有 O_SYNC,映射区域默认是可缓存的——你以为写进了寄存器,实际只写到了 CPU cache,硬件根本没收到。

Linux 专属黑坑三:时间不准,定时任务偏移

嵌入式 Linux 上跑 usleep(1000),你以为睡了 1ms,实际可能睡了 5ms、甚至 20ms。

原因:Linux 默认不是实时内核,调度延迟无法保证。

# 查看系统 HZ(内核时钟频率)
grep "CONFIG_HZ=" /boot/config-$(uname -r)
# 普通嵌入式内核通常是 CONFIG_HZ=100 或 250,定时精度 4-10ms

# 查看当前调度策略
chrt -p $$

如果你的应用需要精确定时,有几种方案:

// 方案1:用 clock_nanosleep 替代 usleep,精度更高
struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 };  // 1ms
clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL);

// 方案2:提升进程实时优先级
struct sched_param param = { .sched_priority = 80 };
sched_setscheduler(0, SCHED_FIFO, ¶m);

// 方案3:用 timerfd 实现精确周期任务
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
struct itimerspec its = {
    .it_interval = { .tv_sec = 0, .tv_nsec = 1000000 },  // 1ms 周期
    .it_value    = { .tv_sec = 0, .tv_nsec = 1000000 },
};
timerfd_settime(tfd, 0, &its, NULL);

while (1) {
    uint64_t expirations;
    read(tfd, &expirations, sizeof(expirations));  // 阻塞到下一个周期
    if (expirations > 1) {
        // 发生了超时!记录日志,分析原因
        log_overrun(expirations);
    }
    do_periodic_task();
}

如果精度要求到微秒级(比如电机控制),普通 Linux 内核根本扛不住,需要打 PREEMPT_RT 补丁或者换用实时内核。

# 检查是否是 RT 内核
uname -v | grep PREEMPT_RT

# 测量调度延迟
cyclictest -p 80 -t -n -i 1000 -l 10000
# 看 Max 延迟,普通内核可能到几十ms,RT 内核通常 < 100us

老鸟的快速诊断表(裸机 + Linux 双版本)

现象 裸机根因 Linux 根因 排查工具
加 printf/printk 好了 竞态 / 时序 竞态 / 内存屏障缺失 ftrace、逻辑分析仪
-O2 崩溃 volatile / UB READ_ONCE / 内联汇编 -fsanitize、KASAN
偶发死机 栈溢出 / heap 破坏 use-after-free / DMA 对齐 KASAN、kmemleak、addr2line
串口乱码 时钟 / 电源纹波 termios / RS485 极性 stty、示波器
现场才出问题 EMI / 温度 EMI 中断风暴 / thermal 降频 cyclictest、dmesg thermal
重启恢复 看门狗掩盖崩溃 watchdog 线程独立于业务 systemd watchdog、dmesg
sleep 能解决 时序不满足 用 sleep 替代 completion perf、wait_event
驱动 probe 不调用 compatible 不匹配 / DTB 旧 /sys/bus/platform
mmap 寄存器数据错 缺 volatile / 缺 O_SYNC valgrind、ioremap
定时任务漂移 非 RT 内核调度延迟 cyclictest、PREEMPT_RT

总结

嵌入式没有玄学,只有你还没弄清楚的底层机制。从裸机的 volatile 到 Linux 内核的 READ_ONCE,从 FreeRTOS 的栈水位到内核的 KASAN,工具一直都在——真正的问题,是你得先知道去哪里找。

踩坑不丢人,踩了坑不总结,才是最大的浪费。希望这篇梳理能帮你建立起一套系统的调试思路,下次再遇到诡异 Bug,不用求神拜佛,而是可以冷静地说一句:“这个现象,我见过。”




上一篇:Google发布Code Wiki:用AI生成活文档,五分钟搞懂百万行代码的开源项目
下一篇:Coding Agent 越能干,越不该裸跑你电脑:隔离沙箱长任务实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-10 02:57 , Processed in 0.677068 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表