驱动第三阶段

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

实验现象:

回环测试:

相关推荐
fengyehongWorld2 小时前
linux Capability中的CAP_NET_BIND_SERVICE
linux·运维·服务器
Gl�ria2 小时前
Linux 查看日志常用命令
java·linux·servlet
handler012 小时前
【Linux】动态库、静态库使用与原理
linux·运维·服务器·c++·c·动态库·静态库
MSTcheng.2 小时前
【Linux学习】Linux学习第四弹——Linux基本指令3
linux·windows·学习·操作系统·指令
筝筝ba2 小时前
OpenBMC 自动化测试中的多进程陷阱:从 Execute Process 返回异常到 output.xml 损坏问题分析
xml·linux·运维·服务器·网络
饕餮争锋2 小时前
PATH路径列表介绍
java·linux·服务器
V158897262012 小时前
从滴滴模式看机器人租赁:撮合型租赁平台开发源码的调度系统设计思路
linux·前端·机器人
代码村新手3 小时前
Linux-vim的使用和配置
linux
吴声子夜歌3 小时前
编写Shell脚本——启动项目
linux·运维·shell