驱动第三阶段

一、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 同步传输。

实验现象:

回环测试:

相关推荐
2035去旅行11 小时前
bootcmd 详细讲解
linux·嵌入式·arm·soc
陈希12311 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡12 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
且从容.15 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道16 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.19 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落20 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨20 小时前
WSL2的安装与使用
linux·wsl
微小的鱼20 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng21 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器