面灵AI→

影石嵌入式一二面: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. 介绍一个你在项目中遇到的难题,具体怎么定位和解决的?

二面

  1. Linux 的 platform 总线、设备和驱动之间是什么关系?设备是怎么和驱动匹配的?
  2. 设备树是为了解决什么问题?为什么要把硬件描述从驱动代码中分离出来?
  3. DTS 和 DTSI 有什么区别?设备树节点的属性覆盖规则是什么?
  4. 在一个新的开发板上接入 I2C 传感器,从设备树到驱动注册,大致需要做哪些工作?
  5. Linux 字符设备驱动的基本框架是什么?cdev 结构体起什么作用?
  6. 用户程序打开 /dev 下的设备节点时,最终怎么调用到驱动中的 open 函数?
  7. file_operations 结构体有哪些常见成员?read、write、ioctl 分别负责什么?
  8. 为什么要注册设备节点?用户态访问设备为什么不直接操作硬件寄存器?
  9. 驱动需要使用 GPIO 或中断资源时,一般通过什么内核接口申请?为什么不能直接随意占用这些资源?
  10. Linux 中断处理流程是什么?为什么要区分上半部和下半部?
  11. 中断下半部能不能睡眠?tasklet 和 workqueue 在使用场景上有什么不同?
  12. 自旋锁和互斥锁有什么区别?哪些上下文不允许使用可能睡眠的锁?
  13. U-Boot 的启动流程大致是什么?Linux 内核启动以后主要完成哪些初始化?
  14. Linux 内核有哪些主要子系统?你真正看过哪部分源码?
  15. Linux 程序发生内存异常,你会怎么排查?gdb、core dump、strace 分别有什么作用?
  16. 系统出现性能瓶颈,怎么判断问题在哪里?top、free、perf 这些工具实际怎么使用?
  17. 如果要优化嵌入式 Linux 的开机时间,你会从哪些地方入手?假设目标压到 5 秒,你怎么分析?
  18. 网络传输出现丢包、延迟或卡顿,怎么利用 tcpdump、Wireshark 等工具定位?
  19. TCP/IP 有哪些可调参数?MTU 是什么意思?
  20. 如果让你维护一个已经交付的设备驱动,怎么验证修改之后的稳定性?

《参考解析》

I2C 总线被拉低锁死,是嵌入式 Linux 里最经典的一道题,要答出「谁拉低、怎么复位」。 I2C 是开漏 + 上拉,SDA 被拉低说明某个从机正在输出 0(通常是它的状态机被中断在半途——主机在传输中被复位或重启,从机却在等后续时钟)。标准处理是「手工补时钟」:把 SCL 切成 GPIO 输出,连续打 9 个以上时钟脉冲(每打一个检查 SDA 是否恢复高),让从机把剩余的位收发完、状态机回到空闲;然后补一个 STOP 条件,再把引脚切回 I2C 功能、重新初始化控制器。原因排查要看:是否有热插拔导致从机掉电重启、主机侧是否有硬复位从机的机制、上拉电阻是否过大导致上升沿过慢被判错、以及多主场景下仲裁失败。

SPI 的四种模式,本质是「空闲电平 + 采样边沿」两个参数。 CPOL 决定时钟空闲时的电平(0 为低、1 为高),CPHA 决定数据在第一个还是第二个边沿采样(0 为第一个边沿采样、在第一个边沿之前就需把数据准备好;1 为第二个边沿采样)。组合起来就是 Mode 0~3,最常用 Mode 0(CPOL=0, CPHA=0)与 Mode 3(CPOL=1, CPHA=1)。调试出错时先确认两端模式一致,再看片选时序(CS 拉低到第一个时钟的建立时间)、时钟频率是否超过从机上限、以及 MISO 是否被多个从机同时驱动(多从机必须独立 CS,否则总线冲突)。

platform 总线与设备树,是现代 Linux 驱动的骨架。 platform 是「虚拟总线」,用于那些不挂在物理总线(PCI/USB)上的片内外设;platform_device 描述硬件资源(寄存器基址、中断号、时钟、GPIO),platform_driver 提供 probe/remove 和 of_match_table。匹配的路径有两条:老式靠 device.name == driver.name,新式靠设备树 compatible 字符串与 of_device_id 表匹配;匹配成功后内核调用 probe,驱动在里面做 ioremap、申请中断、注册字符设备或子系统。设备树的意义是把「板级硬件描述」从内核代码里搬出来:同一颗 SoC 配不同板子,只需要换 DTS,不用改驱动、也不用为每块板编一份内核。DTS 是板级描述、DTSI 是可复用的 SoC 级公共描述(DTS include DTSI),节点属性覆盖遵循「后加载的覆盖先加载的、同名节点合并、同名属性取后者」,/delete-node/ 与 status = "disabled" 用来关掉不需要的外设。

从 /dev 的 open 到驱动的 open,这条调用链要能一路说出来。 用户态 open("/dev/xxx") 进入内核的 do_sys_open → do_filp_open → path_openat,通过路径查到该 inode 对应的 cdev(字符设备在 cdev_add 时已把 dev_t 与 file_operations 绑定),内核据此把 file->f_op 指向驱动的 file_operations,然后调用 f_op->open()。所以驱动的基本框架是:申请设备号(alloc_chrdev_region 或静态指定)→ 初始化并 cdev_init/cdev_add → 创建类与设备节点(class_create + device_create,udev/mdev 据此自动建 /dev 节点)→ 实现 file_operations(open/release/read/write/ioctl/mmap/poll)。用户态不直接操作寄存器,是因为要经过 MMU 与权限检查、要有统一的内核资源仲裁(同一寄存器可能被多个驱动/子系统访问)、以及要提供稳定的抽象接口——让用户态随便读写物理地址等于把整个系统的稳定性交出去。read/write 适合流式数据,ioctl 适合带参数的控制命令(配置寄存器、启停采集),注意 ioctl 的命令号要用 _IOR/_IOW 宏生成以避免冲突。

中断上下半部与锁的选择,是「能不能睡眠」这一条线串起来的。 硬中断上下文不能睡眠(没有进程上下文、不能调度),所以上半部只做「清中断标志、读数据、发通知」,其余交给下半部。下半部里 tasklet 运行在软中断上下文,仍然不能睡眠,适合短小的收尾;workqueue 交给内核工作线程,处在进程上下文、可以睡眠,适合需要拿锁、访问文件、做 IO 的活。因此凡是「可能睡眠」的上下文(硬中断、软中断、持有自旋锁、禁用抢占)都不能用互斥锁或 kmalloc(GFP_KERNEL);自旋锁用于极短的临界区且不能睡眠,互斥锁用于可能长时间持有、允许睡眠的路径。

「开机时间压到 5 秒」这题考的是分析框架,不是清单。 先测量再优化:用 bootgraph/ftrace 的 initcall_debug 打出每个 initcall 的耗时、用 GPIO 打点或 printk 时间戳划出 U-Boot、内核解压、内核初始化、挂载根文件系统、init 启动服务、应用启动各占多少。常见的大头依次是:U-Boot 的延时与自检(去掉 bootdelay、关掉不必要的命令与外设初始化)、内核解压与镜像加载(换更快的存储、用未压缩内核或 LZ4)、内核初始化(关掉不用的驱动与调试选项、把非关键驱动改成模块并延迟加载)、根文件系统(用 squashfs/erofs 代替 ext4、减少 udev 规则)、用户态(systemd 并行化、砍掉不必要服务、关键服务先起)。要主动指出「并行化」和「延迟初始化」是两把主要杠杆:能并行的 initcall 改异步、能后置的(如 UI、日志上传)等关键路径起来后再做。最后一定要说验收方式:连续冷启动 N 次取 P95,而不只看一次。

排查与验证的通用答法。 内存异常:先看 dmesg 的 Oops/段错误地址,用 gdb 加载 core dump 看调用栈与寄存器,strace 看系统调用的返回值(ENOMEM、EFAULT),再用 valgrind/ASan 抓越界与 UAF。性能:top 看 CPU 占用与 load、free 看内存与 cache/buffer、perf top 看热点函数、iostat 看 IO、pidstat 看上下文切换与缺页。网络:tcpdump 抓包后用 Wireshark 看重传、乱序、零窗口与丢包位置,MTU 不匹配会表现为大包丢失、小包正常。驱动改完后的稳定性验证,要说清「跑多久、跑什么场景、怎么判定通过」:压力场景(满负载采集 + 并发读写 + 反复加载卸载)、异常场景(热插拔、断电、超时注入)、以及长时间老化测试(72 小时)配合内存与句柄泄漏监控,而不是「跑了一会儿没问题」。