你有没有经历过这种绝望——代码改了一个字,系统活了;加了一句 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);
}
}
加了 printf,Task_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 崩溃的调试步骤:
- 用
-fsanitize=undefined 跑一遍 Host 版本(Linux 应用层可以直接跑)
- 检查所有共享变量,裸机用
volatile,Linux 驱动用 READ_ONCE/WRITE_ONCE
- 检查内联汇编是否有
volatile 和正确的 clobber 列表
- 检查指针强转是否存在对齐问题
第三坑:偶发死机,重启就好
这是最折磨人的一类 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 驱动里,msleep 和 usleep_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,不用求神拜佛,而是可以冷静地说一句:“这个现象,我见过。”