嵌入式软件工程师秋招知识点梳理

前言

  • 本文目标:梳理ARM/ZYNQ相关方向的嵌入式软件工程师的秋招面试准备

嵌入式软件基础

C语言基础知识

位操作

  1. 常用宏定义 (面试手写常考)

    • 置位: #define SET_BIT(x, bit) ((x) |= (1 << (bit)))
    • 清零: #define CLEAR_BIT(x, bit) ((x) &= ~(1 << (bit)))
    • 翻转: #define TOGGLE_BIT(x, bit)((x) ^= (1 << (bit)))
  2. 位域 (Bit-field)

    • 缺陷: 内存排布(大小端、字节对齐)由编译器决定,不可移植.
    • 结论: 驱动开发中操作硬件寄存器时,慎用位域,推荐使用移位+掩码.
  3. 算法技巧

    • 计算二进制中1的个数: n = n & (n-1)
    • 原理: 每次执行都会消去二进制中最右边的一个 1,直到 n 变为 0.

volatile

  1. 核心作用: 告诉编译器不要优化该变量,每次读写必须直接访问内存/寄存器,而不是读取缓存或寄存器备份.
  2. 三大应用场景 (必背):
    • 硬件寄存器 (如状态寄存器).
    • 中断服务程序 (ISR) 中修改的全局变量.
    • 多线程共享的标志位.
  3. 特例: const volatile int x; 是合法的.表示“程序不能修改它,但硬件可能会修改它” (例如只读的状态寄存器).
  4. 常见误区 (Critical):
    • 不保证原子性: volatile int i; i++; 不是线程安全的. (读-改-写 是三条指令).
    • 不保证乱序 (Reordering): 编译器只保证 volatile 变量间的顺序,但不保证 volatile volatile 变量之间的执行顺序.
      • 这里的顺序指的是生成的汇编代码的先后顺序
    • 解决方案: 多核同步必须使用 内存屏障 (Memory Barrier)互斥锁.

内存布局与堆栈

  1. 内存段分布 (从低地址 -> 高地址)

    • 代码段 (.text): 只读,存放代码逻辑.
    • 数据段 (.data): 存放已初始化的全局变量、static变量.
    • BSS段 (.bss): 存放未初始化或初始化为0的全局、static变量(程序加载时由内核清零).
    • 堆 (Heap): 手动分配 (malloc),向高地址生长,链表管理,易产生碎片.
    • 栈 (Stack): 自动分配,向低地址生长,存放局部变量、函数上下文.
  2. 为什么栈比堆快?

    • 指令效率: 栈只需移动栈指针寄存器 (SP);堆需要查询空闲内存链表.
    • 缓存命中: 栈满足 CPU 的空间局部性 (Spatial Locality),栈顶热点数据常驻 L1 Cache.

内存对齐

  1. 核心原则: 数据的起始地址必须是其类型大小的整数倍 (如 int 地址需模 4 为 0).
  2. 为什么需要对齐? (Why):
    • 性能: CPU 访问内存是按“字 (Word)”读取的.未对齐的数据可能需要 2 个总线周期 才能读完.
    • 硬件限制 (Crash): 某些架构 (如旧版 ARM、DSP) 访问未对齐地址会直接触发 Bus ErrorHard Fault.
  3. 结构体填充 (Padding):
    • 编译器会在成员之间插入填充字节,以满足对齐要求.
    • 考题: struct { char a; int b; } 大小是 8 字节 (a 占 1, 填充 3, b 占 4).
  4. 控制对齐:
    • alignas(16): 强制按 16 字节对齐 (SIMD 指令优化常用).
    • #pragma pack(1): 取消对齐/紧凑排列. 用于定义网络协议包头或硬件通信协议,以空间换时间 (牺牲访问速度,节省带宽).

关键字解析

static

  1. 修饰局部变量:改变生命周期.存储在 .data.bss,随程序结束才释放,但作用域不变.
  2. 修饰全局变量/函数: 改变作用域.限制在当前 .c 文件内可见 (Internal Linkage),防止符号冲突.

inline

  1. 本质: 建议编译器将函数体直接在调用处展开.
  2. 优缺点: 省去函数调用的压栈、跳转开销 (提速),但会增加代码体积 (Code Bloat).
  3. 对比宏: inline 有类型检查且便于调试,宏没有.

指针陷阱

  1. int *p(int): 指针函数 (本质是函数,返回值为指针).
  2. int (*p)(int): 函数指针 (本质是指针,指向函数).常用于回调函数 (Callback) 和驱动层的结构体成员 (如 file_operations).

C++ 系统编程

RAII (资源获取即初始化)

  1. 核心机制: 将资源的生命周期绑定到对象的生命周期.构造函数获取资源,析构函数释放资源.
  2. 优势: 防止资源泄漏(内存、锁、句柄),且异常安全(Stack Unwinding 时自动析构).
  3. C 语言的对应:
    • 常用 goto 跳转到统一清理块 (Linux 内核常用).
    • GCC 扩展: __attribute__((cleanup(...))).
  4. 典型应用: std::lock_guard (管理锁的 RAII 封装),避免手动解锁导致的死锁.

零成本抽象 (Zero-cost Abstractions)

  1. 概念: 未使用的特性不产生开销;使用的特性,手写代码也不会比它更高效.
    • 使用编译期优化,函数模板的特性,减少抽象的成本
  2. 经典面试题: std::sort vs qsort:
    • qsort (C): 运行时通过函数指针调用比较函数,无法内联,有间接调用开销.
    • std::sort (C++): 利用模板在编译期生成特定类型的代码,比较函数被内联,效率极高.

智能指针 (Smart Pointers)

unique_ptr

  1. 语义: 独占所有权.禁止拷贝,只能移动 (std::move).
  2. 性能: 大小等于裸指针,零开销.
  3. 场景: 工厂函数返回对象,成员变量独占.

shared_ptr

  1. 语义: 共享所有权.
  2. 原理: 内部包含两个指针:一个指向对象,一个指向控制块 (Control Block).
    • 控制块: 包含引用计数 (ref count)弱引用计数.
    • 线程安全: 引用计数的增减是原子操作 (线程安全);但读写管理的对象不是线程安全的 (需要加锁).
  3. 性能开销:
    • 内存是裸指针的 2 倍.
    • 原子操作导致比裸指针慢.
    • 优化: 推荐使用 std::make_shared,它将“对象内存”和“控制块”合并为一次 malloc,减少内存碎片和分配开销.

weak_ptr

  1. 核心作用: 解决循环引用 (Circular Reference) 导致的内存泄漏.
    • 场景: A 指向 B,B 指向 A,计数器永不归零.
    • 解法: 将其中一方改为 weak_ptr (不增加引用计数).
  2. 使用: 必须通过 .lock() 升级为 shared_ptr 才能访问对象.若对象已销毁,返回空指针.

移动语义 (Move Semantics)

  1. 左值 vs 右值:
    • 左值: 有名字、有地址 (变量).
    • 右值: 临时对象、字面量 (表达式结果).
  2. std::move: 本质是强制类型转换 (static_cast<T&&>),告诉编译器“这个对象快死了,资源你可以拿走”.
    • 将变量转换为右值
  3. 核心价值: 避免深拷贝.例如 std::vector 扩容或返回大对象时,直接转移堆内存的所有权指针.

虚函数与底层 (Virtual Functions)

  1. 核心机制 (vptr & vtable):

    • 虚函数表 (vtable): 编译期生成.每个维持一份,存放虚函数地址.通常在只读数据段 (.rodata).
    • 虚表指针 (vptr): 运行时初始化.每个对象持有一份.隐式位于对象内存头部 (前 4/8 字节).
    • 调用流程: ptr->func() -> 读 vptr -> 查 vtable -> 拿到地址 call 跳转.
  2. 性能与开销:

    • 空间: 每个对象多一个指针大小.
    • 时间: 两次访存带来的间接寻址开销 + 阻碍内联 (Inline) (这是性能损失主因).
  3. 虚析构函数 (Virtual Destructor):

    • 铁律: 基类的析构函数必须声明为 virtual.
    • 场景: Base* p = new Derived(); delete p;
    • 后果: 若非虚,只调用 ~Base(),派生类资源 (内存/句柄) 泄漏.
    • 机制: 声明 virtual 后,析构动作走 vtable,先调 ~Derived(),再自动调 ~Base().
  4. 构造/析构中的陷阱:

    • 禁忌: 禁止在构造或析构函数中调用虚函数.
    • 原理: 构造/析构期间,对象的运行时类型退化为当前类.
      • 构造 Base 时: vptr 指向 Base vtable (多态未生效).
      • 析构 Base 时: Derived 已销毁,vptr 退回指向 Base vtable (多态已失效).
  5. 纯虚函数 (Abstract Class):

    • 语法: virtual void func() = 0;
    • 作用: 定义接口 (Interface),强制派生类实现.包含纯虚函数的类无法实例化.
  6. 继承下的虚表演变 (V-Table Evolution)

    • 构建过程 (单继承):
      • 拷贝: 派生类 vtable 首先拷贝基类 vtable 的所有内容.
      • 覆盖 (Override): 若派生类重写了虚函数 A, vtable 中对应 A 的位置被替换为派生类函数的地址.
      • 追加: 派生类新增的虚函数,按声明顺序追加到 vtable 末尾.
    • 多态实质:
      • Base* p = new Derived();
      • 虽然 p 是基类指针,但它指向的内存块头部 vptr 指向的是 Derived vtable.
      • 调用 p->func() 时,查的是替换后的表,因此执行的是派生类版本.
    • 多重继承 (必考难点):
      • 对象内存中会有多个 vptr (对应每个有虚函数的基类).
      • 转换指针类型 (Base1*Base2*) 时,编译器会自动调整指针地址 (Offset Adjustment),使其指向对应的 vptr.
  7. 多重继承下的追加规则 (Multiple Inheritance Appending)

    • 核心结论: 新增的虚函数只追加第一个基类 (Primary Base) 的 vtable 后.
    • 主基类 (Primary Base): 声明继承列表时的第一个类.
      • class Derived : public Base1, public Base2 -> Base1 是主基类.
    • 表结构差异:
      • vptr1 (Base1): 指向 { Base1 原有/覆盖函数 + Derived 新增函数 }.
      • vptr2 (Base2): 指向 { Base2 原有/覆盖函数 }. 不包含 Derived 的新增函数.
    • this 指针修正 (Thunk):
      • 当通过 Base2* 调用被 Derived 覆盖的函数时,编译器会自动插入代码将 this 指针向前偏移 (offset),使其指向 Derived 对象的起始位置.

静态多态 (CRTP)

  1. 动态多态 (虚函数): 运行时查询 vtable,有开销,阻碍内联.
  2. 静态多态 (模板/CRTP):
    • 原理: 派生类将自己作为模板参数传给基类 class Derived : public Base<Derived>.
    • 优势: 编译期确定函数调用,无虚函数开销,可内联.适合高性能计算.

并发编程

锁与原子操作

  1. 自旋锁 (Spinlock):
    • 机制: 忙等待 (Loop),不放弃 CPU.
    • 场景: 内核开发、持有锁时间极短的代码.避免上下文切换的昂贵开销.
  2. 互斥锁 (Mutex):
    • 机制: 拿不到锁就休眠 (放弃 CPU),由 OS 调度唤醒.
    • 场景: 用户态通用场景,持有锁时间较长.
  3. 死锁 (Deadlock):
    • 四个条件: 互斥、占有且等待、不可抢占、循环等待.
    • 破解: 按固定顺序加锁 (破坏循环等待).

条件变量 (Condition Variable)

  1. 为了解决线程协作而不是线程抢占的问题
  2. 虚假唤醒 (Spurious Wakeup): 线程可能在没有 notify 的情况下醒来.
    • 必考点: 必须使用 while (而不是 if) 检查条件.
    • while (!condition) { cv.wait(lock); }
  3. wait 机制: 调用 wait 时会自动释放锁并阻塞;被唤醒后会自动重新竞争锁.

无锁编程 (Lock-free)

  1. 单产单消 (SPSC): 无锁环形队列.生产者改尾指针,消费者改头指针,互不干扰,配合 atomic load/store 即可.
  2. 多产多消 (MPMC): 通常需要 CAS (Compare And Swap) 或双条件变量优化.

高级网络与并发 (Boost.Asio 模型)

Proactor 模式

  1. 机制: 类似于“外卖”.应用发起请求并注册回调 -> 内核完成 IO -> 通知应用执行回调.
  2. 对比 Reactor: Reactor (如 select/poll) 是通知“可以读/写了”,应用自己去读写;Proactor 是通知“读写完成了”.

核心组件

  1. io_context: 程序与 OS IO 机制 (epoll/kqueue) 的桥梁.
  2. run(): 运行事件循环.通常配合线程池,多个线程同时运行 io_context.run(),实现并发处理回调.
  3. Strand (面试难点):
    • 痛点: 多线程运行 run() 时,回调函数可能并发执行,导致竞争.
    • 作用: 串行化执行回调.保证被 Strand 包装的任务,严格排队执行,无需显式加锁.

异步回调的生命周期 (Life Cycle)

  1. The Bug: 发起异步操作 (async_read) 后,函数返回,TcpSession 对象可能被析构.当 IO 完成调用回调时,this 指针悬空,程序崩溃.
  2. The Fix (shared_from_this):
    • 类继承 std::enable_shared_from_this.
    • 在绑定回调时,不传 this,而是传 shared_from_this() (创建一个新的 shared_ptr).
    • 效果: 只要回调函数还在等待执行,shared_ptr 引用计数至少为 1,强行延长对象生命周期直到回调结束.

交叉编译与构建工具

编译与链接

  1. 编译全流程:

    • 预处理 (.c -> .i): 宏展开、头文件包含.
    • 编译 (.i -> .s): 翻译成汇编代码.
    • 汇编 (.s -> .o): 翻译成机器码 (可重定位目标文件).
    • 链接 (.o + 库 -> ELF): 符号解析、地址重定位,生成最终可执行文件.
  2. 静态链接 vs 动态链接 (面试必考):

    • 静态链接 (.a):
      • 原理: 将库的代码副本直接“焊”进可执行文件.
      • 优点: 移植性强 (不依赖目标机环境)、启动稍快.
      • 缺点: 体积大、库更新需重新编译程序.
    • 动态链接 (.so):
      • 原理: 仅记录符号引用,运行时由动态链接器加载库 (/usr/lib).
      • 优点: 节省内存 (多进程共享同一份库代码)、体积小.
      • 缺点: 环境依赖 (库版本不一致会导致运行失败).
  3. C 库 (libc) 选择:

    • glibc: 功能全,体积大 (桌面/服务器标准).
    • musl / uClibc: 专为嵌入式设计,体积微小,静态链接表现好.

GDB 远程调试

  1. 调试架构: C/S 架构.
    • Target (开发板): 运行 gdbserver.负责执行指令、读写寄存器,不含符号表 (轻量).
    • Host (PC): 运行 gdb-multiarch.拥有源码 + 符号表 (大脑),负责逻辑分析.
  2. 核心原理:
    • 宿主机必须加载带调试符号 (Symbol) 的可执行文件,才能将目标机的机器码地址映射回 C 语言源码行号.
  3. 高频指令:
    • bt (backtrace): 查看函数调用栈 (程序崩溃/死锁时必用).
    • b (断点), c (继续), n (单步跳过), s (单步进入), p (打印变量).

Makefile 与 CMake

  1. 核心区别:
    • Makefile: 具体的构建脚本,Make 工具直接解析执行.
    • CMake: 元构建工具 (Generator).它读取 CMakeLists.txt 生成 Makefile (或 Ninja),跨平台能力更强.
  2. 交叉编译配置:
    • Makefile: 需手动指定 CC, CXX, CROSS_COMPILE 等变量.
    • CMake: 使用 Toolchain File (.cmake).
      • 关键变量: CMAKE_C_COMPILER (编译器路径), CMAKE_SYSROOT (指定头文件/库的查找根目录,防止去宿主机 /usr 找).

Shell 脚本高频考点

  1. 重定向: cmd > file 2>&1
    • 将标准错误 (fd 2) 重定向到标准输出 (fd 1),再一起写入文件.
  2. 管道 (|): 上一个命令的 stdout 作为下一个命令的 stdin.
  3. 查找与替换:
    • grep -r "pattern" .: 递归搜索当前目录下的文本内容.
    • sed 's/old/new/g': 全局替换文本流.
  4. 状态码: $? (0 代表成功,非 0 代表失败).

硬件与接口知识 (核心篇)

Zynq MPSoC 架构

  1. AMP (非对称多核) 架构:
    • PS (Processing System): 4核 A53 (跑 Linux) + 2核 R5 (跑 FreeRTOS/裸机).负责控制与系统管理.
    • PL (Programmable Logic): FPGA 逻辑资源.负责高速并行计算、接口协议转换.
    • 交互: 核心通过 AXI 总线 连接,辅以中断 (IRQ_F2P) 和 GPIO.

AXI 总线协议 (面试必考 Top1)

  1. 核心机制:

    • 分离的读写通道: 读和写可以同时进行,全双工.
    • 5 个独立通道:
      • : 写地址(AW)、写数据(W)、写响应(B).
      • : 读地址(AR)、读数据(R).
    • 握手信号 (Handshake):
      • 所有通道都遵循 VALID (发送方有效) 和 READY (接收方准备好) 机制.
      • 只有当 VALID=1 且 READY=1 时,传输才发生.
  2. 三种协议对比:

    • AXI4-Lite: 无突发 (一次只传一个数据).用于配置寄存器 (低速控制).
    • AXI4-Full: 支持突发 (Burst),一次地址对应最多 256 个数据.用于DDR 内存读写 (大数据量).
    • AXI4-Stream: 无地址 (像水管).只有数据流 (TDATA, TVALID, TREADY).用于视频/ADC 数据流.
  3. 乱序 (Out-of-Order):

    • 利用 ID 标签,Slave 可以先处理处理得快的请求,只要最后按 ID 对应返回即可.提高总线利用率.

AXI DMA 与 UIO (驱动开发核心)

  1. 工作流程 (用户态驱动):

    • 预留内存: 通过 Bootargs/CMA 预留物理连续内存 (DMA 只能访问物理连续地址).
    • 映射寄存器: 使用 UIO 驱动 (generic-uio),将 DMA 控制器的寄存器通过 mmap 映射到用户态.
    • 启动传输: 往映射后的寄存器写入源/目的物理地址和长度.
    • 等待完成: 使用 read() 阻塞等待 UIO 中断.
  2. Cache 一致性 (Killer Question):

    • 问题: CPU 读写的是 Cache,DMA 读写的是 DDR.
    • DMA 读 (Device -> Mem): 传输前,CPU 需 Invalidate (作废) Cache,强迫 CPU 下次读取时从 DDR 拿最新数据.
    • DMA 写 (Mem -> Device): 传输前,CPU 需 Flush (刷回) Cache,将 Cache 中的脏数据写回 DDR,确保 DMA 搬运的是正确数据.
  3. 特殊 DMA 对比:

    • VDMA (Video): 懂“二维图像”.支持 Stride (跨度,跳过行尾无效数据) 和 帧缓存管理 (避免画面撕裂).
    • QDMA (Queue): 针对 PCIe 优化的 DMA,用于 PC 与 FPGA 的高吞吐通信.

ARMv8 (A53) 体系结构

  1. 异常等级 (Exception Levels):

    • EL0 (User): 应用程序.
    • EL1 (Kernel): Linux 内核 (特权模式).
    • EL2 (Hypervisor): 虚拟化 (KVM/Xen).
    • EL3 (Secure Monitor): 安全世界入口 (TrustZone),负责安全启动和密钥管理.
  2. Linux 启动流程 (Zynq MPSoC):

    • BootROM (固化代码, EL3) -> FSBL (初始化硬件, 加载下一级) -> U-Boot (EL2/EL3, 加载内核) -> Linux Kernel (EL1) -> Rootfs/App (EL0).

MGT 与 PCIe/NVMe

  1. MGT (Multi-Gigabit Transceiver):

    • 本质: 物理层 (PHY).负责将并行数据转为高速串行差分信号 (SerDes).
    • 协议无关性: MGT 本身不识协议,它上面接 PCIe 控制器就是 PCIe,接 SATA 控制器就是 SATA.
  2. M.2 NVMe 识别流程:

    • 物理层: M.2 接口走 PCIe 通道.
    • 内核层: Linux 启动 -> PCIe 子系统枚举 (Enumeration) -> 发现 NVMe 设备 ID -> 加载 nvme 驱动 -> 注册块设备 (/dev/nvme0n1).

高速信号设计 (PCB与SI)

  1. 差分信号 (Differential Signaling)

    • 核心优势:
      • 抗干扰强: 接收端只看电压差,忽略共模噪声 (Common Mode Noise).
      • 低功耗: 电压摆幅 (Swing) 低,切换速度快.
    • 关键参数: 特征阻抗 (通常 100Ω).
    • 等长绕线: 必须保证 P/N 线等长,否则产生相位偏斜 (Skew),导致共模抑制能力失效.
  2. 信号完整性 (SI) 考点:

    • 反射 (Reflection): 阻抗不匹配导致.解决办法是终端匹配电阻 (Termination).
    • 低速 vs 高速: 当信号上升沿时间小于传输延时的 1/6 (或更严格) 时,必须视为传输线处理.

内存映射 I/O (MMIO)

  1. 访问机制:

    • Linux 用户态: 通过 open("/dev/mem") + mmap() 将物理地址映射到虚拟地址.
    • 关键关键字: volatile.
      • 必考题: 为什么读写寄存器指针必须加 volatile?
      • 答案: 防止编译器优化.编译器可能认为变量没变就直接读缓存/寄存器备份,而硬件寄存器的值随时可能被外设修改,volatile 强迫 CPU 每次都去内存地址(总线)读取.
  2. 片内存储区分:

    • OCM (On-Chip Memory): SRAM.确定性延迟,由程序员手动管理 (放堆栈、关键代码).
    • Cache: SRAM.非确定性 (Cache Miss),由硬件自动管理.

CAN 总线 (工业/汽车核心)

  1. 物理层:

    • 双绞线差分: CAN_H, CAN_L.
    • 终端电阻: 两端各接 120Ω (消除反射).
  2. 仲裁机制 (面试杀手锏):

    • 线与逻辑 (Wired-AND):
      • 显性电平 (Dominant): 逻辑 0 (电压差大).0 能覆盖 1.
      • 隐性电平 (Recessive): 逻辑 1 (电压差 0).
    • 非破坏性仲裁:
      • 节点边发边听.如果自己发 1 (隐性) 但总线上听到 0 (显性),说明有高优先级设备在发.
      • 动作: 立即停止发送,转为接收,不破坏当前总线数据.
    • 结论: ID 越小,优先级越高 (因为 ID 高位是 0 的容易抢占总线).

IIC (I2C) 总线

  1. 硬件特性:

    • 两根线: SCL (时钟), SDA (数据).
    • 开漏输出 (Open-Drain): 必须接上拉电阻.
      • 优势: 允许多主设备 (线与逻辑),不同电压等级设备电平兼容.
  2. 时序图谱 (手画重点):

    • 起始位 (Start): SCL 高电平时,SDA 由高变低.
    • 停止位 (Stop): SCL 高电平时,SDA 由低变高.
    • 数据有效性: SCL 高电平期间数据必须稳定,SCL 低电平期间 SDA 才能翻转.
  3. ACK 机制:

    • 每传 8 位数据,第 9 个时钟周期由接收方将 SDA 拉低 (ACK).若 SDA 为高则为 NACK (通常意味着结束或出错).

SPI 与 QSPI

  1. SPI (标准四线):

    • 全双工: MOSI 和 MISO 同时传输.
    • 核心原理: 移位寄存器交换.Master 和 Slave 像转轮一样,Master 推一位出去,同时也把 Slave 的一位拉进来.
    • 模式: 常见 Mode 0 (CPOL=0, CPHA=0) 和 Mode 3.
  2. QSPI (四线/Flash专用):

    • 半双工: 4 根线 IO0-IO3 全用于传数据.带宽是 SPI 的 4 倍.
    • XIP (Execute In Place): CPU 直接通过 QSPI 接口在 Flash 上取指运行,无需拷贝到 RAM.
    • 传输阶段: 命令 (单线) -> 地址 (单线/四线) -> 数据 (四线).

UART (串口)

  1. 基本概念:
    • 异步通信: 无时钟线,依赖双方约定的波特率.
    • 采样: 接收方在检测到起始位后,在每个位周期的中间点采样.
  2. 区分:
    • UART: 协议层 (TTL 电平).
    • RS232/RS485: 物理接口标准 (高电压/差分传输).UART 信号经过电平转换芯片 (如 MAX232) 变成 RS232.

操作系统与内核 (构建与驱动篇)

操作系统核心概念 (RTOS与IPC)

裸机 vs RTOS vs Linux

  1. 裸机 (Bare-Metal):
    • 架构: while(1) 主循环 + 中断服务程序 (ISR).
    • 缺点: 难以处理复杂的并发逻辑,无任务隔离.
  2. RTOS (如 FreeRTOS):
    • 核心: 强实时性 (Hard Real-time).
    • 调度: 基于优先级抢占.高优先级任务就绪时,必须立即打断低优先级任务.
  3. Linux:
    • 核心: 高吞吐量公平性 (分时复用).
    • 调度: CFS (完全公平调度器).标准 Linux 不是硬实时的 (但在嵌入式中常配合 PREEMPT_RT 补丁使用).

进程间通信 (IPC) 纵向对比

IPC 方式 核心机制 场景与优缺点
管道 (Pipe) 内核缓冲区,单向流动 父子进程通信.简单,半双工.
命名管道 (FIFO) 文件系统可见的文件 无亲缘关系进程通信.
消息队列 内核维护的消息链表 传输带类型的数据块.比管道灵活,有容量限制.
共享内存 物理内存映射 (Zero-copy) 速度最快.缺点: 不安全,必须自行配合信号量进行同步.
信号量 计数器 (PV操作) 不传数据,只用于同步与互斥 (锁).
信号 (Signal) 异步通知 (软中断) 携带信息极少 (如 kill -9),用于处理紧急事件.
Socket 网络协议栈 通用性最强 (可跨机),但开销大 (数据拷贝).

Petalinux 与 Yocto

  1. 核心概念辨析

    • Yocto: 嵌入式 Linux 的构建系统引擎 (底层),行业标准.
    • Petalinux: Xilinx 基于 Yocto 封装的专用开发工具 (上层),简化了配置流程.
    • 关系: Petalinux = Yocto 引擎 + Xilinx BSP (板级支持包) + 自动化工具脚本.
  2. 开发工作流 (面试口述逻辑)

    • 导入: petalinux-config --get-hw-description 导入 Vivado 导出的 .xsa (硬件描述).
    • 配置:
      • config kernel: 裁剪内核驱动 (如开启 NVMe).
      • config rootfs: 添加用户库和工具.
    • 构建: petalinux-build 生成 Image.
    • 打包: petalinux-package 生成 BOOT.BIN (包含 FSBL, Bitstream, U-Boot).

Device Tree (DTS) 设备树

核心概念

  1. 定义: 一种描述硬件拓扑结构的数据结构 (.dts).

    • 目的: 将硬件信息 (地址、中断) 与 驱动源码 分离.
    • 优势: 修改硬件配置只需改 DTS,无需重新编译内核 (解决 ARM Linux 源码膨胀问题).
  2. 文件类型:

    • .dts / .dtsi: 源码 (Source),人类可读.
    • .dtb: 二进制 (Blob),内核可解析 (由 DTC 编译器生成).

语法与关键属性 (必考)

  1. 节点 (Node): 格式为 label: name@address { ... };.
  2. 四大核心属性:
    • compatible (灵魂属性): 字符串列表 (如 "vendor,device"). 驱动程序靠它来匹配设备.
    • reg: 描述寄存器基地址和长度 (配合 #address-cells 使用).
    • interrupts: 描述中断号及触发方式 (高电平/边缘触发).
    • status: "okay" (启用) 或 "disabled" (禁用).

设备树解析流程 (高频考点)

面试题: “设备树里的节点,是怎么变成驱动里的 probe 函数被调用的?”

  1. 加载: U-Boot 将 .dtb 加载到内存,并将内存地址传给 Kernel.
  2. 展开: Kernel 启动早期解析 .dtb,将节点转换为内核结构体 device_node.
  3. 注册: Kernel 遍历带有 compatible 的节点,将其注册为 platform_device (平台设备),挂载到平台总线 (Platform Bus) 上.
  4. 匹配:
    • 驱动注册 platform_driver.
    • Platform Bus 自动对比 Devicecompatible 属性和 Driverof_match_table.
  5. 触发: 一旦字符串匹配成功,内核自动调用驱动的 probe() 函数.

Linux 驱动分类与框架

驱动类型简析

  1. 内核驱动 (Kernel Driver): 运行在 EL1 (内核态).标准方式,崩溃会导致死机.
  2. UIO (Userspace I/O): 运行在 EL0 (用户态).简单暴力,内核只负责映射内存,逻辑全在用户态.适合控制简单的 FPGA 寄存器.
  3. Bypass 驱动 (DPDK): 绕过内核协议栈,追求极致性能 (零拷贝),常用于服务器网络/存储.

平台驱动模型 (Platform Driver) 开发核心

驱动入口与出口 (套路代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
static int __init my_driver_init(void) {
// 向内核注册平台驱动
return platform_driver_register(&my_pdrv);
}

static void __exit my_driver_exit(void) {
// 注销驱动
platform_driver_unregister(&my_pdrv);
}

module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
  • 考点: __init__exit 宏的作用是告诉内核,这些函数只在加载/卸载时用一次,用完可以回收内存.

核心结构体: platform_driver

这是驱动开发的主战场,面试时建议能手写结构体框架:

1
2
3
4
5
6
7
8
static struct platform_driver my_pdrv = {
.probe = my_probe, // 匹配成功时调用 (初始化硬件)
.remove = my_remove, // 卸载或设备移除时调用
.driver = {
.name = "my_driver_name",
.of_match_table = my_match_table, // ★★★ 靠这个表匹配设备树
},
};

匹配表: of_match_table

1
2
3
4
static const struct of_device_id my_match_table[] = {
{ .compatible = "my-company,my-ip-v1.0" }, // 必须与 DTS 一模一样
{ /* Sentinel (末尾空元素必须有) */ }
};

Makefile (原理篇)

不需要背代码,但要懂核心变量:

  1. obj-m := my_driver.o: 告诉构建系统生成模块 (.ko).
  2. make -C $(KDIR) M=$(PWD):
    • -C $(KDIR): 切换到内核源码目录 (借用内核顶层 Makefile 的规则).
    • M=$(PWD): 告诉内核“源码在我当前目录,编译完放回来”.

常用验证命令

  • insmod xxx.ko: 加载模块.
  • lsmod: 查看已加载模块.
  • dmesg: 查看内核打印日志 (最常用的调试手段).
  • /sys/bus/platform/drivers/: 查看驱动是否注册成功.

Linux 驱动实现细节

获取硬件资源 (Probe 函数核心)

  • 目标: 在 probe 函数中,将设备树 (DTS) 中的资源转化为内核可用的指针和中断号.
  • 常用 API:
    • 获取内存资源:
      • platform_get_resource(pdev, IORESOURCE_MEM, 0): 获取物理地址范围.
      • devm_ioremap_resource(&pdev->dev, res): 核心映射.将物理地址映射为虚拟地址.
      • 面试点: devm_ 前缀表示 Managed Resource,驱动卸载时内核会自动释放,无需手动 iounmap,防内存泄漏.
    • 读写寄存器:
      • readl(addr) / writel(val, addr): 读写 32 位寄存器.
    • 获取中断:
      • platform_get_irq(pdev, 0): 获取虚拟中断号.
      • devm_request_irq(...): 注册中断处理函数 (ISR).

保存设备上下文 (Context)

  • 场景: 驱动中有多个函数 (read/write/irq),如何让它们都能访问到同一个设备结构体?
  • 机制: 私有数据挂载.
    • platform_set_drvdata(pdev, priv_data): 在 probe 中,把私有结构体指针挂到 pdev 上.
    • platform_get_drvdata(pdev): 在 remove 或其他地方,把它取出来.

创建设备文件 (File Operations)

这是用户空间与内核空间交互的桥梁.

  • file_operations 结构体:
    1
    2
    3
    4
    5
    6
    static struct file_operations my_fops = {
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
    };
  • 关键函数实现:
    • open:
      • 技巧: filp->private_data = my_dev_ptr;
      • 将设备结构体存入文件指针,后续 read/write 就不需要全局变量了 (支持多设备实例).
    • read / write (数据搬运):
      • 必考 API:
        • copy_to_user(user_buf, kernel_buf, size): 内核 -> 用户.
        • copy_from_user(kernel_buf, user_buf, size): 用户 -> 内核.
      • 原因: 用户空间指针在内核可能是非法的 (缺页或无权限),直接访问会导致 Kernel Panic.

Linux 启动流程 (Boot Process)

面试口诀: 上电 -> BootROM -> FSBL -> U-Boot -> Kernel -> Init -> App

  1. BootROM (固化):
    • 芯片上电运行的第一行代码.
    • 读取引脚 (Boot Mode),找到启动介质 (SD/Flash),加载 FSBL.
  2. FSBL (First Stage Boot Loader):
    • 初始化 PS 端核心硬件 (DDR内存、PLL时钟).
    • 加载 PL Bitstream (配置 FPGA).
    • 加载 U-Boot 到 DDR.
  3. U-Boot:
    • 初始化更多外设 (网卡、USB).
    • 提供命令行交互 (倒计时).
    • 加载 Kernel (uImage)设备树 (.dtb) 到内存.
    • 通过 bootargs 传递启动参数 (如 console=ttyPS0, root=/dev/mmcblk0p2).
  4. Linux Kernel:
    • 自解压 -> 初始化子系统 (MMU, 调度) -> 解析设备树 -> 挂载 RootFS.
  5. Init 进程:
    • 启动 PID=1 进程 (Systemd/SysVinit).
    • 启动用户服务 (SSH, 你的 App).

中断处理机制 (Interrupts)

硬件与内核路径

设备 -> GIC -> CPU -> 向量表 -> 通用分发器 -> 驱动 ISR

  • GIC (Generic Interrupt Controller): ARM 的中断管家.负责优先级仲裁、路由到指定 CPU 核.
  • 上下文保存: 硬件自动保存 PC/CPSR,跳转到异常向量表.

顶半部与底半部 (Top / Bottom Half)

面试核心: 为什么要分两半? -> 为了系统的响应速度 (Real-time Latency).

  • 顶半部 (Top Half / Hard IRQ):
    • 就是 ISR: request_irq 注册的函数.
    • 状态: 关中断 (优先级最高,谁都打断不了它).
    • 任务: 必须极快.只做清除中断标志、拷贝少量数据.然后调度底半部.
  • 底半部 (Bottom Half):
    • 状态: 开中断 (可以被其他中断打断).
    • 任务: 处理耗时逻辑 (解析协议、复杂运算).
    • 三种实现:
      1. Softirq:
        • 权限: 内核核心专用 (驱动不可新增). 静态编译,仅限网络、定时器等少数高吞吐子系统.
        • 上下文: 中断上下文 (不可睡眠, 不可持互斥锁).
        • 并发性: 全并发. 同一个处理函数可在不同 CPU 同时运行.
        • 要求: 代码必须完全可重入 (Re-entrant),需自行处理复杂的自旋锁保护.
      2. Tasklet(小任务):
        • 本质: 基于 Softirq 封装的动态机制 (TASKLET_SOFTIRQ).
        • 上下文: 中断上下文 (不可睡眠).
        • 并发性: 同类串行. 同一个 Tasklet 实例不能并发执行 (简化了锁的需求).
        • 场景: 一般驱动程序 (USB, 键盘, 鼠标) 的首选.
      3. Workqueue (工作队列): 运行在内核线程上下文,可以睡眠 (可以调用延时函数或持锁).
        • 本质: 将任务交给内核线程 (kworker) 去执行.
        • 上下文: 进程上下文 (Process Context).
        • 特权: 可以睡眠. 可以调用阻塞函数 (sleep, kmalloc(GFP_KERNEL)), 可以持有互斥锁 (mutex).
        • 代价: 涉及上下文切换, 开销比 Softirq/Tasklet 大.

MSI/MSI-X

  • 机制: 设备不拉物理线,而是往特定内存地址写数据触发中断.
  • 优势: 支持多队列 (Multi-Queue),不同队列绑定不同 CPU,大幅提升 PCIe SSD/网卡 性能.

Linux 内存管理

虚拟内存 (Virtual Memory)

  • 核心作用:
    • 隔离: 进程 A 崩溃不影响进程 B.
    • 假象: 每个进程都以为自己独占 4GB (32位) 空间.
  • 空间划分:
    • 用户空间 (0~3G): 进程独有,通过页表隔离.
    • 内核空间 (3G~4G): 所有进程共享同一个映射.

MMU 工作原理 (地址翻译)

  • 流程: CPU 发出虚拟地址 -> TLB (缓存) 查询 -> 未命中则查询 页表 (Page Table) -> 得到物理地址 -> 访问 DDR.
  • Page Fault (缺页异常):
    • Lazy Allocation: malloc 申请内存时,内核只给承诺 (虚拟地址),不给物理内存.
    • 触发: 当程序第一次真正写入该地址时,触发 Page Fault,内核才暂停程序,分配物理页,更新页表,然后恢复执行.

内核内存申请

  • kmalloc: 申请物理连续内存 (类似于 malloc).用于 DMA 缓冲区 (因为 DMA 不经过 MMU).
  • vmalloc: 申请虚拟连续但物理不连续内存.用于大块内存分配 (避免碎片).
  • ioremap: 物理地址 -> 虚拟地址.专门用于访问硬件寄存器.

异常与看门狗

异常 (Exception)

  • 定义: 任何打断 CPU 正常执行流的事件.
  • 关系: 中断 (Interrupt) 是一种异步异常.

WDT (Watchdog Timer)

  • 本质: 一个独立的硬件倒计时计数器.
  • 机制: 程序必须周期性 “喂狗” (Feed/Kick) 重置计数器.如果程序卡死 (未喂狗),计数器归零,硬件强制复位系统.

存储与文件系统 (PCIe/NVMe/SSD)

PCIe 总线协议

核心概念

  • 定义: 高速、串行、点对点、全双工的总线标准.
  • 拓扑: 树形结构.Root Complex (RC) -> Switch/Bridge -> Endpoint (EP).

总线枚举 (Bus Enumeration)

面试题: “Linux 启动时是怎么知道有哪些 PCIe 设备的?”

  • 执行者: BIOS/UEFI 或 U-Boot.
  • 流程 (深度优先遍历):
    1. 从 Bus 0 (RC) 开始扫描.
    2. 发送 Configuration Read TLP 探测每个 BDF (Bus/Device/Function).
    3. 如果有回音 (Vendor ID 有效),说明有设备.
    4. 分配总线号,读取资源需求 (BAR),分配地址空间.

BAR (Base Address Register)与地址映射

  • 作用: 将设备内部的寄存器/内存,映射到 CPU 的物理地址空间 (MMIO).
  • 探测过程:
    1. 软件向 BAR 写全 1.
    2. 设备返回掩码 (如 0xFFF00000),表示需要 1MB 空间.
    3. 软件分配一个 1MB 的物理基地址,写回 BAR.
  • 结果: 此后 CPU 读写这段物理地址,就会被 RC 转化为 TLP 包发给该 PCIe 设备.

TLP (Transaction Layer Packet)

  • 本质: PCIe 通信的基本单元.
  • 结构: Header (路由信息, 地址, 类型) + Data Payload (数据载荷) + CRC.

NVMe 协议 (高性能存储)

核心机制: 多队列 (Multi-Queue)

对比: AHCI (SATA) 只有一个队列,NVMe 有 64K 个队列.

  • SQ (Submission Queue): 主机 (Host) 往里放命令 (读/写).
  • CQ (Completion Queue): 设备 (SSD) 往里放结果 (完成状态).
  • Doorbell (门铃):
    • 一个 MMIO 寄存器.
    • 机制: Host 放完命令后,写一下 Doorbell.SSD 收到信号,通过 DMA 抓取命令执行.

DMA 数据传输 (Scatter-Gather)

  • 背景: 操作系统的大块内存物理上往往是不连续的.
  • PRP (Physical Region Page): 描述物理页列表 (适合小数据).
  • SGL (Scatter Gather List): 描述 [地址, 长度] 列表 (适合大数据/非连续内存).

SSD (固态硬盘) 底层原理

NAND Flash 特性 (必考)

  • 读写不对称: 按 Page (页, 4KB) 读写,按 Block (块, 2MB) 擦除.
  • 先擦后写: 不能覆盖写.要修改数据,必须先把整个块擦除.
  • 寿命: P/E Cycles (擦写次数) 有限.SLC > MLC > TLC > QLC.

FTL (Flash Translation Layer)

  • 定义: SSD 主控固件,将“烂”的 NAND 伪装成完美的块设备.
  • 核心功能:
    • LBA -> PPA 映射: 逻辑地址 (操作系统看到的) -> 物理地址 (NAND 真实的).
    • 磨损均衡 (Wear Leveling): 确保所有块“一起老”,防止某块早死.
    • 垃圾回收 (GC): 搬运有效数据到新块,擦除旧块.GC 是导致 SSD 偶尔掉速的元凶.

Linux IO 栈与块设备层

IO 请求的生命周期 (五层模型)

用户写 -> VFS -> 文件系统 -> 块设备层 -> 驱动 -> 硬件

  1. 用户层: write(fd, buf, size).
  2. VFS: 虚拟文件系统,统一接口.
  3. 文件系统 (Ext4): 计算 LBA,生成 bio 结构体.
  4. 块设备层 (Block Layer):
    • IO 调度 (Scheduler): 合并相邻请求,电梯算法排序.
    • NVMe 优化: 对于 NVMe,通常使用 none (不做复杂调度),因为 SSD 随机读写够快.
  5. NVMe 驱动:
    • bio 转化为 NVMe Command,放入 SQ,敲 Doorbell.

文件系统日志 (Journaling)

  • 目的: 防止断电导致文件系统崩溃 (Metadata损坏).
  • 模式:
    • writeback (最快): 只记元数据日志,不保数据.
    • ordered (默认): 元数据记日志,但强迫数据先落盘.

高级 IO 模式

  • Buffered IO (默认): 经过 Page Cache.快,但有丢数据风险.
  • O_DIRECT (Direct IO): 绕过 Page Cache.
    • 场景: 数据库 (自己管理缓存)、测试磁盘真性能.
    • 要求: 内存地址必须对齐 (通常 4KB).
  • O_SYNC: 同步写.等到数据真正落盘才返回.安全但慢.

通信协议与数据处理

基础数据处理

字节序 (Endianness)

  • 大端 (Big-Endian): 高位在低地址.网络字节序 (TCP/IP) 标准.
  • 小端 (Little-Endian): 低位在低地址.x86/ARM (默认) 标准.
  • 考点: ntohl() (Network to Host) 和 htonl() 的作用 —— 这里的 “h” 通常就是小端,”n” 是大端.

完整性校验

  • Checksum (校验和): 简单的累加和.用于 TCP/IP 头部.抗干扰弱,计算极快.
  • CRC (循环冗余校验):
    • 原理: 模二除法.将数据流看作多项式,除以生成多项式 (Generator Polynomial).
    • 优势: 对位反转 (Bit Error) 极度敏感,能检测突发错误.
    • 实现 (CRC32 查表法):
      • 预计算 256 个值的 CRC 表.
      • 处理时:reg = (reg >> 8) ^ table[(reg ^ byte) & 0xFF].
      • 效率比逐位计算快 8 倍.

零拷贝 (Zero-Copy)

面试题: “传统的 sendfile 为什么要拷贝 4 次?零拷贝怎么优化的?”

  • 传统 IO (read + write):
    1. 硬盘 -> 内核页缓存 (DMA).
    2. 内核页缓存 -> 用户缓冲区 (CPU).
    3. 用户缓冲区 -> 内核 Socket 缓冲区 (CPU).
    4. Socket 缓冲区 -> 网卡 (DMA).
    • 代价: 4 次拷贝,2 次 CPU 参与,4 次上下文切换.
  • 零拷贝 (sendfile + SG-DMA):
    1. 硬盘 -> 内核页缓存 (DMA).
    2. 不拷贝数据,只将页缓存的描述符 (地址+长度) 传给 Socket 缓冲区.
    3. 网卡根据描述符直接从页缓存读取数据 (DMA).
    • 优势: 0 次 CPU 数据拷贝,2 次上下文切换.

网络协议栈 (嵌入式视点)

RTSP 流媒体协议

  • RTSP: 遥控器.负责 Play, Pause, Teardown 等控制命令 (TCP).
  • RTP: 运输车.负责传输实际的音视频数据包 (UDP).
  • RTCP: 质检员.周期性反馈丢包率、抖动,用于流量控制.
  • SDP (Session Description): 说明书.
    • 告诉客户端:这个流是 H.264 还是 H.265?采样率多少?
    • 客户端收到 DESCRIBE 响应后的 SDP 才能初始化解码器.

TCP 核心机制 (精简版)

  • 可靠性: 序列号 (Seq) + 确认号 (Ack) + 超时重传.
  • 流控 (Flow Control): 滑动窗口.接收方告诉发送方“我还能吃多少”.
  • 拥塞控制 (Congestion Control): 慢启动、拥塞避免.发送方探测“路有多堵”.
  • 粘包问题: TCP 是字节流,没有边界.应用层必须自己处理分包 (如:长度头 + 数据).

WebSocket

  • 痛点: HTTP 是“一问一答” (半双工).服务器无法主动推消息.
  • 解决:
    • 握手阶段借用 HTTP (兼容防火墙).
    • 建立后升级为 TCP 全双工 长连接.
    • 适合:实时监控、即时通讯.

FPGA 基础知识 (软硬协同必问)

Verilog 核心考点

赋值语句

  • 阻塞赋值 (=):
    • 顺序执行 (像 C 语言).
    • 用于 组合逻辑 (assign 或 always @*).
  • 非阻塞赋值 (<=):
    • 并行执行.在时钟沿到来瞬间,所有寄存器同时更新.
    • 用于 时序逻辑 (always @posedge clk).
    • 口诀: 时序逻辑用 <=,组合逻辑用 =.混用必死.

跨时钟域处理 (CDC)

面试必考: “慢时钟采快信号和快时钟采慢信号有什么区别?”

  • 单比特信号:
    • 快 -> 慢 (打两拍): 使用两级触发器同步,消除亚稳态.
    • 慢 -> 快 (脉冲展宽): 快时钟信号太短,慢时钟采不到.必须将信号拉长 (Handshake 或展宽电路) 至少 1.5 个慢时钟周期.
  • 多比特信号 (数据总线):
    • 绝对禁止直接打两拍 (因为各 bit 延迟不同,会导致数据错乱).
    • 解法: 使用 异步 FIFO双口 RAM.