面灵AI→

影石 嵌入式软件开发一面二面:C 语言细节、I2C 时序与 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. Linux 的 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 等工具定位?
  37. TCP/IP 有哪些可调参数?MTU 是什么意思?
  38. 如果让你维护一个已经交付的设备驱动,怎么验证修改之后的稳定性?

《参考解析》

I2C 的时序与上拉:起始是 SCL 高时拉低 SDA,停止是 SCL 高时释放 SDA;每字节 8 位后第 9 个时钟,接收方拉低 SDA 表示 ACK。上拉电阻是因为总线开漏:器件只能主动拉低,高电平靠上拉回上去,所以空闲时自然是高电平。阻值在上升时间与功耗之间取舍,速度快、容性大就要小电阻。7 位地址理论 128 个,扣掉保留地址少一些,真正的限制是总线电容(几百 pF 量级)与地址冲突。速度模式常见 100k、400k、1M、3.4M。

NACK 与总线锁死:NACK 先分方向:地址阶段说明设备没应答——地址错、没上电、复位没释放、上拉缺失;数据阶段多是器件忙或写保护。排查顺序是 i2cdetect 扫地址、示波器看波形电平、查供电与复位、查上拉与总线电容、核对时序是否超规格。总线一直被拉低,通常是传输中途主机异常复位、从机还在等时钟;恢复办法是把主机切到 GPIO 模拟时钟,最多补 9 个 SCL 让它把字节发完,再发 STOP。产品里应该把「超时 → 总线恢复」做进驱动。

C 语言里的几个细节:数组名不是指针,sizeof 在数组上是整体大小、传参后退化成指针;typedef 是类型别名、受作用域约束、能被编译器检查,#define 是文本替换,括号和副作用要自己防。union 共享存储,做协议多视图时要注意对齐与别名规则。内存分布是 .text 放代码与常量、.data 放已初始化的全局与静态、.bss 放未初始化或零初始化,堆向上、栈向下。野指针是没初始化、指向随机地址,悬空指针是所指对象已释放;防法是定义即初始化、释放即置空、所有权写清楚。

platform 总线与设备树:platform 是虚拟总线,把 SoC 内部不挂物理总线的控制器(I2C、GPIO、UART)抽象成 device 与 driver,靠 compatible 或 id_table 匹配,probe 时从设备树取寄存器地址、中断号、时钟、GPIO。设备树的动机是「一份驱动跑很多板子」:把硬件差异从 C 代码挪到 DTS。DTS 是板级,DTSI 是被 include 的公共部分(通常 SoC 级),合并时同名属性后者覆盖前者,板级文件只写差异。

字符设备与 open 的路径:框架是申请设备号 → cdev_init 绑定 file_operations → cdev_add 注册 → class_create 与 device_create 让 udev 生成节点;cdev 就是把设备号区间与操作函数表连起来的抽象。用户 open(“/dev/xxx”) 先进 VFS,按 inode 的设备号找到 cdev 取出函数表建 file,最终调到驱动的 open。用户态不能直接操作寄存器,是因为地址空间隔离、权限和并发都不受控,映射与仲裁必须由内核做。

中断上下半部与锁:上半部在中断上下文,只做应答中断、取数据这类紧急且短的事;下半部推迟重活。能否睡眠取决于载体:softirq 与 tasklet 在中断上下文跑,不能睡眠;workqueue 由内核线程执行,可以睡眠。tasklet 适合同一 tasklet 不并发的短任务,workqueue 适合可能阻塞的耗时工作。自旋锁忙等、可在中断上下文用(配 irqsave),临界区必须极短;互斥锁会睡眠,只能用在进程上下文。

开机压到 5 秒怎么分析:先测量再优化。用 bootloader 打点、内核 initcall_debug、用户态打点把时间轴切成 bootloader、内核初始化与驱动 probe、用户态服务四段,看谁占大头。bootloader 侧跳过不必要的自检、换解压更快的算法;内核侧裁配置、把非启动必需驱动改成模块或延迟 probe、精简 initramfs、关掉多余 console;用户态并行启动、砍无用服务、把非关键服务后置。真正区分有没有做过的是判断哪些必须在首帧前完成(显示、存储、触摸),哪些可以推迟。

工具怎么选:崩溃先开 core dump,用 gdb 看调用栈与寄存器;不确定卡在哪个系统调用用 strace;内存越界用 ASan。性能先 top 看 CPU 分布、free 看内存压力、perf 看热点与上下文切换。网络用 tcpdump 抓包、Wireshark 看重传乱序窗口,先排除物理层错包与 MTU 分片。维护已交付的驱动,靠回归用例加长时间压力测试、异常注入和日志观察。