面灵AI→

影石嵌入式驱动一二面:C 基础、I2C/SPI 与 Linux 驱动框架

轮次
一面+二面
时间
2026-10
来源
牛客网

《面试题目》

  1. C 和 C++ 哪个更熟悉?C 语言中指针和数组有什么区别?
  2. typedef 和 #define 有什么区别?分别在什么情况下使用?
  3. 结构体和联合体有什么区别?结构体内存对齐是怎么回事?
  4. const、static、volatile 分别有什么作用?在嵌入式里用过哪些?
  5. 野指针和悬空指针有什么区别?怎么避免?
  6. C 语言的内存分布是怎样的?代码段、数据段、BSS、堆和栈分别存放什么?
  7. 程序从源代码到可执行文件,预处理、编译、汇编和链接分别做什么?
  8. Linux 进程和线程有什么区别?进程间通信方式有哪些?
  9. I2C 的完整通信时序是什么?起始、停止、ACK、NACK 怎么产生?
  10. I2C 总线可以挂多少个从设备?总线速度有哪些常见模式?
  11. I2C 为什么通常需要上拉电阻?空闲时为什么是高电平?
  12. I2C 通信出现 NACK,从哪些方面排查?
  13. I2C 总线被拉低后一直无法恢复,可能是什么原因?怎么处理?
  14. Linux 下用过哪些 I2C 调试工具?i2cdetect、i2cget、i2cset 的作用?
  15. SPI 的工作原理是什么?CPOL、CPHA 对应的四种模式怎么理解?
  16. SPI 传输数据出错,怎么定位?有没有遇到过时序问题?
  17. UART 一般使用哪些信号线?除了 TX、RX,还知道哪些流控信号?
  18. 介绍一个项目中遇到的难题,具体怎么定位和解决?
  19. platform 总线、设备和驱动之间是什么关系?设备怎么和驱动匹配?
  20. 设备树是为了解决什么问题?为什么把硬件描述从驱动代码里分离?
  21. DTS 和 DTSI 有什么区别?设备树节点的属性覆盖规则是什么?
  22. 在新开发板上接入 I2C 传感器,从设备树到驱动注册需要做哪些工作?
  23. Linux 字符设备驱动的基本框架是什么?cdev 结构体起什么作用?
  24. 用户程序打开 /dev 下的设备节点时,最终怎么调用到驱动的 open?
  25. file_operations 有哪些常见成员?read、write、ioctl 分别负责什么?
  26. 为什么要注册设备节点?用户态访问设备为什么不直接操作硬件寄存器?
  27. 驱动需要 GPIO 或中断资源时,通过什么内核接口申请?为什么不能随意占用?
  28. Linux 中断处理流程是什么?为什么要分上半部和下半部?
  29. 中断下半部能不能睡眠?tasklet 和 workqueue 的使用场景有什么不同?
  30. 自旋锁和互斥锁有什么区别?哪些上下文不允许使用可能睡眠的锁?
  31. U-Boot 的启动流程大致是什么?Linux 内核启动后主要完成哪些初始化?
  32. Linux 内核有哪些主要子系统?你真正看过哪部分源码?
  33. Linux 程序发生内存异常怎么排查?gdb、core dump、strace 分别有什么用?
  34. 系统出现性能瓶颈怎么判断?top、free、perf 实际怎么用?
  35. 优化嵌入式 Linux 开机时间,目标压到 5 秒,你会从哪些地方入手?
  36. 网络传输丢包、延迟、卡顿,怎么用 tcpdump、Wireshark 定位?TCP/IP 有哪些可调参数?MTU 是什么?
  37. 维护已交付的设备驱动,怎么验证修改后的稳定性?

《参考解析》

C 基础:指针是变量,存地址;数组名在大多数表达式里退化为指向首元素的指针,但 sizeof 和取地址行为不同。typedef 是编译器层面的类型别名,有作用域和类型检查;#define 是预处理文本替换,容易踩括号和副作用。结构体成员各自有地址,联合体成员共享同一块内存,大小取最大成员;内存对齐是为了让 CPU 高效访问,结构体大小按最大成员对齐补齐,调整成员顺序可以减少 padding。const 修饰只读,static 修饰文件作用域或静态存储期,volatile 告诉编译器该变量可能被外部改变、不要优化掉读写。野指针是未初始化的指针,悬空指针是指向已释放内存的指针;初始化、释放后置空、使用前判空、用工具(ASan/Valgrind)检查。

内存与编译链接:代码段存指令和只读常量,数据段存已初始化的全局/静态变量,BSS 存未初始化或零初始化的全局/静态变量(不占文件空间,运行时清零),堆由 malloc/new 管理,栈存函数调用帧和局部变量。预处理展开宏和头文件,编译生成汇编,汇编生成目标文件,链接把目标文件和库合并、重定位符号、生成可执行文件。动态链接在运行时解析符号。

I2C/SPI/UART:I2C 是两线(SCL/SDA)半双工总线,起始条件是 SCL 高时 SDA 由高变低,停止相反;每个字节后第 9 个时钟由从机拉低 SDA 表示 ACK,不拉低是 NACK。7 位地址最多 127 个从设备(实际受地址冲突和电容限制),速度有 100k/400k/1M/3.4M 等模式。开漏输出必须上拉,空闲时上拉为高。NACK 排查:地址对不对、从设备是否上电/复位、时序/速率、总线电容、上拉阻值、是否被其它设备占用。总线被拉低不恢复常见于从机死锁、时钟拉伸、电源异常,处理可以尝试手动发 9 个时钟、复位从机或重新上电。工具用 i2cdetect 扫地址、i2cget/i2cset 读写寄存器。SPI 是四线(SCLK/MOSI/MISO/CS)全双工,CPOL 决定空闲时钟电平,CPHA 决定采样沿,组合成 4 种模式;出错先查模式、片选、时钟频率、线序和地。UART 有 TX/RX,流控常见 RTS/CTS(硬件)和 XON/XOFF(软件)。

Linux 驱动模型:platform 总线是虚拟总线,用于 SoC 内嵌设备;设备用 device tree 或 board file 描述,驱动用 platform_driver 注册,二者通过 compatible 字符串匹配,probe 时拿资源(reg、irq、gpio、clk)。设备树把硬件描述从代码里分离,同一驱动可适配不同板子,DTS 是板级文件,DTSI 是被 include 的公共描述,属性覆盖按节点合并规则。字符设备驱动用 alloc_chrdev_region 申请设备号、cdev_init/cdev_add 注册、class_create/device_create 生成 /dev 节点,file_operations 提供 open/read/write/ioctl/release。用户态 open 触发 VFS,根据 inode 的设备号找到 cdev,调用对应的 file_operations。设备节点是用户态访问内核驱动的标准接口,直接操作寄存器会破坏权限、抽象和可移植性。

中断与锁:中断处理分上半部(快速响应、清中断、尽量短)和下半部(耗时处理)。下半部中 softirq/tasklet 运行在中断上下文,不能睡眠;workqueue 运行在进程上下文,可以睡眠。自旋锁忙等、适合短临界区,中断上下文只能用自旋锁;互斥锁会睡眠,不能在中断上下文使用。抢占、中断关闭和锁的顺序要注意死锁。

启动与性能:U-Boot 分 SPL/TPL 和主 U-Boot 两阶段,初始化 DRAM、时钟、外设,加载内核和设备树,跳转内核;内核启动做解压、页表、调度器、驱动 probe、挂载 rootfs、启动 init。优化开机:裁剪内核配置和驱动、减少 probe 耗时、并行初始化、延迟加载非关键模块、优化 rootfs 和 init 脚本、用 initramfs、开启动画掩盖。测量用 bootgraph、ftrace、systemd-analyze、串口打点,先定位时间花在哪。性能排障用 top/htop 看 CPU、free 看内存、iostat 看 IO、perf 采样热点、strace 看系统调用。网络用 tcpdump 抓包、Wireshark 分析重传/乱序/窗口,MTU 是链路最大传输单元,过大会分片。

稳定性验证:改驱动后要做功能回归、压力测试(长时间读写、热插拔、并发)、异常注入(拔线、断电、超时)、日志与 crash 分析,并在目标硬件和内核版本上验证;保留可回滚版本。