
一、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设备树反编译设备树对比i2c13860000和i2c138B0000可以看到缺乏的设备树属性总线匹配从设备的流程1. platform 总线扫 DTS发现 hsi2c 控制器节点└─ compatible 匹配到内核自带控制器驱动 i2c-exynos5.c└─ 控制器驱动 probe()2. 控制器驱动 probe 里调 i2c_add_numbered_adapter()└─ 注册 i2c_adapter分配 bus number如 i2c-53. i2c_add_numbered_adapter() 内部调 of_i2c_register_devices()└─内核i2c 扫该 adapter 对应 DTS 节点下的所有子节点└─ 对每个子节点mpu605068调 i2c_new_device()└─ 创建 i2c_clientreg0x68 → client-addr└─ device_register() ← 注册从设备 触发匹配一体└─ i2c_device_match() 比对 compatible└─ 匹配成功 → mpu6050_probe()platform 总线是虚拟兜底总线挂的是没有真实总线的设备各类控制器 LED/按键/ADC 等直接外设i2c/spi 这类真实协议总线是控制器延伸出来的物理线上面挂的才是从设备 从设备驱动i2c_client/i2c_driver、spi_device/spi_driver。先通过 I2C 地址 0x68 选中芯片 → 再向芯片发送寄存器地址 0x75 → 读取返回值如果是 0x68 就说明芯片通信正常、身份匹配。实验现象i2cdetect 是用户态工具它依赖内核导出字符设备 /dev/i2c‑*而生成这个设备节点就需要打开 I2C device interface。就算 I2C 硬件驱动完好关掉这个选项/dev 下没有节点i2cdetect 直接报错无法运行。二、IIO子系统起始地址为0x126c0000,大小为0x20 (32字节)内核自带驱动drivers/iio/adc/exynos_adc.cprobe函数中获取了两次资源第一个获取的是ADC控制器基址第二个是ADC PHY 使能寄存器ADC PHY 是共享的模拟前端必须先打开电源否则两个 ADC 都读不到东西 找到ADC_PHY_CONTROL地址0x1002_0000 0x0718 0x1002_0718ADC_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条件就绪后别的代码唤醒进程继续往下执行。)使用等待队列实现阻塞 IO先定义并初始化 wait_queue_head_t在 read 函数中使用 wait_event 判断条件条件不满足则进程休眠在中断或者别的内核上下文事件就绪后调用 wake_up 唤醒进程唤醒后再次判断条件条件成立继续执行。阻塞 IO用户态调用 read没有数据的时候进程卡在 read 系统调用进程休眠不占用 CPU直到有数据就绪唤醒。等待队列内核用来存放等待某个事件条件的进程的链表。典型场景按键驱动、串口接收没有数据时 read 阻塞中断收到数据后 wake_up 唤醒等待的读进程。[ 用户空间 ] | 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 空转自旋占 CPUSMP 多核临界区短小不能睡眠中断上下文可以用互斥锁 (mutex)拿不到锁线程休眠让出 CPU被唤醒后继续执行会阻塞睡眠进程上下文临界区可以大可以调用 sleep不能用在中断上下文信号量 (semaphore)计数器版本互斥锁mutex 是信号量 count1 特例会阻塞睡眠允许 N 个线程同时进入临界区进程上下文可睡眠临界区很短、不能睡眠中断、原子上下文 → 原子操作 / 自旋锁临界区长、可以睡眠只允许一个线程访问 → mutex 互斥锁允许多个线程同时访问资源有限并发 → 信号量不是系统只有一把锁锁是对象可以创建很多把不同共享资源用不同锁。锁保护的不是线程锁保护共享数据哪个数据要防竞态就给它套一把锁。四、GPIO子系统首先打开内核中Device Drivers- GPIO Support- /sys/class/gpio/… (sysfs interface)找到片选引脚/SPI_2_nSS复用功能含义场景Xi2s2LRCKI2S2 左右声道时钟音频 I2S帧同步 LRCKPCM_2_FSYNCPCM2 帧同步信号PCM 音频接口帧同步SPI_2_nSSSPI2 硬件片选低有效当前 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 base27 # 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所以设备树里写的 flags0 或 1被直接忽略了驱动只拿到 GPIO 编号。CS 有效选通从设备SPI_CS_HIGH0 时 → 输出 0 (低电平选中)CS 无效释放从设备SPI_CS_HIGH0 时 → 输出 1 (高电平释放)驱动行为取决于是否在设备树中加了 spi-cs-high 属性DTS 属性spi‑modeCS 选通电平不写 spi‑cs‑highSPI_CS_HIGH0CS0 选通写 spi‑cs‑highSPI_CS_HIGH1CS1 选通所以cs-gpio 写 0 还是 1 实际运行效果完全一样。 按惯例写 0 即可即cs-gpio gpc1 2 0读总线核心代码选择走哪种片选方式不设置默认低电平有效选中设备 拉低引脚设置 SPI_CS_HIGH高电平有效选中设备 拉高引脚通过翻转 enable 变量实现极性适配所有 CS 控制都走这一个入口保证极性全局统一我们选的SPI2 的 CS 走GPC1_2GPIO所以 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 代表 CS0reg 1 代表 CS1。从设备通信速率最大 10MHz 。所以写spi-max-frequency 10000000;驱动会在从设备子节点下查找名为 controller-data 的子节点找不到就直接返回错误SPI 设备初始化直接失败在spi-bus.txt中文档属性DTScontroller-data{}里的写法含义cs-gpiocs-gpio gpc1 2 0;指定 SPI 控制器用来做片选的 GPIO 引脚由控制器驱动软件控制电平samsung,spi-feedback-delaysamsung,spi-feedback-delay 0;MISO 采样相位偏移补偿走线信号延迟取值 0~3 对应 0/90/180/270 度它会检查cs-gpio只要存在这个属性就启用 cs-gpio 功能不存在就禁用整个功能。寄存器地址CNF30x28CNF20x29CNF10x2ACANINTE0x2BCANINTF0x2CCANSTAT0x0ECANCTRL0x0FREAD/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 同步传输。实验现象回环测试