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