影石 嵌入式软件开发一面二面:C 语言细节、I2C 时序与 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 的时序与上拉:起始是 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 分片。维护已交付的驱动,靠回归用例加长时间压力测试、异常注入和日志观察。