一、i2c子系统
底层:I2C 控制器驱动(SOC 总线驱动)。
由芯片厂商实现,直接操作 I2C 硬件寄存器输出时序。
中间层:i2c core I2C 核心层
承上启下,定义一套统一标准接口:强制底层控制器驱动实现规定接口;给上层从设备驱动提供统一 API,代表就是i2c_transfer();屏蔽不同 SOC 控制器硬件差异;上层代码不用关心底层是哪款 SOC 的 I2C 控制器。
上层:I2C 从设备驱动(client 芯片驱动)
只关心外设芯片逻辑,组装消息,调用i2c_transfer()把数据交给 i2c core;由 core 再向下调用对应控制器驱动完成硬件收发;从设备驱动还可以导出接口给用户应用程序使用。


2.IIC总线的数据传输格式

3.原理图

4.引脚及通信方式

从机地址:

时序:




I2C_5对应第5个基地址(从0开始数)

查阅芯片厂商写的I2C_5设备树

反编译设备树


对比i2c@13860000和i2c@138B0000可以看到缺乏的设备树属性

总线匹配从设备的流程:
DTS:i2c 控制器节点下写子节点(外设)
i2c 控制器驱动加载,调用i2c_add_adapter()注册i2c_adapter
i2c‑core 扫描该 adapter 下的设备树子节点,解析of_node,直接生成 i2c_client
将i2c_client(设备)挂到 I2C 总线上
I2C 总线执行匹配:拿 client 的compatible,对比i2c_driver里面of_match_table
匹配成功,调用 i2c_driver 的probe(struct i2c_client *client)
、
先通过 I2C 地址 0x68 选中芯片 → 再向芯片发送寄存器地址 0x75 → 读取返回值,如果是 0x68 就说明芯片通信正常、身份匹配。


实验现象:


i2cdetect 是用户态工具,它依赖内核导出字符设备 /dev/i2c‑*;而生成这个设备节点,就需要打开 I2C device interface。
就算 I2C 硬件驱动完好,关掉这个选项,/dev 下没有节点,i2cdetect 直接报错无法运行。
二、IIO子系统



起始地址为0x126c0000,大小为0x20 (32字节)
内核自带驱动drivers/iio/adc/exynos_adc.c
probe函数中获取了两次资源,第一个获取的是ADC控制器基址,第二个是ADC PHY 使能寄存器

ADC PHY 是共享的模拟前端,必须先打开电源,否则两个 ADC 都读不到东西 找到ADC_PHY_CONTROL地址,0x1002_0000 + 0x0718 = 0x1002_0718


ADC_PHY_CONTROL 就一个 32 位寄存器,只需 4位。 所以reg = <0x126c0000 0x20>,<0x10020718 0x4>;
通过查找可得ADC中断需要的2个32位数字表示


第一个是组号,第二个是组内的中断号


还需要获取时钟





找到adc的时钟

获取设备电源(regulator 电源管理子系统)
devm_regulator_get 的默认行为是:如果设备树里没找到 vdd-supply 属性,内核不会报错,而是会返回一个"虚拟调节器(Dummy Regulator)"。这个虚拟调节器永远处于"已使能"状态,调用 regulator_enable 对它没有任何实际作用,但也不会返回错误。所以vdd-supply属性可写也可不写


查看原理图找adc供电引脚,看它连到 PMIC 的哪一路




regulator-always-on:内核启动后这个 LDO 永远打开,不能关闭;
min == max:固定电压,不允许调压;

在这填充 了indio_dev 字段,并将注册到 IIO,告诉用户空间可以通过 /sys/bus/iio/devices到它
上层调用read_raw函数就会回调到exynos_read_raw函数
读数据找 exynos_read_raw; 调试寄存器找 exynos_adc_reg_access; 加一把锁(引用计数)防止工作到一半被赶走。

只读原始数据并加锁,保证同一时刻只有一个采样在进行

阻塞等待,挂起当前进程,等 ISR 的 complete() 唤醒,或 1 秒超时。

等待队列 = 一个"挂号处"(链表)。进程 read() 没数据时,不是死等,而是主动把自己挂到挂号处睡觉(wait_event)。等有数据时,驱动通过 wake_up 叫号,唤醒挂号处上的进程继续执行。这样 CPU 不会空转,系统效率最高。(条件不满足 → 进程休眠让出 CPU;条件就绪后,别的代码唤醒进程,继续往下执行。)

[ 用户空间 ]
| cat / cat in_voltage0_raw 或 ./newadc-app
↓
[ VFS 虚拟文件系统 ]
|
| 根据文件节点找到对应的 IIO 设备
↓
[ IIO 核心框架 (drivers/iio/industrialio-core.c) ]
|
| 调用 indio_dev->info->read_raw(函数指针跳转)
↓
[ exynos_read_raw ]
|
| 1. mutex_lock(防竞态)
| 2. 写寄存器选通道(chan->address)
| 3. 写寄存器启动转换
| 4. 轮询等待 EOC 标志
| 5. 读数据寄存器得到 raw 值
| 6. mutex_unlock
↓
[ 返回 raw 值给 IIO 核心 ]
↓
[ 返回给用户空间 App ]
↓
[ 终端打印:adc_raw = 4095 / voltage =1.8000 V ]

三、并发和竞态
并发:通过算法将CPU使用权合理分配给多个任务
并行:多核处理器的每个核心都可以独立执行任务
竞态:并发可能造成多个程序同时访问一个共享资源,同时访问一个共享资源产生的问题就是竞争。
为什么需要并发的原因:如果系统只是单线程顺序执行,CPU会浪费大量时间在等待上。引入并发,本质是为了提高硬件利用率和系统响应速度。充分利用多核CPU(性能)、屏蔽I/O延迟(吞吐量)、提高用户交互体验(响应性)、处理硬件突发(中断机制)。
为什么一定会产生竞态:竞态产生的根源在于:"资源是独占的,但访问是并发的"。操作的非原子性、硬件资源的排他性、内核的可抢占性(当前进程正在操作一个链表,时间片刚到被高优先级进程抢占,高优先级进程也去操作这个链表,链表指针就会损坏)
处理竟态的方法:
1.互斥锁:互斥锁( Mutual Exclusion Lock ) 是多线程编程中用于保护共享资源的一种机制, 确保同一时间只有一个线程进 入临界区, 访问临界资源。
| 函数声明 | 功能说明 | 调用位置 |
|---|---|---|
void mutex_init(struct mutex *lock) |
初始化互斥锁 | 通常在驱动的 probe 函数中调用 |
void mutex_lock(struct mutex *lock) |
获取互斥锁,若锁已被占用则进入睡眠等待 | 通常在 open 函数中调用 |
void mutex_unlock(struct mutex *lock) |
释放互斥锁,唤醒等待的进程 | 通常在 release 函数中调用 |
2.自旋锁 :自旋锁 ( spin lock ) 也是为了保护共享资源的一种锁机制, 是一种非阻塞锁,如果线程获取自旋锁,但锁已经被其他线程占用,该线程不会挂起,而是消耗 CPU 的时间,不停尝试获取锁。
| 函数声明 | 功能说明 |
|---|---|
spin_lock_init(spinlock_t *lock) |
初始化自旋锁,通常在驱动的 probe 函数中调用 |
void spin_lock(spinlock_t *lock) |
获取自旋锁,若锁已被占用则原地忙等(自旋),直到获取成功 |
void spin_unlock(spinlock_t *lock) |
释放自旋锁,允许其他进程 / CPU 获取锁 |
3.信号量 :本质上是一个全局变量,信号量的值表示控制访问资源的线程数,当线程访问共享资源时,信号量执行"减一"操作,访问完成后再执行"加一"操作
| 函数声明 | 功能说明 |
|---|---|
void sema_init(struct semaphore *sem, int val) |
初始化信号量并设置初始计数值 |
int down_interruptible(struct semaphore *sem) |
获取信号量(计数值减 1),若信号量不可用则进入睡眠等待,且等待可被信号打断 |
void up(struct semaphore *sem) |
释放信号量(计数值加 1)并唤醒一个等待者 |
4.原子操作 :只能做一些简单的增减 将操作变为一个整体,中间不会有上下文切换无法被打断。
5.那锁、信号量、原子操作分别解决什么问题?
| 机制 | 本质 | 阻塞? | 适用场景 |
|---|---|---|---|
| 原子操作 | 硬件 CPU 指令,单条指令完成读改写,不会被打断 | 不阻塞 | 简单整数变量加减、位操作;临界区必须很短,不能有函数调用 |
| 自旋锁 | 忙等待循环;拿不到锁就原地 CPU 空转 | 自旋(占 CPU) | SMP 多核,临界区短小;不能睡眠,中断上下文可以用 |
| 互斥锁 (mutex) | 拿不到锁,线程休眠让出 CPU;被唤醒后继续执行 | 会阻塞睡眠 | 进程上下文,临界区可以大,可以调用 sleep;不能用在中断上下文 |
| 信号量 (semaphore) | 计数器版本互斥锁;mutex 是信号量 count=1 特例 | 会阻塞睡眠 | 允许 N 个线程同时进入临界区;进程上下文;可睡眠 |
临界区很短、不能睡眠(中断、原子上下文) → 原子操作 / 自旋锁
临界区长、可以睡眠,只允许一个线程访问 → mutex 互斥锁
允许多个线程同时访问资源(有限并发) → 信号量
不是系统只有一把锁,锁是对象,可以创建很多把;不同共享资源用不同锁。
锁保护的不是线程,锁保护共享数据:哪个数据要防竞态,就给它套一把锁。
四、GPIO子系统


首先打开内核中Device Drivers-> GPIO Support-> /sys/class/gpio/... (sysfs interface)

找到片选引脚


/SPI_2_nSS

| 复用功能 | 含义 | 场景 |
|---|---|---|
| Xi2s2LRCK | I2S2 左右声道时钟 | 音频 I2S,帧同步 LRCK |
| PCM_2_FSYNC | PCM2 帧同步信号 | PCM 音频接口帧同步 |
| SPI_2_nSS | SPI2 硬件片选(低有效) | 当前 MCP2515 CS 就用这个功能,SPI2 的硬件 nSS 片选脚 |
| GPC1_2 | 普通 GPIO 输入输出 | 可以当做普通 GPIO,软件模拟 SPI 片选 |
# 挨个打印每个gpiochip的 label 和 base
for chip in /sys/class/gpio/gpiochip*; do
echo "$chip label: $(cat $chip/label) base: $(cat $chip/base)"
done

# Step 1:查 base(已完成,从上面截图确认 gpc1 base=27)
# Step 2: export 导出
echo 29 > /sys/class/gpio/export
# Step 3: 设为输出
echo out > /sys/class/gpio/gpio29/direction
# Step 4: 拉低 / 拉高
echo 0 > /sys/class/gpio/gpio29/value # CS 有效 →选中 MCP2515
echo 1 > /sys/class/gpio/gpio29/value # CS 释放
# Step 5: 清理
echo 29 > /sys/class/gpio/unexport

五、spi子系统



Exynos4412 硬件 SPI 控制器驱动





在**Documentation/devicetree/bindings/pinctrl/samsung-pinctrl.txt中看到**

其中flags参数如何写呢

flags 出参传了 NULL,所以设备树里写的 flags(0 或 1)被直接忽略了,驱动只拿到 GPIO 编号。

CS 有效(选通从设备):SPI_CS_HIGH=0 时 → 输出 0 (低电平选中)

CS 无效(释放从设备):SPI_CS_HIGH=0 时 → 输出 1 (高电平释放)

驱动行为取决于是否在设备树中加了 spi-cs-high 属性:
| DTS 属性 | spi‑>mode | CS 选通电平 |
|---|---|---|
| 不写 spi‑cs‑high | SPI_CS_HIGH=0 | CS=0 选通 |
| 写 spi‑cs‑high | SPI_CS_HIGH=1 | CS=1 选通 |
所以cs-gpio 写 0 还是 1 实际运行效果完全一样。 按惯例写 0 即可,即cs-gpio = <gpc1 2 0>
读总线核心代码选择走哪种片选方式:
不设置(默认):低电平有效,选中设备 = 拉低引脚
设置 SPI_CS_HIGH:高电平有效,选中设备 = 拉高引脚
通过翻转 enable 变量实现极性适配,所有 CS 控制都走这一个入口,保证极性全局统一


我们选的SPI2 的 CS 走 GPC1_2(GPIO) ,所以 DTS 里必须配 cs-gpio; + controller-data { cs-gpio } → 走软件片选分支
从当前 SPI 子设备节点(也就是 mcp2515)读取 reg 属性的值,存到 value。读取成功 → spi->chip_select = value;
把读到的数值,赋值给 struct spi_device 的 chip_select 成员,代表这个设备的片选编号。


SPI 子节点里的 reg 不是内存地址,是 SPI 总线上的片选序号所以 reg = <0> 就是数组下标,告诉内核"这个设备用 cs-gpios 数组里的第 0 个 GPIO"。在 SPI 设备树中,reg 属性不是寄存器地址,而是片选(CS)引脚编号。比如 reg = <0> 代表 CS0,reg = <1> 代表 CS1。
从设备通信速率最大 10MHz 。所以写spi-max-frequency = <10000000>;

驱动会在从设备子节点下查找名为 controller-data 的子节点,找不到就直接返回错误,SPI 设备初始化直接失败

在spi-bus.txt中

| 文档属性 | DTS controller-data{} 里的写法 |
含义 |
|---|---|---|
cs-gpio |
cs-gpio = <&gpc1 2 0>; |
指定 SPI 控制器用来做片选的 GPIO 引脚,由控制器驱动软件控制电平 |
samsung,spi-feedback-delay |
samsung,spi-feedback-delay = <0>; |
MISO 采样相位偏移,补偿走线信号延迟,取值 0~3 对应 0/90/180/270 度 |
它会检查cs-gpio,只要存在这个属性,就启用 cs-gpio 功能;不存在就禁用整个功能。



| 寄存器 | 地址 |
|---|---|
| CNF3 | 0x28 |
| CNF2 | 0x29 |
| CNF1 | 0x2A |
| CANINTE | 0x2B |
| CANINTF | 0x2C |
| CANSTAT | 0x0E |
| CANCTRL | 0x0F |


READ/WRITE/LOAD TX/RX BUF/RTS 每一张时序图,CS 都是整段传输保持低、结束拉高------这是最直观的"标准低有效"定义





MCP2515 上电 / RESET 后默认进入 Configuration 模式 ,所以 OPMOD 一定是 100 = 0x80。这是 reset 标准值

在 spi 子系统中在进行数据传输时,会将数据封装成 spi_message 结构体,形成消息队列再进行发送. spi_message 结构体定义如下:

一个 spi_message 对象可以由多个 spi_transfer 来构成,spi_transfer 的定义如下:

master 管总线队列;message 代表一次片选有效周期;transfer 代表一次次的数据收发。
在 spi 传输过程中,可以使用 同步方式 与 异步方式 :
同步方式 是指数据传输的发起者必须等待本次传输的结束,期间不能做其它事情,用代码来解释就是,调用传输的函数后,直到数据传输完成,函数才会返回。
异步方式则正好相反,数据传输的发起者无需等待传输的结束,数据传输期间还可以做其 它事情,用代码来解释就是,调用传输的函数后,函数会立刻返回而不用等待数据传输完成,我们只需设置一个回调函数,传输完成后,该回调函数会被调用以通知发起者数据传送已经完成。
在 Linux 内核 spi 子系统,可以调用 spi_sync() 函数用于 spi 同步传输。

实验现象:

回环测试:


