news 2026/8/20 23:11:58

# 嵌入式Linux驱动工程师面试 — **驱动总线子系统深挖面经(I2C / SPI / SDIO / 中断专题)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
# 嵌入式Linux驱动工程师面试 — **驱动总线子系统深挖面经(I2C / SPI / SDIO / 中断专题)

第一部分:I2C 子系统彻底拆解

1.1 I2C 子系统分层架构(必考)

┌─────────────────────────────────────────────────────────┐ │ 应用层 │ │ - 用户态调用:read/write/ioctl │ │ - 文件:/dev/i2c-0(用户态直接操作I2C总线) │ ├─────────────────────────────────────────────────────────┤ │ I2C 核心层(drivers/i2c/i2c-core.c) │ │ - 管理I2C总线、适配器、设备 │ │ - 提供总线/设备注册注销的API │ │ - 转发读写请求到适配器驱动(含总线锁) │ ├─────────────────────────────────────────────────────────┤ │ I2C 适配器层(drivers/i2c/busses/i2c-hi3516.c) │ │ - 硬件控制器驱动(如Hi3516的I2C控制器) │ │ - 实现 struct i2c_algorithm 的回调 │ │ - 负责实际收发时序(启动/停止/ACK/NACK) │ ├─────────────────────────────────────────────────────────┤ │ I2C 设备驱动层(你写的ICM-20602驱动) │ │ - 实现 struct i2c_driver │ │ - 只关心「发什么命令/读什么数据」,不关心硬件时序 │ └─────────────────────────────────────────────────────────┘

面试话术
> I2C子系统从下到上分为四层:设备驱动层(我写的ICM-20602驱动)、适配器层(Hi3516的I2C控制器驱动)、核心层(i2c-core,管理总线注册和设备添加)、应用层(用户态通过 /dev/i2c-* 访问)。我的驱动工作在设备驱动层,通过核心层的 API(i2c_transfer)向适配器层发起读写,适配器层再操作硬件完成实际时序。


1.2 关键结构体逐一详解(重点背)

① struct i2c_adapter(适配器 = 一个I2C控制器)
struct i2c_adapter { struct module *owner; // 所属模块 unsigned int class; // 适配器支持的功能类别 const struct i2c_algorithm *algo; // ← 传输算法回调(核心) int timeout; // 超时时间(jiffies) int retries; // 重试次数 int nr; // 总线编号(如0 → /dev/i2c-0) char name[48]; // 适配器名字("hi3516-i2c") struct i2c_bus_recovery_info *bus_recovery_info; // 总线恢复 ... };

algo的本质:

struct i2c_algorithm { // 核心传输函数(适配器驱动必须实现) int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); // 每条 i2c_msg 走一次 master_xfer;多个msg用 i2c_transfer 连续调用 int (*smbus_xfer)(...); // SMBus传输(简化版,可选) u32 (*functionality)(struct i2c_adapter *); // 该控制器支持的功能位 };
② struct i2c_msg(一次I2C传输的消息单位)
struct i2c_msg { __u16 addr; // 从设备地址(7bit 或 10bit) __u16 flags; // 标志:I2C_M_RD(读)、I2C_M_TEN(10位地址)等 __u16 len; // 数据长度 __u8 *buf; // 数据缓冲区 };

读操作写法(例:读WHO_AM_I,地址0x75)

struct i2c_msg msgs[2]; u8 reg = 0x75; // 要读的寄存器地址 u8 val = 0; // 读回的数据 msgs[0].addr = 0x68; // 设备地址 msgs[0].flags = 0; // 写 msgs[0].len = 1; msgs[0].buf = ® msgs[1].addr = 0x68; msgs[1].flags = I2C_M_RD; // 读 msgs[1].len = 1; msgs[1].buf = &val; i2c_transfer(client->adapter, msgs, 2); // 先写寄存器地址,再读数据 // 结果:val = WHO_AM_I(应为0x12)
③ struct i2c_client(I2C从设备实例)
struct i2c_client { unsigned short addr; // 设备地址(7bit) char name[I2C_NAME_SIZE]; // 设备名 struct i2c_adapter *adapter; // 它挂在哪个适配器上 struct device dev; // 内核设备模型(关联设备树) };
④ struct i2c_driver(I2C设备驱动)
struct i2c_driver { int (*probe)(struct i2c_client *client); // 匹配成功调用 void (*remove)(struct i2c_client *client); const struct i2c_device_id *id_table; // 老方式匹配表 const struct of_device_id *of_match_table; // 设备树匹配表(常用) };
⑤ 设备树匹配
// 设备树: i2c0 { icm20602: icm20602@68 { compatible = "invensense,icm20602"; // ← 用于of_match_table reg = <0x68>; // ← i2c地址 }; }; // 驱动: static const struct of_device_id icm20602_of_match[] = { { .compatible = "invensense,icm20602" }, {} }; MODULE_DEVICE_TABLE(of, icm20602_of_match); // 绑定流程: // 内核解析DTB → 发现i2c0上的设备@0x68 → 创建i2c_client // → 将 compatible 与驱动 of_match_table 比较 // → 匹配成功 → 调用 probe()

1.3 I2C 读写操作详解

// 写操作(寄存器地址+数据) static int icm20602_write_reg(struct i2c_client *client, u8 reg, u8 value) { u8 buf[2] = {reg, value}; struct i2c_msg msg = { .addr = client->addr, .flags = 0, // 写 .buf = buf, .len = 2, }; int ret = i2c_transfer(client->adapter, &msg, 1); return (ret == 1) ? 0 : -EIO; } // 辅助API(常用封装,省去手动组装msg) i2c_smbus_write_byte_data(client, reg, val); // 写1字节 i2c_smbus_read_byte_data(client, reg); // 读1字节 i2c_master_recv(client, buf, len); // 连续读 len 字节 i2c_master_send(client, buf, len); // 连续写 len 字节

注意i2c_smbus_*是SMBus简化协议,很多设备支持;但有些严格的设备(如部分sensor)需要标准i2c_transfer


1.4 陀螺仪 I2C 无中断 vs 有中断(重点)

① 无中断版(轮询方式)
工作原理: 应用/线程循环调用读函数 → 每读一次,I2C控制器发起一次完整事务 代码模式: while (1) { icm20602_read_all(client, &data); // 同步阻塞读取 process(data); usleep(2000); // 按需频率轮询(如500Hz → 2ms) } 特点: ✅ 实现简单;稳定(无中断丢失) ❌ 浪费CPU;数据时序不确定(可能读到旧值) ❌ 6轴不一致(读一半sensor更新) ❌ 总线频繁占用 适用:低频采样、实时性要求不高
② 有中断版(DRDY中断触发)
工作原理: ICM-20602的INT/DRDY引脚,每有新数据 → 引脚脉冲(下降沿) → 接GPIO → 触发中断 → 中断中发起I2C传输读取「最新数据」 → 保证每次读都是最新且6轴时序一致 实现方式两种: 方案A:在ISR(上半部)中直接I2C传输 —— ❌ 不推荐,I2C可能睡眠 方案B:中断标记 + threaded irq / workqueue —— ✅ 推荐
③ 有中断的标准实现(threaded irq)
// probe中申请中断: // 设备树:interrupt-parent = <&gpio2>; interrupts = <3 IRQ_TYPE_EDGE_FALLING>; static int icm20602_probe(struct i2c_client *client) { ret = devm_request_threaded_irq( &client->dev, client->irq, // I2C client自动从设备树获取IRQ NULL, // 上半部:NULL = 全交给线程 icm20602_irq_thread, // 下半部:线程化中断处理 IRQF_TRIGGER_FALLING, // 下降沿触发 "icm20602", data); } // 线程化中断处理(可以sleep、可以I2C传输): static irqreturn_t icm20602_irq_thread(int irq, void *dev_id) { struct icm20602_data *data = dev_id; icm20602_read_all(data->client, &data->raw); // 可以在进程上下文做I2C // 处理:更新共享数据 → 唤醒应用 return IRQ_HANDLED; }
④ 有中断 vs 无中断 对比总结(面试直接背诵)
维度无中断(轮询)有中断(DRDY)
CPU占用
数据实时性可能读旧值保证最新
6轴一致性可能不一致数据就绪后读,一致
功耗
实现复杂度
适用场景低频<10Hz高频采样(防抖需400Hz+)

面试加分话术
> 防抖需要高频(400Hz左右)且实时性强的采样,所以用中断方案。ICM-20602有DRDY引脚,每有新数据触发一次中断。我在中断中使用threaded irq(线程化中断),因为I2C传输可能睡眠,不能放在原子上下文(普通ISR)里。这样既保证数据最新,又不阻塞其他系统任务。


1.5 I2C 常见问题与排查(必背)

常见问题1:I2C读写失败,返回 -EIO / -EREMOTEIO
排查: ① 地址:设备树reg与i2cdetect是否一致?7bit vs 8bit(左移一位是常见坑) ② 总线号:设备树挂的i2c0,驱动client->adapter是否为i2c-0? ③ 上电/复位:读WHO_AM_I失败→设备没上电/复位被拉低/电源时序错 ④ 速率:降到100kHz排除速率问题
常见问题2:I2C总线锁死(SDA被拉低)
原因:从设备死机、传输被中断干扰导致状态机错乱 处理: ① 软件恢复:i2c_recover_bus() ② 硬件:断开设备电源,重新上电复位 ③ 优化:I2C传输中避免被高优先级中断打断(用irqsave锁)
常见问题3:I2C多设备共享总线
- 地址必须唯一(每个设备地址不能相同) - 同一总线多设备访问必须串行(i2c-core内部有mutex锁) - 速率取所有设备支持的最小值 面试常问:为什么不会冲突? → i2c-core 中 i2c_adapter 有锁(mutex),每次 i2c_transfer 都加锁, 保证同一时刻只有一个设备在访问总线。

第二部分:SPI 子系统彻底拆解

2.1 SPI 物理层与协议(对比I2C)

SPI 四线: ┌─────────┐ ┌─────────┐ │ Master │── SCK (时钟) ──────────│ Slave │ │ (SoC) │── MOSI (主→从) ────────│ 设备 │ │ │── MISO (从→主) ────────│ │ │ │── CS# (片选,低有效) ──│ │ └─────────┘ └─────────┘ 特点(对比I2C): 全双工(MISO/MOSI同时) 无应答(Slave不回ACK) 片选单独控制 → 多设备需多CS 无地址概念 → 物理连接决定 最高可达几十MHz

2.2 SPI 分层架构(与I2C对应)

┌─────────────────────────────────────────────┐ │ 应用层:/dev/spidevX.Y │ ├─────────────────────────────────────────────┤ │ SPI核心层(drivers/spi/spi.c) │ │ - 管理SPI主机/设备 │ │ - 提供 spi_sync / spi_async │ │ - 消息队列调度 spi_message │ ├─────────────────────────────────────────────┤ │ SPI控制器层(drivers/spi/spi-hisi-sfc.c) │ │ - 实现 struct spi_master │ │ - 硬件传输(可能带DMA) │ ├─────────────────────────────────────────────┤ │ SPI设备驱动层(你的驱动) │ │ - struct spi_driver │ │ - 通过 spi_transfer 描述传输 │ └─────────────────────────────────────────────┘

2.3 SPI 关键结构体

① struct spi_master(控制器)
struct spi_master { struct device dev; s16 bus_num; // 总线编号 u32 mode_bits; // 支持的SPI_MODE_0~3 u32 min_speed_hz; u32 max_speed_hz; int (*transfer)(struct spi_device *spi, struct spi_message *mesg); // 核心 };
② struct spi_device(从设备)
struct spi_device { struct spi_master *master; u32 max_speed_hz; // 速率(设备树指定) u8 chip_select; // CS片选 u8 mode; // SPI模式(CPOL/CPHA) u8 bits_per_word;
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/20 23:11:42

C++从基础语法到递归、重载与宏定义

题目及题解1 判定素数编写程序&#xff0c;用一个函数判定输入的某个数是否为素数。12345678910111213141516171819#include <iostream>#include <cmath>// 判定素数函数bool isPrime(int n) {if (n < 2) return false;for (int i 2; i < sqrt(n); i) {if (…

作者头像 李华
网站建设 2026/8/20 23:11:18

Java大厂面试核心:Spring Boot与微服务架构深度解析

1. 互联网大厂Java技术栈面试全景解析最近三年Java技术岗的竞争态势愈发激烈&#xff0c;尤其是头部互联网公司的岗位&#xff0c;经常出现数百人竞争一个职位的场景。根据我作为面试官参与过的近百场技术面试经验&#xff0c;候选人普遍存在技术栈理解碎片化、项目经验与理论脱…

作者头像 李华
网站建设 2026/8/20 23:10:14

新能源汽车补贴政策下的行业生态与后补贴时代竞争力分析

1. 从“补贴”到“生态”&#xff1a;一个行业的明暗面干了这么多年汽车行业&#xff0c;从传统燃油车到新能源&#xff0c;我亲眼见证了一个时代的更迭。最近几年&#xff0c;关于新能源汽车的各种“潜规则”和“骗补”传闻&#xff0c;在圈内圈外都传得沸沸扬扬。很多人一听“…

作者头像 李华
网站建设 2026/8/20 23:09:17

【iOS】 Alamofire + Moya + SwiftData 完整教程

这三个框架是构建现代 iOS 应用的数据层的经典组合&#xff0c;它们各司其职&#xff0c;形成了一个清晰的网络请求 → 数据抽象 → 本地持久化的架构闭环。 架构概览&#xff1a;三者的角色定位 Alamofire (网络地基): 基于URLSession的 Swift 网络库&#xff0c;负责执行实际…

作者头像 李华
网站建设 2026/8/20 23:08:08

新能源汽车技术全解析:从三电系统到智能驾驶的演进与应用

1. 从“四个轮子加沙发”到“四个轮子加电脑”&#xff1a;我们到底在谈论什么&#xff1f;如果你在十年前问一个路人“什么是汽车”&#xff0c;他大概率会告诉你&#xff0c;那是一台烧汽油或柴油、能带着人跑的机器。但今天&#xff0c;当“新能源汽车”这个词铺天盖地而来时…

作者头像 李华