影石嵌入式驱动一二面:C 基础、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,还知道哪些流控信号?
- 介绍一个项目中遇到的难题,具体怎么定位和解决?
- 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 是什么?
- 维护已交付的设备驱动,怎么验证修改后的稳定性?
《参考解析》
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 分析,并在目标硬件和内核版本上验证;保留可回滚版本。